diff --git a/atom-lib/menu-template.js b/atom-lib/menu-template.js index cadc401a..d1015cc6 100644 --- a/atom-lib/menu-template.js +++ b/atom-lib/menu-template.js @@ -1,5 +1,6 @@ const electron = require('electron') const BrowserWindow = electron.BrowserWindow +const shell = electron.shell module.exports = [ { @@ -90,7 +91,17 @@ module.exports = [ click: function () { BrowserWindow.getFocusedWindow().reload() } - } + }, + // { + // label: 'Toggle Developer Tools', + // accelerator: (function () { + // if (process.platform === 'darwin') return 'Alt+Command+I' + // else return 'Ctrl+Shift+I' + // })(), + // click: function (item, focusedWindow) { + // if (focusedWindow) BrowserWindow.getFocusedWindow().toggleDevTools() + // } + // } ] }, { @@ -117,6 +128,24 @@ module.exports = [ }, { label: 'Help', - submenu: [] + role: 'help', + submenu: [ + { + label: 'Boost official site', + click: function () { shell.openExternal('https://b00st.io/') } + }, + { + label: 'Tutorial page', + click: function () { shell.openExternal('https://b00st.io/tutorial.html') } + }, + { + label: 'Discussions', + click: function () { shell.openExternal('https://github.com/BoostIO/boost-app-discussions/issues') } + }, + { + label: 'Changelog', + click: function () { shell.openExternal('https://github.com/BoostIO/boost-releases/blob/master/changelog.md') } + } + ] } ] diff --git a/browser/finder/index.js b/browser/finder/index.js index c72987f2..36360693 100644 --- a/browser/finder/index.js +++ b/browser/finder/index.js @@ -32,11 +32,20 @@ class FinderMain extends React.Component { } componentDidMount () { + this.keyDownHandler = e => this.handleKeyDown(e) + document.addEventListener('keydown', this.keyDownHandler) ReactDOM.findDOMNode(this.refs.finderInput.refs.input).focus() + this.focusHandler = e => { + let { dispatch } = this.props + + dispatch(searchArticle('')) + } + window.addEventListener('focus', this.focusHandler) } - handleClick (e) { - ReactDOM.findDOMNode(this.refs.finderInput.refs.input).focus() + componentWillUnmount () { + document.removeEventListener('keydown', this.keyDownHandler) + window.removeEventListener('focus', this.focusHandler) } handleKeyDown (e) { @@ -58,6 +67,11 @@ class FinderMain extends React.Component { hideFinder() e.preventDefault() } + if (e.keyCode === 91 || e.metaKey) { + return + } + + ReactDOM.findDOMNode(this.refs.finderInput.refs.input).focus() } saveToClipboard () { @@ -99,7 +113,7 @@ class FinderMain extends React.Component { let { articles, activeArticle, status, dispatch } = this.props let saveToClipboard = () => this.saveToClipboard() return ( -
this.handleClick(e)} onKeyDown={e => this.handleKeyDown(e)} className='Finder'> +
this.handleClick(e)} className='Finder'> this.handleSearchChange(e)} ref='finderInput' diff --git a/browser/main/HomePage.js b/browser/main/HomePage.js index cc0c127a..7ccedd2d 100644 --- a/browser/main/HomePage.js +++ b/browser/main/HomePage.js @@ -9,7 +9,7 @@ import _ from 'lodash' import { isModalOpen, closeModal } from 'boost/modal' const electron = require('electron') -const BrowserWindow = electron.remote.BrowserWindow +const remote = electron.remote const TEXT_FILTER = 'TEXT_FILTER' const FOLDER_FILTER = 'FOLDER_FILTER' @@ -29,13 +29,6 @@ class HomePage extends React.Component { } handleKeyDown (e) { - if (process.env.BOOST_ENV === 'development' && e.keyCode === 73 && e.metaKey && e.altKey) { - e.preventDefault() - e.stopPropagation() - BrowserWindow.getFocusedWindow().toggleDevTools() - return - } - if (isModalOpen()) { if (e.keyCode === 27) closeModal() return @@ -106,7 +99,7 @@ class HomePage extends React.Component { list.selectNextArticle() } - if (e.keyCode === 65 || (e.keyCode === 13 && e.metaKey) || (e.keyCode === 78 && e.metaKey)) { + if ((e.keyCode === 65 && !e.metaKey && !e.ctrlKey) || (e.keyCode === 13 && e.metaKey) || (e.keyCode === 78 && e.metaKey)) { nav.handleNewPostButtonClick() e.preventDefault() } @@ -142,6 +135,7 @@ class HomePage extends React.Component { Copied! + copied: false, + failed: false + } +} + +export default class ShareButton extends React.Component { + constructor (props) { + super(props) + this.state = getDefault() + } + + componentWillReceiveProps (nextProps) { + this.setState(getDefault()) + } + + componentDidMount () { + this.dropdownInterceptor = e => { + this.dropdownClicked = true + } + ReactDOM.findDOMNode(this.refs.dropdown).addEventListener('click', this.dropdownInterceptor) + this.shareViaPublicURLHandler = e => { + this.handleShareViaPublicURLClick(e) + } + } + + componentWillUnmount () { + document.removeEventListener('click', this.dropdownHandler) + ReactDOM.findDOMNode(this.refs.dropdown).removeEventListener('click', this.dropdownInterceptor) + } + + handleOpenButtonClick (e) { + this.openDropdown() + if (this.dropdownHandler == null) { + this.dropdownHandler = e => { + if (!this.dropdownClicked) { + this.closeDropdown() + } else { + this.dropdownClicked = false + } + } + } + document.removeEventListener('click', this.dropdownHandler) + document.addEventListener('click', this.dropdownHandler) + } + + openDropdown () { + this.setState({openDropdown: true}) + } + + closeDropdown () { + document.removeEventListener('click', this.dropdownHandler) + this.setState({openDropdown: false}) + } + + handleShareViaPublicURLClick (e) { + let { user } = this.props + let input = Object.assign({}, this.props.article, { + clientKey: clientKey.get(), + writerName: user.name + }) + this.setState({ + isSharing: true, + failed: false + }, () => { + api.shareViaPublicURL(input) + .then(res => { + let url = res.body.url + this.setState({url: url, isSharing: false}) + activityRecord.emit('ARTICLE_SHARE') + }) + .catch(err => { + console.log(err) + this.setState({isSharing: false, failed: true}) + }) + }) + } + + handleCopyURLClick () { + clipboard.writeText(this.state.url) + this.setState({copied: true}) + } + + // Restore copy url tooltip + handleCopyURLMouseLeave () { + this.setState({copied: false}) + } + + render () { + let hasPublicURL = this.state.url != null + return ( +
+ +
+ { + !hasPublicURL ? ( + + ) : ( +
+ + +
This url is valid for 7 days.
+
+ ) + } +
+
+ ) + } +} + +ShareButton.propTypes = { + article: PropTypes.shape({ + publicURL: PropTypes.string + }), + user: PropTypes.shape({ + name: PropTypes.string + }) +} diff --git a/browser/main/HomePage/ArticleDetail.js b/browser/main/HomePage/ArticleDetail/index.js similarity index 97% rename from browser/main/HomePage/ArticleDetail.js rename to browser/main/HomePage/ArticleDetail/index.js index e11c140b..59fe0d1f 100644 --- a/browser/main/HomePage/ArticleDetail.js +++ b/browser/main/HomePage/ArticleDetail/index.js @@ -24,6 +24,8 @@ import TagLink from 'boost/components/TagLink' import TagSelect from 'boost/components/TagSelect' import ModeSelect from 'boost/components/ModeSelect' import activityRecord from 'boost/activityRecord' +import api from 'boost/api' +import ShareButton from './ShareButton' const electron = require('electron') const clipboard = electron.clipboard @@ -106,12 +108,16 @@ export default class ArticleDetail extends React.Component { isTagChanged: false, isTitleChanged: false, isContentChanged: false, - isModeChanged: false + isModeChanged: false, + openShareDropdown: false } } componentDidMount () { this.refreshTimer = setInterval(() => this.forceUpdate(), 60 * 1000) + this.shareDropdownInterceptor = e => { + e.stopPropagation() + } } componentWillUnmount () { @@ -191,7 +197,7 @@ export default class ArticleDetail extends React.Component { } renderIdle () { - let { status, activeArticle, folders } = this.props + let { status, activeArticle, folders, user } = this.props let tags = activeArticle.tags != null ? activeArticle.tags.length > 0 ? activeArticle.tags.map(tag => { @@ -234,9 +240,15 @@ export default class ArticleDetail extends React.Component {
{tags}
+ + + @@ -286,7 +298,7 @@ export default class ArticleDetail extends React.Component { dispatch(unlockStatus()) - delete newArticle.status + newArticle.status = null newArticle.updatedAt = new Date() newArticle.title = newArticle.title.trim() if (newArticle.createdAt == null) { @@ -586,7 +598,8 @@ export default class ArticleDetail extends React.Component { ArticleDetail.propTypes = { status: PropTypes.shape(), activeArticle: PropTypes.shape(), - activeUser: PropTypes.shape(), + user: PropTypes.shape(), + folders: PropTypes.array, dispatch: PropTypes.func } ArticleDetail.prototype.linkState = linkState diff --git a/browser/main/MainPage.js b/browser/main/MainPage.js index e1490e96..a5db1fa9 100644 --- a/browser/main/MainPage.js +++ b/browser/main/MainPage.js @@ -2,8 +2,6 @@ const electron = require('electron') const ipc = electron.ipcRenderer import React, { PropTypes } from 'react' -var ContactModal = require('boost/components/modal/ContactModal') - export default class MainContainer extends React.Component { constructor (props) { super(props) @@ -20,20 +18,12 @@ export default class MainContainer extends React.Component { ipc.send('update-app', 'Deal with it.') } - openContactModal () { - this.openModal(ContactModal) - } - render () { return (
{this.state.updateAvailable ? ( ) : null} - {/* */} {this.props.children}
) diff --git a/browser/styles/main/HomeContainer/components/ArticleDetail.styl b/browser/styles/main/HomeContainer/components/ArticleDetail.styl index e64e5da6..61e4d8d9 100644 --- a/browser/styles/main/HomeContainer/components/ArticleDetail.styl +++ b/browser/styles/main/HomeContainer/components/ArticleDetail.styl @@ -284,7 +284,87 @@ iptFocusBorderColor = #369DCD color noTagsColor .right z-index 30 - button + div.share-dropdown + position absolute + right 5px + top 30px + background-color transparentify(invBackgroundColor, 80%) + padding 5px 0 + width 200px + &.hide + display none + &>button + width 200px + text-align left + display block + height 33px + background-color transparent + color white + font-size 14px + padding 0 10px + border none + &:hover + background-color transparentify(lighten(invBackgroundColor, 30%), 80%) + &>.ShareButton-url + clearfix() + input.ShareButton-url-input + width 155px + margin 0 0 0 5px + height 25px + outline none + border none + border-top-left-radius 5px + border-bottom-left-radius 5px + float left + padding 5px + button.ShareButton-url-button + width 35px + height 25px + border none + margin 0 5px 0 0 + outline none + border-top-right-radius 5px + border-bottom-right-radius 5px + background-color darken(white, 5%) + color inactiveTextColor + float right + div.ShareButton-url-button-tooltip + tooltip() + right 10px + &:hover + color textColor + div.ShareButton-url-button-tooltip + opacity 1 + div.ShareButton-url-alert + float left + height 25px + line-height 25px + padding 0 15px + color white + + .ShareButton + display inline-block + button.ShareButton-open-button + border-radius 16.5px + cursor pointer + height 33px + width 33px + border none + margin-right 5px + font-size 18px + color inactiveTextColor + background-color darken(white, 5%) + padding 0 + .tooltip + tooltip() + margin-top 25px + margin-left -40px + &:hover + color textColor + .tooltip + opacity 1 + + &>button border-radius 16.5px cursor pointer height 33px diff --git a/finder.js b/finder.js index 02da4244..627a3cbd 100755 --- a/finder.js +++ b/finder.js @@ -27,6 +27,10 @@ app.on('ready', function () { var appIcon = new Tray(__dirname + '/resources/tray-icon.png') appIcon.setToolTip('Boost') + var template = require('./atom-lib/menu-template') + var menu = Menu.buildFromTemplate(template) + Menu.setApplicationMenu(menu) + finderWindow = require('./atom-lib/finder-window') finderWindow.webContents.on('did-finish-load', function () { var trayMenu = new Menu() diff --git a/lib/actions.js b/lib/actions.js index 39c7a4f4..b33b0945 100644 --- a/lib/actions.js +++ b/lib/actions.js @@ -146,3 +146,23 @@ export function toggleTutorial () { type: TOGGLE_TUTORIAL } } + +export default { + updateUser, + clearNewArticle, + updateArticle, + destroyArticle, + createFolder, + updateFolder, + destroyFolder, + replaceFolder, + switchFolder, + switchMode, + switchArticle, + setSearchFilter, + setTagFilter, + clearSearch, + lockStatus, + unlockStatus, + toggleTutorial +} diff --git a/lib/activityRecord.js b/lib/activityRecord.js index 79036010..abf368b4 100644 --- a/lib/activityRecord.js +++ b/lib/activityRecord.js @@ -1,8 +1,8 @@ import _ from 'lodash' import moment from 'moment' -import keygen from 'boost/keygen' import dataStore from 'boost/dataStore' import { request, WEB_URL } from 'boost/api' +import clientKey from 'boost/clientKey' const electron = require('electron') const version = electron.remote.app.getVersion() @@ -28,16 +28,6 @@ export function init () { } } -export function getClientKey () { - let clientKey = localStorage.getItem('clientKey') - if (!_.isString(clientKey) || clientKey.length !== 40) { - clientKey = keygen() - localStorage.setItem('clientKey', clientKey) - } - - return clientKey -} - export function getAllRecords () { return JSON.parse(localStorage.getItem('activityRecords')) } @@ -67,7 +57,7 @@ export function postRecords (data) { console.log('posting...', records) let input = { - clientKey: getClientKey(), + clientKey: clientKey.get(), records } return request.post(WEB_URL + 'apis/activity') @@ -108,6 +98,7 @@ export function emit (type, data = {}) { case 'FINDER_OPEN': case 'FINDER_COPY': case 'MAIN_DETAIL_COPY': + case 'ARTICLE_SHARE': todayRecord[type] = todayRecord[type] == null ? 1 : todayRecord[type] + 1 @@ -142,6 +133,5 @@ export function emit (type, data = {}) { export default { init, emit, - getClientKey, postRecords } diff --git a/lib/api.js b/lib/api.js index 6a5f9f91..729f1456 100644 --- a/lib/api.js +++ b/lib/api.js @@ -1,191 +1,22 @@ import superagent from 'superagent' import superagentPromise from 'superagent-promise' -import auth from 'boost/auth' +// import auth from 'boost/auth' -export const API_URL = 'http://boost-api4.elasticbeanstalk.com/' -export const WEB_URL = 'https://b00st.io/' -// export const WEB_URL = 'http://localhost:3333/' +export const SERVER_URL = 'https://b00st.io/' +// export const SERVER_URL = 'http://localhost:3333/' export const request = superagentPromise(superagent, Promise) -export function login (input) { +export function shareViaPublicURL (input) { return request - .post(API_URL + 'auth/login') - .send(input) -} - -export function signup (input) { - return request - .post(API_URL + 'auth/register') - .send(input) -} - -export function updateUserInfo (input) { - return request - .put(API_URL + 'auth/user') - .set({ - Authorization: 'Bearer ' + auth.token() - }) - .send(input) -} - -export function updatePassword (input) { - return request - .post(API_URL + 'auth/password') - .set({ - Authorization: 'Bearer ' + auth.token() - }) - .send(input) -} - -export function fetchCurrentUser () { - return request - .get(API_URL + 'auth/user') - .set({ - Authorization: 'Bearer ' + auth.token() - }) -} - -export function fetchArticles (userId) { - return request - .get(API_URL + 'teams/' + userId + '/articles') - .set({ - Authorization: 'Bearer ' + auth.token() - }) -} - -export function createArticle (input) { - return request - .post(API_URL + 'articles/') - .set({ - Authorization: 'Bearer ' + auth.token() - }) - .send(input) -} - -export function saveArticle (input) { - return request - .put(API_URL + 'articles/' + input.id) - .set({ - Authorization: 'Bearer ' + auth.token() - }) - .send(input) -} - -export function destroyArticle (articleId) { - return request - .del(API_URL + 'articles/' + articleId) - .set({ - Authorization: 'Bearer ' + auth.token() - }) -} - -export function createTeam (input) { - return request - .post(API_URL + 'teams') - .set({ - Authorization: 'Bearer ' + auth.token() - }) - .send(input) -} - -export function updateTeamInfo (teamId, input) { - return request - .put(API_URL + 'teams/' + teamId) - .set({ - Authorization: 'Bearer ' + auth.token() - }) - .send(input) -} - -export function destroyTeam (teamId) { - return request - .del(API_URL + 'teams/' + teamId) - .set({ - Authorization: 'Bearer ' + auth.token() - }) -} - -export function searchUser (key) { - return request - .get(API_URL + 'search/users') - .query({key: key}) -} - -export function setMember (teamId, input) { - return request - .post(API_URL + 'teams/' + teamId + '/members') - .set({ - Authorization: 'Bearer ' + auth.token() - }) - .send(input) -} - -export function deleteMember (teamId, input) { - return request - .del(API_URL + 'teams/' + teamId + '/members') - .set({ - Authorization: 'Bearer ' + auth.token() - }) - .send(input) -} - -export function createFolder (input) { - return request - .post(API_URL + 'folders/') - .set({ - Authorization: 'Bearer ' + auth.token() - }) - .send(input) -} - -export function updateFolder (id, input) { - return request - .put(API_URL + 'folders/' + id) - .set({ - Authorization: 'Bearer ' + auth.token() - }) - .send(input) -} - -export function destroyFolder (id) { - return request - .del(API_URL + 'folders/' + id) - .set({ - Authorization: 'Bearer ' + auth.token() - }) -} - -export function sendEmail (input) { - return request - .post(API_URL + 'mail') - .set({ - Authorization: 'Bearer ' + auth.token() - }) + .post(SERVER_URL + 'apis/share') + // .set({ + // Authorization: 'Bearer ' + auth.token() + // }) .send(input) } export default { - API_URL, - WEB_URL, - request, - login, - signup, - updateUserInfo, - updatePassword, - fetchCurrentUser, - fetchArticles, - createArticle, - saveArticle, - destroyArticle, - createTeam, - updateTeamInfo, - destroyTeam, - searchUser, - setMember, - deleteMember, - createFolder, - updateFolder, - destroyFolder, - sendEmail + SERVER_URL, + shareViaPublicURL } diff --git a/lib/clientKey.js b/lib/clientKey.js new file mode 100644 index 00000000..b76d6bb8 --- /dev/null +++ b/lib/clientKey.js @@ -0,0 +1,23 @@ +import _ from 'lodash' +import keygen from 'boost/keygen' + +function getClientKey () { + let clientKey = localStorage.getItem('clientKey') + if (!_.isString(clientKey) || clientKey.length !== 40) { + clientKey = keygen() + setClientKey(clientKey) + } + + return clientKey +} + +function setClientKey (newKey) { + localStorage.setItem('clientKey', newKey) +} + +getClientKey() + +export default { + get: getClientKey, + set: setClientKey +} diff --git a/lib/components/CodeEditor.js b/lib/components/CodeEditor.js index e6941ed8..91b39ac3 100644 --- a/lib/components/CodeEditor.js +++ b/lib/components/CodeEditor.js @@ -31,6 +31,14 @@ module.exports = React.createClass({ editor.setTheme('ace/theme/xcode') editor.clearSelection() editor.moveCursorTo(0, 0) + editor.commands.addCommand({ + name: 'Emacs cursor up', + bindKey: {mac: 'Ctrl-P'}, + exec: function (editor) { + editor.navigateUp(1) + }, + readOnly: true + }) editor.setReadOnly(!!this.props.readOnly) diff --git a/lib/components/modal/ContactModal.js b/lib/components/modal/ContactModal.js deleted file mode 100644 index f3106ea6..00000000 --- a/lib/components/modal/ContactModal.js +++ /dev/null @@ -1,85 +0,0 @@ -import React, { PropTypes, findDOMNode } from 'react' -import linkState from 'boost/linkState' -import { sendEmail } from 'boost/api' - -export default class ContactModal extends React.Component { - constructor (props) { - super(props) - - this.linkState = linkState - - this.state = { - isSent: false, - mail: { - title: '', - content: '' - } - } - } - - onKeyCast (e) { - switch (e.status) { - case 'closeModal': - this.props.close() - break - case 'submitContactModal': - if (this.state.isSent) { - this.props.close() - return - } - this.sendEmail() - break - } - } - - componentDidMount () { - findDOMNode(this.refs.title).focus() - } - - sendEmail () { - sendEmail(this.state.mail) - .then(function (res) { - this.setState({isSent: !this.state.isSent}) - }.bind(this)) - .catch(function (err) { - console.error(err) - }) - } - - render () { - return ( -
-

Contact form

- - {!this.state.isSent ? ( -
-
-
- -
-
-