diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000000..6b9cb05220 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,24 @@ +# Contributing + +## Auto labelling + +The PR template contains a section that looks something like this: + +``` + +``` + +If you check one of these boxes the corresponding label will automatically be applied to the PR by our bot. For example ticking "WIP" will assign the "WIP: Building" label: + + +``` +- [x] WIP +``` diff --git a/admin/src/components/formElements/style.js b/admin/src/components/formElements/style.js index d16f4c83f5..b50f52a0ac 100644 --- a/admin/src/components/formElements/style.js +++ b/admin/src/components/formElements/style.js @@ -1,5 +1,6 @@ import styled, { css } from 'styled-components'; import { FlexRow, Transition } from '../globals'; +import Textarea from 'react-textarea-autosize'; export const StyledLabel = styled.label` display: flex; @@ -98,7 +99,7 @@ export const StyledInput = styled.input` } `; -export const StyledTextArea = styled.textarea` +export const StyledTextArea = styled(Textarea)` flex: 1 0 auto; width: 100%; background: ${({ theme }) => theme.bg.default}; diff --git a/dangerfile.js b/dangerfile.js index 7b60d69edb..7180717b4b 100644 --- a/dangerfile.js +++ b/dangerfile.js @@ -3,7 +3,9 @@ import path from 'path'; 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 noTestShortcuts from 'danger-plugin-no-test-shortcuts'; +import noConsole from 'danger-plugin-no-console'; const APP_FOLDERS = [ 'admin', @@ -17,7 +19,7 @@ const APP_FOLDERS = [ 'src', 'vulcan', ]; -const CHECKBOXES = /^- \[x\] *(.*)?$/gim; +const CHECKBOXES = /^\s*-\s*\[x\]\s*(.+?)$/gim; const possibleAutoLabels = { wip: 'WIP: Building', 'needs testing': 'WIP: Needs Testing', @@ -31,15 +33,18 @@ if (danger.github.pr.body.length < 10) { // Add automatic labels to the PR schedule(async () => { - const pr = danger.github.pr; + const pr = danger.github.thisPR; const api = danger.github.api; - const checkedBoxes = pr.body.match(CHECKBOXES); + const checkedBoxes = danger.github.pr.body.match(CHECKBOXES); if (!checkedBoxes || checkedBoxes.length === 0) return; - const matches = checkedBoxes.map(result => result[1]); + const matches = checkedBoxes + .map(result => new RegExp(CHECKBOXES.source, 'mi').exec(result)) + .filter(Boolean) + .map(res => res[1]); - const matchingLabels = matches.filter(match => - Object.keys(possibleAutoLabels).includes(match.toLowerCase()) + const matchingLabels = matches.filter( + match => Object.keys(possibleAutoLabels).indexOf(match.toLowerCase()) > -1 ); if (!matchingLabels || matchingLabels.length === 0) return; @@ -64,3 +69,14 @@ jest(); noTestShortcuts({ testFilePredicate: filePath => filePath.endsWith('.test.js'), }); + + +schedule(noConsole({ whitelist: ['error'] })); + +schedule( + flow({ + // Don't fail the build, only warn the submitter + warn: true, + blacklist: ['flow-typed/**/*.js', 'public/**/*.js'], + }) +); diff --git a/flow-typed/npm/danger-plugin-flow_vx.x.x.js b/flow-typed/npm/danger-plugin-flow_vx.x.x.js new file mode 100644 index 0000000000..271c742ab7 --- /dev/null +++ b/flow-typed/npm/danger-plugin-flow_vx.x.x.js @@ -0,0 +1,39 @@ +// flow-typed signature: 45f3a6b6919397f3ea67b75c1a9601bf +// flow-typed version: <>/danger-plugin-flow_vx.x.x/flow_v0.66.0 + +/** + * This is an autogenerated libdef stub for: + * + * 'danger-plugin-flow' + * + * Fill this stub out by replacing all the `any` types. + * + * Once filled out, we encourage you to share your work with the + * community by sending a pull request to: + * https://github.com/flowtype/flow-typed + */ + +declare module 'danger-plugin-flow' { + declare module.exports: any; +} + +/** + * We include stubs for each file inside this npm package in case you need to + * require those files directly. Feel free to delete any files that aren't + * needed. + */ +declare module 'danger-plugin-flow/dist/index' { + declare module.exports: any; +} + +declare module 'danger-plugin-flow/dist/index.test' { + declare module.exports: any; +} + +// Filename aliases +declare module 'danger-plugin-flow/dist/index.js' { + declare module.exports: $Exports<'danger-plugin-flow/dist/index'>; +} +declare module 'danger-plugin-flow/dist/index.test.js' { + declare module.exports: $Exports<'danger-plugin-flow/dist/index.test'>; +} diff --git a/flow-typed/npm/danger-plugin-no-console_vx.x.x.js b/flow-typed/npm/danger-plugin-no-console_vx.x.x.js new file mode 100644 index 0000000000..0a6a630ad4 --- /dev/null +++ b/flow-typed/npm/danger-plugin-no-console_vx.x.x.js @@ -0,0 +1,95 @@ +// flow-typed signature: be735722bffb485a8cf54a7c7080821a +// flow-typed version: <>/danger-plugin-no-console_v1.x/flow_v0.66.0 + +/** + * This is an autogenerated libdef stub for: + * + * 'danger-plugin-no-console' + * + * Fill this stub out by replacing all the `any` types. + * + * Once filled out, we encourage you to share your work with the + * community by sending a pull request to: + * https://github.com/flowtype/flow-typed + */ + +declare module 'danger-plugin-no-console' { + declare module.exports: any; +} + +/** + * We include stubs for each file inside this npm package in case you need to + * require those files directly. Feel free to delete any files that aren't + * needed. + */ +declare module 'danger-plugin-no-console/dist/index' { + declare module.exports: any; +} + +declare module 'danger-plugin-no-console/docs/script/inherited-summary' { + declare module.exports: any; +} + +declare module 'danger-plugin-no-console/docs/script/inner-link' { + declare module.exports: any; +} + +declare module 'danger-plugin-no-console/docs/script/manual' { + declare module.exports: any; +} + +declare module 'danger-plugin-no-console/docs/script/patch-for-local' { + declare module.exports: any; +} + +declare module 'danger-plugin-no-console/docs/script/prettify/prettify' { + declare module.exports: any; +} + +declare module 'danger-plugin-no-console/docs/script/pretty-print' { + declare module.exports: any; +} + +declare module 'danger-plugin-no-console/docs/script/search_index' { + declare module.exports: any; +} + +declare module 'danger-plugin-no-console/docs/script/search' { + declare module.exports: any; +} + +declare module 'danger-plugin-no-console/docs/script/test-summary' { + declare module.exports: any; +} + +// Filename aliases +declare module 'danger-plugin-no-console/dist/index.js' { + declare module.exports: $Exports<'danger-plugin-no-console/dist/index'>; +} +declare module 'danger-plugin-no-console/docs/script/inherited-summary.js' { + declare module.exports: $Exports<'danger-plugin-no-console/docs/script/inherited-summary'>; +} +declare module 'danger-plugin-no-console/docs/script/inner-link.js' { + declare module.exports: $Exports<'danger-plugin-no-console/docs/script/inner-link'>; +} +declare module 'danger-plugin-no-console/docs/script/manual.js' { + declare module.exports: $Exports<'danger-plugin-no-console/docs/script/manual'>; +} +declare module 'danger-plugin-no-console/docs/script/patch-for-local.js' { + declare module.exports: $Exports<'danger-plugin-no-console/docs/script/patch-for-local'>; +} +declare module 'danger-plugin-no-console/docs/script/prettify/prettify.js' { + declare module.exports: $Exports<'danger-plugin-no-console/docs/script/prettify/prettify'>; +} +declare module 'danger-plugin-no-console/docs/script/pretty-print.js' { + declare module.exports: $Exports<'danger-plugin-no-console/docs/script/pretty-print'>; +} +declare module 'danger-plugin-no-console/docs/script/search_index.js' { + declare module.exports: $Exports<'danger-plugin-no-console/docs/script/search_index'>; +} +declare module 'danger-plugin-no-console/docs/script/search.js' { + declare module.exports: $Exports<'danger-plugin-no-console/docs/script/search'>; +} +declare module 'danger-plugin-no-console/docs/script/test-summary.js' { + declare module.exports: $Exports<'danger-plugin-no-console/docs/script/test-summary'>; +} diff --git a/flow-typed/npm/rethinkdb-changefeed-reconnect_vx.x.x.js b/flow-typed/npm/rethinkdb-changefeed-reconnect_vx.x.x.js new file mode 100644 index 0000000000..3df6f6fba5 --- /dev/null +++ b/flow-typed/npm/rethinkdb-changefeed-reconnect_vx.x.x.js @@ -0,0 +1,74 @@ +// flow-typed signature: 592c0e5852cd969e5602279b98fb79c2 +// flow-typed version: <>/rethinkdb-changefeed-reconnect_v0.3.2/flow_v0.66.0 + +/** + * This is an autogenerated libdef stub for: + * + * 'rethinkdb-changefeed-reconnect' + * + * Fill this stub out by replacing all the `any` types. + * + * Once filled out, we encourage you to share your work with the + * community by sending a pull request to: + * https://github.com/flowtype/flow-typed + */ + +declare module 'rethinkdb-changefeed-reconnect' { + declare module.exports: any; +} + +/** + * We include stubs for each file inside this npm package in case you need to + * require those files directly. Feel free to delete any files that aren't + * needed. + */ +declare module 'rethinkdb-changefeed-reconnect/example/example.babel' { + declare module.exports: any; +} + +declare module 'rethinkdb-changefeed-reconnect/example/example' { + declare module.exports: any; +} + +declare module 'rethinkdb-changefeed-reconnect/example/index' { + declare module.exports: any; +} + +declare module 'rethinkdb-changefeed-reconnect/lib/__tests__/index.test' { + declare module.exports: any; +} + +declare module 'rethinkdb-changefeed-reconnect/lib/index' { + declare module.exports: any; +} + +declare module 'rethinkdb-changefeed-reconnect/src/__tests__/index.test' { + declare module.exports: any; +} + +declare module 'rethinkdb-changefeed-reconnect/src/index' { + declare module.exports: any; +} + +// Filename aliases +declare module 'rethinkdb-changefeed-reconnect/example/example.babel.js' { + declare module.exports: $Exports<'rethinkdb-changefeed-reconnect/example/example.babel'>; +} +declare module 'rethinkdb-changefeed-reconnect/example/example.js' { + declare module.exports: $Exports<'rethinkdb-changefeed-reconnect/example/example'>; +} +declare module 'rethinkdb-changefeed-reconnect/example/index.js' { + declare module.exports: $Exports<'rethinkdb-changefeed-reconnect/example/index'>; +} +declare module 'rethinkdb-changefeed-reconnect/lib/__tests__/index.test.js' { + declare module.exports: $Exports<'rethinkdb-changefeed-reconnect/lib/__tests__/index.test'>; +} +declare module 'rethinkdb-changefeed-reconnect/lib/index.js' { + declare module.exports: $Exports<'rethinkdb-changefeed-reconnect/lib/index'>; +} +declare module 'rethinkdb-changefeed-reconnect/src/__tests__/index.test.js' { + declare module.exports: $Exports<'rethinkdb-changefeed-reconnect/src/__tests__/index.test'>; +} +declare module 'rethinkdb-changefeed-reconnect/src/index.js' { + declare module.exports: $Exports<'rethinkdb-changefeed-reconnect/src/index'>; +} diff --git a/iris/models/community.js b/iris/models/community.js index def6fc13e6..6397b9c1cc 100644 --- a/iris/models/community.js +++ b/iris/models/community.js @@ -9,6 +9,7 @@ import { } from 'shared/bull/queues'; import { removeMemberInChannel } from './usersChannels'; import type { DBCommunity } from 'shared/types'; +import type { Timeframe } from './utils'; export const getCommunityById = (id: string): Promise => { return db @@ -589,7 +590,7 @@ export const getThreadCount = (communityId: string) => { export const getCommunityGrowth = async ( table: string, - range: string, + range: Timeframe, field: string, communityId: string, filter?: mixed diff --git a/iris/models/directMessageThread.js b/iris/models/directMessageThread.js index bb505259b9..dda7f6ed41 100644 --- a/iris/models/directMessageThread.js +++ b/iris/models/directMessageThread.js @@ -1,6 +1,6 @@ //@flow const { db } = require('./db'); -import { NEW_DOCUMENTS, eachAsyncNewValue } from './utils'; +import { NEW_DOCUMENTS, createChangefeed } from './utils'; export type DBDirectMessageThread = { createdAt: Date, @@ -80,8 +80,8 @@ const hasChanged = (field: string) => .ne(db.row('new_val')(field)); const THREAD_LAST_ACTIVE_CHANGED = hasChanged('threadLastActive'); -const listenToUpdatedDirectMessageThreads = (cb: Function): Function => { - return db +const getUpdatedDirectMessageThreadChangefeed = () => + db .table('directMessageThreads') .changes({ includeInitial: false, @@ -92,7 +92,14 @@ const listenToUpdatedDirectMessageThreads = (cb: Function): Function => { right: ['id', 'createdAt', 'threadId', 'lastActive', 'lastSeen'], }) .zip() - .run(eachAsyncNewValue(cb)); + .run(); + +const listenToUpdatedDirectMessageThreads = (cb: Function): Function => { + return createChangefeed( + getUpdatedDirectMessageThreadChangefeed, + cb, + 'listenToUpdatedDirectMessageThreads' + ); }; // prettier-ignore diff --git a/iris/models/message.js b/iris/models/message.js index 4121ef6ce4..5cedbdf302 100644 --- a/iris/models/message.js +++ b/iris/models/message.js @@ -6,7 +6,7 @@ import { processReputationEventQueue, _adminProcessToxicMessageQueue, } from 'shared/bull/queues'; -import { NEW_DOCUMENTS, eachAsyncNewValue } from './utils'; +import { NEW_DOCUMENTS, createChangefeed } from './utils'; import { setThreadLastActive } from './thread'; export type MessageTypes = 'text' | 'media'; @@ -149,14 +149,17 @@ export const storeMessage = ( }); }; -export const listenToNewMessages = (cb: Function): Function => { - return db +const getNewMessageChangefeed = () => + db .table('messages') .changes({ includeInitial: false, }) .filter(NEW_DOCUMENTS)('new_val') - .run(eachAsyncNewValue(cb)); + .run(); + +export const listenToNewMessages = (cb: Function): Function => { + return createChangefeed(getNewMessageChangefeed, cb, 'listenToNewMessages'); }; export const getMessageCount = (threadId: string): Promise => { diff --git a/iris/models/notification.js b/iris/models/notification.js index f7a3e91f93..e0bae8794b 100644 --- a/iris/models/notification.js +++ b/iris/models/notification.js @@ -1,6 +1,6 @@ // @flow const { db } = require('./db'); -import { NEW_DOCUMENTS, eachAsyncNewValue } from './utils'; +import { NEW_DOCUMENTS, createChangefeed } from './utils'; export const getNotificationsByUser = ( userId: string, @@ -62,8 +62,8 @@ const hasChanged = (field: string) => const MODIFIED_AT_CHANGED = hasChanged('entityAddedAt'); -export const listenToNewNotifications = (cb: Function): Function => { - return db +const getNewNotificationsChangefeed = () => + db .table('usersNotifications') .changes({ includeInitial: false, @@ -75,11 +75,18 @@ export const listenToNewNotifications = (cb: Function): Function => { }) .zip() .filter(row => row('context')('type').ne('DIRECT_MESSAGE_THREAD')) - .run(eachAsyncNewValue(cb)); + .run(); + +export const listenToNewNotifications = (cb: Function): Function => { + return createChangefeed( + getNewNotificationsChangefeed, + cb, + 'listenToNewNotifications' + ); }; -export const listenToNewDirectMessageNotifications = (cb: Function) => { - return db +const getNewDirectMessageNotificationsChangefeed = () => + db .table('usersNotifications') .changes({ includeInitial: false, @@ -91,5 +98,12 @@ export const listenToNewDirectMessageNotifications = (cb: Function) => { }) .zip() .filter(row => row('context')('type').eq('DIRECT_MESSAGE_THREAD')) - .run(eachAsyncNewValue(cb)); + .run(); + +export const listenToNewDirectMessageNotifications = (cb: Function) => { + return createChangefeed( + getNewDirectMessageNotificationsChangefeed, + cb, + 'listenToNewDirectMessageNotifications' + ); }; diff --git a/iris/models/thread.js b/iris/models/thread.js index f6a346533d..96c632c16c 100644 --- a/iris/models/thread.js +++ b/iris/models/thread.js @@ -6,11 +6,12 @@ import { sendThreadNotificationQueue, _adminProcessToxicThreadQueue, } from 'shared/bull/queues'; -const { NEW_DOCUMENTS, parseRange, eachAsyncNewValue } = require('./utils'); +const { NEW_DOCUMENTS, parseRange, createChangefeed } = require('./utils'); import { deleteMessagesInThread } from '../models/message'; import { turnOffAllThreadNotifications } from '../models/usersThreads'; import type { PaginationOptions } from '../utils/paginate-arrays'; import type { DBThread } from 'shared/types'; +import type { Timeframe } from './utils'; export const getThread = (threadId: string): Promise => { return db @@ -90,7 +91,7 @@ export const getThreadsByCommunity = ( export const getThreadsByCommunityInTimeframe = ( communityId: string, - range: string + range: Timeframe ): Promise> => { const { current } = parseRange(range); return db @@ -102,7 +103,7 @@ export const getThreadsByCommunityInTimeframe = ( }; export const getThreadsInTimeframe = ( - range: string + range: Timeframe ): Promise> => { const { current } = parseRange(range); return db @@ -130,7 +131,7 @@ export const getViewableThreadsByUser = async ( const getCurrentUsersChannelIds = db .table('usersChannels') .getAll(currentUser, { index: 'userId' }) - .filter({ isBlocked: false }) + .filter({ isBlocked: false, isMember: true }) .map(userChannel => userChannel('channelId')) .run(); @@ -200,7 +201,7 @@ export const getViewableParticipantThreadsByUser = async ( const getCurrentUsersChannelIds = db .table('usersChannels') .getAll(currentUser, { index: 'userId' }) - .filter({ isBlocked: false }) + .filter({ isBlocked: false, isMember: true }) .map(userChannel => userChannel('channelId')) .run(); @@ -257,6 +258,7 @@ export const getPublicParticipantThreadsByUser = ( return db .table('usersThreads') .getAll(evalUser, { index: 'userId' }) + .filter({ isParticipant: true }) .eqJoin('threadId', db.table('threads')) .without({ left: [ @@ -505,12 +507,19 @@ const hasChanged = (field: string) => .ne(db.row('new_val')(field)); const LAST_ACTIVE_CHANGED = hasChanged('lastActive'); -export const listenToUpdatedThreads = (cb: Function): Function => { - return db +const getUpdatedThreadsChangefeed = () => + db .table('threads') .changes({ includeInitial: false, }) .filter(NEW_DOCUMENTS.or(LAST_ACTIVE_CHANGED))('new_val') - .run(eachAsyncNewValue(cb)); + .run(); + +export const listenToUpdatedThreads = (cb: Function): Function => { + return createChangefeed( + getUpdatedThreadsChangefeed, + cb, + 'listenToUpdatedThreads' + ); }; diff --git a/iris/models/utils.js b/iris/models/utils.js index 81d0823072..2350efb034 100644 --- a/iris/models/utils.js +++ b/iris/models/utils.js @@ -1,42 +1,45 @@ -const { db } = require('./db'); +// @flow +const debug = require('debug')('iris:models:utils'); +import processChangefeed from 'rethinkdb-changefeed-reconnect'; +import { db } from './db'; +import Raven from 'shared/raven'; +import type { Cursor } from 'rethinkdbdash'; export const NEW_DOCUMENTS = db .row('old_val') .eq(null) .and(db.not(db.row('new_val').eq(null))); -export const eachAsyncNewValue = (cb: Function) => ( - err?: Error, - cursor: Cursor +export const createChangefeed = ( + getChangefeed: () => Promise, + callback: (arg: any) => void, + name?: string ) => { - if (err) throw err; - cursor - .eachAsync(data => { - // Call the passed callback with the message directly - cb(data); - }) - .catch(err => { + return processChangefeed( + getChangefeed, + callback, + err => { console.error(err); - try { - cursor.close(); - } catch (err) {} - }); -}; - -export const listenToNewDocumentsIn = (table, cb) => { - return ( - db - .table(table) - .changes({ - includeInitial: false, - }) - // Filter to only include newly inserted messages in the changefeed - .filter(NEW_DOCUMENTS) - .run(eachAsyncNewValue(cb)) + Raven.captureException(err); + }, + { + changefeedName: name, + attemptDelay: 60000, + maxAttempts: Infinity, + logger: { + // Ignore log and info logs in production + log: debug, + info: debug, + warn: console.warn.bind(console), + error: console.error.bind(console), + }, + } ); }; -export const parseRange = timeframe => { +export type Timeframe = 'daily' | 'weekly' | 'monthly' | 'quarterly'; + +export const parseRange = (timeframe?: Timeframe) => { switch (timeframe) { case 'daily': { return { current: 60 * 60 * 24, previous: 60 * 60 * 24 * 2 }; @@ -56,7 +59,7 @@ export const parseRange = timeframe => { } }; -export const getAu = (range: string) => { +export const getAu = (range: Timeframe) => { const { current } = parseRange(range); return db .table('users') @@ -68,7 +71,7 @@ export const getAu = (range: string) => { export const getGrowth = async ( table: string, - range: string, + range: Timeframe, field: string, filter: ?mixed ) => { diff --git a/iris/package.json b/iris/package.json index a01cfe9732..0aa08ec1bc 100644 --- a/iris/package.json +++ b/iris/package.json @@ -101,6 +101,7 @@ "redraft": "0.8.0", "redux": "^3.6.0", "redux-thunk": "^2.2.0", + "rethinkdb-changefeed-reconnect": "^0.3.2", "rethinkdb-inspector": "^0.3.3", "rethinkdb-migrate": "^1.1.0", "rethinkdbdash": "^2.3.29", diff --git a/iris/queries/community/topAndNewThreads.js b/iris/queries/community/topAndNewThreads.js index 8f13b6373a..aa16bacec2 100644 --- a/iris/queries/community/topAndNewThreads.js +++ b/iris/queries/community/topAndNewThreads.js @@ -15,7 +15,7 @@ export default ({ id }: DBCommunity, __: any, { user }: GraphQLContext) => { return new UserError('You must be signed in to continue.'); } - return getThreadsByCommunityInTimeframe(id, 'week').then(async threads => { + return getThreadsByCommunityInTimeframe(id, 'weekly').then(async threads => { if (!threads) return { topThreads: [], newThreads: [] }; const messageCountPromises = threads.map(async ({ id }) => ({ diff --git a/iris/queries/meta/topThreads.js b/iris/queries/meta/topThreads.js index 450433c3d9..db20ba954d 100644 --- a/iris/queries/meta/topThreads.js +++ b/iris/queries/meta/topThreads.js @@ -7,7 +7,7 @@ import { getMessageCount } from '../../models/message'; export default async (_: any, __: any, { user }: GraphQLContext) => { if (!isAdmin(user.id)) return null; - const threads = await getThreadsInTimeframe('week'); + const threads = await getThreadsInTimeframe('weekly'); const messageCountPromises = threads.map(async ({ id, ...thread }) => ({ id, diff --git a/iris/yarn.lock b/iris/yarn.lock index 7115273e21..2a97c87316 100644 --- a/iris/yarn.lock +++ b/iris/yarn.lock @@ -6200,6 +6200,12 @@ resolve@1.1.7: version "1.1.7" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.1.7.tgz#203114d82ad2c5ed9e8e0411b3932875e889e97b" +rethinkdb-changefeed-reconnect@^0.3.2: + version "0.3.2" + resolved "https://registry.yarnpkg.com/rethinkdb-changefeed-reconnect/-/rethinkdb-changefeed-reconnect-0.3.2.tgz#2999f5313205ab35d9ac2d1b0533765ee3376923" + dependencies: + babel-runtime "^6.18.0" + rethinkdb-inspector@^0.3.3: version "0.3.3" resolved "https://registry.yarnpkg.com/rethinkdb-inspector/-/rethinkdb-inspector-0.3.3.tgz#f0d88c66d17e0234b5518ca51cd8c272cb787003" diff --git a/package.json b/package.json index 8e259b6dfb..6bc812058a 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "cross-env": "^5.0.5", "danger": "^3.1.8", "danger-plugin-jest": "^1.1.0", + "danger-plugin-no-console": "^1.0.0", "danger-plugin-yarn": "^1.2.1", "eslint": "^4.15.0", "eslint-plugin-flowtype": "^2.39.1", @@ -64,6 +65,7 @@ "cookie-session": "^2.0.0-beta.3", "cors": "^2.8.3", "css.escape": "^1.5.1", + "danger-plugin-flow": "^1.2.1", "danger-plugin-no-test-shortcuts": "^2.0.0", "dataloader": "^1.3.0", "debounce": "^1.1.0", @@ -148,6 +150,7 @@ "redraft": "0.8.0", "redux": "^3.6.0", "redux-thunk": "^2.2.0", + "rethinkdb-changefeed-reconnect": "^0.3.2", "rethinkdb-inspector": "^0.3.3", "rethinkdb-migrate": "^1.1.0", "rethinkdbdash": "^2.3.29", diff --git a/shared/graphql/queries/user/getCurrentUserEverythingFeed.js b/shared/graphql/queries/user/getCurrentUserEverythingFeed.js index c521a3238d..b20e3bdf89 100644 --- a/shared/graphql/queries/user/getCurrentUserEverythingFeed.js +++ b/shared/graphql/queries/user/getCurrentUserEverythingFeed.js @@ -33,6 +33,9 @@ export const getCurrentUserEverythingQuery = gql` `; const getCurrentUserEverythingOptions = { + options: () => ({ + fetchPolicy: 'cache-and-network', + }), props: ({ ownProps, data: { diff --git a/shared/graphql/queries/user/getUserCommunityConnection.js b/shared/graphql/queries/user/getUserCommunityConnection.js index 5b5cb8a457..0d6fa5001c 100644 --- a/shared/graphql/queries/user/getUserCommunityConnection.js +++ b/shared/graphql/queries/user/getUserCommunityConnection.js @@ -40,12 +40,13 @@ const getUserCommunityConnectionOptions = { variables: { id, }, + fetchPolicy: 'cache-and-network', }), }; export const getCurrentUserCommunityConnection = graphql( getCurrentUserCommunityConnectionQuery, - { options: { fetchPolicy: 'cache-first' } } + { options: { fetchPolicy: 'cache-and-network' } } ); export const getUserCommunityConnection = graphql( diff --git a/src/components/formElements/style.js b/src/components/formElements/style.js index 1668c62261..a51a5367e1 100644 --- a/src/components/formElements/style.js +++ b/src/components/formElements/style.js @@ -1,5 +1,6 @@ import styled, { css } from 'styled-components'; import { FlexRow, Transition, hexa, zIndex } from '../globals'; +import Textarea from 'react-textarea-autosize'; export const StyledLabel = styled.label` display: flex; @@ -100,7 +101,7 @@ export const StyledInput = styled.input` } `; -export const StyledTextArea = styled.textarea` +export const StyledTextArea = styled(Textarea)` flex: 1 0 auto; width: 100%; background: ${({ theme }) => theme.bg.default}; diff --git a/src/components/loginButtonSet/facebook.js b/src/components/loginButtonSet/facebook.js index 48f5181aba..eefb21bf2a 100644 --- a/src/components/loginButtonSet/facebook.js +++ b/src/components/loginButtonSet/facebook.js @@ -5,19 +5,13 @@ import { FacebookButton, Label, A } from './style'; import Icon from '../icons'; export const FacebookSigninButton = (props: ButtonProps) => { - const { - verb = 'Sign in', - href, - preferred, - showAfter, - onClickHandler, - } = props; + const { href, preferred, showAfter, onClickHandler } = props; return ( onClickHandler && onClickHandler('facebook')} href={href}> - + ); diff --git a/src/components/loginButtonSet/github.js b/src/components/loginButtonSet/github.js index 25d79d631e..624d38cba6 100644 --- a/src/components/loginButtonSet/github.js +++ b/src/components/loginButtonSet/github.js @@ -5,19 +5,13 @@ import { GithubButton, Label, A } from './style'; import Icon from '../icons'; export const GithubSigninButton = (props: ButtonProps) => { - const { - verb = 'Sign in', - href, - preferred, - showAfter, - onClickHandler, - } = props; + const { href, preferred, showAfter, onClickHandler } = props; return ( onClickHandler && onClickHandler('github')} href={href}> - + ); diff --git a/src/components/loginButtonSet/google.js b/src/components/loginButtonSet/google.js index 871d9f86b5..f207e249f5 100644 --- a/src/components/loginButtonSet/google.js +++ b/src/components/loginButtonSet/google.js @@ -5,19 +5,13 @@ import { GoogleButton, Label, A } from './style'; import Icon from '../icons'; export const GoogleSigninButton = (props: ButtonProps) => { - const { - verb = 'Sign in', - href, - preferred, - showAfter, - onClickHandler, - } = props; + const { href, preferred, showAfter, onClickHandler } = props; return ( onClickHandler && onClickHandler('google')} href={href}> - + ); diff --git a/src/components/loginButtonSet/index.js b/src/components/loginButtonSet/index.js index 59dcf9b558..43bee2cd14 100644 --- a/src/components/loginButtonSet/index.js +++ b/src/components/loginButtonSet/index.js @@ -13,7 +13,6 @@ import { GithubSigninButton } from './github'; type Props = { redirectPath: ?string, location: Object, - signinType: string, }; export type ButtonProps = { @@ -21,7 +20,6 @@ export type ButtonProps = { href: string, preferred: boolean, showAfter: boolean, - verb: ?string, }; class LoginButtonSet extends React.Component { @@ -30,9 +28,7 @@ class LoginButtonSet extends React.Component { }; render() { - const { redirectPath, location, signinType } = this.props; - - const verb = signinType === 'login' ? 'Log in ' : 'Sign in '; + const { redirectPath, location } = this.props; let r; if (location) { @@ -60,7 +56,6 @@ class LoginButtonSet extends React.Component { href={`${SERVER_URL}/auth/twitter${postAuthRedirectPath}`} preferred={nonePreferred ? true : preferredSigninMethod === 'twitter'} showAfter={preferredSigninMethod === 'twitter'} - verb={verb} /> { nonePreferred ? true : preferredSigninMethod === 'facebook' } showAfter={preferredSigninMethod === 'facebook'} - verb={verb} /> { href={`${SERVER_URL}/auth/google${postAuthRedirectPath}`} preferred={nonePreferred ? true : preferredSigninMethod === 'google'} showAfter={preferredSigninMethod === 'google'} - verb={verb} /> { href={`${SERVER_URL}/auth/github${postAuthRedirectPath}`} preferred={nonePreferred ? true : preferredSigninMethod === 'github'} showAfter={preferredSigninMethod === 'github'} - verb={verb} /> ); diff --git a/src/components/loginButtonSet/twitter.js b/src/components/loginButtonSet/twitter.js index 2f032d6825..2ad68f3bab 100644 --- a/src/components/loginButtonSet/twitter.js +++ b/src/components/loginButtonSet/twitter.js @@ -5,19 +5,13 @@ import { TwitterButton, Label, A } from './style'; import Icon from '../icons'; export const TwitterSigninButton = (props: ButtonProps) => { - const { - verb = 'Sign in', - href, - preferred, - showAfter, - onClickHandler, - } = props; + const { href, preferred, showAfter, onClickHandler } = props; return ( onClickHandler && onClickHandler('twitter')} href={href}> - + ); diff --git a/src/components/modals/ChatInputLoginModal/index.js b/src/components/modals/ChatInputLoginModal/index.js index ef7da9c8ee..d3abe4bab4 100644 --- a/src/components/modals/ChatInputLoginModal/index.js +++ b/src/components/modals/ChatInputLoginModal/index.js @@ -32,7 +32,7 @@ class ChatInputLoginModal extends React.Component { /* TODO(@mxstbr): Fix this */ ariaHideApp={false} isOpen={isOpen} - contentLabel={'Log in or sign up'} + contentLabel={'Sign in'} onRequestClose={this.close} shouldCloseOnOverlayClick={true} style={styles} @@ -42,7 +42,7 @@ class ChatInputLoginModal extends React.Component { We pass the closeModal dispatch into the container to attach the action to the 'close' icon in the top right corner of all modals */} - + ( + {props.icon && } {props.heading && {props.heading}} {props.copy && {props.copy}} {props.children} @@ -187,7 +189,7 @@ export class UpsellSignIn extends React.Component { Already have an account?{' '} this.toggleSigningIn('login')}> {' '} - Log in + Sign in diff --git a/src/components/upsell/style.js b/src/components/upsell/style.js index 4e3cddb352..3dcacbec04 100644 --- a/src/components/upsell/style.js +++ b/src/components/upsell/style.js @@ -133,6 +133,11 @@ export const NullCol = styled(FlexCol)` align-items: center; position: relative; align-self: center; + + > div { + color: ${props => props.theme.text.alt}; + margin-bottom: 8px; + } `; export const NullRow = styled(FlexRow)` diff --git a/src/views/channel/index.js b/src/views/channel/index.js index 2bfbf875e5..5b0c71a723 100644 --- a/src/views/channel/index.js +++ b/src/views/channel/index.js @@ -124,12 +124,16 @@ class ChannelView extends React.Component { }/${channel.slug}` : `/login?r=${CLIENT_URL}/${channel.community.slug}/${channel.slug}`; + const redirectPath = `${CLIENT_URL}/${channel.community.slug}/${ + channel.slug + }`; + // if the channel is private but the user isn't logged in, redirect to the login page if (!isLoggedIn && channel.isPrivate) { if (channel.community.brandedLogin.isEnabled) { - return ; + return ; } else { - return ; + return ; } } diff --git a/src/views/communityLogin/index.js b/src/views/communityLogin/index.js index 48db335baf..6bd3715503 100644 --- a/src/views/communityLogin/index.js +++ b/src/views/communityLogin/index.js @@ -28,6 +28,7 @@ type Props = { ...$Exact, history: Object, match: Object, + redirectPath: ?string, }; export class Login extends React.Component { @@ -35,7 +36,7 @@ export class Login extends React.Component { this.props.history.push(`/${this.props.match.params.communitySlug}`); }; render() { - const { data: { community }, isLoading } = this.props; + const { data: { community }, isLoading, redirectPath } = this.props; if (community && community.id) { const { brandedLogin } = community; @@ -53,14 +54,17 @@ export class Login extends React.Component { src={community.profilePhoto} /> - Log in to the {community.name} community + Sign in to the {community.name} community {brandedLogin.message && brandedLogin.message.length > 0 ? brandedLogin.message : 'Spectrum is a place where communities can share, discuss, and grow together. Sign in below to get in on the conversation.'} - + By using Spectrum, you agree to our{' '} diff --git a/src/views/dashboard/components/communityList.js b/src/views/dashboard/components/communityList.js index 4a3f0e2059..ece66f8b59 100644 --- a/src/views/dashboard/components/communityList.js +++ b/src/views/dashboard/components/communityList.js @@ -68,8 +68,9 @@ class CommunityList extends React.Component { changedActiveCommunity || changedActiveChannel || changedCommunitiesAmount - ) + ) { return true; + } return false; } diff --git a/src/views/dashboard/components/inboxThread.js b/src/views/dashboard/components/inboxThread.js index 224add4285..3353c0de8b 100644 --- a/src/views/dashboard/components/inboxThread.js +++ b/src/views/dashboard/components/inboxThread.js @@ -16,8 +16,9 @@ import { ThreadTitle, AttachmentsContainer, ThreadMeta, - MetaText, - MetaTextPill, + StatusText, + NewThreadPill, + NewMessagePill, LockedTextPill, MiniLinkPreview, } from '../style'; @@ -54,13 +55,13 @@ class InboxThread extends React.Component { if (!data) return null; const defaultMessageCountString = ( - + {messageCount === 0 ? `${messageCount} messages` : messageCount > 1 ? `${messageCount} messages` : `${messageCount} message`} - + ); if (isLocked) { @@ -80,9 +81,9 @@ class InboxThread extends React.Component { } return ( - + New thread! - + ); } @@ -90,9 +91,9 @@ class InboxThread extends React.Component { if (active) return defaultMessageCountString; return ( - + New messages! - + ); } @@ -229,11 +230,11 @@ class WatercoolerThreadPure extends React.Component { )} {messageCount > 0 && ( - + {messageCount > 1 ? `${messageCount} messages` : `${messageCount} message`} - + )} diff --git a/src/views/dashboard/style.js b/src/views/dashboard/style.js index d3eb1e11a4..73634c6685 100644 --- a/src/views/dashboard/style.js +++ b/src/views/dashboard/style.js @@ -505,7 +505,7 @@ export const EmptyParticipantHead = styled.span` } `; -export const MetaText = styled.span` +export const StatusText = styled.span` font-size: 14px; color: ${props => props.new @@ -525,11 +525,11 @@ export const MetaText = styled.span` } `; -export const MetaTextPill = styled(MetaText)` +const StatusPill = styled(StatusText)` color: ${props => props.active ? props.theme.brand.alt : props.theme.text.reverse}; background: ${props => - props.active ? props.theme.text.reverse : props.theme.warn.alt}; + props.active ? props.theme.text.reverse : props.theme.brand.alt}; border-radius: 20px; padding: 0 12px; font-size: 11px; @@ -539,11 +539,25 @@ export const MetaTextPill = styled(MetaText)` align-items: center; `; -export const LockedTextPill = styled(MetaTextPill)` +export const NewThreadPill = styled(StatusPill)` + color: ${props => + props.active ? props.theme.brand.alt : props.theme.text.reverse}; + background: ${props => + props.active ? props.theme.text.reverse : props.theme.success.alt}; +`; + +export const NewMessagePill = styled(StatusPill)` + color: ${props => + props.active ? props.theme.brand.alt : props.theme.text.reverse}; + background: ${props => + props.active ? props.theme.text.reverse : props.theme.warn.alt}; +`; + +export const LockedTextPill = styled(StatusPill)` color: ${props => props.active ? props.theme.brand.alt : props.theme.text.alt}; background: ${props => - props.active ? props.theme.text.reverse : props.theme.bg.border}; + props.active ? props.theme.brand.wash : props.theme.bg.border}; `; export const MetaCommunityName = styled(Link)` diff --git a/src/views/login/index.js b/src/views/login/index.js index 354a829d44..5492cac5cb 100644 --- a/src/views/login/index.js +++ b/src/views/login/index.js @@ -26,7 +26,7 @@ export class Login extends React.Component { const viewSubtitle = signinType === 'login' - ? "We're happy to see you again - log in below to get back into the conversation!" + ? "We're happy to see you again - sign in below to get back into the conversation!" : 'Spectrum is a place where communities can share, discuss, and grow together. Sign in below to get in on the conversation.'; return ( diff --git a/src/views/splash/nav.js b/src/views/splash/nav.js index c375a1f656..36bbf3d6c1 100644 --- a/src/views/splash/nav.js +++ b/src/views/splash/nav.js @@ -80,7 +80,7 @@ class Nav extends Component { ) : ( - + )} @@ -123,7 +123,7 @@ class Nav extends Component { ) : ( - Log in + Sign in )} diff --git a/src/views/thread/components/actionBar.js b/src/views/thread/components/actionBar.js index f7593f9025..68ef63bd57 100644 --- a/src/views/thread/components/actionBar.js +++ b/src/views/thread/components/actionBar.js @@ -150,9 +150,7 @@ class ActionBar extends React.Component { loading={notificationStateLoading} onClick={this.toggleNotification} > - {thread.receiveNotifications - ? 'Following conversation' - : 'Follow conversation'} + {thread.receiveNotifications ? 'Subscribed' : 'Notify me'} ) : ( @@ -162,7 +160,7 @@ class ActionBar extends React.Component { tipText={'Get notified about replies'} tipLocation={'top-right'} > - Follow conversation + Notify me )} @@ -226,19 +224,19 @@ class ActionBar extends React.Component { onClick={this.toggleFlyout} /> - + {thread.receiveNotifications - ? 'Unfollow conversation' - : 'Follow conversation'} + ? 'Subscribed' + : 'Notify me'} @@ -246,12 +244,10 @@ class ActionBar extends React.Component { - + )} @@ -264,15 +260,11 @@ class ActionBar extends React.Component { hoverColor={ isPinned ? 'warn.default' : 'special.default' } - tipText={ - isPinned - ? 'Un-pin thread' - : `Pin in ${thread.community.name}` - } - tipLocation="top-left" onClick={this.props.togglePinThread} > - + )} @@ -280,10 +272,10 @@ class ActionBar extends React.Component { - Change channel + Move thread @@ -295,14 +287,14 @@ class ActionBar extends React.Component { icon={ thread.isLocked ? 'private' : 'private-unlocked' } - hoverColor="space.alt" - tipText={ - thread.isLocked ? 'Unlock chat' : 'Lock chat' + hoverColor={ + thread.isLocked ? 'success.default' : 'warn.alt' } - tipLocation="top-left" onClick={this.props.threadLock} > - + )} @@ -313,9 +305,7 @@ class ActionBar extends React.Component { diff --git a/src/views/thread/index.js b/src/views/thread/index.js index 16a0510760..1102ce9999 100644 --- a/src/views/thread/index.js +++ b/src/views/thread/index.js @@ -414,7 +414,10 @@ class ThreadContainer extends React.Component { {!isEditing && isLocked && ( - + )} @@ -490,7 +493,10 @@ class ThreadContainer extends React.Component { {!isEditing && isLocked && ( - + )} diff --git a/src/views/thread/style.js b/src/views/thread/style.js index 2c4e91a2be..2746e45c7f 100644 --- a/src/views/thread/style.js +++ b/src/views/thread/style.js @@ -110,8 +110,9 @@ export const ChatInputWrapper = styled(FlexCol)` export const DetailViewWrapper = styled(FlexCol)` background-image: ${({ theme }) => - `linear-gradient(to right, ${theme.bg.wash}, ${theme.bg - .default} 15%, ${theme.bg.default} 85%, ${theme.bg.wash})`}; + `linear-gradient(to right, ${theme.bg.wash}, ${theme.bg.default} 15%, ${ + theme.bg.default + } 85%, ${theme.bg.wash})`}; flex: auto; justify-content: flex-start; align-items: center; @@ -164,7 +165,9 @@ export const ContextRow = styled(FlexRow)` align-content: flex-start; `; -export const EditDone = styled.div`position: relative;`; +export const EditDone = styled.div` + position: relative; +`; export const DropWrap = styled(FlexCol)` width: 32px; @@ -223,10 +226,18 @@ export const FlyoutRow = styled(FlexRow)` } } - ${p => - p.hideBelow && + ${props => + props.hideBelow && + css` + @media (max-width: ${props.hideBelow}px) { + display: none; + } + `}; + + ${props => + props.hideAbove && css` - @media screen and (max-width: ${p.hideBelow}px) { + @media (min-width: ${props.hideAbove}px) { display: none; } `}; @@ -242,12 +253,20 @@ export const Byline = styled.div` font-size: 14px; `; -export const BylineMeta = styled(FlexCol)`margin-left: 12px;`; +export const BylineMeta = styled(FlexCol)` + margin-left: 12px; +`; -export const AuthorAvatar = styled(Avatar)`cursor: pointer;`; +export const AuthorAvatar = styled(Avatar)` + cursor: pointer; +`; -export const AuthorNameLink = styled(Link)`display: flex;`; -export const AuthorNameNoLink = styled.div`display: flex;`; +export const AuthorNameLink = styled(Link)` + display: flex; +`; +export const AuthorNameNoLink = styled.div` + display: flex; +`; export const AuthorName = styled(H3)` font-weight: 500; max-width: 100%; @@ -319,7 +338,9 @@ export const Timestamp = styled.span` } `; -export const Edited = styled(Timestamp)`margin-left: 4px;`; +export const Edited = styled(Timestamp)` + margin-left: 4px; +`; export const ChatWrapper = styled.div` width: 100%; @@ -518,10 +539,16 @@ export const PillLinkPinned = styled.div` `; export const PillLabel = styled.span` - ${props => props.isPrivate && css`position: relative;`}; + ${props => + props.isPrivate && + css` + position: relative; + `}; `; -export const Lock = styled.span`margin-right: 4px;`; +export const Lock = styled.span` + margin-right: 4px; +`; export const PinIcon = styled.span` margin-right: 4px; margin-left: -2px; @@ -556,7 +583,7 @@ export const FollowButton = styled(Button)` &:hover { background: ${props => props.theme.bg.default}; - color: ${props => props.theme.text.default}; + color: ${props => props.theme.brand.alt}; } @media (max-width: 768px) { @@ -696,7 +723,9 @@ export const RelatedCount = styled.p` color: ${props => props.theme.text.alt}; `; -export const Label = styled.p`font-size: 14px;`; +export const Label = styled.p` + font-size: 14px; +`; export const WatercoolerDescription = styled.h4` font-size: 18px; @@ -727,4 +756,6 @@ export const WatercoolerTitle = styled.h3` margin-bottom: 8px; `; -export const WatercoolerAvatar = styled(Avatar)`margin-bottom: 16px;`; +export const WatercoolerAvatar = styled(Avatar)` + margin-bottom: 16px; +`; diff --git a/src/views/userSettings/index.js b/src/views/userSettings/index.js index ccdd924329..5bf58c7e65 100644 --- a/src/views/userSettings/index.js +++ b/src/views/userSettings/index.js @@ -53,7 +53,7 @@ class UserSettings extends React.Component {