diff --git a/package.json b/package.json
index 32418e0d3..9a7aa843b 100644
--- a/package.json
+++ b/package.json
@@ -19,6 +19,8 @@
"dev": "yarn compile && concurrently --kill-others \"yarn compile -w\" \"yarn workspace @devhub/web start\" \"yarn workspace @devhub/mobile start\"",
"dev:web": "yarn compile && concurrently --kill-others \"yarn compile -w\" \"yarn workspace @devhub/web start\"",
"dev:mobile": "yarn compile && concurrently --kill-others \"yarn compile -w\" \"yarn workspace @devhub/mobile start\"",
+ "dev:desktop": "yarn compile && concurrently --kill-others \"yarn compile -w\" \"yarn workspace @devhub/desktop start\"",
+ "build:desktop": "yarn compile && concurrently --kill-others \"yarn compile -w\" \"yarn workspace @devhub/desktop build\"",
"format": "yarn workspaces run format",
"lint": "yarn workspaces run lint",
"now": "npx now",
diff --git a/packages/desktop/.gitignore b/packages/desktop/.gitignore
new file mode 100644
index 000000000..dd9baee6c
--- /dev/null
+++ b/packages/desktop/.gitignore
@@ -0,0 +1,15 @@
+.DS_Store
+.env.development.local
+.env.local
+.env.production.local
+.env.test.local
+.history
+.jest
+.vscode
+build
+coverage
+dist
+node_modules
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
diff --git a/packages/desktop/package.json b/packages/desktop/package.json
new file mode 100644
index 000000000..658c16f1e
--- /dev/null
+++ b/packages/desktop/package.json
@@ -0,0 +1,80 @@
+{
+ "name": "@devhub/desktop",
+ "version": "0.0.1",
+ "private": false,
+ "main": "dist/electron.js",
+ "scripts": {
+ "build": "tsc -b && electron-builder -mw",
+ "clean": "rm -rf dist/*",
+ "format": "prettier --write '{.,src/**}/*.{js,jsx,ts,tsx}'",
+ "lint": "tslint -p .",
+ "predeploy": "yarn run build",
+ "start": "rm -rf dist && tsc && electron .",
+ "test": "echo \"Error: no test specified\" && exit 1"
+ },
+ "build": {
+ "productName": "devhub",
+ "appId": "com.devhubapp.devhub",
+ "dmg": {
+ "contents": [
+ {
+ "x": 410,
+ "y": 150,
+ "type": "link",
+ "path": "/Applications"
+ },
+ {
+ "x": 130,
+ "y": 150,
+ "type": "file"
+ }
+ ]
+ },
+ "directories": {
+ "output": "build"
+ },
+ "files": [
+ "dist/**/*",
+ "node_modules/**/*",
+ "package.json"
+ ],
+ "mac": {
+ "icon": "public/static/icns/logo.icns"
+ },
+ "win": {
+ "target": "nsis",
+ "icon": "public/static/icns/logo.ico"
+ },
+ "nsis": {
+ "oneClick": true
+ }
+ },
+ "dependencies": {
+ "@babel/polyfill": "^7.0.0",
+ "@devhub/components": "0.39.2",
+ "react": "^16.7.0-alpha.2",
+ "react-app-polyfill": "^0.1.3",
+ "react-art": "^16.7.0-alpha.2",
+ "react-dom": "^16.7.0-alpha.2",
+ "react-native-web": "^0.9.8",
+ "react-scripts": "2.1.1",
+ "resize-observer-polyfill": "^1.5.0"
+ },
+ "devDependencies": {
+ "electron": "^4.0.0",
+ "electron-builder": "^20.38.3",
+ "tslint": "^5.11.0",
+ "tslint-config-airbnb": "^5.11.1",
+ "tslint-config-prettier": "^1.17.0",
+ "typescript": "^3.2.1"
+ },
+ "resolutions": {
+ "scheduler": "0.12.0-alpha.3"
+ },
+ "browserslist": [
+ ">0.2%",
+ "not dead",
+ "not ie <= 9",
+ "not op_mini all"
+ ]
+}
diff --git a/packages/desktop/public/favicon.ico b/packages/desktop/public/favicon.ico
new file mode 100644
index 000000000..da4c18b30
Binary files /dev/null and b/packages/desktop/public/favicon.ico differ
diff --git a/packages/desktop/public/index.html b/packages/desktop/public/index.html
new file mode 100644
index 000000000..e1c50ddaa
--- /dev/null
+++ b/packages/desktop/public/index.html
@@ -0,0 +1,103 @@
+
+
+
+
+
+
+
+ DevHub - TweetDeck for GitHub
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/packages/desktop/public/manifest.json b/packages/desktop/public/manifest.json
new file mode 100644
index 000000000..68610dc2c
--- /dev/null
+++ b/packages/desktop/public/manifest.json
@@ -0,0 +1,39 @@
+{
+ "name": "DevHub",
+ "short_name": "DevHub",
+ "description": "TweetDeck for GitHub",
+ "lang": "en-US",
+ "start_url": "./?utm_source=web_app_manifest",
+ "display": "standalone",
+ "orientation": "any",
+ "theme_color": "#292c33",
+ "icons": [
+ {
+ "src": "static/media/logo.png",
+ "sizes": "100x100",
+ "type": "image/png"
+ },
+ {
+ "src": "static/media/logo.png",
+ "sizes": "200x200",
+ "type": "image/png"
+ },
+ {
+ "src": "static/media/logo.png",
+ "sizes": "300x300",
+ "type": "image/png"
+ },
+ {
+ "src": "static/media/logo@2x.png",
+ "sizes": "600x600",
+ "type": "image/png"
+ },
+ {
+ "src": "static/media/logo@3x.png",
+ "sizes": "900x900",
+ "type": "image/png"
+ }
+ ],
+ "background_color": "#1c1c1c",
+ "prefer_related_applications": true
+}
\ No newline at end of file
diff --git a/packages/desktop/public/static/css/arrow.css b/packages/desktop/public/static/css/arrow.css
new file mode 100644
index 000000000..7f772a1fd
--- /dev/null
+++ b/packages/desktop/public/static/css/arrow.css
@@ -0,0 +1,14 @@
+body {
+ background: transparent !important;
+ -webkit-app-region: drag !important;
+}
+
+.header-arrow {
+ width: 0;
+ height: 0;
+ border-right: 10px solid transparent;
+ border-bottom: 10px solid #fff;
+ border-left: 10px solid transparent;
+ margin: auto;
+ transition: border-bottom-width .2s ease,border-bottom-color .3s ease;
+}
\ No newline at end of file
diff --git a/packages/desktop/public/static/css/error.css b/packages/desktop/public/static/css/error.css
new file mode 100644
index 000000000..bced294f2
--- /dev/null
+++ b/packages/desktop/public/static/css/error.css
@@ -0,0 +1,25 @@
+* {
+ box-sizing: border-box;
+}
+
+body,
+html {
+ width: 100%;
+ height: 100%;
+ margin: 0;
+ padding: 0;
+}
+
+body {
+ display: flex;
+ align-content: center;
+ align-items: center;
+ background-color: #1c1c1c;
+ text-align: center;
+ color: #fff;
+ font-family: sans-serif;
+}
+
+#error-container {
+ margin: auto;
+}
\ No newline at end of file
diff --git a/packages/desktop/public/static/icns/logo.icns b/packages/desktop/public/static/icns/logo.icns
new file mode 100644
index 000000000..92491eedf
Binary files /dev/null and b/packages/desktop/public/static/icns/logo.icns differ
diff --git a/packages/desktop/public/static/icns/logo.ico b/packages/desktop/public/static/icns/logo.ico
new file mode 100644
index 000000000..cd76e6c02
Binary files /dev/null and b/packages/desktop/public/static/icns/logo.ico differ
diff --git a/packages/desktop/public/static/media/logo-transparent@2x.png b/packages/desktop/public/static/media/logo-transparent@2x.png
new file mode 100644
index 000000000..9aaa6d9d2
Binary files /dev/null and b/packages/desktop/public/static/media/logo-transparent@2x.png differ
diff --git a/packages/desktop/public/static/media/logo.png b/packages/desktop/public/static/media/logo.png
new file mode 100644
index 000000000..71330d5fd
Binary files /dev/null and b/packages/desktop/public/static/media/logo.png differ
diff --git a/packages/desktop/public/static/media/logo@2x.png b/packages/desktop/public/static/media/logo@2x.png
new file mode 100644
index 000000000..17400f328
Binary files /dev/null and b/packages/desktop/public/static/media/logo@2x.png differ
diff --git a/packages/desktop/public/static/media/logo@3x.png b/packages/desktop/public/static/media/logo@3x.png
new file mode 100644
index 000000000..bfcd83a45
Binary files /dev/null and b/packages/desktop/public/static/media/logo@3x.png differ
diff --git a/packages/desktop/src/electron.ts b/packages/desktop/src/electron.ts
new file mode 100644
index 000000000..373234d51
--- /dev/null
+++ b/packages/desktop/src/electron.ts
@@ -0,0 +1,373 @@
+import { constants } from '@devhub/core'
+import {
+ app,
+ BrowserWindow,
+ Menu,
+ nativeImage,
+ shell,
+ TouchBar,
+ Tray,
+} from 'electron'
+import fs from 'fs'
+import path from 'path'
+
+const URL = 'https://devhubapp.com'
+const WIDTH = 350
+const HEIGHT = 450
+
+let mainWindow: Electron.BrowserWindow
+let menubarWindow: Electron.BrowserWindow
+let tray: Electron.Tray
+
+const template: Electron.MenuItemConstructorOptions[] = [
+ {
+ label: 'Edit',
+ submenu: [
+ {
+ label: 'Undo',
+ accelerator: 'CmdOrCtrl+Z',
+ role: 'undo',
+ },
+ {
+ label: 'Redo',
+ accelerator: 'Shift+CmdOrCtrl+Z',
+ role: 'redo',
+ },
+ {
+ type: 'separator',
+ },
+ {
+ label: 'Cut',
+ accelerator: 'CmdOrCtrl+X',
+ role: 'cut',
+ },
+ {
+ label: 'Copy',
+ accelerator: 'CmdOrCtrl+C',
+ role: 'copy',
+ },
+ {
+ label: 'Paste',
+ accelerator: 'CmdOrCtrl+V',
+ role: 'paste',
+ },
+ {
+ label: 'Select All',
+ accelerator: 'CmdOrCtrl+A',
+ role: 'selectall',
+ },
+ ],
+ },
+ {
+ label: 'View',
+ submenu: [
+ {
+ label: 'Reload',
+ accelerator: 'CmdOrCtrl+R',
+ },
+ ],
+ },
+ {
+ label: 'Window',
+ role: 'window',
+ submenu: [
+ {
+ label: 'Minimize',
+ accelerator: 'CmdOrCtrl+M',
+ role: 'minimize',
+ },
+ {
+ label: 'Close',
+ accelerator: 'CmdOrCtrl+W',
+ role: 'close',
+ },
+ ],
+ },
+ {
+ type: 'separator',
+ },
+ {
+ label: 'Bring All to Front',
+ role: 'front',
+ },
+ {
+ label: 'Help',
+ role: 'help',
+ submenu: [
+ {
+ label: 'Learn More',
+ click: () => {
+ shell.openExternal('https://devhubapp.com')
+ },
+ },
+ ],
+ },
+]
+
+if (process.platform === 'darwin') {
+ const name = app.getName()
+ template.unshift({
+ label: name,
+ submenu: [
+ {
+ label: `About ${name}`,
+ role: 'about',
+ },
+ {
+ type: 'separator',
+ },
+ {
+ label: `Hide ${name}`,
+ accelerator: 'Command+H',
+ role: 'hide',
+ },
+ {
+ label: 'Hide Others',
+ accelerator: 'Command+Alt+H',
+ role: 'hideothers',
+ },
+ {
+ type: 'separator',
+ },
+ {
+ label: 'Quit',
+ accelerator: 'Command+Q',
+ click: () => {
+ app.quit()
+ },
+ },
+ ],
+ })
+}
+
+function createWindow() {
+ // Create a new window
+ mainWindow = new BrowserWindow({
+ minWidth: constants.MIN_COLUMN_WIDTH,
+ width: WIDTH,
+ height: HEIGHT,
+ // Don't show the window until it ready, this prevents any white flickering
+ show: false,
+ })
+
+ mainWindow.loadURL(URL)
+
+ // Emitted when the window is closed.
+ mainWindow.on('closed', () => {
+ // Dereference the window object, usually you would store windows
+ // in an array if your app supports multi windows, this is the time
+ // when you should delete the corresponding element.
+ mainWindow.destroy()
+ })
+
+ // Show window when page is ready
+ mainWindow.once('ready-to-show', () => {
+ mainWindow.show()
+ const menu = Menu.buildFromTemplate(template)
+ Menu.setApplicationMenu(menu)
+ if (process.platform === 'darwin') {
+ const spin = new TouchBar.TouchBarButton({
+ label: 'devhub',
+ })
+
+ const touchBar = new TouchBar({
+ items: [spin],
+ })
+ mainWindow.setTouchBar(touchBar)
+ }
+ })
+
+ mainWindow.on('move', () => {
+ const [x, y] = mainWindow.getPosition()
+ if (y <= 100) {
+ mainWindow.hide()
+ showMenubarWindow()
+ }
+ })
+}
+
+function createMenubarWindow() {
+ menubarWindow = new BrowserWindow({
+ minWidth: constants.MIN_COLUMN_WIDTH,
+ width: WIDTH,
+ height: HEIGHT,
+ show: false,
+ frame: false,
+ fullscreenable: false,
+ movable: true,
+ hasShadow: false,
+ transparent: true,
+ webPreferences: {
+ backgroundThrottling: false,
+ },
+ })
+ menubarWindow.loadURL(URL)
+ const webContents = menubarWindow.webContents
+
+ webContents.on('dom-ready', () => {
+ const position = getWindowPosition()
+ setWindowPosition(menubarWindow, position.x, position.y)
+ webContents.insertCSS(
+ fs.readFileSync(
+ path.join(__dirname, '../public/static/css/arrow.css'),
+ 'utf8',
+ ),
+ )
+ menubarWindow.show()
+ menubarWindow.focus()
+ })
+
+ // Hide the window when it loses focus
+ menubarWindow.on('blur', () => {
+ menubarWindow.hide()
+ })
+
+ menubarWindow.on('move', () => {
+ const [x, y] = menubarWindow.getPosition()
+ if (y <= 100) {
+ const position = getWindowPosition()
+ menubarWindow.setPosition(position.x, position.y, false)
+ } else {
+ menubarWindow.hide()
+ showMainWindow(x, y)
+ }
+ })
+}
+
+function createTray() {
+ const trayIcon = nativeImage.createFromPath(
+ `${path.join(__dirname, '../public/static/media/logo-transparent@2x.png')}`,
+ )
+ tray = new Tray(
+ trayIcon.resize({
+ width: 22,
+ height: 22,
+ }),
+ )
+
+ const trayMenuTemplate = [
+ {
+ label: 'Devhub',
+ click() {
+ menubarWindow.hide()
+ toggleMainWindow()
+ },
+ },
+ {
+ label: 'Quit',
+ click() {
+ app.quit()
+ },
+ },
+ ]
+ tray.setToolTip(app.getName())
+
+ tray.on('click', () => {
+ toggleMenubarWindow()
+ })
+
+ tray.on('right-click', () => {
+ const contextMenu = Menu.buildFromTemplate(trayMenuTemplate)
+ tray.popUpContextMenu(contextMenu)
+ })
+}
+
+// Wait until the app is ready
+app.on('ready', () => {
+ app.setAsDefaultProtocolClient('x-devhub-client')
+ createTray()
+ createMenubarWindow()
+ if (process.platform === 'darwin') {
+ app.setAboutPanelOptions({
+ applicationName: 'devhub',
+ applicationVersion: app.getVersion(),
+ copyright: 'Copyright 2018',
+ credits: 'devhub',
+ })
+ }
+})
+
+// window-all-closed
+app.on('window-all-closed', () => {
+ if (process.platform !== 'darwin') {
+ app.quit()
+ }
+})
+
+// activate
+app.on('activate', () => {
+ if (menubarWindow === null) {
+ createTray()
+ }
+})
+
+// web-contents-created
+app.on('web-contents-created', (event, webContents) => {
+ webContents.on('new-window', (e, url) => {
+ if (!url.includes('https://api.devhubapp.com/oauth')) {
+ e.preventDefault()
+ shell.openExternal(url)
+ }
+ })
+})
+
+function getWindowPosition() {
+ const windowBounds = menubarWindow.getBounds()
+ const trayBounds = tray.getBounds()
+
+ // Center window horizontally below the tray icon
+ const x = Math.round(
+ trayBounds.x + trayBounds.width / 2 - windowBounds.width / 2,
+ )
+
+ // Position window 4 pixels vertically below the tray icon
+ const y = Math.round(trayBounds.y + trayBounds.height + 4)
+
+ return { x, y }
+}
+
+// toggle menubar window
+function toggleMenubarWindow() {
+ if (menubarWindow.isVisible()) {
+ menubarWindow.hide()
+ } else {
+ showMenubarWindow()
+ }
+}
+
+// toggle main window
+function toggleMainWindow() {
+ if (mainWindow === undefined) {
+ createWindow()
+ } else if (mainWindow.isVisible()) {
+ mainWindow.hide()
+ } else {
+ mainWindow.show()
+ }
+}
+
+function showMainWindow(x: number, y: number) {
+ if (mainWindow === undefined) {
+ createWindow()
+ } else {
+ mainWindow.show()
+ mainWindow.focus()
+ }
+ setWindowPosition(mainWindow, x, y)
+}
+
+function showMenubarWindow() {
+ const position = getWindowPosition()
+ menubarWindow.setPosition(position.x, position.y, false)
+ setWindowPosition(menubarWindow, position.x, position.y)
+ menubarWindow.show()
+ menubarWindow.focus()
+}
+
+function setWindowPosition(
+ window: Electron.BrowserWindow,
+ x: number,
+ y: number,
+): void {
+ window.setPosition(x, y)
+}
diff --git a/packages/desktop/tsconfig.json b/packages/desktop/tsconfig.json
new file mode 100644
index 000000000..efbde400d
--- /dev/null
+++ b/packages/desktop/tsconfig.json
@@ -0,0 +1,17 @@
+{
+ "extends": "../../tsconfig.json",
+ "compilerOptions": {
+ "target": "es2015",
+ "module": "commonjs",
+ "allowJs": false,
+ "composite": true,
+ "declaration": true,
+ "noEmit": false,
+ "outDir": "dist/",
+ "rootDir": "src",
+ "typeRoots": [
+ "../../@types",
+ "../../node_modules/@types"
+ ]
+ }
+}
diff --git a/packages/desktop/tslint.json b/packages/desktop/tslint.json
new file mode 100644
index 000000000..b520bae6b
--- /dev/null
+++ b/packages/desktop/tslint.json
@@ -0,0 +1,3 @@
+{
+ "extends": ["../../tslint.json"]
+}
\ No newline at end of file
diff --git a/packages/web/public/index.html b/packages/web/public/index.html
index e1c50ddaa..986d5af5b 100644
--- a/packages/web/public/index.html
+++ b/packages/web/public/index.html
@@ -53,8 +53,15 @@
+
-