Showing preview only (577K chars total). Download the full file or copy to clipboard to get everything.
Repository: leinelissen/aeon
Branch: master
Commit: 53ac8fb259be
Files: 184
Total size: 529.3 KB
Directory structure:
gitextract_uqsqlvwh/
├── .eslintrc.json
├── .gitbook.yaml
├── .github/
│ └── workflows/
│ ├── build.yml
│ ├── codeql-analysis.yml
│ ├── electronegativity.yml
│ └── release-notes.yml
├── .gitignore
├── .husky/
│ ├── pre-commit
│ └── pre-push
├── .npmrc
├── .vscode/
│ ├── debug-launcher.sh
│ ├── launch.json
│ └── settings.json
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── SECURITY.md
├── data/
│ └── .gitignore
├── docs/
│ ├── README.md
│ ├── SUMMARY.md
│ ├── extending-aeon/
│ │ ├── architecture/
│ │ │ ├── README.md
│ │ │ └── providers.md
│ │ ├── local-development.md
│ │ └── reporting-issues.md
│ └── using-aeon/
│ ├── getting-started.md
│ └── installation.md
├── entitlements.plist
├── forge.config.js
├── package.json
├── playwright.config.ts
├── renovate.json
├── scripts/
│ ├── prepareNodegit.js
│ └── setupMacOSCertificates.sh
├── src/
│ ├── app/
│ │ ├── assets/
│ │ │ └── open-data-rights.ts
│ │ ├── components/
│ │ │ ├── App.tsx
│ │ │ ├── Button.tsx
│ │ │ ├── Code.tsx
│ │ │ ├── IconBadge.tsx
│ │ │ ├── Input.tsx
│ │ │ ├── Loading.tsx
│ │ │ ├── Menu.tsx
│ │ │ ├── Modal/
│ │ │ │ ├── Menu.tsx
│ │ │ │ └── index.tsx
│ │ │ ├── NoData.tsx
│ │ │ ├── PanelGrid.tsx
│ │ │ ├── RightSideOverlay.tsx
│ │ │ ├── Timestamp.tsx
│ │ │ ├── Tooltip.tsx
│ │ │ ├── Tour/
│ │ │ │ ├── index.tsx
│ │ │ │ ├── steps.tsx
│ │ │ │ └── useTour.tsx
│ │ │ ├── Typography.tsx
│ │ │ └── Utility.tsx
│ │ ├── index.ejs
│ │ ├── index.tsx
│ │ ├── polyfill.ts
│ │ ├── preload.ts
│ │ ├── screens/
│ │ │ ├── Accounts/
│ │ │ │ ├── components/
│ │ │ │ │ ├── AccountOverlay.tsx
│ │ │ │ │ ├── EmailProvider.tsx
│ │ │ │ │ └── NewAccountModal.tsx
│ │ │ │ ├── getDescription.ts
│ │ │ │ └── index.tsx
│ │ │ ├── Data/
│ │ │ │ ├── components/
│ │ │ │ │ └── DatumOverlay.tsx
│ │ │ │ ├── index.tsx
│ │ │ │ ├── styles.tsx
│ │ │ │ └── types.tsx
│ │ │ ├── Erasure/
│ │ │ │ ├── Emails.tsx
│ │ │ │ ├── generateEmail.ts
│ │ │ │ └── index.tsx
│ │ │ ├── Graph/
│ │ │ │ ├── calculateGraph.ts
│ │ │ │ ├── explainer.tsx
│ │ │ │ ├── index.tsx
│ │ │ │ ├── renderNode.ts
│ │ │ │ └── style.ts
│ │ │ ├── Onboarding/
│ │ │ │ └── index.tsx
│ │ │ ├── Settings/
│ │ │ │ ├── email/
│ │ │ │ │ ├── components/
│ │ │ │ │ │ ├── ImapInitialiser.tsx
│ │ │ │ │ │ └── NewAccountModal.tsx
│ │ │ │ │ └── index.tsx
│ │ │ │ └── index.tsx
│ │ │ ├── Timeline/
│ │ │ │ ├── components/
│ │ │ │ │ ├── Commit.tsx
│ │ │ │ │ └── Diff.tsx
│ │ │ │ └── index.tsx
│ │ │ ├── index.tsx
│ │ │ └── types.ts
│ │ ├── store/
│ │ │ ├── accounts/
│ │ │ │ ├── actions.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── selectors.ts
│ │ │ │ └── types.ts
│ │ │ ├── data/
│ │ │ │ ├── actions.ts
│ │ │ │ ├── index.ts
│ │ │ │ └── selectors.ts
│ │ │ ├── email/
│ │ │ │ ├── actions.ts
│ │ │ │ ├── index.ts
│ │ │ │ └── selectors.ts
│ │ │ ├── index.ts
│ │ │ ├── migrations.ts
│ │ │ ├── new-commits/
│ │ │ │ ├── actions.ts
│ │ │ │ └── index.ts
│ │ │ ├── onboarding/
│ │ │ │ ├── actions.ts
│ │ │ │ └── index.ts
│ │ │ ├── persist.ts
│ │ │ └── telemetry/
│ │ │ ├── actions.ts
│ │ │ └── index.ts
│ │ ├── styles/
│ │ │ ├── global.css
│ │ │ ├── index.ts
│ │ │ ├── snippets.ts
│ │ │ └── theme.css
│ │ └── utilities/
│ │ ├── DataType.tsx
│ │ ├── Email.ts
│ │ ├── Providers.ts
│ │ ├── Repository.ts
│ │ ├── convertMetaToObject.ts
│ │ ├── env.ts
│ │ ├── isValidUrl.ts
│ │ └── usePrevious.ts
│ ├── icon.icns
│ ├── main/
│ │ ├── email-client/
│ │ │ ├── bridge.ts
│ │ │ ├── gmail/
│ │ │ │ ├── index.ts
│ │ │ │ ├── oauth.ts
│ │ │ │ └── types.ts
│ │ │ ├── imap/
│ │ │ │ └── index.ts
│ │ │ ├── index.ts
│ │ │ ├── outlook/
│ │ │ │ ├── index.ts
│ │ │ │ └── oauth.ts
│ │ │ └── types.ts
│ │ ├── index.ts
│ │ ├── initialise.ts
│ │ ├── lib/
│ │ │ ├── app-path.ts
│ │ │ ├── constants.ts
│ │ │ ├── create-secure-window.ts
│ │ │ ├── crypto-fs/
│ │ │ │ └── index.ts
│ │ │ ├── logger.ts
│ │ │ ├── map-map.ts
│ │ │ ├── map-object-to-key-value.ts
│ │ │ ├── notifications/
│ │ │ │ ├── index.ts
│ │ │ │ └── types.ts
│ │ │ ├── oauth.ts
│ │ │ ├── object-to-map.ts
│ │ │ ├── persisted-map.ts
│ │ │ ├── protocol-handler.ts
│ │ │ ├── repository/
│ │ │ │ ├── bridge.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── types.ts
│ │ │ │ └── utilities/
│ │ │ │ ├── diff-map.ts
│ │ │ │ ├── generate-diff.ts
│ │ │ │ ├── generate-parsed-commit.ts
│ │ │ │ ├── parse-csv.ts
│ │ │ │ ├── parse-open-data-rights.ts
│ │ │ │ └── parse-schema.ts
│ │ │ ├── unwrap-provider-source.ts
│ │ │ └── window-store.ts
│ │ ├── providers/
│ │ │ ├── bridge.ts
│ │ │ ├── facebook/
│ │ │ │ ├── index.ts
│ │ │ │ └── parser.ts
│ │ │ ├── index.ts
│ │ │ ├── instagram/
│ │ │ │ ├── index.ts
│ │ │ │ ├── parser.ts
│ │ │ │ └── urls.json
│ │ │ ├── linkedin/
│ │ │ │ ├── index.ts
│ │ │ │ └── parser.ts
│ │ │ ├── open-data-rights/
│ │ │ │ └── index.ts
│ │ │ ├── parsers.ts
│ │ │ ├── spotify/
│ │ │ │ ├── index.ts
│ │ │ │ └── parser.ts
│ │ │ └── types/
│ │ │ ├── Data.ts
│ │ │ ├── Events.ts
│ │ │ ├── Provider.ts
│ │ │ └── index.ts
│ │ ├── store.ts
│ │ └── updates.ts
│ └── typings/
│ ├── cytoscape-fcose.d.ts
│ ├── fonts.d.ts
│ ├── images.d.ts
│ └── redux-persist.d.ts
├── test/
│ ├── .gitignore
│ ├── spec.ts
│ └── utilities/
│ ├── getRandomNode.ts
│ └── getRoute.ts
├── tsconfig.json
├── webpack.main.config.js
├── webpack.plugins.js
├── webpack.renderer.config.js
└── webpack.rules.js
================================================
FILE CONTENTS
================================================
================================================
FILE: .eslintrc.json
================================================
{
"env": {
"browser": true,
"es6": true,
"node": true
},
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/eslint-recommended",
"plugin:@typescript-eslint/recommended",
"plugin:import/errors",
"plugin:import/warnings",
"plugin:import/typescript",
"plugin:react/recommended",
"airbnb-typescript",
"plugin:jsx-a11y/recommended"
],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"tsConfigRootDir": "./",
"project": "./tsconfig.json"
},
"plugins": [
"@typescript-eslint",
"import",
"deprecation",
"jsx-a11y"
],
"rules": {
"indent": ["error", 4, {
"SwitchCase": 1
}],
"react/jsx-indent": ["error", 4],
"react/jsx-indent-props": ["error", 4],
"@typescript-eslint/indent": ["error", 4],
"react/prop-types": "off",
"arrow-parens": "error",
"deprecation/deprecation": "warn"
},
"settings": {
"import/resolver": {
"typescript": {}, // this loads <rootdir>/tsconfig.json to eslint
"node": {
"paths": [ "src" ]
}
},
"import/core-modules": [ "electron", "nodegit" ],
"react": {
"version": "detect"
}
}
}
================================================
FILE: .gitbook.yaml
================================================
root: ./docs/
================================================
FILE: .github/workflows/build.yml
================================================
# yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json
name: Build
on: [push]
jobs:
lint:
name: 'Lint'
runs-on: macos-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 1
submodules: true
- name: Setup Node
uses: actions/setup-node@v3
with:
node-version: '16'
- uses: actions/cache@v3
with:
path: '**/node_modules'
key: linter-node_modules-${{hashFiles('**/package-lock.json') }}
- name: Install dependencies
env:
JOBS: 'max'
run: npm install
- name: Run linter
run: npm run lint
build:
name: Build (${{ matrix.os }} - ${{ matrix.arch }})
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
# Build for supported platforms
# https://github.com/electron/electron-packager/blob/ebcbd439ff3e0f6f92fa880ff28a8670a9bcf2ab/src/targets.js#L9
include:
- os: ubuntu-20.04
arch: arm64
- os: ubuntu-20.04
arch: x64
- os: macOS-11
arch: arm64
openssl_dir: '/tmp/openssl@1.1/1.1.1l_1'
- os: macOS-11
arch: x64
openssl_dir: '/usr/local/opt/openssl@1.1'
- os: windows-2019
arch: x64
openssl_dir: 'C:\Program Files\OpenSSL-Win64\'
# - os: ubuntu-20.04
# arch: armv7l
# - os: windows-2019
# arch: arm64
# openssl_dir: 'C:\Program Files\OpenSSL-Win64\'
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 1
submodules: true
- name: Setup Node
uses: actions/setup-node@v3
with:
node-version: '16'
- uses: actions/cache@v3
with:
path: '**/node_modules'
key: ${{ matrix.os }}-${{ matrix.arch }}-node_modules-${{hashFiles('**/package-lock.json') }}
- name: Install Build dependencies (macOS-arm64)
if: |
matrix.os == 'macOS-11'
&& matrix.arch == 'arm64'
run: |
curl -L -H "Authorization: Bearer QQ==" -o /tmp/openssl-1.1.1l_1-arm64.tar.gz https://ghcr.io/v2/homebrew/core/openssl/1.1/blobs/sha256:8f5b0bee61c1570b9f0fc0a21d6c322e904ae7975bdaada5787451d18e9677a6
tar -xvf /tmp/openssl-1.1.1l_1-arm64.tar.gz -C /tmp
- name: Install Build dependencies (Windows)
if: matrix.os == 'windows-2019'
run: choco install openssl
- name: Install Build dependencies (Ubuntu)
if: matrix.os == 'ubuntu-20.04'
run: |
sudo apt update
sudo apt install libkrb5-dev
- name: Install dependencies
env:
JOBS: 'max'
npm_config_openssl_dir: ${{ matrix.openssl_dir }}
run: |
npm install
- name: Rebuild native modules
env:
JOBS: 'max'
npm_config_openssl_dir: ${{ matrix.openssl_dir }}
run: |
npm run rebuild:native-modules -- --arch=${{ matrix.arch }}
npm run prepare:nodegit
- name: Load Developer Certificates (macOS)
if: |
startsWith(github.ref, 'refs/tags/')
&& matrix.os == 'macOS-11'
env:
MACOS_CERT_P12: ${{ secrets.MACOS_CERT_P12 }}
MACOS_CERT_PASSWORD: ${{ secrets.MACOS_CERT_PASSWORD }}
run:
chmod +x scripts/setupMacOSCertificates.sh && ./scripts/setupMacOSCertificates.sh
- name: Compile and Sign
if: startsWith(github.ref, 'refs/tags/')
env:
npm_config_openssl_dir: ${{ matrix.openssl_dir }}
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_ID_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }}
MACOS_CERT_P12: ${{ secrets.MACOS_CERT_P12 }}
MACOS_CERT_PASSWORD: ${{ secrets.MACOS_CERT_PASSWORD }}
GMAIL_OAUTH_CLIENT_ID: ${{ secrets.GMAIL_OAUTH_CLIENT_ID }}
GMAIL_OAUTH_CLIENT_SECRET: ${{ secrets.GMAIL_OAUTH_CLIENT_SECRET }}
run:
npm run make -- --arch=${{ matrix.arch }}
- name: Compile
if: startsWith(github.ref, 'refs/tags/') == false
env:
npm_config_openssl_dir: ${{ matrix.openssl_dir }}
GMAIL_OAUTH_CLIENT_ID: ${{ secrets.GMAIL_OAUTH_CLIENT_ID }}
GMAIL_OAUTH_CLIENT_SECRET: ${{ secrets.GMAIL_OAUTH_CLIENT_SECRET }}
run: npm run make -- --arch=${{ matrix.arch }}
- name: Test
if: |
matrix.os != 'ubuntu-20.04'
&& matrix.arch == 'x64'
&& matrix.os != 'macOS-11'
&& matrix.os != 'windows-2019'
run: npm run test
- name: Test (Ubuntu)
if: |
matrix.os == 'ubuntu-20.04'
&& matrix.arch == 'x64'
run: xvfb-run --auto-servernum -- npm test
- name: Upload artifacts
if: ${{ always() }}
uses: actions/upload-artifact@v3
with:
name: ${{ matrix.os }}-${{ matrix.arch }}
path: |
out/make
test/output
- name: Release
uses: softprops/action-gh-release@v1
if: startsWith(github.ref, 'refs/tags/')
with:
files: "out/make/**"
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
================================================
FILE: .github/workflows/codeql-analysis.yml
================================================
# For most projects, this workflow file will not need changing; you simply need
# to commit it to your repository.
#
# You may wish to alter this file to override the set of languages analyzed,
# or to provide custom queries or build logic.
name: "CodeQL"
on:
push:
branches: [master]
pull_request:
# The branches below must be a subset of the branches above
branches: [master]
schedule:
- cron: '0 5 * * 1'
jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
# Override automatic language detection by changing the below list
# Supported options are ['csharp', 'cpp', 'go', 'java', 'javascript', 'python']
language: ['javascript']
# Learn more...
# https://docs.github.com/en/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#overriding-automatic-language-detection
steps:
- name: Checkout repository
uses: actions/checkout@v3
with:
# We must fetch at least the immediate parents so that if this is
# a pull request then we can checkout the head.
fetch-depth: 2
# If this run was triggered by a pull request event, then checkout
# the head of the pull request instead of the merge commit.
- run: git checkout HEAD^2
if: ${{ github.event_name == 'pull_request' }}
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v2
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# queries: ./path/to/local/query, your-org/your-repo/queries@main
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v2
# ℹ️ Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
# and modify them (or add more) to build your code if your project
# uses a compiled language
#- run: |
# make bootstrap
# make release
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v2
================================================
FILE: .github/workflows/electronegativity.yml
================================================
name: "Electronegativity"
on:
push:
jobs:
build_job:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: '12'
- uses: doyensec/electronegativity-action@v1.1
- name: Upload sarif
uses: github/codeql-action/upload-sarif@v2
with:
sarif_file: ../results
================================================
FILE: .github/workflows/release-notes.yml
================================================
name: Release Notes
on:
release:
types:
- created
jobs:
create_notes:
runs-on: ubuntu-latest
steps:
- name: Context
env:
GITHUB_CONTEXT: ${{ toJson(github) }}
run: echo "$GITHUB_CONTEXT"
- uses: actions/checkout@v3
with:
fetch-depth: 1
- name: Setup Node
uses: actions/setup-node@v3
with:
node-version: 14.x
- name: Run Gren
env:
GREN_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
npx github-release-notes release -o
================================================
FILE: .gitignore
================================================
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
.DS_Store
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
.env.test
# parcel-bundler cache (https://parceljs.org/)
.cache
# next.js build output
.next
# nuxt.js build output
.nuxt
# vuepress build output
.vuepress/dist
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# Webpack
.webpack/
# Electron-Forge
out/
================================================
FILE: .husky/pre-commit
================================================
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
# empty for now
================================================
FILE: .husky/pre-push
================================================
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
# Check if there are any outstanding changes
# NOTE: This is necessary as husky will run checks on pushing,
# while there might be changes that are not yet committed interfering
# with the tests and linter
git diff HEAD --quiet
npm run lint
npm run make
npm test
================================================
FILE: .npmrc
================================================
legacy-peer-deps=true
================================================
FILE: .vscode/debug-launcher.sh
================================================
#!/usr/bin/env bash
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
ARGS=$@
ARGS=${ARGS// /\~ \~}
node "$DIR/../node_modules/@electron-forge/cli/dist/electron-forge-start" --vscode -- \~$ARGS\~
================================================
FILE: .vscode/launch.json
================================================
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "pwa-node",
"request": "launch",
"name": "Electron Main",
"program": "${workspaceFolder}/node_modules/@electron-forge/cli/dist/electron-forge-start.js",
"args": [
"--vscode"
],
"cwd": "${workspaceFolder}",
"sourceMaps": true,
"windows": {
"runtimeExecutable": "${workspaceFolder}/node_modules/.bin/electron-forge-vscode-win.cmd",
"console": "internalConsole",
"outputCapture": "std",
},
"resolveSourceMapLocations": [
"${workspaceFolder}/**",
"!**/node_modules/**"
]
}
]
}
================================================
FILE: .vscode/settings.json
================================================
{
"eslint.enable": true
}
================================================
FILE: CODE_OF_CONDUCT.md
================================================
# Contributor Covenant Code of Conduct
## Our Pledge
In the interest of fostering an open and welcoming environment, we as
contributors and maintainers pledge to making participation in our project and
our community a harassment-free experience for everyone, regardless of age, body
size, disability, ethnicity, sex characteristics, gender identity and expression,
level of experience, education, socio-economic status, nationality, personal
appearance, race, religion, or sexual identity and orientation.
## Our Standards
Examples of behavior that contributes to creating a positive environment
include:
* Using welcoming and inclusive language
* Being respectful of differing viewpoints and experiences
* Gracefully accepting constructive criticism
* Focusing on what is best for the community
* Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
* The use of sexualized language or imagery and unwelcome sexual attention or
advances
* Trolling, insulting/derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or electronic
address, without explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Our Responsibilities
Project maintainers are responsible for clarifying the standards of acceptable
behavior and are expected to take appropriate and fair corrective action in
response to any instances of unacceptable behavior.
Project maintainers have the right and responsibility to remove, edit, or
reject comments, commits, code, wiki edits, issues, and other contributions
that are not aligned to this Code of Conduct, or to ban temporarily or
permanently any contributor for other behaviors that they deem inappropriate,
threatening, offensive, or harmful.
## Scope
This Code of Conduct applies both within project spaces and in public spaces
when an individual is representing the project or its community. Examples of
representing a project or community include using an official project e-mail
address, posting via an official social media account, or acting as an appointed
representative at an online or offline event. Representation of a project may be
further defined and clarified by project maintainers.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported by contacting the project team at lei@codified.nl. All
complaints will be reviewed and investigated and will result in a response that
is deemed necessary and appropriate to the circumstances. The project team is
obligated to maintain confidentiality with regard to the reporter of an incident.
Further details of specific enforcement policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good
faith may face temporary or permanent repercussions as determined by other
members of the project's leadership.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
[homepage]: https://www.contributor-covenant.org
For answers to common questions about this code of conduct, see
https://www.contributor-covenant.org/faq
================================================
FILE: CONTRIBUTING.md
================================================
# Contributing to Aeon
Aeon welcomes all contributions to it warmly and with open arms.
## GitHub Issues
GitHub issues is our main method for tracking project progress, feature requests, bugs, etc. If you have an idea for a new implementation, just open a new GitHub issue, and we'll try to get you on the way to help with contributing.
## Documentation
Documentation for inner workings of Aeon are available as part of the [Aeon documentation](https://docs.aeon.technology). Please refer to this document when you are trying to implement new features. In case you get stuck, or need help, please create a GitHub issue!
## Coding Conventions
All code is written with four-spaced tabs. We have ESLint setup, and we kindly request you to lint your code before submitting it for a pull request: `npm run lint`.
================================================
FILE: LICENSE
================================================
European Union Public Licence
V. 1.2
EUPL © the European Union 2007, 2016
This European Union Public Licence (the ‘EUPL’) applies to the Work (as
defined below) which is provided under the terms of this Licence. Any use of
the Work, other than as authorised under this Licence is prohibited (to the
extent such use is covered by a right of the copyright holder of the Work).
The Work is provided under the terms of this Licence when the Licensor (as
defined below) has placed the following notice immediately following the
copyright notice for the Work: “Licensed under the EUPL”, or has expressed by
any other means his willingness to license under the EUPL.
1. Definitions
In this Licence, the following terms have the following meaning:
— ‘The Licence’: this Licence.
— ‘The Original Work’: the work or software distributed or communicated by the
‘Licensor under this Licence, available as Source Code and also as
‘Executable Code as the case may be.
— ‘Derivative Works’: the works or software that could be created by the
‘Licensee, based upon the Original Work or modifications thereof. This
‘Licence does not define the extent of modification or dependence on the
‘Original Work required in order to classify a work as a Derivative Work;
‘this extent is determined by copyright law applicable in the country
‘mentioned in Article 15.
— ‘The Work’: the Original Work or its Derivative Works.
— ‘The Source Code’: the human-readable form of the Work which is the most
convenient for people to study and modify.
— ‘The Executable Code’: any code which has generally been compiled and which
is meant to be interpreted by a computer as a program.
— ‘The Licensor’: the natural or legal person that distributes or communicates
the Work under the Licence.
— ‘Contributor(s)’: any natural or legal person who modifies the Work under
the Licence, or otherwise contributes to the creation of a Derivative Work.
— ‘The Licensee’ or ‘You’: any natural or legal person who makes any usage of
the Work under the terms of the Licence.
— ‘Distribution’ or ‘Communication’: any act of selling, giving, lending,
renting, distributing, communicating, transmitting, or otherwise making
available, online or offline, copies of the Work or providing access to its
essential functionalities at the disposal of any other natural or legal
person.
2. Scope of the rights granted by the Licence
The Licensor hereby grants You a worldwide, royalty-free, non-exclusive,
sublicensable licence to do the following, for the duration of copyright
vested in the Original Work:
— use the Work in any circumstance and for all usage,
— reproduce the Work,
— modify the Work, and make Derivative Works based upon the Work,
— communicate to the public, including the right to make available or display
the Work or copies thereof to the public and perform publicly, as the case
may be, the Work,
— distribute the Work or copies thereof,
— lend and rent the Work or copies thereof,
— sublicense rights in the Work or copies thereof.
Those rights can be exercised on any media, supports and formats, whether now
known or later invented, as far as the applicable law permits so.
In the countries where moral rights apply, the Licensor waives his right to
exercise his moral right to the extent allowed by law in order to make
effective the licence of the economic rights here above listed.
The Licensor grants to the Licensee royalty-free, non-exclusive usage rights
to any patents held by the Licensor, to the extent necessary to make use of
the rights granted on the Work under this Licence.
3. Communication of the Source Code
The Licensor may provide the Work either in its Source Code form, or as
Executable Code. If the Work is provided as Executable Code, the Licensor
provides in addition a machine-readable copy of the Source Code of the Work
along with each copy of the Work that the Licensor distributes or indicates,
in a notice following the copyright notice attached to the Work, a repository
where the Source Code is easily and freely accessible for as long as the
Licensor continues to distribute or communicate the Work.
4. Limitations on copyright
Nothing in this Licence is intended to deprive the Licensee of the benefits
from any exception or limitation to the exclusive rights of the rights owners
in the Work, of the exhaustion of those rights or of other applicable
limitations thereto.
5. Obligations of the Licensee
The grant of the rights mentioned above is subject to some restrictions and
obligations imposed on the Licensee. Those obligations are the following:
Attribution right: The Licensee shall keep intact all copyright, patent or
trademarks notices and all notices that refer to the Licence and to the
disclaimer of warranties. The Licensee must include a copy of such notices and
a copy of the Licence with every copy of the Work he/she distributes or
communicates. The Licensee must cause any Derivative Work to carry prominent
notices stating that the Work has been modified and the date of modification.
Copyleft clause: If the Licensee distributes or communicates copies of the
Original Works or Derivative Works, this Distribution or Communication will be
done under the terms of this Licence or of a later version of this Licence
unless the Original Work is expressly distributed only under this version of
the Licence — for example by communicating ‘EUPL v. 1.2 only’. The Licensee
(becoming Licensor) cannot offer or impose any additional terms or conditions
on the Work or Derivative Work that alter or restrict the terms of the
Licence.
Compatibility clause: If the Licensee Distributes or Communicates Derivative
Works or copies thereof based upon both the Work and another work licensed
under a Compatible Licence, this Distribution or Communication can be done
under the terms of this Compatible Licence. For the sake of this clause,
‘Compatible Licence’ refers to the licences listed in the appendix attached to
this Licence. Should the Licensee's obligations under the Compatible Licence
conflict with his/her obligations under this Licence, the obligations of the
Compatible Licence shall prevail.
Provision of Source Code: When distributing or communicating copies of the
Work, the Licensee will provide a machine-readable copy of the Source Code or
indicate a repository where this Source will be easily and freely available
for as long as the Licensee continues to distribute or communicate the Work.
Legal Protection: This Licence does not grant permission to use the trade
names, trademarks, service marks, or names of the Licensor, except as required
for reasonable and customary use in describing the origin of the Work and
reproducing the content of the copyright notice.
6. Chain of Authorship
The original Licensor warrants that the copyright in the Original Work granted
hereunder is owned by him/her or licensed to him/her and that he/she has the
power and authority to grant the Licence.
Each Contributor warrants that the copyright in the modifications he/she
brings to the Work are owned by him/her or licensed to him/her and that he/she
has the power and authority to grant the Licence.
Each time You accept the Licence, the original Licensor and subsequent
Contributors grant You a licence to their contributions to the Work, under the
terms of this Licence.
7. Disclaimer of Warranty
The Work is a work in progress, which is continuously improved by numerous
Contributors. It is not a finished work and may therefore contain defects or
‘bugs’ inherent to this type of development.
For the above reason, the Work is provided under the Licence on an ‘as is’
basis and without warranties of any kind concerning the Work, including
without limitation merchantability, fitness for a particular purpose, absence
of defects or errors, accuracy, non-infringement of intellectual property
rights other than copyright as stated in Article 6 of this Licence.
This disclaimer of warranty is an essential part of the Licence and a
condition for the grant of any rights to the Work.
8. Disclaimer of Liability
Except in the cases of wilful misconduct or damages directly caused to natural
persons, the Licensor will in no event be liable for any direct or indirect,
material or moral, damages of any kind, arising out of the Licence or of the
use of the Work, including without limitation, damages for loss of goodwill,
work stoppage, computer failure or malfunction, loss of data or any commercial
damage, even if the Licensor has been advised of the possibility of such
damage. However, the Licensor will be liable under statutory product liability
laws as far such laws apply to the Work.
9. Additional agreements
While distributing the Work, You may choose to conclude an additional
agreement, defining obligations or services consistent with this Licence.
However, if accepting obligations, You may act only on your own behalf and on
your sole responsibility, not on behalf of the original Licensor or any other
Contributor, and only if You agree to indemnify, defend, and hold each
Contributor harmless for any liability incurred by, or claims asserted against
such Contributor by the fact You have accepted any warranty or additional
liability.
10. Acceptance of the Licence
The provisions of this Licence can be accepted by clicking on an icon ‘I
agree’ placed under the bottom of a window displaying the text of this Licence
or by affirming consent in any other similar way, in accordance with the rules
of applicable law. Clicking on that icon indicates your clear and irrevocable
acceptance of this Licence and all of its terms and conditions.
Similarly, you irrevocably accept this Licence and all of its terms and
conditions by exercising any rights granted to You by Article 2 of this
Licence, such as the use of the Work, the creation by You of a Derivative Work
or the Distribution or Communication by You of the Work or copies thereof.
11. Information to the public
In case of any Distribution or Communication of the Work by means of
electronic communication by You (for example, by offering to download the Work
from a remote location) the distribution channel or media (for example, a
website) must at least provide to the public the information requested by the
applicable law regarding the Licensor, the Licence and the way it may be
accessible, concluded, stored and reproduced by the Licensee.
12. Termination of the Licence
The Licence and the rights granted hereunder will terminate automatically upon
any breach by the Licensee of the terms of the Licence. Such a termination
will not terminate the licences of any person who has received the Work from
the Licensee under the Licence, provided such persons remain in full
compliance with the Licence.
13. Miscellaneous
Without prejudice of Article 9 above, the Licence represents the complete
agreement between the Parties as to the Work.
If any provision of the Licence is invalid or unenforceable under applicable
law, this will not affect the validity or enforceability of the Licence as a
whole. Such provision will be construed or reformed so as necessary to make it
valid and enforceable.
The European Commission may publish other linguistic versions or new versions
of this Licence or updated versions of the Appendix, so far this is required
and reasonable, without reducing the scope of the rights granted by the
Licence. New versions of the Licence will be published with a unique version
number.
All linguistic versions of this Licence, approved by the European Commission,
have identical value. Parties can take advantage of the linguistic version of
their choice.
14. Jurisdiction
Without prejudice to specific agreement between parties,
— any litigation resulting from the interpretation of this License, arising
between the European Union institutions, bodies, offices or agencies, as a
Licensor, and any Licensee, will be subject to the jurisdiction of the Court
of Justice of the European Union, as laid down in article 272 of the Treaty
on the Functioning of the European Union,
— any litigation arising between other parties and resulting from the
interpretation of this License, will be subject to the exclusive
jurisdiction of the competent court where the Licensor resides or conducts
its primary business.
15. Applicable Law
Without prejudice to specific agreement between parties,
— this Licence shall be governed by the law of the European Union Member State
where the Licensor has his seat, resides or has his registered office,
— this licence shall be governed by Belgian law if the Licensor has no seat,
residence or registered office inside a European Union Member State.
Appendix
‘Compatible Licences’ according to Article 5 EUPL are:
— GNU General Public License (GPL) v. 2, v. 3
— GNU Affero General Public License (AGPL) v. 3
— Open Software License (OSL) v. 2.1, v. 3.0
— Eclipse Public License (EPL) v. 1.0
— CeCILL v. 2.0, v. 2.1
— Mozilla Public Licence (MPL) v. 2
— GNU Lesser General Public Licence (LGPL) v. 2.1, v. 3
— Creative Commons Attribution-ShareAlike v. 3.0 Unported (CC BY-SA 3.0) for
works other than software
— European Union Public Licence (EUPL) v. 1.1, v. 1.2
— Québec Free and Open-Source Licence — Reciprocity (LiLiQ-R) or
Strong Reciprocity (LiLiQ-R+)
— The European Commission may update this Appendix to later versions of the
above licences without producing a new version of the EUPL, as long as they
provide the rights granted in Article 2 of this Licence and protect the
covered Source Code from exclusive appropriation.
— All other changes or additions to this Appendix require the production of a
new EUPL version.
================================================
FILE: README.md
================================================
<a href="https://aeon.technology"></a>
<p align="center">
<em>📡 Scan the internet for your personal information and modify or remove it</em>
</p>
<br />
<div align="center">
<img src="https://github.com/leinelissen/aeon/workflows/Build/badge.svg" />
<img alt="License" src="https://img.shields.io/badge/license-EUPL-green">
<img alt="GitHub package.json version" src="https://img.shields.io/github/package-json/v/leinelissen/aeon?color=green">
<a href="https://docs.aeon.technology"><img alt="Documentation" src="https://img.shields.io/badge/documentation-up-green"></a>
</div>
<br />
<br />
# What is Aeon?
📡 Ever wondered what personal information is scattered around the internet? Aeon scans popuplar platforms for your personal information and (*almost*) automatically retrieves it.
👀 Use Aeon to download, archive and visualise your personal information.
❌ Don't agree with the data Facebook (or another platform) knows about you? Generate a request for modification or deletion with the click of a button!
<br />
<p align="center">
<img src="./docs/.gitbook/assets/aeon-demo.gif" width="600" />
</p>
<br />
# Installing Aeon
<svg width="285" height="50" viewBox="0 0 285 50" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M21.544 32L22.7457 28.5355H27.8594L29.0547 32H31.1513L26.4403 18.9091H24.1584L19.4474 32H21.544ZM23.321 26.8736L25.2514 21.2869H25.3537L27.2841 26.8736H23.321ZM40.057 22.1818H38.0051L35.525 29.7372H35.4228L32.9363 22.1818H30.8844L34.4512 32H36.4966L40.057 22.1818ZM44.4116 32.2173C46.0352 32.2173 46.9492 31.3928 47.3136 30.6577H47.3903V32H49.2567V25.4801C49.2567 22.6229 47.0067 22.054 45.4471 22.054C43.6701 22.054 42.0337 22.7699 41.3945 24.5597L43.1907 24.9688C43.4719 24.272 44.1879 23.6009 45.4727 23.6009C46.7063 23.6009 47.3391 24.2464 47.3391 25.3587V25.4034C47.3391 26.1001 46.6232 26.0874 44.859 26.2919C42.9989 26.5092 41.0941 26.995 41.0941 29.2259C41.0941 31.1562 42.5451 32.2173 44.4116 32.2173ZM44.8271 30.6832C43.7468 30.6832 42.967 30.1974 42.967 29.2514C42.967 28.2287 43.8746 27.8643 44.9805 27.7173C45.6005 27.6342 47.0707 27.468 47.3455 27.1932V28.4588C47.3455 29.6222 46.4187 30.6832 44.8271 30.6832ZM51.8024 32H53.7136V22.1818H51.8024V32ZM52.7676 20.6669C53.426 20.6669 53.9757 20.1555 53.9757 19.5291C53.9757 18.9027 53.426 18.3849 52.7676 18.3849C52.1028 18.3849 51.5595 18.9027 51.5595 19.5291C51.5595 20.1555 52.1028 20.6669 52.7676 20.6669ZM58.196 18.9091H56.2848V32H58.196V18.9091ZM63.6245 32.2173C65.248 32.2173 66.1621 31.3928 66.5265 30.6577H66.6032V32H68.4696V25.4801C68.4696 22.6229 66.2196 22.054 64.66 22.054C62.883 22.054 61.2466 22.7699 60.6074 24.5597L62.4036 24.9688C62.6848 24.272 63.4007 23.6009 64.6855 23.6009C65.9192 23.6009 66.552 24.2464 66.552 25.3587V25.4034C66.552 26.1001 65.8361 26.0874 64.0719 26.2919C62.2118 26.5092 60.307 26.995 60.307 29.2259C60.307 31.1562 61.758 32.2173 63.6245 32.2173ZM64.04 30.6832C62.9597 30.6832 62.1799 30.1974 62.1799 29.2514C62.1799 28.2287 63.0875 27.8643 64.1934 27.7173C64.8134 27.6342 66.2836 27.468 66.5584 27.1932V28.4588C66.5584 29.6222 65.6316 30.6832 64.04 30.6832ZM71.1687 32H73.0352V30.4723H73.195C73.5401 31.0987 74.2433 32.1918 76.033 32.1918C78.4109 32.1918 80.1367 30.2869 80.1367 27.1101C80.1367 23.9268 78.3853 22.054 76.0138 22.054C74.1921 22.054 73.5337 23.1662 73.195 23.7734H73.0799V18.9091H71.1687V32ZM73.0415 27.0909C73.0415 25.0391 73.9364 23.6776 75.6048 23.6776C77.337 23.6776 78.2063 25.1413 78.2063 27.0909C78.2063 29.0597 77.3114 30.5618 75.6048 30.5618C73.962 30.5618 73.0415 29.1555 73.0415 27.0909ZM84.1941 18.9091H82.2828V32H84.1941V18.9091ZM90.9968 32.1982C93.1381 32.1982 94.6531 31.1435 95.0877 29.5455L93.2788 29.2195C92.9336 30.1463 92.1026 30.6193 91.016 30.6193C89.3796 30.6193 88.2802 29.5582 88.229 27.6662H95.2092V26.9886C95.2092 23.4411 93.087 22.054 90.8626 22.054C88.1268 22.054 86.3242 24.1378 86.3242 27.1548C86.3242 30.2038 88.1012 32.1982 90.9968 32.1982ZM88.2354 26.2344C88.3121 24.8409 89.3221 23.6328 90.8754 23.6328C92.3583 23.6328 93.3299 24.7322 93.3363 26.2344H88.2354ZM106.766 22.1818H104.65V21.2997C104.65 20.4304 105.008 19.9574 105.941 19.9574C106.338 19.9574 106.619 20.0469 106.798 20.1044L107.245 18.5575C106.977 18.4553 106.421 18.2955 105.647 18.2955C104.094 18.2955 102.733 19.2031 102.733 21.044V22.1818H101.218V23.7159H102.733V32H104.65V23.7159H106.766V22.1818ZM112.487 32.1982C115.255 32.1982 117.064 30.1719 117.064 27.1357C117.064 24.0803 115.255 22.054 112.487 22.054C109.719 22.054 107.91 24.0803 107.91 27.1357C107.91 30.1719 109.719 32.1982 112.487 32.1982ZM112.493 30.5938C110.684 30.5938 109.841 29.0149 109.841 27.1293C109.841 25.25 110.684 23.652 112.493 23.652C114.289 23.652 115.133 25.25 115.133 27.1293C115.133 29.0149 114.289 30.5938 112.493 30.5938ZM119.197 32H121.108V26.0043C121.108 24.7195 122.099 23.7926 123.454 23.7926C123.85 23.7926 124.298 23.8629 124.451 23.9077V22.0795C124.259 22.054 123.882 22.0348 123.639 22.0348C122.489 22.0348 121.504 22.6868 121.146 23.7415H121.044V22.1818H119.197V32Z" fill="currentColor"/>
<path d="M172.18 26.707C172.18 25.4062 172.777 24.457 173.938 23.7188C173.27 22.7695 172.285 22.2773 170.984 22.1719C169.719 22.0664 168.348 22.875 167.855 22.875C167.328 22.875 166.133 22.207 165.184 22.207C163.215 22.2422 161.141 23.7539 161.141 26.8828C161.141 27.7969 161.281 28.7461 161.633 29.7305C162.09 31.0312 163.707 34.1953 165.395 34.125C166.273 34.125 166.906 33.4922 168.066 33.4922C169.191 33.4922 169.754 34.125 170.738 34.125C172.461 34.125 173.938 31.2422 174.359 29.9414C172.074 28.8516 172.18 26.7773 172.18 26.707ZM170.211 20.9414C171.16 19.8164 171.055 18.7617 171.055 18.375C170.211 18.4453 169.227 18.9727 168.664 19.6055C168.031 20.3086 167.68 21.1875 167.75 22.1367C168.664 22.207 169.508 21.75 170.211 20.9414Z" fill="currentColor"/>
<path d="M135 20.5547V25.8984H141.434V19.6758L135 20.5547ZM135 31.9805L141.434 32.8594V26.707H135V31.9805ZM142.137 32.9648L150.75 34.125V26.707H142.137V32.9648ZM142.137 19.5703V25.8984H150.75V18.375L142.137 19.5703Z" fill="currentColor"/>
<path d="M193.719 17.5312C188.902 17.5312 185 21.4336 185 26.25C185 31.0664 188.902 34.9688 193.719 34.9688C198.535 34.9688 202.438 31.0664 202.438 26.25C202.438 21.4336 198.535 17.5312 193.719 17.5312ZM195.547 20.8008C195.863 20.2734 196.566 20.0977 197.094 20.4141C197.621 20.7305 197.797 21.3984 197.48 21.9258C197.199 22.4883 196.496 22.6641 195.969 22.3477C195.441 22.0312 195.23 21.3633 195.547 20.8008ZM188.059 27.375C187.426 27.375 186.934 26.8828 186.934 26.25C186.934 25.6523 187.426 25.1602 188.059 25.1602C188.691 25.1602 189.184 25.6523 189.184 26.25C189.184 26.8828 188.691 27.375 188.059 27.375ZM189.043 27.4805C189.816 26.8828 189.816 25.6875 189.043 25.0547C189.359 23.8945 190.062 22.9102 191.047 22.2422L191.855 23.6484C190.062 24.9141 190.062 27.6211 191.855 28.8867L191.047 30.2578C190.062 29.625 189.359 28.6406 189.043 27.4805ZM197.094 32.1211C196.531 32.4375 195.863 32.2617 195.547 31.6992C195.23 31.1719 195.441 30.5039 195.969 30.1875C196.496 29.8711 197.199 30.0469 197.48 30.6094C197.797 31.1367 197.621 31.8047 197.094 32.1211ZM197.094 29.6953C196.145 29.3086 195.125 29.9062 194.984 30.9258C194.773 30.9609 193.262 31.418 191.574 30.5742L192.348 29.168C194.352 30.082 196.707 28.7461 196.883 26.5664H198.5C198.43 27.7969 197.902 28.8867 197.094 29.6953ZM196.883 25.9688C196.707 23.7891 194.387 22.418 192.348 23.3672L191.574 21.9609C193.262 21.1172 194.773 21.5742 194.949 21.6094C195.125 22.6289 196.145 23.2266 197.094 22.8398C197.902 23.6484 198.43 24.7383 198.5 25.9688H196.883Z" fill="currentColor"/>
<path d="M220.91 18.375C216.551 18.375 213 21.8906 213 26.25V32.2266C212.965 32.2266 212.965 32.2266 212.965 32.2617C212.965 33.2812 213.844 34.125 214.863 34.125H220.84C225.199 34.1602 228.75 30.6445 228.75 26.2852C228.75 21.9258 225.234 18.4102 220.91 18.375ZM226.852 23.9297L224.707 21.7148C224.777 21.5391 224.812 21.3984 224.812 21.2227V21.1523L226.781 23.1211C226.816 23.4023 226.852 23.6484 226.852 23.9297ZM224.637 20.5898C225.586 21.0117 226.324 21.8203 226.676 22.8398L224.777 20.9062C224.742 20.8008 224.672 20.6953 224.637 20.5898ZM217.148 25.9688C217.043 26.0742 216.938 26.2148 216.867 26.3555L216.551 26.0742C216.762 26.0039 216.938 25.9688 217.148 25.9688ZM216.41 26.1094L216.797 26.5312L216.762 26.8125C216.762 26.9531 216.797 27.0938 216.867 27.2344L215.918 26.2852C216.059 26.2148 216.234 26.1445 216.41 26.1094ZM215.742 26.3555L217.113 27.7266C216.938 27.7617 216.762 27.7969 216.586 27.8672L215.355 26.6016C215.496 26.5312 215.602 26.4258 215.742 26.3555ZM215.215 26.707L216.445 27.9727C216.34 28.043 216.199 28.1484 216.094 28.2539L214.863 27.0234C214.969 26.918 215.074 26.8125 215.215 26.707ZM214.758 27.1289L215.988 28.3594C215.883 28.5 215.777 28.6406 215.707 28.7812L214.441 27.5156C214.547 27.375 214.652 27.2695 214.758 27.1289ZM214.371 27.6562L215.637 28.9219C215.566 29.0977 215.531 29.2734 215.531 29.4492L214.125 28.0781C214.195 27.9375 214.266 27.7969 214.371 27.6562ZM214.055 28.2188L215.496 29.6953C215.531 30.0117 215.602 30.3281 215.742 30.6094L213.879 28.7109C213.949 28.5703 213.984 28.3945 214.055 28.2188ZM213.773 29.7305L215.953 31.9102C215.883 32.0508 215.812 32.2266 215.812 32.4023V32.4727L213.879 30.5039C213.809 30.2578 213.773 30.0117 213.773 29.7305ZM213.949 30.8203L215.883 32.7188C215.918 32.8594 215.953 32.9648 216.023 33.0352C215.039 32.6484 214.301 31.8047 213.949 30.8203ZM213.773 29.5195C213.773 29.3086 213.809 29.0977 213.844 28.9219L216.375 31.4531C216.27 31.5586 216.129 31.6289 216.059 31.7695L213.773 29.5195ZM222.668 27.6914H221.191V29.6602C221.191 32.0859 218.906 33.7383 216.762 33.2812C216.551 33.2812 216.059 32.9648 216.059 32.4023C216.059 31.9453 216.445 31.5586 216.938 31.5586C217.148 31.5586 217.148 31.5938 217.465 31.5938C218.555 31.5938 219.434 30.7148 219.434 29.6602L219.469 28.0078C219.469 27.832 219.293 27.6914 219.152 27.6914L217.957 27.6562C216.797 27.6562 216.832 25.9336 217.957 25.9336H219.469V23.9648C219.469 21.9258 221.121 20.2734 223.16 20.2734C223.195 20.2734 223.195 20.2734 223.195 20.2734C223.441 20.2734 223.652 20.3086 223.898 20.3789C224.285 20.4141 224.602 20.7656 224.602 21.2227C224.602 21.75 224.074 22.1719 223.512 22.0312C222.562 21.8555 221.191 22.5586 221.191 23.9648V25.6172C221.191 25.793 221.332 25.9336 221.508 25.9336H222.703C223.828 25.9688 223.828 27.6914 222.668 27.6914ZM223.512 27.6914C223.617 27.5508 223.723 27.4102 223.793 27.2695L224.074 27.5859C223.898 27.6211 223.688 27.6562 223.512 27.6914ZM224.25 27.5156L223.828 27.0938L223.863 26.8125C223.863 26.6719 223.828 26.5312 223.793 26.3906L224.742 27.3398C224.566 27.4102 224.426 27.4805 224.25 27.5156ZM224.883 27.2695L223.547 25.9336C223.723 25.8633 223.898 25.8281 224.039 25.7578L225.305 27.0234C225.164 27.0938 225.023 27.1992 224.883 27.2695ZM225.41 26.918L224.18 25.6875C224.32 25.582 224.426 25.4766 224.566 25.3711L225.797 26.6016C225.656 26.707 225.551 26.8125 225.41 26.918ZM225.902 26.4961L224.672 25.2656C224.777 25.125 224.848 25.0195 224.918 24.8438L226.184 26.1094C226.078 26.25 226.008 26.3906 225.902 26.4961ZM226.289 26.0039L224.988 24.7031C225.059 24.5273 225.094 24.3516 225.129 24.1758L226.5 25.582C226.43 25.7227 226.359 25.8633 226.289 26.0039ZM226.746 24.9141C226.711 25.0898 226.641 25.2305 226.57 25.4062L225.129 23.9297C225.129 23.6133 225.023 23.2969 224.883 23.0156L226.746 24.9141ZM226.781 24.7031L224.25 22.1719C224.391 22.1016 224.496 21.9961 224.602 21.8555L226.852 24.1055C226.852 24.3164 226.816 24.5273 226.781 24.7031Z" fill="currentColor"/>
<path d="M249.16 20.6953H247.578V24.4922L250.285 21.8203L249.16 20.6953ZM243.465 21.8203L246.137 24.4922V20.6953H244.555L243.465 21.8203ZM244.906 20.3438H246.488V24.8438L246.875 25.2305L247.262 24.8438V20.3438H248.809L246.875 18.375L244.906 20.3438ZM245.82 26.25L245.469 25.8633H240.934V24.3164L239 26.25L240.934 28.2188V26.6367H245.469L245.82 26.25ZM248.633 25.5117H252.43V23.9648L251.305 22.8398L248.633 25.5117ZM254.715 26.25L252.781 24.3164V25.8633H248.281L247.895 26.25L248.281 26.6367H252.781V28.2188L254.715 26.25ZM241.285 23.4727L242.41 22.3477L245.609 25.5117H246.137V24.9844L242.973 21.8203L244.062 20.6953H241.285V23.4727ZM252.43 20.6953H249.652L250.777 21.8203L247.578 24.9844V25.5117H248.141L251.305 22.3477L252.43 23.4727V20.6953ZM246.137 31.8047V28.0078L243.465 30.7148L244.555 31.8047H246.137ZM241.285 25.5117H245.117L242.41 22.8398L241.285 23.9648V25.5117ZM252.43 29.0625L251.305 30.1523L248.141 26.9883H247.578V27.5156L250.777 30.7148L249.652 31.8047H252.43V29.0625ZM252.43 26.9883H248.633L251.305 29.6602L252.43 28.5703V26.9883ZM250.285 30.7148L247.578 28.0078V31.8047H249.16L250.285 30.7148ZM242.41 29.6602L245.117 26.9883H241.285V28.5703L242.41 29.6602ZM248.809 32.1562H247.262V27.6562L246.875 27.3047L246.488 27.6562V32.1562H244.906L246.875 34.125L248.809 32.1562ZM242.973 30.7148L246.137 27.5156V26.9883H245.609L242.41 30.1523L241.285 29.0625V31.8047H244.062L242.973 30.7148Z" fill="currentColor"/>
</svg>
Aeon is available for Windows, macOS, apt and yum! Download the application here and follow the instructions on [the download page](https://aeon.technology/download).
[](https://aeon.technology/download)
Not sure how to start using Aeon? Follow the [Getting Started guide](https://docs.aeon.technology/using-aeon/getting-started) for detailed instructions.
# How does it work?
All companies worldwide are required to offer you access to all data they retain about you. But most of the time, this process is hard, convoluted, slow or all three at once! Aeon has rules for each platform that make requesting your personal information just a couple of clicks!
<p align="center">
<img src="./docs/.gitbook/assets/timeline.png" width="600" />
</p>
The resulting data is downloaded on your local computer for safekeeping. Often, data is formatted in machine-readable formats such as JSON and CSV. To make it easier to digest your personal information, Aeon visualises it for you.
<p align="center">
<img src="./docs/.gitbook/assets/graph.png" width="600" />
</p>
Companies are not only required to grant access, they must respect your wishes too. This means you can require them to delete or modify the personal information they have. Aeon contains a generator for data subject rights emails, that help you take control of your personal data.
<p align="center">
<img src="./docs/.gitbook/assets/erasure.png" width="600" />
</p>
# Supported Platforms
<svg width="363" height="50" viewBox="0 0 363 50" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M31.3111 23.1662C30.8636 20.3665 28.6712 18.7301 25.9034 18.7301C22.5156 18.7301 20.0163 21.2678 20.0163 25.4545C20.0163 29.6413 22.5028 32.179 25.9034 32.179C28.7798 32.179 30.8828 30.3764 31.3111 27.7876L29.3168 27.7812C28.978 29.456 27.5717 30.3764 25.9162 30.3764C23.6726 30.3764 21.9787 28.657 21.9787 25.4545C21.9787 22.2777 23.6662 20.5327 25.9226 20.5327C27.5909 20.5327 28.9908 21.4723 29.3168 23.1662H31.3111ZM39.7406 27.9283C39.747 29.5966 38.5069 30.3892 37.4331 30.3892C36.2505 30.3892 35.4324 29.5327 35.4324 28.1967V22.1818H33.5211V28.4268C33.5211 30.8622 34.8571 32.1278 36.7427 32.1278C38.2193 32.1278 39.2228 31.348 39.6767 30.2997H39.7789V32H41.6582V22.1818H39.7406V27.9283ZM44.2262 32H46.1374V26.0043C46.1374 24.7195 47.1282 23.7926 48.4833 23.7926C48.8796 23.7926 49.3271 23.8629 49.4805 23.9077V22.0795C49.2887 22.054 48.9116 22.0348 48.6687 22.0348C47.5181 22.0348 46.5337 22.6868 46.1758 23.7415H46.0735V22.1818H44.2262V32ZM51.1344 32H53.0456V26.0043C53.0456 24.7195 54.0364 23.7926 55.3915 23.7926C55.7878 23.7926 56.2353 23.8629 56.3887 23.9077V22.0795C56.1969 22.054 55.8198 22.0348 55.5769 22.0348C54.4263 22.0348 53.4419 22.6868 53.084 23.7415H52.9817V22.1818H51.1344V32ZM61.9226 32.1982C64.0639 32.1982 65.5788 31.1435 66.0135 29.5455L64.2045 29.2195C63.8594 30.1463 63.0284 30.6193 61.9418 30.6193C60.3054 30.6193 59.206 29.5582 59.1548 27.6662H66.1349V26.9886C66.1349 23.4411 64.0128 22.054 61.7884 22.054C59.0526 22.054 57.25 24.1378 57.25 27.1548C57.25 30.2038 59.027 32.1982 61.9226 32.1982ZM59.1612 26.2344C59.2379 24.8409 60.2479 23.6328 61.8011 23.6328C63.2841 23.6328 64.2557 24.7322 64.2621 26.2344H59.1612ZM70.1667 26.1705C70.1667 24.6044 71.1255 23.7095 72.4551 23.7095C73.7527 23.7095 74.5389 24.5597 74.5389 25.9851V32H76.4501V25.755C76.4501 23.326 75.1142 22.054 73.1071 22.054C71.6305 22.054 70.6653 22.7379 70.2115 23.7798H70.09V22.1818H68.2555V32H70.1667V26.1705ZM83.5964 22.1818H81.5829V19.8295H79.6717V22.1818H78.2335V23.7159H79.6717V29.5135C79.6653 31.2969 81.0268 32.1598 82.5353 32.1278C83.1426 32.1214 83.5517 32.0064 83.7754 31.9233L83.4302 30.3445C83.3024 30.37 83.0659 30.4276 82.7591 30.4276C82.139 30.4276 81.5829 30.223 81.5829 29.1172V23.7159H83.5964V22.1818ZM87.8327 18.9091H85.9215V32H87.8327V18.9091ZM91.612 35.6818C93.1909 35.6818 94.188 34.8572 94.7569 33.3104L98.8159 22.201L96.7512 22.1818L94.2647 29.8011H94.1625L91.676 22.1818H89.6305L93.2228 32.1278L92.9863 32.7798C92.5005 34.0838 91.8166 34.1925 90.7683 33.9048L90.3081 35.4709C90.5382 35.5732 91.0368 35.6818 91.612 35.6818ZM112.738 24.5788C112.341 23.0511 111.146 22.054 109.024 22.054C106.806 22.054 105.233 23.2237 105.233 24.9624C105.233 26.3558 106.077 27.2827 107.918 27.6918L109.58 28.0561C110.526 28.267 110.967 28.6889 110.967 29.3026C110.967 30.0632 110.155 30.6577 108.903 30.6577C107.758 30.6577 107.023 30.1655 106.793 29.2003L104.946 29.4815C105.265 31.2202 106.71 32.1982 108.915 32.1982C111.287 32.1982 112.93 30.9389 112.93 29.1619C112.93 27.7749 112.047 26.9183 110.245 26.5028L108.685 26.1449C107.605 25.8892 107.138 25.5249 107.145 24.8601C107.138 24.1058 107.956 23.5689 109.043 23.5689C110.232 23.5689 110.782 24.2273 111.006 24.8857L112.738 24.5788ZM121.25 27.9283C121.257 29.5966 120.017 30.3892 118.943 30.3892C117.76 30.3892 116.942 29.5327 116.942 28.1967V22.1818H115.031V28.4268C115.031 30.8622 116.367 32.1278 118.252 32.1278C119.729 32.1278 120.733 31.348 121.186 30.2997H121.289V32H123.168V22.1818H121.25V27.9283ZM125.736 35.6818H127.647V30.4723H127.762C128.107 31.0987 128.811 32.1918 130.6 32.1918C132.978 32.1918 134.704 30.2869 134.704 27.1101C134.704 23.9268 132.953 22.054 130.581 22.054C128.759 22.054 128.101 23.1662 127.762 23.7734H127.602V22.1818H125.736V35.6818ZM127.609 27.0909C127.609 25.0391 128.504 23.6776 130.172 23.6776C131.904 23.6776 132.774 25.1413 132.774 27.0909C132.774 29.0597 131.879 30.5618 130.172 30.5618C128.529 30.5618 127.609 29.1555 127.609 27.0909ZM136.845 35.6818H138.757V30.4723H138.872C139.217 31.0987 139.92 32.1918 141.71 32.1918C144.088 32.1918 145.813 30.2869 145.813 27.1101C145.813 23.9268 144.062 22.054 141.691 22.054C139.869 22.054 139.21 23.1662 138.872 23.7734H138.712V22.1818H136.845V35.6818ZM138.718 27.0909C138.718 25.0391 139.613 23.6776 141.281 23.6776C143.014 23.6776 143.883 25.1413 143.883 27.0909C143.883 29.0597 142.988 30.5618 141.281 30.5618C139.639 30.5618 138.718 29.1555 138.718 27.0909ZM152.09 32.1982C154.858 32.1982 156.667 30.1719 156.667 27.1357C156.667 24.0803 154.858 22.054 152.09 22.054C149.323 22.054 147.514 24.0803 147.514 27.1357C147.514 30.1719 149.323 32.1982 152.09 32.1982ZM152.097 30.5938C150.288 30.5938 149.444 29.0149 149.444 27.1293C149.444 25.25 150.288 23.652 152.097 23.652C153.893 23.652 154.737 25.25 154.737 27.1293C154.737 29.0149 153.893 30.5938 152.097 30.5938ZM158.8 32H160.712V26.0043C160.712 24.7195 161.702 23.7926 163.058 23.7926C163.454 23.7926 163.901 23.8629 164.055 23.9077V22.0795C163.863 22.054 163.486 22.0348 163.243 22.0348C162.092 22.0348 161.108 22.6868 160.75 23.7415H160.648V22.1818H158.8V32ZM170.872 22.1818H168.858V19.8295H166.947V22.1818H165.509V23.7159H166.947V29.5135C166.941 31.2969 168.302 32.1598 169.811 32.1278C170.418 32.1214 170.827 32.0064 171.051 31.9233L170.706 30.3445C170.578 30.37 170.341 30.4276 170.034 30.4276C169.414 30.4276 168.858 30.223 168.858 29.1172V23.7159H170.872V22.1818ZM172.986 32H174.897V22.1818H172.986V32ZM173.951 20.6669C174.61 20.6669 175.159 20.1555 175.159 19.5291C175.159 18.9027 174.61 18.3849 173.951 18.3849C173.286 18.3849 172.743 18.9027 172.743 19.5291C172.743 20.1555 173.286 20.6669 173.951 20.6669ZM179.38 26.1705C179.38 24.6044 180.338 23.7095 181.668 23.7095C182.966 23.7095 183.752 24.5597 183.752 25.9851V32H185.663V25.755C185.663 23.326 184.327 22.054 182.32 22.054C180.843 22.054 179.878 22.7379 179.424 23.7798H179.303V22.1818H177.468V32H179.38V26.1705ZM192.343 35.8864C194.842 35.8864 196.772 34.7422 196.772 32.2173V22.1818H194.9V23.7734H194.759C194.42 23.1662 193.743 22.054 191.914 22.054C189.543 22.054 187.798 23.9268 187.798 27.0526C187.798 30.1847 189.581 31.853 191.902 31.853C193.704 31.853 194.401 30.8366 194.746 30.2102H194.868V32.1406C194.868 33.6811 193.813 34.3459 192.362 34.3459C190.77 34.3459 190.15 33.5469 189.811 32.9844L188.169 33.6619C188.686 34.8636 189.997 35.8864 192.343 35.8864ZM192.324 30.2678C190.617 30.2678 189.728 28.9574 189.728 27.027C189.728 25.1413 190.598 23.6776 192.324 23.6776C193.992 23.6776 194.887 25.0391 194.887 27.027C194.887 29.0533 193.973 30.2678 192.324 30.2678Z" fill="currentColor"/>
<path d="M230.719 25.25C230.719 20.4336 226.816 16.5312 222 16.5312C217.184 16.5312 213.281 20.4336 213.281 25.25C213.281 29.6094 216.445 33.2305 220.629 33.8633V27.7812H218.414V25.25H220.629V23.3516C220.629 21.1719 221.93 19.9414 223.898 19.9414C224.883 19.9414 225.867 20.1172 225.867 20.1172V22.2617H224.777C223.688 22.2617 223.336 22.9297 223.336 23.6328V25.25H225.762L225.375 27.7812H223.336V33.8633C227.52 33.2305 230.719 29.6094 230.719 25.25Z" fill="currentColor"/>
<path d="M255.625 17.375H242.09C241.492 17.375 241 17.9023 241 18.5352V32C241 32.6328 241.492 33.125 242.09 33.125H255.625C256.223 33.125 256.75 32.6328 256.75 32V18.5352C256.75 17.9023 256.223 17.375 255.625 17.375ZM245.746 30.875H243.426V23.3867H245.746V30.875ZM244.586 22.332C243.812 22.332 243.215 21.7344 243.215 20.9961C243.215 20.2578 243.812 19.625 244.586 19.625C245.324 19.625 245.922 20.2578 245.922 20.9961C245.922 21.7344 245.324 22.332 244.586 22.332ZM254.5 30.875H252.145V27.2188C252.145 26.375 252.145 25.25 250.949 25.25C249.719 25.25 249.543 26.1992 249.543 27.1836V30.875H247.223V23.3867H249.438V24.4062H249.473C249.789 23.8086 250.562 23.1758 251.688 23.1758C254.043 23.1758 254.5 24.7578 254.5 26.7617V30.875Z" fill="currentColor"/>
<path d="M275.719 16.5312C270.902 16.5312 267 20.4688 267 25.25C267 30.0664 270.902 33.9688 275.719 33.9688C280.5 33.9688 284.438 30.0664 284.438 25.25C284.438 20.4688 280.5 16.5312 275.719 16.5312ZM279.234 29.3633C279.094 29.3633 278.988 29.3281 278.883 29.2578C276.668 27.9219 274.137 27.8867 271.605 28.3789C271.465 28.4141 271.289 28.4844 271.184 28.4844C270.832 28.4844 270.621 28.2031 270.621 27.9219C270.621 27.5703 270.832 27.3945 271.113 27.3242C273.996 26.6914 276.914 26.7617 279.445 28.2383C279.656 28.3789 279.762 28.5195 279.762 28.8359C279.762 29.1523 279.516 29.3633 279.234 29.3633ZM280.184 27.0781C280.008 27.0781 279.867 26.9727 279.762 26.9375C277.547 25.6367 274.277 25.1094 271.359 25.8828C271.184 25.918 271.113 25.9883 270.938 25.9883C270.586 25.9883 270.27 25.6719 270.27 25.2852C270.27 24.9336 270.445 24.6875 270.797 24.582C271.781 24.3008 272.801 24.0898 274.242 24.0898C276.527 24.0898 278.742 24.6523 280.465 25.707C280.746 25.8477 280.887 26.0938 280.887 26.375C280.852 26.7617 280.57 27.0781 280.184 27.0781ZM281.273 24.4062C281.098 24.4062 280.992 24.3359 280.816 24.2656C278.32 22.7539 273.855 22.4023 270.938 23.2109C270.832 23.2461 270.656 23.3164 270.48 23.3164C270.023 23.3164 269.672 22.9297 269.672 22.4727C269.672 21.9805 269.988 21.7344 270.305 21.6289C271.535 21.2773 272.906 21.1016 274.418 21.1016C276.984 21.1016 279.691 21.6289 281.625 22.7891C281.906 22.9297 282.082 23.1406 282.082 23.5625C282.082 24.0547 281.695 24.4062 281.273 24.4062Z" fill="currentColor"/>
<path d="M302.875 21.207C300.625 21.207 298.832 23.0352 298.832 25.25C298.832 27.5 300.625 29.293 302.875 29.293C305.09 29.293 306.918 27.5 306.918 25.25C306.918 23.0352 305.09 21.207 302.875 21.207ZM302.875 27.8867C301.434 27.8867 300.238 26.7266 300.238 25.25C300.238 23.8086 301.398 22.6484 302.875 22.6484C304.316 22.6484 305.477 23.8086 305.477 25.25C305.477 26.7266 304.316 27.8867 302.875 27.8867ZM308.008 21.0664C308.008 20.5391 307.586 20.1172 307.059 20.1172C306.531 20.1172 306.109 20.5391 306.109 21.0664C306.109 21.5938 306.531 22.0156 307.059 22.0156C307.586 22.0156 308.008 21.5938 308.008 21.0664ZM310.68 22.0156C310.609 20.75 310.328 19.625 309.414 18.7109C308.5 17.7969 307.375 17.5156 306.109 17.4453C304.809 17.375 300.906 17.375 299.605 17.4453C298.34 17.5156 297.25 17.7969 296.301 18.7109C295.387 19.625 295.105 20.75 295.035 22.0156C294.965 23.3164 294.965 27.2188 295.035 28.5195C295.105 29.7852 295.387 30.875 296.301 31.8242C297.25 32.7383 298.34 33.0195 299.605 33.0898C300.906 33.1602 304.809 33.1602 306.109 33.0898C307.375 33.0195 308.5 32.7383 309.414 31.8242C310.328 30.875 310.609 29.7852 310.68 28.5195C310.75 27.2188 310.75 23.3164 310.68 22.0156ZM308.992 29.8906C308.746 30.5938 308.184 31.1211 307.516 31.4023C306.461 31.8242 304 31.7188 302.875 31.7188C301.715 31.7188 299.254 31.8242 298.234 31.4023C297.531 31.1211 297.004 30.5938 296.723 29.8906C296.301 28.8711 296.406 26.4102 296.406 25.25C296.406 24.125 296.301 21.6641 296.723 20.6094C297.004 19.9414 297.531 19.4141 298.234 19.1328C299.254 18.7109 301.715 18.8164 302.875 18.8164C304 18.8164 306.461 18.7109 307.516 19.1328C308.184 19.3789 308.711 19.9414 308.992 20.6094C309.414 21.6641 309.309 24.125 309.309 25.25C309.309 26.4102 309.414 28.8711 308.992 29.8906Z" fill="currentColor"/>
<path d="M334.219 23.4062H328.594V17.7812C328.594 17.6406 328.453 17.5 328.312 17.5H327.188C327.012 17.5 326.906 17.6406 326.906 17.7812V23.4062H321.281C321.105 23.4062 321 23.5469 321 23.6875V24.8125C321 24.9883 321.105 25.0938 321.281 25.0938H326.906V30.7188C326.906 30.8945 327.012 31 327.188 31H328.312C328.453 31 328.594 30.8945 328.594 30.7188V25.0938H334.219C334.359 25.0938 334.5 24.9883 334.5 24.8125V23.6875C334.5 23.5469 334.359 23.4062 334.219 23.4062Z" fill="currentColor"/>
</svg>
Aeon currently has native support for the following platforms:
* Facebook
* Instagram
* LinkedIn
* Spotify
* ...more coming soon
Want to see a particular platform added? [Create a GitHub issue](https://github.com/leinelissen/aeon/issues/new/choose) with the name of the particular provider.
Want to help out with adding new platforms? Providers are easily defined with a Provider config. [Check out the documentation on Providers](https://docs.aeon.technology/extending-aeon/architecture/providers) to find out how they work. You can always create a Pull Request
# Contributing
Aeon is being developed out in the open. Have an idea for a feature or a suggestion for a new provider? [Create a GitHub issue](https://github.com/leinelissen/aeon/issues/new/choose) and tag me (@leinelissen) if you need any help.
# Documentation
[](https://aeon.technology/download)
## Using Aeon
You can find the latest build of Aeon over at the [releases page](https://github.com/leinelissen/aeon/releases). There's builds for Windows, macOS and Linux.
If you're feeling more adventurous, clone the repository and compile your own nightly build. The only dependency is [NodeJS](https://nodejs.org/en/download/package-manager/). Prepare the codebase and start a development build by running the following commands:
```
npm install
npm start
```
## The Technical Stuff
Aeon is an [Electron](https://www.electronjs.org/)-based app, a mature platform for building JavaScript applications on the desktop. It is backed by a locally encrypted Git repository, made available through use of the excellent [nodegit](https://www.nodegit.org/) package.
A custom and modular back-end allows for tracking and retrieving data from multiple sources. This is done through retrieval from an API, asynchronous data requests or a combination of both. Parser logic then allows for extracting common data types from the resulting JSON or CSV.
================================================
FILE: SECURITY.md
================================================
# Security Policy
## Supported Versions
Aeon is currently in open beta. Until a v1 is released, only the latest version (as per GitHub releases) is supported. No official support is offered for self-compiled Aeon versions, but any questions are allowed to be posted to the GitHub Issues.
## Reporting a Vulnerability
In case you find a vulnerability in Aeon, please contact [lei@codified.nl](mailto:lei@codified.nl). You can find a PGP key at the bottom of this file.
As soon as your report is received, we will respond to your report within 7 days, indicating necessary changes and required time for addressing the vulnerability. We kindly ask you to not share any details until a fix adressing the issue has been released.
<details>
<summary>GPG Key (lei@codified.nl)</summary>
```
-----BEGIN PGP PUBLIC KEY BLOCK-----
mQINBFgrN6gBEADDyFkpAlIkLFC19FD3K9At1liZUbrcxGdxpsihbd9di/15wTnY
xMJgAQRZCZ68AXUqkmJaxEgNonfItSOcR8M+kBViNeOj+7hRUAy04QzjCleNDQ2Z
Y6tfo7/b1uTbwkq7tWcMLP3MKQHuLLq8JQywWbq1hNKH3Ap7kJatO7UOyA0aL7lM
2Vo8fjRLdenQizJryrA6BqCglmSz/kKcbvOgES6fPMP2MllVxLM1wQDAhyv8VsKM
X5Fjts2vBacGDxafIRVQzadE98fSKg0nlndY+ITRsV0dtNeN7rnqsuPIADLHpDDO
tJ0H3haC4o/AuE01UtwhveLyu6RiMqEVvCGohLKPSCVbqvhQ+MDlH5xpHtI49fF6
dWLdAgMuTEynGiqU/TZR9LpYXdK6uRPQW4VEebRyo6oP7bDFh9uVlhRZnXc3ypxe
Yy2Y1wxzAOv5dSFCH8jxoUWGIOL0YDPjCjoYR9GtujFkKQt5Y5BRiiXfY7vYVpFd
NorLJbvj6+iXzQIA4uoPLC31SETlMy3toJnWSIjEe9dp/6TVXzyuApFBa00lm6pz
zgz2nyDrtSE8+44LjHbe3m6w6Wtet2B2jKFOvfnqfS7QzQIyiJhufNOoXDkLrZV8
RoKQg0DUtfxCKR7LDYk6GSWmp55OOGF/0z3N3wBfFOc00XMVEXE/3t8HtQARAQAB
tB5MZWkgTmVsaXNzZW4gPGxlaUBjb2RpZmllZC5ubD6JAjkEEwEIACMFAlgrN6gC
GwMHCwkIBwMCAQYVCAIJCgsEFgIDAQIeAQIXgAAKCRAln6nAUV1QX/oNEAC9o/aX
CbWzErt6UmHGFI1lWcvFeGDJnTP7ZppFnY6Ru0zxjYrNs1tXjROxsUZoTA1Z8H3Y
FTSK27HT0d0MtCAMfwzWaUvfOTKMeoTZY8Tn6XLV6gHSlnCadTfmd7Os+V7qy2xA
MF/OKTnskQsObJnYxMuungHMC6XFzE895OKnGl+uhrJyZ8Pw475bODVbIyIFLuJc
+Tm/Ty9euL/EwDfw6CW3S7FBQH+gAP+CN0wF6efRfYICvRIaFsPwJkeWuWyfKfCL
cptXakLi+RwK/f71nf/pX+uEe4xLPHAQWTeIWPKu4Z4kIkDU0Wesd5V9MCFZ9U4t
SVbAL634gGz88x6UVKtFhG1GDhU4QO4hIFBtDFescAAZ8wmfeAGoYt7BKRGk7VwS
ojNj5cpcQCMGLq2kMb6Io6A2RTw+W3BgdRJ3Qkz79k1AU/COF/48H2SCt6gNWbdm
37RpkUInWXo6UHd7kQmwttAlbA+UXykU9YZtVlzgSCBA8Y1qdPjqlmlSJiHVHKex
oYVSZIpNmo5XSWmrMyj441UJGUaSP/ABh47H4DBSe1XVADhdNpLei0el0aCYscvp
kkML4NdG9Jg7z2AFuMwAJEPM+Wqkio1RgaM+ppG2eKsqi1Je6kWBL+dGl6Ho9zew
FPXDbjppPi4rn4G2KwWih/O2GkCuiTmeGlvEvbkCDQRYKzeoARAAwgE/0oUWKhHi
LG0CDxahkxKLF7npLa+DAxtpvVgYTp34W9etN85KutqHYc6eJQmR4JGUe0oS6Pz+
vS5lHxbMfz+7UoLbYheepjGyQ/BlU+9Xu1EoM2LVRtSVBvKgvlNOaHQWWOfjvqvf
PWIlejKpQjm+JtGn1/1T+ogC8u3RqDdp8m0l7pfqvTJ41Di3kXtQvwlqXbMQLrHG
zFrmyq9q7psAWUm4oEMVsBgUQBjbDrMx+IDpxldrjjhLwBjWNAB6+KF34ALjF7jY
/0qb80t3GkLVl1D+OMiyoR7TDyHYWLyQseLEqjgfEIFjJyTeWmvZPSHFjfwZ0lKv
pPbFjh3bJNaEiFdOV1gyYLjuJCGTw2wes01qQbvh3Ovsz8gpPsRaxD2uy5+6/EvK
dnUNKd5W66pwiAYCIf7FoLUnwSyie12CM5TvbDXDaGgnI8UQQ4UyQXCcCDL5YigM
ozvJAYuKJ6V3Aou5T+ub4y+jJOZSICWzkwtgYtzj3Jn0C01KvoK7zunkefMXzSSL
nEAJiw/pbpDy0cKZRsq8wol4S14dwuQSXMFt0plIb2Hs4oWMmmqPg5rfKzybMpmU
ggkjiCVI5sd5KpRtkm/QVcUsS+j2DTQ4alrHJHpmILGkxTJ4ahE/+06LZyEO8wYN
2AMEShKhy4PTiLihY4J7eoL4LFefPNEAEQEAAYkCHwQYAQgACQUCWCs3qAIbDAAK
CRAln6nAUV1QX2rUD/4nPos5hJadjrbsj3vzx9/IBby7oXQe7sq3za0vCZpe4pmF
NRE1GEujnDp7NdRkS7i0CLA4PI/CHAYr5ltkmJoeYI9RP+Ix2nvlykRgbPrpFbdh
2P9FY+zF3p/7elW91/zOKQ2GQcOOYewEu5Q7zuLCfuDz7crf7pImzxs7BO974s3p
4qL+PkOOK+YczGjvMT6Ewglz0oEdMUuvUNYhIdRsoTvSmYsMbhSMt1aKTGrkkG0g
g3pRWrswdKMzoBvheSgFt/jdm2prQLjjmjUXqOsyiZPI787MgEv60n2rkScl1IwM
2ia04hU644jrGwpddyD0CtisRDa/aDljRNU/0K1VYGjN2rAcdMP2vg52kpDzpAMs
9t6HbqIFcTFKFhFrQtRy8Rt+gm6fLm164Pm0dPpTzZhrkyTWQOjp4X5TvL6uOh4L
dnID0SlFu4iw0HHDqqynkRgABSHyo5V333bsSuuxhzhwt69tb5ipGxr+ZQ3c1e7R
qtAERkY4Zzer2im/CW+2qg3wyB/FQWsaEy9gtfqUCUiOMcaYNQEhPtWu3YgEcJYP
hdngamPzSpkMf5KbPbQxz8Sz2OyZJMf4vTH7VpG4RhDceY22grzGknl/NxZ4Cdqo
k+6qAbrYL6msx9TAH2Q0HuWujyvilepHIvR8yAVjUHcoqkjyNk5bBJVAmC/Uwg==
=MlxM
-----END PGP PUBLIC KEY BLOCK-----
```
</details>
================================================
FILE: data/.gitignore
================================================
*
!.gitignore
================================================
FILE: docs/README.md
================================================
# Aeon Documentation
.png>)
Welcome to the Aeon documentation, which attempts to make it as easy as possible for you to use and extend the Aeon tool.
.png>)
### What does Aeon do?
Aeon is a tool that is designed to make getting your personal data that is stored by organisations as easy as possible. You can add all the tools and organisations you work with, and Aeon will reach out to them to get your data back.
Once you do get the data, Aeon shows you what has changed in it, and give you the tools to start adding, modifying or updating said data. All made as easy as possible.
.png>)
### How do I use the documentation?
This documentation is split into two sections. First, we discuss how to use Aeon as an end-user. Secondly, we discuss how Aeon can be extended as a developer, to tailor it to the use cases you desire.
{% content-ref url="using-aeon/installation.md" %}
[installation.md](using-aeon/installation.md)
{% endcontent-ref %}
{% content-ref url="extending-aeon/local-development.md" %}
[local-development.md](extending-aeon/local-development.md)
{% endcontent-ref %}
================================================
FILE: docs/SUMMARY.md
================================================
# Table of contents
* [Aeon Documentation](README.md)
## Using Aeon
* [Installation](using-aeon/installation.md)
* [Getting Started](using-aeon/getting-started.md)
## Extending Aeon
* [Local Development](extending-aeon/local-development.md)
* [Reporting Issues](extending-aeon/reporting-issues.md)
* [Architecture](extending-aeon/architecture/README.md)
* [Providers](extending-aeon/architecture/providers.md)
================================================
FILE: docs/extending-aeon/architecture/README.md
================================================
# Architecture
Aeon is built to reach out to various platforms, gather data, store it and then report it to the user. This page will go into detail into how these goals are worked out technically.
## Technologies
### TypeScript
The entire Aeon codebase is written in [TypeScript](https://www.typescriptlang.org/). TypeScript is a language that compiles to Javascript, with the additional benefit of types, describing inputs outputs and datatypes. TypeScript is not only used for a better developer experience, but the data typing system actually relies heavily on TypeScript enums and data formats.
### Electron
Aeon is built on Electron, and leverages its integration with browsers heavily to use existing data download workflows to gather data. Electronc consists of a NodeJS back-end \(called `main` in Electron parlance and the Aeon codebase\) coupled with a Chromium front-end \(dito, called `app`\). You'll find that the codebase is neatly seperated along these lines, with the `src/main` folder containg all back-end code and the `src/app` folder containing all front-end code.
Following Electron best-practices, these environments run seperately and are sandboxes, to prevent external webpages from getting system-level access through NodeJS. This means that communication between the front-end and back-end happens through a set of [bridges](https://www.electronjs.org/docs/api/context-bridge) \(for example, see the [RepositoryBridge](https://github.com/leinelissen/aeon/blob/master/src/main/lib/repository/bridge.ts) and the app-side [Repository utility](https://github.com/leinelissen/aeon/blob/master/src/app/utilities/Repository.ts)\). All front-end calls to the back-end should go through these bridges to ensure security. Additionally, [the preload file](https://github.com/leinelissen/aeon/blob/master/src/app/preload.ts) specifies some unique cases where the front-end has access to back-end methods.
### React
The front-end of the application is built out using [React](https://reactjs.org/), a Javascript framework that allows the building out of reactive interfaces in browsers. The `src/app` folder contains the entire front-end and is seperated out into assets, components, pages, store, style and utilities. Each interface screen is specified in `src/app/pages` and may pull in components from other directories. Styling is done using [Styled Components](https://styled-components.com/), routing using [React Router](https://reactrouter.com/) and the store is backed by [undux](https://undux.org/).
### Git
In order to version incoming data, Aeon builds and updates a local Git repository that is bundled with the application. Aeon interfaces with this repository through [NodeGit](https://www.nodegit.org/), a set of NodeJS bindings for the widely used libgit2 library. All interfacing with the repository is done in NodeJS, though there is a bridge available for calling certain functions from the front-end.
## Concepts
### Providers
The first point of Aeon is making sure data is retrieved from a series of sources. These sources are abstracted into **Providers**. In Aeon, a _Provider_ is a binding for a particular platform \(e.g. Facebook, Instagram, LinkedIn\) that can directly pull data, initiate data requests, download data and parse data. For more detailed info on the inner workings of providers, and how to write one, find the particular page.
>
{% page-ref page="providers.md" %}
================================================
FILE: docs/extending-aeon/architecture/providers.md
================================================
# Providers
The first point of Aeon is making sure data is retrieved from a series of sources. These sources are abstracted into **Providers**. In Aeon, a _Provider_ is a binding for a particular platform \(e.g. Facebook, Instagram, LinkedIn\) that can directly pull data, initiate data requests, download data and parse data.
### Anatomy
Each Provider owns their own folder in `src/main/providers` , and may consist of several files making up two distinct elements: the Provider itself \(a class that either implements `Provider` or `DataRequestProvider`\), and a Parser that interprets the resulting data for visualisation. For instance, see the [Instagram Provider](https://github.com/leinelissen/aeon/blob/master/src/main/providers/instagram/index.ts) and [Instagram Parser](https://github.com/leinelissen/aeon/blob/master/src/main/providers/instagram/parser.ts).
The anatomy of what a Provider, Parser and datatypes look like are described in Typescript, and are available in the [Provider types file](https://github.com/leinelissen/aeon/blob/master/src/main/providers/types.ts). This should be a regular reference when starting to build out your own providers.
### Provider
A Provider is a class that implements the bindings for a particular service. In this example we'll focus on Instagram. Each provider follows a particular workflow that covers a set of methods a platform must support: `initialise`, `update`, `dispatchDataRequest`, `isDataRequestComplete` and `parseDataRequest`.
Please note that a Provider distinguishes between immediately accessible data \(i.e. that can be directly downloaded via an API\) as _updates,_ while data that takes a while to gather and is downloaded in one go is considered as _data requests_.
#### Initialisation
When the user wants to add an account for a certain platform, a new Provider is instantiated, and the thew `initialise` function is called. The point of this function is to authenticate a user to said platform, so that we can make further calls to the platform. In the case of Instagram, a very basic implementation of this looks as follows:
```typescript
class Instagram extends Provider {
initialise(): Promise<boolean> {
return withSecureWindow<boolean>(windowParams, (window) => {
// Load a URL in the browser, and see if we get redirected or not
const profileUrl = 'https://www.instagram.com/accounts/access_tool/ads_interests';
window.loadURL(profileUrl);
// Create a promise that should resolve when the user is logged in
return new Promise((resolve) => {
window.webContents.on('did-navigate', () => {
if (profileUrl === window.webContents.getURL()) {
resolve();
}
});
});
});
}
}
```
A BrowserWindow is opened containing a link to a protected URL. Instagram will normally redirect us to the login page, which is then shown to the user. We then wait for the login form to succeed and redirect us back to the original, protected URL. When we detect this change, the user has successfully logged in and we can return true. We can later use the cookies that are set in this instance, to send out specific requests.
_Small note: you'll find that this implementation slightly differs from the_ [_actual implementation_](https://github.com/leinelissen/aeon/blob/master/src/main/providers/instagram/index.ts)_, mostly in retrieving and setting cookies relevant for later use. This is the simplest implementation, but you should find inspiration in the fully-fledged implementations that are available in the repository._
#### Updates
Now that we have a set of cookies, we can talk to any Instagram data as if we are the user. Fortunately, Instagram has a set of data APIs available that we can immediately gather our data from:import scrapingUrls from './urls.json';
```typescript
import scrapingUrls from './urls.json';
// eg: [
// "https://www.instagram.com/accounts/access_tool/account_privacy_changes?__a=1",
// "https://www.instagram.com/accounts/access_tool/password_changes?__a=1",
// "https://www.instagram.com/accounts/access_tool/former_emails?__a=1",
// ....
class Instagram extends Provider {
update = async (): Promise<ProviderFile[]> => {
const cookies = await this.verifyLoggedInStatus();
// We extract the right cookies, and create a config we can then
// use for successive requests
const sessionid = cookies.find(cookie => cookie.name === 'sessionid').value;
// Then, we'll setup a config for each individual fetch requests
const fetchConfig = {
method: 'GET',
headers: {
Accept: 'application/json',
Referer: 'https://www.instagram.com/accounts/access_tool/ads_interests',
'X-CSRFToken': crypto.randomBytes(20).toString('hex'),
cookie: `sessionid=${sessionid}; shbid=${''}`
},
};
// Now we do all API requests in order to retrieve the data
const responses = await Promise.all(
scrapingUrls.map(url =>
fetch(url, fetchConfig).then(response => response.json())
)
);
// We then transform the data so that we can return it to the handler
return responses.map(response => {
return {
filepath: `${response.page_name}.json`,
data: JSON.stringify(response.data.data, null, 4),
};
});
}
}
```
As you can see, we have a set of URLs available, and by stealing some of the cookies from the browser, we can just call all APIs, and return the data gathered from it to the ProviderManager. For this we follow the format `{ filepath: 'path_to_file', data: { //data }` .
_Note that retrieving data from APIs may not as easy with all services. If you do not want to implement immediate updates, you can return `null` from this function._
#### Dispatch Data Requests
Lots of services have an automated way of downloading your data. You click a button, wait some time, and then download a .ZIP file. Our goal is to automate the entirety of this process, starting with actually requesting the data. In our reference, Instagram has a dedicated page for these kinds of requests:
```typescript
class Instagram extends Provider {
async dispatchDataRequest(): Promise<void> {
return withSecureWindow<void>(windowParams, async (window) => {
// Load the dispatched window
window.hide();
await new Promise((resolve) => {
window.webContents.on('did-finish-load', resolve)
window.loadURL('https://www.instagram.com/download/request/');
});
// We'll click the button for the user, but we'll need to defer to the
// user for a password
window.webContents.executeJavaScript(`
Array.from(document.querySelectorAll('button'))
.find(el => el.textContent === 'Next')
.click?.()
`);
window.show();
// Now we must defer the page to the user, so that they can enter their
// password. We then listen for a succesfull AJAX call
return new Promise((resolve) => {
window.webContents.session.webRequest.onCompleted({
urls: [ 'https://*.facebook.com/*' ]
}, (details: Electron.OnCompletedListenerDetails) => {
console.log('NEW REQUEST', details);
if (details.url === 'https://www.facebook.com/api/graphql/'
&& details.statusCode === 200) {
resolve();
}
});
});
});
}
}
```
As you can see, Instagram requires their user so enter a password before they can send out a request. To resolve this, we present this page to the user and wait for them to enter their password. We then use particular Electron listeners to wait for the form to be submitted.
#### Check if Data Request is complete
It then might take a while for the data request to resolve. We currently use a polling approach to periodically check the particular page to see if the request has been completed:
```typescript
class Instagram extends Provider {
async isDataRequestComplete(): Promise<boolean> {
return withSecureWindow<boolean>(windowParams, async (window) => {
// Load page URL
await new Promise((resolve) => {
window.webContents.once('did-finish-load', resolve)
window.loadURL('https://www.instagram.com/download/request/');
});
// Find a heading that reads 'Your Download is Ready'
return window.webContents.executeJavaScript(`
!!Array.from(document.querySelectorAll('h1'))
.find(el => el.textContent === 'Your Download is Ready');
`);
});
}
}
```
This comes down to loading the page and seeing if a particular element \(in this case a h1 tag reading "Your download is ready"\) is present on the page. This functions thus return true or false depending on whether the request is complete.
#### Download the Data Request
If the data request is complete, all that is left is downloading it and passing it back to Aeon. This is done as follows:
```typescript
import { app } from 'electron';
import AdmZip from 'adm-zip';
import fs from 'fs';
const requestSavePath = path.join(app.getAppPath(), 'data');
class Instagram extends Provider {
async parseDataRequest(extractionPath: string): Promise<ProviderFile[]> {
return withSecureWindow<ProviderFile[]>(windowParams, async (window) => {
// Load page URL
await new Promise((resolve) => {
window.webContents.once('did-finish-load', resolve)
window.loadURL('https://www.instagram.com/download/request/');
});
await new Promise((resolve) => {
// Now we defer to the user to enter their credentials
window.webContents.once('did-navigate', resolve);
window.webContents.executeJavaScript(`
Array.from(document.querySelectorAll('button'))
.find(el => el.textContent === 'Log In Again')
.click?.()
`);
});
// We can now show the window for the login screen
window.show();
// Then we'll await the navigation back to the data download page from
// the login page
await new Promise((resolve) => {
window.webContents.once('will-navigate', resolve);
});
// We can now close the window
window.hide();
// Now that we're successfully authenticated on the data download page,
// the only thing we have to do is download the data.
const filePath = path.join(requestSavePath, 'instagram.zip');
await new Promise((resolve) => {
// Create a handler for any file saving actions
window.webContents.session.once('will-download', (event, item) => {
// Save the item to the data folder temporarily
item.setSavePath(filePath);
item.once('done', resolve);
});
// And then trigger the button click
window.webContents.executeJavaScript(`
Array.from(document.querySelectorAll('button'))
.find(el => el.textContent === 'Download Data')
.click?.()
`);
});
// We have the ZIP, all that's left to do is unpack it and pipe it to
// the repository
const zip = new AdmZip(filePath);
await new Promise((resolve) =>
zip.extractAllToAsync(extractionPath, true, resolve)
);
// Translate this into a form that is readable for the ParserManager
const files = zip.getEntries().map((entry): ProviderFile => {
return {
filepath: entry.entryName,
data: null,
};
});
// And dont forget to remove the zip file after it's been processed
await fs.promises.unlink(filePath);
return files;
});
}
}
```
While this piece of code is a bit longer, the process is still quite simple. First, we click the download button for the user. As Instagram requires users to enter their password before downloading the file, we open the window to the user and wait for them to enter their password. We then wait for the download to be registered by the BrowserWindow, save it to a particular location, unpack it and return a list of the files that were created by this dump. Aeon will recognise this data, and create a new commit that includes all changed files from the dump.
#### withSecureWindow
In the reference implementations, you'll find ample use of the `withSecureWindow` function. This functions creates and spawns an [Electron BrowserWindow](https://www.electronjs.org/docs/api/browser-window) for the duration of a set of requests. It returns a promise that can be returned from the callback, and rejects if an error occurs, or the user closes the window. It makes it easy to create windows a user should interact with \(e.g. to log in or to manually click a button\), while keeping everything secure. If you wish to keep state \(i.e. cookies\) between different windows, you should specify a global `windowParams` object that is implemented in each call to `withSecureWindow`.
### Parsers
Now that data is actually present in the repository, we need to parse into a readable format for the front-end. We do this by means of parsers. The basic layout for a parser is as follows:
```typescript
const parsers: ProviderParser[] = [
{
source: 'accounts_following_you.json',
schemas: [
{
key: 'text',
type: ProvidedDataTypes.FOLLOWER
}
],
},
];
```
A parser is an array of files that have specific schemas attached to them. First, a new object is specified with a source: this is the filename for a particular file that is found in the repository folder for the particular Provider. Next, a schema is a mapping of a key found anywhere in that file, to a particular datatype. This means that in the file `accounts_following_you.json` , the schema looks for any value that is associated with the key `text`. This pieces of data is then associated with the `FOLLOWER` datatype when Aeon attempts to parse the data.
In case there isn't a neat mapping, or some data needs to be converted, a `transformer` can be set on the schema that interprets and transforms the data:
```typescript
import { parseISO } from 'date-fns';
/**
* This specifies an input object in which the data is structured in an object,
* in which the keys represent usernames, and the values are ISO dates. These
* can be iterated upon to extract the desired data
*/
type GenericKeyedData = {
[key: string]: string
};
const parsers: ProviderParser[] = [
{
source: 'connections.json',
schemas: [
{
key: 'following',
type: ProvidedDataTypes.ACCOUNT_FOLLOWING,
// Transform the data from { username: '9 Jan 2011', ... } to an array of
// objects that store both the username and the date
transformer: (obj: GenericKeyedData): Partial<AccountFollowing>[] => {
// Loop through each available key on the object
return Object.keys(obj).map((key): Partial<AccountFollowing> => {
// Return an object in which the data is the key (username)
// and the timestamp is a JS Date object.
return {
data: key,
timestamp: parseISO(obj[key]),
};
});
}
}
],
},
];
```
There are many more possibilities with the returned data and the transformer. Consult the [`ProviderParser` types](https://github.com/leinelissen/aeon/blob/master/src/main/providers/types.ts) to see what possibilities you can use. For instance, you can also specify multiple keys for the same schema.
### Registering Providers and Parsers
If you have managed to setup your own providers and parsers, the last thing that is left is registering them with Aeon. This is done in three places:
First, add the `Provider` class you created to the `providers` array in [`src/main/providers/index.ts`](https://github.com/leinelissen/aeon/blob/master/src/main/providers/index.ts). Then, add the resulting `ProviderParser` object to the `providerParsers` object in [`src/main/providers/parsers.ts`](https://github.com/leinelissen/aeon/blob/master/src/main/providers/parsers.ts). Lastly, add an icon for your provider to the `getIcon` method in [`src/app/utilities/Providers.ts`](https://github.com/leinelissen/aeon/blob/master/src/app/utilities/Providers.ts) to make sure your Provider appears pretty in the interface. Job well done!
================================================
FILE: docs/extending-aeon/local-development.md
================================================
# Local Development
As Aeon is a desktop app, yout development happens locally as well. In this guide, we'll go step by step what you need to do, to get Aeon set up locally and get started on its development.
### Working with the terminal
Aeon is built using command line tools \(sometimes abbreviated as CLI\). These are tools that don't use neat graphical interfaces, but a terminal interface: the old-school style text-based screen you usually only see in movies. To access the command line, you need to open up a terminal in your operating system. Either use one of your OS-provided terminals \(Terminal for macOS and Ubuntu; Command Prompt for Windows\), or find a cross-platform terminal app such as [Hyper](https://hyper.is/).
We actually recommend working with [Visual Studio Code](https://code.visualstudio.com/) for this reason: it comes with a terminal built-in! You can open it up with Ctrl + Backtick on Windows/Linux and Cmd + Backtick on macOS. Do make sure you know how to operate your terminal before you start working on Aeon. To help you get along, find this [guide on how to operate the Ubuntu terminal](https://ubuntu.com/tutorials/command-line-for-beginners#1-overview). You'll find that the macOS and Linux terminal are actually pretty similar once you get inside.
These guides will assume you are working on a UNIX terminal \(e.g. bash, zsh, fish\), as these work across macOS, FreeBSD and Linux distributions. If you are on a Windows machine and you cannot translate these to Command Prompt or Powershell commands yourself, consider installing the [Windows Subsystem for Linux](https://docs.microsoft.com/en-us/windows/wsl/install-win10). This allows you to run a bash terminal on Windows, on which all commands in this guide should work flawlessly.
### Prerequisites
Aeon is based on [Electron](https://www.electronjs.org/), a tool that combines the [Chromium](https://www.chromium.org/) browser \(on which [Chrome](https://www.google.com/chrome/) is built\) with a NodeJS \(best described as server-side Chromium again\) back-end in a single, executable desktop package. It is no suprise that everything is written in Javascript. In order to get started, you're going to need a couple of tools so that we can actually compile and run the application. These are as follows:
#### Git
A version control tool for code: Git helps you make small changes to a large codebase such as Aeon's. Either [install the binary directly](https://git-scm.com/downloads), or use one of your favorite package managers:
Windows \([Chocolatey](https://chocolatey.org/docs/installation)\)
```text
choco install git
```
macOS \([Homebrew](https://brew.sh/)\)
```text
brew install git
```
Ubuntu \(apt\)
```text
sudo apt-get install git
```
#### NodeJS
A server runtime for JavaScript: helps you build powerful Javascript-based applications that can run on the desktop rather than the web! Either [install the binary directly](https://nodejs.org/en/) or use one of your favorite package managers:
Windows \([Chocolatey](https://chocolatey.org/docs/installation)\)
```text
choco install nodejs
```
macOS \([Homebrew](https://brew.sh/)\)
```text
brew install node
```
For Linux installs, find the relevant package manager of your choice in the [NodeJS install guide](https://nodejs.org/en/download/package-manager/).
### Cloning the repository
Now that you have installed all prerequisites, you need the source code that compiles to the Aeon application. Fortunately, this is accessible from GitHub and you can copy it to your computer very easily. First, make sure you navigate to the folder that will hold the folder containing all the Aeon code. Make sure you navigate to a particular folder on your own system.
```text
cd Documents/Code
```
Now that we're in the directory, we're going to copy the entire codebase to a folder within this directory. This operation is called cloning in Git terminology. You do it as follows:
```text
git clone https://github.com/leinelissen/aeon
```
When the command finishes, the Aeon codebase should be found in a folder called `aeon`. Navigate into this folder to get started on installing dependencies.
```text
cd aeon
```
### Installing dependencies
Aeon builds upon lots of tools and libraries that make working with data, interfaces and other stuff a lot easier. These dependencies must be installed first, before we can start compiling the application. You do this as follows:
```text
npm install
```
### Development Mode
In order to make development really easy, Aeon has a development mode that incrementally compiles Aeon and runs it immediately. This means that when you change a file, the application is re-compiled and reloaded. To start development mode, run the following command:
```text
npm run start
```
#### Using the Visual Studio Code Debugger
The repository also includes a basic setup for integrating the VSCode debugger. This allows you to inspect objects that are logged, set breakpoints and do object inspection while you are developing. To use it, either press `F5` or run `Electron Main` from the debugger tab.
### Compiling
If you want to generate an application package for distribution, you can run a build command as follows:
```text
npm run make
```
When the command finishes \(note: this may take a while\), you can find a build for your specific platform in the `out/make` folder.
### What's next?
Now that you know how that make local development work, have a look at the architecture diagram, and how specific core concepts work. If you feel adventurous, you can get to work and start creating pull requests with new features.
{% page-ref page="architecture/" %}
{% page-ref page="reporting-issues.md" %}
================================================
FILE: docs/extending-aeon/reporting-issues.md
================================================
# Reporting Issues
Aeon is still a work in progress, so you might run into issues while using, building or abusing the application. This is expected. Alternatively, you might have suggestions for new platforms to add, or other features you think are crucial to its development. We accept and welcome any issues or feedback you have for this application.
In order to give some shape to this feedback, please use the GitHub issue tracker to report anything you find interesting. You can [create a new issue here](https://github.com/leinelissen/aeon/issues/new/choose) or [browse through existing issues here](https://github.com/leinelissen/aeon/issues). If you find an existing issue already matches your feedback, please consider adding your feedback to the already existing issue.
================================================
FILE: docs/using-aeon/getting-started.md
================================================
# Getting Started
Not sure how to get started with Aeon? Let us guide you through a few crucial steps to get started with requesting and owning your personal information!
Make sure you have installed Aeon first. If you don't know how to do so, [follow our guide](installation.md).
### Adding Accounts
First, find the accounts tab in the left menu.

Then, click the **Add New Account** button on the bottom of the screen.

A screen pops up, in which you can select a particular provided you would like to add. Click the platform of your choice (in this case Facebook) and the click the **Add New Facebook Account** button. 
Once you have clicked the button, a new window will pop up for logging in to the platform. Enter your credentials and click login.
Note: Aeon never stores your password, and all tokens that are kept on your behalf are stored locally and safely.
Your account is now set up, and you can feel free to add other accounts too!
### Starting Data Requests
If your accounts are all set up, you can browser through them in the Accounts tab.
 

 If you wish to retrieve data, select an account and click **Start Data Request**. You might need to enter your credentials again.
Data requests often take time. If your request is started successfully, you can kick it back and wait the hours / days it takes for the data request to complete. Aeon will let you know when the request is done.
### Completing Data Requests
Whenever a data request is finished, Aeon will let you know. You can complete a finished data request by going to the account and clicking **Complete Data Request**. Your data will be downloaded into Aeon.
### Visualising Data
Once your data is in Aeon, you can use its powerful visualisation tools to see what is going on with your data. Find the Graph tab on the left side, for instance.

The large blue balls represent platforms, the squares represent data types, and the small dots represent a single data point that was retrieved from the platform.
If you like, you can click an individual data point to inspect it:

### Removing Data
Feel like a platform should not be having one of your data points? You can automatically generate a request for deleting it. Click the **Delete This Data Point** button in any data visualisation to add it to data points slated for deleting.
 
.png>)
If you are sure you want to delete the data points, go to the **Erasure (1)** tab on the left-hand side.

Aeon will then display a list of datapoints that you have selected. Click **Remove 1 Data Point **to generate the request.

An email has been generated for each platform of which data is deleted. You will need to send this email yourself. Click **Send in E-mail Client** to open your default email client, so you can send the email. 
================================================
FILE: docs/using-aeon/installation.md
================================================
# Installation
Installing Aeon is really easy: [find the latest release on GitHub](https://github.com/leinelissen/aeon/releases/latest), and download the right package for your architecture. At this moment the following operating systems are supported.
* Windows (with setup.exe or with NuGet: .nupkg)
* macOS (the darwin .zip)
* Ubuntu / Debian (the .deb file)
* Fedora / CentOS / RHEL (the .rpm file)
If you find a platform you are regularly using missing, please create an issue, or find our guide to compiling your own version. ARM is theoretically supported, but please do report any issues you encounter while trying to do so.
### Getting Started
Not sure how to get started? Follow our guide:
{% content-ref url="getting-started.md" %}
[getting-started.md](getting-started.md)
{% endcontent-ref %}
Run into issues? Make sure you report them, so that we can improve on the software:
{% content-ref url="../extending-aeon/reporting-issues.md" %}
[reporting-issues.md](../extending-aeon/reporting-issues.md)
{% endcontent-ref %}
================================================
FILE: entitlements.plist
================================================
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.cs.allow-jit</key>
<true/>
<key>com.apple.security.cs.debugger</key>
<true/>
</dict>
</plist>
================================================
FILE: forge.config.js
================================================
const package = require('./package.json');
const hash = require('child_process')
.execSync('git rev-parse --short HEAD')
.toString().trim()
/**
* This is the base electron-forge config
*/
const config = {
packagerConfig: {
name: process.platform === 'linux' ? 'aeon' : 'Aeon',
icon: './src/icon',
executableName: process.platform === 'linux' ? 'aeon' : 'Aeon',
asar: false,
buildVersion: `${package.version}-${hash}`,
protocols: [
{
name: "Aeon",
schemes: ["aeon"]
}
]
},
makers: [
{
name: '@electron-forge/maker-squirrel',
config: {
iconUrl: 'https://raw.githubusercontent.com/leinelissen/aeon/master/src/icon.ico',
setupIcon: './src/icon.ico'
}
},
{
name: '@electron-forge/maker-dmg',
config: {
// background: './assets/dmg-background.png',
format: 'ULFO'
}
},
{
name: "@electron-forge/maker-zip",
platforms: [ "darwin" ],
},
{
name: '@electron-forge/maker-deb',
config: {}
},
{
name: '@electron-forge/maker-rpm',
config: {}
}
],
plugins: [
{
name: '@electron-forge/plugin-webpack',
config: {
// HMR Woes: https://github.com/electron-userland/electron-forge/issues/2560
devServer: {
liveReload: false,
},
mainConfig: './webpack.main.config.js',
renderer: {
config: './webpack.renderer.config.js',
entryPoints: [
{
html: './src/app/index.ejs',
js: './src/app/index.tsx',
name: 'main_window',
preload: {
js: './src/app/preload.ts',
},
}
]
},
loggerPort: 9001
}
},
],
};
/**
* This function inserts config for notarizing applications.
* Idea stolen from: https://github.com/electron/fiddle/blob/master/forge.config.js
*/
function notarizeMaybe() {
// GUARD: Only notarize macOS-based applications
if (process.platform !== 'darwin') {
return;
}
// Only notarize in CI
if (!process.env.CI) {
console.log(`Not in CI, skipping notarization`);
return;
}
// GUARD: Credentials are required
if (!process.env.APPLE_ID || !process.env.APPLE_ID_PASSWORD) {
console.warn(
'Should be notarizing, but environment variables APPLE_ID or APPLE_ID_PASSWORD are missing!',
);
return;
}
// Inject the notarization config if everything is right
config.packagerConfig.osxNotarize = {
appBundleId: 'nl.leinelissen.aeon',
appleId: process.env.APPLE_ID,
appleIdPassword: process.env.APPLE_ID_PASSWORD,
ascProvider: '238P3C58WC',
};
// Also inject signing config
config.packagerConfig.osxSign = {
identity: 'Developer ID Application: Bureau Moeilijke Dingen BV (238P3C58WC)',
'hardened-runtime': true,
entitlements: 'entitlements.plist',
'entitlements-inherit': 'entitlements.plist',
'signature-flags': 'library',
};
}
notarizeMaybe();
/**
* For some reason OpenSSL isn't compiled directly into the NodeGit native module. We
* thus have to include manually on Windows only.
*/
function bundleOpenSSLMaybe() {
if (process.platform !== 'win32') {
return;
}
// Add a hook to include the file
config.hooks = {
postPackage: async () => {
const fs = require('fs');
const path = require('path');
await fs.promises.copyFile(
// TODO: It's probably a bad idea to hardcode the DLL location here. Maybe it
// is a good idea to pull it from some side of config or environment variable.
'C:\\WINDOWS\\system32\\libcrypto-1_1-x64.dll',
path.join(__dirname, 'out', 'Aeon-win32-x64', 'resources', 'app', '.webpack', 'main', 'native_modules', 'build', 'Release', 'libcrypto-1_1-x64.dll'),
);
}
};
}
bundleOpenSSLMaybe();
// Finally, export it
module.exports = config;
================================================
FILE: package.json
================================================
{
"name": "aeon",
"productName": "Aeon",
"version": "0.2.22",
"description": "Online identity versioning for the masses",
"main": ".webpack/main",
"scripts": {
"start": "electron-forge start",
"package": "electron-forge package",
"make": "cross-env NODE_ENV=production electron-forge make",
"publish": "electron-forge publish",
"lint": "eslint --ext .ts,.tsx ./src && tsc --noEmit",
"test": "playwright test",
"prepare:nodegit": "node scripts/prepareNodegit.js",
"rebuild:native-modules": "electron-rebuild",
"postinstall": "npm run prepare:nodegit",
"prepare": "husky install"
},
"keywords": [],
"author": {
"name": "Lei Nelissen",
"email": "lei@codified.nl"
},
"license": "EUPL-1.2",
"repository": {
"type": "git",
"url": "https://github.com/leinelissen/aeon"
},
"devDependencies": {
"@electron-forge/cli": "6.1.0",
"@electron-forge/maker-deb": "6.1.0",
"@electron-forge/maker-dmg": "6.1.0",
"@electron-forge/maker-rpm": "6.1.0",
"@electron-forge/maker-squirrel": "6.1.0",
"@electron-forge/maker-zip": "6.1.0",
"@electron-forge/plugin-webpack": "6.1.0",
"@playwright/test": "1.31",
"@swc/core": "1.3.42",
"@types/adm-zip": "0.5.0",
"@types/cytoscape": "3.19.9",
"@types/imapflow": "^1.0.9",
"@types/lodash-es": "4.17.7",
"@types/mailparser": "3.4.0",
"@types/node": "16.18.22",
"@types/node-fetch": "3.0.3",
"@types/nodegit": "0.28.3",
"@types/nodemailer": "6.4.7",
"@types/react": "17.0.55",
"@types/react-dom": "18.0.11",
"@types/react-redux": "7.1.25",
"@types/react-select": "5.0.1",
"@types/source-map-support": "0.5.6",
"@types/stream-chain": "2.0.1",
"@types/styled-components": "5.1.26",
"@types/yargs": "17.0.24",
"@typescript-eslint/eslint-plugin": "5.57.0",
"@typescript-eslint/parser": "5.57.0",
"@vercel/webpack-asset-relocator-loader": "1.7.3",
"cross-env": "7.0.3",
"csp-html-webpack-plugin": "5.1.0",
"css-loader": "6.7.3",
"dotenv-webpack": "7.1.1",
"electron": "23.2.0",
"electron-devtools-installer": "3.2.0",
"eslint": "8.37.0",
"eslint-config-airbnb": "19.0.4",
"eslint-config-airbnb-typescript": "17.0.0",
"eslint-import-resolver-typescript": "3.5.4",
"eslint-plugin-deprecation": "1.3.3",
"eslint-plugin-import": "2.27.5",
"eslint-plugin-jsx-a11y": "6.7.1",
"eslint-plugin-react": "7.32.2",
"eslint-plugin-react-hooks": "4.6.0",
"husky": "8.0.3",
"mini-css-extract-plugin": "2.7.5",
"node-loader": "2.0.0",
"playwright": "1.31",
"swc-loader": "0.2.3",
"typescript": "4.9.5",
"webpack-bundle-analyzer": "4.8.0"
},
"dependencies": {
"@fast-csv/parse": "4.3.6",
"@fontsource/ibm-plex-mono": "4.5.13",
"@fontsource/ibm-plex-sans": "4.5.13",
"@fontsource/inter": "4.5.15",
"@fortawesome/fontawesome-svg-core": "^6.2.1",
"@fortawesome/free-brands-svg-icons": "6.4.0",
"@fortawesome/free-solid-svg-icons": "6.4.0",
"@fortawesome/react-fontawesome": "0.2.0",
"@metrichor/jmespath": "0.3.1",
"@pmmmwh/react-refresh-webpack-plugin": "0.5.10",
"@popperjs/core": "2.11.7",
"@reactour/tour": "2.13.0",
"@reduxjs/toolkit": "1.9.3",
"adm-zip": "0.5.10",
"cytoscape": "3.23.0",
"cytoscape-fcose": "2.2.0",
"date-fns": "2.29.3",
"electron-squirrel-startup": "1.0.0",
"electron-store": "8.1.0",
"eventemitter2": "6.4.9",
"history": "5.3.0",
"imapflow": "1.0.125",
"keytar": "7.9.0",
"lodash-es": "4.17.21",
"mailparser": "3.6.4",
"node-abi": "3.33.0",
"node-fetch": "3.3.1",
"nodegit": "0.28.0-alpha.21",
"nodemailer": "6.9.1",
"react": "18.2.0",
"react-dom": "18.2.0",
"react-popper": "2.3.0",
"react-redux": "8.0.5",
"react-refresh": "0.14.0",
"react-router-dom": "6.9.0",
"react-select": "5.7.2",
"react-spring": "9.7.1",
"redux": "4.2.1",
"redux-persist": "6.0.0",
"stream-chain": "2.2.5",
"styled-components": "5.3.9",
"v8-compile-cache": "2.3.0",
"winston": "3.8.2",
"yargs": "17.7.1"
}
}
================================================
FILE: playwright.config.ts
================================================
import { PlaywrightTestConfig } from '@playwright/test';
import path from 'path';
const config: PlaywrightTestConfig = {
testDir: './test',
maxFailures: 0,
outputDir: path.join(__dirname, 'test', 'output', 'playwright'),
workers: 1,
use: {
screenshot: 'on',
trace: 'on',
},
};
export default config;
================================================
FILE: renovate.json
================================================
{
"extends": [
"config:base",
"group:allNonMajor",
"schedule:earlyMondays"
],
"git-submodules": {
"enabled": true
}
}
================================================
FILE: scripts/prepareNodegit.js
================================================
const path = require('path');
const fs = require('fs');
const debugPath = path.join(__dirname, '..', 'node_modules', 'nodegit', 'build', 'Debug');
const mainEntry = path.join(__dirname, '..', 'node_modules', 'nodegit', 'lib', 'nodegit.js');
// First, we add an empty debug file, so we can satisfy webpack one exists
fs.promises.mkdir(debugPath, { recursive: true })
.then(() => {
return fs.promises.writeFile(path.join(debugPath, 'nodegit.node'), '');
});
// Then we modify the base files to not use dynamic requires
fs.promises.readFile(mainEntry)
.then((file) => {
const data = file.toString('utf-8');
const newData = data.replace(/importExtension\(\"(.*?)\"\);/g, (match, name) => {
return `try { require('./${name}'); } catch { }`;
});
return fs.promises.writeFile(mainEntry, newData);
})
================================================
FILE: scripts/setupMacOSCertificates.sh
================================================
#!/usr/bin/env sh
# Based on https://github.com/electron/fiddle/blob/master/tools/add-macos-cert.sh
KEY_CHAIN=build.keychain
MACOS_CERT_P12_FILE=certificate.p12
# Recreate the certificate from the secure environment variable
echo $MACOS_CERT_P12 | base64 --decode > $MACOS_CERT_P12_FILE
#create a keychain
security create-keychain -p actions $KEY_CHAIN
# Make the keychain the default so identities are found
security default-keychain -s $KEY_CHAIN
# Unlock the keychain
security unlock-keychain -p actions $KEY_CHAIN
security import $MACOS_CERT_P12_FILE -k $KEY_CHAIN -P $MACOS_CERT_PASSWORD -T /usr/bin/codesign;
security set-key-partition-list -S apple-tool:,apple: -s -k actions $KEY_CHAIN
# remove certs
rm -fr *.p12
================================================
FILE: src/app/assets/open-data-rights.ts
================================================
import { IconDefinition, IconName } from '@fortawesome/fontawesome-svg-core';
const faOpenDataRights: IconDefinition = {
prefix: 'fab',
iconName: 'open-data-rights' as IconName,
icon: [
512,
512,
[],
'f1e9',
'M500.2,399.6L392.4,291.8C452.5,272.4,496,216,496,149.5C496,66.9,429.1,0,346.5,0S197,66.9,197,149.5v47.6h-47.6 C66.9,197.1,0,264,0,346.5S66.9,496,149.5,496c66.5,0,122.9-43.5,142.3-103.6l107.8,107.8c7,7,16.1,10.5,25.2,10.5 s18.3-3.5,25.2-10.5l50.2-50.2C514.2,436.2,514.2,413.5,500.2,399.6z M299,149.5c0-26.2,21.3-47.6,47.6-47.6s47.6,21.3,47.6,47.6 c0,26.2-21.3,47.6-47.6,47.6H299V149.5z M247.8,299H264v16.2L247.8,299z M230.4,427.5c-21.6,21.6-50.4,33.5-81,33.5 c-30.6,0-59.3-11.9-81-33.5c-21.6-21.6-33.5-50.4-33.5-81c0-30.6,11.9-59.3,33.5-81c21.6-21.6,50.4-33.5,81-33.5H197v49.4h-47.6 c-35.9,0-65.1,29.2-65.1,65.1s29.2,65.1,65.1,65.1s65.1-29.2,65.1-65.1v-35.9c1.7,4.7,4.5,9,8.2,12.8l40,40 C259.2,387.6,248,409.9,230.4,427.5z M179.5,316.5v30.1c0,16.6-13.5,30.1-30.1,30.1c-16.6,0-30.1-13.5-30.1-30.1 s13.5-30.1,30.1-30.1H179.5z M475.5,425.3l-50.2,50.2c-0.1,0.1-0.2,0.2-0.5,0.2s-0.4-0.1-0.5-0.2L298.9,350.1c0-1.2,0-2.4,0-3.6V299 h47.6c1.2,0,2.4,0,3.5,0l125.4,125.4c0.1,0.1,0.2,0.2,0.2,0.5C475.7,425.1,475.6,425.3,475.5,425.3z',
],
};
export default faOpenDataRights;
================================================
FILE: src/app/components/App.tsx
================================================
import React, { Component } from 'react';
import styled, { StyleSheetManager } from 'styled-components';
import { HashRouter } from 'react-router-dom';
import { Provider } from 'react-redux';
import { PersistGate } from 'redux-persist/integration/react';
import 'app/styles';
import Pages from 'app/screens';
import store, { persistor } from 'app/store';
import Loading from './Loading';
import { RepositorySubscription } from 'app/store/data/selectors';
import { ProviderSubscription } from 'app/store/accounts/selectors';
import { EmailSubscription } from 'app/store/email/selectors';
import Tour from './Tour';
const Main = styled.main`
position: relative;
`;
class App extends Component {
componentDidMount(): void {
document.getElementById('loader').style.display = 'none';
}
render(): JSX.Element {
return (
<HashRouter>
<StyleSheetManager disableVendorPrefixes>
<Provider store={store}>
<Tour>
<PersistGate loading={<Loading />} persistor={persistor}>
{/** Presentational components */}
<Main>
<Pages />
</Main>
{/** Subscription managers */}
<ProviderSubscription />
<EmailSubscription />
<RepositorySubscription />
</PersistGate>
</Tour>
</Provider>
</StyleSheetManager>
</HashRouter>
);
}
}
export default App;
================================================
FILE: src/app/components/Button.tsx
================================================
import React, { CSSProperties, HTMLAttributes, PropsWithChildren } from 'react';
import styled, { css } from 'styled-components';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { Ball } from './Loading';
import { IconProp } from '@fortawesome/fontawesome-svg-core';
import { Link } from 'react-router-dom';
type ThemeColor = 'blue' | 'red' | 'yellow' | 'green' | 'gray';
interface ButtonProps extends HTMLAttributes<HTMLButtonElement> {
backgroundColor?: ThemeColor;
fullWidth?: boolean;
color?: string;
}
const StyledButtonStyles = css<ButtonProps>`
background-color: var(--color-${(props) => props.backgroundColor || 'blue'}-500);
display: flex;
align-items: center;
justify-content: center;
color: ${(props) => props.color || 'var(--color-white)'};
height: 40px;
font-size: 14px;
font-weight: 500;
text-decoration: none !important;
font-family: var(--font-heading);
border-radius: 8px;
outline: 0 !important;
margin: 4px 0;
border: 0;
padding: 0 16px;
border: 1px solid var(--color-${(props) => props.backgroundColor || 'blue'}-600);
overflow-wrap: break-word;
letter-spacing: -0.3px;
&:hover&:not(:disabled) {
cursor: default;
background-color: var(--color-${(props) => props.backgroundColor || 'blue'}-600);
}
&:disabled {
background-color: var(--color-gray-300);
cursor: not-allowed;
color: var(--color-gray-600);
border-color: var(--color-gray-400);
}
${(props) => props.fullWidth && css`
width: 100%;
`}
`;
const StyledButton = styled.button<ButtonProps>`
${StyledButtonStyles}
`;
const Label = styled.div`
flex: 0 1 auto;
min-width: 0;
white-space: nowrap;
overflow: hidden;
`;
const Margin = styled.div`
width: 8px;
`;
export const LinkButton = styled(Link)`
${StyledButtonStyles};
`;
export const SimpleButton = styled.button`
border: 0;
margin: 0;
padding: 0;
background-color: inherit;
color: inherit;
`;
export const StyledGhostButton = styled(StyledButton)`
color: ${(props) => props.backgroundColor && props.backgroundColor !== 'blue'
? `var(--color-${props.backgroundColor}-500)`
: 'var(--color-heading)'};
font-size: 14px;
padding: 8px 16px;
background-color: ${() => 'transparent'};
border: 0;
font-weight: 500;
&:hover&:not(:disabled) {
background-color: var(--color-${(props) => props.backgroundColor || 'blue'}-50);
}
&:active {
background-color: var(--color-${(props) => props.backgroundColor || 'blue'}-100);
}
`;
interface Props extends ButtonProps {
loading?: boolean;
onClick?: (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void;
disabled?: boolean;
icon?: IconProp;
style?: CSSProperties;
}
const Button = ({ children, loading, onClick, disabled, icon, ...props }: PropsWithChildren<Props>): JSX.Element => {
return (
<StyledButton onClick={onClick} disabled={loading || disabled} {...props}>
{icon && !loading ? <FontAwesomeIcon icon={icon} style={{ marginRight: 8 }} fixedWidth /> : null}
<Label>
{children}
</Label>
{loading ? (<><Margin /><Ball size={10} /></>) : null}
</StyledButton>
);
};
export const GhostButton = ({ children, loading, onClick, disabled, icon, ...props }: PropsWithChildren<Props>): JSX.Element => {
return (
<StyledGhostButton onClick={onClick} disabled={loading || disabled} {...props}>
{icon ? <FontAwesomeIcon icon={icon} style={{ marginRight: 8 }} fixedWidth /> : null}
<Label>
{children}
</Label>
{loading ? (<><Margin /><Ball size={10} /></>) : null}
</StyledGhostButton>
);
};
export default Button;
================================================
FILE: src/app/components/Code.tsx
================================================
import styled, { css } from 'styled-components';
const Code = styled.div<{ removed?: boolean; added?: boolean; updated?: boolean }>`
font-family: var(--font-mono);
background-color: #f8f8f8;
padding: 16px 24px;
max-width: 100%;
line-height: 2;
white-space: pre-wrap;
user-select: text;
display: flex;
flex-direction: column;
word-break: break-all;
font-size: 12px;
margin: 4px 0;
border-radius: 8px;
h5 {
margin-top: 0;
opacity: 1;
font-weight: 600;
}
&.icon {
height: 1em;
}
${(props) => props.added && css`
background-color: var(--color-green-100);
color: var(--color-green-500);
`}
${(props) => props.removed && css`
background-color: var(--color-red-100);
color: var(--color-red-500);
`}
${(props) => props.updated && css`
background-color: var(--color-yellow-100);
color: var(--color-yellow-500);
`}
`;
export default Code;
================================================
FILE: src/app/components/IconBadge.tsx
================================================
import React, { PropsWithChildren } from 'react';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import styled from 'styled-components';
import { IconDefinition } from '@fortawesome/fontawesome-svg-core';
import { H2 } from './Typography';
import { PullContainer } from './Utility';
const Container = styled.div`
background-color: var(--color-blue-200);
color: var(--color-blue-700);
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: 8px;
font-size: 2em;
width: 2em;
height: 2em;
flex: 0 0 auto;
margin-right: 0.5em;
`;
interface IconBadgeProps {
icon: IconDefinition;
}
function IconBadge({ icon }: IconBadgeProps) {
return (
<Container>
<FontAwesomeIcon
icon={icon}
fixedWidth
/>
</Container>
);
}
export function IconBadgeWithTitle({ children, icon }: PropsWithChildren<IconBadgeProps>) {
return (
<PullContainer verticalAlign>
<IconBadge icon={icon} />
<H2 lines={1}>{children}</H2>
</PullContainer>
);
}
export default IconBadge;
================================================
FILE: src/app/components/Input.tsx
================================================
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faChevronDown } from '@fortawesome/free-solid-svg-icons';
import React, { useCallback } from 'react';
import styled, { css } from 'styled-components';
export const TextInput = styled.input`
padding: 16px;
border-radius: 4px;
font-size: 16px;
margin-bottom: 16px;
background-color: var(--color-gray-50);
border: 1px solid var(--color-border);
color: var(--color-gray-800);
&::placeholder {
color: var(--color-gray-500);
}
`;
export const Label = styled.label`
font-size: 0.8em;
display: flex;
flex-direction: column;
font-family: var(--font-mono);
color: var(--color-gray-600);
& > span {
margin-left: 4px;
margin-bottom: 2px;
}
`;
const Select = styled.select<{ hasPlaceholder?: boolean }>`
height: 50px;
padding: 16px;
border-radius: 4px;
width: 100%;
margin-bottom: 16px;
background-color: var(--color-gray-50);
border: 1px solid var(--color-border);
color: inherit;
appearance: none;
${(props) => props.hasPlaceholder && css`
color: #00000066;
`};
&:disabled {
background-color: var(--color-gray-300);
color: var(--color-gray-700);
cursor: not-allowed;
}
`;
const SelectContainer = styled.div`
position: relative;
svg {
position: absolute;
right: 16px;
top: calc(50% - 8px);
transform: translateY(-50%);
}
`;
interface DropdownProps {
label: string;
options: string[] | Record<string, JSX.Element>;
value: string;
onSelect: (selectedValue: string) => void;
disabled?: boolean;
placeholder?: string;
}
type Option = { key: string, value: unknown };
export function Dropdown(props: DropdownProps): JSX.Element {
const { label, options, value, disabled, placeholder, onSelect } = props;
const availableOptions: Option[] = Array.isArray(options)
? options.map<Option>((o) => ({ key: o, value: o }))
: Object.keys(options).map((key) => ({ key, value: options[key] }));
const handleChange = useCallback((event) => {
onSelect(event.target.value);
}, [onSelect]);
return (
<Label>
<span>{label}</span>
<SelectContainer>
<Select
value={value}
disabled={disabled}
onChange={handleChange}
placeholder="Please select an account"
// hasPlaceholder={value === ''}
>
<option key="" disabled={value !== ''}>{placeholder || 'Please select an option'}</option>
{availableOptions.map((option) =>
<option key={option.key}>{option.value}</option>,
)}
</Select>
<FontAwesomeIcon icon={faChevronDown} />
</SelectContainer>
</Label>
);
}
================================================
FILE: src/app/components/Loading.tsx
================================================
import React from 'react';
import styled from 'styled-components';
const LoadingContainer = styled.div`
display: flex;
width: 100%;
height: 100%;
justify-content: center;
align-items: center;
`;
interface Props {
size?: number;
color?: string;
}
export const Ball = styled.div<Props>`
height: ${(props: Props): number => props.size || 20}px;
width: ${(props: Props): number => props.size || 20}px;
border-radius: 50px;
background-color: ${(props: Props): string => props.color || 'var(--color-primary)'};
animation: bounce 0.5s alternate infinite cubic-bezier(.5, 0.05, 1, .5),
fade-in 1s;
margin-top: -${(props: Props): number => props.size ? props.size * 2.5 : 40}px;
@keyframes bounce {
from {
transform: translate3d(0, 0px, 0);
}
to {
transform: translate3d(0, ${(props: Props): number => props.size ? props.size * 2 : 40}px, 0);
}
}
@keyframes fade-in {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
`;
const Loading = (): JSX.Element => (
<LoadingContainer>
<Ball />
</LoadingContainer>
);
export default Loading;
================================================
FILE: src/app/components/Menu.tsx
================================================
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faProjectDiagram, faClock, faCog, faUser, faTable, faTrash } from '@fortawesome/free-solid-svg-icons';
import React from 'react';
import { NavLink, useLocation } from 'react-router-dom';
import styled from 'styled-components';
import { useSelector } from 'react-redux';
import { State } from 'app/store';
import { PullDown } from './Utility';
import useTour from './Tour/useTour';
export const PageContainer = styled.div`
display: grid;
grid-template-columns: 200px 1fr;
gap: 0px 0px;
grid-template-areas:
"menu content";
height: 100vh;
width: 100vw;
overflow: hidden;
`;
const TitleBarContainer = styled.div`
position: absolute;
top: 0;
left: 0;
z-index: 200;
width: 100%;
height: 50px;
-webkit-app-region: drag;
background: transparent;
transition: all 0.3s ease;
backdrop-filter: blur(25px) brightness(1.1);
text-transform: capitalize;
display: flex;
align-items: center;
justify-content: center;
font-family: var(--font-heading);
border-bottom: 1px solid var(--color-border);
span {
font-size: 14px;
font-weight: 600;
}
`;
export const ContentContainer = styled.div`
grid-area: content;
overflow: auto;
position: relative;
background-color: var(--color-background);
`;
const Container = styled.div`
grid-area: menu;
display: flex;
flex-direction: column;
padding: 5px 0;
-webkit-app-region: drag;
padding: 50px 0 10px 0;
border-right: 1px solid var(--color-gray-400);
box-shadow: inset -10px 0 8px -10px rgba(0,0,0,0.04);
`;
const Link = styled(NavLink)`
background: none;
border: 0;
font-family: var(--font-heading);
text-align: left;
height: 36px;
display: flex;
align-items: center;
padding: 0 12px;
margin: 0px 8px;
font-weight: 400;
-webkit-app-region: no-drag;
color: var(--color-heading);
border-radius: 6px;
cursor: default;
span:not(.icon) {
margin-left: 8px;
font-size: 14px;
}
span.icon {
/* font-size: 16px; */
color: var(--color-gray-700);
}
&.active {
@media (prefers-color-scheme: dark) {
background-color: #FFFFFF26;
}
@media (prefers-color-scheme: light) {
background-color: #00000018;
}
}
&:hover:not(.active) {
@media (prefers-color-scheme: dark) {
background-color: #FFFFFF12;
}
@media (prefers-color-scheme: light) {
background-color: #00000008;
}
}
`;
export function TitleBar(): JSX.Element {
const location = useLocation();
const title = location.pathname.split('/')[1];
return (
<TitleBarContainer>
<span>{title}</span>
</TitleBarContainer>
);
}
export default function Menu(): JSX.Element {
const deleted = useSelector((state: State) => state.data.deleted);
useTour(deleted.length ? '/screen/erasure' : null);
return (
<Container id="menu">
<Link to="/timeline" id="timeline" className={({ isActive }) => isActive ? 'active' : ''}>
<span className="icon"><FontAwesomeIcon icon={faClock} fixedWidth /></span>
<span>Timeline</span>
</Link>
<Link to="/accounts" id="accounts" className={({ isActive }) => isActive ? 'active' : ''}>
<span className="icon"><FontAwesomeIcon icon={faUser} fixedWidth /></span>
<span>Accounts</span>
</Link>
<Link to="/data" id="data" className={({ isActive }) => isActive ? 'active' : ''}>
<span className="icon"><FontAwesomeIcon icon={faTable} fixedWidth /></span>
<span>Data</span>
</Link>
<Link to="/graph" id="graph" className={({ isActive }) => isActive ? 'active' : ''}>
<span className="icon"><FontAwesomeIcon icon={faProjectDiagram} fixedWidth /></span>
<span>Graph</span>
</Link>
{deleted.length ? (
<>
<Link to="/erasure" id="erasure" className={({ isActive }) => isActive ? 'active' : ''} data-tour="erasure-screen">
<span className="icon"><FontAwesomeIcon icon={faTrash} fixedWidth /></span>
<span>Erasure ({deleted.length})</span>
</Link>
</>
) : null}
<PullDown>
<Link to="/settings" id="settings" className={({ isActive }) => isActive ? 'active' : ''}>
<span className="icon"><FontAwesomeIcon icon={faCog} fixedWidth /></span>
<span>Settings</span>
</Link>
</PullDown>
</Container>
);
}
================================================
FILE: src/app/components/Modal/Menu.tsx
================================================
import React, { useState } from 'react';
import styled from 'styled-components';
import { SimpleButton } from '../Button';
const MenuContainer = styled.div<{ active?: boolean; }>`
display: flex;
justify-content: flex-end;
margin-top: -25px;
border-bottom: 1px solid var(--color-border);
${SimpleButton} {
height: 40px;
margin-right: 16px;
color: var(--color-gray-700);
text-transform: capitalize;
position: relative;
&.active {
color: var(--color-primary);
font-weight: 600;
letter-spacing: -0.35px;
&::after {
content: " ";
background-color: var(--color-primary);
color: inherit;
position: absolute;
bottom: 0;
left: -4px;
right: -4px;
height: 3px;
border-top-left-radius: 4px;
border-top-right-radius: 4px;
}
}
&:hover:not(.active) {
color: var(--color-gray-800);
}
&:first-child {
margin-left: 40px;
}
}
`;
type Props = {
children: JSX.Element[] | JSX.Element;
labels?: JSX.Element[] | string[];
};
function ModalMenu({ children, labels = [] }: Props): JSX.Element {
const [selectedItem, setSelectedItem] = useState(0);
return (
<>
<MenuContainer data-tour="modal-menu-options">
{[...new Array(Array.isArray(children) ? children.length : 1)].map((a, i) =>
<SimpleButton key={i} onClick={() => setSelectedItem(i)} className={selectedItem === i ? 'active' : ''}>
{labels[i] || i}
</SimpleButton>,
)}
</MenuContainer>
<div data-tour="modal-menu-container">
{Array.isArray(children) ? children[selectedItem] : children}
</div>
</>
);
}
export default ModalMenu;
================================================
FILE: src/app/components/Modal/index.tsx
================================================
import React, { Component, useRef, useEffect, PropsWithChildren, HTMLAttributes, CSSProperties } from 'react';
import { createPortal } from 'react-dom';
import styled from 'styled-components';
import { Transition, config, animated } from 'react-spring';
import { GhostButton } from '../Button';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faArrowUp } from '@fortawesome/free-solid-svg-icons';
import { LargeShadow } from 'app/styles/snippets';
interface SpringProps {
transform?: string;
backgroundOpacity?: number;
opacity?: number;
pointerEvents?: CSSProperties['pointerEvents'];
}
type NextFunc = (props: SpringProps) => Promise<void>;
interface Props {
isOpen?: boolean;
onRequestClose?: () => void;
}
const Container = styled(animated.div)`
position: fixed;
left: 0;
top: 0;
height: 100vh;
width: 100vw;
display: flex;
align-items: flex-start;
justify-content: center;
padding: 10vh 0;
z-index: 5000;
/* backdrop-filter: blur(50px); */
@media (prefers-color-scheme: dark) {
background-color: #222222f0;
}
@media (prefers-color-scheme: light) {
background-color: #eeeeeef0;
}
`;
const StyledDialog = styled(animated.div)`
border-radius: 8px;
margin-top: 10vh;
min-width: 50vw;
min-height: 25h;
max-height: 80vh;
max-width: 700px;
padding-top: 32px;
overflow-y: auto;
background-color: var(--color-modal-background);
${LargeShadow}
`;
type DialogProps = PropsWithChildren<
HTMLAttributes<HTMLDivElement>
>;
function Dialog(props: DialogProps): JSX.Element {
const { children, ...rest } = props;
const ref = useRef<HTMLDivElement>();
useEffect(() => {
ref.current?.focus();
}, []);
return (
<StyledDialog {...rest} ref={ref} tabIndex={0}>
{children}
</StyledDialog>
);
}
const CloseButton = styled(GhostButton)`
position: fixed;
top: 0;
left: 4px;
padding: 16px;
`;
class Modal extends Component<Props> {
element = document.getElementById('modal');
handleBlur = (): void => this.props.onRequestClose();
componentDidMount(): void {
document.addEventListener('keydown', this.handleKeyDown);
}
componentWillUnmount(): void {
document.removeEventListener('keydown', this.handleKeyDown);
}
handleKeyDown = (event: KeyboardEvent): void => {
if (event.key === 'Escape') {
this.props.onRequestClose();
}
};
handleContainerClick = (event: React.MouseEvent<HTMLDivElement, MouseEvent>): void => {
// GUARD: Check if the currenttarget is the parent where the events
// orginate from.
if (event.currentTarget === event.target) {
this.props.onRequestClose();
}
};
render(): JSX.Element {
const { isOpen, onRequestClose, children } = this.props;
return createPortal((
<Transition
items={isOpen}
from={{ transform: 'translate3d(0,-40px,0)', opacity: 0, backgroundOpacity: 0, pointerEvents: 'all' }}
enter={() => async (next: NextFunc) => {
try {
next({ backgroundOpacity: 1, pointerEvents: 'all' }).catch(() => null);
await new Promise((resolve) => setTimeout(resolve, 200));
await next({ transform: 'translate3d(0,0px,0)', opacity: 1 });
} catch {
// Async animation may be interruped. We don't care
// about this, but we still need to catch an error so we
// can suppress any warnings about it to the user.
}
}}
leave={{ transform: 'translate3d(0,-40px,0)', opacity: 0, backgroundOpacity: 0, pointerEvents: 'none' }}
config={config.wobbly}
>
{({ backgroundOpacity, pointerEvents, ...props }, item) => {
return item && (
<Container style={{ opacity: backgroundOpacity, pointerEvents }} onClick={this.handleContainerClick}>
<Dialog
style={props}
// onBlur={this.handleBlur}
>
{children}
<CloseButton onClick={onRequestClose} data-telemetry-id="modal-close">
<FontAwesomeIcon icon={faArrowUp} fixedWidth />
</CloseButton>
</Dialog>
</Container>
);
}}
</Transition>
), this.element);
}
}
export default Modal;
================================================
FILE: src/app/components/NoData.tsx
================================================
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faPlus } from '@fortawesome/free-solid-svg-icons';
import { LinkButton } from 'app/components/Button';
import { MarginRight } from 'app/components/Utility';
import React from 'react';
import styled from 'styled-components';
import Graphic from '../assets/undraw_empty_xct9.svg';
import { H2, H3 } from './Typography';
const Container = styled.div`
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
height: 100%;
text-align: center;
img {
width: 400px;
max-width: 75vw;
margin-bottom: 48px;
}
`;
function NoData(): JSX.Element {
return (
<Container>
<img src={Graphic} alt="Man with an empty box" />
<H2>Nothing here...</H2>
<br />
<H3>You haven't requested any data yet.<br /> Start your first data request by heading over to Accounts.</H3>
<br />
<LinkButton to="/accounts?create-new-account">
<MarginRight><FontAwesomeIcon icon={faPlus} /></MarginRight>
Add your first account
</LinkButton>
</Container>
);
}
export default NoData;
================================================
FILE: src/app/components/PanelGrid.tsx
================================================
import React, { PropsWithChildren } from 'react';
import { IconProp } from '@fortawesome/fontawesome-svg-core';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { NavLink, NavLinkProps } from 'react-router-dom';
import styled, { css } from 'styled-components';
import { faChevronRight } from '@fortawesome/free-solid-svg-icons';
import { PullRight } from './Utility';
import { Section } from './RightSideOverlay';
export const ListItem = styled.div`
padding: 8px 32px;
flex-grow: 0;
flex-shrink: 0;
`;
export const RowHeading = styled(ListItem)`
font-weight: 400;
position: sticky;
top: 0;
align-self: flex-start;
z-index: 2;
font-size: 14px;
width: 100%;
border-bottom: 1px solid var(--color-border);
padding: 16px 32px;
font-family: var(--font-heading);
font-weight: 600;
`;
export const RowDescription = styled(RowHeading)`
font-weight: 400;
font-size: 11px;
margin-bottom: 8px;
`;
export const SubHeading = styled(RowHeading)`
font-size: 10px;
font-family: 'IBM Plex Mono';
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
background-color: var(--color-background);
color: var(--color-gray-700);
border: 0;
padding: 16px 32px 8px 16px;
`;
export const PanelGrid = styled.div<{ columns?: number; noTopPadding?: boolean; }>`
display: grid;
grid-auto-columns: auto;
grid-template-columns: repeat(${(props) => props.columns || 3}, 1fr);
padding-top: ${(props) => props.noTopPadding ? 0 : 50}px;
height: 100%;
position: relative;
overflow: hidden;
${(props) => (!props.columns || props.columns === 3) && css`
& ${Section} {
margin: 24px;
}
`}
`;
export const List = styled.div<{ topMargin?: boolean }>`
display: flex;
flex-direction: column;
flex-grow: 1;
overflow-y: auto;
position: relative;
border-right: 1px solid var(--color-border);
${(props) => props.topMargin && css`
margin-top: 0px;
`}
`;
export const SplitPanel = styled.div`
display: flex;
flex-direction: column;
`;
export const PanelBottomButtons = styled.div`
margin-top: auto;
padding: 16px;
border-top: 1px solid var(--color-border);
border-right: 1px solid var(--color-border);
`;
const IconWrapper = styled.div`
margin-right: 8px;
font-size: 1.25em;
`;
const ChevronWrapper = styled(PullRight)`
opacity: 0.25;
font-size: 0.7em;
`;
type ListButtonProps = {
deleted?: boolean;
modified?: boolean;
added?: boolean;
large?: boolean;
} & NavLinkProps & React.RefAttributes<HTMLAnchorElement>;
export const NavigatableListEntryContainer = styled<React.ForwardRefExoticComponent<ListButtonProps>>(NavLink).withConfig({
shouldForwardProp: (prop) => typeof prop === 'string' && !['deleted', 'modified', 'added', 'large'].includes(prop),
})`
border: 0;
background: transparent;
display: flex;
align-items: center;
font-size: 14px;
margin: 1px 8px;
padding: 8px 12px;
font-weight: 400;
border-radius: 8px;
overflow: hidden;
white-space: nowrap;
color: var(--color-heading);
font-family: var(--font-heading);
img {
max-height: 100px;
width: auto;
border-radius: 5px;
}
svg {
flex: 0 0 auto;
color: var(--color-gray-700);
}
&.active {
background: var(--color-blue-500);
color: var(--color-white);
svg {
flex: 0 0 auto;
color: var(--color-white);
}
}
&:hover:not(.active) {
background: var(--color-blue-50);
}
&:active {
background: var(--color-blue-100);
}
${(props) => props.added && css`
background-color: var(--color-green-100);
&.active {
background-color: var(--color-green-500);
}
`}
${(props) => props.deleted && css`
background-color: var(--color-red-100);
&.active {
background-color: var(--color-red-500);
}
`}
${(props) => props.modified && css`
background-color: var(--color-yellow-100);
&.active {
background-color: var(--color-yellow-500);
}
`}
${(props) => props.large && css`
font-size: 16px;
font-weight: 500;
`}
&:disabled {
opacity: 0.25;
}
${PullRight} {
padding-left: 0.5em;
}
`;
const CategoryContainer = styled.div`
margin-bottom: 16px;
`;
export function Category({ title, children, id }: PropsWithChildren<{ title?: string, id?: string }>) {
return (
<>
{title && <SubHeading>{title}</SubHeading>}
{children && (
<CategoryContainer id={id}>
{children}
</CategoryContainer>
)}
</>
);
}
type NavigatableListEntryProps = PropsWithChildren<{
to: string,
icon?: IconProp,
} & ListButtonProps>;
export function NavigatableListEntry({
icon,
children,
...props
}: NavigatableListEntryProps): JSX.Element {
return (
<NavigatableListEntryContainer {...props}>
{icon ?
<IconWrapper>
<FontAwesomeIcon fixedWidth icon={icon} />
</IconWrapper>
: null}
{children}
<ChevronWrapper>
<FontAwesomeIcon fixedWidth icon={faChevronRight} />
</ChevronWrapper>
</NavigatableListEntryContainer>
);
}
================================================
FILE: src/app/components/RightSideOverlay.tsx
================================================
import React, { PropsWithChildren } from 'react';
import { GhostButton } from 'app/components/Button';
import styled, { css } from 'styled-components';
import { animated, useTransition } from 'react-spring';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faArrowLeft } from '@fortawesome/free-solid-svg-icons';
import { Shadow } from 'app/styles/snippets';
import usePrevious from 'app/utilities/usePrevious';
export type RightSideOverlayProps = PropsWithChildren<{
onClose?: () => void;
marginTop?: number;
overlay?: boolean;
}>;
const Container = styled.div`
pointer-events: all;
padding-bottom: 1em;
code {
margin-bottom: 0;
}
`;
export const CloseButton = styled(GhostButton)`
margin-left: 8px;
color: var(--color-gray-400);
&:hover {
color: var(--color-gray-800);
}
`;
export const Section = styled.div<{ well?: boolean }>`
margin: 24px 36px;
p:first-child {
margin-top: 0;
}
p:last-child {
margin-bottom: 0;
}
img {
max-width: 100%;
height: auto;
border-radius: 5px;
}
${(props) => props.well && css`
background-color: var(--color-gray-100);
padding: 16px 24px;
border-radius: 8px;
margin: 8px 16px;
overflow: hidden;
font-family: var(--font-heading);
`}
`;
export const DetailListItem = styled.div`
display: flex;
&:not(:last-child) {
margin-bottom: 4px;
}
& > * {
flex: 0 1 auto;
color: var(--color-gray-800);
overflow: hidden;
white-space: nowrap;
}
& > *:first-child {
margin-right: 12px;
flex: 0 0 auto;
}
`;
export const RightSideOverlayOffset = styled.div`
margin-top: 50px;
position: relative;
height: 100%;
`;
export const AbsolutePosition = styled(animated.div)`
position: absolute;
right: 8px;
top: 58px;
background-color: var(--color-background);
border-radius: 8px;
width: 33%;
${Shadow};
`;
const RightSideOverlay = (props: RightSideOverlayProps): JSX.Element => {
const {
onClose: handleClose,
children,
marginTop,
overlay,
...otherProps
} = props;
const previousChildren = usePrevious(children);
const transitions = useTransition(children, {
from: { opacity: 0 },
enter: { opacity: 1 },
leave: { opacity: 0 },
config: {
tension: 300,
friction: 20,
},
immediate: !!children && !!previousChildren,
});
// Optionally wrap the overlay in an absolute position
const Wrapper = overlay ? AbsolutePosition : animated.div;
return transitions(({ opacity }, items) =>
items ? (
<Wrapper
style={{
opacity: opacity.to({ range: [0.0, 1.0], output: [0, 1] }),
transform: opacity.to((x: number) => `translateX(${-x * 20 + 20}%)`),
marginTop,
}}
{...otherProps}
>
<Container>
{handleClose ?
<CloseButton onClick={handleClose}>
<FontAwesomeIcon icon={faArrowLeft} />
</CloseButton>
: null}
{items || ''}
</Container>
</Wrapper>
) : null,
);
};
export default RightSideOverlay;
================================================
FILE: src/app/components/Timestamp.tsx
================================================
import { formatDistanceToNow } from 'date-fns';
import React, { useMemo } from 'react';
import Tooltip from './Tooltip';
interface TimestampProps {
children?: Date | string | number;
}
function Timestamp({ children: date }: TimestampProps) {
const parsedDate = useMemo(() => (
typeof date === 'string'
? new Date(date)
: date
), []);
const relativeTime = useMemo(() => (
parsedDate ? formatDistanceToNow(parsedDate) : null
), [parsedDate]);
return (
<Tooltip title={parsedDate?.toLocaleString()}>
<time dateTime={parsedDate instanceof Date ? parsedDate?.toISOString() : parsedDate?.toString()}>
{relativeTime ? `${relativeTime} ago` : 'Never'}
</time>
</Tooltip>
);
}
export default Timestamp;
================================================
FILE: src/app/components/Tooltip.tsx
================================================
import { Placement } from '@popperjs/core';
import { Shadow } from 'app/styles/snippets';
import React, { PropsWithChildren, ReactNode, useCallback, useMemo, useState } from 'react';
import { usePopper } from 'react-popper';
import { animated, useTransition } from 'react-spring';
import styled from 'styled-components';
const StyledTooltip = styled(animated.div)`
background-color: var(--color-background);
border-radius: 4px;
padding: 4px 8px;
${Shadow}
`;
function getTransitionConfig(placement: Placement) {
switch (placement) {
case 'left':
case 'left-end':
case 'left-start':
return {
from: { opacity: 0, pos: [-25, 0] },
enter: { opacity: 1, pos: [0, 0] },
leave: { opacity: 0, pos: [-25, 0] },
};
case 'right':
case 'right-end':
case 'right-start':
return {
from: { opacity: 0, pos: [25, 0] },
enter: { opacity: 1, pos: [0, 0] },
leave: { opacity: 0, pos: [25, 0] },
};
case 'bottom':
case 'bottom-end':
case 'bottom-start':
default:
return {
from: { opacity: 0, pos: [0, -25] },
enter: { opacity: 1, pos: [0, 0] },
leave: { opacity: 0, pos: [0, -25] },
};
case 'top':
case 'top-end':
case 'top-start':
return {
from: { opacity: 0, pos: [0, 25] },
enter: { opacity: 1, pos: [0, 0] },
leave: { opacity: 0, pos: [0, 25] },
};
}
}
type TooltipProps = PropsWithChildren<{
title: ReactNode;
placement?: Placement;
}>;
/**
* This component will render a tooltip (passed as `title`) when hovering over a
* component (passed as `children`). The implementation is deliberately simple.
*/
function Tooltip({ title, children, placement = 'auto' }: TooltipProps) {
// Track whether the child components are being hovered
const [isHovered, setHovered] = useState(false);
// Set refs for Popper
const [referenceElement, setReferenceElement] = useState(null);
const [popperElement, setPopperElement] = useState(null);
// Create config for popper
const { styles, attributes, state } = usePopper(referenceElement, popperElement, {
placement,
modifiers: [
{ name: 'offset', options: { offset: [0, 8] } },
{ name: 'hide' },
],
});
// Define callback handlers for hover events
const handleMouseOver = useCallback(() => setHovered(true), [setHovered]);
const handleMouseOut = useCallback(() => setHovered(false), [setHovered]);
// Create config for react-spring
const config = useMemo(() => getTransitionConfig(state?.placement), [state?.placement]);
const transitions = useTransition(isHovered, {
...config,
config: {
tension: 300,
friction: 20,
},
});
return (
<>
<span
ref={setReferenceElement}
onMouseOver={handleMouseOver}
onMouseOut={handleMouseOut}
onFocus={handleMouseOver}
onBlur={handleMouseOut}
>
{children}
</span>
{transitions(({ opacity, pos }, item) => (
item && (
<div ref={setPopperElement} style={styles.popper} {...attributes.popper}>
<StyledTooltip style={{
opacity,
transform: pos.to((x, y) => `translate3d(${x}%,${y}%,0)`),
}}>
{title}
</StyledTooltip>
</div>
)
))}
</>
);
}
export default Tooltip;
================================================
FILE: src/app/components/Tour/index.tsx
================================================
import React, { PropsWithChildren } from 'react';
import { TourProvider } from '@reactour/tour';
import steps, { TourKeys } from './steps';
import { faCheck } from '@fortawesome/free-solid-svg-icons';
import Button from '../Button';
function Tour({ children }: PropsWithChildren<unknown>): JSX.Element {
return (
<TourProvider
steps={Object.keys(steps).flatMap((key) => steps[key as TourKeys])}
nextButton={({
Button: BaseButton,
currentStep,
stepsLength,
setIsOpen,
setCurrentStep,
}) => {
const last = currentStep === stepsLength - 1;
return (
<BaseButton
hideArrow={last}
onClick={() => {
if (last) {
setIsOpen(false);
} else {
setCurrentStep((s) => (s === Object.keys(steps).length - 1 ? 0 : s + 1));
}
}}
>
{last ? <Button icon={faCheck}>Done</Button> : null}
</BaseButton>
);
}}
styles={{
maskWrapper: (base) => ({ ...base, color: 'var(--color-gray-400)', opacity: 0.9 }),
popover: (base) => ({
...base,
borderRadius: 8,
'--reactour-accent': 'var(--color-primary)',
}),
}}
>
{children}
</TourProvider>
);
}
export default Tour;
================================================
FILE: src/app/components/Tour/steps.tsx
================================================
import GraphExplainer from 'app/screens/Graph/explainer';
import React, { useEffect } from 'react';
import { StepType } from '@reactour/tour';
import { demoMode } from 'app/utilities/env';
/**
* Clicks an element that is targeted by the supplied selector on mount. This is
* especially helpful if you need to guide a user through multiple screens.
*/
const ClickOnMount = ({ selector }: { selector: string }): JSX.Element => {
useEffect(() => {
const el = document.querySelector(selector);
(el as HTMLElement)?.click();
}, [selector]);
return null;
};
export type TourKeys = '/screen/timeline'
| '/screen/graph'
| '/screen/data'
| '/screen/settings'
| '/screen/accounts/has-accounts'
| '/screen/accounts/no-accounts'
| '/screen/accounts/new-account'
| '/screen/erasure';
const steps: Record<TourKeys, StepType[]> = {
'/screen/timeline': [
{
selector: null,
content: 'This is the timeline! The timeline is where you get a chronological overview of all of your data. Every time Aeon retrieves data from the internet, a new entry is added to this list. Use this screen to get a grip on how your online identity has evolved over time.',
},
{
selector: '[data-tour="timeline-commits-list"]',
content: 'This is the list of recent changes to data belonging to you. Every time Aeon finds new data, or data is removed, this list will show it.',
},
{
selector: '[data-tour="timeline-first-commit"]',
content: 'This is the most recent change to your identity. By clicking it, you can select it.',
},
{
selector: '[data-tour="timeline-diff-container"]',
content: 'Here, we go slightly more in-depth what the last change is actually about.',
},
{
selector: '[data-tour="timeline-diff-info"]',
content: 'This contains information about the circumstances in which the change was made. It contains when the change was made, which platform the change is from and which account was involved with the change.',
},
{
selector: '[data-tour="timeline-diff-data"]',
content: 'Here, all data points that were added or removed are gathered.',
},
{
selector: null,
content: 'If you find this view a little overwhelming, go over to the Graph screen. There, you\'ll find a slightly more convenient overview.',
},
],
'/screen/accounts/no-accounts': [
{
selector: null,
content: 'The accounts screen is all about the accounts you use in your daily life. By adding them, Aeon can automatically gather data from them.',
},
{
selector: '[data-tour="accounts-create-account"]',
content: 'Click this button to get started with adding your first account!',
},
],
'/screen/accounts/new-account': [
{
selector: null,
content: 'This pop-up will help you create a new account.',
},
{
selector: '[data-tour="modal-menu-options"]',
content: (
<>
<p>These are all currently supported options for gathering your data. You'll recognise a number of popular social platforms.</p>
<p>If you miss any particular platform, please consider contributing these yourself.</p>
</>
),
},
{
selector: '[data-tour="accounts-create-account-open-data-rights"]',
content: (
<>
<p>This one is a special one. Rather than being tied to a particular website, it allows data requests from any website that supports the Open Data Rights API.</p>
<p>Check if the organisation you want to retrieve data from supports it. If not, petition them to start doing so. Implementing it is quite straightforward!</p>
</>
),
},
],
'/screen/accounts/has-accounts': [
{
selector: null,
content: 'Now that you\'ve added your first account, you can get started on issuing data requests for this account.',
},
{
selector: '[data-tour="accounts-first-account"]',
content: (
<>
<p>This is the account you just created. By clicking on it, you get further information on what you can use it for.</p>
<ClickOnMount selector='a[data-tour="accounts-first-account"]' />
</>
),
},
{
selector: '[data-tour="accounts-account-overlay"]',
content: 'This details all the information for this account, such as when data was requested.',
},
{
selector: '[data-tour="accounts-start-data-request"]',
content: 'While Aeon will automatically gather data from some platforms, you will need to explicitly request it for all. If you\'re ready to get started, feel free to click it!',
},
],
'/screen/data': [
{
selector: null,
content: 'This screen gives you a bit more detailed insight into all of the data that is currently active on the internet.',
},
{
selector: '[data-tour="data-categories-list"]',
content: <>
<p>This list contains every type of data that Aeon is capable of processing. When you click on them, you get a list of all data points of this type that are present.</p>
<ClickOnMount selector='[data-tour="data-category-button"]:not([disabled])' />
</>,
},
{
selector: '[data-tour="data-data-points-list"]',
content: <>
<p>The second column contains all of the data points that exist in this particular category. When you click on a single data point, you can get a closer look.</p>
<ClickOnMount selector='[data-tour="data-data-point-button"]:not([disabled])' />
</>,
},
{
selector: '[data-tour="data-datum-overlay"]',
content: 'The right column shows where the data point has come from, why it\'s there and more.',
},
{
selector: '[data-tour="data-delete-datum-button"]',
content: 'If you don\'t feel whoever has created the data point should be holding on the data, you can choose to remove it. When you do so, Aeon will send out an email to ther organisation with a request to remove the data points.',
},
],
'/screen/graph': [
{
selector: null,
content: 'This screen contains a visualisation of all of your data points. It should help you get an insight.',
},
{
selector: '[data-tour="graph-container"]',
content: <>
<p>In this visualisation, the following elements are used:</p>
<GraphExplainer />
</>,
},
{
selector: '[data-tour="graph-container"]',
content: 'Tip: move your mouse over the various elements, and they will highlight connections!',
},
],
'/screen/settings': [],
'/screen/erasure': [
{
content: 'You\'ve just tentatively deleted a data point. When you\'re done selecting data points, head over to the Erasure screen to create a erasure request.',
selector: '[data-tour="erasure-screen"]',
},
],
};
// Tentatively add a tour step for creating email-based data requests
if (demoMode) {
steps['/screen/accounts/new-account'].push({
selector: '[data-tour="accounts-create-account-email"]',
content: 'Or, if the organisation you want to get data from an organisation that does not support any of the other methods, you can just send a plain old email! Just link an email account to Aeon and select the organisation you want to get your data from.',
});
}
export default steps;
================================================
FILE: src/app/components/Tour/useTour.tsx
================================================
import { State, useAppDispatch } from 'app/store';
import { completeTour } from 'app/store/onboarding/actions';
import { useEffect } from 'react';
import { useSelector } from 'react-redux';
import steps, { TourKeys } from './steps';
import { useTour as useBaseTour } from '@reactour/tour';
import { tour } from 'app/utilities/env';
function useTour(screen: TourKeys) {
const dispatch = useAppDispatch();
const isTourComplete = useSelector((state: State) => state.onboarding.tour.includes(screen));
const { setIsOpen, setSteps, setCurrentStep } = useBaseTour();
useEffect(() => {
// GUARD: Check if tours are enabled application-wide
if (!tour) {
return;
}
if (!isTourComplete) {
setSteps(steps[screen]);
setCurrentStep(0);
setIsOpen(true);
dispatch(completeTour(screen));
}
}, [setIsOpen, setSteps, isTourComplete, screen]);
}
export function Tour({ screen }: { screen: TourKeys }): null {
useTour(screen);
return null;
}
export default useTour;
================================================
FILE: src/app/components/Typography.tsx
================================================
import styled, { css } from 'styled-components';
export interface BaseTextProps {
lines?: number;
}
export const BaseText = styled.span.attrs((props) => ({
title: typeof props.children === 'string' ? props.children : undefined,
}))<BaseTextProps>`
${(props) => props.lines && css`
-webkit-line-clamp: ${props.lines};
overflow: hidden;
white-space: nowrap;
`}
`;
export const H1 = styled(BaseText).attrs({ as: 'h1' })`
font-weight: 600;
color: var(--color-heading);
`;
export const H2 = styled(BaseText).attrs({ as: 'h2' })`
font-weight: 600;
font-size: 24px;
margin: 8px 0;
line-height: 1.3;
font-family: var(--font-heading);
color: var(--color-heading);
`;
export const H3 = styled(BaseText).attrs({ as: 'h3' })`
font-weight: 400;
font-size: 16px;
margin: 8px 0;
color: var(--color-heading);
`;
export const H5 = styled(BaseText).attrs({ as: 'h5' })`
opacity: 0.5;
font-size: 12px;
text-transform: uppercase;
font-weight: 400;
margin: 8px 0;
`;
export const Badge = styled(BaseText).attrs({ as: 'div' })`
background-color: var(--color-blue-100);
color: var(--color-blue-500);
font-weight: 500;
font-family: var(--font-heading);
letter-spacing: 0.2px;
padding: 4px 8px;
display: inline-block;
border-radius: 4px;
text-transform: uppercase;
font-size: 12px;
flex-shrink: 0;
white-space: nowrap;
`;
export const FontLarge = styled(BaseText).attrs({ as: 'span' })`
font-size: 14px;
`;
================================================
FILE: src/app/components/Utility.tsx
================================================
import styled, { css } from 'styled-components';
export const Margin = styled.div`
padding: 32px;
`;
export const MarginSmall = styled.div`
padding: 16px 32px;
`;
interface PullContainerProps {
vertical?: boolean;
verticalAlign?: boolean;
center?: boolean;
gap?: boolean;
}
export const PullContainer = styled.div<PullContainerProps>`
display: flex;
${(props) => props.vertical && css`
flex-direction: column;
`}
${(props) => props.verticalAlign && css`
align-items: center;
`}
${(props) => props.center && css`
justify-content: center;
`}
${(props) => props.gap && css`
gap: 8px;
`}
`;
export const PullLeft = styled.div`
margin-right: auto;
`;
export const PullRight = styled.div`
margin-left: auto;
`;
export const PullDown = styled.div`
margin-top: auto;
`;
export const PullCenter = styled.div`
display: flex;
justify-content: center;
`;
export const EmptyIcon = styled.span`
display: inline-block;
width: 1.25em;
`;
export const MarginLeft = styled.span`
margin-left: 1em;
`;
export const MarginRight = styled.span`
margin-right: 1em;
`;
export const Ellipsis = styled.div.attrs((props) => ({
title: props.children,
}))`
white-space: nowrap;
overflow: hidden;
margin-right: 5px;
`;
================================================
FILE: src/app/index.ejs
================================================
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Aeon</title>
<style type="text/css">
#loader {
display: flex;
width: 100vw;
height: 100vh;
justify-content: center;
align-items: center;
color: #0000FF;
user-select: none;
animation: bounce 0.5s;
animation-direction: alternate;
animation-timing-function: cubic-bezier(.5, 0.05, 1, .5);
animation-iteration-count: infinite;
margin-top: -50px;
}
@keyframes bounce {
from {
transform: translate3d(0, 0px, 0);
}
to {
transform: translate3d(0, 50px, 0);
}
}
</style>
</head>
<body>
<div id="root"></div>
<div id="modal"></div>
<div id="loader" >
<svg viewBox="0 0 50 50" width="25" height="25">
<circle cx="25" cy="25" r="25" fill="currentColor" />
</svg>
</div>
</body>
</html>
================================================
FILE: src/app/index.tsx
================================================
import './polyfill';
import React from 'react';
import { render } from 'react-dom';
import App from './components/App';
// Activate the sourcemaps
window.api.sourceMapSupport.install();
// Log unhandlred rejections as warnings to the console
// NOTE: This mainly serves to catch async react-spring errors
// that are thrown when an animation is cancelled early.
window.addEventListener('unhandledrejection', (err) => {
console.warn(err.reason);
err.preventDefault();
});
// Initialise React
render(<App />, document.getElementById('root'));
================================================
FILE: src/app/polyfill.ts
================================================
/* eslint-disable */
// @ts-ignore
global = globalThis;
================================================
FILE: src/app/preload.ts
================================================
import {
contextBridge,
ipcRenderer,
shell,
} from 'electron';
import ElectronStore from 'electron-store';
import type { CommandLineArguments } from 'main/lib/constants';
// eslint-disable-next-line
import sourceMapSupport from 'source-map-support';
import type { State } from './store';
const store = new ElectronStore();
interface WindowApi {
send: typeof ipcRenderer.send;
invoke: typeof ipcRenderer.invoke;
on: typeof ipcRenderer.on;
removeListener: typeof ipcRenderer.removeListener;
sourceMapSupport: typeof sourceMapSupport;
store: {
persist: (store: unknown) => void;
retrieve: () => State;
clear: () => void;
},
openEmailClient: (email: string, subject: string, body: string) => Promise<void>;
env: CommandLineArguments;
}
declare global {
interface Window {
api: WindowApi
}
}
const channelWhitelist = ['repository', 'providers', 'notifications', 'email' ];
const windowApi: WindowApi = {
send: (channel, ...args) => {
// whitelist channels
if (channelWhitelist.includes(channel)) {
ipcRenderer.send(channel, ...args);
}
},
invoke: (channel, ...args) => {
if (channelWhitelist.includes(channel)) {
return ipcRenderer.invoke(channel, ...args);
}
},
on: (channel, listener) => {
if (channelWhitelist.includes(channel)) {
return ipcRenderer.on(channel, listener);
}
},
removeListener: (channel, listener) => {
if (channelWhitelist.includes(channel)) {
return ipcRenderer.removeListener(channel, listener);
}
},
store: {
persist: (state: State) => {
return store.set('app_store', state);
},
retrieve: () => {
return store.get('app_store') as State;
},
clear: () => {
return store.clear();
},
},
sourceMapSupport: sourceMapSupport,
openEmailClient: (email, subject, body) => shell.openExternal(
`mailto:${encodeURIComponent(email)}?subject=${encodeURIComponent(subject)}&body=${encodeURIComponent(body)}`,
),
env: ipcRenderer.sendSync('env') as CommandLineArguments,
};
// Expose protected methods that allow the renderer process to use
// the ipcRenderer without exposing the entire object
contextBridge.exposeInMainWorld('api', windowApi);
================================================
FILE: src/app/screens/Accounts/components/AccountOverlay.tsx
================================================
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faBell, faCheck, faClock, faLink, faPlus, faQuestion, faUpload } from '@fortawesome/free-solid-svg-icons';
import faOpenDataRights from 'app/assets/open-data-rights';
import Button, { GhostButton } from 'app/components/Button';
import RightSideOverlay, { DetailListItem, Section } from 'app/components/RightSideOverlay';
import { FontLarge } from 'app/components/Typography';
import { State, useAppDispatch } from 'app/store';
import { dispatchEmailRequest } from 'app/store/accounts/actions';
import { EmailProvider } from 'app/store/accounts/types';
import Providers from 'app/utilities/Providers';
import { InitialisedAccount } from 'main/providers/types';
import React, { useCallback, useState } from 'react';
import { useSelector } from 'react-redux';
import { useNavigate } from 'react-router-dom';
import { IconBadgeWithTitle } from 'app/components/IconBadge';
import Timestamp from 'app/components/Timestamp';
interface Props {
selectedAccount: string;
}
function hasUrl(account: InitialisedAccount | EmailProvider): account is InitialisedAccount {
return 'url' in (account as InitialisedAccount);
}
function AccountOverlay({ selectedAccount }: Props): JSX.Element {
const dispatch = useAppDispatch();
const navigate = useNavigate();
const account = useSelector((state: State) => {
if (!selectedAccount) {
return;
}
return selectedAccount.startsWith('email_')
? state.accounts.emailProviders.byKey[selectedAccount.split('email_')[1]]
: state.accounts.byKey[selectedAccount];
});
const [isLoading, setLoading] = useState(false);
// Handle a request for a new request
const handleNewRequest = useCallback(async () => {
// If the provider is of email type, we just dispatch directly to
// the store.
if (account.provider === 'email') {
// Add some arbitrary delay
setLoading(true);
await new Promise((resolve) => setTimeout(resolve, 1500));
dispatch(dispatchEmailRequest(selectedAccount.split('email_')[1]));
setLoading(false);
} else {
// If not, we dispatch to the backend and have the back-end deal
// with it
setLoading(true);
await Providers.dispatchDataRequest(selectedAccount).catch(() => null);
setLoading(false);
}
}, [selectedAccount, dispatch, setLoading]);
const handleClose = useCallback(() => {
navigate('/accounts');
}, [navigate]);
// GUARD: If there is no account data available (sometimes the data hasn't
// yet been retrieved from the back-end), don't render anything.
if (!account) {
return null;
}
return (
<RightSideOverlay data-tour="accounts-account-overlay" onClose={handleClose}>
{selectedAccount && (
<>
<Section>
<IconBadgeWithTitle icon={Providers.getIcon(account.provider)}>
{account.account}
</IconBadgeWithTitle>
</Section>
<Section well>
<FontLarge>
{hasUrl(account) &&
<>
<DetailListItem>
<span>
<FontAwesomeIcon
icon={faOpenDataRights}
fixedWidth
/>
</span>
<span>Open Data Rights API-based</span>
</DetailListItem>
<DetailListItem>
<span>
<FontAwesomeIcon
icon={faLink}
fixedWidth
/>
</span>
<span>Host: {account.url}</span>
</DetailListItem>
</>
}
<DetailListItem>
<span>
<FontAwesomeIcon
icon={faQuestion}
fixedWidth
/>
</span>
<span>Data requested: <Timestamp>{account.status?.dispatched}</Timestamp></span>
</DetailListItem>
<DetailListItem>
<span>
<FontAwesomeIcon
icon={faClock}
fixedWidth
/>
</span>
<span>
Last check: <Timestamp>{account.status.lastCheck}</Timestamp>
</span>
</DetailListItem>
{account.status?.completed &&
<DetailListItem>
<span>
<FontAwesomeIcon
icon={faCheck}
fixedWidth
/>
</span>
<span>Completed: <Timestamp>{account.status?.completed}</Timestamp></span>
</DetailListItem>
}
</FontLarge>
</Section>
{account.status?.dispatched && !account.status.completed ?
<Section>
<p>The data request you issued has not been completed yet. We'll let you know as soon as it's completed. We'll notify you if the request exceeds the legal limit of thirty days.</p>
{account.provider !== 'email'
? (
<Button
fullWidth
icon={faCheck}
onClick={handleNewRequest}
disabled
>
Complete Data Request
</Button>
) : (
<>
<Button icon={faUpload} fullWidth>
Upload archive
</Button>
<Button icon={faBell} disabled fullWidth>
Send reminder
</Button>
</>
)
}
</Section>
:
<Section data-tour="accounts-start-data-request">
<p>If you would like to retrieve your data, use the button below to start a new data request.</p>
{account.provider !== 'email'
? <p>When you click the button, a new window will appear, in which you will asked to enter your credentials. Aeon does not store any of your credentials. Rather, the window is used to perform actions on your behalf.</p>
: <p>When you click the button, we will send out an email on your behalf on the account {(account as EmailProvider).emailAccount}. You have to check if the request is completed yourself, and then upload the resulting archive here.</p>
}
<GhostButton
fullWidth
icon={faPlus}
onClick={handleNewRequest}
disabled={account.status?.dispatched && !account.status.completed}
loading={isLoading}
>
Start Data Request
</GhostButton>
</Section>
}
</>
)}
</RightSideOverlay>
);
}
export default AccountOverlay;
================================================
FILE: src/app/screens/Accounts/components/EmailProvider.tsx
================================================
import { Dropdown, Label } from 'app/components/Input';
import { Margin, MarginSmall, PullCenter } from 'app/components/Utility';
import { State, useAppDispatch } from 'app/store';
import React, { useCallback, useEffect, useState } from 'react';
import AsyncSelect, { AsyncProps } from 'react-select/async';
import { useSelector } from 'react-redux';
import { GroupBase } from 'react-select';
import styled from 'styled-components';
import Button from 'app/components/Button';
import { faEnvelope } from '@fortawesome/free-solid-svg-icons';
import { createEmailAccount } from 'app/store/accounts/actions';
import { useNavigate } from 'react-router-dom';
export interface APIResponse {
count: number;
next: string;
previous: null;
results: Result[];
}
export interface Result {
id: number;
display_name: string;
legal_name: string;
url: string;
department: string;
street_address: null | string;
city: null | string;
neighbourhood: null | string;
postal_code: null | string;
region: null | string;
country: string;
postal_address: string;
requires_identification: boolean | null;
operating_countries: string[];
custom_identifier: null | string;
identifiers: string[];
requests:Requests;
}
export interface Requests {
url?:string;
note?: string;
email: string;
access: Access;
deletion:Access;
portability: Access;
correction: Access;
}
export interface Access {
url?: string;
note?: string;
}
const Select = styled(AsyncSelect)`
& .Select__control {
border: 1px solid #eee;
padding: 6px 8px;
font-family: var(--font-body);
}
`;
function EmailProvider(): JSX.Element {
const dispatch = useAppDispatch();
const navigate = useNavigate();
const emailAccounts = useSelector((state: State) => state.email.accounts.all);
const [selectedEmail, setSelectedEmail] = useState(emailAccounts.length ? emailAccounts[0] : '');
const [selectedOrganisation, setSelectedOrganisation] = useState<Result>(undefined);
const [organisations, setOrganisations] = useState<Result[]>([]);
// A handler responding to React-Select requesting more options
const handleSearch: AsyncProps<unknown, false, GroupBase<unknown>>['loadOptions'] = useCallback(async (inputValue: string) => {
// Retrieve a set of organisations from the MyDataDone right API
const response = await fetch(`https://api.mydatadoneright.eu/api/v1/organizations.json?limit=5&q=${inputValue}`)
.then((res) => res.json()) as APIResponse;
// Translate the response to a react-select readable set of options
const options = response.results.map((organisation) => {
return {
label: organisation.display_name,
value: organisation.id,
};
});
// Feed them back to react-select
setOrganisations(response.results);
return options;
}, []);
// Also create a handler for an organisation being selected
const handleSelect = useCallback(({ value }: { value: number }) => {
const organisation = organisations.find((d) => d.id === value);
setSelectedOrganisation(organisation);
}, [setSelectedOrganisation, organisations]);
// Lastly, handle creating a email-account for the selected provider
const handleCreate = useCallback(() => {
dispatch(createEmailAccount({
account: `${selectedOrganisation.display_name}_${selectedEmail}`,
emailAccount: selectedEmail,
organisation: selectedOrganisation.display_name,
status: {},
provider: 'email',
}));
navigate('/accounts');
}, [selectedOrganisation, selectedEmail, navigate]);
// Redirect a user to the Create New Email Account modal, when they select
// the option from the email accounts dropdown
useEffect(() => {
if (selectedEmail === 'Create New Email Account...') {
navigate('/settings/email-accounts?create-new-email-account');
setSelectedEmail(emailAccounts.length ? emailAccounts[0] : '');
}
}, [selectedEmail, setSelectedEmail, navigate]);
return (
<Margin>
<p>For organisations that do not offer dedicated manners of requesting data, there is always email. You will be able to send out a generated email, after which you can upload the retrieved data yourself.</p>
<p>In order to use this provider, you must link an email address to Aeon. Selected a previously linked email address in the list below or link one first. Also select the organisation you wish to receive data from.</p>
<Dropdown
options={[...emailAccounts, 'Create New Email Account...']}
label="Email Account"
value={selectedEmail}
onSelect={setSelectedEmail}
/>
<Label>
Organisation
<Select
classNamePrefix="Select"
loadOptions={handleSearch}
onChange={handleSelect}
placeholder="Start typing to select..."
/>
</Label>
<MarginSmall>
<PullCenter>
<Button
disabled={!selectedEmail || !selectedOrganisation}
onClick={handleCreate}
icon={faEnvelope}
>
Create account for {selectedOrganisation?.display_name}
</Button>
</PullCenter>
</MarginSmall>
</Margin>
);
}
export default EmailProvider;
================================================
FILE: src/app/screens/Accounts/components/NewAccountModal.tsx
================================================
import React, { PropsWithChildren, useCallback, useEffect, useState } from 'react';
import { faPlus } from '@fortawesome/free-solid-svg-icons';
import Button from 'app/components/Button';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import Modal from 'app/components/Modal';
import ModalMenu from 'app/components/Modal/Menu';
import { PullContainer, Margin, PullCenter } from 'app/components/Utility';
import { addProviderAccount } from 'app/store/accounts/actions';
import { State, useAppDispatch } from 'app/store';
import { useSelector } from 'react-redux';
import Providers from 'app/utilities/Providers';
import { Dropdown, Label, TextInput } from 'app/components/Input';
import { InitOptionalParameters } from 'main/providers/types';
import isValidUrl from 'app/utilities/isValidUrl';
import { useNavigate, useLocation } from 'react-router-dom';
import EmailProvider from './EmailProvider';
import useTour from 'app/components/Tour/useTour';
import { demoMode } from 'app/utilities/env';
type NewAccountProps = PropsWithChildren<{
client: string,
onComplete: () => void,
disabled?: boolean;
optionalParameters: InitOptionalParameters;
selectedEmail?: string;
}>;
function NewAccountButton({ client, children, onComplete, optionalParameters, ...props }: NewAccountProps): JSX.Element {
const [isActive, setActive] = useState(false);
const dispatch = useAppDispatch();
// A handler for creating a new email account
const handleClick = useCallback(async () => {
// Set activity flag
setActive(true);
// Actually create a new account
await dispatch(addProviderAccount({ client, optionalParameters }));
// Set new activity flag, and let parent component know we're done
setActive(false);
onComplete();
}, [dispatch, client, setActive, optionalParameters]);
return (
<Button icon={faPlus} {...props} onClick={handleClick} loading={isActive}>{children}</Button>
);
}
function NewAccountModal(): JSX.Element {
useTour('/screen/accounts/new-account');
const location = useLocation();
const navigate = useNavigate();
// Selectors
const { allProviders, availableProviders } = useSelector((state: State) => state.accounts);
const emailAccounts = useSelector((state: State) => state.email.accounts.all);
// If demo mode is activated, we insert a dummy 'email' provider option,
// which theoretically can be used to automatically send emails to
// providers, but as of yet is not working. Lets call it Wizard of Oz.
const all = demoMode
? [...allProviders, 'email']
: allProviders;
// State
const [modalIsOpen, setModal] = useState(false);
const [selectedEmail, setSelectedEmail] = useState(emailAccounts.length ? emailAccounts[0] : '');
const [selectedUrl, setSelectedUrl] = useState('');
// Handlers
const closeModal = useCallback(() => {
navigate(location.pathname);
}, [location]);
const openModal = useCallback(() => {
navigate(location.pathname + '?create-new-account');
}, [location]);
const handleUrlChange = useCallback((event) => {
setSelectedUrl(event.target.value);
}, [setSelectedUrl]);
// Make whether the modal is open dependent on the query parameters
useEffect(() => {
const params = new URLSearchParams(location.search);
setModal(params.has('create-new-account'));
}, [location, setModal]);
// Redirect a user to the Create New Email Account modal, when they select
// the option from the email accounts dropdown
useEffect(() => {
if (selectedEmail === 'Create New Email Account...') {
navigate('/settings/email-accounts?create-new-email-account');
setSelectedEmail(emailAccounts.length ? emailAccounts[0] : '');
}
}, [selectedEmail, setSelectedEmail, navigate]);
return (
<>
<Button fullWidth icon={faPlus} onClick={openModal} data-tour="accounts-create-account">
Add new account
</Button>
<Modal isOpen={modalIsOpen} onRequestClose={closeModal}>
<ModalMenu labels={all.map((key) =>
<PullContainer verticalAlign key={key} data-tour={`accounts-create-account-${key}`}>
<FontAwesomeIcon icon={Providers.getIcon(key)} style={{ marginRight: '1em' }} />
{key.replace(/(-|_)/g, ' ')}
</PullContainer>,
)}>
{all.map((key) => key === 'email' ?
<EmailProvider key="email" />
: (
<Margin key={key}>
{key !== 'email' && key !== 'open-data-rights'
? <p>By adding a new account for {key}, you are able to retrieve your data from them.</p>
: <p>With this option, you are requesting your data for another organisation that is served by this method.</p>}
{key !== 'email'
? <p>When you create a new account, a window will pop up asking for your credentials. Aeon will never store your credentials. Rather, when you log in, Aeon can hijack the window to perform actions on your behalf. These actions are limited to doing data requests for you.</p>
: null}
{availableProviders[key].requiresEmail &&
<>
<p>In order to use this provider, you must link an email address to Aeon. Selected a previously linked email address in the list below or link one first.</p>
<Dropdown
options={[...emailAccounts, 'Create New Email Account...']}
label="Email Account"
value={selectedEmail}
onSelect={setSelectedEmail}
placeholder="Please select an email account"
/>
</>
}
{availableProviders[key].requiresUrl &&
<>
<p>This provider allows you to get data from any organisation that supports the Open Data Rights API. Please enter the URL for the organisation:</p>
<Label>
<span>Open Data Rights API URL</span>
<TextInput
value={selectedUrl}
onChange={handleUrlChange}
placeholder="https://open-data.acme-corp.com"
type="url"
/>
</Label>
</>
}
<PullCenter>
<NewAccountButton
client={key}
optionalParameters={{
accountName: availableProviders[key].requiresEmail ? selectedEmail : undefined,
apiUrl: availableProviders[key].requiresUrl ? selectedUrl : undefined,
}}
onComplete={closeModal}
disabled={
availableProviders[key].requiresEmail && selectedEmail === ''
|| availableProviders[key].requiresUrl && !isValidUrl(selectedUrl)
}
>
Add new {key.replace(/(-|_)/g, ' ')} account
</NewAccountButton>
</PullCenter>
</Margin>
))}
</ModalMenu>
</Modal>
</>
);
}
export default NewAccountModal;
================================================
FILE: src/app/screens/Accounts/getDescription.ts
================================================
import { formatDistanceToNow } from 'date-fns';
import { DataRequestStatus } from 'main/providers/types';
/**
* A helper to convert a particular DataRequestStatus to a human-readable string
*/
export default function getDescription(status?: DataRequestStatus): string {
if (status?.completed) {
return `Received data ${formatDistanceToNow(new Date(status.completed))} ago`;
}
if (status?.dispatched) {
return `Requested data ${formatDistanceToNow(new Date(status.dispatched))} ago`;
}
return 'No data requested yet';
}
================================================
FILE: src/app/screens/Accounts/index.tsx
================================================
import React, { useCallback } from 'react';
import { Category, List, NavigatableListEntry, PanelBottomButtons, PanelGrid, SplitPanel } from 'app/components/PanelGrid';
import Providers from 'app/utilities/Providers';
import { useParams } from 'react-router-dom';
import { RouteProps } from '../types';
import AccountOverlay from './components/AccountOverlay';
import getDescription from './getDescription';
import styled from 'styled-components';
import { GhostButton } from 'app/components/Button';
import { faEnvelope, faSync } from '@fortawesome/free-solid-svg-icons';
import { useAccounts } from 'app/store/accounts/selectors';
import { State, useAppDispatch } from 'app/store';
import { useSelector } from 'react-redux';
import { refreshRequests } from 'app/store/accounts/actions';
import NewAccountModal from './components/NewAccountModal';
import useTour from 'app/components/Tour/useTour';
const StatusDescription = styled.span`
font-size: 12px;
opacity: 0.5;
font-weight: 400;
`;
const Rows = styled.div`
display: flex;
flex-direction: column;
line-height: 1.5;
`;
function Acc
gitextract_uqsqlvwh/ ├── .eslintrc.json ├── .gitbook.yaml ├── .github/ │ └── workflows/ │ ├── build.yml │ ├── codeql-analysis.yml │ ├── electronegativity.yml │ └── release-notes.yml ├── .gitignore ├── .husky/ │ ├── pre-commit │ └── pre-push ├── .npmrc ├── .vscode/ │ ├── debug-launcher.sh │ ├── launch.json │ └── settings.json ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── SECURITY.md ├── data/ │ └── .gitignore ├── docs/ │ ├── README.md │ ├── SUMMARY.md │ ├── extending-aeon/ │ │ ├── architecture/ │ │ │ ├── README.md │ │ │ └── providers.md │ │ ├── local-development.md │ │ └── reporting-issues.md │ └── using-aeon/ │ ├── getting-started.md │ └── installation.md ├── entitlements.plist ├── forge.config.js ├── package.json ├── playwright.config.ts ├── renovate.json ├── scripts/ │ ├── prepareNodegit.js │ └── setupMacOSCertificates.sh ├── src/ │ ├── app/ │ │ ├── assets/ │ │ │ └── open-data-rights.ts │ │ ├── components/ │ │ │ ├── App.tsx │ │ │ ├── Button.tsx │ │ │ ├── Code.tsx │ │ │ ├── IconBadge.tsx │ │ │ ├── Input.tsx │ │ │ ├── Loading.tsx │ │ │ ├── Menu.tsx │ │ │ ├── Modal/ │ │ │ │ ├── Menu.tsx │ │ │ │ └── index.tsx │ │ │ ├── NoData.tsx │ │ │ ├── PanelGrid.tsx │ │ │ ├── RightSideOverlay.tsx │ │ │ ├── Timestamp.tsx │ │ │ ├── Tooltip.tsx │ │ │ ├── Tour/ │ │ │ │ ├── index.tsx │ │ │ │ ├── steps.tsx │ │ │ │ └── useTour.tsx │ │ │ ├── Typography.tsx │ │ │ └── Utility.tsx │ │ ├── index.ejs │ │ ├── index.tsx │ │ ├── polyfill.ts │ │ ├── preload.ts │ │ ├── screens/ │ │ │ ├── Accounts/ │ │ │ │ ├── components/ │ │ │ │ │ ├── AccountOverlay.tsx │ │ │ │ │ ├── EmailProvider.tsx │ │ │ │ │ └── NewAccountModal.tsx │ │ │ │ ├── getDescription.ts │ │ │ │ └── index.tsx │ │ │ ├── Data/ │ │ │ │ ├── components/ │ │ │ │ │ └── DatumOverlay.tsx │ │ │ │ ├── index.tsx │ │ │ │ ├── styles.tsx │ │ │ │ └── types.tsx │ │ │ ├── Erasure/ │ │ │ │ ├── Emails.tsx │ │ │ │ ├── generateEmail.ts │ │ │ │ └── index.tsx │ │ │ ├── Graph/ │ │ │ │ ├── calculateGraph.ts │ │ │ │ ├── explainer.tsx │ │ │ │ ├── index.tsx │ │ │ │ ├── renderNode.ts │ │ │ │ └── style.ts │ │ │ ├── Onboarding/ │ │ │ │ └── index.tsx │ │ │ ├── Settings/ │ │ │ │ ├── email/ │ │ │ │ │ ├── components/ │ │ │ │ │ │ ├── ImapInitialiser.tsx │ │ │ │ │ │ └── NewAccountModal.tsx │ │ │ │ │ └── index.tsx │ │ │ │ └── index.tsx │ │ │ ├── Timeline/ │ │ │ │ ├── components/ │ │ │ │ │ ├── Commit.tsx │ │ │ │ │ └── Diff.tsx │ │ │ │ └── index.tsx │ │ │ ├── index.tsx │ │ │ └── types.ts │ │ ├── store/ │ │ │ ├── accounts/ │ │ │ │ ├── actions.ts │ │ │ │ ├── index.ts │ │ │ │ ├── selectors.ts │ │ │ │ └── types.ts │ │ │ ├── data/ │ │ │ │ ├── actions.ts │ │ │ │ ├── index.ts │ │ │ │ └── selectors.ts │ │ │ ├── email/ │ │ │ │ ├── actions.ts │ │ │ │ ├── index.ts │ │ │ │ └── selectors.ts │ │ │ ├── index.ts │ │ │ ├── migrations.ts │ │ │ ├── new-commits/ │ │ │ │ ├── actions.ts │ │ │ │ └── index.ts │ │ │ ├── onboarding/ │ │ │ │ ├── actions.ts │ │ │ │ └── index.ts │ │ │ ├── persist.ts │ │ │ └── telemetry/ │ │ │ ├── actions.ts │ │ │ └── index.ts │ │ ├── styles/ │ │ │ ├── global.css │ │ │ ├── index.ts │ │ │ ├── snippets.ts │ │ │ └── theme.css │ │ └── utilities/ │ │ ├── DataType.tsx │ │ ├── Email.ts │ │ ├── Providers.ts │ │ ├── Repository.ts │ │ ├── convertMetaToObject.ts │ │ ├── env.ts │ │ ├── isValidUrl.ts │ │ └── usePrevious.ts │ ├── icon.icns │ ├── main/ │ │ ├── email-client/ │ │ │ ├── bridge.ts │ │ │ ├── gmail/ │ │ │ │ ├── index.ts │ │ │ │ ├── oauth.ts │ │ │ │ └── types.ts │ │ │ ├── imap/ │ │ │ │ └── index.ts │ │ │ ├── index.ts │ │ │ ├── outlook/ │ │ │ │ ├── index.ts │ │ │ │ └── oauth.ts │ │ │ └── types.ts │ │ ├── index.ts │ │ ├── initialise.ts │ │ ├── lib/ │ │ │ ├── app-path.ts │ │ │ ├── constants.ts │ │ │ ├── create-secure-window.ts │ │ │ ├── crypto-fs/ │ │ │ │ └── index.ts │ │ │ ├── logger.ts │ │ │ ├── map-map.ts │ │ │ ├── map-object-to-key-value.ts │ │ │ ├── notifications/ │ │ │ │ ├── index.ts │ │ │ │ └── types.ts │ │ │ ├── oauth.ts │ │ │ ├── object-to-map.ts │ │ │ ├── persisted-map.ts │ │ │ ├── protocol-handler.ts │ │ │ ├── repository/ │ │ │ │ ├── bridge.ts │ │ │ │ ├── index.ts │ │ │ │ ├── types.ts │ │ │ │ └── utilities/ │ │ │ │ ├── diff-map.ts │ │ │ │ ├── generate-diff.ts │ │ │ │ ├── generate-parsed-commit.ts │ │ │ │ ├── parse-csv.ts │ │ │ │ ├── parse-open-data-rights.ts │ │ │ │ └── parse-schema.ts │ │ │ ├── unwrap-provider-source.ts │ │ │ └── window-store.ts │ │ ├── providers/ │ │ │ ├── bridge.ts │ │ │ ├── facebook/ │ │ │ │ ├── index.ts │ │ │ │ └── parser.ts │ │ │ ├── index.ts │ │ │ ├── instagram/ │ │ │ │ ├── index.ts │ │ │ │ ├── parser.ts │ │ │ │ └── urls.json │ │ │ ├── linkedin/ │ │ │ │ ├── index.ts │ │ │ │ └── parser.ts │ │ │ ├── open-data-rights/ │ │ │ │ └── index.ts │ │ │ ├── parsers.ts │ │ │ ├── spotify/ │ │ │ │ ├── index.ts │ │ │ │ └── parser.ts │ │ │ └── types/ │ │ │ ├── Data.ts │ │ │ ├── Events.ts │ │ │ ├── Provider.ts │ │ │ └── index.ts │ │ ├── store.ts │ │ └── updates.ts │ └── typings/ │ ├── cytoscape-fcose.d.ts │ ├── fonts.d.ts │ ├── images.d.ts │ └── redux-persist.d.ts ├── test/ │ ├── .gitignore │ ├── spec.ts │ └── utilities/ │ ├── getRandomNode.ts │ └── getRoute.ts ├── tsconfig.json ├── webpack.main.config.js ├── webpack.plugins.js ├── webpack.renderer.config.js └── webpack.rules.js
SYMBOL INDEX (417 symbols across 116 files)
FILE: forge.config.js
function notarizeMaybe (line 83) | function notarizeMaybe() {
function bundleOpenSSLMaybe (line 127) | function bundleOpenSSLMaybe() {
FILE: src/app/components/App.tsx
class App (line 20) | class App extends Component {
method componentDidMount (line 21) | componentDidMount(): void {
method render (line 25) | render(): JSX.Element {
FILE: src/app/components/Button.tsx
type ThemeColor (line 8) | type ThemeColor = 'blue' | 'red' | 'yellow' | 'green' | 'gray';
type ButtonProps (line 10) | interface ButtonProps extends HTMLAttributes<HTMLButtonElement> {
type Props (line 100) | interface Props extends ButtonProps {
FILE: src/app/components/IconBadge.tsx
type IconBadgeProps (line 22) | interface IconBadgeProps {
function IconBadge (line 26) | function IconBadge({ icon }: IconBadgeProps) {
function IconBadgeWithTitle (line 37) | function IconBadgeWithTitle({ children, icon }: PropsWithChildren<IconBa...
FILE: src/app/components/Input.tsx
type DropdownProps (line 69) | interface DropdownProps {
type Option (line 78) | type Option = { key: string, value: unknown };
function Dropdown (line 80) | function Dropdown(props: DropdownProps): JSX.Element {
FILE: src/app/components/Loading.tsx
type Props (line 12) | interface Props {
FILE: src/app/components/Menu.tsx
function TitleBar (line 109) | function TitleBar(): JSX.Element {
function Menu (line 120) | function Menu(): JSX.Element {
FILE: src/app/components/Modal/Menu.tsx
type Props (line 47) | type Props = {
function ModalMenu (line 52) | function ModalMenu({ children, labels = [] }: Props): JSX.Element {
FILE: src/app/components/Modal/index.tsx
type SpringProps (line 10) | interface SpringProps {
type NextFunc (line 16) | type NextFunc = (props: SpringProps) => Promise<void>;
type Props (line 18) | interface Props {
type DialogProps (line 58) | type DialogProps = PropsWithChildren<
function Dialog (line 62) | function Dialog(props: DialogProps): JSX.Element {
class Modal (line 84) | class Modal extends Component<Props> {
method componentDidMount (line 89) | componentDidMount(): void {
method componentWillUnmount (line 93) | componentWillUnmount(): void {
method render (line 111) | render(): JSX.Element {
FILE: src/app/components/NoData.tsx
function NoData (line 25) | function NoData(): JSX.Element {
FILE: src/app/components/PanelGrid.tsx
type ListButtonProps (line 99) | type ListButtonProps = {
function Category (line 194) | function Category({ title, children, id }: PropsWithChildren<{ title?: s...
type NavigatableListEntryProps (line 207) | type NavigatableListEntryProps = PropsWithChildren<{
function NavigatableListEntry (line 212) | function NavigatableListEntry({
FILE: src/app/components/RightSideOverlay.tsx
type RightSideOverlayProps (line 10) | type RightSideOverlayProps = PropsWithChildren<{
FILE: src/app/components/Timestamp.tsx
type TimestampProps (line 5) | interface TimestampProps {
function Timestamp (line 9) | function Timestamp({ children: date }: TimestampProps) {
FILE: src/app/components/Tooltip.tsx
function getTransitionConfig (line 15) | function getTransitionConfig(placement: Placement) {
type TooltipProps (line 53) | type TooltipProps = PropsWithChildren<{
function Tooltip (line 62) | function Tooltip({ title, children, placement = 'auto' }: TooltipProps) {
FILE: src/app/components/Tour/index.tsx
function Tour (line 7) | function Tour({ children }: PropsWithChildren<unknown>): JSX.Element {
FILE: src/app/components/Tour/steps.tsx
type TourKeys (line 19) | type TourKeys = '/screen/timeline'
FILE: src/app/components/Tour/useTour.tsx
function useTour (line 9) | function useTour(screen: TourKeys) {
function Tour (line 29) | function Tour({ screen }: { screen: TourKeys }): null {
FILE: src/app/components/Typography.tsx
type BaseTextProps (line 3) | interface BaseTextProps {
FILE: src/app/components/Utility.tsx
type PullContainerProps (line 11) | interface PullContainerProps {
FILE: src/app/preload.ts
type WindowApi (line 14) | interface WindowApi {
type Window (line 30) | interface Window {
FILE: src/app/screens/Accounts/components/AccountOverlay.tsx
type Props (line 18) | interface Props {
function hasUrl (line 22) | function hasUrl(account: InitialisedAccount | EmailProvider): account is...
function AccountOverlay (line 26) | function AccountOverlay({ selectedAccount }: Props): JSX.Element {
FILE: src/app/screens/Accounts/components/EmailProvider.tsx
type APIResponse (line 14) | interface APIResponse {
type Result (line 21) | interface Result {
type Requests (line 41) | interface Requests {
type Access (line 51) | interface Access {
function EmailProvider (line 64) | function EmailProvider(): JSX.Element {
FILE: src/app/screens/Accounts/components/NewAccountModal.tsx
type NewAccountProps (line 20) | type NewAccountProps = PropsWithChildren<{
function NewAccountButton (line 28) | function NewAccountButton({ client, children, onComplete, optionalParame...
function NewAccountModal (line 50) | function NewAccountModal(): JSX.Element {
FILE: src/app/screens/Accounts/getDescription.ts
function getDescription (line 7) | function getDescription(status?: DataRequestStatus): string {
FILE: src/app/screens/Accounts/index.tsx
function Accounts (line 30) | function Accounts(): JSX.Element {
FILE: src/app/screens/Data/components/DatumOverlay.tsx
type Props (line 17) | interface Props {
FILE: src/app/screens/Data/index.tsx
function Data (line 17) | function Data(): JSX.Element {
FILE: src/app/screens/Data/styles.tsx
type ListButtonProps (line 9) | interface ListButtonProps extends HTMLAttributes<HTMLAnchorElement> {
type ClickableCategoryProps (line 71) | interface ClickableCategoryProps extends Omit<ListButtonProps, 'onClick'> {
type ClickableDataPointProps (line 85) | interface ClickableDataPointProps extends Omit<ListButtonProps, 'onClick...
FILE: src/app/screens/Data/types.tsx
type GroupedData (line 3) | type GroupedData = { [key: string]: ProviderDatum<string, ProvidedDataT...
type DeletedData (line 4) | type DeletedData = { [key: string]: number[] };
FILE: src/app/screens/Erasure/Emails.tsx
function ErasureEmails (line 25) | function ErasureEmails(): JSX.Element {
FILE: src/app/screens/Erasure/generateEmail.ts
function generateEmail (line 9) | function generateEmail(data: ProviderDatum<unknown, unknown>[], provider...
FILE: src/app/screens/Erasure/index.tsx
function Erasure (line 49) | function Erasure(): JSX.Element {
FILE: src/app/screens/Graph/calculateGraph.ts
function calculateGraph (line 11) | function calculateGraph(data: ProviderDatum<unknown, unknown>[]): Elemen...
FILE: src/app/screens/Graph/explainer.tsx
function GraphExplainer (line 50) | function GraphExplainer(): JSX.Element {
FILE: src/app/screens/Graph/index.tsx
type HoveredNode (line 19) | type HoveredNode = {
type CytoEvent (line 26) | type CytoEvent = { target: NodeSingular };
function Graph (line 28) | function Graph(): JSX.Element {
FILE: src/app/screens/Graph/renderNode.ts
function getIconFromNode (line 10) | function getIconFromNode(element: NodeSingular): IconDefinition {
function renderNode (line 28) | function renderNode(color: string, size: number): (element: NodeSingular...
FILE: src/app/screens/Onboarding/index.tsx
function Onboarding (line 31) | function Onboarding(): JSX.Element {
FILE: src/app/screens/Settings/email/components/ImapInitialiser.tsx
type Props (line 17) | interface Props {
function ImapInitialiser (line 21) | function ImapInitialiser({ onComplete }: Props) {
FILE: src/app/screens/Settings/email/components/NewAccountModal.tsx
type NewAccountProps (line 14) | type NewAccountProps = PropsWithChildren<{
function NewAccountButton (line 19) | function NewAccountButton({ client, children, onComplete, ...props }: Ne...
function NewAccountModal (line 41) | function NewAccountModal(): JSX.Element {
FILE: src/app/screens/Settings/email/index.tsx
function EmailSettings (line 15) | function EmailSettings({ settingId: selectedAccount }: { settingId?: str...
FILE: src/app/screens/Settings/index.tsx
type CategoryPanel (line 8) | type CategoryPanel = (props: { settingId?: string }) => JSX.Element;
function Settings (line 18) | function Settings(): JSX.Element {
FILE: src/app/screens/Timeline/components/Commit.tsx
type Props (line 8) | interface Props extends Omit<React.HTMLAttributes<HTMLButtonElement>, 'o...
class Commit (line 83) | class Commit extends Component<Props> {
method render (line 88) | render(): JSX.Element {
FILE: src/app/screens/Timeline/components/Diff.tsx
type Props (line 18) | interface Props {
type State (line 23) | interface State {
class Diff (line 50) | class Diff extends PureComponent<Props, State> {
method componentDidMount (line 55) | componentDidMount(): void {
method componentDidUpdate (line 59) | componentDidUpdate(prevProps: Props): void {
method filterAndSortExtractedData (line 71) | filterAndSortExtractedData(diff: DiffResult<ExtractedDataDiff>[]): Ext...
method render (line 87) | render(): JSX.Element {
FILE: src/app/screens/Timeline/index.tsx
type State (line 17) | interface State {
type Props (line 22) | interface Props {
class Timeline (line 40) | class Timeline extends Component<Props, State> {
method componentDidMount (line 46) | componentDidMount(): void {
method componentDidUpdate (line 51) | componentDidUpdate(prevProps: Props) {
method componentWillUnmount (line 57) | componentWillUnmount(): void {
method render (line 85) | render(): JSX.Element {
FILE: src/app/screens/index.tsx
function Router (line 13) | function Router(): JSX.Element {
FILE: src/app/screens/types.ts
type RouteProps (line 3) | interface RouteProps {
FILE: src/app/store/accounts/index.ts
type AccountsState (line 6) | interface AccountsState {
FILE: src/app/store/accounts/selectors.ts
type RequestState (line 10) | type RequestState = {
function useAccounts (line 20) | function useAccounts(): RequestState {
function useProvider (line 35) | function useProvider(key: string): InitialisedAccount {
function ProviderSubscription (line 42) | function ProviderSubscription(): null {
FILE: src/app/store/accounts/types.ts
type EmailProvider (line 3) | interface EmailProvider {
FILE: src/app/store/data/index.ts
type DataState (line 5) | interface DataState {
FILE: src/app/store/data/selectors.ts
function RepositorySubscription (line 6) | function RepositorySubscription(): JSX.Element {
FILE: src/app/store/email/index.ts
type EmailState (line 4) | interface EmailState {
FILE: src/app/store/email/selectors.ts
function EmailSubscription (line 9) | function EmailSubscription(): null {
FILE: src/app/store/index.ts
type State (line 37) | type State = ReturnType<typeof rootReducer>;
type AppDispatch (line 72) | type AppDispatch = typeof store.dispatch;
FILE: src/app/store/new-commits/index.ts
type NewCommit (line 4) | interface NewCommit extends Commit {
type NewCommitState (line 8) | type NewCommitState = NewCommit[];
method add (line 14) | add(state, action: PayloadAction<NewCommit>) {
FILE: src/app/store/onboarding/index.ts
type OnboardingState (line 4) | interface OnboardingState {
method complete (line 18) | complete(state, action: PayloadAction<keyof Omit<OnboardingState, 'tour'...
method completeTour (line 21) | completeTour(state, action: PayloadAction<TourKeys>) {
FILE: src/app/store/persist.ts
function ElectronStorage (line 3) | function ElectronStorage(): Storage {
FILE: src/app/store/telemetry/index.ts
type Event (line 3) | interface Event {
method log (line 16) | log(state, action: PayloadAction<Event>) {
FILE: src/app/utilities/DataType.tsx
class DataType (line 60) | class DataType {
method getIcon (line 65) | static getIcon(type: ProvidedDataTypes): IconDefinition {
method toString (line 172) | static toString(datum: ProviderDatum<unknown, unknown>): JSX.Element |...
method getDescription (line 267) | static getDescription(datum: ProviderDatum<unknown, unknown>): string {
FILE: src/app/utilities/Email.ts
type SubscriptionHandler (line 9) | type SubscriptionHandler = (event: IpcRendererEvent, type: EmailEvents) ...
class Email (line 11) | class Email {
method subscribe (line 12) | static subscribe(handler: SubscriptionHandler): void {
method unsubscribe (line 16) | static unsubscribe(handler: SubscriptionHandler): void {
method initialise (line 20) | static initialise(client: string): Promise<string> {
method initialiseImap (line 24) | static initialiseImap({ user, pass, host, port, secure = true }: ImapC...
method delete (line 28) | static delete(account: string): Promise<string> {
method getAccounts (line 32) | static getAccounts(): Promise<Record<string, string>> {
method getClients (line 36) | static getClients(): Promise<string[]> {
method testImap (line 40) | static testImap({
method getIcon (line 50) | static getIcon(clientKey: string): IconDefinition {
FILE: src/app/utilities/Providers.ts
type DataRequestReturnType (line 10) | type DataRequestReturnType = {
type SubscriptionHandler (line 15) | type SubscriptionHandler = (event: IpcRendererEvent, type: ProviderEvent...
class Providers (line 17) | class Providers {
method subscribe (line 18) | static subscribe(handler: SubscriptionHandler): void {
method unsubscribe (line 22) | static unsubscribe(handler: SubscriptionHandler): void {
method initialise (line 26) | static initialise(key: string, optional?: InitOptionalParameters): Pro...
method update (line 30) | static update(key: string): Promise<void> {
method updateAll (line 34) | static updateAll(): Promise<void> {
method dispatchDataRequest (line 38) | static dispatchDataRequest(key: string): Promise<void> {
method dispatchDataRequestToAll (line 42) | static dispatchDataRequestToAll(): Promise<void> {
method refresh (line 46) | static refresh(): Promise<void> {
method getAccounts (line 50) | static getAccounts(): Promise<DataRequestReturnType> {
method getAvailableProviders (line 54) | static getAvailableProviders(): Promise<Record<string, { requiresEmail...
method getIcon (line 58) | static getIcon(key: string): IconDefinition {
method getPrivacyEmail (line 77) | static getPrivacyEmail(key: string): string {
FILE: src/app/utilities/Repository.ts
type SubscriptionHandler (line 8) | type SubscriptionHandler = (event: IpcRendererEvent, type: RepositoryEve...
class Repository (line 10) | class Repository {
method subscribe (line 11) | static subscribe(handler: SubscriptionHandler): void {
method unsubscribe (line 15) | static unsubscribe(handler: SubscriptionHandler): void {
method diff (line 19) | static diff(refTree?: string | RepositoryArguments, comparedTree?: str...
method parsedCommit (line 23) | static parsedCommit(tree?: string | RepositoryArguments): Promise<Prov...
method log (line 27) | static log(): Promise<Commit[]> {
method status (line 31) | static status(): Promise<StatusFile[]> {
FILE: src/app/utilities/convertMetaToObject.ts
type CommitMetadata (line 3) | interface CommitMetadata {
function convertMetaToObject (line 16) | function convertMetaToObject(message: string): CommitMetadata {
FILE: src/app/utilities/isValidUrl.ts
function isValidUrl (line 6) | function isValidUrl(input: string): boolean {
FILE: src/app/utilities/usePrevious.ts
function usePrevious (line 7) | function usePrevious<T>(value: T): T {
FILE: src/main/email-client/bridge.ts
class EmailBridge (line 14) | class EmailBridge {
method constructor (line 17) | constructor(manager: EmailManager) {
method send (line 55) | public static send(event: string): void {
FILE: src/main/email-client/gmail/index.ts
type ListResponse (line 9) | interface ListResponse {
type MessageResponse (line 15) | interface MessageResponse {
class GmailEmailClient (line 19) | class GmailEmailClient extends OauthEmailClient<GmailTokenResponse> {
method initialize (line 22) | async initialize(): Promise<string> {
method refreshTokens (line 36) | async refreshTokens(expiredTokens: GmailTokenResponse): Promise<GmailT...
method findMessages (line 42) | async findMessages(query: EmailQuery): Promise<Email[]> {
method getMessage (line 69) | async getMessage(messageId: string): Promise<Email> {
method sendMessage (line 83) | async sendMessage(options: Mail.Options): Promise<void> {
method getEmailAddress (line 101) | async getEmailAddress(): Promise<string> {
FILE: src/main/email-client/gmail/oauth.ts
constant GMAIL_OAUTH_CLIENT_ID (line 11) | const GMAIL_OAUTH_CLIENT_ID = process.env.GMAIL_OAUTH_CLIENT_ID;
constant GMAIL_OAUTH_CLIENT_SECRET (line 12) | const GMAIL_OAUTH_CLIENT_SECRET = process.env.GMAIL_OAUTH_CLIENT_SECRET;
type CodeAndRedirectUri (line 14) | interface CodeAndRedirectUri {
function exchangeAccessCode (line 24) | async function exchangeAccessCode(response: CodeAndRedirectUri, verifier...
type CodeCallback (line 41) | type CodeCallback = (code: CodeAndRedirectUri) => void;
function setupRedirectListener (line 47) | async function setupRedirectListener(callback: CodeCallback): Promise<st...
function refreshGmailTokens (line 120) | function refreshGmailTokens(refresh_token: string): Promise<GmailTokenRe...
function retrieveAuthenticationCode (line 139) | async function retrieveAuthenticationCode(verifier: string): Promise<Cod...
function authenticateGmailUser (line 168) | async function authenticateGmailUser(): Promise<GmailTokenResponse> {
FILE: src/main/email-client/gmail/types.ts
type GmailTokenResponse (line 1) | interface GmailTokenResponse {
FILE: src/main/email-client/imap/index.ts
type ImapCredentials (line 7) | interface ImapCredentials {
function testImap (line 19) | async function testImap(
class ImapClient (line 51) | class ImapClient extends EmailClient {
method initialize (line 56) | async initialize(): Promise<string> {
method initializeImap (line 60) | async initializeImap(
method delete (line 93) | async delete(): Promise<void> {
method findMessages (line 98) | async findMessages(query?: EmailQuery): Promise<ParsedMail[]> {
FILE: src/main/email-client/index.ts
class EmailManager (line 15) | class EmailManager extends EventEmitter2 {
method constructor (line 24) | constructor() {
method initialiseNewAddress (line 44) | async initialiseNewAddress(clientKey: string, ...args: unknown[]): Pro...
method deleteAccount (line 89) | deleteAccount(address: string): void {
FILE: src/main/email-client/outlook/index.ts
type ProfileResponse (line 7) | interface ProfileResponse {
class OutlookEmailClient (line 22) | class OutlookEmailClient extends OauthEmailClient<OutlookTokenResponse> {
method initialize (line 25) | async initialize(): Promise<string> {
method getEmailAddress (line 40) | async getEmailAddress(): Promise<string> {
method findMessages (line 45) | async findMessages(query?: EmailQuery): Promise<ParsedMail[]> {
method getMessage (line 86) | async getMessage(id: string): Promise<ParsedMail> {
method refreshTokens (line 92) | async refreshTokens(expiredTokens: OutlookTokenResponse): Promise<Outl...
FILE: src/main/email-client/outlook/oauth.ts
constant OUTLOOK_OAUTH_CLIENT_ID (line 7) | const OUTLOOK_OAUTH_CLIENT_ID = process.env.OUTLOOK_OAUTH_CLIENT_ID;
constant OUTLOOK_OAUTH_CLIENT_SECRET (line 8) | const OUTLOOK_OAUTH_CLIENT_SECRET = process.env.OUTLOOK_OAUTH_CLIENT_SEC...
constant REDIRECT_URI (line 11) | const REDIRECT_URI = 'https://login.microsoftonline.com/common/oauth2/na...
constant SCOPES (line 12) | const SCOPES = [
type OutlookTokenResponse (line 17) | interface OutlookTokenResponse {
type TokenError (line 27) | interface TokenError {
function authenticateOutlookUser (line 39) | async function authenticateOutlookUser(): Promise<OutlookTokenResponse> {
function refreshOutlookTokens (line 129) | async function refreshOutlookTokens({ refresh_token, windowKey }: Outloo...
FILE: src/main/email-client/types.ts
type EmailQuery (line 4) | interface EmailQuery {
type Email (line 13) | type Email = ParsedMail;
method constructor (line 53) | constructor(emailAddress: string | null) {
type EmailClient (line 58) | interface EmailClient {
type EmailCommands (line 66) | enum EmailCommands {
type EmailEvents (line 74) | enum EmailEvents {
FILE: src/main/initialise.ts
function initialise (line 12) | function initialise(): void {
FILE: src/main/lib/app-path.ts
constant APP_DATA_PATH (line 4) | const APP_DATA_PATH = process.env.NODE_ENV === 'production' ? app.getPat...
FILE: src/main/lib/constants.ts
type CommandLineArguments (line 13) | interface CommandLineArguments {
function setDefaults (line 35) | function setDefaults(cliArgs: Partial<CommandLineArguments>): CommandLin...
FILE: src/main/lib/create-secure-window.ts
type SecureWindowParameters (line 6) | interface SecureWindowParameters {
function createSecureWindow (line 20) | function createSecureWindow(params: SecureWindowParameters): BrowserWind...
function withSecureWindow (line 95) | function withSecureWindow<U>(
FILE: src/main/lib/crypto-fs/index.ts
constant ALGORITHM (line 4) | const ALGORITHM = 'aes-256-cbc';
class CryptoFs (line 6) | class CryptoFs {
method constructor (line 9) | constructor(password: string) {
method init (line 16) | init(): typeof fs {
FILE: src/main/lib/logger.ts
type Logger (line 27) | type Logger = {
FILE: src/main/lib/map-map.ts
type Map (line 2) | interface Map<K, V> {
FILE: src/main/lib/map-object-to-key-value.ts
function mapObjectToKeyValue (line 9) | function mapObjectToKeyValue(obj: { [key: string]: any }): { key: any, v...
function objectToKeyValueTransformer (line 22) | function objectToKeyValueTransformer(obj: { [key: string]: any }): Parti...
FILE: src/main/lib/notifications/index.ts
class Notifications (line 6) | class Notifications {
method success (line 7) | public static success(message: string): void {
method info (line 12) | public static info(message: string): void {
method loading (line 17) | public static loading(message: string): void {
method warn (line 22) | public static warn(message: string): void {
method error (line 27) | public static error(message: string): void {
FILE: src/main/lib/notifications/types.ts
type NotificationTypes (line 1) | enum NotificationTypes {
FILE: src/main/lib/oauth.ts
function generateVerifier (line 9) | function generateVerifier(): string {
function objectToUrlParams (line 28) | function objectToUrlParams(data: Record<string, unknown>) {
type BaseTokenResponse (line 40) | interface BaseTokenResponse {
method delete (line 60) | async delete(): Promise<void> {
method getTokens (line 67) | async getTokens(): Promise<T> {
method storeTokens (line 92) | async storeTokens(tokens: T, persist = true): Promise<void> {
method get (line 110) | async get(url: string, init: RequestInit = null, parseType: 'json' | 'te...
method errorMiddleware (line 160) | async errorMiddleware(response: Response): Promise<Response> {
FILE: src/main/lib/persisted-map.ts
class PersistedMap (line 5) | class PersistedMap<K, V> extends Map<K, V> {
method constructor (line 8) | constructor(rows: [K, V][], callback: (map: PersistedMap<K, V>) => voi...
method set (line 15) | set(key: K, value: V): this {
method delete (line 28) | delete(key: K): boolean {
method clear (line 34) | clear(): void {
FILE: src/main/lib/protocol-handler.ts
type ProtocolCallback (line 5) | type ProtocolCallback = (url: string) => void;
function getProtocolResultForPath (line 23) | function getProtocolResultForPath(urlPath: string) {
function registerProtocolCallbackForPath (line 43) | function registerProtocolCallbackForPath(urlPath: string, callback: Prot...
function setupProtocolHandlers (line 50) | function setupProtocolHandlers() {
FILE: src/main/lib/repository/bridge.ts
class RepositoryBridge (line 8) | class RepositoryBridge {
method constructor (line 13) | constructor(repository: Repository) {
method send (line 51) | public static send(event: RepositoryEvents): void {
FILE: src/main/lib/repository/index.ts
constant EMPTY_REPO_HASH (line 25) | const EMPTY_REPO_HASH = '4b825dc642cb6eb9a060e54bf8d69288fbee4904';
constant ENABLE_ENCRYPTION (line 27) | const ENABLE_ENCRYPTION = process.env.ENABLE_ENCRYPTION === 'true';
class Repository (line 30) | class Repository extends EventEmitter {
method constructor (line 56) | constructor() {
method initialiseRepository (line 78) | private async initialiseRepository(): Promise<NodeGitRepository> {
method diff (line 108) | public async diff(
method save (line 166) | public async save(filepath: string, data: string | Buffer): Promise<vo...
method getParsedCommit (line 183) | public async getParsedCommit(ref = 'HEAD'): Promise<ProviderDatum<unkn...
method add (line 248) | public async add(filepath: string): Promise<void> {
method log (line 252) | public async log(): Promise<Commit[]> {
method commit (line 279) | public async commit(message: string): Promise<string> {
method status (line 300) | public status(): Promise<StatusFile[]> {
FILE: src/main/lib/repository/types.ts
type DiffType (line 3) | enum DiffType {
type ObjectChange (line 17) | interface ObjectChange<O = Record<string, unknown>> {
type DiffResult (line 29) | interface DiffResult<D> {
type ObjectDiff (line 36) | type ObjectDiff = ObjectChange<unknown>;
type ExtractedDataDiff (line 37) | type ExtractedDataDiff = ObjectChange<ProviderDatum<unknown>[]>;
type Status (line 41) | type Status = 'ignored' | 'unmodified' | '*modified' | '*deleted' | '*ad...
type StatusResult (line 43) | interface StatusResult {
type RepositoryCommands (line 48) | enum RepositoryCommands {
type RepositoryArguments (line 55) | enum RepositoryArguments {
type RepositoryEvents (line 61) | enum RepositoryEvents {
type Commit (line 65) | interface Commit {
FILE: src/main/lib/repository/utilities/generate-diff.ts
type DataArrayDiff (line 7) | interface DataArrayDiff {
function diffDataArray (line 20) | function diffDataArray(
function generateDiff (line 100) | async function generateDiff(
FILE: src/main/lib/repository/utilities/generate-parsed-commit.ts
type ParsedExtension (line 19) | type ParsedExtension = typeof parsedExtensions[number];
function getObjectByExtension (line 26) | function getObjectByExtension(extension: ParsedExtension, blob: Blob) {
function generateParsedCommit (line 43) | async function generateParsedCommit(
FILE: src/main/lib/repository/utilities/parse-csv.ts
function parseCsv (line 11) | function parseCsv(blob: Blob): Promise<unknown> {
FILE: src/main/lib/repository/utilities/parse-open-data-rights.ts
type OpenDataRightsDatum (line 3) | type OpenDataRightsDatum = Pick<
function parseOpenDataRights (line 12) | function parseOpenDataRights(
FILE: src/main/lib/repository/utilities/parse-schema.ts
function recursivelyExtractData (line 22) | function recursivelyExtractData(haystack: {[key: string]: any}, needle: ...
function parseSchema (line 90) | function parseSchema(file: Buffer | { [key: string] : any }, parser: Pro...
FILE: src/main/lib/unwrap-provider-source.ts
function unwrapParserSource (line 7) | function unwrapParserSource(source: string | string[]) {
FILE: src/main/lib/window-store.ts
class WindowStore (line 3) | class WindowStore {
method getInstance (line 8) | static getInstance(): WindowStore {
method getWindow (line 16) | static getWindow(): BrowserWindow {
method window (line 20) | set window(window: BrowserWindow) {
method window (line 24) | get window(): BrowserWindow {
FILE: src/main/providers/bridge.ts
class ProviderBridge (line 10) | class ProviderBridge {
method constructor (line 15) | constructor(providers: Providers) {
method send (line 78) | public static send(event: ProviderEvents, ...props: unknown[]): void {
FILE: src/main/providers/facebook/index.ts
class Facebook (line 11) | class Facebook extends DataRequestProvider {
method initialise (line 26) | async initialise(): Promise<string> {
method isDataRequestComplete (line 143) | async isDataRequestComplete(): Promise<boolean> {
method parseDataRequest (line 161) | async parseDataRequest(extractionPath: string): Promise<ProviderFile[]> {
FILE: src/main/providers/index.ts
class ProviderManager (line 53) | class ProviderManager extends EventEmitter2 {
method constructor (line 72) | constructor(repository: Repository, email: EmailManager) {
FILE: src/main/providers/instagram/index.ts
class Instagram (line 17) | class Instagram extends EmailDataRequestProvider {
method initialise (line 32) | async initialise(): Promise<string> {
method getAccountName (line 40) | async getAccountName(): Promise<string> {
method dispatchDataRequest (line 131) | async dispatchDataRequest(): Promise<number> {
method parseEmailForDownloadUrl (line 171) | async parseEmailForDownloadUrl(date: number): Promise<string | undefin...
method isDataRequestComplete (line 204) | async isDataRequestComplete(date: number): Promise<boolean> {
method parseDataRequest (line 209) | async parseDataRequest(extractionPath: string, date: number): Promise<...
FILE: src/main/providers/instagram/parser.ts
type GenericKeyedData (line 20) | type GenericKeyedData = {
FILE: src/main/providers/linkedin/index.ts
class LinkedIn (line 11) | class LinkedIn extends DataRequestProvider {
method initialise (line 26) | async initialise(): Promise<string> {
method isDataRequestComplete (line 138) | async isDataRequestComplete(): Promise<boolean> {
method parseDataRequest (line 160) | async parseDataRequest(extractionPath: string): Promise<ProviderFile[]> {
FILE: src/main/providers/linkedin/parser.ts
function semiColonSeperatedTransformer (line 11) | function semiColonSeperatedTransformer(object: string): Partial<Provider...
FILE: src/main/providers/open-data-rights/index.ts
type Token (line 8) | type Token = {
class OpenDataRights (line 13) | class OpenDataRights extends OpenDataRightsProvider {
method constructor (line 20) | constructor(windowKey: string, accountName?: string) {
method initialise (line 33) | async initialise(): Promise<string> {
method update (line 74) | async update(): Promise<false> {
FILE: src/main/providers/parsers.ts
function getParserByFileName (line 56) | function getParserByFileName(filepath: string): ProviderParser | void {
FILE: src/main/providers/spotify/index.ts
class Spotify (line 9) | class Spotify extends EmailDataRequestProvider {
method initialise (line 19) | async initialise(): Promise<string> {
method recursivelyWaitForConfirmationEmail (line 103) | async recursivelyWaitForConfirmationEmail(): Promise<void> {
method isDataRequestComplete (line 136) | async isDataRequestComplete(): Promise<boolean> {
method parseDataRequest (line 157) | async parseDataRequest(extractionPath: string): Promise<ProviderFile[]> {
FILE: src/main/providers/spotify/parser.ts
type SpotifyStreamHistorySong (line 4) | type SpotifyStreamHistorySong = {
type SpotifyUserData (line 11) | type SpotifyUserData = {
FILE: src/main/providers/types/Data.ts
type ProvidedDataTypes (line 2) | enum ProvidedDataTypes {
type ProviderDatum (line 101) | interface ProviderDatum<D, T = ProvidedDataTypes> {
type Email (line 123) | type Email = ProviderDatum<string, ProvidedDataTypes.EMAIL>;
type FirstName (line 124) | type FirstName = ProviderDatum<string, ProvidedDataTypes.FIRST_NAME>;
type LastName (line 125) | type LastName = ProviderDatum<string, ProvidedDataTypes.LAST_NAME>;
type FullName (line 126) | type FullName = ProviderDatum<string, ProvidedDataTypes.FULL_NAME>;
type IpAddress (line 127) | type IpAddress = ProviderDatum<string, ProvidedDataTypes.IP_ADDRESS>;
type UserAgent (line 128) | type UserAgent = ProviderDatum<string, ProvidedDataTypes.USER_AGENT>;
type UserLanguage (line 129) | type UserLanguage = ProviderDatum<string, ProvidedDataTypes.USER_LANGUAGE>;
type Follower (line 130) | type Follower = ProviderDatum<string, ProvidedDataTypes.FOLLOWER>;
type AccountFollowing (line 131) | type AccountFollowing = ProviderDatum<string, ProvidedDataTypes.ACCOUNT_...
type BlockedAccount (line 132) | type BlockedAccount = ProviderDatum<string, ProvidedDataTypes.BLOCKED_AC...
type HashtagFollowing (line 133) | type HashtagFollowing = ProviderDatum<string, ProvidedDataTypes.HASHTAG_...
type AdInterest (line 134) | type AdInterest = ProviderDatum<string, ProvidedDataTypes.AD_INTEREST>;
type TelephoneNumber (line 135) | type TelephoneNumber = ProviderDatum<string, ProvidedDataTypes.TELEPHONE...
type Device (line 136) | type Device = ProviderDatum<string, ProvidedDataTypes.DEVICE>;
type Username (line 137) | type Username = ProviderDatum<string, ProvidedDataTypes.USERNAME>;
type PlaceOfResidence (line 138) | type PlaceOfResidence = ProviderDatum<string, ProvidedDataTypes.PLACE_OF...
type Address (line 139) | type Address = ProviderDatum<{ street?: string; number?: number; state?:...
type Country (line 140) | type Country = ProviderDatum<string, ProvidedDataTypes.COUNTRY>;
type Like (line 141) | type Like = ProviderDatum<string, ProvidedDataTypes.LIKE>;
type LoginInstance (line 142) | type LoginInstance = ProviderDatum<number, ProvidedDataTypes.LOGIN_INSTA...
type LogOutInstance (line 143) | type LogOutInstance = ProviderDatum<unknown, ProvidedDataTypes.LOGOUT_IN...
type Photo (line 144) | type Photo = ProviderDatum<{ url: string; description: string; }, Provid...
type Message (line 145) | type Message = ProviderDatum<string, ProvidedDataTypes.MESSAGE>;
type Gender (line 146) | type Gender = ProviderDatum<string, ProvidedDataTypes.GENDER>;
type ProfilePicture (line 147) | type ProfilePicture = ProviderDatum<string, ProvidedDataTypes.PROFILE_PI...
type DateOfBirth (line 148) | type DateOfBirth = ProviderDatum<Date, ProvidedDataTypes.DATE_OF_BIRTH>;
type JoinDate (line 149) | type JoinDate = ProviderDatum<Date, ProvidedDataTypes.JOIN_DATE>;
type SearchQuery (line 150) | type SearchQuery = ProviderDatum<string, ProvidedDataTypes.SEARCH_QUERY>;
type PostSeen (line 151) | type PostSeen = ProviderDatum<string, ProvidedDataTypes.POST_SEEN>;
type PrivacySetting (line 153) | type PrivacySetting = ProviderDatum<{ key: string; value: any; }, Provid...
type UploadedContact (line 154) | type UploadedContact = ProviderDatum<unknown, ProvidedDataTypes.UPLOADED...
type SessionData (line 155) | type SessionData = {
type Session (line 163) | type Session = ProviderDatum<SessionData, ProvidedDataTypes.SESSION>;
type PeerGroup (line 164) | type PeerGroup = ProviderDatum<string, ProvidedDataTypes.PEER_GROUP>;
type Employment (line 165) | type Employment = ProviderDatum<{ jobTitle: string; company: string; }, ...
type VisitedPage (line 166) | type VisitedPage = ProviderDatum<{ name: string; uri?: string; }, Provid...
type OffSiteActivity (line 167) | type OffSiteActivity = ProviderDatum<{ type?: string; website: string; }...
type EventResponse (line 168) | type EventResponse = ProviderDatum<{ name?: string; response?: string; }...
type Timezone (line 169) | type Timezone = ProviderDatum<string, ProvidedDataTypes.TIMEZONE>;
type Currency (line 170) | type Currency = ProviderDatum<string, ProvidedDataTypes.CURRENCY>;
type EducationExperience (line 171) | type EducationExperience = ProviderDatum<{ institution: string; graduate...
type RegistrationDate (line 172) | type RegistrationDate = ProviderDatum<Date, ProvidedDataTypes.REGISTRATI...
type MobileDevice (line 173) | type MobileDevice = ProviderDatum<{ type: string; os?: string; advertise...
type Inference (line 174) | type Inference = ProviderDatum<string, ProvidedDataTypes.INFERENCE>;
type PlayedSong (line 175) | type PlayedSong = ProviderDatum<{
type Biography (line 181) | type Biography = ProviderDatum<string, ProvidedDataTypes.BIOGRAPHY>;
type Advertiser (line 182) | type Advertiser = ProviderDatum<string, ProvidedDataTypes.ADVERTISER>;
type ProviderParser (line 184) | interface ProviderParser {
FILE: src/main/providers/types/Events.ts
type ProviderCommands (line 3) | enum ProviderCommands {
type ProviderEvents (line 14) | enum ProviderEvents {
type CheckingDataRequests (line 25) | type CheckingDataRequests = Record<string, never>;
type DataRequestActionRequired (line 27) | interface DataRequestActionRequired {
type DataRequestCompleted (line 35) | interface DataRequestCompleted extends DataRequestActionRequired {
type DataRequestDispatched (line 39) | type DataRequestDispatched = DataRequestActionRequired;
type UpdateComplete (line 41) | interface UpdateComplete {
type AccountCreated (line 49) | interface AccountCreated {
type AccountDeleted (line 56) | type AccountDeleted = AccountCreated;
type ProvidersReady (line 58) | type ProvidersReady = Record<string, never>;
FILE: src/main/providers/types/Provider.ts
method constructor (line 39) | constructor(windowKey: string, accountName?: string) {
type DataRequestProvider (line 50) | interface DataRequestProvider extends Provider {
method setEmailClient (line 87) | setEmailClient(email: EmailClient): void {
method setUrl (line 115) | setUrl(url: string): void {
type ProviderUnion (line 124) | type ProviderUnion = typeof DataRequestProvider | typeof Provider | type...
type UninstatiatedProvider (line 125) | type UninstatiatedProvider = new (windowKey: string, accountName?: strin...
FILE: src/main/providers/types/index.ts
type ProviderFile (line 1) | interface ProviderFile {
type DataRequestStatus (line 6) | interface DataRequestStatus {
type InitialisedAccount (line 17) | interface InitialisedAccount {
type ProviderUpdateType (line 32) | enum ProviderUpdateType {
type InitOptionalParameters (line 37) | type InitOptionalParameters = {
FILE: src/main/store.ts
class KeyStore (line 10) | class KeyStore {
method get (line 13) | static get(account: string) {
method set (line 17) | static set(account: string, password: string) {
method delete (line 21) | static delete(account: string) {
FILE: src/typings/redux-persist.d.ts
type PersistConfig (line 4) | interface PersistConfig extends BasePersistConfig {
FILE: test/utilities/getRandomNode.ts
function getRandomNode (line 6) | async function getRandomNode(locator: Locator): Promise<Locator> {
FILE: test/utilities/getRoute.ts
function getRoute (line 6) | function getRoute(page: Page): string | null {
Condensed preview — 184 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (569K chars).
[
{
"path": ".eslintrc.json",
"chars": 1390,
"preview": "{\n \"env\": {\n \"browser\": true,\n \"es6\": true,\n \"node\": true\n },\n \"extends\": [\n \"eslin"
},
{
"path": ".gitbook.yaml",
"chars": 14,
"preview": "root: ./docs/\n"
},
{
"path": ".github/workflows/build.yml",
"chars": 5346,
"preview": "# yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json\nname: Build\n\non: [push]\n\njobs:\n lint:"
},
{
"path": ".github/workflows/codeql-analysis.yml",
"chars": 2578,
"preview": "# For most projects, this workflow file will not need changing; you simply need\n# to commit it to your repository.\n#\n# Y"
},
{
"path": ".github/workflows/electronegativity.yml",
"chars": 401,
"preview": "name: \"Electronegativity\"\n\non: \n push:\n \njobs:\n build_job:\n runs-on: ubuntu-latest\n steps:\n - uses: acti"
},
{
"path": ".github/workflows/release-notes.yml",
"chars": 572,
"preview": "name: Release Notes\n\non:\n release:\n types:\n - created\n\njobs:\n create_notes:\n runs-on: ubuntu-latest\n\n step"
},
{
"path": ".gitignore",
"chars": 1156,
"preview": "# Logs\nlogs\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\nlerna-debug.log*\n\n# Diagnostic reports (https://nodejs."
},
{
"path": ".husky/pre-commit",
"chars": 58,
"preview": "#!/bin/sh\n. \"$(dirname \"$0\")/_/husky.sh\"\n\n# empty for now\n"
},
{
"path": ".husky/pre-push",
"chars": 306,
"preview": "#!/bin/sh\n. \"$(dirname \"$0\")/_/husky.sh\"\n\n# Check if there are any outstanding changes\n# NOTE: This is necessary as husk"
},
{
"path": ".npmrc",
"chars": 21,
"preview": "legacy-peer-deps=true"
},
{
"path": ".vscode/debug-launcher.sh",
"chars": 204,
"preview": "#!/usr/bin/env bash\nDIR=\"$( cd \"$( dirname \"${BASH_SOURCE[0]}\" )\" && pwd )\"\n\nARGS=$@\nARGS=${ARGS// /\\~ \\~}\n\nnode \"$DIR/."
},
{
"path": ".vscode/launch.json",
"chars": 986,
"preview": "{\n // Use IntelliSense to learn about possible attributes.\n // Hover to view descriptions of existing attributes.\n"
},
{
"path": ".vscode/settings.json",
"chars": 29,
"preview": "{\n \"eslint.enable\": true\n}"
},
{
"path": "CODE_OF_CONDUCT.md",
"chars": 3347,
"preview": "# Contributor Covenant Code of Conduct\n\n## Our Pledge\n\nIn the interest of fostering an open and welcoming environment, w"
},
{
"path": "CONTRIBUTING.md",
"chars": 812,
"preview": "# Contributing to Aeon\nAeon welcomes all contributions to it warmly and with open arms. \n\n## GitHub Issues\nGitHub issues"
},
{
"path": "LICENSE",
"chars": 13699,
"preview": "European Union Public Licence\nV. 1.2\n\nEUPL © the European Union 2007, 2016\n\nThis European Union Public Licence (the ‘EUP"
},
{
"path": "README.md",
"chars": 28264,
"preview": "<a href=\"https://aeon.technology\"></a>\n\n<p align=\"center\">\n <"
},
{
"path": "SECURITY.md",
"chars": 3875,
"preview": "# Security Policy\n\n## Supported Versions\n\nAeon is currently in open beta. Until a v1 is released, only the latest versio"
},
{
"path": "data/.gitignore",
"chars": 13,
"preview": "*\n!.gitignore"
},
{
"path": "docs/README.md",
"chars": 1267,
"preview": "# Aeon Documentation\n\n.png>)\n\nWelcome to the Aeon documentation, which attemp"
},
{
"path": "docs/SUMMARY.md",
"chars": 417,
"preview": "# Table of contents\n\n* [Aeon Documentation](README.md)\n\n## Using Aeon\n\n* [Installation](using-aeon/installation.md)\n* [G"
},
{
"path": "docs/extending-aeon/architecture/README.md",
"chars": 3442,
"preview": "# Architecture\n\nAeon is built to reach out to various platforms, gather data, store it and then report it to the user. T"
},
{
"path": "docs/extending-aeon/architecture/providers.md",
"chars": 17588,
"preview": "# Providers\n\nThe first point of Aeon is making sure data is retrieved from a series of sources. These sources are abstra"
},
{
"path": "docs/extending-aeon/local-development.md",
"chars": 5729,
"preview": "# Local Development\n\nAs Aeon is a desktop app, yout development happens locally as well. In this guide, we'll go step by"
},
{
"path": "docs/extending-aeon/reporting-issues.md",
"chars": 784,
"preview": "# Reporting Issues\n\nAeon is still a work in progress, so you might run into issues while using, building or abusing the "
},
{
"path": "docs/using-aeon/getting-started.md",
"chars": 3336,
"preview": "# Getting Started\n\nNot sure how to get started with Aeon? Let us guide you through a few crucial steps to get started wi"
},
{
"path": "docs/using-aeon/installation.md",
"chars": 1041,
"preview": "# Installation\n\nInstalling Aeon is really easy: [find the latest release on GitHub](https://github.com/leinelissen/aeon/"
},
{
"path": "entitlements.plist",
"chars": 308,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/P"
},
{
"path": "forge.config.js",
"chars": 4600,
"preview": "const package = require('./package.json');\nconst hash = require('child_process')\n .execSync('git rev-parse --short HEAD"
},
{
"path": "package.json",
"chars": 4642,
"preview": "{\n \"name\": \"aeon\",\n \"productName\": \"Aeon\",\n \"version\": \"0.2.22\",\n \"description\": \"Online identity versioning"
},
{
"path": "playwright.config.ts",
"chars": 342,
"preview": "import { PlaywrightTestConfig } from '@playwright/test';\nimport path from 'path';\n\nconst config: PlaywrightTestConfig = "
},
{
"path": "renovate.json",
"chars": 142,
"preview": "{\n \"extends\": [\n \"config:base\",\n \"group:allNonMajor\",\n \"schedule:earlyMondays\"\n ],\n \"git-submodules\": {\n "
},
{
"path": "scripts/prepareNodegit.js",
"chars": 863,
"preview": "const path = require('path');\nconst fs = require('fs');\n\nconst debugPath = path.join(__dirname, '..', 'node_modules', 'n"
},
{
"path": "scripts/setupMacOSCertificates.sh",
"chars": 730,
"preview": "#!/usr/bin/env sh\n\n# Based on https://github.com/electron/fiddle/blob/master/tools/add-macos-cert.sh\n\nKEY_CHAIN=build.ke"
},
{
"path": "src/app/assets/open-data-rights.ts",
"chars": 1331,
"preview": "import { IconDefinition, IconName } from '@fortawesome/fontawesome-svg-core';\n\nconst faOpenDataRights: IconDefinition = "
},
{
"path": "src/app/components/App.tsx",
"chars": 1729,
"preview": "import React, { Component } from 'react';\nimport styled, { StyleSheetManager } from 'styled-components';\nimport { HashRo"
},
{
"path": "src/app/components/Button.tsx",
"chars": 3901,
"preview": "import React, { CSSProperties, HTMLAttributes, PropsWithChildren } from 'react';\nimport styled, { css } from 'styled-com"
},
{
"path": "src/app/components/Code.tsx",
"chars": 1007,
"preview": "import styled, { css } from 'styled-components';\n\nconst Code = styled.div<{ removed?: boolean; added?: boolean; updated?"
},
{
"path": "src/app/components/IconBadge.tsx",
"chars": 1167,
"preview": "import React, { PropsWithChildren } from 'react';\nimport { FontAwesomeIcon } from '@fortawesome/react-fontawesome';\nimpo"
},
{
"path": "src/app/components/Input.tsx",
"chars": 3001,
"preview": "import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';\nimport { faChevronDown } from '@fortawesome/free-solid"
},
{
"path": "src/app/components/Loading.tsx",
"chars": 1240,
"preview": "import React from 'react';\nimport styled from 'styled-components';\n\nconst LoadingContainer = styled.div`\n display: fl"
},
{
"path": "src/app/components/Menu.tsx",
"chars": 4903,
"preview": "import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';\nimport { faProjectDiagram, faClock, faCog, faUser, faT"
},
{
"path": "src/app/components/Modal/Menu.tsx",
"chars": 2026,
"preview": "import React, { useState } from 'react';\nimport styled from 'styled-components';\nimport { SimpleButton } from '../Button"
},
{
"path": "src/app/components/Modal/index.tsx",
"chars": 4870,
"preview": "import React, { Component, useRef, useEffect, PropsWithChildren, HTMLAttributes, CSSProperties } from 'react';\nimport { "
},
{
"path": "src/app/components/NoData.tsx",
"chars": 1248,
"preview": "import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';\nimport { faPlus } from '@fortawesome/free-solid-svg-ic"
},
{
"path": "src/app/components/PanelGrid.tsx",
"chars": 5585,
"preview": "import React, { PropsWithChildren } from 'react';\nimport { IconProp } from '@fortawesome/fontawesome-svg-core';\nimport {"
},
{
"path": "src/app/components/RightSideOverlay.tsx",
"chars": 3522,
"preview": "import React, { PropsWithChildren } from 'react';\nimport { GhostButton } from 'app/components/Button';\nimport styled, { "
},
{
"path": "src/app/components/Timestamp.tsx",
"chars": 823,
"preview": "import { formatDistanceToNow } from 'date-fns';\nimport React, { useMemo } from 'react';\nimport Tooltip from './Tooltip';"
},
{
"path": "src/app/components/Tooltip.tsx",
"chars": 3908,
"preview": "import { Placement } from '@popperjs/core';\nimport { Shadow } from 'app/styles/snippets';\nimport React, { PropsWithChild"
},
{
"path": "src/app/components/Tour/index.tsx",
"chars": 1694,
"preview": "import React, { PropsWithChildren } from 'react';\nimport { TourProvider } from '@reactour/tour';\nimport steps, { TourKey"
},
{
"path": "src/app/components/Tour/steps.tsx",
"chars": 8112,
"preview": "import GraphExplainer from 'app/screens/Graph/explainer';\nimport React, { useEffect } from 'react';\nimport { StepType } "
},
{
"path": "src/app/components/Tour/useTour.tsx",
"chars": 1079,
"preview": "import { State, useAppDispatch } from 'app/store';\nimport { completeTour } from 'app/store/onboarding/actions';\nimport {"
},
{
"path": "src/app/components/Typography.tsx",
"chars": 1549,
"preview": "import styled, { css } from 'styled-components';\n\nexport interface BaseTextProps {\n lines?: number;\n}\n\nexport const B"
},
{
"path": "src/app/components/Utility.tsx",
"chars": 1347,
"preview": "import styled, { css } from 'styled-components';\n\nexport const Margin = styled.div`\n padding: 32px;\n`;\n\nexport const "
},
{
"path": "src/app/index.ejs",
"chars": 1234,
"preview": "<!DOCTYPE html>\n<html>\n <head>\n <meta charset=\"UTF-8\">\n <title>Aeon</title>\n <style type=\"text/c"
},
{
"path": "src/app/index.tsx",
"chars": 551,
"preview": "import './polyfill';\nimport React from 'react';\nimport { render } from 'react-dom';\nimport App from './components/App';\n"
},
{
"path": "src/app/polyfill.ts",
"chars": 55,
"preview": "/* eslint-disable */\n// @ts-ignore\nglobal = globalThis;"
},
{
"path": "src/app/preload.ts",
"chars": 2408,
"preview": "import {\n contextBridge,\n ipcRenderer,\n shell,\n} from 'electron';\nimport ElectronStore from 'electron-store';\ni"
},
{
"path": "src/app/screens/Accounts/components/AccountOverlay.tsx",
"chars": 9033,
"preview": "import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';\nimport { faBell, faCheck, faClock, faLink, faPlus, faQ"
},
{
"path": "src/app/screens/Accounts/components/EmailProvider.tsx",
"chars": 5756,
"preview": "import { Dropdown, Label } from 'app/components/Input';\nimport { Margin, MarginSmall, PullCenter } from 'app/components/"
},
{
"path": "src/app/screens/Accounts/components/NewAccountModal.tsx",
"chars": 8523,
"preview": "import React, { PropsWithChildren, useCallback, useEffect, useState } from 'react';\nimport { faPlus } from '@fortawesome"
},
{
"path": "src/app/screens/Accounts/getDescription.ts",
"chars": 559,
"preview": "import { formatDistanceToNow } from 'date-fns';\nimport { DataRequestStatus } from 'main/providers/types';\n\n/**\n * A help"
},
{
"path": "src/app/screens/Accounts/index.tsx",
"chars": 4188,
"preview": "import React, { useCallback } from 'react';\nimport { Category, List, NavigatableListEntry, PanelBottomButtons, PanelGrid"
},
{
"path": "src/app/screens/Data/components/DatumOverlay.tsx",
"chars": 7308,
"preview": "import React, { memo, useCallback } from 'react';\nimport { GhostButton } from 'app/components/Button';\nimport { Provided"
},
{
"path": "src/app/screens/Data/index.tsx",
"chars": 2876,
"preview": "import React from 'react';\nimport { ProvidedDataTypes } from 'main/providers/types/Data';\nimport Loading from 'app/compo"
},
{
"path": "src/app/screens/Data/styles.tsx",
"chars": 2865,
"preview": "import React, { HTMLAttributes } from 'react';\nimport { ProvidedDataTypes, ProviderDatum } from 'main/providers/types/Da"
},
{
"path": "src/app/screens/Data/types.tsx",
"chars": 223,
"preview": "import { ProvidedDataTypes, ProviderDatum } from 'main/providers/types/Data';\n\nexport type GroupedData = { [key: string"
},
{
"path": "src/app/screens/Erasure/Emails.tsx",
"chars": 2643,
"preview": "import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';\nimport { faEnvelope } from '@fortawesome/free-solid-sv"
},
{
"path": "src/app/screens/Erasure/generateEmail.ts",
"chars": 1396,
"preview": "import DataType from 'app/utilities/DataType';\nimport { ProviderDatum } from 'main/providers/types/Data';\n\n/**\n * Genera"
},
{
"path": "src/app/screens/Erasure/index.tsx",
"chars": 4243,
"preview": "import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';\nimport { faTrash } from '@fortawesome/free-solid-svg-i"
},
{
"path": "src/app/screens/Graph/calculateGraph.ts",
"chars": 2876,
"preview": "import DataType from 'app/utilities/DataType';\nimport { EdgeDefinition, ElementsDefinition, NodeDefinition } from 'cytos"
},
{
"path": "src/app/screens/Graph/explainer.tsx",
"chars": 1551,
"preview": "import React from 'react';\nimport styled from 'styled-components';\n\nconst Provider = styled.div`\n width: 25px;\n he"
},
{
"path": "src/app/screens/Graph/index.tsx",
"chars": 6583,
"preview": "\nimport React, { useCallback, useEffect, useRef, useState } from 'react';\nimport { useNavigate, useParams } from 'react-"
},
{
"path": "src/app/screens/Graph/renderNode.ts",
"chars": 1478,
"preview": "import { IconDefinition } from '@fortawesome/fontawesome-svg-core';\nimport DataType from 'app/utilities/DataType';\nimpor"
},
{
"path": "src/app/screens/Graph/style.ts",
"chars": 4418,
"preview": "import { GhostButton } from 'app/components/Button';\nimport { Shadow } from 'app/styles/snippets';\nimport { Stylesheet }"
},
{
"path": "src/app/screens/Onboarding/index.tsx",
"chars": 2242,
"preview": "import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';\nimport { faPlus } from '@fortawesome/free-solid-svg-ic"
},
{
"path": "src/app/screens/Settings/email/components/ImapInitialiser.tsx",
"chars": 3652,
"preview": "import React, { ChangeEvent, useCallback, useState } from 'react';\nimport { Label, TextInput } from 'app/components/Inpu"
},
{
"path": "src/app/screens/Settings/email/components/NewAccountModal.tsx",
"chars": 4339,
"preview": "import { faGoogle, faMicrosoft } from '@fortawesome/free-brands-svg-icons';\nimport { FontAwesomeIcon } from '@fortawesom"
},
{
"path": "src/app/screens/Settings/email/index.tsx",
"chars": 3608,
"preview": "import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';\nimport { faTrash } from '@fortawesome/free-solid-svg-i"
},
{
"path": "src/app/screens/Settings/index.tsx",
"chars": 1289,
"preview": "import React from 'react';\nimport { Category, List, NavigatableListEntry, PanelGrid } from 'app/components/PanelGrid';\ni"
},
{
"path": "src/app/screens/Timeline/components/Commit.tsx",
"chars": 2861,
"preview": "\nimport styled, { css } from 'styled-components';\nimport React, { Component, MouseEventHandler } from 'react';\nimport { "
},
{
"path": "src/app/screens/Timeline/components/Diff.tsx",
"chars": 8308,
"preview": "import React, { PureComponent } from 'react';\nimport { DiffResult, ExtractedDataDiff, Commit } from 'main/lib/repository"
},
{
"path": "src/app/screens/Timeline/index.tsx",
"chars": 4914,
"preview": "import React, { Component } from 'react';\nimport Repository from 'app/utilities/Repository';\nimport styled from 'styled-"
},
{
"path": "src/app/screens/index.tsx",
"chars": 2295,
"preview": "import React from 'react';\nimport { Route, Routes } from 'react-router-dom';\nimport Menu, { ContentContainer, PageContai"
},
{
"path": "src/app/screens/types.ts",
"chars": 401,
"preview": "import { ProvidedDataTypes } from 'main/providers/types/Data';\n\nexport interface RouteProps {\n timeline: {\n co"
},
{
"path": "src/app/store/accounts/actions.ts",
"chars": 1211,
"preview": "import { createAction, createAsyncThunk } from '@reduxjs/toolkit';\nimport Providers from 'app/utilities/Providers';\nimpo"
},
{
"path": "src/app/store/accounts/index.ts",
"chars": 2573,
"preview": "import { createSlice } from '@reduxjs/toolkit';\nimport { InitialisedAccount } from 'main/providers/types';\nimport { crea"
},
{
"path": "src/app/store/accounts/selectors.ts",
"chars": 3546,
"preview": "import Providers from 'app/utilities/Providers';\nimport { InitialisedAccount } from 'main/providers/types';\nimport { Dat"
},
{
"path": "src/app/store/accounts/types.ts",
"chars": 220,
"preview": "import { DataRequestStatus } from 'main/providers/types';\n\nexport interface EmailProvider {\n organisation: string;\n "
},
{
"path": "src/app/store/data/actions.ts",
"chars": 395,
"preview": "import { createAction, createAsyncThunk } from '@reduxjs/toolkit';\nimport Repository from 'app/utilities/Repository';\n\ne"
},
{
"path": "src/app/store/data/index.ts",
"chars": 3110,
"preview": "import { createSlice } from '@reduxjs/toolkit';\nimport { ProvidedDataTypes, ProviderDatum } from 'main/providers/types/D"
},
{
"path": "src/app/store/data/selectors.ts",
"chars": 877,
"preview": "import Repository from 'app/utilities/Repository';\nimport { useCallback, useEffect } from 'react';\nimport { useAppDispat"
},
{
"path": "src/app/store/email/actions.ts",
"chars": 626,
"preview": "import { createAsyncThunk } from '@reduxjs/toolkit';\nimport Email from 'app/utilities/Email';\n\nexport const fetchEmailAc"
},
{
"path": "src/app/store/email/index.ts",
"chars": 1193,
"preview": "import { createSlice } from '@reduxjs/toolkit';\nimport { createEmailAccount, fetchEmailAccounts, fetchEmailClients } fro"
},
{
"path": "src/app/store/email/selectors.ts",
"chars": 956,
"preview": "import Email from 'app/utilities/Email';\nimport { useCallback, useEffect } from 'react';\nimport { useAppDispatch } from "
},
{
"path": "src/app/store/index.ts",
"chars": 2071,
"preview": "import { configureStore } from '@reduxjs/toolkit';\nimport { combineReducers } from 'redux';\nimport { \n createMigrate,"
},
{
"path": "src/app/store/migrations.ts",
"chars": 1300,
"preview": "import { MigrationManifest } from 'redux-persist';\n\nconst migrations: MigrationManifest = {\n /**\n * Migrate from "
},
{
"path": "src/app/store/new-commits/actions.ts",
"chars": 99,
"preview": "import { newCommitsSlice } from '.';\n\nexport const { add: addNewCommit } = newCommitsSlice.actions;"
},
{
"path": "src/app/store/new-commits/index.ts",
"chars": 523,
"preview": "import { createSlice, PayloadAction } from '@reduxjs/toolkit';\nimport { Commit, ExtractedDataDiff } from 'main/lib/repos"
},
{
"path": "src/app/store/onboarding/actions.ts",
"chars": 167,
"preview": "import { onboardingSlice } from '.';\n\nexport const { complete: completeOnboarding } = onboardingSlice.actions;\nexport co"
},
{
"path": "src/app/store/onboarding/index.ts",
"chars": 705,
"preview": "import { createSlice, PayloadAction } from '@reduxjs/toolkit';\nimport { TourKeys } from 'app/components/Tour/steps';\n\nin"
},
{
"path": "src/app/store/persist.ts",
"chars": 448,
"preview": "import { Storage } from 'redux-persist';\n\nexport default function ElectronStorage(): Storage {\n return {\n get"
},
{
"path": "src/app/store/telemetry/actions.ts",
"chars": 100,
"preview": "import { telemetrySlice } from '.';\n\nexport const { log: addTelemetryLog } = telemetrySlice.actions;"
},
{
"path": "src/app/store/telemetry/index.ts",
"chars": 462,
"preview": "import { createSlice, PayloadAction } from '@reduxjs/toolkit';\n\nexport interface Event {\n event: string;\n element:"
},
{
"path": "src/app/styles/global.css",
"chars": 826,
"preview": "html {\n min-height: 100%;\n}\n\nbody {\n min-height: 100vh;\n font-family: var(--font-body), -apple-system, BlinkMac"
},
{
"path": "src/app/styles/index.ts",
"chars": 157,
"preview": "import '@fontsource/ibm-plex-mono';\nimport '@fontsource/ibm-plex-sans';\nimport '@fontsource/inter/variable.css';\nimport "
},
{
"path": "src/app/styles/snippets.ts",
"chars": 669,
"preview": "import { css } from 'styled-components';\n\nexport const LargeShadow = css`\n box-shadow: 0 1px 2px rgba(0,0,0,0.07), \n "
},
{
"path": "src/app/styles/theme.css",
"chars": 4218,
"preview": ":root {\n /* Blue */\n --color-blue-50: hsl(223, 100%, 97%);\n --color-blue-100: hsl(223, 100%, 95%);\n --color-"
},
{
"path": "src/app/utilities/DataType.tsx",
"chars": 15229,
"preview": "import React from 'react';\nimport {\n ProvidedDataTypes,\n ProviderDatum,\n Address,\n Photo,\n PrivacySetting"
},
{
"path": "src/app/utilities/Email.ts",
"chars": 2023,
"preview": "import { faGoogle, faMicrosoft, IconDefinition } from '@fortawesome/free-brands-svg-icons';\nimport { faEnvelope } from '"
},
{
"path": "src/app/utilities/Providers.ts",
"chars": 2933,
"preview": "import { InitOptionalParameters, InitialisedAccount } from 'main/providers/types';\nimport { ProviderCommands, ProviderEv"
},
{
"path": "src/app/utilities/Repository.ts",
"chars": 1328,
"preview": "import { DiffResult, RepositoryCommands, RepositoryArguments, RepositoryEvents, Commit } from 'main/lib/repository/types"
},
{
"path": "src/app/utilities/convertMetaToObject.ts",
"chars": 1755,
"preview": "import { ProviderUpdateType } from 'main/providers/types';\n\ninterface CommitMetadata {\n title: string;\n provider?:"
},
{
"path": "src/app/utilities/env.ts",
"chars": 334,
"preview": "// Retrieve environment sychronously from main\nconst env = window.api.env;\n\n// Destructure all individual options\nexport"
},
{
"path": "src/app/utilities/isValidUrl.ts",
"chars": 326,
"preview": "/**\n * Determine if the input is a valid URL\n * https://stackoverflow.com/questions/5717093/check-if-a-javascript-string"
},
{
"path": "src/app/utilities/usePrevious.ts",
"chars": 657,
"preview": "import { useRef, useEffect } from 'react';\n\n/**\n * This will return the previous value of an input supplied to the hook\n"
},
{
"path": "src/main/email-client/bridge.ts",
"chars": 2104,
"preview": "import { ipcMain, IpcMainInvokeEvent } from 'electron';\nimport logger from 'main/lib/logger';\nimport WindowStore from 'm"
},
{
"path": "src/main/email-client/gmail/index.ts",
"chars": 3529,
"preview": "import authenticateGmailUser, { refreshGmailTokens } from './oauth';\nimport { GmailTokenResponse } from './types';\nimpor"
},
{
"path": "src/main/email-client/gmail/oauth.ts",
"chars": 6395,
"preview": "import http from 'http';\nimport fetch from 'node-fetch';\nimport { URLSearchParams } from 'url';\nimport { Socket } from '"
},
{
"path": "src/main/email-client/gmail/types.ts",
"chars": 159,
"preview": "export interface GmailTokenResponse {\n access_token: string;\n expires_in: number;\n refresh_token: string;\n s"
},
{
"path": "src/main/email-client/imap/index.ts",
"chars": 4046,
"preview": "import { ImapFlow } from 'imapflow';\nimport { ParsedMail, simpleParser } from 'mailparser';\nimport logger from 'main/lib"
},
{
"path": "src/main/email-client/index.ts",
"chars": 3957,
"preview": "import { EventEmitter2 } from 'eventemitter2';\nimport logger from 'main/lib/logger';\nimport PersistedMap from 'main/lib/"
},
{
"path": "src/main/email-client/outlook/index.ts",
"chars": 3146,
"preview": "import { formatISO } from 'date-fns';\nimport { ParsedMail, simpleParser } from 'mailparser';\nimport { OauthEmailClient }"
},
{
"path": "src/main/email-client/outlook/oauth.ts",
"chars": 5690,
"preview": "import { withSecureWindow } from 'main/lib/create-secure-window';\nimport crypto from 'crypto';\nimport { generateVerifier"
},
{
"path": "src/main/email-client/types.ts",
"chars": 2001,
"preview": "import type { ParsedMail } from 'mailparser';\nimport type { Options } from 'nodemailer/lib/mailer';\n\nexport interface Em"
},
{
"path": "src/main/index.ts",
"chars": 3245,
"preview": "import 'v8-compile-cache';\n// eslint-disable-next-line\nrequire('source-map-support').install();\n\nimport './lib/map-map';"
},
{
"path": "src/main/initialise.ts",
"chars": 1702,
"preview": "import { autoUpdater } from 'electron';\nimport EmailManager from './email-client';\nimport EmailBridge from './email-clie"
},
{
"path": "src/main/lib/app-path.ts",
"chars": 199,
"preview": "import { app } from 'electron';\n\n// This is the path our files should be stored under\nexport const APP_DATA_PATH = proce"
},
{
"path": "src/main/lib/constants.ts",
"chars": 3498,
"preview": "import path from 'path';\nimport yargs from 'yargs';\nimport { app, ipcMain } from 'electron';\nimport { hideBin } from 'ya"
},
{
"path": "src/main/lib/create-secure-window.ts",
"chars": 4787,
"preview": "import { BrowserWindow } from 'electron';\nimport { URL } from 'url';\nimport crypto from 'crypto';\nimport logger from './"
},
{
"path": "src/main/lib/crypto-fs/index.ts",
"chars": 2106,
"preview": "import crypto from 'crypto';\nimport fs from 'fs';\n\nconst ALGORITHM = 'aes-256-cbc';\n\nclass CryptoFs {\n private key?: "
},
{
"path": "src/main/lib/logger.ts",
"chars": 2570,
"preview": "import { existsSync, mkdirSync } from 'fs';\nimport path from 'path';\nimport { Container, Logger as WinstonLogger, format"
},
{
"path": "src/main/lib/map-map.ts",
"chars": 536,
"preview": "// eslint-disable-next-line @typescript-eslint/no-unused-vars\ninterface Map<K, V> {\n // eslint-disable-next-line\n "
},
{
"path": "src/main/lib/map-object-to-key-value.ts",
"chars": 842,
"preview": "import { ProviderDatum } from 'main/providers/types/Data';\n\n/**\n * Maps an object that is written as { key: value } to a"
},
{
"path": "src/main/lib/notifications/index.ts",
"chars": 1164,
"preview": "import { NotificationTypes } from './types';\nimport WindowStore from 'main/lib/window-store';\n\nconst channelName = 'noti"
},
{
"path": "src/main/lib/notifications/types.ts",
"chars": 90,
"preview": "export enum NotificationTypes {\n SUCCESS,\n INFO,\n LOADING,\n WARN,\n ERROR,\n}"
},
{
"path": "src/main/lib/oauth.ts",
"chars": 6032,
"preview": "import crypto from 'crypto';\nimport { EmailClient } from 'main/email-client/types';\nimport { KeyStore } from 'main/store"
},
{
"path": "src/main/lib/object-to-map.ts",
"chars": 0,
"preview": ""
},
{
"path": "src/main/lib/persisted-map.ts",
"chars": 1091,
"preview": "/**\n * A class that accepts a function that is called each time the map is updated,\n * as to make persisting it (ie. in "
},
{
"path": "src/main/lib/protocol-handler.ts",
"chars": 3242,
"preview": "import { app } from 'electron';\nimport path from 'path';\n\n/** The registered callback */\ntype ProtocolCallback = (url: "
},
{
"path": "src/main/lib/repository/bridge.ts",
"chars": 1889,
"preview": "import Repository from '.';\nimport { RepositoryCommands, RepositoryEvents } from './types';\nimport { ipcMain, IpcMainInv"
},
{
"path": "src/main/lib/repository/index.ts",
"chars": 11302,
"preview": "import path from 'path';\nimport { EventEmitter } from 'events';\nimport {\n TreeEntry,\n Repository as NodeGitReposit"
},
{
"path": "src/main/lib/repository/types.ts",
"chars": 1843,
"preview": "import { ProviderDatum } from 'main/providers/types/Data';\n\nexport enum DiffType {\n // An object DiffType is the diff"
},
{
"path": "src/main/lib/repository/utilities/diff-map.ts",
"chars": 856,
"preview": "import generateDiff from './generate-diff';\nimport { DiffResult } from '../types';\nimport { TreeEntry } from 'nodegit';\n"
},
{
"path": "src/main/lib/repository/utilities/generate-diff.ts",
"chars": 4121,
"preview": "import { DiffType, DiffResult } from '../types';\nimport generateParsedCommit from './generate-parsed-commit';\nimport { P"
},
{
"path": "src/main/lib/repository/utilities/generate-parsed-commit.ts",
"chars": 2679,
"preview": "import path from 'path';\nimport { getParserByFileName } from 'main/providers/parsers';\nimport parseSchema from './parse-"
},
{
"path": "src/main/lib/repository/utilities/parse-csv.ts",
"chars": 2205,
"preview": "import { Blob } from 'nodegit';\nimport { parse } from '@fast-csv/parse';\nimport { Readable } from 'stream-chain';\n\nconst"
},
{
"path": "src/main/lib/repository/utilities/parse-open-data-rights.ts",
"chars": 713,
"preview": "import { ProviderDatum } from 'main/providers/types/Data';\n\nexport type OpenDataRightsDatum = Pick<\nProviderDatum<unknow"
},
{
"path": "src/main/lib/repository/utilities/parse-schema.ts",
"chars": 7547,
"preview": "import { compile, TreeInterpreter } from '@metrichor/jmespath';\nimport { ExpressionNodeTree } from '@metrichor/jmespath/"
},
{
"path": "src/main/lib/unwrap-provider-source.ts",
"chars": 366,
"preview": "import path from 'path';\n\n/**\n * Unwrap a provider source that is either a string or a string array. As per\n * the provi"
},
{
"path": "src/main/lib/window-store.ts",
"chars": 595,
"preview": "import { BrowserWindow } from 'electron';\n\nclass WindowStore {\n private static instance: WindowStore;\n\n private _w"
},
{
"path": "src/main/providers/bridge.ts",
"chars": 3385,
"preview": "import Providers, { providers as availableProviders } from '.';\nimport { ProviderCommands, ProviderEvents } from './typ"
},
{
"path": "src/main/providers/facebook/index.ts",
"chars": 8970,
"preview": "import { app } from 'electron';\nimport { withSecureWindow } from 'main/lib/create-secure-window';\nimport { ProviderFile "
},
{
"path": "src/main/providers/facebook/parser.ts",
"chars": 9049,
"preview": "import { EducationExperience, Employment, EventResponse, MobileDevice, OffSiteActivity, ProvidedDataTypes, ProviderParse"
},
{
"path": "src/main/providers/index.ts",
"chars": 17240,
"preview": "import { differenceInDays } from 'date-fns';\nimport path from 'path';\nimport crypto from 'crypto';\nimport { EventEmitter"
},
{
"path": "src/main/providers/instagram/index.ts",
"chars": 10997,
"preview": "import { ProviderFile } from '../types';\nimport { EmailDataRequestProvider } from '../types/Provider';\nimport crypto fro"
},
{
"path": "src/main/providers/instagram/parser.ts",
"chars": 8522,
"preview": "import {\n ProvidedDataTypes,\n ProviderParser,\n Follower,\n AccountFollowing,\n Photo,\n PrivacySetting,\n "
},
{
"path": "src/main/providers/instagram/urls.json",
"chars": 1818,
"preview": "[\n \"https://www.instagram.com/accounts/access_tool/account_privacy_changes?__a=1\",\n \"https://www.instagram.com/acc"
},
{
"path": "src/main/providers/linkedin/index.ts",
"chars": 8509,
"preview": "import { app } from 'electron';\nimport { withSecureWindow } from 'main/lib/create-secure-window';\nimport { ProviderFile "
},
{
"path": "src/main/providers/linkedin/parser.ts",
"chars": 1776,
"preview": "import {\n ProviderParser,\n ProvidedDataTypes,\n ProviderDatum,\n} from '../types/Data';\n\n/**\n * Will transform a "
},
{
"path": "src/main/providers/open-data-rights/index.ts",
"chars": 4430,
"preview": "import { ProviderFile } from '../types';\nimport { OpenDataRightsProvider } from '../types/Provider';\nimport fetch, { Req"
},
{
"path": "src/main/providers/parsers.ts",
"chars": 2897,
"preview": "import unwrapParserSource from 'main/lib/unwrap-provider-source';\nimport Facebook from './facebook/parser';\nimport Insta"
},
{
"path": "src/main/providers/spotify/index.ts",
"chars": 8814,
"preview": "import path from 'path';\nimport fs from 'fs';\nimport AdmZip from 'adm-zip';\nimport { subHours } from 'date-fns';\nimport "
},
{
"path": "src/main/providers/spotify/parser.ts",
"chars": 3313,
"preview": "import path from 'path';\nimport { Address, PlayedSong, ProvidedDataTypes, ProviderParser } from '../types/Data';\n\ntype S"
},
{
"path": "src/main/providers/types/Data.ts",
"chars": 10402,
"preview": "\nexport enum ProvidedDataTypes {\n /** A email adress */\n EMAIL = 'email',\n /** A first name */\n FIRST_NAME ="
},
{
"path": "src/main/providers/types/Events.ts",
"chars": 1430,
"preview": "import { DataRequestStatus } from '.';\n\nexport enum ProviderCommands {\n UPDATE,\n UPDATE_ALL,\n DISPATCH_DATA_REQ"
},
{
"path": "src/main/providers/types/Provider.ts",
"chars": 5300,
"preview": "import { EmailClient } from 'main/email-client/types';\nimport { ProviderFile } from '.';\n\n/**\n * The base structure for "
},
{
"path": "src/main/providers/types/index.ts",
"chars": 1156,
"preview": "export interface ProviderFile {\n filepath: string;\n data: Buffer | string;\n}\n\nexport interface DataRequestStatus {"
},
{
"path": "src/main/store.ts",
"chars": 672,
"preview": "import ElectronStore from 'electron-store';\nimport Keytar from 'keytar';\nimport { storePath } from './lib/constants';\n\nc"
},
{
"path": "src/main/updates.ts",
"chars": 1869,
"preview": "import { app, autoUpdater, dialog } from 'electron';\nimport { autoUpdates } from './lib/constants';\nimport logger from '"
},
{
"path": "src/typings/cytoscape-fcose.d.ts",
"chars": 135,
"preview": "declare module 'cytoscape-fcose' {\n import type { Ext } from 'cytoscape';\n\n declare const fcose: Ext;\n export d"
},
{
"path": "src/typings/fonts.d.ts",
"chars": 50,
"preview": "declare module '*.woff';\ndeclare module '*.woff2';"
},
{
"path": "src/typings/images.d.ts",
"chars": 23,
"preview": "declare module '*.svg';"
},
{
"path": "src/typings/redux-persist.d.ts",
"chars": 243,
"preview": "import { PersistConfig as BasePersistConfig } from 'redux-persist';\n\ndeclare module 'redux-persist' {\n export interfa"
},
{
"path": "test/.gitignore",
"chars": 19,
"preview": "test-outputs\noutput"
},
{
"path": "test/spec.ts",
"chars": 11038,
"preview": "import { ElectronApplication, _electron as electron, ConsoleMessage, Page, BrowserContext } from 'playwright';\nimport { "
},
{
"path": "test/utilities/getRandomNode.ts",
"chars": 737,
"preview": "import { Locator } from 'playwright';\n\n/**\n * Retrieve a random child from a locator.\n */\nasync function getRandomNode(l"
},
{
"path": "test/utilities/getRoute.ts",
"chars": 444,
"preview": "import { Page } from 'playwright';\n\n/**\n * Retrieve the react-router MemoryRouter route from the location hash\n */\nfunct"
},
{
"path": "tsconfig.json",
"chars": 485,
"preview": "{\n \"compilerOptions\": {\n \"allowJs\": true,\n \"module\": \"commonjs\",\n \"skipLibCheck\": true,\n \"esModuleInterop\":"
},
{
"path": "webpack.main.config.js",
"chars": 933,
"preview": "const [ Dotenv ] = require('./webpack.plugins');\nconst path = require('path');\n\nmodule.exports = {\n /**\n * This is"
},
{
"path": "webpack.plugins.js",
"chars": 702,
"preview": "const Dotenv = require('dotenv-webpack');\nconst CspHtmlWebpackPlugin = require('csp-html-webpack-plugin');\n// const Bund"
},
{
"path": "webpack.renderer.config.js",
"chars": 1219,
"preview": "const path = require('path');\nconst rules = require('./webpack.rules');\nconst plugins = require('./webpack.plugins');\nco"
},
{
"path": "webpack.rules.js",
"chars": 1091,
"preview": "module.exports = [\n // Add support for native node modules\n {\n // We're specifying native_modules in the te"
}
]
// ... and 1 more files (download for full content)
About this extraction
This page contains the full source code of the leinelissen/aeon GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 184 files (529.3 KB), approximately 131.0k tokens, and a symbol index with 417 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.