Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Integrate GitHub sync #389

Merged
merged 22 commits into from
Oct 25, 2020
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion src/client/components/Tabs/Tab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export const Tab: React.FC<TabProps> = ({ activeTab, label, icon: IconCmp, onCli
const className = activeTab === label ? 'tab active' : 'tab'

return (
<div className={className} onClick={() => onClick(label)}>
<div key={label} className={className} onClick={() => onClick(label)}>
<IconCmp size={18} className="mr-1" aria-hidden="true" focusable="false" /> {label}
</div>
)
Expand Down
6 changes: 3 additions & 3 deletions src/client/components/Tabs/Tabs.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useState } from 'react'
import React, { Fragment, useState } from 'react'

import { Tab } from './Tab'

Expand Down Expand Up @@ -31,10 +31,10 @@ export const Tabs: React.FC<TabsProps> = ({ children }) => {
if (child.props.label !== activeTab) return

return (
<>
<Fragment key={`${child.props.label}-panel`}>
<h3>{child.props.label}</h3>
{child.props.children}
</>
</Fragment>
)
})}
</div>
Expand Down
2 changes: 1 addition & 1 deletion src/client/containers/NoteMenuBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ export const NoteMenuBar = () => {
onClick={syncNotesHandler}
data-testid={TestID.TOPBAR_ACTION_SYNC_NOTES}
>
{syncing ? <Loader size={18} /> : <RefreshCw size={18} />}
{syncing ? <Loader size={18} className="rotating-svg" /> : <RefreshCw size={18} />}
</button>
<button className="note-menu-bar-button" onClick={toggleDarkThemeHandler}>
{darkTheme ? <Sun size={18} /> : <Moon size={18} />}
Expand Down
12 changes: 6 additions & 6 deletions src/client/containers/TakeNoteApp.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,8 @@ export const TakeNoteApp: React.FC = () => {
const _loadSettings = () => dispatch(loadSettings())
const _swapCategories = (categoryId: number, destinationId: number) =>
dispatch(swapCategories({ categoryId, destinationId }))
const _sync = (notes: NoteItem[], categories: CategoryItem[]) =>
dispatch(sync({ notes, categories }))
// const _sync = (notes: NoteItem[], categories: CategoryItem[]) =>
// dispatch(sync({ notes, categories }))

// ===========================================================================
// Handlers
Expand Down Expand Up @@ -71,9 +71,9 @@ export const TakeNoteApp: React.FC = () => {
_loadSettings()
}, [])

useInterval(() => {
_sync(notes, categories)
}, 20000)
// useInterval(() => {
taniarascia marked this conversation as resolved.
Show resolved Hide resolved
// _sync(notes, categories)
// }, 20000)

useBeforeUnload((event: BeforeUnloadEvent) => (pendingSync ? event.preventDefault() : null))

Expand All @@ -88,7 +88,7 @@ export const TakeNoteApp: React.FC = () => {
<TempStateProvider>
<div className={determineAppClass(darkTheme, sidebarVisible, activeFolder)}>
<DragDropContext onDragEnd={onDragEnd}>
<SplitPane split="vertical" minSize={150} maxSize={500} defaultSize={230}>
<SplitPane split="vertical" minSize={150} maxSize={500} defaultSize={240}>
<AppSidebar />
<SplitPane split="vertical" minSize={200} maxSize={600} defaultSize={330}>
<NoteList />
Expand Down
9 changes: 8 additions & 1 deletion src/client/sagas/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import {
updateNotesSortStrategy,
} from '@/slices/settings'
import { SyncAction } from '@/types'
import { getSettings } from '@/selectors'
import { getSettings, getNotes, getCategories } from '@/selectors'

/**
* Log in user
Expand Down Expand Up @@ -93,8 +93,15 @@ function* fetchSettings() {
}

function* syncData({ payload }: SyncAction) {
const { notes } = yield select(getNotes)
const { categories } = yield select(getCategories)
const { settings } = yield select(getSettings)

const body = { notes, categories, settings }

try {
yield saveState(payload)
yield axios.post('/api/sync', body)
yield put(syncSuccess(dayjs().format()))
} catch (error) {
yield put(syncError(error.message))
Expand Down
4 changes: 4 additions & 0 deletions src/client/styles/_dark.scss
Original file line number Diff line number Diff line change
Expand Up @@ -326,4 +326,8 @@ $dark-editor: #3f3f3f;
}
}
}

.slider {
background-color: darken($dark-sidebar, 10%);
}
}
30 changes: 24 additions & 6 deletions src/client/styles/_helpers.scss
Original file line number Diff line number Diff line change
Expand Up @@ -49,11 +49,11 @@
height: 0;

&:checked + .slider {
background: $primary;
background: #72ce6e;
}

&:focus + .slider {
box-shadow: 0 0 1px $primary;
box-shadow: 0 0 1px #72ce6e;
}

&:checked + .slider:before {
Expand All @@ -76,13 +76,14 @@
&:before {
position: absolute;
content: '';
height: 16px;
width: 16px;
left: 4px;
bottom: 4px;
height: 20px;
width: 20px;
left: 2px;
bottom: 2px;
background: white;
transition: 0.4s;
border-radius: 50%;
box-shadow: 2px 3px 5px rgba(0, 0, 0, 0.07), 2px 3px 2px rgba(0, 0, 0, 0.2);
}
}

Expand Down Expand Up @@ -220,3 +221,20 @@ kbd {
transform: scale(1);
}
}

.rotating-svg {
animation-name: rotating;
animation-duration: 15.5s;
animation-iteration-count: infinite;
transform-origin: 50% 50%;
display: inline-block;
}

@keyframes rotating {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
2 changes: 1 addition & 1 deletion src/client/utils/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export const getNoteTitle = (text: string): string => {
// Remove whitespace from both ends
// Get the first n characters
// Remove # from the title in the case of using markdown headers in your title
const noteText = text.trim().match(/[^#]{1,38}/)
const noteText = text.trim().match(/[^#]{1,45}/)

// Get the first line of text after any newlines
// In the future, this should break on a full word
Expand Down
4 changes: 3 additions & 1 deletion src/client/utils/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export function useInterval(callback: () => void, delay: number | null) {
}

export function useKey(key: string, action: () => void) {
let actionRef = useRef(noop)
const actionRef = useRef(noop)
actionRef.current = action

useEffect(() => {
Expand All @@ -35,6 +35,7 @@ export function useKey(key: string, action: () => void) {
actionRef.current()
}
})

return () => mousetrap.unbind(key)
}, [key])
}
Expand Down Expand Up @@ -66,6 +67,7 @@ export function useBeforeUnload(handler: Function = () => {}) {

if (typeof returnValue === 'string') {
event.returnValue = returnValue

return returnValue
}
}
Expand Down
79 changes: 70 additions & 9 deletions src/server/handlers/auth.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { Request, Response, NextFunction } from 'express'
import { Request, Response } from 'express'
import axios from 'axios'
import * as dotenv from 'dotenv'

import { thirtyDayCookie } from '../utils/constants'
import { SDK } from '../utils/helpers'
import { Method } from '../utils/enums'

dotenv.config()

Expand Down Expand Up @@ -54,7 +56,7 @@ export default {
const accessToken = data.access_token

// Set cookie
response.cookie('accessTokenGH', accessToken, thirtyDayCookie)
response.cookie('githubAccessToken', accessToken, thirtyDayCookie)

// Redirect to the app when logged in
response.redirect('/app')
Expand All @@ -71,16 +73,23 @@ export default {
* If an access token cookie exists, attempt to determine the currently logged
* in user. If the access token is in some way incorrect, expired, etc., throw
* an error.
*
* After successful login, check if it's the first time logging in by seeing if a repo
* named `takenote-data` exists. If it doesn't, create it.
*/
login: async (request: Request, response: Response, next: NextFunction) => {
login: async (request: Request, response: Response) => {
const { accessToken } = response.locals

try {
const { data } = await axios('https://github.com/gitapi/user', {
headers: {
Authorization: `token ${accessToken}`,
},
})
const { data } = await SDK(Method.GET, '/user', accessToken)
const username = data.login

const isFirstTimeLoggingIn = await firstTimeLoginCheck(username, accessToken)

if (isFirstTimeLoggingIn) {
await createTakeNoteDataRepo(username, accessToken)
await createInitialCommit(username, accessToken)
}

response.status(200).send(data)
} catch (error) {
Expand All @@ -89,8 +98,60 @@ export default {
},

logout: async (request: Request, response: Response) => {
response.clearCookie('accessTokenGH')
response.clearCookie('githubAccessToken')

response.status(200).send({ message: 'Logged out' })
},
}

async function firstTimeLoginCheck(username: string, accessToken: string): Promise<boolean> {
try {
await SDK(Method.GET, `/repos/${username}/takenote-data`, accessToken)

// If repo already exists, we assume it's the takenote data repo and can move on
return false
} catch (error) {
// If repo doesn't exist, we'll try to create it
return true
}
}

async function createTakeNoteDataRepo(username: string, accessToken: string): Promise<void> {
const takenoteDataRepo = {
name: 'takenote-data',
description: 'Database of notes for TakeNote',
private: true,
visibility: 'private',
has_issues: false,
has_projects: false,
has_wiki: false,
is_template: false,
auto_init: false,
allow_squash_merge: false,
allow_rebase_merge: false,
}
try {
await SDK(Method.POST, `/user/repos`, accessToken, takenoteDataRepo)
} catch (error) {
throw new Error(error)
}
}

async function createInitialCommit(username: string, accessToken: string): Promise<void> {
const readme = Buffer.from('Hello World!').toString('base64')
const initialCommit = {
message: 'Initial commit',
content: readme,
branch: 'master',
}
try {
await SDK(
Method.PUT,
`/repos/${username}/takenote-data/contents/README.md`,
accessToken,
initialCommit
)
} catch (error) {
throw new Error(error)
}
}
48 changes: 48 additions & 0 deletions src/server/handlers/sync.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { Request, Response } from 'express'
import dayjs from 'dayjs'

import { SDK } from '../utils/helpers'
import { Method } from '../utils/enums'

export default {
sync: async (request: Request, response: Response) => {
const { accessToken, userData } = response.locals
const {
body: { notes, categories, settings },
} = request
const username = userData.login
const repo = 'takenote-data'

try {
// Create blob
// https://docs.github.com/en/free-pro-team@latest/rest/reference/git#create-a-blob
const noteBlob = await SDK(Method.POST, `/repos/${username}/${repo}/git/blobs`, accessToken, {
content: JSON.stringify(notes, null, 2),
})

// Create tree
// https://docs.github.com/en/free-pro-team@latest/rest/reference/git#create-a-tree
const tree = await SDK(Method.POST, `/repos/${username}/${repo}/git/trees`, accessToken, {
tree: [{ path: 'notes.json', mode: '100644', type: 'blob', sha: noteBlob.data.sha }],
})

// Create commit
// https://docs.github.com/en/free-pro-team@latest/rest/reference/git#create-a-commit
const commit = await SDK(Method.POST, `/repos/${username}/${repo}/git/commits`, accessToken, {
message: 'Notes ' + dayjs(Date.now()).format('h:mm A M/D/YYYY'),
tree: tree.data.sha,
})

// Update a reference
// https://docs.github.com/en/free-pro-team@latest/rest/reference/git#update-a-reference
await SDK(Method.POST, `/repos/${username}/${repo}/git/refs/heads/master`, accessToken, {
sha: commit.data.sha,
force: true,
})

response.status(200).send({ message: 'Success' })
} catch (error) {
response.status(400).send({ message: error.message })
}
},
}
2 changes: 1 addition & 1 deletion src/server/middleware/checkAuth.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Request, Response, NextFunction } from 'express'

const checkAuth = async (request: Request, response: Response, next: NextFunction) => {
const accessToken = request.cookies?.accessTokenGH
const accessToken = request.cookies?.githubAccessToken

if (accessToken) {
response.locals.accessToken = accessToken
Expand Down
19 changes: 19 additions & 0 deletions src/server/middleware/getUser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { Request, Response, NextFunction } from 'express'

import { SDK } from '../utils/helpers'
import { Method } from '../utils/enums'

const getUser = async (request: Request, response: Response, next: NextFunction) => {
const { accessToken } = response.locals

try {
const { data } = await SDK(Method.GET, '/user', accessToken)
response.locals.userData = data

next()
} catch (error) {
response.status(403).send({ message: 'Forbidden Resource', status: 403 })
}
}

export default getUser
Loading