├── .github └── ISSUE_TEMPLATE │ ├── bug.md │ ├── feature-request.md │ └── question.md ├── .gitignore ├── .vscode └── launch.json ├── Dockerfile.chummy-builder ├── LICENSE ├── README.md ├── SECURITY.md ├── buildspec.yml ├── chummy.code-workspace └── extension ├── .babelrc ├── .commitlintrc.js ├── .eslintignore ├── .eslintrc ├── .firebaserc ├── .graphqlconfig.yml ├── .prettierignore ├── .prettierrc ├── .version ├── .versionrc.js ├── .vscode └── settings.json ├── CHANGELOG.md ├── amplify ├── .config │ └── project-config.json ├── backend │ ├── api │ │ ├── chummy │ │ │ ├── build │ │ │ │ ├── cloudformation-template.json │ │ │ │ ├── parameters.json │ │ │ │ ├── pipelineFunctions │ │ │ │ │ ├── InvokeChummyCheckoutTriggerLambdaDataSource.req.vtl │ │ │ │ │ └── InvokeChummyCheckoutTriggerLambdaDataSource.res.vtl │ │ │ │ ├── resolvers │ │ │ │ │ ├── Mutation.cancelSubscription.req.vtl │ │ │ │ │ ├── Mutation.cancelSubscription.res.vtl │ │ │ │ │ ├── Mutation.createBookmark.req.vtl │ │ │ │ │ ├── Mutation.createBookmark.res.vtl │ │ │ │ │ ├── Mutation.createUser.req.vtl │ │ │ │ │ ├── Mutation.createUser.res.vtl │ │ │ │ │ ├── Mutation.deleteBookmark.req.vtl │ │ │ │ │ ├── Mutation.deleteBookmark.res.vtl │ │ │ │ │ ├── Mutation.deleteUser.req.vtl │ │ │ │ │ ├── Mutation.deleteUser.res.vtl │ │ │ │ │ ├── Mutation.syncUser.req.vtl │ │ │ │ │ ├── Mutation.syncUser.res.vtl │ │ │ │ │ ├── Mutation.updateBookmark.req.vtl │ │ │ │ │ ├── Mutation.updateBookmark.res.vtl │ │ │ │ │ ├── Mutation.updateUser.req.vtl │ │ │ │ │ ├── Mutation.updateUser.res.vtl │ │ │ │ │ ├── Query.getBookmark.req.vtl │ │ │ │ │ ├── Query.getBookmark.res.vtl │ │ │ │ │ ├── Query.getUser.req.vtl │ │ │ │ │ ├── Query.getUser.res.vtl │ │ │ │ │ ├── Query.listBookmarks.req.vtl │ │ │ │ │ ├── Query.listBookmarks.res.vtl │ │ │ │ │ ├── Query.listUsers.req.vtl │ │ │ │ │ ├── Query.listUsers.res.vtl │ │ │ │ │ ├── Subscription.onCreateBookmark.req.vtl │ │ │ │ │ ├── Subscription.onCreateBookmark.res.vtl │ │ │ │ │ ├── Subscription.onCreateUser.req.vtl │ │ │ │ │ ├── Subscription.onCreateUser.res.vtl │ │ │ │ │ ├── Subscription.onDeleteBookmark.req.vtl │ │ │ │ │ ├── Subscription.onDeleteBookmark.res.vtl │ │ │ │ │ ├── Subscription.onDeleteUser.req.vtl │ │ │ │ │ ├── Subscription.onDeleteUser.res.vtl │ │ │ │ │ ├── Subscription.onUpdateBookmark.req.vtl │ │ │ │ │ ├── Subscription.onUpdateBookmark.res.vtl │ │ │ │ │ ├── Subscription.onUpdateUser.req.vtl │ │ │ │ │ ├── Subscription.onUpdateUser.res.vtl │ │ │ │ │ ├── User.bookmarks.req.vtl │ │ │ │ │ └── User.bookmarks.res.vtl │ │ │ │ ├── schema.graphql │ │ │ │ └── stacks │ │ │ │ │ ├── Bookmark.json │ │ │ │ │ ├── ConnectionStack.json │ │ │ │ │ ├── CustomResources.json │ │ │ │ │ ├── FunctionDirectiveStack.json │ │ │ │ │ └── User.json │ │ │ ├── parameters.json │ │ │ ├── schema.graphql │ │ │ ├── stacks │ │ │ │ └── CustomResources.json │ │ │ └── transform.conf.json │ │ └── chummyRestApi │ │ │ ├── api-params.json │ │ │ ├── chummyRestApi-cloudformation-template.json │ │ │ └── parameters.json │ ├── auth │ │ └── chummy9948bcd5 │ │ │ ├── chummy9948bcd5-cloudformation-template.yml │ │ │ └── parameters.json │ ├── backend-config.json │ ├── function │ │ ├── chummyLambdaLayer │ │ │ ├── chummyLambdaLayer-awscloudformation-template.json │ │ │ ├── layer-runtimes.json │ │ │ ├── lib │ │ │ │ └── nodejs │ │ │ │ │ ├── package-lock.json │ │ │ │ │ └── package.json │ │ │ └── parameters.json │ │ ├── chummyPostAuthTrigger │ │ │ ├── amplify.state │ │ │ ├── chummyPostAuthTrigger-cloudformation-template.json │ │ │ ├── function-parameters.json │ │ │ ├── parameters.json │ │ │ └── src │ │ │ │ ├── event.json │ │ │ │ ├── index.js │ │ │ │ ├── package-lock.json │ │ │ │ ├── package.json │ │ │ │ ├── queries.js │ │ │ │ └── util.js │ │ └── chummyServer │ │ │ ├── amplify.state │ │ │ ├── chummyServer-cloudformation-template.json │ │ │ ├── function-parameters.json │ │ │ ├── parameters.json │ │ │ └── src │ │ │ ├── app.js │ │ │ ├── event.json │ │ │ ├── index.js │ │ │ ├── package-lock.json │ │ │ ├── package.json │ │ │ ├── queries.js │ │ │ └── util.js │ └── tags.json ├── cli.json └── team-provider-info.json ├── cypress.json ├── cypress ├── fixtures │ ├── bookmarks.json │ ├── repositories.json │ ├── store.json │ └── user.json ├── integration │ └── pages.spec.js ├── mock │ └── api.js ├── plugins │ └── index.js ├── support │ ├── commands.js │ └── index.js └── utils │ ├── http.js │ └── index.js ├── firebase.json ├── manifest-base.json ├── package.json ├── public ├── icon │ ├── chummy128.png │ ├── chummy16.png │ ├── chummy256.png │ ├── chummy32.png │ ├── chummy512.png │ └── chummy64.png ├── index.html └── social │ └── social1200_628.png ├── scripts ├── BACKUP.prepublish.sh ├── download.sh ├── localpublish.sh ├── promote.dev.sh ├── promote.gamma.sh ├── promote.prod.sh └── publish.sh ├── src ├── background │ ├── app.js │ ├── constants.ts │ ├── dao.js │ ├── index.html │ ├── index.js │ ├── redirect.inject.js │ ├── signin.inject.js │ ├── storage.js │ ├── style.inject.js │ ├── throttling.util.js │ ├── url-parser.util.js │ └── util.js ├── components │ ├── Buttons │ │ ├── IconAndTextButton.js │ │ ├── IconButton.js │ │ └── TextButton.js │ ├── Containers │ │ ├── Center.js │ │ └── ExtensionRootContainer.js │ ├── ExternalLink.js │ ├── Form │ │ ├── Input.js │ │ ├── Select.js │ │ └── index.js │ ├── Icon │ │ ├── IconWithBadge.js │ │ ├── IconWithSubIcon.js │ │ ├── Icons.js │ │ └── index.js │ ├── Image │ │ ├── CircleImage.js │ │ └── index.js │ ├── Loading │ │ ├── Spinner.js │ │ └── SplashSpinner.js │ ├── Node │ │ ├── Base.style.js │ │ ├── Bookmark.js │ │ ├── BookmarkRepoNode.js │ │ ├── File.js │ │ ├── Folder.js │ │ ├── RepoNode.js │ │ ├── SearchResultFileNode.js │ │ ├── SearchResultMatchNode.js │ │ ├── Tab.js │ │ ├── TreeOrBlobNode.js │ │ └── util.js │ ├── OpenCloseChevron.js │ ├── Panel │ │ ├── index.js │ │ └── style.js │ ├── ResizableSidebar │ │ ├── ActionButtons │ │ │ ├── Collapse.js │ │ │ ├── DistractionFreeMode.js │ │ │ └── util.js │ │ ├── index.js │ │ ├── style.js │ │ └── util.js │ ├── Scrollbars.js │ ├── Section │ │ ├── SplitSections.js │ │ └── index.js │ ├── Text │ │ └── index.js │ └── Toast │ │ ├── Container.js │ │ └── Toast.js ├── config │ ├── browser.js │ ├── dao │ │ ├── index.js │ │ └── queries.js │ ├── log.js │ ├── octokit │ │ ├── index.js │ │ └── queries.js │ ├── routes.js │ ├── store │ │ ├── I.file.store.ts │ │ ├── I.root.store.ts │ │ ├── I.ui.store.ts │ │ ├── I.user.store.ts │ │ ├── constants.ts │ │ ├── context.ts │ │ ├── file.store.ts │ │ ├── root.store.ts │ │ ├── ui.store.ts │ │ ├── user.store.ts │ │ └── util.ts │ └── theme │ │ ├── context.js │ │ ├── fonts.css │ │ ├── global.style.js │ │ ├── selector.js │ │ ├── themes.js │ │ └── utils.js ├── constants │ ├── colors.js │ ├── languages.js │ ├── sizes.js │ └── theme.js ├── content-scripts │ ├── index.js │ └── util.js ├── global │ ├── constants.ts │ ├── errors │ │ ├── index.js │ │ ├── throttling.error.js │ │ ├── user.error.js │ │ └── window.error.js │ └── limits │ │ ├── constants.ts │ │ ├── features.js │ │ └── operations.js ├── graphql │ ├── mutations.js │ ├── queries.js │ └── subscriptions.js ├── hooks │ ├── dao.js │ ├── getFolderFiles.js │ ├── octokit.js │ ├── store.js │ ├── useBookmarkState.js │ ├── useDebounce.js │ ├── useDimension.js │ ├── usePrevious.js │ ├── useTheme.js │ └── useWindowSize.js ├── options │ ├── index.html │ └── index.js ├── pages │ ├── Account │ │ ├── SignIn.js │ │ ├── Signin.style.js │ │ └── index.js │ ├── App │ │ └── index.js │ ├── Bookmarks │ │ ├── index.js │ │ └── util.js │ ├── Notifications │ │ └── index.js │ ├── Search │ │ ├── LanguagesSelect.js │ │ └── index.js │ ├── Settings │ │ ├── index.js │ │ └── options.js │ ├── Tree │ │ ├── Files.js │ │ ├── OpenTabs.js │ │ └── index.js │ └── Vcs │ │ └── index.js ├── popup │ ├── index.html │ └── index.js ├── themes │ ├── dracula.dark.json │ ├── janestheme.light.json │ ├── material.dark.json │ ├── material.light.json │ ├── monokai.dark.json │ ├── onedark.dark.json │ ├── vanilla.dark.json │ └── vanilla.light.json └── utils │ ├── bookmark.js │ ├── browser.js │ ├── index.js │ ├── repository.js │ ├── tabs.js │ ├── throttling.js │ └── user.js ├── tsconfig.json ├── webpack ├── BACKUP.prod.web.config.js ├── dev.base.config.js ├── dev.moz.config.js ├── dev.web.config.js ├── gamma.moz.config.js ├── gamma.web.cloud.config.js ├── gamma.web.config.js ├── prod.base.config.js ├── prod.chrome.config.js ├── prod.edge.config.js ├── prod.moz.config.js ├── prod.opera.config.js ├── prod.web.config.js └── util.js └── yarn.lock /.github/ISSUE_TEMPLATE/bug.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug 3 | about: "I spy a bug \U0001F41E" 4 | title: '[BUG] ' 5 | labels: bug-report 6 | assignees: '' 7 | --- 8 | 9 | ### Version Information 10 | 11 | | Software | Information | 12 | | ----------------- | --------------------- | 13 | | Browser | (e.g. Chrome, Safari) | 14 | | Browser Version | (e.g. 22) | 15 | | Extension Version | (e.g. 0.1.0) | 16 | 17 | ### What is the expected behavior? 18 | 19 | ### What is the actual behavior? 20 | 21 | ### Screenshot or animated gif 22 | 23 | You may want to **include screenshots and animated GIFs** which help you demonstrate the steps or point out the part which the suggestion is related to. You can use [this tool](https://www.cockos.com/licecap/) to record GIFs on macOS and Windows, and [this tool](https://github.com/colinkeenan/silentcast) or [this tool](https://github.com/GNOME/byzanz) on Linux. 24 | 25 | ### Other notes on how to reproduce the issue? 26 | 27 | ### Any possible solutions? 28 | 29 | ### If the bug is confirmed, would you be willing to submit a PR? 30 | 31 | Yes / No _(Help can be provided if you need assistance submitting a PR)_ 32 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature-request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature Request 3 | about: "I have an idea \U0001F4A1" 4 | title: '[FEAT] ' 5 | labels: feature-request 6 | assignees: '' 7 | --- 8 | 9 | ### Version Information 10 | 11 | | Software | Information | 12 | | ----------------- | --------------------- | 13 | | Browser | (e.g. Chrome, Safari) | 14 | | Browser Version | (e.g. 22) | 15 | | Extension Version | (e.g. 0.1.0) | 16 | 17 | ### Context 18 | 19 | What are you trying to do and how would you want to do it differently? Is it something you currently you cannot do? Is this related to an issue/problem? 20 | 21 | ### Screenshot or animated gif 22 | 23 | You may want to **include screenshots and animated GIFs** which help you demonstrate the steps or point out the part which the suggestion is related to. You can use [this tool](https://www.cockos.com/licecap/) to record GIFs on macOS and Windows, and [this tool](https://github.com/colinkeenan/silentcast) or [this tool](https://github.com/GNOME/byzanz) on Linux. 24 | 25 | ### Alternatives 26 | 27 | Can you achieve the same result doing it in an alternative way? Is the alternative considerable? 28 | 29 | ### Has the feature been requested before? 30 | 31 | Please provide a link to the issue. 32 | 33 | ### If the feature request is approved, would you be willing to submit a PR? 34 | 35 | Yes / No _(Help can be provided if you need assistance submitting a PR)_ 36 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/question.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Question 3 | about: I need some clarification ❓ 4 | title: '[Q] ' 5 | labels: question 6 | assignees: '' 7 | --- 8 | 9 | **Before submitting this issue, make sure you've read over the [I Have a Question](https://github.com/AtomicCodeLabs/chummy/blob/master/docs/CONTRIBUTING.md#i-have-a-question) section of the Contributing guidelines.** 10 | 11 | ### Version Information 12 | 13 | | Software | Information | 14 | | ----------------- | --------------------- | 15 | | Browser | (e.g. Chrome, Safari) | 16 | | Browser Version | (e.g. 22) | 17 | | Extension Version | (e.g. 0.1.0) | 18 | 19 | ### Alternatives 20 | 21 | Can you achieve the same result doing it in an alternative way? Is the alternative considerable? 22 | 23 | ### Has this question been asked before? 24 | 25 | Please provide a link to the issue. 26 | 27 | ### Screenshot or animated gif 28 | 29 | You may want to **include screenshots and animated GIFs** which help you demonstrate the steps or point out the part which the suggestion is related to. You can use [this tool](https://www.cockos.com/licecap/) to record GIFs on macOS and Windows, and [this tool](https://github.com/colinkeenan/silentcast) or [this tool](https://github.com/GNOME/byzanz) on Linux. 30 | 31 | ### If the feature request is approved, would you be willing to submit a PR? 32 | 33 | Yes / No _(Help can be provided if you need assistance submitting a PR)_ 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *node_modules* 2 | *.env* 3 | *.pem* 4 | ci/ 5 | 6 | # Extension 7 | extension/dist 8 | extension/dist.prod 9 | extension/webpack/reports/**/* 10 | extension/cypress/screenshots 11 | extension/cypress/videos 12 | ## Amplify 13 | extension/amplify-backup 14 | extension/amplify/\#current-cloud-backend 15 | extension/amplify/.config/local-* 16 | extension/amplify/logs 17 | extension/amplify/mock-data 18 | extension/amplify/backend/amplify-meta.json 19 | extension/amplify/backend/awscloudformation 20 | extension/amplify/backend/.temp 21 | extension/src/aws-exports.js 22 | **/awsconfiguration.json 23 | **/amplifyconfiguration.json 24 | **/amplify-build-config.json 25 | **/amplify-gradle-config.json 26 | **/amplifytools.xcconfig 27 | **/.secret-* 28 | 29 | # Website 30 | website/public 31 | website/.cache 32 | 33 | # Mac 34 | .DS_Store 35 | .AppleDouble 36 | .LSOverride 37 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "Launch Dev Server", 9 | "request": "launch", 10 | "runtimeExecutable": "yarn", 11 | "cwd": "${workspaceFolder}/extension", 12 | "runtimeArgs": ["dev:web"], 13 | "type": "pwa-node", 14 | "resolveSourceMapLocations": [ 15 | "${workspaceFolder}/**", 16 | "!**/node_modules/**" 17 | ] 18 | }, 19 | { 20 | "name": "Build", 21 | "request": "launch", 22 | "runtimeExecutable": "yarn", 23 | "cwd": "${workspaceFolder}/extension", 24 | "runtimeArgs": ["build:web"], 25 | "type": "pwa-node", 26 | "resolveSourceMapLocations": [ 27 | "${workspaceFolder}/**", 28 | "!**/node_modules/**" 29 | ] 30 | }, 31 | { 32 | "name": "Launch Chrome", 33 | "type": "chrome", 34 | "request": "launch", 35 | "port": 9222, 36 | "url": "https://github.com/AtomicCodeLabs/chummy", 37 | "webRoot": "${workspaceFolder}/extension/dist/dev" 38 | }, 39 | { 40 | "name": "Attach Chrome", 41 | "type": "chrome", 42 | "request": "attach", 43 | "port": 9222, 44 | "url": "chrome-extension://dagfciopbcdjofghegmkodmhicjpdgbf", 45 | "webRoot": "${workspaceFolder}/extension" 46 | } 47 | ] 48 | } 49 | -------------------------------------------------------------------------------- /Dockerfile.chummy-builder: -------------------------------------------------------------------------------- 1 | FROM node:12.19.0 2 | LABEL AUTHOR=alexkim205 3 | 4 | # Install other prerequisites 5 | RUN apt-get update && \ 6 | apt-get install -y \ 7 | zip \ 8 | unzip 9 | 10 | # Install AWS CLI 11 | RUN apt-get install -y \ 12 | python3 \ 13 | python3-pip \ 14 | python3-setuptools \ 15 | groff \ 16 | less \ 17 | && pip3 install --upgrade pip \ 18 | && apt-get clean 19 | 20 | RUN pip3 --no-cache-dir install --upgrade awscli 21 | 22 | # Install Amplify CLI @latest 23 | RUN npm install -g @aws-amplify/cli 24 | 25 | # Install Cypress prerequisites 26 | RUN apt-get update && \ 27 | apt-get install -y \ 28 | libgtk2.0-0 \ 29 | libgtk-3-0 \ 30 | libgbm-dev \ 31 | libnotify-dev \ 32 | libgconf-2-4 \ 33 | libnss3 \ 34 | libxss1 \ 35 | libasound2 \ 36 | libxtst6 \ 37 | xauth \ 38 | xvfb \ 39 | firefox-esr 40 | 41 | USER root -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Reporting a Vulnerability 4 | 5 | Please email alexatatomiccode@gmail.com if you find any security vulnerabilities. 6 | -------------------------------------------------------------------------------- /buildspec.yml: -------------------------------------------------------------------------------- 1 | version: 0.2 2 | 3 | env: 4 | variables: 5 | VERSION: "1.1.2" 6 | WEBSITE_SIGNIN: "signin/" 7 | WEBSITE_REDIRECT: "account/" 8 | parameter-store: 9 | ROOT_ACCESS_KEY_ID: "ROOT_ACCESS_KEY_ID" 10 | ROOT_SECRET_ACCESS_KEY: "ROOT_SECRET_ACCESS_KEY" 11 | 12 | phases: 13 | install: 14 | commands: 15 | - cd extension 16 | - REACTCONFIG="{\"SourceDir\":\"src\",\"DistributionDir\":\"dist\",\"BuildCommand\":\"yarn build:web\",\"StartCommand\":\"yarn dev\"}" 17 | - AWSCLOUDFORMATIONCONFIG="{\"configLevel\":\"project\",\"useProfile\":false,\"profileName\":\"default\",\"accessKeyId\":\"$ROOT_ACCESS_KEY_ID\",\"secretAccessKey\":\"$ROOT_SECRET_ACCESS_KEY\",\"region\":\"$AWS_DEFAULT_REGION\"}" 18 | - AMPLIFY="{\"projectName\":\"chummy\",\"appId\":\"d37x99ddsw850\",\"envName\":\"$STAGE\",\"defaultEditor\":\"code\"}" 19 | - FRONTEND="{\"frontend\":\"javascript\",\"framework\":\"react\",\"config\":$REACTCONFIG}" 20 | - PROVIDERS="{\"awscloudformation\":$AWSCLOUDFORMATIONCONFIG}" 21 | - amplify init --amplify $AMPLIFY --frontend $FRONTEND --providers $PROVIDERS --yes 22 | - echo $CHUMMY_KEY_PEM > ./key.pem 23 | - yarn install --frozen-lockfile 24 | pre_build: 25 | commands: 26 | - yarn lint:check 27 | - yarn format:check 28 | build: 29 | commands: 30 | - yarn build:moz 31 | - yarn build:chrome 32 | - yarn build:edge 33 | - yarn build:opera 34 | post_build: 35 | commands: 36 | - yarn cy:run:moz 37 | - yarn cy:run:chrome 38 | - yarn ci:publish 39 | 40 | artifacts: 41 | base-directory: "extension/dist" 42 | files: 43 | - "**/*" 44 | 45 | cache: 46 | paths: 47 | - "~/.cache" 48 | -------------------------------------------------------------------------------- /chummy.code-workspace: -------------------------------------------------------------------------------- 1 | { 2 | "folders": [ 3 | { 4 | "name": "Extension", 5 | "path": "." 6 | }, 7 | { 8 | "name": "Website", 9 | "path": "../chummy-web" 10 | }, 11 | { 12 | "name": "Docs", 13 | "path": "../chummy-docs" 14 | }, 15 | { 16 | "name": "CI", 17 | "path": "../chummy-ci" 18 | }, 19 | { 20 | "name": "GitHub Cognito Shim", 21 | "path": "../github-cognito-openid-wrapper" 22 | } 23 | ], 24 | "settings": {}, 25 | "launch": { 26 | "configurations": [], 27 | "compounds": [ 28 | { 29 | "name": "Dev", 30 | "configurations": [ 31 | { 32 | "folder": "Extension", 33 | "name": "Launch Dev Server" 34 | }, 35 | { 36 | "folder": "Website", 37 | "name": "Launch Dev Server" 38 | }, 39 | { 40 | "folder": "Extension", 41 | "name": "Launch Chrome" 42 | } 43 | ] 44 | } 45 | ] 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /extension/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@babel/preset-env", 5 | { 6 | "useBuiltIns": "usage", // alternative mode: "entry" 7 | "corejs": 3, // default would be 2 8 | "targets": [">0.25%", "not dead", "not ie <= 11", "not op_mini all"] 9 | // set your own target environment here (see Browserslist) 10 | } 11 | ], 12 | "@babel/preset-react" 13 | ], 14 | "plugins": [ 15 | [ 16 | "@babel/plugin-proposal-decorators", 17 | { 18 | "legacy": true 19 | } 20 | ], 21 | [ 22 | "@babel/plugin-proposal-class-properties", 23 | { 24 | "loose": true 25 | } 26 | ], 27 | "@babel/plugin-syntax-dynamic-import", 28 | "@loadable/babel-plugin" 29 | ], 30 | "env": { 31 | "production": { 32 | "plugins": [ 33 | [ 34 | "transform-react-remove-prop-types", 35 | { 36 | "mode": "wrap", 37 | "ignoreFilenames": ["node_modules"] 38 | } 39 | ], 40 | "minify-constant-folding", 41 | "tailcall-optimization" 42 | ] 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /extension/.commitlintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parserPreset: 'conventional-changelog-conventionalcommits', 3 | rules: { 4 | 'body-leading-blank': [1, 'always'], 5 | 'body-max-line-length': [2, 'always', 100], 6 | 'footer-leading-blank': [1, 'always'], 7 | 'footer-max-line-length': [2, 'always', 100], 8 | 'header-max-length': [2, 'always', 100], 9 | 'scope-case': [2, 'always', 'lower-case'], 10 | 'subject-case': [ 11 | 2, 12 | 'never', 13 | ['sentence-case', 'start-case', 'pascal-case', 'upper-case'] 14 | ], 15 | 'subject-empty': [2, 'never'], 16 | 'subject-full-stop': [2, 'never', '.'], 17 | 'type-case': [2, 'always', 'lower-case'], 18 | 'type-empty': [2, 'never'], 19 | 'type-enum': [ 20 | 2, 21 | 'always', 22 | [ 23 | 'build', 24 | 'chore', 25 | 'ci', 26 | 'docs', 27 | 'feat', 28 | 'fix', 29 | 'perf', 30 | 'refactor', 31 | 'revert', 32 | 'style', 33 | 'test' 34 | ] 35 | ] 36 | } 37 | }; 38 | -------------------------------------------------------------------------------- /extension/.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules/* 2 | dist/* 3 | dist.prod/* 4 | .vscode/* 5 | github-cognito-openid-wrapper/* 6 | amplify/* 7 | src/aws-exports.js -------------------------------------------------------------------------------- /extension/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "env": { 4 | "browser": true, 5 | "jest": true, 6 | "es6": true, 7 | "cypress/globals": true 8 | }, 9 | "plugins": ["import", "cypress"], 10 | "extends": ["airbnb", "prettier", "prettier/react"], 11 | "parserOptions": { 12 | "ecmaVersion": 2018, 13 | "sourceType": "module" 14 | }, 15 | "parser": "babel-eslint", 16 | "rules": { 17 | "import/no-extraneous-dependencies": ["error", { "devDependencies": true }], 18 | "consistent-return": "off", 19 | "react/jsx-filename-extension": [1, { "extensions": [".js", ".jsx"] }], 20 | "no-console": "off", 21 | "no-eval": "error", 22 | "import/first": "error", 23 | "no-param-reassign": "off", 24 | "import/extensions": [ 25 | "error", 26 | "ignorePackages", 27 | { 28 | "js": "never", 29 | "jsx": "never", 30 | "ts": "never", 31 | "tsx": "never" 32 | } 33 | ], 34 | "lines-between-class-members": "off" 35 | }, 36 | "settings": { 37 | "import/resolver": { 38 | "typescript": {} 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /extension/.firebaserc: -------------------------------------------------------------------------------- 1 | { 2 | "projects": { 3 | "dev": "chummy-dev-1", 4 | "prod": "chummy-prod-1" 5 | } 6 | } -------------------------------------------------------------------------------- /extension/.graphqlconfig.yml: -------------------------------------------------------------------------------- 1 | projects: 2 | chummy: 3 | schemaPath: amplify/backend/api/chummy/build/schema.graphql 4 | includes: 5 | - src/graphql/**/*.js 6 | excludes: 7 | - ./amplify/** 8 | extensions: 9 | amplify: 10 | codeGenTarget: javascript 11 | generatedFileName: '' 12 | docsFilePath: src/graphql 13 | extensions: 14 | amplify: 15 | version: 3 16 | -------------------------------------------------------------------------------- /extension/.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules/* 2 | dist/* 3 | dist.prod/* 4 | webpack/reports/* 5 | .vscode/* 6 | github-cognito-openid-wrapper/* 7 | amplify/* 8 | src/aws-exports.js 9 | CHANGELOG.md -------------------------------------------------------------------------------- /extension/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "trailingComma": "none", 4 | "singleQuote": true, 5 | "printWidth": 80 6 | } 7 | -------------------------------------------------------------------------------- /extension/.version: -------------------------------------------------------------------------------- 1 | 1.1.2 -------------------------------------------------------------------------------- /extension/.versionrc.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const detectNewline = require('detect-newline'); 3 | 4 | const tracker = { 5 | filename: './package.json', 6 | type: 'json' 7 | }; 8 | 9 | const buildConfig = { 10 | filename: '../buildspec.yml', 11 | updater: { 12 | readVersion: (contents) => { 13 | const newLine = detectNewline(contents); 14 | return contents.match(new RegExp(`VERSION: "(.*)"${newLine}`)).pop(); 15 | }, 16 | writeVersion: (contents, version) => { 17 | const newLine = detectNewline(contents); 18 | return contents.replace( 19 | new RegExp(`VERSION: "(.*)"${newLine}`), 20 | `VERSION: "${version}"${newLine}` 21 | ); 22 | } 23 | } 24 | }; 25 | 26 | const versionFile = { 27 | filename: './.version', 28 | type: 'plain-text' 29 | }; 30 | 31 | module.exports = { 32 | bumpFiles: [tracker, buildConfig, versionFile], 33 | packageFiles: [tracker] 34 | }; 35 | -------------------------------------------------------------------------------- /extension/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.exclude": { 3 | "amplify/.config": true, 4 | "amplify/**/*-parameters.json": true, 5 | "amplify/**/amplify.state": true, 6 | "amplify/**/transform.conf.json": true, 7 | "amplify/#current-cloud-backend": true, 8 | "amplify/backend/amplify-meta.json": true, 9 | "amplify/backend/awscloudformation": true 10 | } 11 | } -------------------------------------------------------------------------------- /extension/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. 4 | 5 | ## [1.1.2](https://github.com/AtomicCodeLabs/chummy/compare/v1.1.0...v1.1.2) (2021-03-16) 6 | 7 | 8 | ### Bug Fixes 9 | 10 | * bug where toggling df mode from settings page was noop ([0ed3f8b](https://github.com/AtomicCodeLabs/chummy/commit/0ed3f8b8225f3774bd40b750c91c024e8ab05841)) 11 | 12 | ## [1.1.1](https://github.com/AtomicCodeLabs/chummy/compare/v1.1.0...v1.1.1) (2021-03-16) 13 | 14 | 15 | ### Bug Fixes 16 | 17 | * bug where toggling df mode from settings page was noop ([0ed3f8b](https://github.com/AtomicCodeLabs/chummy/commit/0ed3f8b8225f3774bd40b750c91c024e8ab05841)) 18 | 19 | ## [1.1.0](https://github.com/AtomicCodeLabs/chummy/compare/v1.0.0...v1.1.0) (2021-03-14) 20 | 21 | 22 | ### Features 23 | 24 | * add janes theme and dracula ([77a9823](https://github.com/AtomicCodeLabs/chummy/commit/77a9823b381cb7923f80a05ede3e2278e15d3548)) 25 | * folder collapsing and distraction free mode ([faf9e44](https://github.com/AtomicCodeLabs/chummy/commit/faf9e44ae7b4f08ae127488df00c275a2a99f7a8)) 26 | 27 | 28 | ### Bug Fixes 29 | 30 | * fix a few active tab change issues ([3e96922](https://github.com/AtomicCodeLabs/chummy/commit/3e96922aed861bd19d0bf84404d23192ccb98593)) 31 | * fix url comparison error that was causing auth bug in prod ([871ef9a](https://github.com/AtomicCodeLabs/chummy/commit/871ef9a8e4ba9792d334aabbf52ec9315641e455)) 32 | * user account type updates when subscription changes ([c6177f5](https://github.com/AtomicCodeLabs/chummy/commit/c6177f501bd210d1d9c193e432d6d85b6be61273)) 33 | 34 | ## 1.0.0 (2021-02-25) 35 | 36 | The very first iteration of Chummy. 37 | -------------------------------------------------------------------------------- /extension/amplify/.config/project-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "projectName": "chummy", 3 | "version": "3.0", 4 | "frontend": "javascript", 5 | "javascript": { 6 | "framework": "react", 7 | "config": { 8 | "SourceDir": "src", 9 | "DistributionDir": "dist", 10 | "BuildCommand": "yarn build:web", 11 | "StartCommand": "yarn dev" 12 | } 13 | }, 14 | "providers": [ 15 | "awscloudformation" 16 | ] 17 | } -------------------------------------------------------------------------------- /extension/amplify/backend/api/chummy/build/parameters.json: -------------------------------------------------------------------------------- 1 | { 2 | "CreateAPIKey": 1, 3 | "AppSyncApiName": "chummy", 4 | "DynamoDBBillingMode": "PAY_PER_REQUEST", 5 | "DynamoDBEnableServerSideEncryption": false, 6 | "AuthCognitoUserPoolId": { 7 | "Fn::GetAtt": [ 8 | "authchummy9948bcd5", 9 | "Outputs.UserPoolId" 10 | ] 11 | }, 12 | "S3DeploymentBucket": "amplify-chummy-dev-213651-deployment", 13 | "S3DeploymentRootKey": "amplify-appsync-files/c835c2d6f952278a277e295d225c89fdab213b79" 14 | } -------------------------------------------------------------------------------- /extension/amplify/backend/api/chummy/build/pipelineFunctions/InvokeChummyCheckoutTriggerLambdaDataSource.req.vtl: -------------------------------------------------------------------------------- 1 | ## [Start] Invoke AWS Lambda data source: ChummyCheckoutTriggerLambdaDataSource. ** 2 | { 3 | "version": "2018-05-29", 4 | "operation": "Invoke", 5 | "payload": { 6 | "typeName": "$ctx.stash.get("typeName")", 7 | "fieldName": "$ctx.stash.get("fieldName")", 8 | "arguments": $util.toJson($ctx.arguments), 9 | "identity": $util.toJson($ctx.identity), 10 | "source": $util.toJson($ctx.source), 11 | "request": $util.toJson($ctx.request), 12 | "prev": $util.toJson($ctx.prev) 13 | } 14 | } 15 | ## [End] Invoke AWS Lambda data source: ChummyCheckoutTriggerLambdaDataSource. ** -------------------------------------------------------------------------------- /extension/amplify/backend/api/chummy/build/pipelineFunctions/InvokeChummyCheckoutTriggerLambdaDataSource.res.vtl: -------------------------------------------------------------------------------- 1 | ## [Start] Handle error or return result. ** 2 | #if( $ctx.error ) 3 | $util.error($ctx.error.message, $ctx.error.type) 4 | #end 5 | $util.toJson($ctx.result) 6 | ## [End] Handle error or return result. ** -------------------------------------------------------------------------------- /extension/amplify/backend/api/chummy/build/resolvers/Mutation.cancelSubscription.req.vtl: -------------------------------------------------------------------------------- 1 | ## [Start] Stash resolver specific context.. ** 2 | $util.qr($ctx.stash.put("typeName", "Mutation")) 3 | $util.qr($ctx.stash.put("fieldName", "cancelSubscription")) 4 | {} 5 | ## [End] Stash resolver specific context.. ** -------------------------------------------------------------------------------- /extension/amplify/backend/api/chummy/build/resolvers/Mutation.cancelSubscription.res.vtl: -------------------------------------------------------------------------------- 1 | $util.toJson($ctx.prev.result) -------------------------------------------------------------------------------- /extension/amplify/backend/api/chummy/build/resolvers/Mutation.createBookmark.res.vtl: -------------------------------------------------------------------------------- 1 | #if( $ctx.error ) 2 | $util.error($ctx.error.message, $ctx.error.type) 3 | #else 4 | $util.toJson($ctx.result) 5 | #end -------------------------------------------------------------------------------- /extension/amplify/backend/api/chummy/build/resolvers/Mutation.createUser.res.vtl: -------------------------------------------------------------------------------- 1 | #if( $ctx.error ) 2 | $util.error($ctx.error.message, $ctx.error.type) 3 | #else 4 | $util.toJson($ctx.result) 5 | #end -------------------------------------------------------------------------------- /extension/amplify/backend/api/chummy/build/resolvers/Mutation.deleteBookmark.res.vtl: -------------------------------------------------------------------------------- 1 | #if( $ctx.error ) 2 | $util.error($ctx.error.message, $ctx.error.type) 3 | #else 4 | $util.toJson($ctx.result) 5 | #end -------------------------------------------------------------------------------- /extension/amplify/backend/api/chummy/build/resolvers/Mutation.deleteUser.res.vtl: -------------------------------------------------------------------------------- 1 | #if( $ctx.error ) 2 | $util.error($ctx.error.message, $ctx.error.type) 3 | #else 4 | $util.toJson($ctx.result) 5 | #end -------------------------------------------------------------------------------- /extension/amplify/backend/api/chummy/build/resolvers/Mutation.syncUser.req.vtl: -------------------------------------------------------------------------------- 1 | ## [Start] Stash resolver specific context.. ** 2 | $util.qr($ctx.stash.put("typeName", "Mutation")) 3 | $util.qr($ctx.stash.put("fieldName", "syncUser")) 4 | {} 5 | ## [End] Stash resolver specific context.. ** -------------------------------------------------------------------------------- /extension/amplify/backend/api/chummy/build/resolvers/Mutation.syncUser.res.vtl: -------------------------------------------------------------------------------- 1 | $util.toJson($ctx.prev.result) -------------------------------------------------------------------------------- /extension/amplify/backend/api/chummy/build/resolvers/Mutation.updateBookmark.res.vtl: -------------------------------------------------------------------------------- 1 | #if( $ctx.error ) 2 | $util.error($ctx.error.message, $ctx.error.type) 3 | #else 4 | $util.toJson($ctx.result) 5 | #end -------------------------------------------------------------------------------- /extension/amplify/backend/api/chummy/build/resolvers/Mutation.updateUser.res.vtl: -------------------------------------------------------------------------------- 1 | #if( $ctx.error ) 2 | $util.error($ctx.error.message, $ctx.error.type) 3 | #else 4 | $util.toJson($ctx.result) 5 | #end -------------------------------------------------------------------------------- /extension/amplify/backend/api/chummy/build/resolvers/Query.getBookmark.req.vtl: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2018-05-29", 3 | "operation": "GetItem", 4 | "key": #if( $modelObjectKey ) $util.toJson($modelObjectKey) #else { 5 | "id": $util.dynamodb.toDynamoDBJson($ctx.args.id) 6 | } #end 7 | } -------------------------------------------------------------------------------- /extension/amplify/backend/api/chummy/build/resolvers/Query.getBookmark.res.vtl: -------------------------------------------------------------------------------- 1 | #if( $util.isNullOrEmpty($ctx.result) ) 2 | #return 3 | #end 4 | ## [Start] Determine request authentication mode ** 5 | #if( $util.isNullOrEmpty($authMode) && !$util.isNull($ctx.identity) && !$util.isNull($ctx.identity.sub) && !$util.isNull($ctx.identity.issuer) && !$util.isNull($ctx.identity.username) && !$util.isNull($ctx.identity.claims) && !$util.isNull($ctx.identity.sourceIp) && !$util.isNull($ctx.identity.defaultAuthStrategy) ) 6 | #set( $authMode = "userPools" ) 7 | #end 8 | ## [End] Determine request authentication mode ** 9 | ## [Start] Check authMode and execute owner/group checks ** 10 | #if( $authMode == "userPools" ) 11 | ## No Static Group Authorization Rules ** 12 | 13 | 14 | ## No Dynamic Group Authorization Rules ** 15 | 16 | 17 | ## [Start] Owner Authorization Checks ** 18 | #set( $isOwnerAuthorized = $util.defaultIfNull($isOwnerAuthorized, false) ) 19 | ## Authorization rule: { allow: owner, ownerField: "owner", identityClaim: "cognito:username" } ** 20 | #set( $allowedOwners0 = $util.defaultIfNull($ctx.result.owner, []) ) 21 | #set( $identityValue = $util.defaultIfNull($ctx.identity.claims.get("username"), $util.defaultIfNull($ctx.identity.claims.get("cognito:username"), "___xamznone____")) ) 22 | #if( $util.isList($allowedOwners0) ) 23 | #foreach( $allowedOwner in $allowedOwners0 ) 24 | #if( $allowedOwner == $identityValue ) 25 | #set( $isOwnerAuthorized = true ) 26 | #end 27 | #end 28 | #end 29 | #if( $util.isString($allowedOwners0) ) 30 | #if( $allowedOwners0 == $identityValue ) 31 | #set( $isOwnerAuthorized = true ) 32 | #end 33 | #end 34 | ## [End] Owner Authorization Checks ** 35 | 36 | 37 | ## [Start] Throw if unauthorized ** 38 | #if( !($isStaticGroupAuthorized == true || $isDynamicGroupAuthorized == true || $isOwnerAuthorized == true) ) 39 | $util.unauthorized() 40 | #end 41 | ## [End] Throw if unauthorized ** 42 | #end 43 | ## [End] Check authMode and execute owner/group checks ** 44 | 45 | #if( $ctx.error ) 46 | $util.error($ctx.error.message, $ctx.error.type) 47 | #else 48 | $util.toJson($ctx.result) 49 | #end -------------------------------------------------------------------------------- /extension/amplify/backend/api/chummy/build/resolvers/Query.getUser.req.vtl: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2018-05-29", 3 | "operation": "GetItem", 4 | "key": #if( $modelObjectKey ) $util.toJson($modelObjectKey) #else { 5 | "id": $util.dynamodb.toDynamoDBJson($ctx.args.id) 6 | } #end 7 | } -------------------------------------------------------------------------------- /extension/amplify/backend/api/chummy/build/resolvers/Query.getUser.res.vtl: -------------------------------------------------------------------------------- 1 | #if( $util.isNullOrEmpty($ctx.result) ) 2 | #return 3 | #end 4 | ## [Start] Determine request authentication mode ** 5 | #if( $util.isNullOrEmpty($authMode) && !$util.isNull($ctx.identity) && !$util.isNull($ctx.identity.sub) && !$util.isNull($ctx.identity.issuer) && !$util.isNull($ctx.identity.username) && !$util.isNull($ctx.identity.claims) && !$util.isNull($ctx.identity.sourceIp) && !$util.isNull($ctx.identity.defaultAuthStrategy) ) 6 | #set( $authMode = "userPools" ) 7 | #end 8 | ## [End] Determine request authentication mode ** 9 | ## [Start] Check authMode and execute owner/group checks ** 10 | #if( $authMode == "userPools" ) 11 | ## No Static Group Authorization Rules ** 12 | 13 | 14 | ## No Dynamic Group Authorization Rules ** 15 | 16 | 17 | ## [Start] Owner Authorization Checks ** 18 | #set( $isOwnerAuthorized = $util.defaultIfNull($isOwnerAuthorized, false) ) 19 | ## Authorization rule: { allow: owner, ownerField: "owner", identityClaim: "cognito:username" } ** 20 | #set( $allowedOwners0 = $util.defaultIfNull($ctx.result.owner, []) ) 21 | #set( $identityValue = $util.defaultIfNull($ctx.identity.claims.get("username"), $util.defaultIfNull($ctx.identity.claims.get("cognito:username"), "___xamznone____")) ) 22 | #if( $util.isList($allowedOwners0) ) 23 | #foreach( $allowedOwner in $allowedOwners0 ) 24 | #if( $allowedOwner == $identityValue ) 25 | #set( $isOwnerAuthorized = true ) 26 | #end 27 | #end 28 | #end 29 | #if( $util.isString($allowedOwners0) ) 30 | #if( $allowedOwners0 == $identityValue ) 31 | #set( $isOwnerAuthorized = true ) 32 | #end 33 | #end 34 | ## [End] Owner Authorization Checks ** 35 | 36 | 37 | ## [Start] Throw if unauthorized ** 38 | #if( !($isStaticGroupAuthorized == true || $isDynamicGroupAuthorized == true || $isOwnerAuthorized == true) ) 39 | $util.unauthorized() 40 | #end 41 | ## [End] Throw if unauthorized ** 42 | #end 43 | ## [End] Check authMode and execute owner/group checks ** 44 | 45 | #if( $ctx.error ) 46 | $util.error($ctx.error.message, $ctx.error.type) 47 | #else 48 | $util.toJson($ctx.result) 49 | #end -------------------------------------------------------------------------------- /extension/amplify/backend/api/chummy/build/resolvers/Query.listBookmarks.req.vtl: -------------------------------------------------------------------------------- 1 | #set( $limit = $util.defaultIfNull($context.args.limit, 100) ) 2 | #set( $ListRequest = { 3 | "version": "2018-05-29", 4 | "limit": $limit 5 | } ) 6 | #if( $context.args.nextToken ) 7 | #set( $ListRequest.nextToken = $context.args.nextToken ) 8 | #end 9 | #if( $context.args.filter ) 10 | #set( $ListRequest.filter = $util.parseJson("$util.transform.toDynamoDBFilterExpression($ctx.args.filter)") ) 11 | #end 12 | #if( !$util.isNull($modelQueryExpression) 13 | && !$util.isNullOrEmpty($modelQueryExpression.expression) ) 14 | $util.qr($ListRequest.put("operation", "Query")) 15 | $util.qr($ListRequest.put("query", $modelQueryExpression)) 16 | #if( !$util.isNull($ctx.args.sortDirection) && $ctx.args.sortDirection == "DESC" ) 17 | #set( $ListRequest.scanIndexForward = false ) 18 | #else 19 | #set( $ListRequest.scanIndexForward = true ) 20 | #end 21 | #else 22 | $util.qr($ListRequest.put("operation", "Scan")) 23 | #end 24 | $util.toJson($ListRequest) -------------------------------------------------------------------------------- /extension/amplify/backend/api/chummy/build/resolvers/Query.listBookmarks.res.vtl: -------------------------------------------------------------------------------- 1 | ## [Start] Determine request authentication mode ** 2 | #if( $util.isNullOrEmpty($authMode) && !$util.isNull($ctx.identity) && !$util.isNull($ctx.identity.sub) && !$util.isNull($ctx.identity.issuer) && !$util.isNull($ctx.identity.username) && !$util.isNull($ctx.identity.claims) && !$util.isNull($ctx.identity.sourceIp) && !$util.isNull($ctx.identity.defaultAuthStrategy) ) 3 | #set( $authMode = "userPools" ) 4 | #end 5 | ## [End] Determine request authentication mode ** 6 | ## [Start] Check authMode and execute owner/group checks ** 7 | #if( $authMode == "userPools" ) 8 | ## No Static Group Authorization Rules ** 9 | 10 | 11 | ## [Start] If not static group authorized, filter items ** 12 | #if( !$isStaticGroupAuthorized ) 13 | #set( $items = [] ) 14 | #foreach( $item in $ctx.result.items ) 15 | ## No Dynamic Group Authorization Rules ** 16 | 17 | 18 | ## [Start] Owner Authorization Checks ** 19 | #set( $isLocalOwnerAuthorized = false ) 20 | ## Authorization rule: { allow: owner, ownerField: "owner", identityClaim: "cognito:username" } ** 21 | #set( $allowedOwners0 = $util.defaultIfNull($item.owner, []) ) 22 | #set( $identityValue = $util.defaultIfNull($ctx.identity.claims.get("username"), $util.defaultIfNull($ctx.identity.claims.get("cognito:username"), "___xamznone____")) ) 23 | #if( $util.isList($allowedOwners0) ) 24 | #foreach( $allowedOwner in $allowedOwners0 ) 25 | #if( $allowedOwner == $identityValue ) 26 | #set( $isLocalOwnerAuthorized = true ) 27 | #end 28 | #end 29 | #end 30 | #if( $util.isString($allowedOwners0) ) 31 | #if( $allowedOwners0 == $identityValue ) 32 | #set( $isLocalOwnerAuthorized = true ) 33 | #end 34 | #end 35 | ## [End] Owner Authorization Checks ** 36 | 37 | 38 | #if( ($isLocalDynamicGroupAuthorized == true || $isLocalOwnerAuthorized == true) ) 39 | $util.qr($items.add($item)) 40 | #end 41 | #end 42 | #set( $ctx.result.items = $items ) 43 | #end 44 | ## [End] If not static group authorized, filter items ** 45 | #end 46 | ## [End] Check authMode and execute owner/group checks ** 47 | 48 | #if( $ctx.error ) 49 | $util.error($ctx.error.message, $ctx.error.type) 50 | #else 51 | $util.toJson($ctx.result) 52 | #end -------------------------------------------------------------------------------- /extension/amplify/backend/api/chummy/build/resolvers/Query.listUsers.req.vtl: -------------------------------------------------------------------------------- 1 | #set( $limit = $util.defaultIfNull($context.args.limit, 100) ) 2 | #set( $ListRequest = { 3 | "version": "2018-05-29", 4 | "limit": $limit 5 | } ) 6 | #if( $context.args.nextToken ) 7 | #set( $ListRequest.nextToken = $context.args.nextToken ) 8 | #end 9 | #if( $context.args.filter ) 10 | #set( $ListRequest.filter = $util.parseJson("$util.transform.toDynamoDBFilterExpression($ctx.args.filter)") ) 11 | #end 12 | #if( !$util.isNull($modelQueryExpression) 13 | && !$util.isNullOrEmpty($modelQueryExpression.expression) ) 14 | $util.qr($ListRequest.put("operation", "Query")) 15 | $util.qr($ListRequest.put("query", $modelQueryExpression)) 16 | #if( !$util.isNull($ctx.args.sortDirection) && $ctx.args.sortDirection == "DESC" ) 17 | #set( $ListRequest.scanIndexForward = false ) 18 | #else 19 | #set( $ListRequest.scanIndexForward = true ) 20 | #end 21 | #else 22 | $util.qr($ListRequest.put("operation", "Scan")) 23 | #end 24 | $util.toJson($ListRequest) -------------------------------------------------------------------------------- /extension/amplify/backend/api/chummy/build/resolvers/Query.listUsers.res.vtl: -------------------------------------------------------------------------------- 1 | ## [Start] Determine request authentication mode ** 2 | #if( $util.isNullOrEmpty($authMode) && !$util.isNull($ctx.identity) && !$util.isNull($ctx.identity.sub) && !$util.isNull($ctx.identity.issuer) && !$util.isNull($ctx.identity.username) && !$util.isNull($ctx.identity.claims) && !$util.isNull($ctx.identity.sourceIp) && !$util.isNull($ctx.identity.defaultAuthStrategy) ) 3 | #set( $authMode = "userPools" ) 4 | #end 5 | ## [End] Determine request authentication mode ** 6 | ## [Start] Check authMode and execute owner/group checks ** 7 | #if( $authMode == "userPools" ) 8 | ## No Static Group Authorization Rules ** 9 | 10 | 11 | ## [Start] If not static group authorized, filter items ** 12 | #if( !$isStaticGroupAuthorized ) 13 | #set( $items = [] ) 14 | #foreach( $item in $ctx.result.items ) 15 | ## No Dynamic Group Authorization Rules ** 16 | 17 | 18 | ## [Start] Owner Authorization Checks ** 19 | #set( $isLocalOwnerAuthorized = false ) 20 | ## Authorization rule: { allow: owner, ownerField: "owner", identityClaim: "cognito:username" } ** 21 | #set( $allowedOwners0 = $util.defaultIfNull($item.owner, []) ) 22 | #set( $identityValue = $util.defaultIfNull($ctx.identity.claims.get("username"), $util.defaultIfNull($ctx.identity.claims.get("cognito:username"), "___xamznone____")) ) 23 | #if( $util.isList($allowedOwners0) ) 24 | #foreach( $allowedOwner in $allowedOwners0 ) 25 | #if( $allowedOwner == $identityValue ) 26 | #set( $isLocalOwnerAuthorized = true ) 27 | #end 28 | #end 29 | #end 30 | #if( $util.isString($allowedOwners0) ) 31 | #if( $allowedOwners0 == $identityValue ) 32 | #set( $isLocalOwnerAuthorized = true ) 33 | #end 34 | #end 35 | ## [End] Owner Authorization Checks ** 36 | 37 | 38 | #if( ($isLocalDynamicGroupAuthorized == true || $isLocalOwnerAuthorized == true) ) 39 | $util.qr($items.add($item)) 40 | #end 41 | #end 42 | #set( $ctx.result.items = $items ) 43 | #end 44 | ## [End] If not static group authorized, filter items ** 45 | #end 46 | ## [End] Check authMode and execute owner/group checks ** 47 | 48 | #if( $ctx.error ) 49 | $util.error($ctx.error.message, $ctx.error.type) 50 | #else 51 | $util.toJson($ctx.result) 52 | #end -------------------------------------------------------------------------------- /extension/amplify/backend/api/chummy/build/resolvers/Subscription.onCreateBookmark.req.vtl: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2018-05-29", 3 | "payload": {} 4 | } -------------------------------------------------------------------------------- /extension/amplify/backend/api/chummy/build/resolvers/Subscription.onCreateBookmark.res.vtl: -------------------------------------------------------------------------------- 1 | ## [Start] Determine request authentication mode ** 2 | #if( $util.isNullOrEmpty($authMode) && !$util.isNull($ctx.identity) && !$util.isNull($ctx.identity.sub) && !$util.isNull($ctx.identity.issuer) && !$util.isNull($ctx.identity.username) && !$util.isNull($ctx.identity.claims) && !$util.isNull($ctx.identity.sourceIp) && !$util.isNull($ctx.identity.defaultAuthStrategy) ) 3 | #set( $authMode = "userPools" ) 4 | #end 5 | ## [End] Determine request authentication mode ** 6 | ## [Start] Check authMode and execute owner/group checks ** 7 | #if( $authMode == "userPools" ) 8 | ## No Static Group Authorization Rules ** 9 | 10 | 11 | ## [Start] Owner Authorization Checks ** 12 | #set( $isOwnerAuthorized = false ) 13 | ## Authorization rule: { allow: owner, ownerField: "owner", identityClaim: "cognito:username" } ** 14 | #set( $allowedOwners0 = $util.defaultIfNull($ctx.args.owner, null) ) 15 | #set( $identityValue = $util.defaultIfNull($ctx.identity.claims.get("username"), 16 | $util.defaultIfNull($ctx.identity.claims.get("cognito:username"), "___xamznone____")) ) 17 | #if( $util.isList($allowedOwners0) ) 18 | #foreach( $allowedOwner in $allowedOwners0 ) 19 | #if( $allowedOwner == $identityValue ) 20 | #set( $isOwnerAuthorized = true ) 21 | #end 22 | #end 23 | #end 24 | #if( $util.isString($allowedOwners0) ) 25 | #if( $allowedOwners0 == $identityValue ) 26 | #set( $isOwnerAuthorized = true ) 27 | #end 28 | #end 29 | ## [End] Owner Authorization Checks ** 30 | 31 | 32 | ## [Start] Throw if unauthorized ** 33 | #if( !($isStaticGroupAuthorized == true || $isOwnerAuthorized == true) ) 34 | $util.unauthorized() 35 | #end 36 | ## [End] Throw if unauthorized ** 37 | #end 38 | ## [End] Check authMode and execute owner/group checks ** 39 | 40 | $util.toJson(null) -------------------------------------------------------------------------------- /extension/amplify/backend/api/chummy/build/resolvers/Subscription.onCreateUser.req.vtl: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2018-05-29", 3 | "payload": {} 4 | } -------------------------------------------------------------------------------- /extension/amplify/backend/api/chummy/build/resolvers/Subscription.onCreateUser.res.vtl: -------------------------------------------------------------------------------- 1 | ## [Start] Determine request authentication mode ** 2 | #if( $util.isNullOrEmpty($authMode) && !$util.isNull($ctx.identity) && !$util.isNull($ctx.identity.sub) && !$util.isNull($ctx.identity.issuer) && !$util.isNull($ctx.identity.username) && !$util.isNull($ctx.identity.claims) && !$util.isNull($ctx.identity.sourceIp) && !$util.isNull($ctx.identity.defaultAuthStrategy) ) 3 | #set( $authMode = "userPools" ) 4 | #end 5 | ## [End] Determine request authentication mode ** 6 | ## [Start] Check authMode and execute owner/group checks ** 7 | #if( $authMode == "userPools" ) 8 | ## No Static Group Authorization Rules ** 9 | 10 | 11 | ## [Start] Owner Authorization Checks ** 12 | #set( $isOwnerAuthorized = false ) 13 | ## Authorization rule: { allow: owner, ownerField: "owner", identityClaim: "cognito:username" } ** 14 | #set( $allowedOwners0 = $util.defaultIfNull($ctx.args.owner, null) ) 15 | #set( $identityValue = $util.defaultIfNull($ctx.identity.claims.get("username"), 16 | $util.defaultIfNull($ctx.identity.claims.get("cognito:username"), "___xamznone____")) ) 17 | #if( $util.isList($allowedOwners0) ) 18 | #foreach( $allowedOwner in $allowedOwners0 ) 19 | #if( $allowedOwner == $identityValue ) 20 | #set( $isOwnerAuthorized = true ) 21 | #end 22 | #end 23 | #end 24 | #if( $util.isString($allowedOwners0) ) 25 | #if( $allowedOwners0 == $identityValue ) 26 | #set( $isOwnerAuthorized = true ) 27 | #end 28 | #end 29 | ## [End] Owner Authorization Checks ** 30 | 31 | 32 | ## [Start] Throw if unauthorized ** 33 | #if( !($isStaticGroupAuthorized == true || $isOwnerAuthorized == true) ) 34 | $util.unauthorized() 35 | #end 36 | ## [End] Throw if unauthorized ** 37 | #end 38 | ## [End] Check authMode and execute owner/group checks ** 39 | 40 | $util.toJson(null) -------------------------------------------------------------------------------- /extension/amplify/backend/api/chummy/build/resolvers/Subscription.onDeleteBookmark.req.vtl: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2018-05-29", 3 | "payload": {} 4 | } -------------------------------------------------------------------------------- /extension/amplify/backend/api/chummy/build/resolvers/Subscription.onDeleteBookmark.res.vtl: -------------------------------------------------------------------------------- 1 | ## [Start] Determine request authentication mode ** 2 | #if( $util.isNullOrEmpty($authMode) && !$util.isNull($ctx.identity) && !$util.isNull($ctx.identity.sub) && !$util.isNull($ctx.identity.issuer) && !$util.isNull($ctx.identity.username) && !$util.isNull($ctx.identity.claims) && !$util.isNull($ctx.identity.sourceIp) && !$util.isNull($ctx.identity.defaultAuthStrategy) ) 3 | #set( $authMode = "userPools" ) 4 | #end 5 | ## [End] Determine request authentication mode ** 6 | ## [Start] Check authMode and execute owner/group checks ** 7 | #if( $authMode == "userPools" ) 8 | ## No Static Group Authorization Rules ** 9 | 10 | 11 | ## [Start] Owner Authorization Checks ** 12 | #set( $isOwnerAuthorized = false ) 13 | ## Authorization rule: { allow: owner, ownerField: "owner", identityClaim: "cognito:username" } ** 14 | #set( $allowedOwners0 = $util.defaultIfNull($ctx.args.owner, null) ) 15 | #set( $identityValue = $util.defaultIfNull($ctx.identity.claims.get("username"), 16 | $util.defaultIfNull($ctx.identity.claims.get("cognito:username"), "___xamznone____")) ) 17 | #if( $util.isList($allowedOwners0) ) 18 | #foreach( $allowedOwner in $allowedOwners0 ) 19 | #if( $allowedOwner == $identityValue ) 20 | #set( $isOwnerAuthorized = true ) 21 | #end 22 | #end 23 | #end 24 | #if( $util.isString($allowedOwners0) ) 25 | #if( $allowedOwners0 == $identityValue ) 26 | #set( $isOwnerAuthorized = true ) 27 | #end 28 | #end 29 | ## [End] Owner Authorization Checks ** 30 | 31 | 32 | ## [Start] Throw if unauthorized ** 33 | #if( !($isStaticGroupAuthorized == true || $isOwnerAuthorized == true) ) 34 | $util.unauthorized() 35 | #end 36 | ## [End] Throw if unauthorized ** 37 | #end 38 | ## [End] Check authMode and execute owner/group checks ** 39 | 40 | $util.toJson(null) -------------------------------------------------------------------------------- /extension/amplify/backend/api/chummy/build/resolvers/Subscription.onDeleteUser.req.vtl: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2018-05-29", 3 | "payload": {} 4 | } -------------------------------------------------------------------------------- /extension/amplify/backend/api/chummy/build/resolvers/Subscription.onDeleteUser.res.vtl: -------------------------------------------------------------------------------- 1 | ## [Start] Determine request authentication mode ** 2 | #if( $util.isNullOrEmpty($authMode) && !$util.isNull($ctx.identity) && !$util.isNull($ctx.identity.sub) && !$util.isNull($ctx.identity.issuer) && !$util.isNull($ctx.identity.username) && !$util.isNull($ctx.identity.claims) && !$util.isNull($ctx.identity.sourceIp) && !$util.isNull($ctx.identity.defaultAuthStrategy) ) 3 | #set( $authMode = "userPools" ) 4 | #end 5 | ## [End] Determine request authentication mode ** 6 | ## [Start] Check authMode and execute owner/group checks ** 7 | #if( $authMode == "userPools" ) 8 | ## No Static Group Authorization Rules ** 9 | 10 | 11 | ## [Start] Owner Authorization Checks ** 12 | #set( $isOwnerAuthorized = false ) 13 | ## Authorization rule: { allow: owner, ownerField: "owner", identityClaim: "cognito:username" } ** 14 | #set( $allowedOwners0 = $util.defaultIfNull($ctx.args.owner, null) ) 15 | #set( $identityValue = $util.defaultIfNull($ctx.identity.claims.get("username"), 16 | $util.defaultIfNull($ctx.identity.claims.get("cognito:username"), "___xamznone____")) ) 17 | #if( $util.isList($allowedOwners0) ) 18 | #foreach( $allowedOwner in $allowedOwners0 ) 19 | #if( $allowedOwner == $identityValue ) 20 | #set( $isOwnerAuthorized = true ) 21 | #end 22 | #end 23 | #end 24 | #if( $util.isString($allowedOwners0) ) 25 | #if( $allowedOwners0 == $identityValue ) 26 | #set( $isOwnerAuthorized = true ) 27 | #end 28 | #end 29 | ## [End] Owner Authorization Checks ** 30 | 31 | 32 | ## [Start] Throw if unauthorized ** 33 | #if( !($isStaticGroupAuthorized == true || $isOwnerAuthorized == true) ) 34 | $util.unauthorized() 35 | #end 36 | ## [End] Throw if unauthorized ** 37 | #end 38 | ## [End] Check authMode and execute owner/group checks ** 39 | 40 | $util.toJson(null) -------------------------------------------------------------------------------- /extension/amplify/backend/api/chummy/build/resolvers/Subscription.onUpdateBookmark.req.vtl: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2018-05-29", 3 | "payload": {} 4 | } -------------------------------------------------------------------------------- /extension/amplify/backend/api/chummy/build/resolvers/Subscription.onUpdateBookmark.res.vtl: -------------------------------------------------------------------------------- 1 | ## [Start] Determine request authentication mode ** 2 | #if( $util.isNullOrEmpty($authMode) && !$util.isNull($ctx.identity) && !$util.isNull($ctx.identity.sub) && !$util.isNull($ctx.identity.issuer) && !$util.isNull($ctx.identity.username) && !$util.isNull($ctx.identity.claims) && !$util.isNull($ctx.identity.sourceIp) && !$util.isNull($ctx.identity.defaultAuthStrategy) ) 3 | #set( $authMode = "userPools" ) 4 | #end 5 | ## [End] Determine request authentication mode ** 6 | ## [Start] Check authMode and execute owner/group checks ** 7 | #if( $authMode == "userPools" ) 8 | ## No Static Group Authorization Rules ** 9 | 10 | 11 | ## [Start] Owner Authorization Checks ** 12 | #set( $isOwnerAuthorized = false ) 13 | ## Authorization rule: { allow: owner, ownerField: "owner", identityClaim: "cognito:username" } ** 14 | #set( $allowedOwners0 = $util.defaultIfNull($ctx.args.owner, null) ) 15 | #set( $identityValue = $util.defaultIfNull($ctx.identity.claims.get("username"), 16 | $util.defaultIfNull($ctx.identity.claims.get("cognito:username"), "___xamznone____")) ) 17 | #if( $util.isList($allowedOwners0) ) 18 | #foreach( $allowedOwner in $allowedOwners0 ) 19 | #if( $allowedOwner == $identityValue ) 20 | #set( $isOwnerAuthorized = true ) 21 | #end 22 | #end 23 | #end 24 | #if( $util.isString($allowedOwners0) ) 25 | #if( $allowedOwners0 == $identityValue ) 26 | #set( $isOwnerAuthorized = true ) 27 | #end 28 | #end 29 | ## [End] Owner Authorization Checks ** 30 | 31 | 32 | ## [Start] Throw if unauthorized ** 33 | #if( !($isStaticGroupAuthorized == true || $isOwnerAuthorized == true) ) 34 | $util.unauthorized() 35 | #end 36 | ## [End] Throw if unauthorized ** 37 | #end 38 | ## [End] Check authMode and execute owner/group checks ** 39 | 40 | $util.toJson(null) -------------------------------------------------------------------------------- /extension/amplify/backend/api/chummy/build/resolvers/Subscription.onUpdateUser.req.vtl: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2018-05-29", 3 | "payload": {} 4 | } -------------------------------------------------------------------------------- /extension/amplify/backend/api/chummy/build/resolvers/Subscription.onUpdateUser.res.vtl: -------------------------------------------------------------------------------- 1 | ## [Start] Determine request authentication mode ** 2 | #if( $util.isNullOrEmpty($authMode) && !$util.isNull($ctx.identity) && !$util.isNull($ctx.identity.sub) && !$util.isNull($ctx.identity.issuer) && !$util.isNull($ctx.identity.username) && !$util.isNull($ctx.identity.claims) && !$util.isNull($ctx.identity.sourceIp) && !$util.isNull($ctx.identity.defaultAuthStrategy) ) 3 | #set( $authMode = "userPools" ) 4 | #end 5 | ## [End] Determine request authentication mode ** 6 | ## [Start] Check authMode and execute owner/group checks ** 7 | #if( $authMode == "userPools" ) 8 | ## No Static Group Authorization Rules ** 9 | 10 | 11 | ## [Start] Owner Authorization Checks ** 12 | #set( $isOwnerAuthorized = false ) 13 | ## Authorization rule: { allow: owner, ownerField: "owner", identityClaim: "cognito:username" } ** 14 | #set( $allowedOwners0 = $util.defaultIfNull($ctx.args.owner, null) ) 15 | #set( $identityValue = $util.defaultIfNull($ctx.identity.claims.get("username"), 16 | $util.defaultIfNull($ctx.identity.claims.get("cognito:username"), "___xamznone____")) ) 17 | #if( $util.isList($allowedOwners0) ) 18 | #foreach( $allowedOwner in $allowedOwners0 ) 19 | #if( $allowedOwner == $identityValue ) 20 | #set( $isOwnerAuthorized = true ) 21 | #end 22 | #end 23 | #end 24 | #if( $util.isString($allowedOwners0) ) 25 | #if( $allowedOwners0 == $identityValue ) 26 | #set( $isOwnerAuthorized = true ) 27 | #end 28 | #end 29 | ## [End] Owner Authorization Checks ** 30 | 31 | 32 | ## [Start] Throw if unauthorized ** 33 | #if( !($isStaticGroupAuthorized == true || $isOwnerAuthorized == true) ) 34 | $util.unauthorized() 35 | #end 36 | ## [End] Throw if unauthorized ** 37 | #end 38 | ## [End] Check authMode and execute owner/group checks ** 39 | 40 | $util.toJson(null) -------------------------------------------------------------------------------- /extension/amplify/backend/api/chummy/build/stacks/CustomResources.json: -------------------------------------------------------------------------------- 1 | { 2 | "AWSTemplateFormatVersion": "2010-09-09", 3 | "Description": "An auto-generated nested stack.", 4 | "Metadata": {}, 5 | "Parameters": { 6 | "AppSyncApiId": { 7 | "Type": "String", 8 | "Description": "The id of the AppSync API associated with this project." 9 | }, 10 | "AppSyncApiName": { 11 | "Type": "String", 12 | "Description": "The name of the AppSync API", 13 | "Default": "AppSyncSimpleTransform" 14 | }, 15 | "env": { 16 | "Type": "String", 17 | "Description": "The environment name. e.g. Dev, Test, or Production", 18 | "Default": "NONE" 19 | }, 20 | "S3DeploymentBucket": { 21 | "Type": "String", 22 | "Description": "The S3 bucket containing all deployment assets for the project." 23 | }, 24 | "S3DeploymentRootKey": { 25 | "Type": "String", 26 | "Description": "An S3 key relative to the S3DeploymentBucket that points to the root\nof the deployment directory." 27 | } 28 | }, 29 | "Resources": { 30 | "EmptyResource": { 31 | "Type": "Custom::EmptyResource", 32 | "Condition": "AlwaysFalse" 33 | } 34 | }, 35 | "Conditions": { 36 | "HasEnvironmentParameter": { 37 | "Fn::Not": [ 38 | { 39 | "Fn::Equals": [ 40 | { 41 | "Ref": "env" 42 | }, 43 | "NONE" 44 | ] 45 | } 46 | ] 47 | }, 48 | "AlwaysFalse": { 49 | "Fn::Equals": [ 50 | "true", 51 | "false" 52 | ] 53 | } 54 | }, 55 | "Outputs": { 56 | "EmptyOutput": { 57 | "Description": "An empty output. You may delete this if you have at least one resource above.", 58 | "Value": "" 59 | } 60 | } 61 | } -------------------------------------------------------------------------------- /extension/amplify/backend/api/chummy/parameters.json: -------------------------------------------------------------------------------- 1 | { 2 | "AppSyncApiName": "chummy", 3 | "DynamoDBBillingMode": "PAY_PER_REQUEST", 4 | "DynamoDBEnableServerSideEncryption": false, 5 | "AuthCognitoUserPoolId": { 6 | "Fn::GetAtt": [ 7 | "authchummy9948bcd5", 8 | "Outputs.UserPoolId" 9 | ] 10 | } 11 | } -------------------------------------------------------------------------------- /extension/amplify/backend/api/chummy/schema.graphql: -------------------------------------------------------------------------------- 1 | type User 2 | @model 3 | @auth( 4 | rules: [ 5 | { 6 | allow: owner 7 | ownerField: "owner" 8 | operations: [create, update, delete, read] 9 | } 10 | { allow: public, provider: apiKey } 11 | ] 12 | ) { 13 | id: ID! 14 | accountType: String! 15 | bookmarks: [Bookmark] @connection(keyName: "byUser", fields: ["owner"]) 16 | metadata: UserMetadata 17 | owner: String! 18 | onMailingList: String 19 | isTrial: String 20 | } 21 | 22 | type UserMetadata { 23 | cognitoUsername: String 24 | cognitoUserPoolId: String 25 | stripeId: String 26 | } 27 | 28 | type Bookmark 29 | @model 30 | @auth( 31 | rules: [ 32 | { 33 | allow: owner 34 | ownerField: "owner" 35 | operations: [create, update, delete, read] 36 | } 37 | { allow: public, provider: apiKey } 38 | ] 39 | ) 40 | @key(name: "byUser", fields: ["owner", "name"]) { 41 | id: ID! 42 | userId: ID! 43 | name: String! 44 | path: String! 45 | pinned: String! # We have to use String type because Boolean types can't be sort keys 46 | branch: String! 47 | repo: String! 48 | owner: String! 49 | } 50 | 51 | # Method resolver - https://dev.to/aws-builders/use-lambda-resolvers-in-your-graphql-api-with-aws-amplify-5e13 52 | type Mutation { 53 | cancelSubscription: CancelSubscriptionResponse 54 | @function(name: "chummyCheckoutTrigger-${env}") 55 | syncUser: SyncUserResponse @function(name: "chummyCheckoutTrigger-${env}") 56 | } 57 | 58 | type CancelSubscriptionResponse { 59 | id: String! 60 | } 61 | 62 | type SyncUserResponse { 63 | id: String! 64 | } 65 | -------------------------------------------------------------------------------- /extension/amplify/backend/api/chummy/stacks/CustomResources.json: -------------------------------------------------------------------------------- 1 | { 2 | "AWSTemplateFormatVersion": "2010-09-09", 3 | "Description": "An auto-generated nested stack.", 4 | "Metadata": {}, 5 | "Parameters": { 6 | "AppSyncApiId": { 7 | "Type": "String", 8 | "Description": "The id of the AppSync API associated with this project." 9 | }, 10 | "AppSyncApiName": { 11 | "Type": "String", 12 | "Description": "The name of the AppSync API", 13 | "Default": "AppSyncSimpleTransform" 14 | }, 15 | "env": { 16 | "Type": "String", 17 | "Description": "The environment name. e.g. Dev, Test, or Production", 18 | "Default": "NONE" 19 | }, 20 | "S3DeploymentBucket": { 21 | "Type": "String", 22 | "Description": "The S3 bucket containing all deployment assets for the project." 23 | }, 24 | "S3DeploymentRootKey": { 25 | "Type": "String", 26 | "Description": "An S3 key relative to the S3DeploymentBucket that points to the root\nof the deployment directory." 27 | } 28 | }, 29 | "Resources": { 30 | "EmptyResource": { 31 | "Type": "Custom::EmptyResource", 32 | "Condition": "AlwaysFalse" 33 | } 34 | }, 35 | "Conditions": { 36 | "HasEnvironmentParameter": { 37 | "Fn::Not": [ 38 | { 39 | "Fn::Equals": [ 40 | { 41 | "Ref": "env" 42 | }, 43 | "NONE" 44 | ] 45 | } 46 | ] 47 | }, 48 | "AlwaysFalse": { 49 | "Fn::Equals": ["true", "false"] 50 | } 51 | }, 52 | "Outputs": { 53 | "EmptyOutput": { 54 | "Description": "An empty output. You may delete this if you have at least one resource above.", 55 | "Value": "" 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /extension/amplify/backend/api/chummy/transform.conf.json: -------------------------------------------------------------------------------- 1 | { 2 | "Version": 5, 3 | "ElasticsearchWarning": true 4 | } -------------------------------------------------------------------------------- /extension/amplify/backend/api/chummyRestApi/api-params.json: -------------------------------------------------------------------------------- 1 | { 2 | "paths": [ 3 | { 4 | "name": "/", 5 | "lambdaFunction": "chummyServer", 6 | "privacy": { 7 | "open": true 8 | } 9 | } 10 | ], 11 | "resourceName": "chummyRestApi", 12 | "apiName": "chummyRestApi", 13 | "functionArns": [ 14 | { 15 | "lambdaFunction": "chummyServer" 16 | } 17 | ], 18 | "privacy": { 19 | "auth": 1, 20 | "unauth": 0, 21 | "authRoleName": "amplify-chummy-dev-213651-authRole", 22 | "unAuthRoleName": "amplify-chummy-dev-213651-unauthRole" 23 | }, 24 | "dependsOn": [ 25 | { 26 | "category": "function", 27 | "resourceName": "chummyServer", 28 | "attributes": [ 29 | "Name", 30 | "Arn" 31 | ] 32 | } 33 | ], 34 | "uuid": "8e42210c" 35 | } -------------------------------------------------------------------------------- /extension/amplify/backend/api/chummyRestApi/parameters.json: -------------------------------------------------------------------------------- 1 | { 2 | "authRoleName": { 3 | "Ref": "AuthRoleName" 4 | }, 5 | "unauthRoleName": { 6 | "Ref": "UnauthRoleName" 7 | } 8 | } -------------------------------------------------------------------------------- /extension/amplify/backend/auth/chummy9948bcd5/parameters.json: -------------------------------------------------------------------------------- 1 | { 2 | "identityPoolName": "chummy9948bcd5_identitypool_9948bcd5", 3 | "allowUnauthenticatedIdentities": false, 4 | "resourceNameTruncated": "chummy9948bcd5", 5 | "userPoolName": "chummy9948bcd5_userpool_9948bcd5", 6 | "autoVerifiedAttributes": [ 7 | "email" 8 | ], 9 | "mfaConfiguration": "OFF", 10 | "mfaTypes": [ 11 | "SMS Text Message" 12 | ], 13 | "smsAuthenticationMessage": "Your authentication code is {####}", 14 | "smsVerificationMessage": "Your verification code is {####}", 15 | "emailVerificationSubject": "Your verification code", 16 | "emailVerificationMessage": "Your verification code is {####}", 17 | "defaultPasswordPolicy": false, 18 | "passwordPolicyMinLength": 8, 19 | "passwordPolicyCharacters": [], 20 | "requiredAttributes": [ 21 | "email" 22 | ], 23 | "userpoolClientGenerateSecret": true, 24 | "userpoolClientRefreshTokenValidity": 30, 25 | "userpoolClientWriteAttributes": [ 26 | "email" 27 | ], 28 | "userpoolClientReadAttributes": [ 29 | "email" 30 | ], 31 | "userpoolClientLambdaRole": "chummy9948bcd5_userpoolclient_lambda_role", 32 | "userpoolClientSetAttributes": false, 33 | "sharedId": "9948bcd5", 34 | "resourceName": "chummy9948bcd5", 35 | "authSelections": "identityPoolAndUserPool", 36 | "authRoleArn": { 37 | "Fn::GetAtt": [ 38 | "AuthRole", 39 | "Arn" 40 | ] 41 | }, 42 | "unauthRoleArn": { 43 | "Fn::GetAtt": [ 44 | "UnauthRole", 45 | "Arn" 46 | ] 47 | }, 48 | "useDefault": "defaultSocial", 49 | "hostedUI": true, 50 | "hostedUIDomainName": "chummy9948bcd5-9948bcd5", 51 | "authProvidersUserPool": [], 52 | "hostedUIProviderMeta": "[]", 53 | "oAuthMetadata": "{\"AllowedOAuthFlows\":[\"code\"],\"AllowedOAuthScopes\":[\"phone\",\"email\",\"openid\",\"profile\",\"aws.cognito.signin.user.admin\"],\"CallbackURLs\":[\"http://localhost:8000/account/\"],\"LogoutURLs\":[\"http://localhost:8000/\"]}", 54 | "userPoolGroupList": [], 55 | "serviceName": "Cognito", 56 | "usernameCaseSensitive": false, 57 | "dependsOn": [] 58 | } -------------------------------------------------------------------------------- /extension/amplify/backend/function/chummyLambdaLayer/layer-runtimes.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "value": "nodejs", 4 | "name": "NodeJS", 5 | "layerExecutablePath": "nodejs/node_modules", 6 | "cloudTemplateValue": "nodejs14.x" 7 | } 8 | ] -------------------------------------------------------------------------------- /extension/amplify/backend/function/chummyLambdaLayer/lib/nodejs/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.0.0", 3 | "lockfileVersion": 1, 4 | "requires": true, 5 | "dependencies": { 6 | "@types/node": { 7 | "version": "14.14.28", 8 | "resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.28.tgz", 9 | "integrity": "sha512-lg55ArB+ZiHHbBBttLpzD07akz0QPrZgUODNakeC09i62dnrywr9mFErHuaPlB6I7z+sEbK+IYmplahvplCj2g==" 10 | }, 11 | "qs": { 12 | "version": "6.9.6", 13 | "resolved": "https://registry.npmjs.org/qs/-/qs-6.9.6.tgz", 14 | "integrity": "sha512-TIRk4aqYLNoJUbd+g2lEdz5kLWIuTMRagAXxl78Q0RiVjAOugHmeKNGdd3cwo/ktpf9aL9epCfFqWDEKysUlLQ==" 15 | }, 16 | "stripe": { 17 | "version": "8.135.0", 18 | "resolved": "https://registry.npmjs.org/stripe/-/stripe-8.135.0.tgz", 19 | "integrity": "sha512-cy2IhKhENtvcwdrqtX4jZK4kgu0crA0YrwMgHovIzMAv/Ebr5LqBQ/nhyBqsssExY1hJjn765vhltNrf0WV+Iw==", 20 | "requires": { 21 | "@types/node": ">=8.1.0", 22 | "qs": "^6.6.0" 23 | } 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /extension/amplify/backend/function/chummyLambdaLayer/lib/nodejs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.0.0", 3 | "description": "", 4 | "main": "index.js", 5 | "dependencies": { 6 | "stripe": "^8.135.0" 7 | }, 8 | "devDependencies": {} 9 | } 10 | -------------------------------------------------------------------------------- /extension/amplify/backend/function/chummyLambdaLayer/parameters.json: -------------------------------------------------------------------------------- 1 | { 2 | "layerVersion": 1 3 | } -------------------------------------------------------------------------------- /extension/amplify/backend/function/chummyPostAuthTrigger/amplify.state: -------------------------------------------------------------------------------- 1 | { 2 | "pluginId": "amplify-nodejs-function-runtime-provider", 3 | "functionRuntime": "nodejs", 4 | "useLegacyBuild": true, 5 | "defaultEditorFile": "src/index.js" 6 | } -------------------------------------------------------------------------------- /extension/amplify/backend/function/chummyPostAuthTrigger/function-parameters.json: -------------------------------------------------------------------------------- 1 | { 2 | "permissions": { 3 | "api": { 4 | "chummy": [ 5 | "create", 6 | "read", 7 | "update", 8 | "delete" 9 | ] 10 | }, 11 | "storage": { 12 | "User:@model(appsync)": [ 13 | "create", 14 | "read", 15 | "update", 16 | "delete" 17 | ], 18 | "Bookmark:@model(appsync)": [ 19 | "create", 20 | "read", 21 | "update", 22 | "delete" 23 | ] 24 | } 25 | }, 26 | "lambdaLayers": [ 27 | { 28 | "type": "ProjectLayer", 29 | "resourceName": "chummyLambdaLayer", 30 | "version": 1 31 | } 32 | ] 33 | } -------------------------------------------------------------------------------- /extension/amplify/backend/function/chummyPostAuthTrigger/parameters.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /extension/amplify/backend/function/chummyPostAuthTrigger/src/event.json: -------------------------------------------------------------------------------- 1 | { 2 | "key1": "value1", 3 | "key2": "value2", 4 | "key3": "value3" 5 | } 6 | -------------------------------------------------------------------------------- /extension/amplify/backend/function/chummyPostAuthTrigger/src/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chummyPostAuthTrigger", 3 | "version": "2.0.0", 4 | "lockfileVersion": 1 5 | } 6 | -------------------------------------------------------------------------------- /extension/amplify/backend/function/chummyPostAuthTrigger/src/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chummyPostAuthTrigger", 3 | "version": "2.0.0", 4 | "description": "Lambda function generated by Amplify", 5 | "main": "index.js", 6 | "license": "Apache-2.0", 7 | "dependencies": {} 8 | } 9 | -------------------------------------------------------------------------------- /extension/amplify/backend/function/chummyPostAuthTrigger/src/queries.js: -------------------------------------------------------------------------------- 1 | const getUser = `query getUser($id: ID!) { 2 | getUser(id: $id) { 3 | id 4 | accountType 5 | metadata { 6 | cognitoUsername 7 | cognitoUserPoolId 8 | stripeId 9 | } 10 | createdAt 11 | updatedAt 12 | owner 13 | bookmarks { 14 | items { 15 | id 16 | userId 17 | name 18 | path 19 | pinned 20 | branch 21 | repo 22 | createdAt 23 | updatedAt 24 | owner 25 | } 26 | } 27 | } 28 | } 29 | `; 30 | 31 | const createUser = `mutation createUser( 32 | $input: CreateUserInput! 33 | $condition: ModelUserConditionInput 34 | ) { 35 | createUser(input: $input, condition: $condition) { 36 | id 37 | accountType 38 | metadata { 39 | cognitoUsername 40 | cognitoUserPoolId 41 | stripeId 42 | } 43 | createdAt 44 | updatedAt 45 | owner 46 | bookmarks { 47 | items { 48 | id 49 | userId 50 | name 51 | path 52 | pinned 53 | branch 54 | repo 55 | createdAt 56 | updatedAt 57 | owner 58 | } 59 | nextToken 60 | } 61 | } 62 | } 63 | `; 64 | 65 | const updateUser = `mutation updateUser( 66 | $input: UpdateUserInput! 67 | $condition: ModelUserConditionInput 68 | ) { 69 | updateUser(input: $input, condition: $condition) { 70 | id 71 | accountType 72 | metadata { 73 | cognitoUsername 74 | cognitoUserPoolId 75 | stripeId 76 | } 77 | createdAt 78 | updatedAt 79 | owner 80 | bookmarks { 81 | items { 82 | id 83 | userId 84 | name 85 | path 86 | pinned 87 | branch 88 | repo 89 | createdAt 90 | updatedAt 91 | owner 92 | } 93 | nextToken 94 | } 95 | } 96 | } 97 | `; 98 | 99 | exports.getUser = getUser; 100 | exports.createUser = createUser; 101 | exports.updateUser = updateUser; 102 | -------------------------------------------------------------------------------- /extension/amplify/backend/function/chummyServer/amplify.state: -------------------------------------------------------------------------------- 1 | { 2 | "pluginId": "amplify-nodejs-function-runtime-provider", 3 | "functionRuntime": "nodejs", 4 | "useLegacyBuild": true, 5 | "defaultEditorFile": "src/app.js" 6 | } -------------------------------------------------------------------------------- /extension/amplify/backend/function/chummyServer/function-parameters.json: -------------------------------------------------------------------------------- 1 | { 2 | "permissions": { 3 | "api": { 4 | "chummy": [ 5 | "create", 6 | "read", 7 | "update", 8 | "delete" 9 | ] 10 | } 11 | }, 12 | "lambdaLayers": [ 13 | { 14 | "type": "ProjectLayer", 15 | "resourceName": "chummyLambdaLayer", 16 | "version": 1 17 | } 18 | ] 19 | } -------------------------------------------------------------------------------- /extension/amplify/backend/function/chummyServer/parameters.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /extension/amplify/backend/function/chummyServer/src/event.json: -------------------------------------------------------------------------------- 1 | { 2 | "key1": "value1", 3 | "key2": "value2", 4 | "key3": "value3" 5 | } 6 | -------------------------------------------------------------------------------- /extension/amplify/backend/function/chummyServer/src/index.js: -------------------------------------------------------------------------------- 1 | const awsServerlessExpress = require('aws-serverless-express'); 2 | const app = require('./app'); 3 | 4 | const server = awsServerlessExpress.createServer(app); 5 | 6 | exports.handler = (event, context) => { 7 | console.log(`EVENT: ${JSON.stringify(event)}`); 8 | return awsServerlessExpress.proxy(server, event, context, 'PROMISE').promise; 9 | }; 10 | -------------------------------------------------------------------------------- /extension/amplify/backend/function/chummyServer/src/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chummyServer", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "dependencies": { 7 | "aws-serverless-express": "^3.3.5", 8 | "body-parser": "^1.19.0", 9 | "express": "^4.15.2" 10 | }, 11 | "devDependencies": {}, 12 | "scripts": { 13 | "test": "echo \"Error: no test specified\" && exit 1" 14 | }, 15 | "author": "", 16 | "license": "ISC" 17 | } 18 | -------------------------------------------------------------------------------- /extension/amplify/backend/function/chummyServer/src/queries.js: -------------------------------------------------------------------------------- 1 | const updateUser = `mutation updateUser( 2 | $input: UpdateUserInput! 3 | $condition: ModelUserConditionInput 4 | ) { 5 | updateUser(input: $input, condition: $condition) { 6 | id 7 | accountType 8 | metadata { 9 | cognitoUsername 10 | cognitoUserPoolId 11 | stripeId 12 | } 13 | createdAt 14 | updatedAt 15 | owner 16 | bookmarks { 17 | items { 18 | id 19 | userId 20 | name 21 | path 22 | pinned 23 | branch 24 | repo 25 | createdAt 26 | updatedAt 27 | owner 28 | } 29 | nextToken 30 | } 31 | } 32 | } 33 | `; 34 | 35 | exports.updateUser = updateUser; 36 | -------------------------------------------------------------------------------- /extension/amplify/backend/tags.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "Key": "user:Stack", 4 | "Value": "{project-env}" 5 | }, 6 | { 7 | "Key": "user:Application", 8 | "Value": "{project-name}" 9 | } 10 | ] -------------------------------------------------------------------------------- /extension/amplify/cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "features": { 3 | "graphqltransformer": { 4 | "addmissingownerfields": true, 5 | "validatetypenamereservedwords": true, 6 | "useexperimentalpipelinedtransformer": false, 7 | "enableiterativegsiupdates": false 8 | }, 9 | "frontend-ios": { 10 | "enablexcodeintegration": true 11 | }, 12 | "auth": { 13 | "enablecaseinsensitivity": true 14 | }, 15 | "codegen": { 16 | "useappsyncmodelgenplugin": true 17 | } 18 | } 19 | } -------------------------------------------------------------------------------- /extension/cypress.json: -------------------------------------------------------------------------------- 1 | { 2 | "retries": { 3 | "runMode": 2, 4 | "openMode": 2 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /extension/cypress/fixtures/bookmarks.json: -------------------------------------------------------------------------------- 1 | { 2 | "action": "get-bookmarks", 3 | "payload": { 4 | "bookmarks": [ 5 | { 6 | "pinned": false, 7 | "branch": { "name": "master" }, 8 | "bookmarkId": "bookmark-alexkim205:chummy:extension/public/icon/up16.png", 9 | "repo": { "name": "chummy", "owner": "alexkim205" }, 10 | "path": "extension/public/icon/up16.png", 11 | "name": "up16.png" 12 | }, 13 | { 14 | "path": "extension/.babelrc", 15 | "branch": { "name": "master" }, 16 | "bookmarkId": "bookmark-alexkim205:chummy:extension/.babelrc", 17 | "name": ".babelrc", 18 | "repo": { "name": "chummy", "owner": "alexkim205" }, 19 | "pinned": false 20 | }, 21 | { 22 | "branch": { "name": "master" }, 23 | "path": ".gitignore", 24 | "pinned": false, 25 | "bookmarkId": "bookmark-alexkim205:DeepSpotify:.gitignore", 26 | "repo": { "name": "DeepSpotify", "owner": "alexkim205" }, 27 | "name": ".gitignore" 28 | }, 29 | { 30 | "repo": { "owner": "alexkim205", "name": "DeepSpotify" }, 31 | "path": "DeepSpotify.yml", 32 | "name": "DeepSpotify.yml", 33 | "bookmarkId": "bookmark-alexkim205:DeepSpotify:DeepSpotify.yml", 34 | "branch": { "name": "master" }, 35 | "pinned": false 36 | }, 37 | { 38 | "repo": { "name": "G-Desktop-Suite", "owner": "alexkim205" }, 39 | "name": "README.md", 40 | "path": "README.md", 41 | "bookmarkId": "bookmark-alexkim205:G-Desktop-Suite:README.md", 42 | "pinned": false, 43 | "branch": { "name": "master" } 44 | }, 45 | { 46 | "bookmarkId": "bookmark-alexkim205:chummy:.gitignore", 47 | "name": ".gitignore", 48 | "pinned": false, 49 | "path": ".gitignore", 50 | "repo": { "owner": "alexkim205", "name": "chummy" }, 51 | "branch": { "name": "master" } 52 | }, 53 | { 54 | "repo": { "name": "chummy", "owner": "alexkim205" }, 55 | "path": ".vscode/launch.json", 56 | "pinned": false, 57 | "bookmarkId": "bookmark-alexkim205:chummy:.vscode/launch.json", 58 | "branch": { "name": "master" }, 59 | "name": "launch.json" 60 | } 61 | ] 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /extension/cypress/fixtures/repositories.json: -------------------------------------------------------------------------------- 1 | { 2 | "action": "get-open-repositories", 3 | "payload": { 4 | "AtomicCodeLabs/chummy": [ 5 | { 6 | "url": "https://github.com/AtomicCodeLabs/chummy", 7 | "owner": "alexkim205", 8 | "repo": "chummy", 9 | "tab": { 10 | "name": "master", 11 | "tabId": 2, 12 | "subpage": "repository", 13 | "nodeName": null 14 | }, 15 | "type": "tree" 16 | }, 17 | { 18 | "url": "https://github.com/AtomicCodeLabs/chummy", 19 | "owner": "alexkim205", 20 | "repo": "chummy", 21 | "tab": { 22 | "name": "master", 23 | "tabId": 11, 24 | "subpage": "repository", 25 | "nodeName": null 26 | }, 27 | "type": "tree" 28 | } 29 | ] 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /extension/cypress/fixtures/store.json: -------------------------------------------------------------------------------- 1 | { 2 | "action": "get-store", 3 | "payload": { 4 | "isBookmarksSectionMinimized": true, 5 | "isSearchSectionMinimized": true, 6 | "isSidebarMinimized": false, 7 | "isStickyWindow": false, 8 | "isTreeSectionMinimized": { 9 | "files": { "isMinimized": false, "lastHeight": 722 }, 10 | "openTabs": { "isMinimized": false, "lastHeight": 257 }, 11 | "sessions": { "isMinimized": true, "lastHeight": 87 } 12 | }, 13 | "language": "en_US", 14 | "selectedBookmarkQuery": null, 15 | "selectedBookmarkRepo": null, 16 | "selectedLanguage": null, 17 | "selectedOpenRepo": null, 18 | "selectedQuery": null, 19 | "sidebarView": 0, 20 | "sidebarWidth": 533, 21 | "spacing": "cozy", 22 | "theme": "vanilla-dark" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /extension/cypress/fixtures/user.json: -------------------------------------------------------------------------------- 1 | { 2 | "action": "get-current-user", 3 | "payload": { 4 | "user": { 5 | "uid": "LR3KUY0FumTy4u79tRJEK88YoTj2", 6 | "email": "agk2144@columbia.edu", 7 | "displayName": "Alex Gyujin Kim", 8 | "photoURL": "https://avatars1.githubusercontent.com/u/13460330?v=4", 9 | "apiKey": "292836e915bf829c4f04307e01f91357a701cfd5", 10 | "accountType": null 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /extension/cypress/integration/pages.spec.js: -------------------------------------------------------------------------------- 1 | import mockApi from '../mock/api'; 2 | 3 | describe('Render each page', () => { 4 | before('set sidebar viewport', () => { 5 | mockApi(); 6 | }); 7 | 8 | beforeEach(() => { 9 | cy.viewport(400, 1000); 10 | }); 11 | 12 | afterEach(() => { 13 | cy.clearCookies(); 14 | }); 15 | 16 | describe('Account', () => { 17 | it('page title is `Account`', () => { 18 | cy.goTo('Account'); 19 | cy.findById('page-title').should('contain.text', 'Account'); 20 | }); 21 | }); 22 | 23 | describe('Bookmarks', () => { 24 | it('page title is `Bookmarks`', () => { 25 | cy.goTo('Bookmarks'); 26 | cy.findById('page-title').should('contain.text', 'Bookmarks'); 27 | }); 28 | }); 29 | 30 | describe('Search', () => { 31 | it('page title is `Search`', () => { 32 | cy.goTo('Search'); 33 | cy.findById('page-title').should('contain.text', 'Search'); 34 | }); 35 | }); 36 | 37 | describe('Settings', () => { 38 | it('page title is `Settings`', () => { 39 | cy.goTo('Settings'); 40 | cy.findById('page-title').should('contain.text', 'Settings'); 41 | }); 42 | }); 43 | 44 | describe('Tree', () => { 45 | it('page title is `Explorer`', () => { 46 | cy.goTo('Tree'); 47 | cy.findById('page-title').should('contain.text', 'Explorer'); 48 | }); 49 | }); 50 | 51 | // describe('VCS', () => { 52 | // it('page title is `Source Control`', () => { 53 | // cy.goTo('VCS'); 54 | // // cy.findById('page-title').should('contain.text', 'Source Control'); 55 | // }); 56 | // }); 57 | }); 58 | -------------------------------------------------------------------------------- /extension/cypress/mock/api.js: -------------------------------------------------------------------------------- 1 | import browser from 'sinon-chrome'; 2 | import { interceptJS } from '../utils/http'; 3 | 4 | export default () => { 5 | browser.runtime.id = 'testid'; // workaround https://github.com/mozilla/webextension-polyfill/issues/218 6 | 7 | interceptJS(); 8 | 9 | // Retrieve test data 10 | cy.fixture('user').then((user) => { 11 | cy.fixture('bookmarks').then((bookmarks) => { 12 | cy.fixture('repositories').then((repositories) => { 13 | cy.fixture('store').then((store) => { 14 | browser.runtime.sendMessage 15 | .withArgs({ action: 'get-current-user' }) 16 | .yields(user); 17 | browser.runtime.sendMessage 18 | .withArgs({ action: 'get-open-repositories' }) 19 | .yields(repositories); 20 | browser.runtime.sendMessage 21 | .withArgs({ 22 | action: 'get-store', 23 | payload: Object.keys(store.payload) 24 | }) 25 | .yields(store); 26 | browser.runtime.sendMessage 27 | .withArgs({ action: 'get-bookmarks' }) 28 | .yields(bookmarks); 29 | 30 | // Load your popup 31 | cy.visit(`popup.html`, { 32 | // If you need to stub `chrome*` API, you should do it there: 33 | onBeforeLoad(win) { 34 | global.chrome = browser; 35 | win.chrome = browser; 36 | } 37 | }); 38 | }); 39 | }); 40 | }); 41 | }); 42 | }; 43 | -------------------------------------------------------------------------------- /extension/cypress/plugins/index.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const extensionLoader = require('cypress-browser-extension-plugin/loader'); 3 | 4 | /// 5 | // *********************************************************** 6 | // This example plugins/index.js can be used to load plugins 7 | // 8 | // You can change the location of this file or turn off loading 9 | // the plugins file with the 'pluginsFile' configuration option. 10 | // 11 | // You can read more here: 12 | // https://on.cypress.io/plugins-guide 13 | // *********************************************************** 14 | 15 | // This function is called when a project is opened or re-opened (e.g. due to 16 | // the project's config changing) 17 | 18 | /** 19 | * @type {Cypress.PluginConfig} 20 | */ 21 | 22 | const sourceDir = path.join(__dirname, '../../dist/', process.env.TARGET); 23 | 24 | module.exports = (on) => { 25 | on('before:browser:launch', async (browser = {}, launchOptions) => { 26 | const loader = extensionLoader.load({ 27 | source: sourceDir, 28 | alias: 'chummy' 29 | }); 30 | 31 | const args = await loader(browser, []); 32 | launchOptions.args.push(...args); 33 | 34 | return launchOptions; 35 | }); 36 | }; 37 | -------------------------------------------------------------------------------- /extension/cypress/support/commands.js: -------------------------------------------------------------------------------- 1 | const addExtensionCommands = require('cypress-browser-extension-plugin/commands'); 2 | const { interceptJS } = require('../utils/http'); 3 | 4 | // *********************************************** 5 | // This example commands.js shows you how to 6 | // create various custom commands and overwrite 7 | // existing commands. 8 | // 9 | // For more comprehensive examples of custom 10 | // commands please read more here: 11 | // https://on.cypress.io/custom-commands 12 | // *********************************************** 13 | // 14 | // 15 | // -- This is a parent command -- 16 | // Cypress.Commands.add("login", (email, password) => { ... }) 17 | // 18 | // 19 | // -- This is a child command -- 20 | // Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... }) 21 | // 22 | // 23 | // -- This is a dual command -- 24 | // Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... }) 25 | // 26 | // 27 | // -- This will overwrite an existing command -- 28 | // Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... }) 29 | // cypress/support/command.js 30 | addExtensionCommands(Cypress); 31 | 32 | // Find element by id 33 | Cypress.Commands.add('findById', (id) => { 34 | return cy.get(`[data-testid=${id}]`, { timeout: 10000 }); 35 | }); 36 | 37 | // Navigate to sidebar page 38 | Cypress.Commands.add('goTo', (pageName) => { 39 | let buttonTestId = 'route-button-'; 40 | switch (pageName) { 41 | case 'Account': 42 | buttonTestId += 'account'; 43 | break; 44 | case 'Bookmarks': 45 | buttonTestId += 'bookmarks'; 46 | break; 47 | case 'Search': 48 | buttonTestId += 'search'; 49 | break; 50 | case 'Settings': 51 | buttonTestId += 'settings'; 52 | break; 53 | case 'Tree': 54 | break; 55 | case 'VCS': 56 | buttonTestId += 'vcs'; 57 | break; 58 | default: 59 | cy.log('Could not navigate to unknown page', pageName); 60 | return; 61 | } 62 | interceptJS(); 63 | cy.findById(buttonTestId).click(); 64 | }); 65 | -------------------------------------------------------------------------------- /extension/cypress/support/index.js: -------------------------------------------------------------------------------- 1 | // *********************************************************** 2 | // This example support/index.js is processed and 3 | // loaded automatically before your test files. 4 | // 5 | // This is a great place to put global configuration and 6 | // behavior that modifies Cypress. 7 | // 8 | // You can change the location of this file or turn off 9 | // automatically serving support files with the 10 | // 'supportFile' configuration option. 11 | // 12 | // You can read more here: 13 | // https://on.cypress.io/configuration 14 | // *********************************************************** 15 | 16 | // Import commands.js using ES2015 syntax: 17 | import './commands'; 18 | 19 | // Alternatively you can use CommonJS syntax: 20 | // require('./commands') 21 | -------------------------------------------------------------------------------- /extension/cypress/utils/http.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line import/prefer-default-export 2 | export const interceptJS = () => { 3 | // Intercept all requests to make sure files are being pulled from the correctly 4 | // sered directory 5 | cy.intercept( 6 | { 7 | method: 'GET', 8 | url: '**/*.js' 9 | }, 10 | (req) => { 11 | const filename = req.url.split('/').pop(); 12 | req.url = `${Cypress.config().baseUrl}/${filename}`; 13 | } 14 | ); 15 | }; 16 | -------------------------------------------------------------------------------- /extension/cypress/utils/index.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AtomicCodeLabs/chummy/5e752bcdbdc3d05c914b504567fdd0084127e707/extension/cypress/utils/index.js -------------------------------------------------------------------------------- /extension/firebase.json: -------------------------------------------------------------------------------- 1 | { 2 | "emulators": { 3 | "firestore": { 4 | "port": 8080 5 | }, 6 | "ui": { 7 | "enabled": true 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /extension/manifest-base.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 2, 3 | "name": "Chummy", 4 | "browser_action": { 5 | "default_icon": { 6 | "16": "icon/chummy16.png", 7 | "32": "icon/chummy32.png", 8 | "64": "icon/chummy64.png", 9 | "128": "icon/chummy128.png" 10 | } 11 | }, 12 | "background": { 13 | "page": "background.html", 14 | "persistent": false 15 | }, 16 | "web_accessible_resources": [ 17 | "background.redirect.inject.js", 18 | "background.signin.inject.js", 19 | "background.style.inject.js" 20 | ], 21 | "permissions": ["tabs", "storage", "*://github.com/*"], 22 | "icons": { 23 | "16": "icon/chummy16.png", 24 | "32": "icon/chummy32.png", 25 | "64": "icon/chummy64.png", 26 | "128": "icon/chummy128.png" 27 | }, 28 | "content_security_policy": "script-src 'self' 'unsafe-eval' https://www.gstatic.com/; object-src 'self'" 29 | } 30 | -------------------------------------------------------------------------------- /extension/public/icon/chummy128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AtomicCodeLabs/chummy/5e752bcdbdc3d05c914b504567fdd0084127e707/extension/public/icon/chummy128.png -------------------------------------------------------------------------------- /extension/public/icon/chummy16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AtomicCodeLabs/chummy/5e752bcdbdc3d05c914b504567fdd0084127e707/extension/public/icon/chummy16.png -------------------------------------------------------------------------------- /extension/public/icon/chummy256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AtomicCodeLabs/chummy/5e752bcdbdc3d05c914b504567fdd0084127e707/extension/public/icon/chummy256.png -------------------------------------------------------------------------------- /extension/public/icon/chummy32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AtomicCodeLabs/chummy/5e752bcdbdc3d05c914b504567fdd0084127e707/extension/public/icon/chummy32.png -------------------------------------------------------------------------------- /extension/public/icon/chummy512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AtomicCodeLabs/chummy/5e752bcdbdc3d05c914b504567fdd0084127e707/extension/public/icon/chummy512.png -------------------------------------------------------------------------------- /extension/public/icon/chummy64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AtomicCodeLabs/chummy/5e752bcdbdc3d05c914b504567fdd0084127e707/extension/public/icon/chummy64.png -------------------------------------------------------------------------------- /extension/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Chummy Build Page 7 | 8 | 9 | 10 |

Chummy Build Page

11 |

If you've reached this page, Chummy Extension has built successfully.

12 | 13 | 14 | -------------------------------------------------------------------------------- /extension/public/social/social1200_628.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AtomicCodeLabs/chummy/5e752bcdbdc3d05c914b504567fdd0084127e707/extension/public/social/social1200_628.png -------------------------------------------------------------------------------- /extension/scripts/BACKUP.prepublish.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | cd $CODEBUILD_SRC_DIR/extension/dist 4 | pwd 5 | 6 | VERSION=$(cat ".version") 7 | STAGE=$1 8 | 9 | # Pushing to 10 | for filename in web/{popup_$VERSION,background.dao_$VERSION}*.js; do 11 | echo "Pushing $filename to s3://chummy-assets-$STAGE/$VERSION/" 12 | aws s3 cp $filename s3://chummy-assets-$STAGE/$VERSION/ 13 | done 14 | 15 | cd $CODEBUILD_SRC_DIR/extension 16 | -------------------------------------------------------------------------------- /extension/scripts/download.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | cd dist.prod 4 | ls 5 | pwd 6 | 7 | VERSION=$(cat "../.version") 8 | 9 | mkdir $VERSION 10 | cd $VERSION 11 | aws s3 cp s3://chummy-assets-prod/$VERSION/ . --recursive 12 | 13 | cd ../.. 14 | -------------------------------------------------------------------------------- /extension/scripts/localpublish.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | cd dist 4 | ls 5 | pwd 6 | 7 | VERSION=$(cat ".version") 8 | 9 | aws s3 cp gamma.web.cloud/background.dao_$VERSION.js s3://chummy-assets-gamma/$VERSION/ 10 | aws s3 cp gamma.web.cloud/popup_$VERSION.js s3://chummy-assets-gamma/$VERSION/ 11 | 12 | zip -r dist_$VERSION.web.cloud.zip gamma.web.cloud 13 | aws s3 cp dist_$VERSION.web.cloud.zip s3://chummy-assets-gamma/$VERSION/ 14 | 15 | cd .. 16 | -------------------------------------------------------------------------------- /extension/scripts/promote.dev.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | # Checkout source branch first 5 | yarn checkout:dev 6 | git add -A 7 | git diff-index --quiet HEAD || HUSKY_SKIP_HOOKS=1 git commit -m "chore(pre-promote): push dev" 8 | git push --follow-tags -u origin extension/dev 9 | 10 | # Push backend 11 | amplify push 12 | -------------------------------------------------------------------------------- /extension/scripts/promote.gamma.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | # Checkout source branch first 5 | yarn checkout:dev 6 | git add -A 7 | git diff-index --quiet HEAD || HUSKY_SKIP_HOOKS=1 git commit -m "chore(pre-promote): push dev" 8 | git push --follow-tags -u origin extension/dev 9 | 10 | # Checkout target branch 11 | yarn checkout:gamma 12 | git add -A 13 | git diff-index --quiet HEAD || HUSKY_SKIP_HOOKS=1 git commit -m "chore(pre-promote): push gamma" 14 | git push --follow-tags -u origin extension/gamma 15 | 16 | # Merge 17 | HUSKY_SKIP_HOOKS=1 git merge --no-edit -s recursive -X ours extension/dev 18 | 19 | # Push target branch 20 | amplify push 21 | git push --follow-tags -u origin extension/gamma 22 | 23 | # Return to dev branch 24 | yarn checkout:dev 25 | -------------------------------------------------------------------------------- /extension/scripts/promote.prod.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | # Checkout source branch first 5 | yarn checkout:gamma 6 | git add -A 7 | git diff-index --quiet HEAD || HUSKY_SKIP_HOOKS=1 git commit -m "chore(pre-promote): push gamma" 8 | git push --follow-tags -u origin extension/gamma 9 | 10 | # Checkout target branch 11 | yarn checkout:prod 12 | git add -A 13 | git diff-index --quiet HEAD || HUSKY_SKIP_HOOKS=1 git commit -m "chore(pre-promote): push prod" 14 | git push --follow-tags -u origin extension/prod 15 | 16 | # Merge 17 | HUSKY_SKIP_HOOKS=1 git merge --no-edit -s recursive -X ours extension/gamma 18 | 19 | # Push target branch 20 | amplify push 21 | git push --follow-tags -u origin extension/prod 22 | 23 | # Return to dev branch 24 | yarn checkout:dev 25 | -------------------------------------------------------------------------------- /extension/scripts/publish.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | cd $CODEBUILD_SRC_DIR/extension/dist 4 | ls 5 | pwd 6 | 7 | VERSION=$(cat ".version") 8 | STAGE=$1 9 | 10 | zip -r dist_$VERSION.chrome.zip chrome 11 | aws s3 cp dist_$VERSION.chrome.zip s3://chummy-assets-$STAGE/$VERSION/ 12 | 13 | zip -r dist_$VERSION.edge.zip edge 14 | aws s3 cp dist_$VERSION.edge.zip s3://chummy-assets-$STAGE/$VERSION/ 15 | 16 | cd opera 17 | zip -r ../dist_$VERSION.opera.zip . 18 | cd .. 19 | aws s3 cp dist_$VERSION.opera.zip s3://chummy-assets-$STAGE/$VERSION/ 20 | 21 | cd moz 22 | zip -r ../dist_$VERSION.moz.zip . 23 | cd .. 24 | aws s3 cp dist_$VERSION.moz.zip s3://chummy-assets-$STAGE/$VERSION/ 25 | 26 | cd $CODEBUILD_SRC_DIR/extension 27 | -------------------------------------------------------------------------------- /extension/src/background/constants.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable camelcase */ 2 | 3 | export const GITHUB_REGEX = new RegExp( 4 | /^(http|https):\/\/github\.com(\/[^/]+){2,}$/ 5 | ); 6 | 7 | // "G-Desktop-Suite/gsuite.rb at revert-68-code-quality-66-prettify · alexkim205/G-Desktop-Suite" 8 | export const REPO_TITLE_REGEX = new RegExp('(?: at )((?:[^ · ]*))'); 9 | 10 | export const generate_ISSUE_TITLE_REGEX = (issueId: number) => 11 | new RegExp(`^(.*?) · Issue #${issueId}`); 12 | 13 | export const generate_PULL_TITLE_REGEX = (pullId: number) => 14 | new RegExp(`^(.*?) · Pull Request #${pullId}`); 15 | 16 | export const NO_WINDOW_EXTENSION_ID = -1; // -3 if it doesn't exist 17 | 18 | export const MIN_MAIN_WINDOW_WIDTH = 500; 19 | -------------------------------------------------------------------------------- /extension/src/background/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /extension/src/background/redirect.inject.js: -------------------------------------------------------------------------------- 1 | import browser from 'webextension-polyfill'; 2 | // eslint-disable-next-line import/prefer-default-export 3 | const redirectPageListeners = () => { 4 | browser.runtime.onMessage.addListener(function redirectListener(request) { 5 | // Make a ajax redirect request 6 | if (request.action === 'redirect-content-script') { 7 | // Garbage collect event listener (listen at most once) 8 | browser.runtime.onMessage.removeListener(redirectListener); 9 | 10 | const { 11 | payload: { owner, repo, type, branch, nodePath } 12 | } = request; 13 | 14 | // Hack: simulate a click on the repository tag by replacing the href 15 | // with the file to redirect to. This gives us a reliable reference element 16 | // to "click" any time any file is chosen. 17 | const repoLink = document.querySelector( 18 | `[data-pjax='#js-repo-pjax-container'][href='/${owner}/${repo}']` 19 | ); 20 | // If navigated to page that doesn't have clickable repo link (like 404) 21 | // fallback to normal redirect. 22 | if (!repoLink) { 23 | window.location.href = `https://github.com/${owner}/${repo}/${type}/${branch}/${nodePath}`; 24 | } else { 25 | // Sometimes ajax redirect will be treated as a normal redirect by GitHub 26 | repoLink.setAttribute( 27 | 'href', 28 | `/${owner}/${repo}/${type}/${branch}/${nodePath}` 29 | ); 30 | repoLink.click(); 31 | } 32 | } 33 | }); 34 | }; 35 | 36 | redirectPageListeners(); 37 | -------------------------------------------------------------------------------- /extension/src/background/signin.inject.js: -------------------------------------------------------------------------------- 1 | import browser from 'webextension-polyfill'; 2 | 3 | console.log('INJECT SIGNIN'); 4 | 5 | /* 6 | Listen for messages from the page. 7 | If the message was from the page script, show an alert. 8 | */ 9 | function authListener() { 10 | window.addEventListener('message', (event) => { 11 | if (event.source === window && event.data?.action === 'auth-from-page') { 12 | console.log(`Content script received message`, event.data); 13 | 14 | const response = { 15 | action: 'auth-from-content-script', 16 | payload: event.data.payload 17 | }; 18 | console.log('Now sending auth message back to extension', response); 19 | browser.runtime.sendMessage(response); 20 | } 21 | }); 22 | } 23 | 24 | // Initialize listener right away 25 | authListener(); 26 | 27 | // Send message after page has loaded 28 | 29 | // Send message to emit auth to content script on script injection 30 | window.postMessage({ action: 'trigger-send-to-cs' }, '*'); 31 | -------------------------------------------------------------------------------- /extension/src/background/storage.js: -------------------------------------------------------------------------------- 1 | import browser from 'webextension-polyfill'; 2 | import log from '../config/log'; 3 | 4 | // Expose chrome storage API that content script can query 5 | const storeAccessListener = async (request) => { 6 | // Sign In 7 | if (request.action === 'set-store') { 8 | try { 9 | await browser.storage.sync.set(request.payload); 10 | } catch (error) { 11 | log.error('Error setting store', error); 12 | } 13 | return null; 14 | } 15 | 16 | // Sign Out 17 | if (request.action === 'get-store') { 18 | try { 19 | const items = await browser.storage.sync.get(request.payload); 20 | return { action: 'get-store', payload: items }; 21 | } catch (error) { 22 | log.error('Error getting store', error); 23 | return null; 24 | } 25 | } 26 | 27 | return null; 28 | }; 29 | browser.runtime.onMessage.addListener((request) => { 30 | if (['set-store', 'get-store'].includes(request.action)) { 31 | return storeAccessListener(request); 32 | } 33 | }); 34 | 35 | // For debugging purposes 36 | browser.storage.onChanged.addListener(async () => { 37 | try { 38 | const items = await browser.storage.sync.get(null); 39 | log.debug('DEBUG', items); 40 | } catch (error) { 41 | log.error('Error getting store changes for debugging', error); 42 | } 43 | }); 44 | -------------------------------------------------------------------------------- /extension/src/background/throttling.util.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/prefer-default-export */ 2 | import { THROTTLING_OPERATION, ACCOUNT_TYPE } from '../global/constants'; 3 | import OperationLimits from '../global/limits/operations'; 4 | 5 | export const isAllowed = (user, operation) => { 6 | if (!user.accountType) return false; 7 | 8 | // Creating a bookmark 9 | if (operation === THROTTLING_OPERATION.CreateBookmark) { 10 | if (!user.bookmarks) return false; 11 | // Check if user exceeds tier limit 12 | if ( 13 | user.bookmarks.length >= 14 | OperationLimits[THROTTLING_OPERATION.CreateBookmark][user.accountType] 15 | ) { 16 | return false; 17 | } 18 | // If within limits, allow user to create bookmark 19 | return true; 20 | } 21 | 22 | // If user is professional or enterprise, they're allowed 23 | // to do anything 24 | if ( 25 | user.accountType === ACCOUNT_TYPE.Professional || 26 | user.accountType === ACCOUNT_TYPE.Enterprise 27 | ) { 28 | return true; 29 | } 30 | 31 | // Default 32 | return false; 33 | }; 34 | -------------------------------------------------------------------------------- /extension/src/components/Buttons/IconButton.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/jsx-props-no-spreading */ 2 | /* eslint-disable react/require-default-props */ 3 | /* eslint-disable react/forbid-prop-types */ 4 | 5 | import React, { Suspense, cloneElement } from 'react'; 6 | import { useHistory, useLocation } from 'react-router-dom'; 7 | import styled from 'styled-components'; 8 | import PropTypes from 'prop-types'; 9 | 10 | import { XIcon } from '@primer/octicons-react'; 11 | 12 | const ContainerButton = styled.div` 13 | display: flex; 14 | justify-content: center; 15 | align-items: center; 16 | cursor: pointer; 17 | 18 | /* width: 100%; */ 19 | height: 100%; 20 | `; 21 | 22 | const IconButton = ({ 23 | style = {}, 24 | Icon, 25 | disabled, 26 | to, 27 | onClick = () => {}, 28 | dataTestId, 29 | ...buttonProps 30 | }) => { 31 | const history = useHistory(); 32 | const location = useLocation(); 33 | const navigateToPage = () => { 34 | onClick(); 35 | 36 | if (disabled) return; 37 | if (!to) return; 38 | if (location.pathname === to) return; 39 | history.push(to); 40 | }; 41 | 42 | return ( 43 | 48 | }> 49 | {cloneElement(Icon, buttonProps)} 50 | 51 | 52 | ); 53 | }; 54 | IconButton.propTypes = { 55 | style: PropTypes.object, 56 | Icon: PropTypes.element.isRequired, 57 | // eslint-disable-next-line react/no-typos 58 | disabled: PropTypes.bool, 59 | to: PropTypes.string, 60 | // eslint-disable-next-line react/no-typos 61 | onClick: PropTypes.func, 62 | dataTestId: PropTypes.string 63 | }; 64 | 65 | export default IconButton; 66 | -------------------------------------------------------------------------------- /extension/src/components/Buttons/TextButton.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | import { 4 | fieldBackgroundColor, 5 | fieldBackgroundLightColor, 6 | fontSize, 7 | lightTextColor 8 | } from '../../constants/theme'; 9 | import { BUTTON } from '../../constants/sizes'; 10 | 11 | const TextButton = styled.a` 12 | display: flex; 13 | align-items: center; 14 | justify-content: center; 15 | box-sizing: border-box; 16 | 17 | height: ${BUTTON.HEIGHT}px; 18 | width: 100%; 19 | max-width: 270px; 20 | font-size: ${fontSize}; 21 | text-decoration: none; 22 | color: ${lightTextColor}; 23 | background-color: ${fieldBackgroundColor}; 24 | cursor: pointer; 25 | &:hover { 26 | background-color: ${fieldBackgroundLightColor}; 27 | } 28 | `; 29 | 30 | export default TextButton; 31 | -------------------------------------------------------------------------------- /extension/src/components/Containers/Center.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | import { HEADER } from '../../constants/sizes'; 4 | 5 | export default styled.div` 6 | display: flex; 7 | justify-content: center; 8 | align-items: center; 9 | width: 100%; 10 | height: calc(100vh - ${HEADER.HEIGHT}px); 11 | `; 12 | -------------------------------------------------------------------------------- /extension/src/components/ExternalLink.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/jsx-props-no-spreading */ 2 | import React from 'react'; 3 | import PropTypes from 'prop-types'; 4 | import styled from 'styled-components'; 5 | 6 | import { redirectToUrl } from '../utils/browser'; 7 | 8 | const Container = styled.div` 9 | cursor: pointer; 10 | display: flex; 11 | `; 12 | 13 | const ExternalLink = ({ to, children, ...props }) => { 14 | const handleClick = () => { 15 | redirectToUrl(to); 16 | }; 17 | return ( 18 | 19 | {children} 20 | 21 | ); 22 | }; 23 | 24 | ExternalLink.propTypes = { 25 | to: PropTypes.string.isRequired, 26 | children: PropTypes.node.isRequired 27 | }; 28 | 29 | export default ExternalLink; 30 | -------------------------------------------------------------------------------- /extension/src/components/Form/index.js: -------------------------------------------------------------------------------- 1 | import styled, { css } from 'styled-components'; 2 | 3 | import { 4 | backgroundHighlightColor, 5 | lighterTextColor, 6 | smallFontSize, 7 | shadowColor, 8 | fieldMargin, 9 | labelMargin, 10 | indentPadding 11 | } from '../../constants/theme'; 12 | 13 | export const HeaderContainer = styled.div` 14 | display: flex; 15 | flex-direction: column; 16 | z-index: 2; 17 | ${({ hasShadow }) => 18 | hasShadow && 19 | css` 20 | box-shadow: 0px 2px 1px 0px ${shadowColor}; 21 | `} 22 | `; 23 | 24 | export const FormContainer = styled.div` 25 | display: flex; 26 | flex-direction: row; 27 | padding: 0.5rem; 28 | padding-left: ${fieldMargin}px; 29 | `; 30 | 31 | export const FormResultsDescriptionContainer = styled.div` 32 | color: ${lighterTextColor}; 33 | font-size: ${smallFontSize}; 34 | padding: 0 0.25rem 0.25rem ${indentPadding}; 35 | `; 36 | 37 | export const Form = styled.form` 38 | display: flex; 39 | flex-direction: column; 40 | flex: 1; 41 | 42 | .react-select-optimized-option { 43 | &:hover { 44 | background-color: ${backgroundHighlightColor}; 45 | } 46 | } 47 | 48 | .input-field { 49 | &:not(:last-child) { 50 | margin-bottom: ${fieldMargin}px; 51 | } 52 | &.is-technically-last { 53 | margin-bottom: 0 !important; 54 | } 55 | } 56 | `; 57 | 58 | export const Label = styled.label` 59 | font-size: ${smallFontSize}; 60 | color: ${lighterTextColor}; 61 | ${labelMargin} 62 | `; 63 | 64 | export const HideContainer = styled.div` 65 | display: ${({ isHidden }) => (isHidden ? 'none' : 'flex')}; 66 | flex-direction: column; 67 | flex: 1; 68 | `; 69 | 70 | export const ContentContainer = styled.div` 71 | flex: 1; 72 | `; 73 | -------------------------------------------------------------------------------- /extension/src/components/Icon/IconWithBadge.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/prop-types */ 2 | import React from 'react'; 3 | import styled from 'styled-components'; 4 | import { observer } from 'mobx-react-lite'; 5 | 6 | import IconWithSubIcon from './IconWithSubIcon'; 7 | import { smallestFontSize } from '../../constants/theme'; 8 | import { useUiStore } from '../../hooks/store'; 9 | import { isBlank } from '../../utils'; 10 | 11 | const iconPadding = 6; 12 | 13 | const Circle = styled.div` 14 | display: flex; 15 | justify-content: center; 16 | align-items: center; 17 | width: calc(4px + ${iconPadding}px); 18 | height: calc(4px + ${iconPadding}px); 19 | position: absolute; 20 | border-radius: 100%; 21 | background-color: ${({ subIconColor, ...props }) => subIconColor(props)}; 22 | `; 23 | 24 | const Subtext = styled.span` 25 | position: absolute; 26 | font-size: ${smallestFontSize}; 27 | color: ${({ fontColor, ...props }) => fontColor(props)}; 28 | `; 29 | 30 | export default observer( 31 | ({ 32 | Icon, 33 | subtext = '', 34 | subtextContext, 35 | hasBadgeContext, 36 | badgeColor, 37 | fontColor 38 | }) => { 39 | const uiStore = useUiStore(); 40 | const subtextToShow = subtextContext ? uiStore[subtextContext] : subtext; 41 | const hasBadge = hasBadgeContext ? uiStore[hasBadgeContext] : false; 42 | return ( 43 | 48 | {!isBlank(subtextContext) && ( 49 | {subtextToShow} 50 | )} 51 | 52 | ) : ( 53 | <> 54 | ) 55 | } 56 | // Subtext={} 57 | /> 58 | ); 59 | } 60 | ); 61 | -------------------------------------------------------------------------------- /extension/src/components/Icon/IconWithSubIcon.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled, { css } from 'styled-components'; 3 | import PropTypes from 'prop-types'; 4 | import { sidebarInactiveIconColor } from '../../constants/theme'; 5 | 6 | const Container = styled.div` 7 | position: absolute; 8 | `; 9 | 10 | const SubContainer = styled.div` 11 | position: absolute; 12 | display: flex; 13 | justify-content: center; 14 | align-items: center; 15 | bottom: -${({ bottom }) => bottom}px; 16 | right: -${({ right }) => right}px; 17 | 18 | ${({ subIconColor, active, ...props }) => 19 | css` 20 | svg { 21 | fill: ${subIconColor(props)} !important; 22 | opacity: ${active ? 1 : 0.5}; 23 | position: absolute; 24 | } 25 | `} 26 | `; 27 | 28 | const IconWithSubIcon = ({ Icon, SubIcon, offsetY, offsetX }) => { 29 | const { 30 | subIconColor = sidebarInactiveIconColor, 31 | active = false 32 | } = SubIcon.props; 33 | return ( 34 | 35 | {Icon} 36 | 42 | {SubIcon} 43 | 44 | 45 | ); 46 | }; 47 | 48 | IconWithSubIcon.propTypes = { 49 | Icon: PropTypes.node.isRequired, 50 | SubIcon: PropTypes.node.isRequired, 51 | offsetY: PropTypes.number, 52 | offsetX: PropTypes.number 53 | }; 54 | 55 | IconWithSubIcon.defaultProps = { 56 | offsetY: 0, 57 | offsetX: 0 58 | }; 59 | 60 | export default IconWithSubIcon; 61 | -------------------------------------------------------------------------------- /extension/src/components/Icon/Icons.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/prop-types */ 2 | /* eslint-disable import/prefer-default-export */ 3 | /* eslint-disable react/jsx-props-no-spreading */ 4 | import React, { forwardRef } from 'react'; 5 | import styled, { css } from 'styled-components'; 6 | 7 | import { BookmarkFillIcon } from '@primer/octicons-react'; 8 | 9 | const RotatedBookmark = styled(BookmarkFillIcon)` 10 | transform: rotate(-90deg) scaleY(0.7); 11 | 12 | ${({ color }) => 13 | color && 14 | css` 15 | fill: ${color}; 16 | `} 17 | `; 18 | 19 | export const FlagIcon = forwardRef(({ color, ...props }, ref) => { 20 | return ; 21 | }); 22 | -------------------------------------------------------------------------------- /extension/src/components/Icon/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/prop-types */ 2 | import React from 'react'; 3 | 4 | import IconWithSubIcon from './IconWithSubIcon'; 5 | import { FlagIcon } from './Icons'; 6 | import useTheme from '../../hooks/useTheme'; 7 | import { flagIconColor } from '../../constants/theme'; 8 | 9 | // eslint-disable-next-line import/prefer-default-export 10 | export const IconWithFeatureFlag = ({ Icon, active = false }) => { 11 | const { theme: mode } = useTheme(); 12 | const subIconSize = Math.round((Icon.props.size * 5) / 7); 13 | return ( 14 | 22 | } 23 | offsetY={10} 24 | offsetX={5} 25 | /> 26 | ); 27 | }; 28 | -------------------------------------------------------------------------------- /extension/src/components/Image/CircleImage.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { XCircleIcon } from '@primer/octicons-react'; 4 | 5 | import BaseImage from './index'; 6 | 7 | const CircleImage = React.forwardRef((props, ref) => ( 8 | 16 | )); 17 | 18 | CircleImage.propTypes = { 19 | size: PropTypes.number.isRequired, 20 | src: PropTypes.string.isRequired, 21 | alt: PropTypes.string, 22 | PlaceholderIcon: PropTypes.element 23 | }; 24 | 25 | CircleImage.defaultProps = { 26 | alt: 'circle-image', 27 | PlaceholderIcon: 28 | }; 29 | 30 | export default CircleImage; 31 | -------------------------------------------------------------------------------- /extension/src/components/Image/index.js: -------------------------------------------------------------------------------- 1 | import React, { useState, cloneElement } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import styled from 'styled-components'; 4 | import { XIcon } from '@primer/octicons-react'; 5 | import { 6 | fieldBackgroundColor, 7 | fieldBackgroundLightColor 8 | } from '../../constants/theme'; 9 | 10 | const Container = styled.img` 11 | width: 100%; 12 | max-width: ${({ size }) => size}px; 13 | max-height: ${({ size }) => size}px; 14 | border: ${({ borderSize }) => borderSize}px solid ${fieldBackgroundColor}; 15 | border-radius: ${({ borderRadius }) => borderRadius}; 16 | transition: border 100ms; 17 | 18 | &:hover { 19 | border: ${({ borderSize }) => borderSize}px solid 20 | ${fieldBackgroundLightColor}; 21 | } 22 | `; 23 | 24 | const BaseImage = ({ 25 | size, 26 | src, 27 | alt, 28 | PlaceholderIcon, 29 | borderRadius, 30 | borderSize 31 | }) => { 32 | const [error, setError] = useState(false); 33 | return ( 34 | <> 35 | {error ? ( 36 | cloneElement(PlaceholderIcon, { size }) 37 | ) : ( 38 | { 43 | setError(true); 44 | }} 45 | borderRadius={borderRadius} 46 | borderSize={borderSize} 47 | /> 48 | )} 49 | 50 | ); 51 | }; 52 | 53 | BaseImage.propTypes = { 54 | size: PropTypes.number.isRequired, 55 | src: PropTypes.string.isRequired, 56 | alt: PropTypes.string, 57 | PlaceholderIcon: PropTypes.element, 58 | borderRadius: PropTypes.string, 59 | borderSize: PropTypes.number 60 | }; 61 | 62 | BaseImage.defaultProps = { 63 | alt: 'circle-image', 64 | PlaceholderIcon: , 65 | borderRadius: '0', 66 | borderSize: 0 67 | }; 68 | 69 | export default BaseImage; 70 | -------------------------------------------------------------------------------- /extension/src/components/Loading/Spinner.js: -------------------------------------------------------------------------------- 1 | // https://gist.github.com/adrianmcli/9fac3ff3c144c2805be90381eaa8d3d4 2 | import styled, { keyframes } from 'styled-components'; 3 | import { nodeIconColor, nodeIconDarkColor } from '../../constants/theme'; 4 | 5 | const rotate360 = keyframes` 6 | from { 7 | transform: rotate(0deg); 8 | } 9 | to { 10 | transform: rotate(360deg); 11 | } 12 | `; 13 | 14 | const Spinner = styled.div` 15 | animation: ${rotate360} 0.5s linear infinite; 16 | transform: translateZ(0); 17 | 18 | border-top: 2px solid ${nodeIconColor}; 19 | border-right: 2px solid ${nodeIconColor}; 20 | border-bottom: 2px solid ${nodeIconColor}; 21 | border-left: 3px solid ${nodeIconDarkColor}; 22 | background: transparent; 23 | width: ${({ size }) => size || 16}px; 24 | height: ${({ size }) => size || 16}px; 25 | border-radius: 50%; 26 | 27 | margin-left: ${({ marginLeft }) => marginLeft || 0}; 28 | margin-right: ${({ marginRight }) => marginRight || 0}; 29 | `; 30 | 31 | export default Spinner; 32 | -------------------------------------------------------------------------------- /extension/src/components/Node/SearchResultMatchNode.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useRef } from 'react'; 2 | import { observer } from 'mobx-react-lite'; 3 | import PropTypes from 'prop-types'; 4 | import { LinkExternalIcon } from '@primer/octicons-react'; 5 | 6 | import StyledNode from './Base.style'; 7 | import { clickedEl } from './util'; 8 | import { redirectToUrl } from '../../utils/browser'; 9 | import useTheme from '../../hooks/useTheme'; 10 | import { ICON } from '../../constants/sizes'; 11 | 12 | const SearchResultMatchNode = observer( 13 | ({ fragment, indices: { start, end }, url }) => { 14 | const { spacing } = useTheme(); 15 | const linkEl = useRef(null); 16 | const [showNewTab, setShowNewTab] = useState(false); 17 | 18 | const handleClick = (e) => { 19 | e.stopPropagation(); 20 | e.preventDefault(); 21 | 22 | // Open search match file in new tab 23 | if (clickedEl(linkEl, e)) { 24 | redirectToUrl(url); 25 | } 26 | }; 27 | 28 | return ( 29 | setShowNewTab(true)} 33 | onMouseLeave={() => setShowNewTab(false)} 34 | noPointer 35 | > 36 | 37 | 38 | 39 | {fragment.slice(0, start)} 40 | {fragment.slice(start, end)} 41 | {fragment.slice(end)} 42 | 43 | 44 | 45 | 46 | {showNewTab && ( 47 | 48 | 52 | 53 | )} 54 | 55 | 56 | ); 57 | } 58 | ); 59 | 60 | SearchResultMatchNode.propTypes = { 61 | fragment: PropTypes.string, 62 | indices: PropTypes.shape({ 63 | start: PropTypes.number, 64 | end: PropTypes.number 65 | }), 66 | url: PropTypes.string 67 | }; 68 | 69 | export default SearchResultMatchNode; 70 | -------------------------------------------------------------------------------- /extension/src/components/Node/TreeOrBlobNode.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | import File from './File'; 5 | // eslint-disable-next-line import/no-cycle 6 | import Folder from './Folder'; 7 | 8 | const TreeOrBlobNode = ({ owner, repo, branch, data, level }) => { 9 | const isFile = data.type === 'blob'; 10 | return ( 11 | <> 12 | {isFile ? ( 13 | 20 | ) : ( 21 | 28 | )} 29 | 30 | ); 31 | }; 32 | 33 | TreeOrBlobNode.propTypes = { 34 | owner: PropTypes.string.isRequired, 35 | repo: PropTypes.string.isRequired, 36 | branch: PropTypes.shape({ 37 | name: PropTypes.string.isRequired, 38 | tabId: PropTypes.number.isRequired, 39 | type: PropTypes.oneOf(['blob', 'tree']).isRequired 40 | }).isRequired, 41 | data: PropTypes.shape({ 42 | oid: PropTypes.string.isRequired, 43 | name: PropTypes.string.isRequired, 44 | type: PropTypes.oneOf(['blob', 'tree']).isRequired, 45 | path: PropTypes.string.isRequired 46 | }).isRequired, 47 | level: PropTypes.number 48 | }; 49 | 50 | TreeOrBlobNode.defaultProps = { 51 | level: 0 52 | }; 53 | 54 | export default TreeOrBlobNode; 55 | -------------------------------------------------------------------------------- /extension/src/components/OpenCloseChevron.js: -------------------------------------------------------------------------------- 1 | import React, { cloneElement, forwardRef } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import styled, { css } from 'styled-components'; 4 | import { ChevronRightIcon } from '@primer/octicons-react'; 5 | import { nodeIconColor, fieldBackgroundColor } from '../constants/theme'; 6 | import { ICON } from '../constants/sizes'; 7 | import useTheme from '../hooks/useTheme'; 8 | 9 | const Container = styled.div` 10 | display: flex; 11 | justify-content: center; 12 | align-items: center; 13 | cursor: pointer; 14 | margin-right: ${ICON.SIDE_MARGIN}px; 15 | 16 | svg { 17 | fill: ${nodeIconColor}; 18 | transition: transform 100ms; 19 | transform: rotate(${({ startDeg }) => startDeg}deg); 20 | ${({ noRotate }) => 21 | !noRotate && 22 | css` 23 | transform: rotate( 24 | ${({ startDeg, open }) => (open ? 90 + startDeg : startDeg)}deg 25 | ); 26 | `} 27 | } 28 | 29 | ${({ highlightOnHover }) => 30 | highlightOnHover && 31 | css` 32 | &:hover { 33 | background-color: ${fieldBackgroundColor}; 34 | } 35 | `} 36 | `; 37 | 38 | const OpenCloseChevron = forwardRef( 39 | ({ open, onClick, highlightOnHover, Icon, noRotate, startDeg }, ref) => { 40 | const { spacing } = useTheme(); 41 | return ( 42 | 50 | {cloneElement(Icon, { 51 | size: ICON.SIZE({ theme: { spacing } }), 52 | verticalAlign: 'middle' 53 | })} 54 | 55 | ); 56 | } 57 | ); 58 | 59 | OpenCloseChevron.propTypes = { 60 | open: PropTypes.bool, 61 | onClick: PropTypes.func, 62 | highlightOnHover: PropTypes.bool, 63 | Icon: PropTypes.node, 64 | startDeg: PropTypes.number, 65 | noRotate: PropTypes.bool 66 | }; 67 | 68 | OpenCloseChevron.defaultProps = { 69 | open: false, 70 | onClick: () => {}, 71 | highlightOnHover: false, 72 | Icon: , 73 | startDeg: 0, 74 | noRotate: false 75 | }; 76 | 77 | export default OpenCloseChevron; 78 | -------------------------------------------------------------------------------- /extension/src/components/ResizableSidebar/ActionButtons/Collapse.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { FoldIcon } from '@primer/octicons-react'; 3 | import { observer } from 'mobx-react-lite'; 4 | 5 | import { Icon } from '../../Node/Base.style'; 6 | import { useFileStore } from '../../../hooks/store'; 7 | import useTheme from '../../../hooks/useTheme'; 8 | import { ICON } from '../../../constants/sizes'; 9 | 10 | const Collapse = observer(() => { 11 | const { closeAllNodesBelow, currentBranch } = useFileStore(); 12 | const { spacing } = useTheme(); 13 | 14 | const collapseFiles = () => { 15 | if (!currentBranch) return; 16 | closeAllNodesBelow( 17 | currentBranch?.repo?.owner, 18 | currentBranch?.repo?.name, 19 | currentBranch, 20 | '' 21 | ); 22 | }; 23 | 24 | return ( 25 | 30 | 34 | 35 | ); 36 | }); 37 | 38 | export default Collapse; 39 | -------------------------------------------------------------------------------- /extension/src/components/ResizableSidebar/ActionButtons/DistractionFreeMode.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { EyeIcon, EyeClosedIcon } from '@primer/octicons-react'; 3 | import { observer } from 'mobx-react-lite'; 4 | 5 | import { Icon } from '../../Node/Base.style'; 6 | import { useUiStore, useUserStore } from '../../../hooks/store'; 7 | import useTheme from '../../../hooks/useTheme'; 8 | import { ICON } from '../../../constants/sizes'; 9 | import { toggleDistractionFreeMode } from './util'; 10 | 11 | const DistractionFree = observer(() => { 12 | const { user } = useUserStore(); 13 | const { isDistractionFreeMode, setIsDistractionFreeMode } = useUiStore(); 14 | const { spacing } = useTheme(); 15 | 16 | const onClick = () => { 17 | toggleDistractionFreeMode(!isDistractionFreeMode); 18 | setIsDistractionFreeMode(!isDistractionFreeMode); 19 | }; 20 | 21 | const isDisabled = !['professional', 'enterprise'].includes( 22 | user?.accountType 23 | ); 24 | 25 | return ( 26 | 36 | {!isDisabled && isDistractionFreeMode ? ( 37 | 41 | ) : ( 42 | 46 | )} 47 | 48 | ); 49 | }); 50 | 51 | export default DistractionFree; 52 | -------------------------------------------------------------------------------- /extension/src/components/ResizableSidebar/ActionButtons/util.js: -------------------------------------------------------------------------------- 1 | import browser from 'webextension-polyfill'; 2 | import cloneDeep from 'lodash.clonedeep'; 3 | 4 | import log from '../../../config/log'; 5 | 6 | // eslint-disable-next-line import/prefer-default-export 7 | export const toggleDistractionFreeMode = (isDistractionFreeMode) => { 8 | const request = cloneDeep({ 9 | action: 'distraction-free', 10 | payload: { isDistractionFreeMode } 11 | }); 12 | log.toBg('Redirect request -> bg', request); 13 | browser.runtime.sendMessage(request); 14 | }; 15 | -------------------------------------------------------------------------------- /extension/src/components/ResizableSidebar/util.js: -------------------------------------------------------------------------------- 1 | import { sidebarRoutes } from '../../config/routes'; 2 | 3 | export const getSidebarHeaderTitle = (pathname) => { 4 | return sidebarRoutes.find((r) => r.pathname === pathname)?.title || ''; 5 | }; 6 | 7 | // Certain pages are blocklisted, like account sign in, minimized, and pages 8 | // behind feature flags 9 | export const isPageWithHeader = (pathname) => 10 | !['/account-sign-in', '/minimized', '/vcs'].includes(pathname); 11 | -------------------------------------------------------------------------------- /extension/src/components/Scrollbars.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | 4 | const Scrollbars = styled.div` 5 | width: 100%; 6 | height: ${({ height }) => height || '100%'}; 7 | overflow: scroll; 8 | scrollbar-width: thin; 9 | `; 10 | 11 | // Wrapper for Scrollbars 12 | // eslint-disable-next-line react/prop-types 13 | export default ({ height, children, ...props }) => { 14 | return ( 15 | 19 | {children} 20 | 21 | ); 22 | }; 23 | -------------------------------------------------------------------------------- /extension/src/components/Section/index.js: -------------------------------------------------------------------------------- 1 | import styled, { css } from 'styled-components'; 2 | import { ICON, NODE } from '../../constants/sizes'; 3 | 4 | import { 5 | backgroundAlternatingDarkColor, 6 | backgroundAlternatingLightColor, 7 | borderColor, 8 | fontSize, 9 | indentPadding, 10 | shadowColor 11 | } from '../../constants/theme'; 12 | 13 | export const SectionContainer = styled.div` 14 | display: flex; 15 | overflow: hidden; 16 | flex-direction: column; 17 | height: 100%; 18 | user-select: none; 19 | transition: 0.2s height; 20 | box-sizing: border-box; 21 | z-index: 1; 22 | `; 23 | 24 | export const SectionNameContainer = styled.div` 25 | display: flex; 26 | flex-direction: row; 27 | cursor: pointer; 28 | padding-left: calc(${indentPadding} - ${ICON.SIZE}px - ${ICON.SIDE_MARGIN}px); 29 | z-index: ${({ zIndex }) => zIndex}; 30 | height: ${NODE.HEIGHT}px; 31 | max-height: ${NODE.HEIGHT}px; 32 | 33 | border-top: 1px solid ${borderColor}; 34 | ${({ hasShadow }) => 35 | hasShadow && 36 | css` 37 | box-shadow: 0px 2px 1px 0px ${shadowColor}; 38 | `} 39 | `; 40 | 41 | export const SectionName = styled.div` 42 | text-transform: uppercase; 43 | text-overflow: ellipsis; 44 | white-space: nowrap; 45 | font-size: ${fontSize}; 46 | line-height: ${NODE.HEIGHT}px; 47 | font-weight: bold; 48 | user-select: none; 49 | overflow: hidden; 50 | `; 51 | 52 | export const SectionContent = styled.div` 53 | display: flex; 54 | flex-direction: column; 55 | left: 0; 56 | right: 0; 57 | min-width: fit-content; 58 | height: 100%; 59 | 60 | div.node:nth-of-type(even) { 61 | background-color: ${backgroundAlternatingLightColor}; 62 | } 63 | div.node:nth-of-type(odd) { 64 | background-color: ${backgroundAlternatingDarkColor}; 65 | } 66 | `; 67 | -------------------------------------------------------------------------------- /extension/src/components/Toast/Container.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import styled from 'styled-components'; 3 | import { observer } from 'mobx-react-lite'; 4 | 5 | import Toast from './Toast'; 6 | import { useUiStore } from '../../hooks/store'; 7 | 8 | const Container = styled.div` 9 | position: absolute; 10 | bottom: 0; 11 | display: flex; 12 | flex-direction: column-reverse; 13 | width: 100%; 14 | `; 15 | 16 | const ToastContainer = observer(() => { 17 | const { pendingNotifications, popPendingNotifications } = useUiStore(); 18 | const [localNotifications, setLocalNotifications] = useState([]); 19 | 20 | const removeNotification = (notification) => { 21 | setLocalNotifications((prevNs) => 22 | prevNs.filter((n) => n.id !== notification.id) 23 | ); 24 | }; 25 | 26 | const addNotification = (notification) => { 27 | setLocalNotifications((prevNs) => [...prevNs, notification]); 28 | 29 | // set a timeout to remove each notification 30 | setTimeout(() => { 31 | removeNotification(notification); 32 | }, 3000); 33 | }; 34 | 35 | // Drain pendingNotifications by creating a notification for each one 36 | useEffect(() => { 37 | if (pendingNotifications.size !== 0) { 38 | const notification = popPendingNotifications(); 39 | addNotification(notification); 40 | } 41 | }, [pendingNotifications.size]); 42 | 43 | return ( 44 | 45 | {localNotifications && 46 | localNotifications.map((n) => ( 47 | 52 | ))} 53 | 54 | ); 55 | }); 56 | 57 | export default ToastContainer; 58 | -------------------------------------------------------------------------------- /extension/src/components/Toast/Toast.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | import PropTypes from 'prop-types'; 4 | import { XIcon } from '@primer/octicons-react'; 5 | 6 | import { 7 | contrastTextColor, 8 | fontSize, 9 | notificationTypeToColor 10 | } from '../../constants/theme'; 11 | import { 12 | nodePaddingY, 13 | nodePaddingX, 14 | RightIconContainer, 15 | Icon, 16 | MiddleSpacer 17 | } from '../Node/Base.style'; 18 | import useTheme from '../../hooks/useTheme'; 19 | import { ICON } from '../../constants/sizes'; 20 | 21 | const Container = styled.div` 22 | display: flex; 23 | flex-direction: row; 24 | box-sizing: border-box; 25 | ${nodePaddingX}; 26 | padding-top: 0; 27 | padding-bottom: 0; 28 | z-index: 10; 29 | 30 | background-color: ${({ type, ...props }) => 31 | notificationTypeToColor[type](props)}; 32 | 33 | .title { 34 | } 35 | 36 | .message { 37 | font-size: ${fontSize}; 38 | color: ${contrastTextColor}; 39 | ${nodePaddingY}; 40 | overflow-x: hidden; 41 | white-space: nowrap; 42 | text-overflow: ellipsis; 43 | } 44 | 45 | svg { 46 | fill: ${contrastTextColor}; 47 | } 48 | `; 49 | 50 | const Toast = ({ notification, removeNotification }) => { 51 | const { type, message } = notification; 52 | const { spacing } = useTheme(); 53 | 54 | const handleClick = () => { 55 | removeNotification(notification); 56 | }; 57 | 58 | return ( 59 | 60 |
{message}
61 | 62 | 63 | 64 | 68 | 69 | 70 |
71 | ); 72 | }; 73 | 74 | Toast.propTypes = { 75 | notification: PropTypes.shape({ 76 | id: PropTypes.string, 77 | type: PropTypes.string, 78 | message: PropTypes.string 79 | }).isRequired, 80 | removeNotification: PropTypes.func 81 | }; 82 | 83 | Toast.defaultProps = { 84 | removeNotification: () => {} 85 | }; 86 | 87 | export default Toast; 88 | -------------------------------------------------------------------------------- /extension/src/config/browser.js: -------------------------------------------------------------------------------- 1 | import Bowser from 'bowser'; 2 | import { CHROMIUM } from '../global/limits/constants'; 3 | 4 | // eslint-disable-next-line import/prefer-default-export 5 | export const browserName = Bowser.getParser(window.navigator.userAgent) 6 | .getBrowser() 7 | .name.toLowerCase(); 8 | 9 | export const isChromium = CHROMIUM.includes(browserName); 10 | -------------------------------------------------------------------------------- /extension/src/config/dao/queries.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/prefer-default-export */ 2 | export const getUserBookmarks = /* GraphQL */ ` 3 | query GetUserBookmarks($userId: ID!) { 4 | getUser(id: $userId) { 5 | bookmarks { 6 | items { 7 | id 8 | userId 9 | name 10 | path 11 | pinned 12 | branch 13 | repo 14 | owner 15 | createdAt 16 | updatedAt 17 | owner 18 | } 19 | nextToken 20 | } 21 | } 22 | } 23 | `; 24 | -------------------------------------------------------------------------------- /extension/src/config/log.js: -------------------------------------------------------------------------------- 1 | import browser from 'webextension-polyfill'; 2 | // eslint-disable-next-line import/no-cycle 3 | import { isProduction } from '../utils'; 4 | 5 | const logWrapper = (styler, msg, ...args) => { 6 | if (!isProduction()) { 7 | browser.extension 8 | .getBackgroundPage() 9 | .console.log(`%c${msg}`, styler, ...args); 10 | } 11 | }; 12 | 13 | // used like log.error(), log(), etc. 14 | export default { 15 | log: (msg, ...args) => { 16 | logWrapper('color: black; background-color: white;', msg, ...args); 17 | }, 18 | error: (msg, ...args) => { 19 | logWrapper( 20 | 'font-weight: bold; color: #470000; background-color: #ff9494;', 21 | msg, 22 | ...args 23 | ); 24 | }, 25 | warn: (msg, ...args) => { 26 | logWrapper( 27 | 'font-weight: bold; color: #523313; background-color: #ffad57;', 28 | msg, 29 | ...args 30 | ); 31 | }, 32 | debug: (msg, ...args) => { 33 | logWrapper( 34 | 'font-weight: bold; color: #410e42; background-color: #fd91ff;', 35 | msg, 36 | ...args 37 | ); 38 | }, 39 | toBg: (msg, ...args) => { 40 | logWrapper( 41 | 'font-weight: bold; color: #0d4f20; background-color: #96ffb4;', 42 | msg, 43 | ...args 44 | ); 45 | }, 46 | fromBg: (msg, ...args) => { 47 | logWrapper( 48 | 'font-weight: bold; color: #100e5c; background-color: #aaa8f7;', 49 | msg, 50 | ...args 51 | ); 52 | }, 53 | apiRead: (msg, ...args) => { 54 | logWrapper( 55 | 'font-weight: bold; color: white; background-color: #100e5c;', 56 | msg, 57 | ...args 58 | ); 59 | }, 60 | apiWrite: (msg, ...args) => { 61 | logWrapper( 62 | 'font-weight: bold; color: white; background-color: #0d4f20;', 63 | msg, 64 | ...args 65 | ); 66 | } 67 | }; 68 | -------------------------------------------------------------------------------- /extension/src/config/octokit/queries.js: -------------------------------------------------------------------------------- 1 | export const formQueryGetRepositorySpecificBranchRootNodes = ( 2 | branch, 3 | pathToFolder = '' 4 | ) => ` 5 | query GetRepositorySpecificBranchRootNodes($owner: String!, $repo: String!) { 6 | repository(owner: $owner, name: $repo) { 7 | defaultBranchRef { 8 | name 9 | } 10 | object(expression: "${branch}:${pathToFolder}") { 11 | ... on Tree { 12 | entries { 13 | name 14 | type 15 | path 16 | oid 17 | } 18 | } 19 | } 20 | } 21 | } 22 | `; 23 | 24 | export const formSearchQuery = ( 25 | owner, 26 | repo, 27 | queryFilename, 28 | queryCode, 29 | queryPath, 30 | language 31 | ) => { 32 | let baseQuery = `repo:${owner}/${repo}`; 33 | if (queryFilename) { 34 | baseQuery += `+filename:${queryFilename}`; 35 | } 36 | if (queryCode) { 37 | baseQuery += `+in:file+${queryCode}`; 38 | } 39 | if (queryPath) { 40 | baseQuery += `+path:${queryPath}`; 41 | } 42 | if (language) { 43 | baseQuery += `+language:${language}`; 44 | } 45 | return baseQuery; 46 | }; 47 | 48 | export const placeholder = 'placeholder'; 49 | -------------------------------------------------------------------------------- /extension/src/config/store/I.root.store.ts: -------------------------------------------------------------------------------- 1 | import IUiStore from './I.ui.store'; 2 | import IFileStore from './I.file.store'; 3 | import IUserStore from './I.user.store'; 4 | 5 | export default interface IRootStore { 6 | uiStore: IUiStore; 7 | fileStore: IFileStore; 8 | userStore: IUserStore; 9 | } 10 | -------------------------------------------------------------------------------- /extension/src/config/store/I.user.store.ts: -------------------------------------------------------------------------------- 1 | import IUiStore from './I.ui.store'; 2 | import IFileStore, { Repo, Session } from './I.file.store'; 3 | import { ACCOUNT_TYPE } from '../../global/constants'; 4 | 5 | export interface User { 6 | uid: string; 7 | displayName: string; 8 | photoURL: string; 9 | apiKey: string; 10 | accountType: ACCOUNT_TYPE; 11 | email: string; 12 | isTrial: boolean; 13 | } 14 | 15 | export default interface IUserStore { 16 | uiStore: IUiStore; 17 | fileStore: IFileStore; 18 | 19 | user: User; 20 | userBookmarks: Map; 21 | numOfBookmarks: number; // Used for triggering ui changes 22 | userSessions: Map; 23 | numOfSessions: number; // Used for triggering ui changes 24 | isPending: boolean; 25 | error: Error; 26 | } 27 | -------------------------------------------------------------------------------- /extension/src/config/store/context.ts: -------------------------------------------------------------------------------- 1 | import { createContext } from 'react'; 2 | import rootStore from './root.store'; 3 | 4 | export default createContext(rootStore); 5 | -------------------------------------------------------------------------------- /extension/src/config/store/root.store.ts: -------------------------------------------------------------------------------- 1 | import IRootStore from './I.root.store'; 2 | import UiStore from './ui.store'; 3 | import IUiStore from './I.ui.store'; 4 | import FileStore from './file.store'; 5 | import IFileStore from './I.file.store'; 6 | import UserStore from './user.store'; 7 | import IUserStore from './I.user.store'; 8 | 9 | export class RootStore implements IRootStore { 10 | uiStore: IUiStore; 11 | fileStore: IFileStore; 12 | userStore: IUserStore; 13 | 14 | constructor() { 15 | this.uiStore = new UiStore(this); 16 | this.userStore = new UserStore(this); 17 | this.fileStore = new FileStore(this); 18 | } 19 | } 20 | 21 | const rootStore: IRootStore = new RootStore(); 22 | 23 | export default rootStore; 24 | -------------------------------------------------------------------------------- /extension/src/config/store/util.ts: -------------------------------------------------------------------------------- 1 | import { browser } from 'webextension-polyfill-ts'; 2 | import { BgRepo, Repo, Tab } from './I.file.store'; 3 | import log from '../log'; 4 | 5 | const clone = (obj: { [key: string]: any }) => { 6 | return JSON.parse(JSON.stringify(obj)); 7 | }; 8 | 9 | export const setInChromeStorage = (object: { [key: string]: any }) => { 10 | browser.runtime.sendMessage(clone({ action: 'set-store', payload: object })); 11 | }; 12 | 13 | export const getFromChromeStorage = async ( 14 | keys: string[], 15 | callback: Function 16 | ) => { 17 | try { 18 | const request = { 19 | action: 'get-store', 20 | payload: keys 21 | }; 22 | log.toBg('Get store request -> bg', request); 23 | const response = await browser.runtime.sendMessage(clone(request)); 24 | if (response?.payload) { 25 | callback(response.payload); 26 | } 27 | } catch (error) { 28 | log.error('Error getting store', keys, error); 29 | } 30 | }; 31 | 32 | export const convertBgRepoToRepo = ( 33 | bgRepo: BgRepo, 34 | populate: boolean = true 35 | ): Repo => { 36 | return { 37 | owner: bgRepo.owner, 38 | name: bgRepo.repo, 39 | type: bgRepo.type, 40 | defaultBranch: bgRepo.defaultBranch, 41 | ...(populate && { 42 | tabs: { 43 | [bgRepo.tab.nodeName || bgRepo.tab.subpage]: { 44 | name: bgRepo.tab.name, 45 | tabId: bgRepo.tab.tabId, 46 | nodeName: bgRepo.tab.nodeName, 47 | subpage: bgRepo.tab.subpage 48 | } 49 | } 50 | }) 51 | }; 52 | }; 53 | 54 | export const convertBgRepoToTabs = (bgRepos: BgRepo[]): Tab[] => { 55 | return bgRepos.map((bgRepo: BgRepo) => ({ 56 | name: bgRepo.tab.name, 57 | tabId: bgRepo.tab.tabId, 58 | nodeName: bgRepo.tab.nodeName, 59 | subpage: bgRepo.tab.subpage, 60 | repo: convertBgRepoToRepo(bgRepo, false) 61 | })); 62 | }; 63 | -------------------------------------------------------------------------------- /extension/src/config/theme/context.js: -------------------------------------------------------------------------------- 1 | import React, { createContext } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { ThemeProvider as SCThemeProvider } from 'styled-components'; 4 | import { observer } from 'mobx-react-lite'; 5 | 6 | import './fonts.css'; 7 | import GlobalStyle from './global.style'; 8 | import { useUiStore } from '../../hooks/store'; 9 | 10 | export const ThemeToggleContext = createContext(); 11 | 12 | export const ThemeProvider = observer(({ children }) => { 13 | const { theme, spacing } = useUiStore(); 14 | 15 | return ( 16 | 17 | 18 | {children} 19 | 20 | ); 21 | }); 22 | 23 | ThemeProvider.propTypes = { 24 | children: PropTypes.node.isRequired 25 | }; 26 | 27 | export default ThemeProvider; 28 | -------------------------------------------------------------------------------- /extension/src/config/theme/fonts.css: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css?family=Source+Sans+Pro:regular,bold,italic&subset=latin,latin-ext'); 2 | -------------------------------------------------------------------------------- /extension/src/config/theme/global.style.js: -------------------------------------------------------------------------------- 1 | import { createGlobalStyle } from 'styled-components'; 2 | 3 | export default createGlobalStyle` 4 | html, body { 5 | font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, 6 | Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; 7 | } 8 | `; 9 | -------------------------------------------------------------------------------- /extension/src/config/theme/selector.js: -------------------------------------------------------------------------------- 1 | import { kebabify, objectMap } from '../../utils'; 2 | import * as themes from './themes'; 3 | 4 | const THEMES = objectMap( 5 | themes, 6 | (k, v) => v.name, 7 | (v) => ({ ...v.theme, type: v.type }) 8 | ); 9 | 10 | export const THEME_NAMES = Object.keys(THEMES); 11 | 12 | // Returns a object with key-value for each theme and property 13 | // [themeName]: [propertyValue] 14 | const getThemesForProperty = (key) => { 15 | return objectMap( 16 | THEMES, 17 | (k) => kebabify(k), 18 | (v) => v[key] 19 | ); 20 | }; 21 | 22 | export default getThemesForProperty; 23 | -------------------------------------------------------------------------------- /extension/src/config/theme/themes.js: -------------------------------------------------------------------------------- 1 | export * as vanillaLight from '../../themes/vanilla.light.json'; 2 | export * as vanillaDark from '../../themes/vanilla.dark.json'; 3 | export * as monokaiDark from '../../themes/monokai.dark.json'; 4 | export * as materialLight from '../../themes/material.light.json'; 5 | export * as materialDark from '../../themes/material.dark.json'; 6 | export * as onedarkDark from '../../themes/onedark.dark.json'; 7 | export * as janesThemeLight from '../../themes/janestheme.light.json'; 8 | export * as draculaDark from '../../themes/dracula.dark.json'; 9 | -------------------------------------------------------------------------------- /extension/src/config/theme/utils.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-bitwise */ 2 | /* eslint-disable prefer-const */ 3 | /* eslint-disable no-param-reassign */ 4 | 5 | export const isThemeDark = (themeType) => themeType === 'dark'; 6 | 7 | // https://css-tricks.com/snippets/javascript/lighten-darken-color/ 8 | export const lightenDarkenColor = (col, amt) => { 9 | let usePound = false; 10 | if (col[0] === '#') { 11 | col = col.slice(1); 12 | usePound = true; 13 | } 14 | let num = parseInt(col, 16); 15 | let r = (num >> 16) + amt; 16 | if (r > 255) r = 255; 17 | else if (r < 0) r = 0; 18 | let b = ((num >> 8) & 0x00ff) + amt; 19 | if (b > 255) b = 255; 20 | else if (b < 0) b = 0; 21 | let g = (num & 0x0000ff) + amt; 22 | if (g > 255) g = 255; 23 | else if (g < 0) g = 0; 24 | return (usePound ? '#' : '') + (g | (b << 8) | (r << 16)).toString(16); 25 | }; 26 | 27 | // Lightens or darkens a color depending on whether theme is light or dark 28 | export const highlightColor = (col, themeType) => { 29 | if (isThemeDark(themeType)) { 30 | return lightenDarkenColor(col, 20); // lighten 31 | } 32 | return lightenDarkenColor(col, -20); // darken 33 | }; 34 | -------------------------------------------------------------------------------- /extension/src/constants/colors.js: -------------------------------------------------------------------------------- 1 | export const GITHUB_COLOR = '#211F1F'; 2 | 3 | export const WHITE = '#FFFFFF'; 4 | 5 | export const BLACK = '#000000'; 6 | -------------------------------------------------------------------------------- /extension/src/constants/sizes.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Global Sizes 3 | */ 4 | import theme from 'styled-theming'; 5 | 6 | export const EXTENSION_WIDTH = { 7 | INITIAL: 350, 8 | MIN: 150, 9 | MAX: 500 10 | }; 11 | 12 | /** 13 | * Sidebar Sizes 14 | */ 15 | 16 | export const SIDE_TAB = { 17 | WIDTH: 50, 18 | BUTTON: { HEIGHT: 55, HIGHLIGHT_WIDTH: 3 } 19 | }; 20 | 21 | export const HEADER = { 22 | HEIGHT: 40 23 | }; 24 | 25 | export const NODE = { 26 | HEIGHT: theme('spacing', { 27 | compact: 19, 28 | cozy: 25, 29 | comfortable: 30 30 | }) 31 | }; 32 | 33 | export const ICON = { 34 | SIZE: theme('spacing', { 35 | compact: 14, 36 | cozy: 16, 37 | comfortable: 18 38 | }), 39 | SIDE_MARGIN: theme('spacing', { 40 | compact: 3, 41 | cozy: 4, 42 | comfortable: 5 43 | }), 44 | PROFILE_IMAGE: { 45 | SIZE: theme('spacing', { 46 | compact: 150, 47 | cozy: 160, 48 | comfortable: 170 49 | }) 50 | }, 51 | SPLASH: { 52 | SIZE: theme('spacing', { 53 | compact: 115, 54 | cozy: 120, 55 | comfortable: 128 56 | }) 57 | } 58 | }; 59 | 60 | export const INPUT = { 61 | SELECT: { 62 | HEIGHT: theme('spacing', { 63 | compact: 28, 64 | cozy: 32, 65 | comfortable: 35 66 | }), 67 | OPTION: { 68 | HEIGHT: theme('spacing', { 69 | compact: 18, 70 | cozy: 21, 71 | comfortable: 24 72 | }) 73 | } 74 | } 75 | }; 76 | 77 | export const BUTTON = { 78 | HEIGHT: theme('spacing', { 79 | compact: 30, 80 | cozy: 35, 81 | comfortable: 40 82 | }), 83 | BIG_HEIGHT: theme('spacing', { 84 | compact: 40, 85 | cozy: 45, 86 | comfortable: 50 87 | }), 88 | SIDE_PADDING: theme('spacing', { 89 | compact: 10, 90 | cozy: 15, 91 | comfortable: 20 92 | }) 93 | }; 94 | 95 | export const RESIZE_GUTTER = { 96 | HEIGHT: 5 97 | }; 98 | 99 | export const TAB_HEIGHT = 50; 100 | 101 | /** 102 | * Tabbar Sizes 103 | */ 104 | 105 | export const TOP_TAB = { 106 | HEIGHT: 50 107 | }; 108 | -------------------------------------------------------------------------------- /extension/src/content-scripts/index.js: -------------------------------------------------------------------------------- 1 | import { isGithubRepoUrl } from './util'; 2 | 3 | const initListeners = () => {}; 4 | 5 | const checkUrl = () => { 6 | // eslint-disable-next-line no-restricted-globals 7 | const isGRUrl = isGithubRepoUrl(location.href); 8 | if (isGRUrl) { 9 | initListeners(); 10 | } 11 | }; 12 | 13 | checkUrl(); 14 | -------------------------------------------------------------------------------- /extension/src/content-scripts/util.js: -------------------------------------------------------------------------------- 1 | import { DEFAULT_BRANCH } from '../global/constants'; 2 | 3 | const GITHUB_REGEX = new RegExp(/^(http|https):\/\/github\.com(\/[^/]+){2,}$/); 4 | 5 | export const isGithubRepoUrl = (url) => { 6 | return url && !!GITHUB_REGEX.exec(url); 7 | }; 8 | 9 | export const parseUrl = (url, title) => { 10 | if (!isGithubRepoUrl(url)) { 11 | return null; 12 | } 13 | 14 | const urlObject = new URL(url); 15 | 16 | // [alexkim205, tomaso, tree?/blob?, filePath?] 17 | const parsedRepoInfo = urlObject.pathname.slice(1).split('/'); 18 | 19 | let payloadRepoInfo = {}; 20 | const isAtRootAndMaster = parsedRepoInfo.length === 2; 21 | 22 | if (isAtRootAndMaster) { 23 | payloadRepoInfo = { 24 | url, 25 | owner: parsedRepoInfo[0], 26 | repo: parsedRepoInfo[1], 27 | branch: DEFAULT_BRANCH, 28 | type: 'tree', 29 | file: null 30 | }; 31 | } else { 32 | // tabTitle looks like "G-Desktop-Suite/gsuite.rb at revert-68-code-quality-66-prettify · alexkim205/G-Desktop-Suite" 33 | // Get branch information from tab title, because it's impossible to discern from 34 | // just the url if the branch name has /'s 35 | const branch = title.match(new RegExp(' at (.*) · '))[1]; // get everything in between " at " and " . " 36 | const parsedWithoutBranch = urlObject.pathname 37 | .slice(1) 38 | .replace(`/${branch}`, '') // remove branch from url to get 39 | .split('/'); // [alexkim205, tomaso, tree?/blob?, filePath?] 40 | payloadRepoInfo = { 41 | url, 42 | owner: parsedRepoInfo[0], 43 | repo: parsedRepoInfo[1], 44 | branch, 45 | type: parsedWithoutBranch[2], 46 | file: parsedWithoutBranch[3] 47 | }; 48 | } 49 | 50 | return payloadRepoInfo; 51 | }; 52 | -------------------------------------------------------------------------------- /extension/src/global/constants.ts: -------------------------------------------------------------------------------- 1 | export enum ACCOUNT_TYPE { 2 | Community = 'community', 3 | Professional = 'professional', 4 | Enterprise = 'enterprise' 5 | } 6 | 7 | export enum SETTING_TYPE { 8 | Theme = 'theme', 9 | Spacing = 'spacing', 10 | StickyWindow = 'stickywindow', 11 | SidebarSide = 'sidebarside' 12 | } 13 | 14 | export enum SIDEBAR_SIDE { 15 | Left = 'left', 16 | Right = 'right' 17 | } 18 | 19 | export enum SPACING { 20 | Compact = 'compact', 21 | Cozy = 'cozy', 22 | Comfortable = 'comfortable' 23 | } 24 | 25 | export enum THROTTLING_OPERATION { 26 | CreateBookmark = 'createbookmark', 27 | OpenTabs = 'opentabs' 28 | } 29 | 30 | export enum SUBPAGES { 31 | Repository = 'repository', 32 | Issues = 'issues', 33 | Pulls = 'pulls', 34 | Actions = 'actions', 35 | Projects = 'projects', 36 | Wiki = 'wiki', 37 | Security = 'security', 38 | Pulse = 'pulse', 39 | Settings = 'settings' 40 | } 41 | 42 | export enum GLOBAL_SUBPAGES { 43 | GlobalSettings = 'settings', 44 | GlobalIssues = 'issues', 45 | GlobalPulls = 'pulls', 46 | GlobalMarketplace = 'marketplace', 47 | GlobalExplore = 'explore', 48 | GlobalNotifications = 'notifications' 49 | } 50 | 51 | export enum BROWSER_TYPE { 52 | Chrome = 'chrome', 53 | Firefox = 'firefox', 54 | Edge = 'microsoft edge', 55 | Safari = 'safari', 56 | Opera = 'opera', 57 | Brave = 'brave' 58 | } 59 | 60 | export const APP_URLS = { 61 | WEBSITE: { 62 | BASE: process.env.WEBSITE_BASE_URL, 63 | SIGNIN: 'signin/', 64 | REDIRECT: 'account/', 65 | ACCOUNT: 'account/', 66 | FEEDBACK: 'account/feedback/', 67 | CHECKOUT: 'checkout/', 68 | TUTORIAL: 'tutorial/' 69 | } 70 | }; 71 | 72 | export const DEFAULT_BRANCH = 'DEFAULT_BRANCH'; 73 | 74 | export const GITHUB_URLS = { 75 | SEARCH_QUERY: 76 | 'https://docs.github.com/en/free-pro-team@latest/github/searching-for-information-on-github/searching-code', 77 | REPO_INDEXING: 78 | 'https://github.blog/changelog/2020-12-17-changes-to-code-search-indexing/', 79 | FEEDBACK: 'https://github.com/AtomicCodeLabs/chummy/issues/new/choose', 80 | MAIN_REPO: 'https://github.com/AtomicCodeLabs/chummy' 81 | }; 82 | -------------------------------------------------------------------------------- /extension/src/global/errors/index.js: -------------------------------------------------------------------------------- 1 | import _ThrottlingError from './throttling.error'; 2 | import _UserError from './user.error'; 3 | import _WindowError from './window.error'; 4 | 5 | export const ThrottlingError = _ThrottlingError; 6 | export const UserError = _UserError; 7 | export const WindowError = _WindowError; 8 | -------------------------------------------------------------------------------- /extension/src/global/errors/throttling.error.js: -------------------------------------------------------------------------------- 1 | export default class ThrottlingError extends Error { 2 | constructor(message) { 3 | super(message); 4 | this.name = 'ThrottlingError'; 5 | } 6 | 7 | static from(error) { 8 | const newError = new ThrottlingError(error.message); 9 | newError.stack = error?.stack; 10 | return newError; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /extension/src/global/errors/user.error.js: -------------------------------------------------------------------------------- 1 | export default class UserError extends Error { 2 | constructor(message) { 3 | super(message); 4 | this.name = 'UserError'; 5 | } 6 | 7 | static from(error) { 8 | const newError = new UserError(error?.message); 9 | newError.stack = error?.stack; 10 | return newError; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /extension/src/global/errors/window.error.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Any error regarding the extension window 3 | */ 4 | export default class WindowError extends Error { 5 | constructor(message) { 6 | super(message); 7 | this.name = 'WindowError'; 8 | } 9 | 10 | static from(error) { 11 | const newError = new WindowError(error.message); 12 | newError.stack = error?.stack; 13 | return newError; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /extension/src/global/limits/constants.ts: -------------------------------------------------------------------------------- 1 | import { ACCOUNT_TYPE, BROWSER_TYPE } from '../constants'; 2 | 3 | export const ALL_TIERS = [ 4 | ACCOUNT_TYPE.Community, 5 | ACCOUNT_TYPE.Professional, 6 | ACCOUNT_TYPE.Enterprise 7 | ]; 8 | 9 | export const NOT_COMMUNITY = [ 10 | ACCOUNT_TYPE.Professional, 11 | ACCOUNT_TYPE.Enterprise 12 | ]; 13 | 14 | export const ALL_BROWSERS = [ 15 | BROWSER_TYPE.Chrome, 16 | BROWSER_TYPE.Firefox, 17 | BROWSER_TYPE.Edge, 18 | BROWSER_TYPE.Safari, 19 | BROWSER_TYPE.Opera, 20 | BROWSER_TYPE.Brave 21 | ]; 22 | 23 | export const CHROMIUM = [ 24 | BROWSER_TYPE.Chrome, 25 | BROWSER_TYPE.Edge, 26 | BROWSER_TYPE.Opera, 27 | BROWSER_TYPE.Brave 28 | ]; 29 | -------------------------------------------------------------------------------- /extension/src/global/limits/features.js: -------------------------------------------------------------------------------- 1 | import { SETTING_TYPE, ACCOUNT_TYPE, SPACING } from '../constants'; 2 | 3 | const FeatureLimits = { 4 | [SETTING_TYPE.Theme]: { 5 | [ACCOUNT_TYPE.Community]: (theme) => theme === 'vanilla-light', 6 | [ACCOUNT_TYPE.Professional]: () => true, 7 | [ACCOUNT_TYPE.Enterprise]: () => true 8 | }, 9 | [SETTING_TYPE.Spacing]: { 10 | [ACCOUNT_TYPE.Community]: (spacing) => spacing === SPACING.Cozy, 11 | [ACCOUNT_TYPE.Professional]: () => true, 12 | [ACCOUNT_TYPE.Enterprise]: () => true 13 | }, 14 | [SETTING_TYPE.StickyWindow]: { 15 | [ACCOUNT_TYPE.Community]: () => true, 16 | [ACCOUNT_TYPE.Professional]: () => true, 17 | [ACCOUNT_TYPE.Enterprise]: () => true 18 | }, 19 | [SETTING_TYPE.SidebarSide]: { 20 | [ACCOUNT_TYPE.Community]: () => true, 21 | [ACCOUNT_TYPE.Professional]: () => true, 22 | [ACCOUNT_TYPE.Enterprise]: () => true 23 | } 24 | }; 25 | 26 | export default FeatureLimits; 27 | -------------------------------------------------------------------------------- /extension/src/global/limits/operations.js: -------------------------------------------------------------------------------- 1 | import { THROTTLING_OPERATION, ACCOUNT_TYPE } from '../constants'; 2 | 3 | const OperationLimits = { 4 | [THROTTLING_OPERATION.CreateBookmark]: { 5 | [ACCOUNT_TYPE.Community]: 15, 6 | [ACCOUNT_TYPE.Professional]: 100000000, 7 | [ACCOUNT_TYPE.Enterprise]: 100000000 8 | }, 9 | [THROTTLING_OPERATION.OpenTabs]: { 10 | [ACCOUNT_TYPE.Community]: 10, 11 | [ACCOUNT_TYPE.Professional]: 100000000, 12 | [ACCOUNT_TYPE.Enterprise]: 100000000 13 | } 14 | }; 15 | 16 | export default OperationLimits; 17 | -------------------------------------------------------------------------------- /extension/src/hooks/dao.js: -------------------------------------------------------------------------------- 1 | import { useContext, useEffect } from 'react'; 2 | import { useHistory, useLocation } from 'react-router-dom'; 3 | import { useUserStore } from './store'; 4 | import { DAOContext } from '../config/dao'; 5 | import useOctoDAO from './octokit'; 6 | 7 | const useDAO = () => { 8 | return useContext(DAOContext); 9 | }; 10 | 11 | // Hook that just triggers current user fetch and returns a pending status that 12 | // frontend can act on conditionally 13 | export const checkCurrentUser = () => { 14 | const firebase = useDAO(); 15 | const octoDAO = useOctoDAO(); 16 | const history = useHistory(); 17 | const location = useLocation(); 18 | const { user, isPending, isLoggedIn } = useUserStore(); 19 | 20 | // Check if user is signed in or not 21 | useEffect(() => { 22 | if (firebase && !isLoggedIn) { 23 | firebase.getCurrentUser(); 24 | } 25 | }, [firebase]); 26 | 27 | // Redirect on userStore.user update 28 | // TODO: use chrome storage to persist user across different sessions 29 | useEffect(() => { 30 | // console.log('user changed in checkCurrentUser', user, location.pathname); 31 | // console.log('firebase user is authenticated', isLoggedIn); 32 | // console.log('octokit is authenticated', octoDAO?.isAuthenticated()); 33 | // If either firebase or octokit isn't authenticated, ask user to sign 34 | // in again. 35 | if (!isLoggedIn || !octoDAO || !octoDAO.isAuthenticated()) { 36 | // If not on sign in page but not logged in. 37 | if (location.pathname !== '/account-sign-in') { 38 | // console.log('navigating to account-signin'); 39 | history.push('/account-sign-in'); 40 | } 41 | } else { 42 | // If coming from sign in page, go to tree 43 | // eslint-disable-next-line no-lonely-if 44 | if (location.pathname === '/account-sign-in') { 45 | // console.log('navigating to /'); 46 | history.push('/'); 47 | } 48 | } 49 | }, [isLoggedIn, octoDAO?.isAuthenticated()]); 50 | 51 | return { isPending, user }; 52 | }; 53 | 54 | export default useDAO; 55 | -------------------------------------------------------------------------------- /extension/src/hooks/getFolderFiles.js: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react'; 2 | import useOctoDAO from './octokit'; 3 | 4 | /* A hook to get all the files in a folder. 5 | * This will be called everytime a Folder component is mounted. 6 | */ 7 | export default (repoInfo, open) => { 8 | const [nodes, setNodes] = useState([]); 9 | const octoDAO = useOctoDAO(); 10 | 11 | useEffect(() => { 12 | const getRepositoryFolderNodes = async () => { 13 | const { owner, repo, branch, treePath } = repoInfo; 14 | const responseNodes = await octoDAO.getRepositoryNodes( 15 | owner, 16 | repo, 17 | branch, 18 | treePath 19 | ); 20 | setNodes(responseNodes); 21 | }; 22 | if (octoDAO && open) { 23 | getRepositoryFolderNodes(); 24 | } 25 | }, [octoDAO?.graphqlAuth, repoInfo, open]); 26 | 27 | return nodes; 28 | }; 29 | -------------------------------------------------------------------------------- /extension/src/hooks/octokit.js: -------------------------------------------------------------------------------- 1 | import { useContext } from 'react'; 2 | import { OctoContext } from '../config/octokit'; 3 | 4 | const useOctoDAO = () => { 5 | return useContext(OctoContext); 6 | }; 7 | 8 | export default useOctoDAO; 9 | -------------------------------------------------------------------------------- /extension/src/hooks/store.js: -------------------------------------------------------------------------------- 1 | import { useContext } from 'react'; 2 | import RootStoreContext from '../config/store/context'; 3 | 4 | export const useStores = () => { 5 | return useContext(RootStoreContext); 6 | }; 7 | 8 | export const useUserStore = () => { 9 | const { userStore } = useStores(); 10 | return userStore; 11 | }; 12 | 13 | export const useFileStore = () => { 14 | const { fileStore } = useStores(); 15 | return fileStore; 16 | }; 17 | 18 | export const useUiStore = () => { 19 | const { uiStore } = useStores(); 20 | return uiStore; 21 | }; 22 | -------------------------------------------------------------------------------- /extension/src/hooks/useDebounce.js: -------------------------------------------------------------------------------- 1 | // https://usehooks.com/useDebounce/ 2 | import { useEffect, useState } from 'react'; 3 | 4 | // Hook 5 | const useDebounce = (value, delay) => { 6 | // State and setters for debounced value 7 | const [debouncedValue, setDebouncedValue] = useState(value); 8 | 9 | useEffect( 10 | () => { 11 | // Update debounced value after delay 12 | const handler = setTimeout(() => { 13 | setDebouncedValue(value); 14 | }, delay); 15 | 16 | // Cancel the timeout if value changes (also on delay change or unmount) 17 | // This is how we prevent debounced value from updating if value is changed ... 18 | // .. within the delay period. Timeout gets cleared and restarted. 19 | return () => { 20 | clearTimeout(handler); 21 | }; 22 | }, 23 | [value, delay] // Only re-call effect if value or delay changes 24 | ); 25 | 26 | return debouncedValue; 27 | }; 28 | 29 | export default useDebounce; 30 | -------------------------------------------------------------------------------- /extension/src/hooks/useDimension.js: -------------------------------------------------------------------------------- 1 | // https://medium.com/datadriveninvestor/usedimension-react-hook-dcd0ecaf1160 2 | import { useRef, useState, useEffect } from 'react'; 3 | import useDebounce from './useDebounce'; 4 | 5 | const initialState = { width: 0, height: 0 }; 6 | 7 | const useDimension = () => { 8 | const [dimensions, setDimensions] = useState(initialState); 9 | const debouncedDimensions = useDebounce(dimensions, 500); 10 | const boxRef = useRef(null); 11 | const resizeObserverRef = useRef(null); 12 | 13 | useEffect(() => { 14 | resizeObserverRef.current = new ResizeObserver((entries = []) => { 15 | entries.forEach((entry) => { 16 | const { width, height } = entry.contentRect; 17 | setDimensions({ width, height }); 18 | }); 19 | }); 20 | if (boxRef.current) resizeObserverRef.current.observe(boxRef.current); 21 | return () => { 22 | if (resizeObserverRef.current) resizeObserverRef.current.disconnect(); 23 | }; 24 | }, [boxRef]); 25 | 26 | return [boxRef, debouncedDimensions]; 27 | }; 28 | 29 | export default useDimension; 30 | -------------------------------------------------------------------------------- /extension/src/hooks/usePrevious.js: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef } from 'react'; 2 | 3 | // Use previous prop 4 | const usePrevious = (value) => { 5 | const ref = useRef(); 6 | useEffect(() => { 7 | ref.current = value; 8 | }); 9 | return ref.current; 10 | }; 11 | 12 | export default usePrevious; 13 | -------------------------------------------------------------------------------- /extension/src/hooks/useTheme.js: -------------------------------------------------------------------------------- 1 | import { useContext } from 'react'; 2 | import { ThemeContext } from 'styled-components'; 3 | 4 | export default () => { 5 | return useContext(ThemeContext); 6 | }; 7 | -------------------------------------------------------------------------------- /extension/src/hooks/useWindowSize.js: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react'; 2 | import { useUiStore } from './store'; 3 | 4 | // https://usehooks.com/useWindowSize/ 5 | const useWindowSize = ({ 6 | keepStoreUpdated = false, 7 | responsive: { 8 | underCallback = () => {}, 9 | overCallback = () => {}, 10 | maxWidth = 0 11 | } 12 | }) => { 13 | // Initialize state with undefined width/height so server and client renders match 14 | // Learn more here: https://joshwcomeau.com/react/the-perils-of-rehydration/ 15 | const [windowSize, setWindowSize] = useState({ 16 | width: undefined, 17 | height: undefined 18 | }); 19 | const { setSidebarWidth } = useUiStore(); 20 | 21 | useEffect(() => { 22 | // Scoped Timeout 23 | let resizeEndTimeout = null; 24 | // Handler to call on window resize 25 | const handleResize = () => { 26 | clearTimeout(resizeEndTimeout); 27 | const newSize = { 28 | width: window.innerWidth, 29 | height: window.innerHeight 30 | }; 31 | // Set window width/height to state 32 | setWindowSize(newSize); 33 | // Call onResizeEnd callback if width doesn't change after 150ms 34 | if (keepStoreUpdated) { 35 | resizeEndTimeout = setTimeout(() => { 36 | setSidebarWidth(newSize.width, keepStoreUpdated); 37 | }, 500); 38 | } 39 | // Call responsive callback if width dips below maxWidth 40 | if (newSize.width < maxWidth) { 41 | underCallback(newSize); 42 | } else { 43 | overCallback(newSize); 44 | } 45 | }; 46 | 47 | // Add event listener 48 | window.addEventListener('resize', handleResize); 49 | 50 | // Call handler right away so state gets updated with initial window size 51 | handleResize(); 52 | 53 | // Remove event listener and timeout on cleanup 54 | return () => { 55 | window.removeEventListener('resize', handleResize); 56 | clearTimeout(resizeEndTimeout); 57 | }; 58 | }, []); // Empty array ensures that effect is only run on mount 59 | 60 | return windowSize; 61 | }; 62 | 63 | export default useWindowSize; 64 | -------------------------------------------------------------------------------- /extension/src/options/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | How to set up React, Webpack, and Babel 6 | 7 | 8 |
9 | 10 | 11 | -------------------------------------------------------------------------------- /extension/src/options/index.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AtomicCodeLabs/chummy/5e752bcdbdc3d05c914b504567fdd0084127e707/extension/src/options/index.js -------------------------------------------------------------------------------- /extension/src/pages/Account/Signin.style.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | import { spacerSpacing } from '../../constants/theme'; 4 | import { ICON } from '../../constants/sizes'; 5 | 6 | export const Container = styled.div` 7 | display: flex; 8 | justify-content: center; 9 | align-items: center; 10 | width: 100%; 11 | height: 100vh; 12 | `; 13 | 14 | export const SignInContainer = styled.div` 15 | padding: 1rem; 16 | margin-top: calc(15vh * -1); 17 | max-width: calc(2 * ${ICON.SPLASH.SIZE}px); 18 | min-width: calc(1.5 * ${ICON.SPLASH.SIZE}px); 19 | text-align: center; 20 | `; 21 | 22 | export const Spacer = styled.div` 23 | height: ${spacerSpacing}; 24 | `; 25 | -------------------------------------------------------------------------------- /extension/src/pages/App/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Switch, Route } from 'react-router-dom'; 3 | 4 | import ExtensionRootContainer from '../../components/Containers/ExtensionRootContainer'; 5 | import ResizableSidebar from '../../components/ResizableSidebar'; 6 | import { routes } from '../../config/routes'; 7 | 8 | // Split into left sidebar and right sidebar. 9 | // Both are horizontal resizable containers - 10 | // https://elastic.github.io/eui/#/layout/resizable-container 11 | 12 | export default () => { 13 | return ( 14 | 15 | 16 | 17 | {routes && 18 | routes.map((route) => ( 19 | 24 | {route.component} 25 | 26 | ))} 27 | 28 | 29 | 30 | ); 31 | }; 32 | -------------------------------------------------------------------------------- /extension/src/pages/Notifications/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { observer } from 'mobx-react-lite'; 3 | 4 | import { checkCurrentUser } from '../../hooks/dao'; 5 | import { useUiStore } from '../../hooks/store'; 6 | import Panel from '../../components/Panel'; 7 | import { 8 | PanelDivider2, 9 | PanelsContainer, 10 | PanelDescriptionContainer 11 | } from '../../components/Panel/style'; 12 | import { A } from '../../components/Text'; 13 | import Scrollbars from '../../components/Scrollbars'; 14 | import { notificationTypeToColor } from '../../constants/theme'; 15 | 16 | export default observer(() => { 17 | checkCurrentUser(); 18 | const { 19 | notifications, 20 | removeNotification, 21 | clearNotifications 22 | } = useUiStore(); 23 | const hasNotifications = !!(!notifications || notifications.size); 24 | 25 | return ( 26 | 27 | 28 |
{hasNotifications ? notifications.size : 0} notifications
29 | {hasNotifications && ( 30 | <> 31 |
32 |
33 | Clear all 34 |
35 | 36 | )} 37 | 38 | {hasNotifications && ( 39 | 40 | {Array.from(notifications) 41 | .reverse() 42 | .map(([, notification]) => ( 43 | 44 | { 49 | removeNotification(notification); 50 | }} 51 | /> 52 | 53 | 54 | ))} 55 | 56 | )} 57 | 58 | ); 59 | }); 60 | -------------------------------------------------------------------------------- /extension/src/pages/Search/LanguagesSelect.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/jsx-props-no-spreading */ 2 | import React from 'react'; 3 | 4 | import { ControlledSelect } from '../../components/Form/Select'; 5 | import { popularLanguages, otherLanguages } from '../../constants/languages'; 6 | 7 | const LanguagesSelect = (props) => ( 8 | ({ 16 | ...lang, 17 | label: lang.name, 18 | value: lang.name 19 | })) 20 | }, 21 | { 22 | label: 'Everything else', 23 | options: otherLanguages.map((lang) => ({ 24 | ...lang, 25 | label: lang.name, 26 | value: lang.name 27 | })) 28 | } 29 | ]} 30 | /> 31 | ); 32 | 33 | export default LanguagesSelect; 34 | -------------------------------------------------------------------------------- /extension/src/pages/Settings/options.js: -------------------------------------------------------------------------------- 1 | import { THEME_NAMES } from '../../config/theme/selector'; 2 | import { SIDEBAR_SIDE } from '../../global/constants'; 3 | import { 4 | ALL_TIERS, 5 | NOT_COMMUNITY, 6 | ALL_BROWSERS, 7 | CHROMIUM 8 | } from '../../global/limits/constants'; 9 | import { kebabify } from '../../utils'; 10 | 11 | const COMMUNITY_THEMES = ['vanilla-light']; 12 | 13 | export const themeConfig = { 14 | browsers: ALL_BROWSERS, 15 | options: THEME_NAMES.map((themeName) => ({ 16 | value: kebabify(themeName), 17 | label: themeName, 18 | tiers: COMMUNITY_THEMES.includes(kebabify(themeName)) 19 | ? ALL_TIERS 20 | : NOT_COMMUNITY 21 | })) 22 | }; 23 | 24 | export const spacingConfig = { 25 | browsers: ALL_BROWSERS, 26 | options: [ 27 | { 28 | value: 'compact', 29 | label: 'Compact', 30 | tiers: NOT_COMMUNITY, 31 | browsers: ALL_BROWSERS 32 | }, 33 | { value: 'cozy', label: 'Cozy', tiers: ALL_TIERS, browsers: ALL_BROWSERS }, 34 | { 35 | value: 'comfortable', 36 | label: 'Comfortable', 37 | tiers: NOT_COMMUNITY, 38 | browsers: ALL_BROWSERS 39 | } 40 | ] 41 | }; 42 | 43 | export const isDistractionFreeModeConfig = { 44 | browsers: ALL_BROWSERS, 45 | options: [ 46 | { value: true, label: 'Yes', tiers: NOT_COMMUNITY }, 47 | { 48 | value: false, 49 | label: 'No', 50 | tiers: ALL_TIERS 51 | } 52 | ] 53 | }; 54 | 55 | export const isStickyWindowConfig = { 56 | browsers: CHROMIUM, 57 | options: [ 58 | { value: true, label: 'Yes', tiers: NOT_COMMUNITY }, 59 | { 60 | value: false, 61 | label: 'No', 62 | tiers: ALL_TIERS 63 | } 64 | ] 65 | }; 66 | 67 | export const sidebarSideConfig = { 68 | browsers: ALL_BROWSERS, 69 | options: [ 70 | { value: SIDEBAR_SIDE.Left, label: 'Left', tiers: ALL_TIERS }, 71 | { 72 | value: SIDEBAR_SIDE.Right, 73 | label: 'Right', 74 | tiers: NOT_COMMUNITY 75 | } 76 | ] 77 | }; 78 | -------------------------------------------------------------------------------- /extension/src/pages/Tree/Files.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import { observer } from 'mobx-react-lite'; 3 | 4 | import Node from '../../components/Node/TreeOrBlobNode'; 5 | import useOctoDAO from '../../hooks/octokit'; 6 | import { useFileStore } from '../../hooks/store'; 7 | 8 | const FilesSection = observer(() => { 9 | const octoDAO = useOctoDAO(); 10 | const { currentBranch } = useFileStore(); 11 | const [nodes, setNodes] = useState([]); 12 | 13 | // Get current repository's files 14 | useEffect(() => { 15 | const getBranchNodes = async () => { 16 | if (!currentBranch) { 17 | // If null (on tab that's not github) 18 | return; 19 | } 20 | const responseNodes = await octoDAO.getRepositoryNodes( 21 | currentBranch?.repo?.owner, 22 | currentBranch?.repo?.name, 23 | currentBranch, 24 | '' 25 | ); 26 | setNodes(responseNodes); 27 | }; 28 | if (octoDAO) { 29 | getBranchNodes(); 30 | } 31 | }, [ 32 | octoDAO?.graphqlAuth, 33 | currentBranch?.repo?.owner, 34 | currentBranch?.repo?.name, 35 | currentBranch?.name 36 | ]); 37 | 38 | return ( 39 | <> 40 | {currentBranch && 41 | nodes && 42 | nodes.map((n) => ( 43 | 50 | ))} 51 | 52 | ); 53 | }); 54 | 55 | export default FilesSection; 56 | -------------------------------------------------------------------------------- /extension/src/pages/Tree/OpenTabs.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | import { observer } from 'mobx-react-lite'; 4 | 5 | import { A } from '../../components/Text'; 6 | import RepoNode from '../../components/Node/RepoNode'; 7 | import { redirectToUrl, openSession } from '../../utils/browser'; 8 | import { useFileStore } from '../../hooks/store'; 9 | import { 10 | h3FontSize, 11 | h2MarginSize, 12 | subTitleMarginSize, 13 | lighterTextColor 14 | } from '../../constants/theme'; 15 | import { GITHUB_URLS } from '../../global/constants'; 16 | 17 | const HelpContainer = styled.div` 18 | height: 100%; 19 | width: 100%; 20 | display: flex; 21 | flex-direction: column; 22 | justify-content: center; 23 | align-items: center; 24 | box-sizing: border-box; 25 | padding: ${h2MarginSize}; 26 | 27 | font-size: ${h3FontSize}; 28 | color: ${lighterTextColor}; 29 | font-weight: 400; 30 | text-align: center; 31 | 32 | .section:not(:last-child) { 33 | margin-bottom: ${subTitleMarginSize}; 34 | } 35 | `; 36 | 37 | const OpenTabsSection = observer(() => { 38 | const { openRepos, currentBranch, currentSession } = useFileStore(); 39 | 40 | const handleGetStartedClick = (e) => { 41 | e.stopPropagation(); 42 | e.preventDefault(); 43 | redirectToUrl(GITHUB_URLS.MAIN_REPO); 44 | }; 45 | const handleRestoreClick = (e) => { 46 | e.stopPropagation(); 47 | e.preventDefault(); 48 | openSession(currentSession); 49 | }; 50 | 51 | if (!openRepos || openRepos.size === 0) { 52 | return ( 53 | 54 |
It's quiet here...
55 |
56 | Open any GitHub repository to 57 | get started. 58 |
59 |
60 | Restore your last session. 61 |
62 |
63 | ); 64 | } 65 | 66 | return ( 67 | <> 68 | {openRepos && 69 | Array.from(openRepos).map(([, repo]) => ( 70 | 75 | ))} 76 | 77 | ); 78 | }); 79 | 80 | export default OpenTabsSection; 81 | -------------------------------------------------------------------------------- /extension/src/pages/Vcs/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | import theme from 'styled-theming'; 4 | import { observer } from 'mobx-react-lite'; 5 | 6 | import { FlagIcon } from '../../components/Icon/Icons'; 7 | import { H2, P } from '../../components/Text'; 8 | import { checkCurrentUser } from '../../hooks/dao'; 9 | import useTheme from '../../hooks/useTheme'; 10 | import { flagIconColor } from '../../constants/theme'; 11 | import { ICON } from '../../constants/sizes'; 12 | 13 | const Container = styled.div` 14 | display: flex; 15 | width: 100%; 16 | height: 100%; 17 | justify-content: center; 18 | align-items: center; 19 | `; 20 | 21 | const innerPadding = theme('spacing', { 22 | compact: '0.6rem', 23 | cozy: '0.8rem', 24 | comfortable: '1.2rem' 25 | }); 26 | 27 | const SplashContainer = styled.div` 28 | display: flex; 29 | flex-direction: column; 30 | justify-content: center; 31 | align-items: center; 32 | text-align: left; 33 | box-sizing: border-box; 34 | 35 | padding: ${innerPadding}; 36 | width: 100%; 37 | max-width: 300px; 38 | `; 39 | 40 | export default observer(() => { 41 | checkCurrentUser(); 42 | const { theme: mode, spacing } = useTheme(); 43 | const STPayload = { theme: { theme: mode, spacing } }; 44 | 45 | return ( 46 | 47 | 48 | 52 |

Code Review is currently in development.

53 |

54 | Manage pull requests, track file changes, comments, approvals, and 55 | everything code review related from one place. Stay tuned for its 56 | upcoming debut! 57 |

58 |
59 |
60 | ); 61 | }); 62 | -------------------------------------------------------------------------------- /extension/src/popup/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Chummy 7 | 8 | 9 |
10 | 11 | 12 | -------------------------------------------------------------------------------- /extension/src/popup/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-vars */ 2 | /* eslint-disable import/no-named-as-default */ 3 | import React from 'react'; 4 | import { MemoryRouter as Router } from 'react-router-dom'; 5 | import { render } from 'react-dom'; 6 | 7 | import App from '../pages/App'; 8 | import ThemeProvider from '../config/theme/context'; 9 | import RootStoreContext from '../config/store/context'; 10 | import rootStore from '../config/store/root.store'; 11 | import DAOProvider from '../config/dao'; 12 | import OctoProvider from '../config/octokit'; 13 | 14 | let app; 15 | 16 | function renderDevPanel() { 17 | app = document.getElementById('my-extension-root'); 18 | 19 | // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Errors/Dead_object 20 | window.log = console.log; 21 | 22 | // unmountComponentAtNode(app); 23 | render( 24 | // eslint-disable-next-line react/jsx-props-no-spreading 25 | 26 | {/* MobX store for general data */} 27 | 28 | {/* GitHub DAO for making requests */} 29 | 30 | {/* Firebase store for auth */} 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | , 39 | app 40 | ); 41 | } 42 | 43 | function init() { 44 | const extensionLoaded = document.getElementById('#my-extension-root'); 45 | if (!extensionLoaded) { 46 | renderDevPanel(); 47 | } 48 | } 49 | 50 | init(); 51 | -------------------------------------------------------------------------------- /extension/src/themes/dracula.dark.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Dracula", 3 | "type": "dark", 4 | "author": "Alex Gyujin Kim", 5 | "theme": { 6 | "sidebarColor": "#282a36", 7 | "sidebarActiveIconColor": "#f8f8f2", 8 | "sidebarInactiveIconColor": "#6272a4", 9 | "backgroundColor": "#44475a", 10 | "backgroundAlternatingDarkColor": "#363845", 11 | "backgroundAlternatingLightColor": "#44475a", 12 | "backgroundHighlightColor": "#282a36", 13 | "backgroundHighlightTextColor": "#e3ded7", 14 | "backgroundHighlightDarkColor": "#282a36", 15 | "backgroundHighlightDarkTextColor": "#e3ded7", 16 | "textColor": "#ffffff", 17 | "lightTextColor": "#e3ded7", 18 | "lighterTextColor": "#bab6b1", 19 | "lightBackgroundTextColor": "#000000", 20 | "darkBackgroundTextColor": "#e3ded7", 21 | "highlightTextColor": "#000000", 22 | "highlightBackgroundColor": "#bd93f9", 23 | "folderIconColor": "#bd93f9", 24 | "flagIconColor": "#ff5555", 25 | "bookmarkIconColor": "#ff79c6", 26 | "nodeIconColor": "#e3ded7", 27 | "nodeIconDarkColor": "#f2eee9", 28 | "shadowColor": "rgba(0, 0, 0, 0.2)", 29 | "fieldBackgroundColor": "#282a36", 30 | "optionBackgroundLightColor": "#6272a4", 31 | "optionDisabledBackgroundColor": "#282a36", 32 | "optionDisabledTextColor": "#6272a4", 33 | "fieldFocusOutlineColor": "#8be9fd", 34 | "borderColor": "#2e2e2e", 35 | "successColor": "#50fa7b", 36 | "errorColor": "#ff5555", 37 | "infoColor": "#8be9fd", 38 | "warningColor": "#f1fa8c" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /extension/src/themes/janestheme.light.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Jane's Theme", 3 | "type": "light", 4 | "author": "Jane Jeong", 5 | "theme": { 6 | "sidebarColor": "#FFFFFF", 7 | "sidebarActiveIconColor": "#FF87EC", 8 | "sidebarInactiveIconColor": "#FFA2F0", 9 | "backgroundColor": "#FFFFFF", 10 | "backgroundAlternatingDarkColor": "#FFE8FC", 11 | "backgroundAlternatingLightColor": "#FFFFFF", 12 | "backgroundHighlightColor": "#FFD8F9", 13 | "backgroundHighlightTextColor": "#000000", 14 | "backgroundHighlightDarkColor": "#FFD8F9", 15 | "backgroundHighlightDarkTextColor": "#000000", 16 | "textColor": "#3c474d", 17 | "lightTextColor": "#515f66", 18 | "lighterTextColor": "#6d7f87", 19 | "lightBackgroundTextColor": "#000000", 20 | "darkBackgroundTextColor": "#e3ded7", 21 | "highlightTextColor": "#000000", 22 | "highlightBackgroundColor": "#fff200", 23 | "folderIconColor": "#F65BE3", 24 | "flagIconColor": "#FFC300", 25 | "bookmarkIconColor": "#F65BE3", 26 | "nodeIconColor": "#4b5259", 27 | "nodeIconDarkColor": "#000000", 28 | "shadowColor": "rgba(0, 0, 0, 0.1)", 29 | "fieldBackgroundColor": "#FFD8F9", 30 | "fieldBackgroundLightColor": "#FFE8FC", 31 | "fieldFocusOutlineColor": "#F65BE3", 32 | "optionDisabledBackgroundColor": "#FFE8FC", 33 | "optionDisabledTextColor": "#FFD8F9", 34 | "borderColor": "#e0e0e0", 35 | "successColor": "#4CAF50", 36 | "errorColor": "#FF4081", 37 | "infoColor": "#3F51B5", 38 | "warningColor": "#FFA000" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /extension/src/themes/material.dark.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Material Dark", 3 | "type": "dark", 4 | "author": "Alex Gyujin Kim", 5 | "theme": { 6 | "sidebarColor": "#212121", 7 | "sidebarActiveIconColor": "#eeffff", 8 | "sidebarInactiveIconColor": "#737a7a", 9 | "backgroundColor": "#212121", 10 | "backgroundAlternatingDarkColor": "#212121", 11 | "backgroundAlternatingLightColor": "#212121", 12 | "backgroundHighlightColor": "#3b3b3b", 13 | "backgroundHighlightTextColor": "#e3ded7", 14 | "backgroundHighlightDarkColor": "#3b3b3b", 15 | "backgroundHighlightDarkTextColor": "#e3ded7", 16 | "textColor": "#ffffff", 17 | "lightTextColor": "#e3ded7", 18 | "lighterTextColor": "#bab6b1", 19 | "lightBackgroundTextColor": "#000000", 20 | "darkBackgroundTextColor": "#e3ded7", 21 | "highlightTextColor": "#000000", 22 | "highlightBackgroundColor": "#ffcb6b", 23 | "folderIconColor": "#ffcb6b", 24 | "flagIconColor": "#9b870c", 25 | "bookmarkIconColor": "#80cbc4", 26 | "nodeIconColor": "#e3ded7", 27 | "nodeIconDarkColor": "#f2eee9", 28 | "shadowColor": "rgba(0, 0, 0, 0.2)", 29 | "fieldBackgroundColor": "#37474f", 30 | "optionBackgroundLightColor": "#465a63", 31 | "optionDisabledBackgroundColor": "#273238", 32 | "optionDisabledTextColor": "#627d8c", 33 | "fieldFocusOutlineColor": "#80cbc4", 34 | "borderColor": "#2e2e2e", 35 | "successColor": "#4CAF50", 36 | "errorColor": "#FF4081", 37 | "infoColor": "#3F51B5", 38 | "warningColor": "#FFA000" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /extension/src/themes/material.light.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Material Light", 3 | "type": "light", 4 | "author": "Alex Gyujin Kim", 5 | "theme": { 6 | "sidebarColor": "#fafafa", 7 | "sidebarActiveIconColor": "#515f66", 8 | "sidebarInactiveIconColor": "#869ba6", 9 | "backgroundColor": "#fafafa", 10 | "backgroundAlternatingDarkColor": "#fafafa", 11 | "backgroundAlternatingLightColor": "#fafafa", 12 | "backgroundHighlightColor": "#d0d8dc", 13 | "backgroundHighlightTextColor": "#3c474d", 14 | "backgroundHighlightDarkColor": "#d0d8dc", 15 | "backgroundHighlightDarkTextColor": "#3c474d", 16 | "textColor": "#3c474d", 17 | "lightTextColor": "#515f66", 18 | "lighterTextColor": "#6d7f87", 19 | "lightBackgroundTextColor": "#3c474d", 20 | "darkBackgroundTextColor": "#f2eee9", 21 | "highlightTextColor": "#3c474d", 22 | "highlightBackgroundColor": "#ffcb6b", 23 | "folderIconColor": "#ffcb6b", 24 | "flagIconColor": "#FFC300", 25 | "bookmarkIconColor": "#2196f3", 26 | "nodeIconColor": "#515f66", 27 | "nodeIconDarkColor": "#3c474d", 28 | "shadowColor": "rgba(0, 0, 0, 0.1)", 29 | "fieldBackgroundColor": "#dedede", 30 | "fieldBackgroundLightColor": "#e8e8e8", 31 | "fieldFocusOutlineColor": "#0069c0", 32 | "optionDisabledBackgroundColor": "#ebebeb", 33 | "optionDisabledTextColor": "#b0b0b0", 34 | "borderColor": "#e0e0e0", 35 | "successColor": "#4CAF50", 36 | "errorColor": "#FF4081", 37 | "infoColor": "#3F51B5", 38 | "warningColor": "#FFA000" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /extension/src/themes/monokai.dark.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Monokai Dark", 3 | "type": "dark", 4 | "author": "Alex Gyujin Kim", 5 | "theme": { 6 | "sidebarColor": "#222222", 7 | "sidebarActiveIconColor": "#F8F8F2", 8 | "sidebarInactiveIconColor": "#888888", 9 | "backgroundColor": "#2f2f2f", 10 | "backgroundAlternatingDarkColor": "#2f2f2f", 11 | "backgroundAlternatingLightColor": "#363636", 12 | "backgroundHighlightColor": "#424242", 13 | "backgroundHighlightTextColor": "#e3ded7", 14 | "backgroundHighlightDarkColor": "#424242", 15 | "backgroundHighlightDarkTextColor": "#e3ded7", 16 | "textColor": "#ffffff", 17 | "lightTextColor": "#e3ded7", 18 | "lighterTextColor": "#bab6b1", 19 | "lightBackgroundTextColor": "#000000", 20 | "darkBackgroundTextColor": "#e3ded7", 21 | "highlightTextColor": "#000000", 22 | "highlightBackgroundColor": "#66D9EF", 23 | "folderIconColor": "#e3ded7", 24 | "flagIconColor": "#75715E", 25 | "bookmarkIconColor": "#cb2d62", 26 | "nodeIconColor": "#e3ded7", 27 | "nodeIconDarkColor": "#f2eee9", 28 | "shadowColor": "rgba(0, 0, 0, 0.2)", 29 | "fieldBackgroundColor": "#222222", 30 | "fieldBackgroundLightColor": "#363636", 31 | "fieldFocusOutlineColor": "#cb2d62", 32 | "optionDisabledBackgroundColor": "#242424", 33 | "optionDisabledTextColor": "#7a7a7a", 34 | "borderColor": "#404040", 35 | "successColor": "#b4d273", 36 | "errorColor": "#eb6ca0", 37 | "infoColor": "#7eb6e0", 38 | "warningColor": "#e87d3e" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /extension/src/themes/onedark.dark.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "One Dark", 3 | "type": "dark", 4 | "author": "Alex Gyujin Kim", 5 | "theme": { 6 | "sidebarColor": "#282C35", 7 | "sidebarActiveIconColor": "#F8F8F2", 8 | "sidebarInactiveIconColor": "#abb2bf", 9 | "backgroundColor": "#282C35", 10 | "backgroundAlternatingDarkColor": "#282C35", 11 | "backgroundAlternatingLightColor": "#2D313C", 12 | "backgroundHighlightColor": "#5c6370", 13 | "backgroundHighlightTextColor": "#F8F8F2", 14 | "backgroundHighlightDarkColor": "#5c6370", 15 | "backgroundHighlightDarkTextColor": "#F8F8F2", 16 | "textColor": "#F8F8F2", 17 | "lightTextColor": "#bac1cf", 18 | "lighterTextColor": "#818896", 19 | "lightBackgroundTextColor": "#000000", 20 | "darkBackgroundTextColor": "#F8F8F2", 21 | "highlightTextColor": "#000000", 22 | "highlightBackgroundColor": "#e6c07b", 23 | "folderIconColor": "#61aeee", 24 | "flagIconColor": "#9b870c", 25 | "bookmarkIconColor": "#61aeee", 26 | "nodeIconColor": "#abb2bf", 27 | "nodeIconDarkColor": "#818896", 28 | "shadowColor": "rgba(0, 0, 0, 0.2)", 29 | "fieldBackgroundColor": "#5c6370", 30 | "fieldBackgroundLightColor": "#6b7382", 31 | "fieldFocusOutlineColor": "#61aeee", 32 | "optionDisabledBackgroundColor": "#3c424f", 33 | "optionDisabledTextColor": "#78849e", 34 | "borderColor": "#434852", 35 | "successColor": "#98C379", 36 | "errorColor": "#E06C75", 37 | "infoColor": "#61AFEF", 38 | "warningColor": "#E5C07B" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /extension/src/themes/vanilla.dark.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Vanilla Dark", 3 | "type": "dark", 4 | "author": "Alex Gyujin Kim", 5 | "theme": { 6 | "sidebarColor": "#3a424a", 7 | "sidebarActiveIconColor": "#FFFFFF", 8 | "sidebarInactiveIconColor": "#888888", 9 | "backgroundColor": "#232323", 10 | "backgroundAlternatingDarkColor": "#232323", 11 | "backgroundAlternatingLightColor": "#2d2d2d", 12 | "backgroundHighlightColor": "#145596", 13 | "backgroundHighlightTextColor": "#e3ded7", 14 | "backgroundHighlightDarkColor": "#145596", 15 | "backgroundHighlightDarkTextColor": "#e3ded7", 16 | "textColor": "#ffffff", 17 | "lightTextColor": "#e3ded7", 18 | "lighterTextColor": "#bab6b1", 19 | "lightBackgroundTextColor": "#000000", 20 | "darkBackgroundTextColor": "#e3ded7", 21 | "highlightTextColor": "#000000", 22 | "highlightBackgroundColor": "#fff200", 23 | "folderIconColor": "#79b8ff", 24 | "flagIconColor": "#9b870c", 25 | "bookmarkIconColor": "#79b8ff", 26 | "nodeIconColor": "#e3ded7", 27 | "nodeIconDarkColor": "#f2eee9", 28 | "shadowColor": "rgba(0, 0, 0, 0.2)", 29 | "fieldBackgroundColor": "#3a424a", 30 | "fieldBackgroundLightColor": "#48525c", 31 | "fieldFocusOutlineColor": "#4078c0", 32 | "optionDisabledBackgroundColor": "#383838", 33 | "optionDisabledTextColor": "#8a8a8a", 34 | "borderColor": "#2e2e2e", 35 | "successColor": "#42ad3d", 36 | "errorColor": "#ff6969", 37 | "infoColor": "#004eff", 38 | "warningColor": "#e0cd19" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /extension/src/themes/vanilla.light.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Vanilla Light", 3 | "type": "light", 4 | "author": "Alex Gyujin Kim", 5 | "theme": { 6 | "sidebarColor": "#24292D", 7 | "sidebarActiveIconColor": "#FFFFFF", 8 | "sidebarInactiveIconColor": "#888888", 9 | "backgroundColor": "#FFFFFF", 10 | "backgroundAlternatingDarkColor": "#F1F8FF", 11 | "backgroundAlternatingLightColor": "#FFFFFF", 12 | "backgroundHighlightColor": "#d4e9ff", 13 | "backgroundHighlightTextColor": "#000000", 14 | "backgroundHighlightDarkColor": "#d4e9ff", 15 | "backgroundHighlightDarkTextColor": "#000000", 16 | "textColor": "#000000", 17 | "lightTextColor": "#24292e", 18 | "lighterTextColor": "#56626e", 19 | "lightBackgroundTextColor": "#000000", 20 | "darkBackgroundTextColor": "#e3ded7", 21 | "highlightTextColor": "#000000", 22 | "highlightBackgroundColor": "#fff200", 23 | "folderIconColor": "#79b8ff", 24 | "flagIconColor": "#FFC300", 25 | "bookmarkIconColor": "#79b8ff", 26 | "nodeIconColor": "#4b5259", 27 | "nodeIconDarkColor": "#000000", 28 | "shadowColor": "rgba(0, 0, 0, 0.1)", 29 | "fieldBackgroundColor": "#DCDCDC", 30 | "fieldBackgroundLightColor": "#cccccc", 31 | "fieldFocusOutlineColor": "#4078c0", 32 | "optionDisabledBackgroundColor": "#ebebeb", 33 | "optionDisabledTextColor": "#b0b0b0", 34 | "borderColor": "#E1E4E8", 35 | "successColor": "#42ad3d", 36 | "errorColor": "#ff4d4d", 37 | "infoColor": "#004eff", 38 | "warningColor": "#baa800" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /extension/src/utils/bookmark.js: -------------------------------------------------------------------------------- 1 | export const bookmarkRepoMapToArray = (repoMap) => 2 | Object.values(repoMap).flat(); 3 | 4 | export const transformBookmarks = (bookmarks) => { 5 | // 6 | // eslint-disable-next-line prefer-const 7 | let userBookmarks = {}; 8 | bookmarks.forEach((b) => { 9 | const repoKey = `${b.repo.owner}:${b.repo.name}`; 10 | const foundRepo = userBookmarks[repoKey]; 11 | if (!foundRepo) { 12 | // If not found, create entry 13 | userBookmarks[repoKey] = { 14 | owner: b.repo.owner, 15 | name: b.repo.name, 16 | bookmarks: [b] 17 | }; 18 | } else { 19 | // Add to existing repo 20 | userBookmarks[repoKey] = { 21 | ...foundRepo, 22 | bookmarks: [...foundRepo.bookmarks, b] 23 | }; 24 | } 25 | }); 26 | return userBookmarks; 27 | }; 28 | 29 | export const PLACEHOLDER = ''; 30 | -------------------------------------------------------------------------------- /extension/src/utils/browser.js: -------------------------------------------------------------------------------- 1 | import browser from 'webextension-polyfill'; 2 | import log from '../config/log'; 3 | 4 | export const redirectToUrl = (url) => { 5 | const request = { 6 | action: 'redirect-to-url', 7 | payload: { url } 8 | }; 9 | log.toBg('Redirect to url request -> bg', request); 10 | browser.runtime.sendMessage(request); 11 | }; 12 | 13 | export const openSession = (session) => { 14 | const request = { 15 | action: 'redirect-to-session', 16 | payload: { session } 17 | }; 18 | log.toBg('Redirect to url request -> bg', request); 19 | browser.runtime.sendMessage(request); 20 | }; 21 | 22 | export const updateSidebarSide = (prevSide, nextSide) => { 23 | const request = { 24 | action: 'sidebar-side-updated', 25 | payload: { prevSide, nextSide } 26 | }; 27 | log.toBg('Update sidebar side request -> bg', request); 28 | browser.runtime.sendMessage(request); 29 | }; 30 | -------------------------------------------------------------------------------- /extension/src/utils/repository.js: -------------------------------------------------------------------------------- 1 | import browser from 'webextension-polyfill'; 2 | import log from '../config/log'; 3 | 4 | // eslint-disable-next-line import/prefer-default-export 5 | export const getOpenRepositories = async (callback = () => {}) => { 6 | try { 7 | const request = { 8 | action: 'get-open-repositories' 9 | }; 10 | log.toBg('Get open repositories request -> bg', request); 11 | const response = await browser.runtime.sendMessage(request); 12 | 13 | if (response) { 14 | callback(response.payload); 15 | } 16 | } catch (error) { 17 | log.error('Error getting open repositories', error); 18 | } 19 | }; 20 | 21 | export const onUpdateOpenRepositories = (callback = () => {}) => { 22 | const toCall = (request) => { 23 | if (request.action === 'tab-updated') { 24 | callback(request.payload); 25 | } 26 | }; 27 | browser.runtime.onMessage.addListener(toCall); 28 | 29 | return () => browser.runtime.onMessage.removeListener(toCall); 30 | }; 31 | -------------------------------------------------------------------------------- /extension/src/utils/tabs.js: -------------------------------------------------------------------------------- 1 | import browser from 'webextension-polyfill'; 2 | 3 | export const onActiveTabChange = (callback = () => {}) => { 4 | const toCall = (request) => { 5 | if (request.action === 'active-tab-changed') { 6 | callback(request.payload); 7 | } 8 | }; 9 | browser.runtime.onMessage.addListener(toCall); 10 | 11 | return () => browser.runtime.onMessage.removeListener(toCall); 12 | }; 13 | 14 | export const FILLER = 0; 15 | -------------------------------------------------------------------------------- /extension/src/utils/throttling.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/prefer-default-export */ 2 | import { ACCOUNT_TYPE, THROTTLING_OPERATION } from '../global/constants'; 3 | import OperationLimits from '../global/limits/operations'; 4 | 5 | export const getMaxOpenTabLimit = (user) => { 6 | const defaultLimit = 7 | OperationLimits[THROTTLING_OPERATION.OpenTabs][ACCOUNT_TYPE.Community]; 8 | if (!user?.accountType) return defaultLimit; 9 | 10 | const limit = 11 | OperationLimits[THROTTLING_OPERATION.OpenTabs]?.[user.accountType]; 12 | if (limit !== null) { 13 | return limit; 14 | } 15 | 16 | return defaultLimit; 17 | }; 18 | -------------------------------------------------------------------------------- /extension/src/utils/user.js: -------------------------------------------------------------------------------- 1 | import browser from 'webextension-polyfill'; 2 | import log from '../config/log'; 3 | import { handleResponse, unproxifyBookmark } from '.'; 4 | 5 | export const onSignInComplete = (callback = () => {}) => { 6 | const toCall = (request) => { 7 | if (request.action === 'sign-in-complete') { 8 | callback(request); 9 | } 10 | }; 11 | browser.runtime.onMessage.addListener(toCall); 12 | return () => browser.runtime.onMessage.removeListener(toCall); 13 | }; 14 | 15 | export const onUserUpdate = (callback = () => {}) => { 16 | const toCall = (request) => { 17 | if (request.action === 'update-user') { 18 | callback(request); 19 | } 20 | }; 21 | browser.runtime.onMessage.addListener(toCall); 22 | return () => browser.runtime.onMessage.removeListener(toCall); 23 | }; 24 | 25 | export const getAllBookmarks = async () => { 26 | const request = { 27 | action: 'get-bookmarks' 28 | }; 29 | log.toBg('Get bookmarks request -> bg', request); 30 | const response = await browser.runtime.sendMessage(request); 31 | return handleResponse(response); 32 | }; 33 | 34 | export const addBookmark = async (bookmark) => { 35 | const request = { 36 | action: 'create-bookmark', 37 | payload: unproxifyBookmark(bookmark) 38 | }; 39 | log.toBg('Create bookmark request -> bg', request); 40 | const response = await browser.runtime.sendMessage(request); 41 | return handleResponse(response); 42 | }; 43 | 44 | export const updateBookmark = async (bookmark) => { 45 | const request = { 46 | action: 'update-bookmark', 47 | payload: unproxifyBookmark(bookmark) 48 | }; 49 | log.toBg('Update bookmark request -> bg', request); 50 | const response = await browser.runtime.sendMessage(request); 51 | return handleResponse(response); 52 | }; 53 | 54 | export const removeBookmark = async (bookmark) => { 55 | const request = { 56 | action: 'remove-bookmark', 57 | payload: unproxifyBookmark(bookmark) 58 | }; 59 | log.toBg('Remove bookmark request -> bg', request); 60 | const response = await browser.runtime.sendMessage(request); 61 | return handleResponse(response); 62 | }; 63 | 64 | export default { 65 | getAllBookmarks, 66 | addBookmark, 67 | updateBookmark, 68 | removeBookmark, 69 | onSignInComplete 70 | }; 71 | -------------------------------------------------------------------------------- /extension/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "./dist/", 4 | "sourceMap": true, 5 | "noImplicitAny": true, 6 | "module": "commonjs", 7 | "target": "es6", 8 | "jsx": "react", 9 | "allowJs": true, 10 | "experimentalDecorators": true, 11 | "types": ["chrome"], 12 | "baseUrl": "." 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /extension/webpack/BACKUP.prod.web.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 3 | const CopyPlugin = require('copy-webpack-plugin'); 4 | const HtmlWebpackTagsPlugin = require('html-webpack-tags-plugin'); 5 | 6 | const base = require('./prod.base.config'); 7 | const { formBaseManifest, generateReports, packageInfo } = require('./util'); 8 | 9 | module.exports = { 10 | ...base, 11 | output: { 12 | ...base.output, 13 | path: path.join(__dirname, '../dist/web'), 14 | publicPath: '/' 15 | }, 16 | plugins: [ 17 | ...base.plugins, 18 | new CopyPlugin({ 19 | patterns: [ 20 | { 21 | from: '../manifest-base.json', 22 | to: './manifest.json', 23 | transform(content) { 24 | return JSON.stringify({ 25 | ...formBaseManifest(content), 26 | key: 27 | 'MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAkZ7eURkxU+9PPkvVaVDUK88dZX39ZXKS9zRtpkAY6so1omoDZ6L3AWjy4e3ds8vz6OxeFcPzgycgTDVaPa2LAgvk2i+/eSbmFO8wvbp8Ce0/iPf2Vp0IqR1MQ+aRT+qD+6swNXvIJuAwFcuPP0LnDMe4veGVHyvI4uoelEVJ7P7RrnrskU4vscUAKHi5FygZLnfXzifrY2Vy6GA2wNipmd2I4+gW4ZnvSTzMs1u6s/k3LSg96cFxOl62AanEnuOcahUrCPl2/aTlU8OrOdgyiGvWKw4DxXsLC7XNZ589QvVP9uRdSsj7sAie/bGkTWRM3/NqYts8YhsMypWCCCxnQQIDAQAB' 28 | }); 29 | } 30 | }, 31 | { from: '../public/icon', to: './icon' }, 32 | { from: '../key.pem', to: './key.pem' }, 33 | { from: '../public/index.html', to: '../index.html' } 34 | ] 35 | }), 36 | new HtmlWebpackPlugin({ 37 | title: 'Chummy', 38 | template: '../src/popup/index.html', 39 | chunks: ['popup'], 40 | publicPath: `${process.env.ASSETS_PUBLIC_PATH}/${packageInfo.version}`, 41 | filename: 'popup.html', 42 | cache: false 43 | }), 44 | new HtmlWebpackPlugin({ 45 | title: 'Chummy Background', 46 | template: '../src/background/index.html', 47 | chunks: ['background', 'background.app', 'background.storage'], 48 | filename: 'background.html', 49 | cache: false 50 | }), 51 | new HtmlWebpackTagsPlugin({ 52 | scripts: [`background.dao_${packageInfo.version}.js`], 53 | publicPath: `${process.env.ASSETS_PUBLIC_PATH}/${packageInfo.version}`, 54 | append: true 55 | }), 56 | ...generateReports('prod', 'web') 57 | ] 58 | }; 59 | -------------------------------------------------------------------------------- /extension/webpack/dev.moz.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const CopyPlugin = require('copy-webpack-plugin'); 3 | 4 | const base = require('./dev.base.config'); 5 | const { formBaseManifest, generateReports, packageInfo } = require('./util'); 6 | 7 | module.exports = { 8 | ...base, 9 | output: { 10 | ...base.outputs, 11 | path: path.join(__dirname, '../dist/dev.moz') 12 | }, 13 | plugins: [ 14 | ...base.plugins, 15 | new CopyPlugin({ 16 | patterns: [ 17 | { 18 | from: '../manifest-base.json', 19 | to: './manifest.json', 20 | transform(content) { 21 | const baseManifest = formBaseManifest(content); 22 | return JSON.stringify({ 23 | ...baseManifest, 24 | browser_specific_settings: { 25 | gecko: { 26 | id: packageInfo.email // packageInfo.extensionId 27 | } 28 | }, 29 | permissions: [ 30 | ...baseManifest.permissions.slice(0, -1), 31 | 'http://localhost/*' // https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Match_patterns 32 | ] 33 | }); 34 | } 35 | }, 36 | { from: '../public/icon', to: './icon' } 37 | ] 38 | }), 39 | ...generateReports('dev', 'moz') 40 | ] 41 | }; 42 | -------------------------------------------------------------------------------- /extension/webpack/dev.web.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const CopyPlugin = require('copy-webpack-plugin'); 3 | 4 | const base = require('./dev.base.config'); 5 | const { formBaseManifest, generateReports } = require('./util'); 6 | 7 | module.exports = { 8 | ...base, 9 | output: { 10 | ...base.outputs, 11 | path: path.join(__dirname, '../dist/dev.web') 12 | }, 13 | plugins: [ 14 | ...base.plugins, 15 | new CopyPlugin({ 16 | patterns: [ 17 | { 18 | from: '../manifest-base.json', 19 | to: './manifest.json', 20 | transform(content) { 21 | return JSON.stringify({ 22 | ...formBaseManifest(content), 23 | key: 24 | 'MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAkZ7eURkxU+9PPkvVaVDUK88dZX39ZXKS9zRtpkAY6so1omoDZ6L3AWjy4e3ds8vz6OxeFcPzgycgTDVaPa2LAgvk2i+/eSbmFO8wvbp8Ce0/iPf2Vp0IqR1MQ+aRT+qD+6swNXvIJuAwFcuPP0LnDMe4veGVHyvI4uoelEVJ7P7RrnrskU4vscUAKHi5FygZLnfXzifrY2Vy6GA2wNipmd2I4+gW4ZnvSTzMs1u6s/k3LSg96cFxOl62AanEnuOcahUrCPl2/aTlU8OrOdgyiGvWKw4DxXsLC7XNZ589QvVP9uRdSsj7sAie/bGkTWRM3/NqYts8YhsMypWCCCxnQQIDAQAB' 25 | }); 26 | } 27 | }, 28 | { from: '../public/icon', to: './icon' } 29 | ] 30 | }), 31 | ...generateReports('dev', 'web') 32 | ] 33 | }; 34 | -------------------------------------------------------------------------------- /extension/webpack/gamma.moz.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 3 | const CopyPlugin = require('copy-webpack-plugin'); 4 | 5 | const base = require('./prod.base.config'); 6 | const { formBaseManifest, generateReports, packageInfo } = require('./util'); 7 | 8 | module.exports = { 9 | ...base, 10 | output: { 11 | ...base.output, 12 | path: path.join(__dirname, '../dist/gamma.moz') 13 | }, 14 | plugins: [ 15 | ...base.plugins, 16 | new CopyPlugin({ 17 | patterns: [ 18 | { 19 | from: '../manifest-base.json', 20 | to: './manifest.json', 21 | transform(content) { 22 | return JSON.stringify({ 23 | ...formBaseManifest(content), 24 | browser_specific_settings: { 25 | gecko: { 26 | id: packageInfo.email // packageInfo.extensionId 27 | } 28 | } 29 | }); 30 | } 31 | }, 32 | { from: '../public/icon', to: './icon' }, 33 | { from: '../key.pem', to: './key.pem' }, 34 | { from: '../public/index.html', to: '../index.html' } 35 | ] 36 | }), 37 | new HtmlWebpackPlugin({ 38 | title: 'Chummy', 39 | template: '../src/popup/index.html', 40 | chunks: ['popup'], 41 | filename: 'popup.html', 42 | cache: false 43 | }), 44 | new HtmlWebpackPlugin({ 45 | title: 'Chummy Background', 46 | template: '../src/background/index.html', 47 | chunks: [ 48 | 'background', 49 | 'background.app', 50 | 'background.dao', 51 | 'background.storage' 52 | ], 53 | filename: 'background.html', 54 | cache: false 55 | }), 56 | ...generateReports('gamma', 'moz') 57 | ] 58 | }; 59 | -------------------------------------------------------------------------------- /extension/webpack/gamma.web.cloud.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 3 | const CopyPlugin = require('copy-webpack-plugin'); 4 | const HtmlWebpackTagsPlugin = require('html-webpack-tags-plugin'); 5 | 6 | const base = require('./prod.base.config'); 7 | const { formBaseManifest, generateReports, packageInfo } = require('./util'); 8 | 9 | module.exports = { 10 | ...base, 11 | output: { 12 | ...base.output, 13 | path: path.join(__dirname, '../dist/gamma.web.cloud') 14 | }, 15 | plugins: [ 16 | ...base.plugins, 17 | new CopyPlugin({ 18 | patterns: [ 19 | { 20 | from: '../manifest-base.json', 21 | to: './manifest.json', 22 | transform(content) { 23 | return JSON.stringify({ 24 | ...formBaseManifest(content), 25 | key: 26 | 'MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAkZ7eURkxU+9PPkvVaVDUK88dZX39ZXKS9zRtpkAY6so1omoDZ6L3AWjy4e3ds8vz6OxeFcPzgycgTDVaPa2LAgvk2i+/eSbmFO8wvbp8Ce0/iPf2Vp0IqR1MQ+aRT+qD+6swNXvIJuAwFcuPP0LnDMe4veGVHyvI4uoelEVJ7P7RrnrskU4vscUAKHi5FygZLnfXzifrY2Vy6GA2wNipmd2I4+gW4ZnvSTzMs1u6s/k3LSg96cFxOl62AanEnuOcahUrCPl2/aTlU8OrOdgyiGvWKw4DxXsLC7XNZ589QvVP9uRdSsj7sAie/bGkTWRM3/NqYts8YhsMypWCCCxnQQIDAQAB' 27 | }); 28 | } 29 | }, 30 | { from: '../public/icon', to: './icon' }, 31 | { from: '../key.pem', to: './key.pem' }, 32 | { from: '../public/index.html', to: '../index.html' } 33 | ] 34 | }), 35 | new HtmlWebpackPlugin({ 36 | title: 'Chummy', 37 | template: '../src/popup/index.html', 38 | chunks: ['popup'], 39 | publicPath: `${process.env.ASSETS_PUBLIC_PATH}/${packageInfo.version}`, 40 | filename: 'popup.html', 41 | cache: false 42 | }), 43 | new HtmlWebpackPlugin({ 44 | title: 'Chummy Background', 45 | template: '../src/background/index.html', 46 | chunks: ['background', 'background.app', 'background.storage'], 47 | filename: 'background.html', 48 | cache: false 49 | }), 50 | new HtmlWebpackTagsPlugin({ 51 | scripts: [`background.dao_${packageInfo.version}.js`], 52 | publicPath: `${process.env.ASSETS_PUBLIC_PATH}/${packageInfo.version}`, 53 | append: true 54 | }), 55 | ...generateReports('gamma', 'web.cloud') 56 | ] 57 | }; 58 | -------------------------------------------------------------------------------- /extension/webpack/gamma.web.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 3 | const CopyPlugin = require('copy-webpack-plugin'); 4 | 5 | const base = require('./prod.base.config'); 6 | const { formBaseManifest, generateReports } = require('./util'); 7 | 8 | module.exports = { 9 | ...base, 10 | output: { 11 | ...base.output, 12 | path: path.join(__dirname, '../dist/gamma.web') 13 | }, 14 | plugins: [ 15 | ...base.plugins, 16 | new CopyPlugin({ 17 | patterns: [ 18 | { 19 | from: '../manifest-base.json', 20 | to: './manifest.json', 21 | transform(content) { 22 | return JSON.stringify({ 23 | ...formBaseManifest(content), 24 | key: 25 | 'MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAkZ7eURkxU+9PPkvVaVDUK88dZX39ZXKS9zRtpkAY6so1omoDZ6L3AWjy4e3ds8vz6OxeFcPzgycgTDVaPa2LAgvk2i+/eSbmFO8wvbp8Ce0/iPf2Vp0IqR1MQ+aRT+qD+6swNXvIJuAwFcuPP0LnDMe4veGVHyvI4uoelEVJ7P7RrnrskU4vscUAKHi5FygZLnfXzifrY2Vy6GA2wNipmd2I4+gW4ZnvSTzMs1u6s/k3LSg96cFxOl62AanEnuOcahUrCPl2/aTlU8OrOdgyiGvWKw4DxXsLC7XNZ589QvVP9uRdSsj7sAie/bGkTWRM3/NqYts8YhsMypWCCCxnQQIDAQAB' 26 | }); 27 | } 28 | }, 29 | { from: '../public/icon', to: './icon' }, 30 | { from: '../key.pem', to: './key.pem' }, 31 | { from: '../public/index.html', to: '../index.html' } 32 | ] 33 | }), 34 | new HtmlWebpackPlugin({ 35 | title: 'Chummy', 36 | template: '../src/popup/index.html', 37 | chunks: ['popup'], 38 | filename: 'popup.html', 39 | cache: false 40 | }), 41 | new HtmlWebpackPlugin({ 42 | title: 'Chummy Background', 43 | template: '../src/background/index.html', 44 | chunks: [ 45 | 'background', 46 | 'background.app', 47 | 'background.dao', 48 | 'background.storage' 49 | ], 50 | filename: 'background.html', 51 | cache: false 52 | }), 53 | ...generateReports('gamma', 'web') 54 | ] 55 | }; 56 | -------------------------------------------------------------------------------- /extension/webpack/prod.chrome.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const CopyPlugin = require('copy-webpack-plugin'); 3 | 4 | const base = require('./prod.web.config'); 5 | const { formBaseManifest, generateReports } = require('./util'); 6 | 7 | module.exports = { 8 | ...base, 9 | output: { 10 | ...base.output, 11 | path: path.join(__dirname, '../dist/chrome'), 12 | publicPath: '/' 13 | }, 14 | plugins: [ 15 | ...base.plugins, 16 | new CopyPlugin({ 17 | patterns: [ 18 | { 19 | from: '../manifest-base.json', 20 | to: './manifest.json', 21 | transform(content) { 22 | return JSON.stringify({ 23 | ...formBaseManifest(content), 24 | key: 25 | 'MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAkZ7eURkxU+9PPkvVaVDUK88dZX39ZXKS9zRtpkAY6so1omoDZ6L3AWjy4e3ds8vz6OxeFcPzgycgTDVaPa2LAgvk2i+/eSbmFO8wvbp8Ce0/iPf2Vp0IqR1MQ+aRT+qD+6swNXvIJuAwFcuPP0LnDMe4veGVHyvI4uoelEVJ7P7RrnrskU4vscUAKHi5FygZLnfXzifrY2Vy6GA2wNipmd2I4+gW4ZnvSTzMs1u6s/k3LSg96cFxOl62AanEnuOcahUrCPl2/aTlU8OrOdgyiGvWKw4DxXsLC7XNZ589QvVP9uRdSsj7sAie/bGkTWRM3/NqYts8YhsMypWCCCxnQQIDAQAB' 26 | }); 27 | } 28 | } 29 | ] 30 | }), 31 | ...generateReports('prod', 'chrome') 32 | ] 33 | }; 34 | -------------------------------------------------------------------------------- /extension/webpack/prod.edge.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const CopyPlugin = require('copy-webpack-plugin'); 3 | 4 | const base = require('./prod.web.config'); 5 | const { formBaseManifest, generateReports } = require('./util'); 6 | 7 | module.exports = { 8 | ...base, 9 | output: { 10 | ...base.output, 11 | path: path.join(__dirname, '../dist/edge'), 12 | publicPath: '/' 13 | }, 14 | plugins: [ 15 | ...base.plugins, 16 | new CopyPlugin({ 17 | patterns: [ 18 | { 19 | from: '../manifest-base.json', 20 | to: './manifest.json', 21 | transform(content) { 22 | return JSON.stringify({ 23 | ...formBaseManifest(content) 24 | // key: 'bpobpfbpikaikajipjoaoiijnkjikpfe' 25 | }); 26 | } 27 | } 28 | ] 29 | }), 30 | ...generateReports('prod', 'edge') 31 | ] 32 | }; 33 | -------------------------------------------------------------------------------- /extension/webpack/prod.moz.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 3 | const CopyPlugin = require('copy-webpack-plugin'); 4 | 5 | const base = require('./prod.base.config'); 6 | const { formBaseManifest, generateReports, packageInfo } = require('./util'); 7 | 8 | module.exports = { 9 | ...base, 10 | output: { 11 | ...base.output, 12 | path: path.join(__dirname, '../dist/moz') 13 | }, 14 | plugins: [ 15 | ...base.plugins, 16 | new CopyPlugin({ 17 | patterns: [ 18 | { 19 | from: '../manifest-base.json', 20 | to: './manifest.json', 21 | transform(content) { 22 | return JSON.stringify({ 23 | ...formBaseManifest(content), 24 | browser_specific_settings: { 25 | gecko: { 26 | id: packageInfo.email // packageInfo.extensionId 27 | } 28 | } 29 | }); 30 | } 31 | }, 32 | { from: '../public/icon', to: './icon' }, 33 | { from: '../key.pem', to: './key.pem' }, 34 | { from: '../public/index.html', to: '../index.html' } 35 | ] 36 | }), 37 | new HtmlWebpackPlugin({ 38 | title: 'Chummy', 39 | template: '../src/popup/index.html', 40 | chunks: ['popup'], 41 | filename: 'popup.html', 42 | cache: false 43 | }), 44 | new HtmlWebpackPlugin({ 45 | title: 'Chummy Background', 46 | template: '../src/background/index.html', 47 | chunks: [ 48 | 'background', 49 | 'background.app', 50 | 'background.dao', 51 | 'background.storage' 52 | ], 53 | filename: 'background.html', 54 | cache: false 55 | }), 56 | ...generateReports('prod', 'moz') 57 | ] 58 | }; 59 | -------------------------------------------------------------------------------- /extension/webpack/prod.opera.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const CopyPlugin = require('copy-webpack-plugin'); 3 | 4 | const base = require('./prod.web.config'); 5 | const { formBaseManifest, generateReports } = require('./util'); 6 | 7 | module.exports = { 8 | ...base, 9 | output: { 10 | ...base.output, 11 | path: path.join(__dirname, '../dist/opera'), 12 | publicPath: '/' 13 | }, 14 | plugins: [ 15 | ...base.plugins, 16 | new CopyPlugin({ 17 | patterns: [ 18 | { 19 | from: '../manifest-base.json', 20 | to: './manifest.json', 21 | transform(content) { 22 | return JSON.stringify({ 23 | ...formBaseManifest(content) 24 | // key: 25 | // 'MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAkZ7eURkxU+9PPkvVaVDUK88dZX39ZXKS9zRtpkAY6so1omoDZ6L3AWjy4e3ds8vz6OxeFcPzgycgTDVaPa2LAgvk2i+/eSbmFO8wvbp8Ce0/iPf2Vp0IqR1MQ+aRT+qD+6swNXvIJuAwFcuPP0LnDMe4veGVHyvI4uoelEVJ7P7RrnrskU4vscUAKHi5FygZLnfXzifrY2Vy6GA2wNipmd2I4+gW4ZnvSTzMs1u6s/k3LSg96cFxOl62AanEnuOcahUrCPl2/aTlU8OrOdgyiGvWKw4DxXsLC7XNZ589QvVP9uRdSsj7sAie/bGkTWRM3/NqYts8YhsMypWCCCxnQQIDAQAB' 26 | }); 27 | } 28 | } 29 | ] 30 | }), 31 | ...generateReports('prod', 'opera') 32 | ] 33 | }; 34 | -------------------------------------------------------------------------------- /extension/webpack/prod.web.config.js: -------------------------------------------------------------------------------- 1 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 2 | const CopyPlugin = require('copy-webpack-plugin'); 3 | 4 | const base = require('./prod.base.config'); 5 | 6 | module.exports = { 7 | ...base, 8 | plugins: [ 9 | ...base.plugins, 10 | new CopyPlugin({ 11 | patterns: [ 12 | { from: '../public/icon', to: './icon' }, 13 | { from: '../key.pem', to: './key.pem' }, 14 | { from: '../public/index.html', to: '../index.html' } 15 | ] 16 | }), 17 | new HtmlWebpackPlugin({ 18 | title: 'Chummy', 19 | template: '../src/popup/index.html', 20 | chunks: ['popup'], 21 | filename: 'popup.html', 22 | cache: false 23 | }), 24 | new HtmlWebpackPlugin({ 25 | title: 'Chummy Background', 26 | template: '../src/background/index.html', 27 | chunks: [ 28 | 'background', 29 | 'background.app', 30 | 'background.dao', 31 | 'background.storage' 32 | ], 33 | filename: 'background.html', 34 | cache: false 35 | }) 36 | ] 37 | }; 38 | -------------------------------------------------------------------------------- /extension/webpack/util.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer'); 3 | const AssetsPlugin = require('assets-webpack-plugin'); 4 | 5 | const packageInfo = JSON.parse( 6 | JSON.stringify( 7 | // eslint-disable-next-line import/no-dynamic-require 8 | require(path.join(__dirname, '../package.json')) 9 | ) 10 | ); 11 | 12 | const stripDomain = (s) => { 13 | // Remove https:// and https://www. from all urls 14 | return s.replace(/^(http|https):\/\/(www.)?/, ''); 15 | }; 16 | 17 | // All shared properties of manifest.json go here. 18 | // eslint-disable-next-line import/prefer-default-export 19 | function formBaseManifest(content) { 20 | const baseManifest = JSON.parse(content); 21 | return { 22 | ...baseManifest, 23 | description: packageInfo.description, 24 | version: packageInfo.version, 25 | permissions: [ 26 | ...baseManifest.permissions, 27 | ...(process.env.NODE_ENV === 'development' 28 | ? [process.env.WEBSITE_BASE_URL] 29 | : [ 30 | `https://${stripDomain(process.env.WEBSITE_BASE_URL)}*`, // always secure https:// 31 | `https://www.${stripDomain(process.env.WEBSITE_BASE_URL)}*` // include www. version 32 | ]) 33 | ] 34 | }; 35 | } 36 | 37 | const generateReports = (domain, browser) => { 38 | return [ 39 | new BundleAnalyzerPlugin({ 40 | analyzerMode: 'static', 41 | reportFilename: path.join( 42 | __dirname, 43 | `./reports/report.${domain}.${browser}.html` 44 | ), 45 | statsFilename: path.join( 46 | __dirname, 47 | `./reports/stats.${domain}.${browser}.json` 48 | ), 49 | generateStatsFile: true, 50 | openAnalyzer: false 51 | }), 52 | new AssetsPlugin({ 53 | filename: `assets.${domain}.${browser}.json`, 54 | path: path.join(__dirname, './reports') 55 | }) 56 | ]; 57 | }; 58 | 59 | module.exports.formBaseManifest = formBaseManifest; 60 | module.exports.generateReports = generateReports; 61 | module.exports.stripDomain = stripDomain; 62 | module.exports.packageInfo = packageInfo; 63 | --------------------------------------------------------------------------------