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 {