diff --git a/README.md b/README.md index 39af082bdd..8b17e4db6f 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ Spectrum aims to be the best platform to build any kind of community online by c ### Status -Spectrum has been under full-time development since March, 2017. See [the roadmap](TK) for up-to-date information about our current areas of focus. +Spectrum has been under full-time development since March, 2017. See [the roadmap](https://github.com/withspectrum/spectrum/projects/19) for up-to-date information about our current areas of focus.
@@ -38,6 +38,7 @@ Spectrum has been under full-time development since March, 2017. See [the roadma - [Code Style](#code-style) - [First time setup](#first-time-setup) - [Running the app locally](#running-the-app-locally) + - [Roadmap](https://github.com/withspectrum/spectrum/projects/19) - Technical - [Testing](docs/testing.md) - [Background Jobs](docs/backend/background-jobs.md) @@ -50,7 +51,7 @@ Spectrum has been under full-time development since March, 2017. See [the roadma ## Contributing -**We heartily welcome any and all contributions that match our product roadmap and engineering standards!** +**We heartily welcome any and all contributions that match [our product roadmap](https://github.com/withspectrum/spectrum/projects/19) and engineering standards!** That being said, this codebase isn't your typical open source project because it's not a library or package with a limited scope—it's our entire product. @@ -66,7 +67,9 @@ If you found a technical bug on Spectrum or have ideas for features we should im #### Fixing a bug or implementing a new feature -If you find a bug on Spectrum and open a PR that fixes it we'll review it as soon as possible to ensure it matches our engineering standards. If you want implement a new feature, open an issue first to discuss what it'd look like and to ensure it fits in our roadmap and plans for the app. +If you find a bug on Spectrum and open a PR that fixes it we'll review it as soon as possible to ensure it matches our engineering standards. If you want implement a new feature, open an issue first to discuss what it'd look like and to ensure it fits in [our roadmap](https://github.com/withspectrum/spectrum/projects/19) and plans for the app. + +If you want to contribute but are unsure to start, we have [a "good first issue" label](https://github.com/withspectrum/spectrum/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22) which is applied to newcomer-friendly issues. Take a look at [the full list of good first issues](https://github.com/withspectrum/spectrum/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22) and pick something you like! Want to fix a bug or implement an agreed-upon feature? Great, jump to the [local setup instructions](#first-time-setup)! diff --git a/config-overrides.js b/config-overrides.js index a2ff6ea02a..e7461ed997 100644 --- a/config-overrides.js +++ b/config-overrides.js @@ -122,7 +122,7 @@ module.exports = function override(config, env) { return; } }, - requestType: ['same-origin'], + requestType: ['navigate'], }, ], ServiceWorker: { diff --git a/cypress.json b/cypress.json index ac3ddcb2da..376bda3540 100644 --- a/cypress.json +++ b/cypress.json @@ -1,4 +1,9 @@ { "baseUrl": "http://localhost:3000", - "viewportWidth": 1300 + "viewportWidth": 1300, + "defaultCommandTimeout": 10000, + "blacklistHosts": ["*.google-analytics.com"], + "env": { + "DEBUG": "src*,testing*,build*" + } } diff --git a/cypress/integration/thread_spec.js b/cypress/integration/thread_spec.js index 2f7e09de29..0ff52ef087 100644 --- a/cypress/integration/thread_spec.js +++ b/cypress/integration/thread_spec.js @@ -34,3 +34,29 @@ describe('Thread View', () => { }); }); }); + +describe('/new/thread', () => { + beforeEach(() => { + cy.auth(author.id); + cy.visit('/new/thread'); + }); + + it('should allow composing new threads', () => { + const title = 'Some new thread'; + const body = "with some fresh content you've never seen before"; + cy.get('[data-e2e-id="rich-text-editor"]').should('be.visible'); + cy.get('[data-e2e-id="composer-community-selector"]').should('be.visible'); + cy.get('[data-e2e-id="composer-channel-selector"]').should('be.visible'); + // Type title and body + cy + .get('[data-e2e-id="composer-title-input"]') + .should('be.visible') + .type(title); + // TODO: Cypress doesn't handle DraftJS very well, it only inlcudes the first character + //cy.get('[contenteditable="true"]').type(body) + cy.get('[data-e2e-id="composer-publish-button"]').click(); + cy.location('pathname').should('contain', 'thread'); + cy.get('[data-e2e-id="thread-view"]'); + cy.contains(title); + }); +}); diff --git a/dangerfile.js b/dangerfile.js index 5ba06c6946..4a11bbacdc 100644 --- a/dangerfile.js +++ b/dangerfile.js @@ -4,7 +4,6 @@ import { warn, fail, message, markdown, schedule, danger } from 'danger'; import yarn from 'danger-plugin-yarn'; import jest from 'danger-plugin-jest'; import flow from 'danger-plugin-flow'; -import labels from 'danger-plugin-labels'; import noTestShortcuts from 'danger-plugin-no-test-shortcuts'; import noConsole from 'danger-plugin-no-console'; @@ -26,17 +25,6 @@ if (danger.github.pr.body.length < 10) { fail('Please add a description to your PR.'); } -// Add automatic labels to the PR -schedule( - labels({ - labels: { - wip: 'WIP: Building', - 'needs testing': 'WIP: Needs Testing', - 'ready for review': 'WIP: Ready for Review', - }, - }) -); - // Make sure the yarn.lock file is updated when dependencies get added and log any added dependencies APP_FOLDERS.forEach(folder => { schedule(yarn(path.join(__dirname, folder, 'package.json'))); @@ -47,7 +35,8 @@ jest(); // Make sure nobody does a it.only and blocks our entire test-suite from running noTestShortcuts({ - testFilePredicate: filePath => filePath.endsWith('.test.js'), + testFilePredicate: filePath => + filePath.endsWith('.test.js') || filePath.endsWith('_spec.js'), }); schedule(noConsole({ whitelist: ['error'] })); diff --git a/iris/models/db.js b/iris/models/db.js index fd160e8b31..136302afd2 100644 --- a/iris/models/db.js +++ b/iris/models/db.js @@ -10,7 +10,6 @@ const DEFAULT_CONFIG = { max: 500, // Maximum number of connections, default is 1000 buffer: 5, // Minimum number of connections open at any given moment, default is 50 timeoutGb: 60 * 1000, // How long should an unused connection stick around, default is an hour, this is a minute - pingInterval: 300, // Ping the connection every 5 minutes (300 seconds) to keep it alive and prevent rethinkdbdash#192 }; const PRODUCTION_CONFIG = { diff --git a/iris/queries/community/members.js b/iris/queries/community/members.js index 3c71d34783..faa1861e57 100644 --- a/iris/queries/community/members.js +++ b/iris/queries/community/members.js @@ -28,8 +28,16 @@ export default ( const lastUserIndex = lastDigits && lastDigits.length > 0 && parseInt(lastDigits[1], 10); + // Note @brian: this is a shitty hack, but if we want to show both + // moderators and admins in a single list, I need to tweak the inbound + // filter here + let dbfilter = filter; + if (filter && (filter.isOwner && filter.isModerator)) { + dbfilter = row => row('isModerator').or(row('isOwner')); + } + // $FlowFixMe - return getMembersInCommunity(id, { first, after: lastUserIndex }, filter) + return getMembersInCommunity(id, { first, after: lastUserIndex }, dbfilter) .then(users => { const permissionsArray = users.map(userId => [userId, id]); // $FlowIssue diff --git a/package.json b/package.json index caed2d1739..1155aa0808 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "Spectrum", - "version": "2.1.12", + "version": "2.1.13", "private": true, "devDependencies": { "babel-cli": "^6.24.1", @@ -83,7 +83,7 @@ "draft-js-image-plugin": "2.0.0-rc8", "draft-js-import-markdown": "^1.2.0", "draft-js-linkify-plugin": "^2.0.0-beta1", - "draft-js-markdown-plugin": "^1.0.0", + "draft-js-markdown-plugin": "1.2.0", "draft-js-plugins-editor": "^2.0.3", "draft-js-prism-plugin": "0.1.1", "draftjs-to-markdown": "^0.4.2", @@ -109,7 +109,7 @@ "imgix-core-js": "^1.0.6", "ioredis": "3.1.4", "isomorphic-fetch": "^2.2.1", - "jest": "^22.1.0", + "jest": "22.4.3", "json-stringify-pretty-compact": "^1.0.4", "jsonwebtoken": "^8.0.1", "keygrip": "^1.0.2", @@ -179,6 +179,8 @@ "resolutions": { "immutable": "3.7.4", "draft-js": "npm:draft-js-fork-mxstbr", + "jest-environment-node": "22.4.3", + "jest": "22.4.3", "fbjs": "0.8.16" }, "scripts": { diff --git a/src/components/composer/index.js b/src/components/composer/index.js index 06354e31d3..d3afa2879f 100644 --- a/src/components/composer/index.js +++ b/src/components/composer/index.js @@ -5,8 +5,8 @@ import Textarea from 'react-textarea-autosize'; import { withRouter } from 'react-router'; import { connect } from 'react-redux'; import isURL from 'validator/lib/isURL'; -import { KeyBindingUtil } from 'draft-js'; import debounce from 'debounce'; +import { KeyBindingUtil } from 'draft-js'; import { URLS } from '../../helpers/regexps'; import { track } from '../../helpers/events'; import { closeComposer } from '../../actions/composer'; @@ -85,30 +85,26 @@ let storedBody; let storedTitle; // We persist the body and title to localStorage // so in case the app crashes users don't loose content -if (localStorage) { - try { - storedBody = toState(JSON.parse(localStorage.getItem(LS_BODY_KEY) || '')); - storedTitle = localStorage.getItem(LS_TITLE_KEY); - } catch (err) { - localStorage.removeItem(LS_BODY_KEY); - localStorage.removeItem(LS_TITLE_KEY); - } -} - -const persistTitle = debounce((title: string) => { - localStorage.setItem(LS_TITLE_KEY, title); -}, 500); - -const persistBody = debounce(body => { - localStorage.setItem(LS_BODY_KEY, JSON.stringify(toJSON(body))); -}, 500); - class ComposerWithData extends Component { bodyEditor: any; constructor(props) { super(props); + let storedBody; + let storedTitle; + if (localStorage) { + try { + storedBody = toState( + JSON.parse(localStorage.getItem(LS_BODY_KEY) || '') + ); + storedTitle = localStorage.getItem(LS_TITLE_KEY); + } catch (err) { + localStorage.removeItem(LS_BODY_KEY); + localStorage.removeItem(LS_TITLE_KEY); + } + } + this.state = { title: storedTitle || '', body: storedBody || fromPlainText(''), @@ -123,6 +119,15 @@ class ComposerWithData extends Component { fetchingLinkPreview: false, postWasPublished: false, }; + + this.persistBodyToLocalStorageWithDebounce = debounce( + this.persistBodyToLocalStorageWithDebounce, + 500 + ); + this.persistTitleToLocalStorageWithDebounce = debounce( + this.persistTitleToLocalStorageWithDebounce, + 500 + ); } handleIncomingProps = props => { @@ -190,6 +195,27 @@ class ComposerWithData extends Component { }); }; + componentWillMount() { + let storedBody; + let storedTitle; + if (localStorage) { + try { + storedBody = toState( + JSON.parse(localStorage.getItem(LS_BODY_KEY) || '') + ); + storedTitle = localStorage.getItem(LS_TITLE_KEY); + } catch (err) { + localStorage.removeItem(LS_BODY_KEY); + localStorage.removeItem(LS_TITLE_KEY); + } + } + + this.setState({ + title: this.state.title || storedTitle || '', + body: this.state.body || storedBody || '', + }); + } + componentDidMount() { this.handleIncomingProps(this.props); // $FlowIssue @@ -226,11 +252,11 @@ class ComposerWithData extends Component { changeTitle = e => { const title = e.target.value; + this.persistTitleToLocalStorageWithDebounce(title); if (/\n$/g.test(title)) { this.bodyEditor.focus(); return; } - persistTitle(title); this.setState({ title, }); @@ -238,7 +264,7 @@ class ComposerWithData extends Component { changeBody = body => { this.listenForUrl(body); - persistBody(body); + this.persistBodyToLocalStorageWithDebounce(body); this.setState({ body, }); @@ -266,14 +292,46 @@ class ComposerWithData extends Component { } closeComposer = (clear?: string) => { + this.persistBodyToLocalStorage(this.state.body); + this.persistTitleToLocalStorage(this.state.title); // we will clear the composer if it unmounts as a result of a post // being published, that way the next composer open will start fresh - if (clear) return this.props.dispatch(closeComposer('', '')); + if (clear) { + this.clearEditorStateAfterPublish(); + } - // otherwise, we will save the editor state to rehydrate the title and - // body if the user reopens the composer in the same session - const { title, body } = this.state; - this.props.dispatch(closeComposer(title, body)); + return this.props.dispatch(closeComposer()); + }; + + clearEditorStateAfterPublish = () => { + try { + localStorage.removeItem(LS_BODY_KEY); + localStorage.removeItem(LS_TITLE_KEY); + } catch (err) { + console.error(err); + } + }; + + persistBodyToLocalStorageWithDebounce = body => { + return localStorage.setItem( + LS_BODY_KEY, + JSON.stringify(toJSON(this.state.body)) + ); + }; + + persistTitleToLocalStorageWithDebounce = title => { + return localStorage.setItem(LS_TITLE_KEY, this.state.title); + }; + + persistTitleToLocalStorage = title => { + return localStorage.setItem(LS_TITLE_KEY, this.state.title); + }; + + persistBodyToLocalStorage = body => { + return localStorage.setItem( + LS_BODY_KEY, + JSON.stringify(toJSON(this.state.body)) + ); }; setActiveCommunity = e => { @@ -387,6 +445,10 @@ class ComposerWithData extends Component { filesToUpload, }; + // one last save to localstorage + this.persistBodyToLocalStorage(this.state.body); + this.persistTitleToLocalStorage(this.state.title); + this.props .publishThread(thread) // after the mutation occurs, it will either return an error or the new @@ -396,8 +458,7 @@ class ComposerWithData extends Component { const id = data.publishThread.id; track('thread', 'published', null); - localStorage.removeItem(LS_BODY_KEY); - localStorage.removeItem(LS_TITLE_KEY); + this.clearEditorStateAfterPublish(); // stop the loading spinner on the publish button this.setState({ @@ -526,6 +587,7 @@ class ComposerWithData extends Component { ) : ( @@ -542,6 +604,7 @@ class ComposerWithData extends Component { ) : ( @@ -559,6 +622,7 @@ class ComposerWithData extends Component {