diff --git a/.eslintrc.js b/.eslintrc.js index a1e3ac97d4..c305e40647 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,5 +1,5 @@ module.exports = { - extends: 'eslint:recommended', + extends: ['eslint:recommended', 'plugin:react/recommended'], parser: 'babel-eslint', env: { es6: true, @@ -16,10 +16,18 @@ module.exports = { rules: { 'no-undef': 0, 'no-console': ['warn', { allow: ['warn', 'error'] }], - 'no-unused-vars': 0, + 'no-unused-vars': 1, 'no-empty': 0, 'no-useless-escape': 1, 'no-fallthrough': 1, 'no-extra-boolean-cast': 1, + 'react/prop-types': 0, + 'react/no-deprecated': 0, + 'react/display-name': 0, + 'react/no-find-dom-node': 1, + 'react/no-unescaped-entities': 'warn', + 'react/no-string-refs': 'warn', + 'react/jsx-no-target-blank': 'warn', + 'react/no-children-prop': 0, }, }; diff --git a/.gitignore b/.gitignore index 405378a4f0..4b7b617a71 100644 --- a/.gitignore +++ b/.gitignore @@ -34,4 +34,4 @@ desktop/release public/uploads cypress/screenshots/ cypress/videos/ -cacert +cacert \ No newline at end of file diff --git a/analytics/package.json b/analytics/package.json index 757c7420b8..c6a245d5b1 100644 --- a/analytics/package.json +++ b/analytics/package.json @@ -5,7 +5,7 @@ }, "dependencies": { "amplitude": "^3.5.0", - "aws-sdk": "^2.409.0", + "aws-sdk": "^2.426.0", "bull": "3.3.10", "datadog-metrics": "^0.8.1", "debug": "^4.1.1", diff --git a/analytics/yarn.lock b/analytics/yarn.lock index 7ea9a177da..bbf21a86db 100644 --- a/analytics/yarn.lock +++ b/analytics/yarn.lock @@ -14,10 +14,10 @@ asynckit@^0.4.0: resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" integrity sha1-x57Zf380y48robyXkLzDZkdLS3k= -aws-sdk@^2.409.0: - version "2.409.0" - resolved "https://registry.yarnpkg.com/aws-sdk/-/aws-sdk-2.409.0.tgz#d017060ba9e005487c68dc34a592af74d916f295" - integrity sha512-QV6j9zBQq/Kz8BqVOrJ03ABjMKtErXdUT1YdYEljoLQZimpzt0ZdQwJAsoZIsxxriOJgrqeZsQUklv9AFQaldQ== +aws-sdk@^2.426.0: + version "2.426.0" + resolved "https://registry.yarnpkg.com/aws-sdk/-/aws-sdk-2.426.0.tgz#cf17361c987daf518f945218f06135fbc1a3690d" + integrity sha512-S4nmIhF/6iYeVEmKUWVG03zo1sw3zELoAPGqBKIZ3isrXbxkFXdP2cgIQxqi37zwWXSqaxt0xjeXVOMLzN6vSg== dependencies: buffer "4.9.1" events "1.1.1" diff --git a/api/apollo-server.js b/api/apollo-server.js index 85d24c4544..65ca1f771d 100644 --- a/api/apollo-server.js +++ b/api/apollo-server.js @@ -1,11 +1,16 @@ // @flow +const debug = require('debug')('api:graphql'); import { ApolloServer } from 'apollo-server-express'; +import responseCachePlugin from 'apollo-server-plugin-response-cache'; import depthLimit from 'graphql-depth-limit'; import costAnalysis from 'graphql-cost-analysis'; +import { RedisCache } from 'apollo-server-cache-redis'; +import { config } from 'shared/cache/redis'; import createLoaders from './loaders'; import createErrorFormatter from './utils/create-graphql-error-formatter'; import schema from './schema'; import { setUserOnline } from 'shared/db/queries/user'; +import { statsd } from 'shared/statsd'; import { getUserIdFromReq } from './utils/session-store'; import UserError from './utils/UserError'; import type { DBUser } from 'shared/types'; @@ -55,6 +60,7 @@ const server = new ProtectedApolloServer({ req.statsdTags = { graphqlOperationName: req.body.operationName || 'unknown_operation', }; + debug(req.body.operationName || 'unknown_operation'); const loaders = createLoaders(); let currentUser = req.user && !req.user.bannedAt ? req.user : null; @@ -70,20 +76,17 @@ const server = new ProtectedApolloServer({ }, subscriptions: { path: '/websocket', - onOperation: (_: any, params: Object) => { - const errorFormatter = createErrorFormatter(); - params.formatError = errorFormatter; - return params; - }, onDisconnect: rawSocket => { + statsd.increment('websocket.connections', -1); return getUserIdFromReq(rawSocket.upgradeReq) .then(id => id && setUserOnline(id, false)) .catch(err => { console.error(err); }); }, - onConnect: (connectionParams, rawSocket) => - getUserIdFromReq(rawSocket.upgradeReq) + onConnect: (connectionParams, rawSocket) => { + statsd.increment('websocket.connections', 1); + return getUserIdFromReq(rawSocket.upgradeReq) .then(id => (id ? setUserOnline(id, true) : null)) .then(user => { return { @@ -96,7 +99,8 @@ const server = new ProtectedApolloServer({ return { loaders: createLoaders({ cache: false }), }; - }), + }); + }, }, playground: process.env.NODE_ENV !== 'production' && { settings: { @@ -118,8 +122,24 @@ const server = new ProtectedApolloServer({ maxFileSize: 25 * 1024 * 1024, // 25MB engine: false, tracing: false, - cacheControl: false, validationRules: [depthLimit(10)], + cacheControl: { + calculateHttpHeaders: false, + // Cache everything for at least a minute since we only cache public responses + defaultMaxAge: 60, + }, + cache: new RedisCache({ + ...config, + prefix: 'apollo-cache:', + }), + plugins: [ + responseCachePlugin({ + sessionId: ({ context }) => (context.user ? context.user.id : null), + // Only cache public responses + shouldReadFromCache: ({ context }) => !context.user, + shouldWriteToCache: ({ context }) => !context.user, + }), + ], }); export default server; diff --git a/api/migrations/20190315142923-backfill-userscommunities-last-seen-community-last-active.js b/api/migrations/20190315142923-backfill-userscommunities-last-seen-community-last-active.js new file mode 100644 index 0000000000..45afc9279d --- /dev/null +++ b/api/migrations/20190315142923-backfill-userscommunities-last-seen-community-last-active.js @@ -0,0 +1,59 @@ +exports.up = function(r, conn) { + return Promise.all([ + r + .table('usersCommunities') + .update( + { + lastSeen: r + .table('users') + .get(r.row('userId'))('lastSeen') + .default(r.row('lastSeen')), + }, + { + nonAtomic: true, + } + ) + .run(conn), + r + .table('communities') + .update( + { + lastActive: r + .table('threads') + .between( + [r.row('communityId'), r.minval], + [r.row('communityId'), r.maxval], + { + index: 'communityIdAndLastActive', + leftBound: 'open', + rightBound: 'open', + } + ) + .orderBy({ index: r.desc('communityIdAndLastActive') }) + .limit(1)('lastActive') + .default(r.row('createdAt')), + }, + { + nonAtomic: true, + } + ) + .run(conn), + ]); +}; + +exports.down = function(r, conn) { + return Promise.all([ + r + .table('usersCommunities') + .update({ + lastSeen: r.literal(), + }) + .run(conn), + r + .table('communities') + .update({ + lastActive: r.literal(), + }) + .run(conn), + ]); +}; diff --git a/api/migrations/seed/default/channelSettings.js b/api/migrations/seed/default/channelSettings.js new file mode 100644 index 0000000000..f054150eeb --- /dev/null +++ b/api/migrations/seed/default/channelSettings.js @@ -0,0 +1,18 @@ +const constants = require('./constants'); +const { PAYMENTS_PRIVATE_CHANNEL_ID } = constants; + +module.exports = [ + { + id: 1, + channelId: PAYMENTS_PRIVATE_CHANNEL_ID, + joinSettings: { + tokenJoinEnabled: true, + token: 'abc', + }, + slackSettings: { + botLinks: { + threadCreated: null, + }, + }, + }, +]; diff --git a/api/migrations/seed/default/communities.js b/api/migrations/seed/default/communities.js index 657bc2444b..8e3a3c9486 100644 --- a/api/migrations/seed/default/communities.js +++ b/api/migrations/seed/default/communities.js @@ -7,6 +7,7 @@ const { DELETED_COMMUNITY_ID, PRIVATE_COMMUNITY_ID, SINGLE_CHANNEL_COMMUNITY_ID, + PRIVATE_COMMUNITY_WITH_JOIN_TOKEN_ID, } = constants; module.exports = [ @@ -81,4 +82,18 @@ module.exports = [ slug: 'single', memberCount: 1, }, + { + id: PRIVATE_COMMUNITY_WITH_JOIN_TOKEN_ID, + createdAt: new Date(DATE), + isPrivate: true, + name: 'private community with join token', + description: 'private community with join token', + website: 'https://spectrum.chat', + profilePhoto: + 'https://spectrum.imgix.net/communities/-Kh6RfPYjmSaIWbkck8i/Twitter Profile.png.0.6225566835336693', + coverPhoto: + 'https://spectrum.imgix.net/communities/-Kh6RfPYjmSaIWbkck8i/Twitter Header.png.0.3303118636071434', + slug: 'private-join', + memberCount: 1, + }, ]; diff --git a/api/migrations/seed/default/communitySettings.js b/api/migrations/seed/default/communitySettings.js new file mode 100644 index 0000000000..e99fcdca42 --- /dev/null +++ b/api/migrations/seed/default/communitySettings.js @@ -0,0 +1,54 @@ +const constants = require('./constants'); +const { + PRIVATE_COMMUNITY_WITH_JOIN_TOKEN_ID, + PAYMENTS_COMMUNITY_ID, +} = constants; + +module.exports = [ + { + id: 1, + communityId: PRIVATE_COMMUNITY_WITH_JOIN_TOKEN_ID, + brandedLogin: { + isEnabled: false, + message: null, + }, + slackSettings: { + connectedAt: null, + connectedBy: null, + teamName: null, + teamId: null, + scope: null, + token: null, + invitesSentAt: null, + invitesMemberCount: null, + invitesCustomMessage: null, + }, + joinSettings: { + tokenJoinEnabled: true, + token: 'abc', + }, + }, + { + id: 2, + communityId: PAYMENTS_COMMUNITY_ID, + brandedLogin: { + isEnabled: false, + message: null, + }, + slackSettings: { + connectedAt: null, + connectedBy: null, + teamName: null, + teamId: null, + scope: null, + token: null, + invitesSentAt: null, + invitesMemberCount: null, + invitesCustomMessage: null, + }, + joinSettings: { + tokenJoinEnabled: true, + token: 'abc', + }, + }, +]; diff --git a/api/migrations/seed/default/constants.js b/api/migrations/seed/default/constants.js index 8482598e25..b18de227a9 100644 --- a/api/migrations/seed/default/constants.js +++ b/api/migrations/seed/default/constants.js @@ -20,6 +20,7 @@ const COMMUNITY_MODERATOR_USER_ID = '9'; // this user is only a member of one community, and that community only has // one channel - use for testing the composer community+channel selection const SINGLE_CHANNEL_COMMUNITY_USER_ID = '10'; +const NEW_USER_ID = '11'; // communities const SPECTRUM_COMMUNITY_ID = '1'; @@ -27,6 +28,7 @@ const PAYMENTS_COMMUNITY_ID = '2'; const DELETED_COMMUNITY_ID = '3'; const PRIVATE_COMMUNITY_ID = '4'; const SINGLE_CHANNEL_COMMUNITY_ID = '5'; +const PRIVATE_COMMUNITY_WITH_JOIN_TOKEN_ID = '6'; // channels const SPECTRUM_GENERAL_CHANNEL_ID = '1'; @@ -53,6 +55,8 @@ module.exports = { CHANNEL_MODERATOR_USER_ID, COMMUNITY_MODERATOR_USER_ID, SINGLE_CHANNEL_COMMUNITY_USER_ID, + PRIVATE_COMMUNITY_WITH_JOIN_TOKEN_ID, + NEW_USER_ID, SPECTRUM_COMMUNITY_ID, PAYMENTS_COMMUNITY_ID, DELETED_COMMUNITY_ID, diff --git a/api/migrations/seed/default/index.js b/api/migrations/seed/default/index.js index d1f58072c8..efa0b78c37 100644 --- a/api/migrations/seed/default/index.js +++ b/api/migrations/seed/default/index.js @@ -14,6 +14,8 @@ const defaultMessages = require('./messages'); const defaultReactions = require('./reactions'); const defaultUsersNotifications = require('./usersNotifications'); const defaultNotifications = require('./notifications'); +const defaultCommunitySettings = require('./communitySettings'); +const defaultChannelSettings = require('./channelSettings'); module.exports = { constants, @@ -30,7 +32,7 @@ module.exports = { defaultUsersSettings, defaultNotifications, defaultUsersNotifications, - defaultCommunitySettings: [], - defaultChannelSettings: [], + defaultCommunitySettings, + defaultChannelSettings, defaultReactions, }; diff --git a/api/migrations/seed/default/users.js b/api/migrations/seed/default/users.js index de99226995..7dfc2b590a 100644 --- a/api/migrations/seed/default/users.js +++ b/api/migrations/seed/default/users.js @@ -10,6 +10,7 @@ const { CHANNEL_MODERATOR_USER_ID, COMMUNITY_MODERATOR_USER_ID, SINGLE_CHANNEL_COMMUNITY_USER_ID, + NEW_USER_ID, DATE, } = constants; @@ -143,4 +144,18 @@ module.exports = [ createdAt: new Date(DATE), lastSeen: new Date(DATE), }, + { + id: NEW_USER_ID, + name: 'New user', + description: 'Just joined spectrum', + website: '', + username: null, + profilePhoto: + 'https://pbs.twimg.com/profile_images/848823167699230721/-9CbPtto_bigger.jpg', + coverPhoto: + 'https://pbs.twimg.com/profile_banners/17106008/1491444958/1500x500', + email: 'hi@newuser.io', + createdAt: new Date(DATE), + lastSeen: new Date(DATE), + }, ]; diff --git a/api/models/channel.js b/api/models/channel.js index 5b2cedd2c4..b8a92486e5 100644 --- a/api/models/channel.js +++ b/api/models/channel.js @@ -59,24 +59,15 @@ const getPublicChannelsByCommunity = (communityId: string): Promise> => { - const channels = await getChannelsByCommunity(communityId); + const channelIds = await channelsByCommunitiesQuery(communityId)('id').run(); - const channelIds = channels.map(c => c.id); - const publicChannels = channels.filter(c => !c.isPrivate).map(c => c.id); - - const usersChannels = await db + return db .table('usersChannels') - .getAll(userId, { index: 'userId' }) - .filter(usersChannel => - db.expr(channelIds).contains(usersChannel('channelId')) - ) - .filter({ isMember: true }) + .getAll(...channelIds.map(id => ([userId, id])), { + index: 'userIdAndChannelId', + }) + .filter({ isMember: true })('channelId') .run(); - - const usersChannelsIds = usersChannels.map(c => c.channelId); - const allPossibleChannels = [...publicChannels, ...usersChannelsIds]; - const distinct = allPossibleChannels.filter((x, i, a) => a.indexOf(x) === i); - return distinct; }; const getChannelsByUser = (userId: string): Promise> => { diff --git a/api/models/channelSettings.js b/api/models/channelSettings.js index 6716ada3ee..e6057da95f 100644 --- a/api/models/channelSettings.js +++ b/api/models/channelSettings.js @@ -9,7 +9,7 @@ import uuidv4 from 'uuid/v4'; const defaultSettings = { joinSettings: { tokenJoinEnabled: false, - message: null, + token: null, }, slackSettings: { botLinks: { diff --git a/api/models/community.js b/api/models/community.js index 7b8d9ea3c0..6598526a90 100644 --- a/api/models/community.js +++ b/api/models/community.js @@ -10,6 +10,7 @@ import { searchQueue, trackQueue, } from 'shared/bull/queues'; +import { createChangefeed } from 'shared/changefeed-utils'; import { events } from 'shared/analytics'; import type { DBCommunity, DBUser } from 'shared/types'; import type { Timeframe } from './utils'; @@ -190,6 +191,16 @@ export const getCommunitiesOnlineMemberCounts = ( .run(); }; +export const setCommunityLastActive = (id: string, lastActive: Date) => { + return db + .table('communities') + .get(id) + .update({ + lastActive: new Date(lastActive), + }) + .run(); +}; + export type CreateCommunityInput = { input: { name: string, @@ -725,3 +736,19 @@ export const setMemberCount = ( .run() .then(result => result.changes[0].new_val || result.changes[0].old_val); }; + +const getUpdatedCommunitiesChangefeed = () => + db + .table('communities') + .changes({ + includeInitial: false, + })('new_val') + .run(); + +export const listenToUpdatedCommunities = (cb: Function): Function => { + return createChangefeed( + getUpdatedCommunitiesChangefeed, + cb, + 'listenToUpdatedCommunities' + ); +}; diff --git a/api/models/notification.js b/api/models/notification.js index c452decbee..501381cef1 100644 --- a/api/models/notification.js +++ b/api/models/notification.js @@ -76,8 +76,7 @@ const getNewNotificationsChangefeed = () => .table('usersNotifications') .changes({ includeInitial: false, - }) - .filter(NEW_DOCUMENTS.or(ENTITY_ADDED))('new_val') + })('new_val') .eqJoin('notificationId', db.table('notifications')) .without({ left: ['notificationId', 'createdAt', 'id', 'entityAddedAt'], diff --git a/api/models/test/__snapshots__/channel.test.js.snap b/api/models/test/__snapshots__/channel.test.js.snap index d204241896..1843d3a227 100644 --- a/api/models/test/__snapshots__/channel.test.js.snap +++ b/api/models/test/__snapshots__/channel.test.js.snap @@ -175,10 +175,9 @@ Array [ exports[`models/channel getChannelsByUserAndCommunity returns correct set of channels 1`] = ` Array [ - "5", - "1", - "8", "2", + "1", + "5", ] `; diff --git a/api/models/thread.js b/api/models/thread.js index 1ce33742f8..9d852b37b7 100644 --- a/api/models/thread.js +++ b/api/models/thread.js @@ -6,7 +6,7 @@ import { trackQueue, searchQueue, } from 'shared/bull/queues'; -const { NEW_DOCUMENTS, parseRange } = require('./utils'); +const { parseRange } = require('./utils'); import { createChangefeed } from 'shared/changefeed-utils'; import { deleteMessagesInThread } from '../models/message'; import { turnOffAllThreadNotifications } from '../models/usersThreads'; @@ -15,6 +15,9 @@ import type { DBThread, FileUpload } from 'shared/types'; import type { Timeframe } from './utils'; import { events } from 'shared/analytics'; +const NOT_WATERCOOLER = thread => + db.not(thread.hasFields('watercooler')).or(thread('watercooler').eq(false)); + export const getThread = (threadId: string): Promise => { return db .table('threads') @@ -93,7 +96,9 @@ export const getThreadsByChannels = ( return db .table('threads') .getAll(...channelIds, { index: 'channelId' }) - .filter(thread => db.not(thread.hasFields('deletedAt'))) + .filter(thread => + db.not(thread.hasFields('deletedAt')).and(NOT_WATERCOOLER(thread)) + ) .orderBy(...order) .skip(after || 0) .limit(first || 999999) @@ -110,7 +115,7 @@ export const getThreadsByCommunity = (communityId: string): Promise db.not(thread.hasFields('deletedAt'))) + .filter(thread => db.not(thread.hasFields('deletedAt')).and(NOT_WATERCOOLER(thread))) .run(); }; @@ -121,7 +126,7 @@ export const getThreadsByCommunityInTimeframe = (communityId: string, range: Tim .table('threads') .getAll(communityId, { index: 'communityId' }) .filter(db.row('createdAt').during(db.now().sub(current), db.now())) - .filter(thread => db.not(thread.hasFields('deletedAt'))) + .filter(thread => db.not(thread.hasFields('deletedAt')).and(NOT_WATERCOOLER(thread))) .run(); }; @@ -588,9 +593,11 @@ export const editThread = (input: EditThreadInput, userId: string, shouldUpdate: { content: input.content, modifiedAt: shouldUpdate ? new Date() : null, + editedBy: userId, edits: db.row('edits').append({ content: db.row('content'), timestamp: new Date(), + editedBy: db.row('editedBy').default(db.row('creatorId')) }), }, { returnChanges: 'always' } @@ -753,19 +760,12 @@ export const decrementReactionCount = (threadId: string) => { .run(); }; -const hasChanged = (field: string) => - db - .row('old_val')(field) - .ne(db.row('new_val')(field)); -const LAST_ACTIVE_CHANGED = hasChanged('lastActive'); - const getUpdatedThreadsChangefeed = () => db .table('threads') .changes({ includeInitial: false, - }) - .filter(NEW_DOCUMENTS.or(LAST_ACTIVE_CHANGED))('new_val') + })('new_val') .run(); export const listenToUpdatedThreads = (cb: Function): Function => { diff --git a/api/models/usersCommunities.js b/api/models/usersCommunities.js index dc69563cde..e73eb270a2 100644 --- a/api/models/usersCommunities.js +++ b/api/models/usersCommunities.js @@ -84,6 +84,7 @@ export const createMemberInCommunity = (communityId: string, userId: string): Pr createdAt: new Date(), isMember: true, receiveNotifications: true, + lastSeen: new Date(), }, { returnChanges: 'always' } ) @@ -97,6 +98,7 @@ export const createMemberInCommunity = (communityId: string, userId: string): Pr communityId, userId, createdAt: new Date(), + lastSeen: new Date(), isMember: true, isOwner: false, isModerator: false, @@ -647,7 +649,7 @@ export const checkUserPermissionsInCommunity = (communityId: string, userId: str .run(); }; -type UserIdAndCommunityId = [string, string]; +type UserIdAndCommunityId = [?string, string]; // prettier-ignore export const getUsersPermissionsInCommunities = (input: Array) => { @@ -706,3 +708,26 @@ export const getUsersTotalReputation = (userIds: Array): Promise { + return db + .table('usersCommunities') + .getAll([userId, communityId], { index: 'userIdAndCommunityId' }) + .update( + { + lastSeen: db.branch( + db.row('lastSeen').lt(lastSeen), + lastSeen, + db.row('lastSeen') + ), + }, + { + returnChanges: true, + } + ) + .run(); +}; diff --git a/api/mutations/community/createCommunity.js b/api/mutations/community/createCommunity.js index bb9fb53d65..efd07ca559 100644 --- a/api/mutations/community/createCommunity.js +++ b/api/mutations/community/createCommunity.js @@ -3,13 +3,19 @@ import type { GraphQLContext } from '../../'; import type { CreateCommunityInput } from '../../models/community'; import UserError from '../../utils/UserError'; import { communitySlugIsBlacklisted } from '../../utils/permissions'; -import { getCommunitiesBySlug, createCommunity } from '../../models/community'; +import { + getCommunitiesBySlug, + createCommunity, + setCommunityWatercoolerId, +} from '../../models/community'; import { createOwnerInCommunity } from '../../models/usersCommunities'; import { createGeneralChannel } from '../../models/channel'; import { createOwnerInChannel } from '../../models/usersChannels'; +import { publishThread } from '../../models/thread'; import { isAuthedResolver as requireAuth } from '../../utils/permissions'; import { trackQueue } from 'shared/bull/queues'; import { events } from 'shared/analytics'; +import Raven from 'shared/raven'; export default requireAuth( async (_: any, args: CreateCommunityInput, { user }: GraphQLContext) => { @@ -92,23 +98,33 @@ export default requireAuth( const community = await createCommunity(sanitizedArgs, user); // create a new relationship with the community - const communityRelationship = await createOwnerInCommunity( - community.id, - user.id - ); + await createOwnerInCommunity(community.id, user.id); // create a default 'general' channel const generalChannel = await createGeneralChannel(community.id, user.id); // create a new relationship with the general channel - const generalChannelRelationship = createOwnerInChannel( - generalChannel.id, - user.id - ); + await createOwnerInChannel(generalChannel.id, user.id); + + try { + const watercooler = await publishThread( + { + channelId: generalChannel.id, + communityId: community.id, + content: { + title: `${community.name} watercooler`, + }, + type: 'DRAFTJS', + watercooler: true, + }, + user.id + ); - return Promise.all([ - communityRelationship, - generalChannelRelationship, - ]).then(() => community); + return setCommunityWatercoolerId(community.id, watercooler.id); + // Do not fail community creation if the watercooler creation does not work out + } catch (err) { + Raven.captureException(err); + return community; + } } ); diff --git a/api/mutations/community/enableCommunityWatercooler.js b/api/mutations/community/enableCommunityWatercooler.js index 80faf367cf..6878f77b73 100644 --- a/api/mutations/community/enableCommunityWatercooler.js +++ b/api/mutations/community/enableCommunityWatercooler.js @@ -57,7 +57,7 @@ export default requireAuth(async (_: any, args: Args, ctx: GraphQLContext) => { channelId: channel.id, communityId: community.id, content: { - title: `${community.name} watercooler`, + title: `${community.name} Chat`, }, type: 'DRAFTJS', watercooler: true, diff --git a/api/mutations/community/index.js b/api/mutations/community/index.js index c066620d60..43019eac47 100644 --- a/api/mutations/community/index.js +++ b/api/mutations/community/index.js @@ -16,6 +16,7 @@ import disableCommunityTokenJoin from './disableCommunityTokenJoin'; import resetCommunityJoinToken from './resetCommunityJoinToken'; import enableCommunityWatercooler from './enableCommunityWatercooler'; import disableCommunityWatercooler from './disableCommunityWatercooler'; +import setCommunityLastSeen from './setCommunityLastSeen'; module.exports = { Mutation: { @@ -36,5 +37,6 @@ module.exports = { resetCommunityJoinToken, enableCommunityWatercooler, disableCommunityWatercooler, + setCommunityLastSeen, }, }; diff --git a/api/mutations/community/setCommunityLastSeen.js b/api/mutations/community/setCommunityLastSeen.js new file mode 100644 index 0000000000..52ef62f9b3 --- /dev/null +++ b/api/mutations/community/setCommunityLastSeen.js @@ -0,0 +1,19 @@ +// @flow +import { setCommunityLastSeen } from '../../models/usersCommunities'; +import { getCommunityById } from '../../models/community'; +import { isAuthedResolver } from '../../utils/permissions'; + +type Args = { + input: { + id: string, + lastSeen: Date, + }, +}; + +export default isAuthedResolver( + (_: void, { input: { id, lastSeen } }: Args, { user }) => { + return setCommunityLastSeen(id, user.id, lastSeen).then(() => + getCommunityById(id) + ); + } +); diff --git a/api/mutations/message/addMessage.js b/api/mutations/message/addMessage.js index e0d6030c68..4fbd07ef61 100644 --- a/api/mutations/message/addMessage.js +++ b/api/mutations/message/addMessage.js @@ -1,6 +1,4 @@ // @flow -import { stateFromMarkdown } from 'draft-js-import-markdown'; -import { convertToRaw } from 'draft-js'; import type { GraphQLContext } from '../../'; import UserError from '../../utils/UserError'; import { uploadImage } from '../../utils/file-storage'; @@ -9,6 +7,8 @@ import { setDirectMessageThreadLastActive } from '../../models/directMessageThre import { setUserLastSeenInDirectMessageThread } from '../../models/usersDirectMessageThreads'; import { createMemberInChannel } from '../../models/usersChannels'; import { createParticipantInThread } from '../../models/usersThreads'; +import { setCommunityLastActive } from '../../models/community'; +import { setCommunityLastSeen } from '../../models/usersCommunities'; import addCommunityMember from '../communityMember/addCommunityMember'; import { trackUserThreadLastSeenQueue } from 'shared/bull/queues'; import type { FileUpload } from 'shared/types'; @@ -351,48 +351,65 @@ export default requireAuth(async (_: any, args: Input, ctx: GraphQLContext) => { ); } - return membershipPromise() - .then(() => createParticipantInThread(message.threadId, user.id)) - .then(async () => { - const contextPermissions = { - communityId: thread.communityId, - reputation: communityPermissions ? communityPermissions.reputation : 0, - isModerator: communityPermissions - ? communityPermissions.isModerator - : false, - isOwner: communityPermissions ? communityPermissions.isOwner : false, - }; - - trackUserThreadLastSeenQueue.add({ - userId: user.id, - threadId: message.threadId, - timestamp: Date.now(), - }); - - calculateThreadScoreQueue.add( - { + const timestamp = new Date(dbMessage.timestamp).getTime(); + return ( + membershipPromise() + .then(() => createParticipantInThread(message.threadId, user.id)) + .then(() => + setCommunityLastActive(thread.communityId, new Date(timestamp)) + ) + // Make sure Community.lastSeen > Community.lastActive by one second + // for the author + .then(() => + setCommunityLastSeen( + thread.communityId, + user.id, + new Date(timestamp + 10000) + ) + ) + .then(async () => { + const contextPermissions = { + communityId: thread.communityId, + reputation: communityPermissions + ? communityPermissions.reputation + : 0, + isModerator: communityPermissions + ? communityPermissions.isModerator + : false, + isOwner: communityPermissions ? communityPermissions.isOwner : false, + }; + + trackUserThreadLastSeenQueue.add({ + userId: user.id, threadId: message.threadId, - }, - { - jobId: message.threadId, - } - ); - return { - ...dbMessage, - contextPermissions, - }; - }) - .catch(err => { - trackQueue.add({ - userId: user.id, - event: eventFailed, - properties: { - message, - reason: 'unknown error', - error: err.message, - }, - }); - console.error('Error sending message', err); - return dbMessage; - }); + timestamp, + }); + + calculateThreadScoreQueue.add( + { + threadId: message.threadId, + }, + { + jobId: message.threadId, + } + ); + return { + ...dbMessage, + contextPermissions, + }; + }) + .catch(err => { + trackQueue.add({ + userId: user.id, + event: eventFailed, + properties: { + message, + reason: 'unknown error', + error: err.message, + }, + }); + console.error('Error sending message', err); + return dbMessage; + }) + ); }); diff --git a/api/mutations/thread/editThread.js b/api/mutations/thread/editThread.js index fd6630db97..fb2f143068 100644 --- a/api/mutations/thread/editThread.js +++ b/api/mutations/thread/editThread.js @@ -64,13 +64,15 @@ export default requireAuth(async (_: any, args: Input, ctx: GraphQLContext) => { getUserPermissionsInChannel(threadToEvaluate.channelId, user.id), ]); + const canEdit = + !channelPermissions.isBlocked && + !communityPermissions.isBlocked && + (threadToEvaluate.creatorId === user.id || + communityPermissions.isModerator || + communityPermissions.isOwner); // only the thread creator can edit the thread // also prevent deletion if the user was blocked - if ( - threadToEvaluate.creatorId !== user.id || - channelPermissions.isBlocked || - communityPermissions.isBlocked - ) { + if (!canEdit) { trackQueue.add({ userId: user.id, event: events.THREAD_EDITED_FAILED, diff --git a/api/mutations/thread/publishThread.js b/api/mutations/thread/publishThread.js index 58d66a1aa0..d88822516a 100644 --- a/api/mutations/thread/publishThread.js +++ b/api/mutations/thread/publishThread.js @@ -2,13 +2,6 @@ const debug = require('debug')('api:mutations:thread:publish-thread'); import stringSimilarity from 'string-similarity'; import slugg from 'slugg'; -import { - convertToRaw, - convertFromRaw, - EditorState, - SelectionState, -} from 'draft-js'; -import { stateFromMarkdown } from 'draft-js-import-markdown'; import type { GraphQLContext } from '../../'; import UserError from '../../utils/UserError'; import { addMessage } from '../message/addMessage'; @@ -21,12 +14,9 @@ import { } from '../../models/thread'; import { createParticipantInThread } from '../../models/usersThreads'; import type { FileUpload, DBThread } from 'shared/types'; -import { - toPlainText, - toJSON, - toState, - fromPlainText, -} from 'shared/draft-utils'; +import { toPlainText, toState } from 'shared/draft-utils'; +import { setCommunityLastActive } from '../../models/community'; +import { setCommunityLastSeen } from '../../models/usersCommunities'; import { processReputationEventQueue, sendThreadNotificationQueue, @@ -344,8 +334,19 @@ export default requireAuth( }); } - // create a relationship between the thread and the author - await createParticipantInThread(dbThread.id, user.id); + // create a relationship between the thread and the author and set community lastActive + const timestamp = new Date(dbThread.createdAt).getTime(); + await Promise.all([ + createParticipantInThread(dbThread.id, user.id), + setCommunityLastActive(dbThread.communityId, new Date(timestamp)), + // Make sure Community.lastSeen > Community.lastActive by one second + // for the author + setCommunityLastSeen( + dbThread.communityId, + user.id, + new Date(timestamp + 1000) + ), + ]); // Post a new message with a link to the new thread to the watercooler thread if one exists if (community.watercoolerId && !channel.isPrivate) { diff --git a/api/package.json b/api/package.json index 650cb19560..924ed0f483 100644 --- a/api/package.json +++ b/api/package.json @@ -3,9 +3,11 @@ "node": "^10.0.0" }, "dependencies": { - "algoliasearch": "^3.32.0", + "algoliasearch": "^3.32.1", "apollo-local-query": "^0.3.1", - "apollo-server-express": "^2.4.8", + "apollo-server-cache-redis": "^0.3.1", + "apollo-server-express": "2.5.0-alpha.0", + "apollo-server-plugin-response-cache": "^0.1.0-alpha.0", "apollo-upload-client": "^9.1.0", "aws-sdk": "2.200.0", "axios": "^0.16.2", @@ -19,7 +21,7 @@ "body-parser": "^1.18.3", "bull": "^3.7.0", "casual": "^1.5.12", - "compression": "^1.7.3", + "compression": "^1.7.4", "cookie-parser": "^1.4.4", "cookie-session": "^2.0.0-beta.3", "cors": "^2.8.5", @@ -56,9 +58,9 @@ "graphql-log": "^0.1.3", "graphql-rate-limit": "^1.2.4", "graphql-tools": "^4.0.4", - "helmet": "^3.15.1", + "helmet": "^3.16.0", "highlight.js": "^9.15.6", - "history": "^4.6.1", + "history": "^4.9.0", "hoist-non-react-statics": "^2.5.5", "host-validation": "^1.2.0", "hot-shots": "^5.9.2", @@ -71,7 +73,7 @@ "iterall": "^1.2.2", "jest": "^21.2.1", "json-stringify-pretty-compact": "^1.2.0", - "jsonwebtoken": "^8.5.0", + "jsonwebtoken": "^8.5.1", "keygrip": "^1.0.3", "linkify-it": "^2.1.0", "localstorage-memory": "^1.0.3", @@ -90,7 +92,7 @@ "passport-google-oauth2": "^0.1.6", "passport-twitter": "^1.0.4", "pre-commit": "^1.2.2", - "prismjs": "^1.15.0", + "prismjs": "^1.16.0", "query-string": "5.1.1", "ratelimiter": "^3.3.0", "raven": "^2.6.4", @@ -107,7 +109,7 @@ "react-router": "^4.0.0-beta.7", "react-router-dom": "^4.0.0-beta.7", "react-textarea-autosize": "^4.0.5", - "react-transition-group": "^2.6.0", + "react-transition-group": "^2.7.0", "react-trend": "^1.2.4", "recompose": "^0.23.1", "redis-tag-cache": "^1.2.1", diff --git a/api/queries/community/communityPermissions.js b/api/queries/community/communityPermissions.js index 7a481f4e60..499c12df4a 100644 --- a/api/queries/community/communityPermissions.js +++ b/api/queries/community/communityPermissions.js @@ -25,6 +25,7 @@ export default async ( ...defaultPermissions, userId: user.id, communityId: id, + lastSeen: null, }; return permissions || fallbackPermissions; diff --git a/api/queries/community/threadConnection.js b/api/queries/community/threadConnection.js index d3aee98d81..186cfec1bb 100644 --- a/api/queries/community/threadConnection.js +++ b/api/queries/community/threadConnection.js @@ -1,6 +1,5 @@ // @flow import type { DBCommunity } from 'shared/types'; -import type { PaginationOptions } from '../../utils/paginate-arrays'; import type { GraphQLContext } from '../../'; import { encode, decode } from '../../utils/base64'; import { @@ -41,10 +40,18 @@ export default async (root: DBCommunity, args: CommunityThreadConnectionPaginati // if the user is signed in, only return stories for the channels // the user is a member of -> this will ensure that they don't see // stories in private channels that they aren't a member of. - // if the user is *not* signed in, only get threads from public channels - // within the community + // if the user is *not* signed in or not a member, only get threads + // from public channels within the community let channels; + let isMember = false; if (user) { + const permissions = await loaders.userPermissionsInCommunity.load([ + user.id, + id, + ]); + isMember = permissions && permissions.isMember; + } + if (user && isMember) { channels = await getChannelsByUserAndCommunity(id, currentUser.id); } else { channels = await getPublicChannelsByCommunity(id); diff --git a/api/queries/directMessageThread/index.js b/api/queries/directMessageThread/index.js index 0fd447678b..5b44df2855 100644 --- a/api/queries/directMessageThread/index.js +++ b/api/queries/directMessageThread/index.js @@ -1,6 +1,6 @@ // @flow import directMessageThread from './rootDirectMessageThread'; -import directMessageThreadByUserId from './rootDirectMessageThreadByUserId'; +import directMessageThreadByUserIds from './rootDirectMessageThreadByUserIds'; import messageConnection from './messageConnection'; import participants from './participants'; import snippet from './snippet'; @@ -8,7 +8,7 @@ import snippet from './snippet'; module.exports = { Query: { directMessageThread, - directMessageThreadByUserId, + directMessageThreadByUserIds, }, DirectMessageThread: { messageConnection, diff --git a/api/queries/directMessageThread/rootDirectMessageThreadByUserId.js b/api/queries/directMessageThread/rootDirectMessageThreadByUserIds.js similarity index 84% rename from api/queries/directMessageThread/rootDirectMessageThreadByUserId.js rename to api/queries/directMessageThread/rootDirectMessageThreadByUserIds.js index ff375050e6..9899618239 100644 --- a/api/queries/directMessageThread/rootDirectMessageThreadByUserId.js +++ b/api/queries/directMessageThread/rootDirectMessageThreadByUserIds.js @@ -4,15 +4,15 @@ import { checkForExistingDMThread } from '../../models/directMessageThread'; import { isAuthedResolver as requireAuth } from '../../utils/permissions'; type Args = { - userId: string, + userIds: Array, }; export default requireAuth(async (_: any, args: Args, ctx: GraphQLContext) => { // signed out users will never be able to view a dm thread const { user: currentUser, loaders } = ctx; - const { userId } = args; + const { userIds } = args; - const allMemberIds = [userId, currentUser.id]; + const allMemberIds = [...userIds, currentUser.id]; const existingThread = await checkForExistingDMThread(allMemberIds); if (!existingThread) return null; diff --git a/api/queries/thread/editedBy.js b/api/queries/thread/editedBy.js new file mode 100644 index 0000000000..c05ae0da87 --- /dev/null +++ b/api/queries/thread/editedBy.js @@ -0,0 +1,47 @@ +// @flow +import type { GraphQLContext } from '../../'; +import type { DBThread } from 'shared/types'; + +export default async ( + { editedBy, communityId, channelId }: DBThread, + _: any, + { loaders }: GraphQLContext +) => { + if (!editedBy) return null; + let [ + user, + communityPermissions = {}, + channelPermissions = {}, + ] = await Promise.all([ + loaders.user.load(editedBy), + loaders.userPermissionsInCommunity.load([editedBy, communityId]), + loaders.userPermissionsInChannel.load([editedBy, channelId]), + ]); + + if (!communityPermissions) communityPermissions = {}; + if (!channelPermissions) channelPermissions = {}; + + const isMember = communityPermissions.isMember || channelPermissions.isMember; + const isOwner = communityPermissions.isOwner; + const isModerator = + communityPermissions.isModerator || channelPermissions.isModerator; + const isBlocked = + channelPermissions.isBlocked || communityPermissions.isBlocked; + const reputation = communityPermissions.reputation; + + const roles = []; + if (isModerator) roles.push('moderator'); + if (isOwner) roles.push('admin'); + if (isBlocked) roles.push('blocked'); + + return { + id: communityPermissions.id, + user, + isOwner, + isModerator, + isBlocked, + isMember, + reputation, + roles, + }; +}; diff --git a/api/queries/thread/index.js b/api/queries/thread/index.js index 0a5e8426b3..1be578ae39 100644 --- a/api/queries/thread/index.js +++ b/api/queries/thread/index.js @@ -16,6 +16,7 @@ import currentUserLastSeen from './currentUserLastSeen'; import content from './content'; import reactions from './reactions'; import metaImage from './metaImage'; +import editedBy from './editedBy'; import type { DBThread } from 'shared/types'; @@ -40,5 +41,6 @@ module.exports = { reactions, metaImage, messageCount: ({ messageCount }: DBThread) => messageCount || 0, + editedBy, }, }; diff --git a/api/queries/thread/rootThread.js b/api/queries/thread/rootThread.js index 9ecea27877..275e16ebfe 100644 --- a/api/queries/thread/rootThread.js +++ b/api/queries/thread/rootThread.js @@ -13,6 +13,8 @@ export default async ( } const thread = await loaders.thread.load(id); + + if (!thread) return null; // If the threads score hasn't been updated in the past // 24 hours add a new job to the queue to update it if ( diff --git a/api/routes/middlewares/index.js b/api/routes/middlewares/index.js index 3d52548201..b310a8cde7 100644 --- a/api/routes/middlewares/index.js +++ b/api/routes/middlewares/index.js @@ -2,9 +2,18 @@ import { Router } from 'express'; const middlewares = Router(); +import threadParamRedirect from 'shared/middlewares/thread-param'; +middlewares.use(threadParamRedirect); + import bodyParser from 'body-parser'; middlewares.use(bodyParser.json()); +middlewares.use((req, res, next) => { + if (req.method === 'POST' && req.url !== '/api') console.log('POST', req.url); + + next(); +}); + if (process.env.NODE_ENV === 'development') { const logging = require('shared/middlewares/logging'); middlewares.use(logging); @@ -63,8 +72,4 @@ middlewares.use((req, res, next) => { next(); }); -// This needs to come after passport otherwise we'll always redirect logged-in users -import threadParamRedirect from 'shared/middlewares/thread-param'; -middlewares.use(threadParamRedirect); - export default middlewares; diff --git a/api/schema.js b/api/schema.js index 3dc8148749..6854a93ab8 100644 --- a/api/schema.js +++ b/api/schema.js @@ -62,6 +62,7 @@ const messageSubscriptions = require('./subscriptions/message'); const notificationSubscriptions = require('./subscriptions/notification'); const directMessageThreadSubscriptions = require('./subscriptions/directMessageThread'); const threadSubscriptions = require('./subscriptions/thread'); +const communitySubscriptions = require('./subscriptions/community'); const rateLimit = require('./utils/rate-limit-directive').default; @@ -132,7 +133,8 @@ const resolvers = merge( messageSubscriptions, notificationSubscriptions, directMessageThreadSubscriptions, - threadSubscriptions + threadSubscriptions, + communitySubscriptions ); if (process.env.NODE_ENV === 'development' && debug.enabled) { diff --git a/api/subscriptions/community.js b/api/subscriptions/community.js new file mode 100644 index 0000000000..9dbe007cc0 --- /dev/null +++ b/api/subscriptions/community.js @@ -0,0 +1,61 @@ +// @flow +const debug = require('debug')('api:subscriptions:community'); +import { listenToUpdatedCommunities } from '../models/community'; +import { getUsersPermissionsInCommunities } from '../models/usersCommunities'; +import { getCommunitiesByUser } from '../models/community'; +import asyncify from '../utils/asyncify'; +import UserError from '../utils/UserError'; +import Raven from 'shared/raven'; +import type { GraphQLContext } from '../'; + +const addCommunityListener = asyncify(listenToUpdatedCommunities); + +module.exports = { + Subscription: { + communityUpdated: { + resolve: (community: any) => community, + subscribe: async ( + _: any, + { communityIds }: { communityIds: Array }, + { user }: GraphQLContext + ) => { + if (!communityIds && (!user || !user.id)) + return new UserError( + 'Please provide a list of channels to listen to when not signed in.' + ); + + let ids = communityIds; + if (ids === undefined || ids === null) { + // If no specific communities were passed listen to all the users communities + const usersCommunities = await getCommunitiesByUser(user.id); + ids = usersCommunities.map(({ id }) => id); + } else { + // If specific communities were passed make sure the user has permission to those communities + const permissions = await getUsersPermissionsInCommunities( + ids.map(id => [user ? user.id : null, id]) + ); + ids = permissions + .filter( + ({ isMember, isOwner, isModerator }) => + isMember === true || isOwner === true || isModerator === true + ) + .map(({ communityId }) => communityId); + } + + debug( + `@${user.username || user.id} listening to ${ + ids.length + } communities updates` + ); + return addCommunityListener({ + filter: community => community && ids.includes(community.id), + onError: err => { + // Don't crash the whole API server on error in the listener + console.error(err); + Raven.captureException(err); + }, + }); + }, + }, + }, +}; diff --git a/api/subscriptions/message.js b/api/subscriptions/message.js index c29ddd8a15..8a919148c6 100644 --- a/api/subscriptions/message.js +++ b/api/subscriptions/message.js @@ -12,7 +12,6 @@ import Raven from 'shared/raven'; const addMessageListener = asyncify(listenToNewMessages); import type { GraphQLContext } from '../'; -import type { GraphQLResolveInfo } from 'graphql'; /** * Define the message subscription resolvers @@ -24,8 +23,7 @@ module.exports = { subscribe: async ( _: any, { thread }: { thread: string }, - { user }: GraphQLContext, - info: GraphQLResolveInfo + { user }: GraphQLContext ) => { // Make sure the user has the permission to view the thread before // subscribing them to changes diff --git a/api/subscriptions/thread.js b/api/subscriptions/thread.js index 6f5e164ee0..fe495cc695 100644 --- a/api/subscriptions/thread.js +++ b/api/subscriptions/thread.js @@ -9,7 +9,6 @@ import asyncify from '../utils/asyncify'; import UserError from '../utils/UserError'; import Raven from 'shared/raven'; import type { GraphQLContext } from '../'; -import type { GraphQLResolveInfo } from 'graphql'; const addThreadListener = asyncify(listenToUpdatedThreads); @@ -20,8 +19,7 @@ module.exports = { subscribe: async ( _: any, { channelIds }: { channelIds: Array }, - { user }: GraphQLContext, - info: GraphQLResolveInfo + { user }: GraphQLContext ) => { if (!channelIds && (!user || !user.id)) return new UserError( diff --git a/api/types/Channel.js b/api/types/Channel.js index fe3a193801..00f7bb6701 100644 --- a/api/types/Channel.js +++ b/api/types/Channel.js @@ -60,7 +60,7 @@ const Channel = /* GraphQL */ ` userId: ID! } - type Channel { + type Channel @cacheControl(maxAge: 1200) { id: ID! createdAt: Date! modifiedAt: Date @@ -71,16 +71,17 @@ const Channel = /* GraphQL */ ` isDefault: Boolean isArchived: Boolean channelPermissions: ChannelPermissions! @cost(complexity: 1) + communityPermissions: CommunityPermissions! - community: Community! @cost(complexity: 1) + community: Community! @cost(complexity: 1) @cacheControl(maxAge: 86400) threadConnection(first: Int = 10, after: String): ChannelThreadsConnection! @cost(complexity: 1, multipliers: ["first"]) memberConnection(first: Int = 10, after: String): ChannelMembersConnection! @cost(complexity: 1, multipliers: ["first"]) memberCount: Int! metaData: ChannelMetaData @cost(complexity: 1) - pendingUsers: [User] @cost(complexity: 3) - blockedUsers: [User] @cost(complexity: 3) + pendingUsers: [User] @cost(complexity: 3) @cacheControl(maxAge: 0) + blockedUsers: [User] @cost(complexity: 3) @cacheControl(maxAge: 0) moderators: [User] @cost(complexity: 3) owners: [User] @cost(complexity: 3) joinSettings: JoinSettings @@ -92,7 +93,7 @@ const Channel = /* GraphQL */ ` id: ID channelSlug: LowercaseString communitySlug: LowercaseString - ): Channel @cost(complexity: 1) + ): Channel @cost(complexity: 1) @cacheControl(maxAge: 1200) } input ArchiveChannelInput { diff --git a/api/types/ChannelSlackSettings.js b/api/types/ChannelSlackSettings.js index 5bfa251594..d8f2ea9a95 100644 --- a/api/types/ChannelSlackSettings.js +++ b/api/types/ChannelSlackSettings.js @@ -3,14 +3,14 @@ const ChannelSlackSettings = /* GraphQL */ ` enum BotLinksEventType { threadCreated } - + type BotLinks { threadCreated: String } - type ChannelSlackSettings { - botLinks: BotLinks - } + type ChannelSlackSettings { + botLinks: BotLinks + } input UpdateChannelSlackBotLinksInput { channelId: String @@ -19,8 +19,8 @@ const ChannelSlackSettings = /* GraphQL */ ` } extend type Mutation { - updateChannelSlackBotLinks(input: UpdateChannelSlackBotLinksInput): Channel - } + updateChannelSlackBotLinks(input: UpdateChannelSlackBotLinksInput): Channel + } `; module.exports = ChannelSlackSettings; diff --git a/api/types/Community.js b/api/types/Community.js index 46cdc75a46..0b64aeaa90 100644 --- a/api/types/Community.js +++ b/api/types/Community.js @@ -135,7 +135,7 @@ const Community = /* GraphQL */ ` subscriptions: [StripeSubscription] } - type Community { + type Community @cacheControl(maxAge: 1200) { id: ID! createdAt: Date name: String! @@ -148,7 +148,9 @@ const Community = /* GraphQL */ ` pinnedThreadId: String pinnedThread: Thread isPrivate: Boolean + lastActive: Date communityPermissions: CommunityPermissions @cost(complexity: 1) + channelConnection: CommunityChannelsConnection @cost(complexity: 1) members( first: Int = 10 @@ -163,16 +165,21 @@ const Community = /* GraphQL */ ` metaData: CommunityMetaData @cost(complexity: 10) memberGrowth: GrowthData @cost(complexity: 10) conversationGrowth: GrowthData @cost(complexity: 3) + topMembers: [CommunityMember] @cost(complexity: 10) + topAndNewThreads: TopAndNewThreads @cost(complexity: 4) + watercooler: Thread brandedLogin: BrandedLogin joinSettings: JoinSettings slackSettings: CommunitySlackSettings @cost(complexity: 2) + watercoolerId: String slackImport: SlackImport @cost(complexity: 2) @deprecated(reason: "Use slack settings field instead") + memberConnection( first: Int = 10 after: String @@ -195,11 +202,12 @@ const Community = /* GraphQL */ ` extend type Query { community(id: ID, slug: LowercaseString): Community + @cacheControl(maxAge: 1200) communities( slugs: [LowercaseString] ids: [ID] curatedContentType: String - ): [Community] + ): [Community] @cacheControl(maxAge: 1200) topCommunities(amount: Int = 20): [Community!] @cost(complexity: 4, multipliers: ["amount"]) recentCommunities: [Community!] @@ -307,6 +315,11 @@ const Community = /* GraphQL */ ` id: ID! } + input SetCommunityLastSeenInput { + id: ID! + lastSeen: Date! + } + extend type Mutation { createCommunity(input: CreateCommunityInput!): Community @rateLimit(max: 3, window: "15m") @@ -343,6 +356,11 @@ const Community = /* GraphQL */ ` disableCommunityWatercooler( input: DisableCommunityWatercoolerInput! ): Community + setCommunityLastSeen(input: SetCommunityLastSeenInput!): Community + } + + extend type Subscription { + communityUpdated(communityIds: [ID!]): Community } `; diff --git a/api/types/CommunityMember.js b/api/types/CommunityMember.js index 249d69c698..3899c311d7 100644 --- a/api/types/CommunityMember.js +++ b/api/types/CommunityMember.js @@ -1,6 +1,6 @@ // @flow const CommunityMember = /* GraphQL */ ` - type CommunityMember { + type CommunityMember @cacheControl(maxAge: 600) { id: ID! user: User! roles: [String] @@ -10,6 +10,7 @@ const CommunityMember = /* GraphQL */ ` isBlocked: Boolean isPending: Boolean reputation: Int + lastSeen: Date } extend type Query { diff --git a/api/types/CommunitySlackSettings.js b/api/types/CommunitySlackSettings.js index 00f77f7851..5387ddd529 100644 --- a/api/types/CommunitySlackSettings.js +++ b/api/types/CommunitySlackSettings.js @@ -1,18 +1,18 @@ // @flow const CommunitySlackSettings = /* GraphQL */ ` type SlackChannel { - id: String - name: String - } + id: String + name: String + } - type CommunitySlackSettings { - isConnected: Boolean - hasSentInvites: Boolean - teamName: String - memberCount: Int - invitesSentAt: Date - slackChannelList: [ SlackChannel ] - } + type CommunitySlackSettings { + isConnected: Boolean + hasSentInvites: Boolean + teamName: String + memberCount: Int + invitesSentAt: Date + slackChannelList: [SlackChannel] + } `; module.exports = CommunitySlackSettings; diff --git a/api/types/DirectMessageThread.js b/api/types/DirectMessageThread.js index 616e951dbc..11ec0dee83 100644 --- a/api/types/DirectMessageThread.js +++ b/api/types/DirectMessageThread.js @@ -34,7 +34,7 @@ const DirectMessageThread = /* GraphQL */ ` extend type Query { directMessageThread(id: ID!): DirectMessageThread - directMessageThreadByUserId(userId: ID!): DirectMessageThread + directMessageThreadByUserIds(userIds: [ID!]): DirectMessageThread } enum MessageType { diff --git a/api/types/Message.js b/api/types/Message.js index 341b450b6e..96160092f1 100644 --- a/api/types/Message.js +++ b/api/types/Message.js @@ -20,7 +20,7 @@ const Message = /* GraphQL */ ` hasReacted: Boolean } - type Message { + type Message @cacheControl(maxAge: 600) { id: ID! timestamp: Date! thread: Thread diff --git a/api/types/Meta.js b/api/types/Meta.js index dd238f6804..a44a1d1ba7 100644 --- a/api/types/Meta.js +++ b/api/types/Meta.js @@ -56,7 +56,9 @@ const Meta = /* GraphQL */ ` } extend type Mutation { - saveUserCommunityPermissions(input: SaveUserCommunityPermissionsInput!): User + saveUserCommunityPermissions( + input: SaveUserCommunityPermissionsInput! + ): User } `; diff --git a/api/types/Reaction.js b/api/types/Reaction.js index 8f8e9f4e57..dc6d8fdead 100644 --- a/api/types/Reaction.js +++ b/api/types/Reaction.js @@ -1,30 +1,30 @@ // @flow const Reaction = /* GraphQL */ ` - enum ReactionTypes { - like - } + enum ReactionTypes { + like + } - type Reaction { - id: ID! - timestamp: Date! - message: Message! - user: User! - type: ReactionTypes! - } + type Reaction @cacheControl(maxAge: 84700) { + id: ID! + timestamp: Date! + message: Message! + user: User! + type: ReactionTypes! + } - input ReactionInput { - messageId: ID! - type: ReactionTypes! - } + input ReactionInput { + messageId: ID! + type: ReactionTypes! + } - extend type Query { - reaction(id: String!): Reaction - } + extend type Query { + reaction(id: String!): Reaction + } - extend type Mutation { - # Returns true if toggling completed successfully - toggleReaction(reaction: ReactionInput!): Message - } + extend type Mutation { + # Returns true if toggling completed successfully + toggleReaction(reaction: ReactionInput!): Message + } `; module.exports = Reaction; diff --git a/api/types/Search.js b/api/types/Search.js index d6e9fb7289..6a2cf45e4a 100644 --- a/api/types/Search.js +++ b/api/types/Search.js @@ -26,7 +26,7 @@ const Search = /* GraphQL */ ` everythingFeed: Boolean } - type SearchResults { + type SearchResults @cacheControl(maxAge: 600) { searchResultsConnection: SearchResultsConnection } diff --git a/api/types/Thread.js b/api/types/Thread.js index 620bd60922..1d5dc64fb7 100644 --- a/api/types/Thread.js +++ b/api/types/Thread.js @@ -29,6 +29,7 @@ const Thread = /* GraphQL */ ` type Edit { timestamp: Date! content: ThreadContent! + # editedBy: User! } enum ThreadType { @@ -42,12 +43,13 @@ const Thread = /* GraphQL */ ` data: String } - type Thread { + type Thread @cacheControl(maxAge: 1200) { id: ID! createdAt: Date! modifiedAt: Date + editedBy: ThreadParticipant @cost(complexity: 2) channel: Channel! - community: Community! @cost(complexity: 1) + community: Community! @cost(complexity: 1) @cacheControl(maxAge: 84700) isPublished: Boolean! content: ThreadContent! isLocked: Boolean @@ -72,6 +74,7 @@ const Thread = /* GraphQL */ ` attachments: [Attachment] @deprecated(reason: "Attachments no longer used for link previews") isCreator: Boolean @deprecated(reason: "Use Thread.isAuthor instead") + creator: User! @deprecated(reason: "Use Thread.author instead") participants: [User] @cost(complexity: 1) @@ -86,7 +89,7 @@ const Thread = /* GraphQL */ ` } extend type Query { - thread(id: ID!): Thread + thread(id: ID!): Thread @cacheControl(maxAge: 1200) searchThreads(queryString: String!, filter: SearchThreadsFilter): [Thread] @deprecated(reason: "Use the new Search query endpoint") } diff --git a/api/types/User.js b/api/types/User.js index 6ac9c8b676..f01bc0f6bc 100644 --- a/api/types/User.js +++ b/api/types/User.js @@ -79,7 +79,7 @@ const User = /* GraphQL */ ` username: String } - type User { + type User @cacheControl(maxAge: 600) { id: ID! name: String firstName: String @@ -96,11 +96,12 @@ const User = /* GraphQL */ ` timezone: Int totalReputation: Int pendingEmail: LowercaseString - betaSupporter: Boolean + betaSupporter: Boolean @cacheControl(maxAge: 84700) isPro: Boolean @deprecated(reason: "Use the betaSupporter field instead") recurringPayments: [RecurringPayment] @deprecated(reason: "Payments are no longer used") + invoices: [Invoice] @deprecated(reason: "Payments are no longer used") # non-schema fields @@ -118,8 +119,10 @@ const User = /* GraphQL */ ` after: String kind: ThreadConnectionType ): UserThreadsConnection! @cost(complexity: 1, multipliers: ["first"]) + everything(first: Int = 10, after: String): EverythingThreadsConnection! @cost(complexity: 1, multipliers: ["first"]) + settings: UserSettings @cost(complexity: 1) githubProfile: GithubProfile @@ -128,8 +131,8 @@ const User = /* GraphQL */ ` } extend type Query { - user(id: ID, username: LowercaseString): User - currentUser: User + user(id: ID, username: LowercaseString): User @cacheControl(maxAge: 1200) + currentUser: User @cacheControl(maxAge: 1200, scope: PRIVATE) searchUsers(string: String): [User] @deprecated(reason: "Use the new Search query endpoint") } diff --git a/api/types/general.js b/api/types/general.js index 0caf9a1eab..9e68bfa8a7 100644 --- a/api/types/general.js +++ b/api/types/general.js @@ -26,6 +26,7 @@ const general = /* GraphQL */ ` isPending: Boolean receiveNotifications: Boolean reputation: Int + lastSeen: Date } type ContextPermissions diff --git a/api/utils/permissions.js b/api/utils/permissions.js index de0a306977..4b02da22d4 100644 --- a/api/utils/permissions.js +++ b/api/utils/permissions.js @@ -141,18 +141,19 @@ export const canViewCommunity = async (user: DBUser, communityId: string, loader const community = await communityExists(communityId, loaders); if (!community) return false; - if (!community.isPrivate) return true - - if (!user) return false + if (!user) { + if (community.isPrivate) return false + return true + } const communityPermissions = await loaders.userPermissionsInCommunity.load([ user.id, communityId, ]); - if (!communityPermissions) return false; - if (communityPermissions.isBlocked) return false - if (!communityPermissions.isMember) return false + if (community.isPrivate && !communityPermissions) return false; + if (communityPermissions && communityPermissions.isBlocked) return false + if (community.isPrivate && !communityPermissions.isMember) return false return true; } @@ -181,22 +182,41 @@ export const canViewThread = async ( if (!channel || !community) return false; if (channel.deletedAt || community.deletedAt) return false; - if (!channel.isPrivate && !community.isPrivate) return true; - - if (channel.isPrivate) - return ( - channelPermissions && - channelPermissions.isMember && - !channelPermissions.isBlocked - ); - if (community.isPrivate) - return ( - communityPermissions && - communityPermissions.isMember && - !communityPermissions.isBlocked - ); + if (userId) { + if (channel.isPrivate) { + if ( + !channelPermissions || + channelPermissions.isBlocked || + !channelPermissions.isMember + ) { + return false; + } + + return true; + } + + if (community.isPrivate) { + if ( + !communityPermissions || + communityPermissions.isBlocked || + !communityPermissions.isMember + ) { + return false; + } + + return true; + } + + if (communityPermissions) { + return !communityPermissions.isBlocked; + } + + if (channelPermissions) { + return !channelPermissions.isBlocked; + } + } - return false; + return !channel.isPrivate && !community.isPrivate; }; export const canViewDMThread = async ( @@ -229,11 +249,12 @@ export const canViewChannel = async (user: DBUser, channelId: string, loaders: a const community = await communityExists(channel.communityId, loaders); if (!community) return false - - if (!channel.isPrivate && !community.isPrivate) return true - - if (!user) return false - + + if (!user) { + if (!community.isPrivate && !channel.isPrivate) return true + return false + } + const [ communityPermissions, channelPermissions @@ -247,7 +268,7 @@ export const canViewChannel = async (user: DBUser, channelId: string, loaders: a channel.id, ]) ]) - + if (channel.isPrivate && !channelPermissions) return false if (community.isPrivate && !communityPermissions) return false if (channel.isPrivate && !channelPermissions.isMember) return false diff --git a/api/yarn.lock b/api/yarn.lock index caa63eecfe..7e47a1c81d 100644 --- a/api/yarn.lock +++ b/api/yarn.lock @@ -1037,13 +1037,13 @@ ajv@^6.0.1, ajv@^6.1.0, ajv@^6.5.5: json-schema-traverse "^0.4.1" uri-js "^4.2.2" -algoliasearch@^3.32.0: - version "3.32.0" - resolved "https://registry.yarnpkg.com/algoliasearch/-/algoliasearch-3.32.0.tgz#5818168c26ff921bd0346a919071bac928b747ce" - integrity sha512-C8oQnPTf0wPuyD2jSZwtBAPvz+lHOE7zRIPpgXGBuNt6ZNcC4omsbytG26318rT77a8h4759vmIp6n9p8iw4NA== +algoliasearch@^3.32.1: + version "3.32.1" + resolved "https://registry.yarnpkg.com/algoliasearch/-/algoliasearch-3.32.1.tgz#605f8a2c17ab8da2af4456110f4d0a02b384e3d0" + integrity sha512-NaaHMboU9tKwrU3aim7LlzSDqKb+1TGaC+Lx3NOttSnuMHbPpaf+7LtJL4KlosbRWEwqb9t5wSYMVDrPTH2dNA== dependencies: agentkeepalive "^2.2.0" - debug "^2.6.8" + debug "^2.6.9" envify "^4.0.0" es6-promise "^4.1.0" events "^1.1.0" @@ -1100,40 +1100,40 @@ anymatch@^2.0.0: micromatch "^3.1.4" normalize-path "^2.1.1" -apollo-cache-control@0.5.2: - version "0.5.2" - resolved "https://registry.yarnpkg.com/apollo-cache-control/-/apollo-cache-control-0.5.2.tgz#47931ede0b11c64d45429850c274b30d19322362" - integrity sha512-uehXDUrd3Qim+nzxqqN7XT1YTbNSyumW3/FY5BxbKZTI8d4oPG4eyVQKqaggooSjswKQnOoIQVes3+qg9tGAkw== +apollo-cache-control@0.6.0-alpha.0: + version "0.6.0-alpha.0" + resolved "https://registry.npmjs.org/apollo-cache-control/-/apollo-cache-control-0.6.0-alpha.0.tgz#ec9bc985b16150bb35a5d2ea874ac8c1e6ff415f" + integrity sha512-38FF+0kGkN6/efPWYda+CNQhwnY7Ee3k0am9SepI395VBKO7eXdLv1tBttwLh/Sn6sIeP7OT+DVhYBcrxdqKKA== dependencies: - apollo-server-env "2.2.0" - graphql-extensions "0.5.4" + apollo-server-env "2.3.0-alpha.0" + graphql-extensions "0.6.0-alpha.0" -apollo-datasource@0.3.1: - version "0.3.1" - resolved "https://registry.yarnpkg.com/apollo-datasource/-/apollo-datasource-0.3.1.tgz#4b7ec4c2dd7d08eb7edc865b95fd46b83a4679fd" - integrity sha512-qdEUeonc9pPZvYwXK36h2NZoT7Pddmy0HYOzdV0ON5pcG1YtNmUyyYi83Q60V5wTWjuaCjyJ9hOY6wr0BMvQuA== +apollo-datasource@0.4.0-alpha.0: + version "0.4.0-alpha.0" + resolved "https://registry.npmjs.org/apollo-datasource/-/apollo-datasource-0.4.0-alpha.0.tgz#4f5a6d1e6ab50b4ab6f2878fb3815c8be5abf0f6" + integrity sha512-vAe/zFRLX8JdIXp1oHioYy6Kx4+19tWYMgRYu2/PdUaC3P3SbBGBEBBdm1HXPiVWBZkw+uBeoVv5MiwgtwyNFQ== dependencies: - apollo-server-caching "0.3.1" - apollo-server-env "2.2.0" + apollo-server-caching "0.4.0-alpha.0" + apollo-server-env "2.3.0-alpha.0" -apollo-engine-reporting-protobuf@0.2.1: - version "0.2.1" - resolved "https://registry.yarnpkg.com/apollo-engine-reporting-protobuf/-/apollo-engine-reporting-protobuf-0.2.1.tgz#8547efcb4078a501ddf606cbfe01a2e8c3ba1afd" - integrity sha512-5pYR84uWeylRS2OJowtkTczT3bWTwOErWtfnkRKccUi/wZ/AZJBP+D5HKNzM7xoFcz9XvrJyS+wBTz1oBi0Jiw== +apollo-engine-reporting-protobuf@0.3.0-alpha.0: + version "0.3.0-alpha.0" + resolved "https://registry.npmjs.org/apollo-engine-reporting-protobuf/-/apollo-engine-reporting-protobuf-0.3.0-alpha.0.tgz#9aca6b57c6fb0f9f5c4c1a1ba1944ec32a50886d" + integrity sha512-zmoZiqjLJ8ZI5hu7+TJoeWAUDjNJEFGPlLDXiXaEFz0hx9kMCmuskJp27lVt3T7FtfyBvVJcwJz6mIGugq7ZMg== dependencies: protobufjs "^6.8.6" -apollo-engine-reporting@1.0.7: - version "1.0.7" - resolved "https://registry.yarnpkg.com/apollo-engine-reporting/-/apollo-engine-reporting-1.0.7.tgz#d326b51b12b1f71a40885b8189dbcd162171c953" - integrity sha512-mFsXvd+1/o5jSa9tI2RoXYGcvCLcwwcfLwchjSTxqUd4ViB8RbqYKynzEZ+Omji7PBRM0azioBm43f7PSsQPqA== +apollo-engine-reporting@1.1.0-alpha.0: + version "1.1.0-alpha.0" + resolved "https://registry.npmjs.org/apollo-engine-reporting/-/apollo-engine-reporting-1.1.0-alpha.0.tgz#4129c035a7325bade5cd04cf88f12c985df28dac" + integrity sha512-4qWGF7FoedbFumgmdAa1DKWUjByOD7BMmP/o1p0QoGP3sXGuw0hlRKYTtrZhAg7AsIGi+HYcWTKUjd5wJRuMRQ== dependencies: - apollo-engine-reporting-protobuf "0.2.1" - apollo-graphql "^0.1.0" - apollo-server-core "2.4.8" - apollo-server-env "2.2.0" + apollo-engine-reporting-protobuf "0.3.0-alpha.0" + apollo-graphql "^0.2.0" + apollo-server-core "2.5.0-alpha.0" + apollo-server-env "2.3.0-alpha.0" async-retry "^1.2.1" - graphql-extensions "0.5.7" + graphql-extensions "0.6.0-alpha.0" apollo-env@0.3.3: version "0.3.3" @@ -1143,11 +1143,21 @@ apollo-env@0.3.3: core-js "3.0.0-beta.13" node-fetch "^2.2.0" -apollo-graphql@^0.1.0: - version "0.1.1" - resolved "https://registry.yarnpkg.com/apollo-graphql/-/apollo-graphql-0.1.1.tgz#dc5eac3062abf9f063ac9869f0ef5c54fdc136e5" - integrity sha512-UImgDIeB0n0fryYqtdz0CwJ9uDtXwg/3Q6rXzRAqgqBYz46VkmWa7nu2LX9GmDtiXB5VUOVCtyMEnvFwC3o27g== +apollo-env@0.4.0: + version "0.4.0" + resolved "https://registry.npmjs.org/apollo-env/-/apollo-env-0.4.0.tgz#f26c8570cc66edc3606d0cf9b66dbc1770b99353" + integrity sha512-TZpk59RTbXd8cEqwmI0KHFoRrgBRplvPAP4bbRrX4uDSxXvoiY0Y6tQYUlJ35zi398Hob45mXfrZxeRDzoFMkQ== dependencies: + core-js "3.0.0-beta.13" + node-fetch "^2.2.0" + sha.js "^2.4.11" + +apollo-graphql@^0.2.0: + version "0.2.0" + resolved "https://registry.npmjs.org/apollo-graphql/-/apollo-graphql-0.2.0.tgz#74d3a84b84fa745716363a38e4ff1022f90ab5e1" + integrity sha512-wwKynD31Yw1L93IAtnEyhSxBhK4X7NXqkY6wBKWRQ4xph5uJKGgmcQmq3sPieKJT91BGL4AQBv+cwGD3blbLNA== + dependencies: + apollo-env "0.4.0" lodash.sortby "^4.7.0" apollo-link-http-common@^0.2.5: @@ -1172,6 +1182,16 @@ apollo-local-query@^0.3.1: dependencies: debug "^2.6.9" +apollo-server-cache-redis@^0.3.1: + version "0.3.1" + resolved "https://registry.yarnpkg.com/apollo-server-cache-redis/-/apollo-server-cache-redis-0.3.1.tgz#207fea925df7ad237918a8ce6978499e444377c4" + integrity sha512-1Wgxu3oOnraox/0vQ2BVszPm20FdZDXEVu6yCoHNkT/zFOYMR/KjqufF7ddlttdSsaQsXqlu0127fWHvX6NDAA== + dependencies: + apollo-server-caching "0.3.1" + apollo-server-env "2.2.0" + dataloader "^1.4.0" + redis "^2.8.0" + apollo-server-caching@0.3.1: version "0.3.1" resolved "https://registry.yarnpkg.com/apollo-server-caching/-/apollo-server-caching-0.3.1.tgz#63fcb2aaa176e1e101b36a8450e6b4c593d2767a" @@ -1179,24 +1199,31 @@ apollo-server-caching@0.3.1: dependencies: lru-cache "^5.0.0" -apollo-server-core@2.4.8: - version "2.4.8" - resolved "https://registry.yarnpkg.com/apollo-server-core/-/apollo-server-core-2.4.8.tgz#47e503a345e314222725597c889773e018d8c67a" - integrity sha512-N+5UOzHhMOnHizEiArJtNvEe/cGhSHQyTn5tlU4RJ36FDBJ/WlYZfPbGDMLISSUCJ6t+aP8GLL4Mnudt9d2PDQ== +apollo-server-caching@0.4.0-alpha.0: + version "0.4.0-alpha.0" + resolved "https://registry.npmjs.org/apollo-server-caching/-/apollo-server-caching-0.4.0-alpha.0.tgz#24425b0081deb871e45e0f0b16fe6c3f3e8bed7f" + integrity sha512-E8YfrUgw7xzI7lPxJ9DdLBKP6zVoGyn+h57liMMasmbdWqc8R7VixNzkskYivq83R5wGiIPjYP9iKuotJGmTaA== + dependencies: + lru-cache "^5.0.0" + +apollo-server-core@2.5.0-alpha.0: + version "2.5.0-alpha.0" + resolved "https://registry.npmjs.org/apollo-server-core/-/apollo-server-core-2.5.0-alpha.0.tgz#4e007c07e9b59329723241751b4c6eb28d925796" + integrity sha512-2c0OxKyV3nQDNxLeSApaSEzIXnzcgFqOXlsV4Jr+cNffzgKocoTDOnkMHHuI/QqAIDn3BmdmLTNLJx5cOCahOA== dependencies: "@apollographql/apollo-tools" "^0.3.3" "@apollographql/graphql-playground-html" "^1.6.6" "@types/ws" "^6.0.0" - apollo-cache-control "0.5.2" - apollo-datasource "0.3.1" - apollo-engine-reporting "1.0.7" - apollo-server-caching "0.3.1" - apollo-server-env "2.2.0" + apollo-cache-control "0.6.0-alpha.0" + apollo-datasource "0.4.0-alpha.0" + apollo-engine-reporting "1.1.0-alpha.0" + apollo-server-caching "0.4.0-alpha.0" + apollo-server-env "2.3.0-alpha.0" apollo-server-errors "2.2.1" - apollo-server-plugin-base "0.3.7" - apollo-tracing "0.5.2" + apollo-server-plugin-base "0.4.0-alpha.0" + apollo-tracing "0.6.0-alpha.0" fast-json-stable-stringify "^2.0.0" - graphql-extensions "0.5.7" + graphql-extensions "0.6.0-alpha.0" graphql-subscriptions "^1.0.0" graphql-tag "^2.9.2" graphql-tools "^4.0.0" @@ -1213,15 +1240,23 @@ apollo-server-env@2.2.0: node-fetch "^2.1.2" util.promisify "^1.0.0" +apollo-server-env@2.3.0-alpha.0: + version "2.3.0-alpha.0" + resolved "https://registry.npmjs.org/apollo-server-env/-/apollo-server-env-2.3.0-alpha.0.tgz#0abe5bdb814c68ae735d32c6f81918ed1abb757b" + integrity sha512-ml35SHu3SGsbohpl23Hk7mFpEWPGR9hmalSJ0ek1mFLuWOn2oRqyU+FRGW+UOA1jOcxs8U+J3Al6RKIfR8Aasg== + dependencies: + node-fetch "^2.1.2" + util.promisify "^1.0.0" + apollo-server-errors@2.2.1: version "2.2.1" resolved "https://registry.yarnpkg.com/apollo-server-errors/-/apollo-server-errors-2.2.1.tgz#f68a3f845929768057da7e1c6d30517db5872205" integrity sha512-wY/YE3iJVMYC+WYIf8QODBjIP4jhI+oc7kiYo9mrz7LdYPKAgxr/he+NteGcqn/0Ea9K5/ZFTGJDbEstSMeP8g== -apollo-server-express@^2.4.8: - version "2.4.8" - resolved "https://registry.yarnpkg.com/apollo-server-express/-/apollo-server-express-2.4.8.tgz#ec9eb61a87324555d49097e9fade3c7d142eb6cb" - integrity sha512-i60l32mfVe33jnKDPNYgUKUKu4Al0xEm2HLOSMgtJ9Wbpe/MbOx5X8M5F27fnHYdM+G5XfAErsakAyRGnQJ48Q== +apollo-server-express@2.5.0-alpha.0: + version "2.5.0-alpha.0" + resolved "https://registry.npmjs.org/apollo-server-express/-/apollo-server-express-2.5.0-alpha.0.tgz#4932e8b40f5bca4f884cbe5454be53821de82f31" + integrity sha512-KJhEyVhWYad5gL9qZMRIwc5Tbzu1/744FGyShJbMONuGEguqOsrc3ChjAwxRcUhvfTT2iNrdzVb48mQEVW56hg== dependencies: "@apollographql/graphql-playground-html" "^1.6.6" "@types/accepts" "^1.3.5" @@ -1229,25 +1264,35 @@ apollo-server-express@^2.4.8: "@types/cors" "^2.8.4" "@types/express" "4.16.1" accepts "^1.3.5" - apollo-server-core "2.4.8" + apollo-server-core "2.5.0-alpha.0" body-parser "^1.18.3" cors "^2.8.4" graphql-subscriptions "^1.0.0" graphql-tools "^4.0.0" type-is "^1.6.16" -apollo-server-plugin-base@0.3.7: - version "0.3.7" - resolved "https://registry.yarnpkg.com/apollo-server-plugin-base/-/apollo-server-plugin-base-0.3.7.tgz#bfa4932fc9481bb36221545578d311db464af5a6" - integrity sha512-hW1jaLKf9qNOxMTwRq2CSqz3eqXsZuEiCc8/mmEtOciiVBq1GMtxFf19oIYM9HQuPvQU2RWpns1VrYN59L3vbg== +apollo-server-plugin-base@0.4.0-alpha.0: + version "0.4.0-alpha.0" + resolved "https://registry.npmjs.org/apollo-server-plugin-base/-/apollo-server-plugin-base-0.4.0-alpha.0.tgz#165d12056f4cc3a4c9ed1ac8b08e25fcac1b4f39" + integrity sha512-L8HMdOOddy6mUkYopNVzx3YgU83FKeNM/pFdfAVft3Y2v4p9Fyu5cdoBijRHO4+gEfpJOdaSlZBqHlCg8wnw/g== -apollo-tracing@0.5.2: - version "0.5.2" - resolved "https://registry.yarnpkg.com/apollo-tracing/-/apollo-tracing-0.5.2.tgz#cc49936fb435fa98d19c841514cfe05237c85b33" - integrity sha512-2FdwRvPIq9uuF6OzONroXep6VBGqzHOkP6LlcFQe7SdwxfRP+SD/ycHNSC1acVg2b8d+am9Kzqg2vV54UpOIKA== +apollo-server-plugin-response-cache@^0.1.0-alpha.0: + version "0.1.0-alpha.0" + resolved "https://registry.npmjs.org/apollo-server-plugin-response-cache/-/apollo-server-plugin-response-cache-0.1.0-alpha.0.tgz#c26c6502641daaeff17e5ebd896afd14049ed900" + integrity sha512-1p6mLlG24y+mg2iPPpJLIGHYVTE5oPSoWt7Pxd0kWu32n89sviQlok40LUc3Bb7nnq4uW1UxJRwRqGBRFiGrdw== dependencies: - apollo-server-env "2.2.0" - graphql-extensions "0.5.4" + apollo-cache-control "0.6.0-alpha.0" + apollo-server-caching "0.4.0-alpha.0" + apollo-server-env "2.3.0-alpha.0" + apollo-server-plugin-base "0.4.0-alpha.0" + +apollo-tracing@0.6.0-alpha.0: + version "0.6.0-alpha.0" + resolved "https://registry.npmjs.org/apollo-tracing/-/apollo-tracing-0.6.0-alpha.0.tgz#d8e393fdbd16b0635b496ebb8438c0081397a961" + integrity sha512-fec4S+Clpfj2zS1PyLSDh9LTYBc6eZzlNM4eA4NC0dNon51flEB1HeZkzFaAPSXbmnsc4mi7pv++sFxvxqFDyA== + dependencies: + apollo-server-env "2.3.0-alpha.0" + graphql-extensions "0.6.0-alpha.0" apollo-upload-client@^9.1.0: version "9.1.0" @@ -2807,23 +2852,23 @@ component-emitter@^1.2.1: resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.2.1.tgz#137918d6d78283f7df7a6b7c5a63e140e69425e6" integrity sha1-E3kY1teCg/ffemt8WmPhQOaUJeY= -compressible@~2.0.14: - version "2.0.15" - resolved "https://registry.yarnpkg.com/compressible/-/compressible-2.0.15.tgz#857a9ab0a7e5a07d8d837ed43fe2defff64fe212" - integrity sha512-4aE67DL33dSW9gw4CI2H/yTxqHLNcxp0yS6jB+4h+wr3e43+1z7vm0HU9qXOH8j+qjKuL8+UtkOxYQSMq60Ylw== +compressible@~2.0.16: + version "2.0.16" + resolved "https://registry.yarnpkg.com/compressible/-/compressible-2.0.16.tgz#a49bf9858f3821b64ce1be0296afc7380466a77f" + integrity sha512-JQfEOdnI7dASwCuSPWIeVYwc/zMsu/+tRhoUvEfXz2gxOA2DNjmG5vhtFdBlhWPPGo+RdT9S3tgc/uH5qgDiiA== dependencies: - mime-db ">= 1.36.0 < 2" + mime-db ">= 1.38.0 < 2" -compression@^1.7.3: - version "1.7.3" - resolved "https://registry.yarnpkg.com/compression/-/compression-1.7.3.tgz#27e0e176aaf260f7f2c2813c3e440adb9f1993db" - integrity sha512-HSjyBG5N1Nnz7tF2+O7A9XUhyjru71/fwgNb7oIsEVHR0WShfs2tIS/EySLgiTe98aOK18YDlMXpzjCXY/n9mg== +compression@^1.7.4: + version "1.7.4" + resolved "https://registry.yarnpkg.com/compression/-/compression-1.7.4.tgz#95523eff170ca57c29a0ca41e6fe131f41e5bb8f" + integrity sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ== dependencies: accepts "~1.3.5" bytes "3.0.0" - compressible "~2.0.14" + compressible "~2.0.16" debug "2.6.9" - on-headers "~1.0.1" + on-headers "~1.0.2" safe-buffer "5.1.2" vary "~1.1.2" @@ -3038,6 +3083,14 @@ create-react-class@^15.6.0: loose-envify "^1.3.1" object-assign "^4.1.1" +create-react-context@^0.2.2: + version "0.2.3" + resolved "https://registry.yarnpkg.com/create-react-context/-/create-react-context-0.2.3.tgz#9ec140a6914a22ef04b8b09b7771de89567cb6f3" + integrity sha512-CQBmD0+QGgTaxDL3OX1IDXYqjkp2It4RIbcb99jS6AEg27Ga+a9G3JtK6SIu0HBwPLZlmwt9F7UwWA4Bn92Rag== + dependencies: + fbjs "^0.8.0" + gud "^1.0.0" + cron-parser@^2.7.3: version "2.7.3" resolved "https://registry.yarnpkg.com/cron-parser/-/cron-parser-2.7.3.tgz#12603f89f5375af353a9357be2543d3172eac651" @@ -3468,6 +3521,11 @@ dotsplit.js@^1.0.3: resolved "https://registry.yarnpkg.com/dotsplit.js/-/dotsplit.js-1.1.0.tgz#25a239eabe922a91ffa5d2a172d6c9fb82451e02" integrity sha1-JaI56r6SKpH/pdKhctbJ+4JFHgI= +double-ended-queue@^2.1.0-0: + version "2.1.0-0" + resolved "https://registry.yarnpkg.com/double-ended-queue/-/double-ended-queue-2.1.0-0.tgz#103d3527fd31528f40188130c841efdd78264e5c" + integrity sha1-ED01J/0xUo9AGIEwyEHv3XgmTlw= + draft-js-checkable-list-item@^2.0.5: version "2.0.6" resolved "https://registry.yarnpkg.com/draft-js-checkable-list-item/-/draft-js-checkable-list-item-2.0.6.tgz#19dfb99421e07ac1a93736f4a5d04e222de2a647" @@ -4151,7 +4209,7 @@ fb-watchman@^2.0.0: dependencies: bser "^2.0.0" -fbjs@^0.8.1, fbjs@^0.8.15, fbjs@^0.8.5, fbjs@^0.8.9: +fbjs@^0.8.0, fbjs@^0.8.1, fbjs@^0.8.15, fbjs@^0.8.5, fbjs@^0.8.9: version "0.8.17" resolved "https://registry.yarnpkg.com/fbjs/-/fbjs-0.8.17.tgz#c4d598ead6949112653d6588b01a5cdcd9f90fdd" integrity sha1-xNWY6taUkRJlPWWIsBpc3Nn5D90= @@ -4623,17 +4681,10 @@ graphql-depth-limit@^1.1.0: dependencies: arrify "^1.0.1" -graphql-extensions@0.5.4: - version "0.5.4" - resolved "https://registry.yarnpkg.com/graphql-extensions/-/graphql-extensions-0.5.4.tgz#18a9674f9adb11aa6c0737485887ea8877914cff" - integrity sha512-qLThJGVMqcItE7GDf/xX/E40m/aeqFheEKiR5bfra4q5eHxQKGjnIc20P9CVqjOn9I0FkEiU9ypOobfmIf7t6g== - dependencies: - "@apollographql/apollo-tools" "^0.3.3" - -graphql-extensions@0.5.7: - version "0.5.7" - resolved "https://registry.yarnpkg.com/graphql-extensions/-/graphql-extensions-0.5.7.tgz#2b647e4e36997dc85b7f58ebd64324a5250fb2cf" - integrity sha512-HrU6APE1PiehZ46scMB3S5DezSeCATd8v+e4mmg2bqszMyCFkmAnmK6hR1b5VjHxhzt5/FX21x1WsXfqF4FwdQ== +graphql-extensions@0.6.0-alpha.0: + version "0.6.0-alpha.0" + resolved "https://registry.npmjs.org/graphql-extensions/-/graphql-extensions-0.6.0-alpha.0.tgz#4e0b0e2c1962e98e12730bc23cefd5881b68525e" + integrity sha512-SY4mUxY0Q+GElKMjHtNsYYQ0ypHiuvky5roNh0CbOWqxTo0HNQp4vkjLKN4yu9QX1nCk02v5hFxivE0NqOj/sg== dependencies: "@apollographql/apollo-tools" "^0.3.3" @@ -4711,6 +4762,11 @@ growly@^1.3.0: resolved "https://registry.yarnpkg.com/growly/-/growly-1.3.0.tgz#f10748cbe76af964b7c96c93c6bcc28af120c081" integrity sha1-8QdIy+dq+WS3yWyTxrzCivEgwIE= +gud@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/gud/-/gud-1.0.0.tgz#a489581b17e6a70beca9abe3ae57de7a499852c0" + integrity sha512-zGEOVKFM5sVPPrYs7J5/hYEw2Pof8KCyOwyhG8sAF26mCAeUFAcYPu1mwB7hhpIP29zOIBaDqwuHdLp0jvZXjw== + handlebars@^4.0.3: version "4.0.12" resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.0.12.tgz#2c15c8a96d46da5e266700518ba8cb8d919d5bc5" @@ -4843,10 +4899,10 @@ helmet-csp@2.7.1: dasherize "2.0.0" platform "1.3.5" -helmet@^3.15.1: - version "3.15.1" - resolved "https://registry.yarnpkg.com/helmet/-/helmet-3.15.1.tgz#2c80d1a59138b6f23929605afca4b1c88b3298ec" - integrity sha512-hgoNe/sjKlKNvJ3g9Gz149H14BjMMWOCmW/DTXl7IfyKGtIK37GePwZrHNfr4aPXdKVyXcTj26RgRFbPKDy9lw== +helmet@^3.16.0: + version "3.16.0" + resolved "https://registry.yarnpkg.com/helmet/-/helmet-3.16.0.tgz#7df41a4bfe4c83d90147c1e30d70893f92a9d97c" + integrity sha512-rsTKRogc5OYGlvSHuq5QsmOsOzF6uDoMqpfh+Np8r23+QxDq+SUx90Rf8HyIKQVl7H6NswZEwfcykinbAeZ6UQ== dependencies: depd "2.0.0" dns-prefetch-control "0.1.0" @@ -4858,8 +4914,8 @@ helmet@^3.15.1: helmet-csp "2.7.1" hide-powered-by "1.0.0" hpkp "2.0.0" - hsts "2.1.0" - ienoopen "1.0.0" + hsts "2.2.0" + ienoopen "1.1.0" nocache "2.0.0" referrer-policy "1.1.0" x-xss-protection "1.1.0" @@ -4874,16 +4930,17 @@ highlight.js@^9.15.6: resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-9.15.6.tgz#72d4d8d779ec066af9a17cb14360c3def0aa57c4" integrity sha512-zozTAWM1D6sozHo8kqhfYgsac+B+q0PmsjXeyDrYIHHcBN0zTVT66+s2GW1GZv7DbyaROdLXKdabwS/WqPyIdQ== -history@^4.6.1, history@^4.7.2: - version "4.7.2" - resolved "https://registry.yarnpkg.com/history/-/history-4.7.2.tgz#22b5c7f31633c5b8021c7f4a8a954ac139ee8d5b" - integrity sha512-1zkBRWW6XweO0NBcjiphtVJVsIQ+SXF29z9DVkceeaSLVMFXHool+fdCZD4spDCfZJCILPILc3bm7Bc+HRi0nA== +history@^4.8.0-beta.0, history@^4.9.0: + version "4.9.0" + resolved "https://registry.yarnpkg.com/history/-/history-4.9.0.tgz#84587c2068039ead8af769e9d6a6860a14fa1bca" + integrity sha512-H2DkjCjXf0Op9OAr6nJ56fcRkTSNrUiv41vNJ6IswJjif6wlpZK0BTfFbi7qK9dXLSYZxkq5lBsj3vUjlYBYZA== dependencies: - invariant "^2.2.1" + "@babel/runtime" "^7.1.2" loose-envify "^1.2.0" resolve-pathname "^2.2.0" + tiny-invariant "^1.0.2" + tiny-warning "^1.0.0" value-equal "^0.4.0" - warning "^3.0.0" hmac-drbg@^1.0.0: version "1.0.1" @@ -4909,7 +4966,7 @@ hoist-non-react-statics@^1.0.0, hoist-non-react-statics@^1.2.0: resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-1.2.0.tgz#aa448cf0986d55cc40773b17174b7dd066cb7cfb" integrity sha1-qkSM8JhtVcxAdzsXF0t90GbLfPs= -hoist-non-react-statics@^2.1.1, hoist-non-react-statics@^2.5.0, hoist-non-react-statics@^2.5.5: +hoist-non-react-statics@^2.1.1, hoist-non-react-statics@^2.5.5: version "2.5.5" resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-2.5.5.tgz#c5903cf409c0dfd908f388e619d86b9c1174cb47" integrity sha512-rqcy4pJo55FTTLWt+bU8ukscqHeE/e9KWvsOW2b/a3afxQZhwkQdT1rPPCJ0rYXdj4vNcasY8zHTH+jF/qStxw== @@ -4957,12 +5014,7 @@ hpp@^0.2.2: lodash "^4.7.0" type-is "^1.6.12" -hsts@2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/hsts/-/hsts-2.1.0.tgz#cbd6c918a2385fee1dd5680bfb2b3a194c0121cc" - integrity sha512-zXhh/DqgrTXJ7erTN6Fh5k/xjMhDGXCqdYN3wvxUvGUQvnxcFfUd8E+6vLg/nk3ss1TYMb+DhRl25fYABioTvA== - -hsts@^2.2.0: +hsts@2.2.0, hsts@^2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/hsts/-/hsts-2.2.0.tgz#09119d42f7a8587035d027dda4522366fe75d964" integrity sha512-ToaTnQ2TbJkochoVcdXYm4HOCliNozlviNsg+X2XQLQvZNI/kCHR9rZxVYpJB3UPcHz80PgxRyWQ7PdU1r+VBQ== @@ -5053,10 +5105,10 @@ ieee754@^1.1.4: resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.12.tgz#50bf24e5b9c8bb98af4964c941cdb0918da7b60b" integrity sha512-GguP+DRY+pJ3soyIiGPTvdiVXjZ+DbXOxGpXn3eMvNW4x4irjqXm4wHKscC+TfxSJ0yw/S1F24tqdMNsMZTiLA== -ienoopen@1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/ienoopen/-/ienoopen-1.0.0.tgz#346a428f474aac8f50cf3784ea2d0f16f62bda6b" - integrity sha1-NGpCj0dKrI9QzzeE6i0PFvYr2ms= +ienoopen@1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/ienoopen/-/ienoopen-1.1.0.tgz#411e5d530c982287dbdc3bb31e7a9c9e32630974" + integrity sha512-MFs36e/ca6ohEKtinTJ5VvAJ6oDRAYFdYXweUnGY9L9vcoqFOU4n2ZhmJ0C4z/cwGZ3YIQRSB3XZ1+ghZkY5NQ== iferr@^0.1.5: version "0.1.5" @@ -6071,12 +6123,12 @@ jsonify@~0.0.0: resolved "https://registry.yarnpkg.com/jsonify/-/jsonify-0.0.0.tgz#2c74b6ee41d93ca51b7b5aaee8f503631d252a73" integrity sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM= -jsonwebtoken@^8.5.0: - version "8.5.0" - resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-8.5.0.tgz#ebd0ca2a69797816e1c5af65b6c759787252947e" - integrity sha512-IqEycp0znWHNA11TpYi77bVgyBO/pGESDh7Ajhas+u0ttkGkKYIIAjniL4Bw5+oVejVF+SYkaI7XKfwCCyeTuA== +jsonwebtoken@^8.5.1: + version "8.5.1" + resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz#00e71e0b8df54c2121a1f26137df2280673bcc0d" + integrity sha512-XjwVfRS6jTMsqYs0EsuJ4LGxXV14zQybNd4L2r0UvbVnSF9Af8x7p5MzbJ90Ioz/9TI41/hTCvznF/loiSzn8w== dependencies: - jws "^3.2.1" + jws "^3.2.2" lodash.includes "^4.3.0" lodash.isboolean "^3.0.3" lodash.isinteger "^4.0.4" @@ -6106,10 +6158,10 @@ jwa@^1.1.5: ecdsa-sig-formatter "1.0.10" safe-buffer "^5.0.1" -jwa@^1.2.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/jwa/-/jwa-1.3.0.tgz#061a7c3bb8ab2b3434bb2f432005a8bb7fca0efa" - integrity sha512-SxObIyzv9a6MYuZYaSN6DhSm9j3+qkokwvCB0/OTSV5ylPq1wUQiygZQcHT5Qlux0I5kmISx3J86TxKhuefItg== +jwa@^1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/jwa/-/jwa-1.4.1.tgz#743c32985cb9e98655530d53641b66c8645b039a" + integrity sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA== dependencies: buffer-equal-constant-time "1.0.1" ecdsa-sig-formatter "1.0.11" @@ -6123,12 +6175,12 @@ jws@^3.1.3: jwa "^1.1.5" safe-buffer "^5.0.1" -jws@^3.2.1: - version "3.2.1" - resolved "https://registry.yarnpkg.com/jws/-/jws-3.2.1.tgz#d79d4216a62c9afa0a3d5e8b5356d75abdeb2be5" - integrity sha512-bGA2omSrFUkd72dhh05bIAN832znP4wOU3lfuXtRBuGTbsmNmDXMQg28f0Vsxaxgk4myF5YkKQpz6qeRpMgX9g== +jws@^3.2.2: + version "3.2.2" + resolved "https://registry.yarnpkg.com/jws/-/jws-3.2.2.tgz#001099f3639468c9414000e99995fa52fb478304" + integrity sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA== dependencies: - jwa "^1.2.0" + jwa "^1.4.1" safe-buffer "^5.0.1" keygrip@^1.0.3, keygrip@~1.0.2: @@ -6663,7 +6715,12 @@ miller-rabin@^4.0.0: bn.js "^4.0.0" brorand "^1.0.1" -"mime-db@>= 1.36.0 < 2", mime-db@~1.37.0: +"mime-db@>= 1.38.0 < 2": + version "1.38.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.38.0.tgz#1a2aab16da9eb167b49c6e4df2d9c68d63d8e2ad" + integrity sha512-bqVioMFFzc2awcdJZIzR3HjZFX20QhilVS7hytkKrv7xFAn8bM1gzc/FOX2awLISvWe0PV8ptFKcon+wZ5qYkg== + +mime-db@~1.37.0: version "1.37.0" resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.37.0.tgz#0b6a0ce6fdbe9576e25f1f2d2fde8830dc0ad0d8" integrity sha512-R3C4db6bgQhlIhPU48fUtdVmKnflq+hRdad7IyKhtFj06VPNVdk2RhiYL3UjQIlso8L+YxAtFkobT0VK+S/ybg== @@ -7147,6 +7204,11 @@ on-headers@~1.0.1: resolved "https://registry.yarnpkg.com/on-headers/-/on-headers-1.0.1.tgz#928f5d0f470d49342651ea6794b0857c100693f7" integrity sha1-ko9dD0cNSTQmUepnlLCFfBAGk/c= +on-headers@~1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/on-headers/-/on-headers-1.0.2.tgz#772b0ae6aaa525c399e489adfad90c403eb3c28f" + integrity sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA== + once@^1.3.0, once@^1.3.1, once@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" @@ -7600,7 +7662,14 @@ pretty-format@^21.2.1: ansi-regex "^3.0.0" ansi-styles "^3.2.0" -prismjs@^1.15.0, prismjs@^1.5.0, prismjs@^1.6.0: +prismjs@^1.16.0: + version "1.16.0" + resolved "https://registry.yarnpkg.com/prismjs/-/prismjs-1.16.0.tgz#406eb2c8aacb0f5f0f1167930cb83835d10a4308" + integrity sha512-OA4MKxjFZHSvZcisLGe14THYsug/nF6O1f0pAJc0KN0wTyAcLqmsbE+lTGKSpyh+9pEW57+k6pg2AfYR+coyHA== + optionalDependencies: + clipboard "^2.0.0" + +prismjs@^1.5.0, prismjs@^1.6.0: version "1.15.0" resolved "https://registry.yarnpkg.com/prismjs/-/prismjs-1.15.0.tgz#8801d332e472091ba8def94976c8877ad60398d9" integrity sha512-Lf2JrFYx8FanHrjoV5oL8YHCclLQgbJcVZR+gikGGMqz6ub5QVWDTM6YIwm3BuPxM/LOV+rKns3LssXNLIf+DA== @@ -7960,29 +8029,33 @@ react-remarkable@^1.1.1: remarkable "^1.x" react-router-dom@^4.0.0-beta.7: - version "4.3.1" - resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-4.3.1.tgz#4c2619fc24c4fa87c9fd18f4fb4a43fe63fbd5c6" - integrity sha512-c/MlywfxDdCp7EnB7YfPMOfMD3tOtIjrQlj/CKfNMBxdmpJP8xcz5P/UAFn3JbnQCNUxsHyVVqllF9LhgVyFCA== + version "4.4.0" + resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-4.4.0.tgz#fad67b46375f7081a76d1c92a83a92d28b5abc35" + integrity sha512-r4knbi8lanTGrwoUXFaWALrJZOAl3h9bdFUz4woHgEm7/bYcpBGfnYhPU82xjXrPeJyWF6OmIxpwXjxos30gOQ== dependencies: - history "^4.7.2" - invariant "^2.2.4" + "@babel/runtime" "^7.1.2" + history "^4.8.0-beta.0" loose-envify "^1.3.1" - prop-types "^15.6.1" - react-router "^4.3.1" - warning "^4.0.1" + prop-types "^15.6.2" + react-router "^4.4.0" + tiny-invariant "^1.0.2" + tiny-warning "^1.0.0" -react-router@^4.0.0-beta.7, react-router@^4.3.1: - version "4.3.1" - resolved "https://registry.yarnpkg.com/react-router/-/react-router-4.3.1.tgz#aada4aef14c809cb2e686b05cee4742234506c4e" - integrity sha512-yrvL8AogDh2X42Dt9iknk4wF4V8bWREPirFfS9gLU1huk6qK41sg7Z/1S81jjTrGHxa3B8R3J6xIkDAA6CVarg== +react-router@^4.0.0-beta.7, react-router@^4.4.0: + version "4.4.0" + resolved "https://registry.yarnpkg.com/react-router/-/react-router-4.4.0.tgz#e8b8b88329f564d4c48b7ee631b10310188c6888" + integrity sha512-qTGsOSF2b02zOsUfcnHjw7muI0Ejx+yA2e4P9qqzB2O+N3Icpca4epViXRgkBIvBjagXBtroxXqH0RJhYDMUbg== dependencies: - history "^4.7.2" - hoist-non-react-statics "^2.5.0" - invariant "^2.2.4" + "@babel/runtime" "^7.1.2" + create-react-context "^0.2.2" + history "^4.9.0" + hoist-non-react-statics "^3.1.0" loose-envify "^1.3.1" path-to-regexp "^1.7.0" - prop-types "^15.6.1" - warning "^4.0.1" + prop-types "^15.6.2" + react-is "^16.6.0" + tiny-invariant "^1.0.2" + tiny-warning "^1.0.0" react-side-effect@^1.1.0: version "1.1.5" @@ -7999,10 +8072,10 @@ react-textarea-autosize@^4.0.5: dependencies: prop-types "^15.5.8" -react-transition-group@^2.6.0: - version "2.6.0" - resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-2.6.0.tgz#3c41cbdd9c044c5f8604d4e8d319e860919c9fae" - integrity sha512-VzZ+6k/adL3pJHo4PU/MHEPjW59/TGQtRsXC+wnxsx2mxjQKNHnDdJL/GpYuPJIsyHGjYbBQfIJ2JNOAdPc8GQ== +react-transition-group@^2.7.0: + version "2.7.0" + resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-2.7.0.tgz#60ca3bb2bf83fe71c50816a936e985ce7b8134dc" + integrity sha512-CzF22K0x6arjQO4AxkasMaiYcFG/QH0MhPNs45FmNsfWsQmsO9jv52sIZJAalnlryD5RgrrbLtV5CMJSokrrMA== dependencies: dom-helpers "^3.3.1" loose-envify "^1.4.0" @@ -8144,7 +8217,7 @@ redis-errors@^1.0.0, redis-errors@^1.2.0: resolved "https://registry.yarnpkg.com/redis-errors/-/redis-errors-1.2.0.tgz#eb62d2adb15e4eaf4610c04afe1529384250abad" integrity sha1-62LSrbFeTq9GEMBK/hUpOEJQq60= -redis-parser@^2.4.0: +redis-parser@^2.4.0, redis-parser@^2.6.0: version "2.6.0" resolved "https://registry.yarnpkg.com/redis-parser/-/redis-parser-2.6.0.tgz#52ed09dacac108f1a631c07e9b69941e7a19504b" integrity sha1-Uu0J2srBCPGmMcB+m2mUHnoZUEs= @@ -8163,6 +8236,15 @@ redis-tag-cache@^1.2.1: dependencies: ioredis "^4.0.0" +redis@^2.8.0: + version "2.8.0" + resolved "https://registry.yarnpkg.com/redis/-/redis-2.8.0.tgz#202288e3f58c49f6079d97af7a10e1303ae14b02" + integrity sha512-M1OkonEQwtRmZv4tEWF2VgpG0JWJ8Fv1PhlgT5+B+uNq2cA3Rt1Yt/ryoR+vQNOQcIEgdCdfH0jr3bDpihAw1A== + dependencies: + double-ended-queue "^2.1.0-0" + redis-commands "^1.2.0" + redis-parser "^2.6.0" + redraft@0.8.0: version "0.8.0" resolved "https://registry.yarnpkg.com/redraft/-/redraft-0.8.0.tgz#db2a5c01a8eb6b553f46cc8382c1ed085325e99f" @@ -9336,7 +9418,7 @@ tiny-emitter@^2.0.0: resolved "https://registry.yarnpkg.com/tiny-emitter/-/tiny-emitter-2.0.2.tgz#82d27468aca5ade8e5fd1e6d22b57dd43ebdfb7c" integrity sha512-2NM0auVBGft5tee/OxP4PI3d8WItkDM+fPnaRAVo6xTDI2knbz9eC5ArWGqtGlYqiH3RU5yMpdyTTO7MguC4ow== -tiny-invariant@^1.0.1: +tiny-invariant@^1.0.1, tiny-invariant@^1.0.2: version "1.0.3" resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.0.3.tgz#91efaaa0269ccb6271f0296aeedb05fc3e067b7a" integrity sha512-ytQx8T4DL8PjlX53yYzcIC0WhIZbpR0p1qcYjw2pHu3w6UtgWwFJQ/02cnhOnBBhlFx/edUIfcagCaQSe3KMWg== @@ -9346,6 +9428,11 @@ tiny-warning@^0.0.3: resolved "https://registry.yarnpkg.com/tiny-warning/-/tiny-warning-0.0.3.tgz#1807eb4c5f81784a6354d58ea1d5024f18c6c81f" integrity sha512-r0SSA5Y5IWERF9Xh++tFPx0jITBgGggOsRLDWWew6YRw/C2dr4uNO1fw1vanrBmHsICmPyMLNBZboTlxUmUuaA== +tiny-warning@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/tiny-warning/-/tiny-warning-1.0.2.tgz#1dfae771ee1a04396bdfde27a3adcebc6b648b28" + integrity sha512-rru86D9CpQRLvsFG5XFdy0KdLAvjdQDyZCsRcuu60WtzFylDM3eAWSxEVz5kzL2Gp544XiUvPbVKtOA/txLi9Q== + tlds@^1.189.0: version "1.203.1" resolved "https://registry.yarnpkg.com/tlds/-/tlds-1.203.1.tgz#4dc9b02f53de3315bc98b80665e13de3edfc1dfc" @@ -9850,13 +9937,6 @@ warning@^3.0.0: dependencies: loose-envify "^1.0.0" -warning@^4.0.1: - version "4.0.2" - resolved "https://registry.yarnpkg.com/warning/-/warning-4.0.2.tgz#aa6876480872116fa3e11d434b0d0d8d91e44607" - integrity sha512-wbTp09q/9C+jJn4KKJfJfoS6VleK/Dti0yqWSm6KMvJ4MRCXFQNapHuJXutJIrWV0Cf4AhTdeIe4qdKHR1+Hug== - dependencies: - loose-envify "^1.0.0" - watch@~0.18.0: version "0.18.0" resolved "https://registry.yarnpkg.com/watch/-/watch-0.18.0.tgz#28095476c6df7c90c963138990c0a5423eb4b986" diff --git a/athena/package.json b/athena/package.json index cea24f4fed..e96704b732 100644 --- a/athena/package.json +++ b/athena/package.json @@ -4,7 +4,7 @@ "start": "NODE_ENV=production node main.js" }, "dependencies": { - "aws-sdk": "^2.409.0", + "aws-sdk": "^2.426.0", "axios": "^0.16.2", "bull": "3.3.10", "cryptr": "^3.0.0", @@ -23,8 +23,8 @@ "now-env": "^3.1.0", "performance-now": "^2.1.0", "raven": "^2.6.4", - "react": "^16.8.4", - "react-dom": "^16.8.4", + "react": "^16.8.5", + "react-dom": "^16.8.5", "redis-tag-cache": "^1.2.1", "rethinkdb-inspector": "^0.3.3", "rethinkdbdash": "^2.3.31", diff --git a/athena/yarn.lock b/athena/yarn.lock index 57ac230ee7..c7fe8a1d33 100644 --- a/athena/yarn.lock +++ b/athena/yarn.lock @@ -23,10 +23,10 @@ asn1.js@^5.0.0: inherits "^2.0.1" minimalistic-assert "^1.0.0" -aws-sdk@^2.409.0: - version "2.418.0" - resolved "https://registry.yarnpkg.com/aws-sdk/-/aws-sdk-2.418.0.tgz#840c4562c28fc63e99a9195a4644d0021f5b3e35" - integrity sha512-15aCtqqCsiyMW+CDwo6Fq3V5jDzpgb5//aPMosL+5FQnQu65t2GiLidcIPx4fWvsYpRiE/i4enz3a0Kqtt2acQ== +aws-sdk@^2.426.0: + version "2.426.0" + resolved "https://registry.yarnpkg.com/aws-sdk/-/aws-sdk-2.426.0.tgz#cf17361c987daf518f945218f06135fbc1a3690d" + integrity sha512-S4nmIhF/6iYeVEmKUWVG03zo1sw3zELoAPGqBKIZ3isrXbxkFXdP2cgIQxqi37zwWXSqaxt0xjeXVOMLzN6vSg== dependencies: buffer "4.9.1" events "1.1.1" @@ -713,30 +713,30 @@ rc@^1.0.0: minimist "^1.2.0" strip-json-comments "~2.0.1" -react-dom@^16.8.4: - version "16.8.4" - resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.8.4.tgz#1061a8e01a2b3b0c8160037441c3bf00a0e3bc48" - integrity sha512-Ob2wK7XG2tUDt7ps7LtLzGYYB6DXMCLj0G5fO6WeEICtT4/HdpOi7W/xLzZnR6RCG1tYza60nMdqtxzA8FaPJQ== +react-dom@^16.8.5: + version "16.8.5" + resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.8.5.tgz#b3e37d152b49e07faaa8de41fdf562be3463335e" + integrity sha512-VIEIvZLpFafsfu4kgmftP5L8j7P1f0YThfVTrANMhZUFMDOsA6e0kfR6wxw/8xxKs4NB59TZYbxNdPCDW34x4w== dependencies: loose-envify "^1.1.0" object-assign "^4.1.1" prop-types "^15.6.2" - scheduler "^0.13.4" + scheduler "^0.13.5" react-is@^16.8.1: version "16.8.4" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.8.4.tgz#90f336a68c3a29a096a3d648ab80e87ec61482a2" integrity sha512-PVadd+WaUDOAciICm/J1waJaSvgq+4rHE/K70j0PFqKhkTBsPv/82UGQJNXAngz1fOQLLxI6z1sEDmJDQhCTAA== -react@^16.8.4: - version "16.8.4" - resolved "https://registry.yarnpkg.com/react/-/react-16.8.4.tgz#fdf7bd9ae53f03a9c4cd1a371432c206be1c4768" - integrity sha512-0GQ6gFXfUH7aZcjGVymlPOASTuSjlQL4ZtVC5YKH+3JL6bBLCVO21DknzmaPlI90LN253ojj02nsapy+j7wIjg== +react@^16.8.5: + version "16.8.5" + resolved "https://registry.yarnpkg.com/react/-/react-16.8.5.tgz#49be3b655489d74504ad994016407e8a0445de66" + integrity sha512-daCb9TD6FZGvJ3sg8da1tRAtIuw29PbKZW++NN4wqkbEvxL+bZpaaYb4xuftW/SpXmgacf1skXl/ddX6CdOlDw== dependencies: loose-envify "^1.1.0" object-assign "^4.1.1" prop-types "^15.6.2" - scheduler "^0.13.4" + scheduler "^0.13.5" redis-commands@1.4.0, redis-commands@^1.2.0: version "1.4.0" @@ -813,10 +813,10 @@ sax@>=0.6.0: resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9" integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw== -scheduler@^0.13.4: - version "0.13.4" - resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.13.4.tgz#8fef05e7a3580c76c0364d2df5e550e4c9140298" - integrity sha512-cvSOlRPxOHs5dAhP9yiS/6IDmVAVxmk33f0CtTJRkmUWcb1Us+t7b1wqdzoC0REw2muC9V5f1L/w5R5uKGaepA== +scheduler@^0.13.5: + version "0.13.5" + resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.13.5.tgz#b7226625167041298af3b98088a9dbbf6d7733a8" + integrity sha512-K98vjkQX9OIt/riLhp6F+XtDPtMQhqNcf045vsh+pcuvHq+PHy1xCrH3pq1P40m6yR46lpVvVhKdEOtnimuUJw== dependencies: loose-envify "^1.1.0" object-assign "^4.1.1" diff --git a/chronos/package.json b/chronos/package.json index 0015be88f1..2cf11f22ab 100644 --- a/chronos/package.json +++ b/chronos/package.json @@ -3,7 +3,7 @@ "start": "NODE_ENV=production node main.js" }, "dependencies": { - "aws-sdk": "^2.409.0", + "aws-sdk": "^2.426.0", "bull": "^3.7.0", "datadog-metrics": "^0.8.1", "debug": "^4.1.1", diff --git a/chronos/yarn.lock b/chronos/yarn.lock index f08a78ecdc..fb21e229ed 100644 --- a/chronos/yarn.lock +++ b/chronos/yarn.lock @@ -7,10 +7,10 @@ asap@~2.0.3: resolved "https://registry.yarnpkg.com/asap/-/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46" integrity sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY= -aws-sdk@^2.409.0: - version "2.409.0" - resolved "https://registry.yarnpkg.com/aws-sdk/-/aws-sdk-2.409.0.tgz#d017060ba9e005487c68dc34a592af74d916f295" - integrity sha512-QV6j9zBQq/Kz8BqVOrJ03ABjMKtErXdUT1YdYEljoLQZimpzt0ZdQwJAsoZIsxxriOJgrqeZsQUklv9AFQaldQ== +aws-sdk@^2.426.0: + version "2.426.0" + resolved "https://registry.yarnpkg.com/aws-sdk/-/aws-sdk-2.426.0.tgz#cf17361c987daf518f945218f06135fbc1a3690d" + integrity sha512-S4nmIhF/6iYeVEmKUWVG03zo1sw3zELoAPGqBKIZ3isrXbxkFXdP2cgIQxqi37zwWXSqaxt0xjeXVOMLzN6vSg== dependencies: buffer "4.9.1" events "1.1.1" diff --git a/cypress/integration/channel/settings/edit_spec.js b/cypress/integration/channel/settings/edit_spec.js index 8e81c9f5db..0f5eeed348 100644 --- a/cypress/integration/channel/settings/edit_spec.js +++ b/cypress/integration/channel/settings/edit_spec.js @@ -40,8 +40,8 @@ describe('edit a channel', () => { .click(); cy.visit(`/${community.slug}/${channel.slug}`); - cy.get('[data-cy="channel-profile-full"]').should('be.visible'); - cy.get('[data-cy="channel-profile-full"]').contains(NEW_NAME); - cy.get('[data-cy="channel-profile-full"]').contains(NEW_DESCRIPTION); + cy.get('[data-cy="channel-profile-card"]').should('be.visible'); + cy.get('[data-cy="channel-profile-card"]').contains(NEW_NAME); + cy.get('[data-cy="channel-profile-card"]').contains(NEW_DESCRIPTION); }); }); diff --git a/cypress/integration/channel/settings/members_spec.js b/cypress/integration/channel/settings/members_spec.js index c52ddfdab6..a335f414a2 100644 --- a/cypress/integration/channel/settings/members_spec.js +++ b/cypress/integration/channel/settings/members_spec.js @@ -41,8 +41,8 @@ describe('sending a message to channel member', () => { .first() .click(); - cy.url().should('include', '/messages/new'); + cy.url().should('include', '/new/message'); - cy.get('[data-cy="selected-user-pill"]').should('be.visible'); + cy.get('[data-cy="write-direct-message-titlebar"]').should('be.visible'); }); }); diff --git a/cypress/integration/channel/settings/private_invite_link_spec.js b/cypress/integration/channel/settings/private_invite_link_spec.js index 85a9e61bc1..53394eb83c 100644 --- a/cypress/integration/channel/settings/private_invite_link_spec.js +++ b/cypress/integration/channel/settings/private_invite_link_spec.js @@ -13,9 +13,7 @@ const enable = () => { cy.get('[data-cy="login-with-token-settings"]').scrollIntoView(); - cy.get('[data-cy="toggle-token-link-invites-unchecked"]') - .should('be.visible') - .click(); + cy.get('[data-cy="toggle-token-link-invites-unchecked"]').click(); cy.get('[data-cy="join-link-input"]').should('be.visible'); }; @@ -51,9 +49,7 @@ describe('private channel invite link settings', () => { }); // disable - cy.get('[data-cy="toggle-token-link-invites-checked"]') - .should('be.visible') - .click(); + cy.get('[data-cy="toggle-token-link-invites-checked"]').click(); cy.get('[data-cy="join-link-input"]').should('not.be.visible'); }); diff --git a/cypress/integration/channel/view/composer_spec.js b/cypress/integration/channel/view/composer_spec.js index cf42bd8b6c..d58a3bffe2 100644 --- a/cypress/integration/channel/view/composer_spec.js +++ b/cypress/integration/channel/view/composer_spec.js @@ -29,9 +29,9 @@ describe('renders composer for logged in members', () => { it('should render composer', () => { cy.get('[data-cy="channel-view"]').should('be.visible'); - cy.get('[data-cy="thread-composer-placeholder"]').should('be.visible'); - - cy.get('[data-cy="thread-composer-placeholder"]').click(); + cy.get('[data-cy="channel-thread-compose-button"]') + .should('be.visible') + .click(); cy.get('[data-cy="rich-text-editor"]').should('be.visible'); }); @@ -47,7 +47,9 @@ describe('does not render composer for non members', () => { it('should not render composer', () => { cy.get('[data-cy="channel-view"]').should('be.visible'); - cy.get('[data-cy="thread-composer-placeholder"]').should('not.be.visible'); + cy.get('[data-cy="channel-thread-compose-button"]').should( + 'not.be.visible' + ); }); }); @@ -59,7 +61,9 @@ describe('does not render composer for logged out users', () => { it('should not render composer', () => { cy.get('[data-cy="channel-view"]').should('be.visible'); - cy.get('[data-cy="thread-composer-placeholder"]').should('not.be.visible'); + cy.get('[data-cy="channel-thread-compose-button"]').should( + 'not.be.visible' + ); }); }); @@ -73,6 +77,8 @@ describe('does not render composer for archived channel', () => { it('should not render composer', () => { cy.get('[data-cy="channel-view"]').should('be.visible'); - cy.get('[data-cy="thread-composer-placeholder"]').should('not.be.visible'); + cy.get('[data-cy="channel-thread-compose-button"]').should( + 'not.be.visible' + ); }); }); diff --git a/cypress/integration/channel/view/members_spec.js b/cypress/integration/channel/view/members_spec.js index 5052261bb0..539c72db8d 100644 --- a/cypress/integration/channel/view/members_spec.js +++ b/cypress/integration/channel/view/members_spec.js @@ -15,9 +15,9 @@ describe('renders members list on channel view', () => { }); it('should render members component', () => { - cy.get('[data-cy="channel-members-list"]') - .scrollIntoView() - .should('be.visible'); + cy.get('[data-cy="channel-members-tab"]') + .should('be.visible') + .click(); members.map(member => { cy.get('[data-cy="channel-view"]') diff --git a/cypress/integration/channel/view/membership_spec.js b/cypress/integration/channel/view/membership_spec.js index bd48c2664a..244ca3dedc 100644 --- a/cypress/integration/channel/view/membership_spec.js +++ b/cypress/integration/channel/view/membership_spec.js @@ -8,10 +8,6 @@ const community = data.communities.find( community => community.id === publicChannel.communityId ); -const { userId: blockedInChannelId } = data.usersChannels.find( - ({ channelId, isBlocked }) => channelId === publicChannel.id && isBlocked -); - const { userId: ownerInChannelId } = data.usersChannels.find( ({ channelId, isOwner }) => channelId === publicChannel.id && isOwner ); @@ -30,21 +26,21 @@ const QUIET_USER_ID = constants.QUIET_USER_ID; const leave = () => { cy.get('[data-cy="channel-leave-button"]') .should('be.visible') - .contains('Leave channel'); + .contains('Member'); cy.get('[data-cy="channel-leave-button"]').click(); - cy.get('[data-cy="channel-join-button"]').contains(`Join `); + cy.get('[data-cy="channel-join-button"]').contains(`Join channel`); }; const join = () => { cy.get('[data-cy="channel-join-button"]') .should('be.visible') - .contains('Join '); + .contains('Join channel'); cy.get('[data-cy="channel-join-button"]').click(); - cy.get('[data-cy="channel-leave-button"]').contains(`Leave channel`); + cy.get('[data-cy="channel-leave-button"]').contains(`Member`); }; describe('logged out channel membership', () => { @@ -118,7 +114,7 @@ describe('private channel profile', () => { }); it('should render channel not found view', () => { - cy.get('[data-cy="channel-not-found"]').should('be.visible'); + cy.get('[data-cy="channel-view-error"]').should('be.visible'); }); }); }); diff --git a/cypress/integration/channel/view/profile_spec.js b/cypress/integration/channel/view/profile_spec.js index 0a29c2467b..e01ff482fe 100644 --- a/cypress/integration/channel/view/profile_spec.js +++ b/cypress/integration/channel/view/profile_spec.js @@ -41,7 +41,7 @@ describe('public channel', () => { }); it('should contain channel metadata', () => { - cy.get('[data-cy="channel-profile-full"]').should('be.visible'); + cy.get('[data-cy="channel-profile-card"]').should('be.visible'); cy.contains(community.name); cy.contains(publicChannel.description); cy.contains(publicChannel.name); @@ -56,7 +56,7 @@ describe('public channel in private community signed out', () => { }); it('should render channel not found view', () => { - cy.get('[data-cy="channel-not-found"]').should('be.visible'); + cy.get('[data-cy="channel-view-error"]').should('be.visible'); }); }); @@ -84,7 +84,7 @@ describe('public channel in private community without permission', () => { }); it('should render channel not found view', () => { - cy.get('[data-cy="channel-not-found"]').should('be.visible'); + cy.get('[data-cy="channel-view-error"]').should('be.visible'); }); }); @@ -98,8 +98,8 @@ describe('archived channel', () => { }); it('should contain archived tag', () => { - cy.get('[data-cy="channel-profile-full"]').should('be.visible'); - cy.contains('(Archived)'); + cy.get('[data-cy="channel-profile-card"]').should('be.visible'); + cy.contains('Archived'); }); }); @@ -109,8 +109,7 @@ describe('deleted channel', () => { }); it('should render error view', () => { - cy.get('[data-cy="channel-not-found"]').should('be.visible'); - cy.contains('We couldn’t find a channel with this name'); + cy.get('[data-cy="channel-view-error"]').should('be.visible'); }); }); @@ -122,8 +121,7 @@ describe('blocked in public channel', () => { }); it('should render error view', () => { - cy.get('[data-cy="channel-view-blocked"]').should('be.visible'); - cy.contains('You don’t have permission to view this channel'); + cy.get('[data-cy="channel-view-error"]').should('be.visible'); }); }); @@ -147,7 +145,7 @@ describe('blocked in private channel', () => { }); it('should render channel not found view', () => { - cy.get('[data-cy="channel-not-found"]').should('be.visible'); + cy.get('[data-cy="channel-view-error"]').should('be.visible'); }); }); @@ -157,6 +155,6 @@ describe('is not logged in', () => { }); it('should render channel not found view', () => { - cy.get('[data-cy="channel-not-found"]').should('be.visible'); + cy.get('[data-cy="channel-view-error"]').should('be.visible'); }); }); diff --git a/cypress/integration/community/view/profile_spec.js b/cypress/integration/community/view/profile_spec.js index c1f14586ae..72bd09becf 100644 --- a/cypress/integration/community/view/profile_spec.js +++ b/cypress/integration/community/view/profile_spec.js @@ -95,11 +95,12 @@ describe('public community signed out', () => { it('should prompt user to login when joining', () => { cy.get('[data-cy="join-community-button-login"]') + .last() .scrollIntoView() .should('be.visible') .click(); - cy.get('[data-cy="login-page"]').should('be.visible'); + cy.get('[data-cy="login-modal"]').should('be.visible'); }); }); @@ -156,15 +157,16 @@ describe('public community signed in without permission', () => { it('should join the community', () => { cy.get('[data-cy="join-community-button"]') + .last() .scrollIntoView() .should('be.visible'); cy.get('[data-cy="join-community-button"]') - .contains(`Join ${publicCommunity.name}`) + .contains(`Join community`) .click(); cy.get('[data-cy="leave-community-button"]') - .contains(`Leave community`) + .contains(`Member`) .click(); // triggered the leave modal @@ -173,8 +175,10 @@ describe('public community signed in without permission', () => { .should('be.visible') .click(); + cy.get('[data-cy="delete-button"]').should('not.be.visible'); + cy.get('[data-cy="join-community-button"]') - .scrollIntoView() + .last() .should('be.visible'); }); }); @@ -212,14 +216,14 @@ describe('private community signed in without permission', () => { }); it('should render the blocked page', () => { - cy.get('[data-cy="community-view-blocked"]').should('be.visible'); + cy.get('[data-cy="community-view-private"]').should('be.visible'); cy.contains('This community is private'); }); it('should request to join the private community', () => { cy.get('[data-cy="request-to-join-private-community-button"]') .should('be.visible') - .contains(`Request to join ${privateCommunity.name}`) + .contains(`Request to join`) .click(); cy.get('[data-cy="cancel-request-to-join-private-community-button"]') @@ -229,7 +233,7 @@ describe('private community signed in without permission', () => { cy.get('[data-cy="request-to-join-private-community-button"]') .should('be.visible') - .contains(`Request to join ${privateCommunity.name}`); + .contains(`Request to join`); }); }); diff --git a/cypress/integration/composer_selection_spec.js b/cypress/integration/composer_selection_spec.js index c310b3976e..2a2d07cb77 100644 --- a/cypress/integration/composer_selection_spec.js +++ b/cypress/integration/composer_selection_spec.js @@ -23,19 +23,10 @@ const channelDropdown = () => cy.get('[data-cy="composer-channel-selector"]'); const communitySelected = () => cy.get('[data-cy="composer-community-selected"]'); const channelSelected = () => cy.get('[data-cy="composer-channel-selected"]'); -const inboxComposeButton = () => cy.get('[data-cy="inbox-view-post-button"]'); -const everythingFilter = () => - cy.get('[data-cy="inbox-community-list-item"]').first(); -const firstCommunityFilter = () => - cy.get('[data-cy="inbox-community-list-item"]').eq(1); -const secondCommunityFilter = () => - cy.get('[data-cy="inbox-community-list-item"]').eq(2); -const inboxChannelFilter = () => - cy.get('[data-cy="inbox-channel-list-item"]').first(); -const secondInboxChannelFilter = () => - cy.get('[data-cy="inbox-channel-list-item"]').eq(1); -const composerPlaceholder = () => - cy.get('[data-cy="thread-composer-placeholder"]'); +const communityComposerButton = () => + cy.get('[data-cy="community-thread-compose-button"]'); +const channelComposerButton = () => + cy.get('[data-cy="channel-thread-compose-button"]'); const titlebarComposeButton = () => cy.get('[data-cy="titlebar-compose-button"]'); @@ -213,7 +204,7 @@ describe('community view composer', () => { it('should lock the community selection', () => { cy.visit('/spectrum'); - composerPlaceholder() + communityComposerButton() .should('be.visible') .click(); communityIsLocked(); @@ -230,7 +221,7 @@ describe('channel view composer', () => { it('should lock the community and channel selection', () => { cy.visit('/spectrum/general'); - composerPlaceholder() + channelComposerButton() .should('be.visible') .click(); communityIsLocked(); @@ -240,89 +231,6 @@ describe('channel view composer', () => { }); }); -describe('inbox composer', () => { - beforeEach(() => { - cy.auth(user.id).then(() => cy.visit('/')); - }); - - it('does not select a community or channel if the composer is opened from the everything feed', () => { - cy.get('[data-cy="inbox-thread-feed"]').should('be.visible'); - inboxComposeButton().should('be.visible'); - inboxComposeButton().click(); - communityDropdownIsEnabled(); - channelDropdownIsHidden(); - }); - - it('selects a community if the composer is opened from a community filter', () => { - cy.get('[data-cy="inbox-thread-feed"]').should('be.visible'); - firstCommunityFilter().click(); - cy.get('[data-cy="inbox-thread-feed"]').should('be.visible'); - inboxComposeButton().should('be.visible'); - inboxComposeButton().click(); - communityIsLocked(); - communitySelected().contains('Spectrum'); - channelDropdownIsEnabled(); - }); - - it('selects both a community and channel if the composer is opened from a channel filter', () => { - cy.get('[data-cy="inbox-thread-feed"]').should('be.visible'); - firstCommunityFilter().click(); - inboxChannelFilter().click(); - inboxComposeButton().should('be.visible'); - inboxComposeButton().click(); - communityIsLocked(); - communitySelected().contains('Spectrum'); - channelIsLocked(); - channelSelected().contains('General'); - }); - - it('updates the community selection in the composer if the community is switched', () => { - cy.get('[data-cy="inbox-thread-feed"]').should('be.visible'); - firstCommunityFilter().click(); - cy.get('[data-cy="inbox-thread-feed"]').should('be.visible'); - inboxComposeButton().should('be.visible'); - inboxComposeButton().click(); - communityIsLocked(); - communitySelected().contains('Spectrum'); - channelDropdownIsEnabled(); - - cancelButton().click(); - - secondCommunityFilter().click(); - cy.get('[data-cy="inbox-thread-feed"]').should('be.visible'); - inboxComposeButton().should('be.visible'); - inboxComposeButton().click(); - communityIsLocked(); - communitySelected().contains('Payments'); - channelDropdownIsEnabled(); - }); - - it('updates the channel selection in the composer if the channel is switched', () => { - cy.get('[data-cy="inbox-thread-feed"]').should('be.visible'); - firstCommunityFilter().click(); - cy.get('[data-cy="inbox-thread-feed"]').should('be.visible'); - inboxChannelFilter().click(); - cy.get('[data-cy="inbox-thread-feed"]').should('be.visible'); - inboxComposeButton().should('be.visible'); - inboxComposeButton().click(); - communityIsLocked(); - communitySelected().contains('Spectrum'); - channelIsLocked(); - channelSelected().contains('General'); - - cancelButton().click(); - - secondInboxChannelFilter().click(); - cy.get('[data-cy="inbox-thread-feed"]').should('be.visible'); - inboxComposeButton().should('be.visible'); - inboxComposeButton().click(); - communityIsLocked(); - communitySelected().contains('Spectrum'); - channelIsLocked(); - channelSelected().contains('Private'); - }); -}); - describe.skip('mobile tabbar composer', () => { beforeEach(() => { cy.auth(user.id); @@ -358,47 +266,4 @@ describe.skip('mobile tabbar composer', () => { 'http://localhost:3000/new/thread?composerCommunityId=1&composerChannelId=1' ); }); - - it('does not select anything from the inbox everything view', () => { - cy.visit('/'); - titlebarComposeButton() - .should('be.visible') - .click(); - communityDropdownIsEnabled(); - channelDropdownIsHidden(); - cy.url().should('eq', 'http://localhost:3000/new/thread'); - }); - - it('selects a community from the inbox view with a community filter', () => { - cy.visit('/'); - openMobileCommunityMenu(); - firstCommunityFilter().click(); - closeMobileCommunityMenu(); - titlebarComposeButton() - .should('be.visible') - .click(); - communityIsLocked(); - channelDropdownIsEnabled(); - cy.url().should( - 'eq', - 'http://localhost:3000/new/thread?composerCommunityId=1' - ); - }); - - it('selects a community and channel from the inbox view with a channel filter', () => { - cy.visit('/'); - openMobileCommunityMenu(); - firstCommunityFilter().click(); - inboxChannelFilter().click(); - closeMobileCommunityMenu(); - titlebarComposeButton() - .should('be.visible') - .click(); - communityIsLocked(); - channelIsLocked(); - cy.url().should( - 'eq', - 'http://localhost:3000/new/thread?composerCommunityId=1&composerChannelId=1' - ); - }); }); diff --git a/cypress/integration/inbox_spec.js b/cypress/integration/inbox_spec.js deleted file mode 100644 index 8696043052..0000000000 --- a/cypress/integration/inbox_spec.js +++ /dev/null @@ -1,31 +0,0 @@ -import data from '../../shared/testing/data'; - -const user = data.users[0]; -const channelIds = data.usersChannels - .filter(({ userId }) => userId === user.id) - .map(({ channelId }) => channelId); -const dashboardThreads = data.threads.filter( - ({ deletedAt, channelId }) => !deletedAt && channelIds.includes(channelId) -); - -describe('Inbox View', () => { - beforeEach(() => { - cy.auth(user.id).then(() => cy.visit('/')); - }); - - it('should render the inbox view', () => { - cy.get('[data-cy="inbox-view"]').should('be.visible'); - dashboardThreads.forEach(thread => { - cy.contains(thread.content.title); - }); - const usersCommunities = data.usersCommunities - .filter(({ userId }) => user.id === userId) - .map(({ communityId }) => - data.communities.find(({ id }) => id === communityId) - ); - usersCommunities.forEach(community => { - cy.contains(community.name); - }); - cy.get('[data-cy="thread-view"]').should('be.visible'); - }); -}); diff --git a/cypress/integration/messages_spec.js b/cypress/integration/messages_spec.js index bcbcb9d7e5..47fb8f4551 100644 --- a/cypress/integration/messages_spec.js +++ b/cypress/integration/messages_spec.js @@ -5,19 +5,60 @@ const directMessages = data.usersDirectMessageThreads.filter( udm => udm.userId === user.id ); -describe('/messages/new', () => { +const newMessage = 'Persist New Message'; + +describe('/new/message', () => { beforeEach(() => { - cy.auth(user.id).then(() => cy.visit('/messages/new')); + cy.auth(user.id); + }); + + it('should show the message composer modal with search', () => { + cy.visit('/new/message'); + cy.get('[data-cy="dm-composer"]').should('be.visible'); + cy.get('[data-cy="dm-composer-search"]').should('be.visible'); + }); + + it('should render in a modal from another view', () => { + cy.visit('/users/bryn'); + cy.get('[data-cy="message-user-button"]') + .last() + .should('be.visible') + .click(); + cy.get('[data-cy="dm-composer"]').should('be.visible'); + cy.get('[data-cy="chat-input"]').should('be.visible'); + cy.url().should('include', '/new/message'); }); - it('should allow to continue composing message incase of crash or reload', () => { - const newMessage = 'Persist New Message'; + it('should send a direct message', () => { + cy.visit('/users/bryn'); + cy.get('[data-cy="message-user-button"]') + .last() + .should('be.visible') + .click(); + cy.get('[data-cy="dm-composer"]').should('be.visible'); + cy.get('[data-cy="chat-input"]').should('be.visible'); cy.get('[data-cy="chat-input"]').type(newMessage); - cy.get('[data-cy="chat-input"]').contains(newMessage); + cy.get('[data-cy="chat-input-send-button"]').click(); + cy.get('[data-cy="message-group"]').should('be.visible'); + }); + it('should persist the dm chat input', () => { + cy.visit('/users/bryn'); + cy.get('[data-cy="message-user-button"]') + .last() + .should('be.visible') + .click(); + cy.get('[data-cy="dm-composer"]').should('be.visible'); + cy.get('[data-cy="chat-input"]').should('be.visible'); + cy.get('[data-cy="chat-input"]').type(newMessage); cy.wait(2000); - // Reload page(incase page closed or crashed ,reload should have same effect) cy.reload(); + cy.visit('/users/bryn'); + cy.get('[data-cy="message-user-button"]') + .last() + .should('be.visible') + .click(); + cy.get('[data-cy="dm-composer"]').should('be.visible'); cy.get('[data-cy="chat-input"]').contains(newMessage); }); }); @@ -46,7 +87,7 @@ describe('/messages', () => { cy.get('[data-cy="compose-dm"]') .should('be.visible') .click(); - cy.url().should('eq', 'http://localhost:3000/messages/new'); + cy.url().should('eq', 'http://localhost:3000/new/message'); }); it('should select an individual conversation', () => { @@ -73,8 +114,7 @@ describe('/messages', () => { cy.contains('Previous member') .should('be.visible') .click(); - cy.get('[data-cy="dm-header"]').should('be.visible'); - cy.get('[data-cy="dm-header"]').contains('Previous member'); + cy.get('[data-cy="dm-header"]').should('not.be.visible'); cy.get('[data-cy="message"]').should($p => { expect($p).to.have.length(0); }); @@ -109,37 +149,37 @@ describe('/messages', () => { describe('messages tab badge count', () => { beforeEach(() => { - cy.auth(user.id).then(() => cy.visit('/')); + cy.auth(user.id).then(() => cy.visit('/spectrum')); }); it('should show a badge for unread direct messages', () => { - cy.get('[data-cy="unread-badge-1"]').should('be.visible'); + cy.get('[data-cy="unread-dm-badge"]').should('be.visible'); }); it('should clear the badge when messages tab clicked', () => { - cy.get('[data-cy="unread-badge-1"]').should('be.visible'); - cy.get('[data-cy="navbar-messages"]').click(); + cy.get('[data-cy="unread-dm-badge"]').should('be.visible'); + cy.get('[data-cy="navigation-messages"]').click(); cy.get('[data-cy="dm-list-item"]').should($p => { expect($p).to.have.length(1); }); cy.get('[data-cy="unread-dm-list-item"]').should($p => { expect($p).to.have.length(1); }); - cy.get('[data-cy="unread-badge-0"]').should('be.visible'); + cy.get('[data-cy="unread-dm-badge"]').should('not.be.visible'); }); it('should not show an unread badge after leaving messages tab', () => { - cy.get('[data-cy="unread-badge-1"]').should('be.visible'); - cy.get('[data-cy="navbar-messages"]').click(); + cy.get('[data-cy="unread-dm-badge"]').should('be.visible'); + cy.get('[data-cy="navigation-messages"]').click(); cy.get('[data-cy="dm-list-item"]').should($p => { expect($p).to.have.length(1); }); cy.get('[data-cy="unread-dm-list-item"]').should($p => { expect($p).to.have.length(1); }); - cy.get('[data-cy="unread-badge-0"]').should('be.visible'); - cy.get('[data-cy="navbar-home"]').click(); - cy.get('[data-cy="unread-badge-0"]').should('be.visible'); + cy.get('[data-cy="unread-dm-badge"]').should('not.be.visible'); + cy.get('[data-cy="navigation-explore"]').click(); + cy.get('[data-cy="unread-dm-badge"]').should('not.be.visible'); }); }); @@ -155,65 +195,6 @@ describe('clearing messages tab', () => { cy.get('[data-cy="unread-dm-list-item"]').should($p => { expect($p).to.have.length(1); }); - cy.get('[data-cy="unread-badge-0"]').should('be.visible'); - }); -}); - -describe('sending a message from user profile', () => { - beforeEach(() => { - cy.auth(user.id).then(() => cy.visit('/users/bryn')); - }); - - const dmButton = () => cy.get('[data-cy="send-dm-button"]'); - const stagedDMPills = () => cy.get('[data-cy="selected-users-pills"]'); - const chatInput = () => cy.get('[data-cy="chat-input"]'); - const sendButton = () => cy.get('[data-cy="chat-input-send-button"]'); - - it('sends a direct message from the user profile on desktop', () => { - dmButton() - .should('be.visible') - .click(); - cy.url('eq', 'http://localhost:3000/messages/new'); - cy.get('[data-cy="unread-dm-list-item"]').should($p => { - expect($p).to.have.length(1); - }); - cy.get('[data-cy="dm-list-item"]').should($p => { - expect($p).to.have.length(1); - }); - stagedDMPills() - .should('be.visible') - .contains('Bryn'); - chatInput().type('New message'); - sendButton().click(); - cy.get('[data-cy="unread-dm-list-item"]').should($p => { - expect($p).to.have.length(1); - }); - cy.get('[data-cy="dm-list-item"]').should($p => { - expect($p).to.have.length(2); - }); - }); - - it('sends a direct message from the user profile on mobile', () => { - cy.viewport('iphone-6'); - dmButton() - .should('be.visible') - .click(); - cy.url('eq', 'http://localhost:3000/messages/new'); - stagedDMPills() - .should('be.visible') - .contains('Bryn'); - chatInput().type('New message'); - sendButton().click(); - cy.contains('Bryn Jackson'); - cy.contains('@bryn'); - cy.contains('New message'); - cy.get('[data-cy="titlebar-back"]').click(); - cy.url('eq', 'http://localhost:3000/messages'); - cy.get('[data-cy="unread-dm-list-item"]').should($p => { - expect($p).to.have.length(1); - }); - cy.get('[data-cy="dm-list-item"]').should($p => { - expect($p).to.have.length(2); - }); + cy.get('[data-cy="unread-dm-badge"]').should('not.be.visible'); }); }); diff --git a/cypress/integration/modal_routes_spec.js b/cypress/integration/modal_routes_spec.js index b37a76f0d7..2e557ab206 100644 --- a/cypress/integration/modal_routes_spec.js +++ b/cypress/integration/modal_routes_spec.js @@ -1,14 +1,13 @@ import data from '../../shared/testing/data'; const user = data.users.find(user => user.username === 'brian'); -const pressEscape = () => cy.get('body').trigger('keydown', { keyCode: 27 }); +const pressEscape = () => + cy.get('[data-cy="modal-container"]').trigger('keydown', { keyCode: 27 }); -const inboxBeforeUrlIsValid = () => - cy.url().should('eq', 'http://localhost:3000/?t=thread-9'); const communityBeforeUrlIsValid = () => - cy.url().should('eq', 'http://localhost:3000/spectrum'); + cy.url().should('eq', 'http://localhost:3000/spectrum?tab=posts'); const channelBeforeUrlIsValid = () => - cy.url().should('eq', 'http://localhost:3000/spectrum/general'); + cy.url().should('eq', 'http://localhost:3000/spectrum/general?tab=posts'); describe('composer modal route', () => { beforeEach(() => { @@ -17,71 +16,16 @@ describe('composer modal route', () => { const threadComposerWrapper = () => cy.get('[data-cy="thread-composer-wrapper"]'); - const threadComposerOverlay = () => - cy.get('[data-cy="thread-composer-overlay"]'); - const threadSliderOverlay = () => cy.get('[data-cy="thread-slider-overlay"]'); - const cancelThreadComposer = () => - cy.get('[data-cy="composer-cancel-button"]').click(); - const threadComposer = () => cy.get('[data-cy="thread-composer"]'); - const composerPlaceholder = () => - cy.get('[data-cy="thread-composer-placeholder"]'); - const inboxComposeButton = () => cy.get('[data-cy="inbox-view-post-button"]'); - - it('handles esc key', () => { - cy.visit('/'); - cy.get('[data-cy="inbox-thread-feed"]').should('be.visible'); - inboxBeforeUrlIsValid(); - inboxComposeButton().should('be.visible'); - inboxComposeButton().click(); - threadComposer().should('be.visible'); - threadComposerWrapper().should('be.visible'); - cy.url().should('eq', 'http://localhost:3000/new/thread'); - - pressEscape(); - - threadComposer().should('not.be.visible'); - threadComposerWrapper().should('not.be.visible'); - inboxBeforeUrlIsValid(); - }); - - it('handles overlay click', () => { - cy.visit('/'); - cy.get('[data-cy="inbox-thread-feed"]').should('be.visible'); - inboxBeforeUrlIsValid(); - inboxComposeButton().should('be.visible'); - inboxComposeButton().click(); - threadComposer().should('be.visible'); - threadComposerWrapper().should('be.visible'); - cy.url().should('eq', 'http://localhost:3000/new/thread'); - - threadComposerWrapper().click(200, 200); - - threadComposer().should('not.be.visible'); - threadComposerWrapper().should('not.be.visible'); - inboxBeforeUrlIsValid(); - }); - - it('handles cancel click', () => { - cy.visit('/'); - cy.get('[data-cy="inbox-thread-feed"]').should('be.visible'); - inboxBeforeUrlIsValid(); - inboxComposeButton().should('be.visible'); - inboxComposeButton().click(); - threadComposer().should('be.visible'); - threadComposerWrapper().should('be.visible'); - cy.url().should('eq', 'http://localhost:3000/new/thread'); - - cancelThreadComposer(); - - threadComposer().should('not.be.visible'); - threadComposerWrapper().should('not.be.visible'); - inboxBeforeUrlIsValid(); - }); + const threadComposer = () => cy.get('[data-cy="modal-container"]'); + const communityComposerPlaceholder = () => + cy.get('[data-cy="community-thread-compose-button"]'); + const channelComposerPlaceholder = () => + cy.get('[data-cy="channel-thread-compose-button"]'); it('handles community view', () => { cy.visit('/spectrum'); communityBeforeUrlIsValid(); - composerPlaceholder().click(); + communityComposerPlaceholder().click(); threadComposer().should('be.visible'); threadComposerWrapper().should('be.visible'); cy.url().should( @@ -99,7 +43,7 @@ describe('composer modal route', () => { it('handles channel view', () => { cy.visit('/spectrum/general'); channelBeforeUrlIsValid(); - composerPlaceholder().click(); + channelComposerPlaceholder().click(); threadComposer().should('be.visible'); threadComposerWrapper().should('be.visible'); cy.url().should( @@ -116,7 +60,7 @@ describe('composer modal route', () => { }); describe('thread modal route', () => { - const threadSlider = () => cy.get('[data-cy="thread-slider"]'); + const threadSlider = () => cy.get('[data-cy="modal-container"]'); const threadSliderClose = () => cy.get('[data-cy="thread-slider-close"]'); it('handles esc key', () => { @@ -147,7 +91,7 @@ describe('thread modal route', () => { 'http://localhost:3000/spectrum/private/yet-another-thread~thread-6' ); - cy.get('body').click(200, 200); + cy.get('[data-cy="overlay"]').click(200, 200, { force: true }); communityBeforeUrlIsValid(); threadSlider().should('not.be.visible'); @@ -187,17 +131,6 @@ describe('thread modal route', () => { threadSlider().should('not.be.visible'); }); - it('handles inbox feed', () => { - cy.auth(user.id); - cy.visit('/'); - cy.get('[data-cy="inbox-thread-feed"]').should('be.visible'); - cy.wait(500); - cy.get('[data-cy="thread-card"]') - .eq(1) - .click(); - threadSlider().should('not.be.visible'); - }); - it('handles thread attachment', () => { cy.auth(user.id); cy.visit('/spectrum/private/yet-another-thread~thread-6'); diff --git a/cypress/integration/navbar_spec.js b/cypress/integration/navbar_spec.js index 2549e31178..66cfe6c430 100644 --- a/cypress/integration/navbar_spec.js +++ b/cypress/integration/navbar_spec.js @@ -3,51 +3,53 @@ import data from '../../shared/testing/data'; const user = data.users[0]; const coreSplashPageNavbarLinksVisible = () => { - cy.get('[data-cy="navbar-splash"]').should('be.visible'); + cy.get('[data-cy="navigation-splash"]').should('be.visible'); - cy.get('[data-cy="navbar-splash-features"]').should('be.visible'); + cy.get('[data-cy="navigation-splash-features"]').should('be.visible'); - cy.get('[data-cy="navbar-splash-apps"]').should('be.visible'); + cy.get('[data-cy="navigation-splash-apps"]').should('be.visible'); - cy.get('[data-cy="navbar-splash-support"]').should('be.visible'); + cy.get('[data-cy="navigation-splash-support"]').should('be.visible'); }; const checkSignedOutSplashNavbarLinksRender = () => { coreSplashPageNavbarLinksVisible(); - cy.get('[data-cy="navbar-splash-signin"]').should('be.visible'); + cy.get('[data-cy="navigation-splash-signin"]').should('be.visible'); }; const checkSignedInSplashNavbarLinksRender = () => { coreSplashPageNavbarLinksVisible(); - cy.get('[data-cy="navbar-splash-profile"]').should('be.visible'); + cy.get('[data-cy="navigation-splash-profile"]').should('be.visible'); }; const checkProductNavbarLinksRender = () => { - cy.get('[data-cy="navbar"]').should('be.visible'); + cy.get('[data-cy="navigation-bar"]').should('be.visible'); - cy.get('[data-cy="navbar-logo"]').should('be.visible'); + cy.get('[data-cy="navigation-composer"]').should('be.visible'); - cy.get('[data-cy="navbar-home"]').should('be.visible'); + cy.get('[data-cy="navigation-messages"]').should('be.visible'); - cy.get('[data-cy="navbar-messages"]').should('be.visible'); + cy.get('[data-cy="navigation-notifications"]').should('be.visible'); - cy.get('[data-cy="navbar-explore"]').should('be.visible'); + cy.get('[data-cy="navigation-explore"]').should('be.visible'); - cy.get('[data-cy="navbar-notifications"]').should('be.visible'); - - cy.get('[data-cy="navbar-profile"]').should('be.visible'); + cy.get('[data-cy="navigation-profile"]').should('be.visible'); }; const checkSignedOutNavbarRenders = () => { - cy.get('[data-cy="navbar"]').should('be.visible'); + cy.get('[data-cy="navigation-bar"]').should('be.visible'); + + cy.get('[data-cy="navigation-explore"]').should('be.visible'); - cy.get('[data-cy="navbar-logo"]').should('be.visible'); + cy.get('[data-cy="navigation-support"]').should('be.visible'); - cy.get('[data-cy="navbar-explore"]').should('be.visible'); + cy.get('[data-cy="navigation-apps"]').should('be.visible'); - cy.get('[data-cy="navbar-support"]').should('be.visible'); + cy.get('[data-cy="navigation-features"]').should('be.visible'); + + cy.get('[data-cy="navigation-login"]').should('be.visible'); }; const checkSignedOutSplashNavbarRenders = () => { @@ -80,7 +82,7 @@ const checkSignedInSplashNavbarRenders = () => { describe('Navbar logged in', () => { beforeEach(() => { - cy.auth(user.id).then(() => cy.visit(`/`)); + cy.auth(user.id).then(() => cy.visit('/explore')); }); it('should render product navbar', () => { @@ -99,6 +101,12 @@ describe('Navbar logged in', () => { cy.visit(`/users/${user.username}`); checkProductNavbarLinksRender(); + + cy.visit(`/spectrum`); + checkProductNavbarLinksRender(); + + cy.visit(`/spectrum/general`); + checkProductNavbarLinksRender(); }); it('should render splash navbar when viewing splash pages', () => { @@ -108,7 +116,7 @@ describe('Navbar logged in', () => { describe('Navbar logged out', () => { beforeEach(() => { - cy.visit(`/`); + cy.visit('/'); }); it('should render splash page navbar', () => { diff --git a/cypress/integration/thread/action_bar_spec.js b/cypress/integration/thread/action_bar_spec.js index c8b1d9d175..8cb8280117 100644 --- a/cypress/integration/thread/action_bar_spec.js +++ b/cypress/integration/thread/action_bar_spec.js @@ -95,12 +95,11 @@ describe('action bar renders', () => { it('should render', () => { cy.get('[data-cy="thread-view"]').should('be.visible'); - cy.get('[data-cy="thread-notifications-toggle"]').should('be.visible'); cy.get('[data-cy="thread-facebook-button"]').should('be.visible'); cy.get('[data-cy="thread-tweet-button"]').should('be.visible'); cy.get('[data-cy="thread-copy-link-button"]').should('be.visible'); cy.get('[data-cy="thread-actions-dropdown-trigger"]').should( - 'not.be.visible' + 'be.visible' ); }); }); @@ -114,12 +113,11 @@ describe('action bar renders', () => { it('should render', () => { cy.get('[data-cy="thread-view"]').should('be.visible'); - cy.get('[data-cy="thread-notifications-toggle"]').should('be.visible'); cy.get('[data-cy="thread-facebook-button"]').should('be.visible'); cy.get('[data-cy="thread-tweet-button"]').should('be.visible'); cy.get('[data-cy="thread-copy-link-button"]').should('be.visible'); cy.get('[data-cy="thread-actions-dropdown-trigger"]').should( - 'not.be.visible' + 'be.visible' ); }); }); @@ -133,12 +131,11 @@ describe('action bar renders', () => { it('should render', () => { cy.get('[data-cy="thread-view"]').should('be.visible'); - cy.get('[data-cy="thread-notifications-toggle"]').should('be.visible'); cy.get('[data-cy="thread-facebook-button"]').should('not.be.visible'); cy.get('[data-cy="thread-tweet-button"]').should('not.be.visible'); cy.get('[data-cy="thread-copy-link-button"]').should('be.visible'); cy.get('[data-cy="thread-actions-dropdown-trigger"]').should( - 'not.be.visible' + 'be.visible' ); }); }); @@ -152,7 +149,6 @@ describe('action bar renders', () => { it('should render', () => { cy.get('[data-cy="thread-view"]').should('be.visible'); - cy.get('[data-cy="thread-notifications-toggle"]').should('be.visible'); cy.get('[data-cy="thread-facebook-button"]').should('be.visible'); cy.get('[data-cy="thread-tweet-button"]').should('be.visible'); cy.get('[data-cy="thread-copy-link-button"]').should('be.visible'); @@ -167,6 +163,7 @@ describe('action bar renders', () => { cy.get('[data-cy="thread-dropdown-move"]').should('not.be.visible'); cy.get('[data-cy="thread-dropdown-lock"]').should('be.visible'); cy.get('[data-cy="thread-dropdown-delete"]').should('be.visible'); + cy.get('[data-cy="thread-dropdown-notifications"]').should('be.visible'); }); it('should lock the thread', () => { @@ -181,7 +178,7 @@ describe('action bar renders', () => { triggerThreadDelete(); }); - it('should edit the thread', () => { + it.only('should edit the thread', () => { cy.auth(publicThreadAuthor.id); openSettingsDropdown(); @@ -198,6 +195,7 @@ describe('action bar renders', () => { cy.contains(title); // undo the edit + openSettingsDropdown(); cy.get('[data-cy="thread-dropdown-edit"]').click(); cy.get('[data-cy="save-thread-edit-button"]').should('be.visible'); const originalTitle = 'The first thread! 🎉'; @@ -220,7 +218,6 @@ describe('action bar renders', () => { it('should render', () => { cy.get('[data-cy="thread-view"]').should('be.visible'); - cy.get('[data-cy="thread-notifications-toggle"]').should('be.visible'); cy.get('[data-cy="thread-facebook-button"]').should('be.visible'); cy.get('[data-cy="thread-tweet-button"]').should('be.visible'); cy.get('[data-cy="thread-copy-link-button"]').should('be.visible'); @@ -230,11 +227,12 @@ describe('action bar renders', () => { cy.get('[data-cy="thread-actions-dropdown"]').should('be.visible'); // dropdown controls - cy.get('[data-cy="thread-dropdown-edit"]').should('not.be.visible'); cy.get('[data-cy="thread-dropdown-pin"]').should('not.be.visible'); cy.get('[data-cy="thread-dropdown-move"]').should('not.be.visible'); + cy.get('[data-cy="thread-dropdown-edit"]').should('be.visible'); cy.get('[data-cy="thread-dropdown-lock"]').should('be.visible'); cy.get('[data-cy="thread-dropdown-delete"]').should('be.visible'); + cy.get('[data-cy="thread-dropdown-notifications"]').should('be.visible'); }); it('should lock the thread', () => { @@ -261,7 +259,6 @@ describe('action bar renders', () => { it('should render', () => { cy.get('[data-cy="thread-view"]').should('be.visible'); - cy.get('[data-cy="thread-notifications-toggle"]').should('be.visible'); cy.get('[data-cy="thread-facebook-button"]').should('be.visible'); cy.get('[data-cy="thread-tweet-button"]').should('be.visible'); cy.get('[data-cy="thread-copy-link-button"]').should('be.visible'); @@ -271,11 +268,12 @@ describe('action bar renders', () => { cy.get('[data-cy="thread-actions-dropdown"]').should('be.visible'); // dropdown controls - cy.get('[data-cy="thread-dropdown-edit"]').should('not.be.visible'); cy.get('[data-cy="thread-dropdown-pin"]').should('not.be.visible'); cy.get('[data-cy="thread-dropdown-move"]').should('not.be.visible'); + cy.get('[data-cy="thread-dropdown-edit"]').should('be.visible'); cy.get('[data-cy="thread-dropdown-lock"]').should('be.visible'); cy.get('[data-cy="thread-dropdown-delete"]').should('be.visible'); + cy.get('[data-cy="thread-dropdown-notifications"]').should('be.visible'); }); it('should lock the thread', () => { @@ -302,7 +300,6 @@ describe('action bar renders', () => { it('should render', () => { cy.get('[data-cy="thread-view"]').should('be.visible'); - cy.get('[data-cy="thread-notifications-toggle"]').should('be.visible'); cy.get('[data-cy="thread-facebook-button"]').should('be.visible'); cy.get('[data-cy="thread-tweet-button"]').should('be.visible'); cy.get('[data-cy="thread-copy-link-button"]').should('be.visible'); @@ -312,11 +309,12 @@ describe('action bar renders', () => { cy.get('[data-cy="thread-actions-dropdown"]').should('be.visible'); // dropdown controls - cy.get('[data-cy="thread-dropdown-edit"]').should('not.be.visible'); cy.get('[data-cy="thread-dropdown-pin"]').should('be.visible'); cy.get('[data-cy="thread-dropdown-move"]').should('be.visible'); cy.get('[data-cy="thread-dropdown-lock"]').should('be.visible'); cy.get('[data-cy="thread-dropdown-delete"]').should('be.visible'); + cy.get('[data-cy="thread-dropdown-edit"]').should('be.visible'); + cy.get('[data-cy="thread-dropdown-notifications"]').should('be.visible'); }); it('should lock the thread', () => { @@ -357,7 +355,6 @@ describe('action bar renders', () => { it('should render', () => { cy.get('[data-cy="thread-view"]').should('be.visible'); - cy.get('[data-cy="thread-notifications-toggle"]').should('be.visible'); cy.get('[data-cy="thread-facebook-button"]').should('be.visible'); cy.get('[data-cy="thread-tweet-button"]').should('be.visible'); cy.get('[data-cy="thread-copy-link-button"]').should('be.visible'); @@ -367,11 +364,12 @@ describe('action bar renders', () => { cy.get('[data-cy="thread-actions-dropdown"]').should('be.visible'); // dropdown controls - cy.get('[data-cy="thread-dropdown-edit"]').should('not.be.visible'); cy.get('[data-cy="thread-dropdown-pin"]').should('be.visible'); cy.get('[data-cy="thread-dropdown-move"]').should('be.visible'); cy.get('[data-cy="thread-dropdown-lock"]').should('be.visible'); cy.get('[data-cy="thread-dropdown-delete"]').should('be.visible'); + cy.get('[data-cy="thread-dropdown-edit"]').should('be.visible'); + cy.get('[data-cy="thread-dropdown-notifications"]').should('be.visible'); }); it('should lock the thread', () => { diff --git a/cypress/integration/thread/chat_input_spec.js b/cypress/integration/thread/chat_input_spec.js index 7e59f6e77e..aa1cb5228f 100644 --- a/cypress/integration/thread/chat_input_spec.js +++ b/cypress/integration/thread/chat_input_spec.js @@ -33,8 +33,8 @@ describe('chat input', () => { cy.get('[data-cy="thread-view"]').should('be.visible'); cy.get('[data-cy="chat-input-send-button"]').should('not.be.visible'); cy.get('[data-cy="chat-input-media-uploader"]').should('not.be.visible'); - cy.get('[data-cy="join-channel-login-upsell"]').should('be.visible'); - cy.get('[data-cy="join-channel-login-upsell"]').click(); + cy.get('[data-cy="join-community-chat-upsell"]').should('be.visible'); + cy.get('[data-cy="join-community-chat-upsell"]').click(); cy.get('[data-cy="login-modal"]').should('be.visible'); }); }); @@ -49,9 +49,7 @@ describe('chat input', () => { it('should render', () => { cy.get('[data-cy="thread-view"]').should('be.visible'); cy.get('[data-cy="chat-input-send-button"]').should('not.be.visible'); - cy.get('[data-cy="thread-join-channel-upsell-button"]').should( - 'be.visible' - ); + cy.get('[data-cy="join-community-chat-upsell"]').should('be.visible'); }); }); @@ -144,6 +142,7 @@ describe('chat input', () => { }); it('should render', () => { + cy.get('[data-cy="thread-view"]').should('be.visible'); cy.get('[data-cy="chat-input-send-button"]').should('not.be.visible'); }); }); diff --git a/cypress/integration/thread/view_spec.js b/cypress/integration/thread/view_spec.js index 2e66e23a57..7433b3755c 100644 --- a/cypress/integration/thread/view_spec.js +++ b/cypress/integration/thread/view_spec.js @@ -41,23 +41,16 @@ describe('sidebar components on thread view', () => { }); it('should render', () => { - // loaded login upsell in sidebar - cy.get('[data-cy="thread-sidebar-login"]') - .scrollIntoView() - .should('be.visible'); - // loaded community info - cy.get('[data-cy="thread-sidebar-community-info"]') + cy.get('[data-cy="community-profile-card"]') .scrollIntoView() .should('be.visible'); // loaded join button which directs to login - cy.get('[data-cy="thread-sidebar-join-login-button"]').should( - 'be.visible' - ); + cy.get('[data-cy="profile-join-button"]').should('be.visible'); // loaded more conversations component - cy.get('[data-cy="thread-sidebar-more-threads"]') + cy.get('[data-cy="trending-conversations"]') .scrollIntoView() .should('be.visible'); }); @@ -71,21 +64,18 @@ describe('sidebar components on thread view', () => { }); it('should render', () => { - // loaded login upsell in sidebar - cy.get('[data-cy="thread-sidebar-login"]').should('not.be.visible'); - // loaded community info - cy.get('[data-cy="thread-sidebar-community-info"]') + cy.get('[data-cy="community-profile-card"]') .scrollIntoView() .should('be.visible'); // loaded join button which directs to login - cy.get('[data-cy="thread-sidebar-join-community-button"]') + cy.get('[data-cy="profile-join-button"]') .scrollIntoView() .should('be.visible'); // loaded more conversations component - cy.get('[data-cy="thread-sidebar-more-threads"]') + cy.get('[data-cy="trending-conversations"]') .scrollIntoView() .should('be.visible'); }); @@ -99,21 +89,18 @@ describe('sidebar components on thread view', () => { }); it('should render', () => { - // loaded login upsell in sidebar - cy.get('[data-cy="thread-sidebar-login"]').should('not.be.visible'); - // loaded community info - cy.get('[data-cy="thread-sidebar-community-info"]') + cy.get('[data-cy="community-profile-card"]') .scrollIntoView() .should('be.visible'); // loaded join button which directs to login - cy.get('[data-cy="thread-sidebar-view-community-button"]') + cy.get('[data-cy="community-profile-card"]') .scrollIntoView() .should('be.visible'); // loaded more conversations component - cy.get('[data-cy="thread-sidebar-more-threads"]') + cy.get('[data-cy="trending-conversations"]') .scrollIntoView() .should('be.visible'); }); @@ -181,7 +168,7 @@ describe('public thread', () => { it('should render', () => { cy.get('[data-cy="thread-view"]').should('not.be.visible'); - cy.get('[data-cy="blocked-thread-view"]').should('be.visible'); + cy.get('[data-cy="null-thread-view"]').should('be.visible'); }); }); @@ -194,7 +181,7 @@ describe('public thread', () => { it('should render', () => { cy.get('[data-cy="thread-view"]').should('not.be.visible'); - cy.get('[data-cy="blocked-thread-view"]').should('be.visible'); + cy.get('[data-cy="null-thread-view"]').should('be.visible'); }); }); }); diff --git a/cypress/integration/thread_spec.js b/cypress/integration/thread_spec.js index 9183cc6aba..503786b85c 100644 --- a/cypress/integration/thread_spec.js +++ b/cypress/integration/thread_spec.js @@ -50,10 +50,9 @@ describe('Thread View', () => { }); it('should prompt logged-out users to log in', () => { - const newMessage = 'A new message!'; cy.get('[data-cy="thread-view"]').should('be.visible'); - cy.get('[data-cy="join-channel-login-upsell"]').should('be.visible'); - cy.get('[data-cy="join-channel-login-upsell"]').click(); + cy.get('[data-cy="join-community-chat-upsell"]').should('be.visible'); + cy.get('[data-cy="join-community-chat-upsell"]').click(); cy.get('[data-cy="login-modal"]').should('be.visible'); }); }); @@ -126,19 +125,9 @@ describe('Thread View', () => { expect($p).to.have.length(1); }); - // the other message should be unselected - cy.get('[data-cy="message"]').should($p => { - expect($p).to.have.length(1); - }); - - // load previous messages should be visible - cy.get('[data-cy="load-previous-messages"]') - .should('be.visible') - .click(); - - // all the messages should be loaded + // the other messages should be unselected cy.get('[data-cy="message"]').should($p => { - expect($p).to.have.length(4); + expect($p).to.have.length(3); }); }); }); @@ -158,18 +147,6 @@ describe('Thread View', () => { .first() .should('be.visible') .click({ force: true }); - // message should be selected - cy.get('[data-cy="message-selected"]').should('be.visible'); - // only one message should be selected - cy.get('[data-cy="message-selected"]').should($p => { - expect($p).to.have.length(1); - }); - // the other three messages should be unselected - cy.get('[data-cy="message"]').should($p => { - expect($p).to.have.length(3); - }); - // the url should contain the message query param - cy.url().should('contain', `${thread.id}?m=MTQ4MzIyNTE5OTk5OQ==`); }); }); @@ -188,17 +165,7 @@ describe('Thread View', () => { .first() .should('be.visible') .click({ force: true }); - // message should be selected - cy.get('[data-cy="message-selected"]').should('be.visible'); - // only one message should be selected - cy.get('[data-cy="message-selected"]').should($p => { - expect($p).to.have.length(1); - }); - // the other three messages should be unselected - cy.get('[data-cy="message"]').should($p => { - expect($p).to.have.length(3); - }); - // the url should contain the message query param + cy.url().should('contain', `?m=MTQ4MzIyNTE5OTk5OQ==`); }); }); diff --git a/cypress/integration/toasts_spec.js b/cypress/integration/toasts_spec.js index 896179d6df..90a378de20 100644 --- a/cypress/integration/toasts_spec.js +++ b/cypress/integration/toasts_spec.js @@ -48,18 +48,6 @@ describe('Toasts and url query paramaters', () => { }); it('should preserve existing query parameters', () => { - const url = new URL('http://localhost:3000/?t=thread-9'); - url.searchParams.append('toastType', 'success'); - url.searchParams.append('toastMessage', toastMessage); - cy.visit(url.toString()); - cy.get('[data-cy="toast-success"]', { timeout: 100 }).should( - 'have.length', - 1 - ); - cy.url().should('eq', 'http://localhost:3000/?t=thread-9'); - }); - - it('should preserve many existing query parameters', () => { const url = new URL( 'http://localhost:3000/spectrum/general/another-thread~thread-2?m=MTQ4MzIyNTIwMDAwMg==' ); diff --git a/cypress/integration/user/new_user_spec.js b/cypress/integration/user/new_user_spec.js new file mode 100644 index 0000000000..1607147294 --- /dev/null +++ b/cypress/integration/user/new_user_spec.js @@ -0,0 +1,141 @@ +import data from '../../../shared/testing/data'; +const user = data.users.find(u => !u.username); + +const setUsernameIsVisible = () => { + cy.get('[data-cy="new-user-onboarding"]').should('be.visible'); + cy.url().should('include', '/new/user'); +}; + +const saveUsername = () => { + cy.get('[data-cy="save-username-button"]') + .should('be.visible') + .click(); +}; + +describe('brand new user', () => { + beforeEach(() => { + cy.auth(user.id).then(() => cy.visit('/')); + }); + + it('should redirect to /new/user', () => { + setUsernameIsVisible(); + }); + + it('should prevent viewing anything without a username', () => { + setUsernameIsVisible(); + + cy.get('[data-cy="navigation-explore"]') + .should('be.visible') + .click(); + setUsernameIsVisible(); + + cy.get('[data-cy="navigation-messages"]') + .should('be.visible') + .click(); + setUsernameIsVisible(); + + cy.get('[data-cy="navigation-notifications"]') + .should('be.visible') + .click(); + setUsernameIsVisible(); + + cy.get('[data-cy="navigation-profile"]') + .should('be.visible') + .click(); + setUsernameIsVisible(); + + cy.visit('/spectrum'); + setUsernameIsVisible(); + + cy.visit('/spectrum/general'); + setUsernameIsVisible(); + + cy.visit('/thread/thread-1'); + setUsernameIsVisible(); + + cy.visit('/me'); + setUsernameIsVisible(); + + cy.visit('/me/settings'); + setUsernameIsVisible(); + }); + + it('should handle username collisions', () => { + setUsernameIsVisible(); + cy.get('[data-cy="username-search"]') + .should('be.visible') + .clear(); + cy.get('[data-cy="username-search"]').type('mxstbr'); + cy.get('[data-cy="username-search-error"]').should('be.visible'); + cy.get('[data-cy="username-search"]') + .clear() + .type('new-user'); + cy.get('[data-cy="username-search-error"]').should('not.be.visible'); + cy.get('[data-cy="username-search-success"]').should('not.be.visible'); + }); + + it('should allow the user to logout', () => { + setUsernameIsVisible(); + cy.get('[data-cy="new-user-onboarding-logout"]').should('be.visible'); + }); +}); + +describe('post username creation redirects', () => { + beforeEach(() => { + cy.auth(user.id).then(() => cy.visit('/')); + }); + + it('should redirect to previously viewed page 1', () => { + cy.visit('/spectrum'); + setUsernameIsVisible(); + saveUsername(); + cy.url().should('include', '/spectrum'); + cy.get('[data-cy="community-profile-card"]').should('be.visible'); + }); + + it('should redirect to previously viewed page 2', () => { + cy.visit('/spectrum/general'); + setUsernameIsVisible(); + saveUsername(); + cy.url().should('include', '/spectrum/general'); + cy.get('[data-cy="channel-profile-card"]').should('be.visible'); + }); + + it('should redirect to previously viewed page 3', () => { + cy.visit('/thread/thread-1'); + setUsernameIsVisible(); + saveUsername(); + cy.url().should('include', '/thread/thread-1'); + cy.get('[data-cy="community-profile-card"]').should('be.visible'); + }); + + it('should redirect to previously viewed page 4', () => { + cy.visit('/me'); + setUsernameIsVisible(); + saveUsername(); + cy.url().should('include', '/users/new-user'); + cy.get('[data-cy="user-view"]').should('be.visible'); + }); + + it('should persist community join token', () => { + cy.visit('/private-join/join/abc'); + setUsernameIsVisible(); + saveUsername(); + cy.url().should('include', '/private-join'); + cy.get('[data-cy="community-profile-card"]').should('be.visible'); + }); + + it('should persist channel join token', () => { + cy.visit('/payments/private/join/abc'); + setUsernameIsVisible(); + saveUsername(); + cy.url().should('include', '/payments/private'); + cy.get('[data-cy="channel-profile-card"]').should('be.visible'); + }); + + it('should take user to explore if no previously viewed page', () => { + setUsernameIsVisible(); + saveUsername(); + cy.get('[data-cy="explore-page"]').should('be.visible'); + }); +}); diff --git a/cypress/integration/user_spec.js b/cypress/integration/user_spec.js index 47c5b44983..72ddf9ed28 100644 --- a/cypress/integration/user_spec.js +++ b/cypress/integration/user_spec.js @@ -1,6 +1,17 @@ import data from '../../shared/testing/data'; - const user = data.users[0]; +const communities = data.communities; +const channels = data.channels; +const threads = data.threads; +const publicAuthoredThreads = threads.filter(thread => { + const community = communities.find( + community => community.id === thread.communityId + ); + const channel = channels.find(channel => channel.id === thread.channelId); + return ( + thread.creatorId === user.id && !community.isPrivate && !channel.isPrivate + ); +}); describe('User View', () => { beforeEach(() => { @@ -14,11 +25,9 @@ describe('User View', () => { cy.contains(user.description); cy.contains(user.website); cy.get('[data-cy="thread-feed"]').should('be.visible'); - data.threads - .filter(thread => thread.creatorId === user.id) - .forEach(thread => { - cy.contains(thread.content.title); - }); + publicAuthoredThreads.forEach(thread => { + cy.contains(thread.content.title); + }); }); it('should list the public communities a user is a member of, including their rep in that community', () => { @@ -34,7 +43,6 @@ describe('User View', () => { const userCommunity = usersCommunities.find( ({ communityId }) => communityId === community.id ); - cy.contains(userCommunity.reputation); }); }); }); diff --git a/desktop/src/config.js b/desktop/src/config.js index 67c6ba2521..484b1dd335 100644 --- a/desktop/src/config.js +++ b/desktop/src/config.js @@ -22,8 +22,8 @@ module.exports = { WINDOW_DEFAULT_HEIGHT: 800, WINDOW_DEFAULT_WIDTH: 1300, WINDOW_MIN_HEIGHT: 500, - WINDOW_MIN_WIDTH: 770, - WINDOW_BG_COLOR: '#F5F8FC', + WINDOW_MIN_WIDTH: 320, + WINDOW_BG_COLOR: '#FAFAFA', ICON: resolve(__dirname, '../resources/icons/png/icon-512x512.png'), }; diff --git a/flow-typed/npm/@tippy.js/react_vx.x.x.js b/flow-typed/npm/@tippy.js/react_vx.x.x.js new file mode 100644 index 0000000000..bee00098bd --- /dev/null +++ b/flow-typed/npm/@tippy.js/react_vx.x.x.js @@ -0,0 +1,53 @@ +// flow-typed signature: d5a9ac3d3049496487995f00a345e5c8 +// flow-typed version: <>/@tippy.js/react_v2.1.1/flow_v0.66.0 + +/** + * This is an autogenerated libdef stub for: + * + * '@tippy.js/react' + * + * 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 '@tippy.js/react' { + 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 '@tippy.js/react/esm/index' { + declare module.exports: any; +} + +declare module '@tippy.js/react/esm/index.min' { + declare module.exports: any; +} + +declare module '@tippy.js/react/umd/index' { + declare module.exports: any; +} + +declare module '@tippy.js/react/umd/index.min' { + declare module.exports: any; +} + +// Filename aliases +declare module '@tippy.js/react/esm/index.js' { + declare module.exports: $Exports<'@tippy.js/react/esm/index'>; +} +declare module '@tippy.js/react/esm/index.min.js' { + declare module.exports: $Exports<'@tippy.js/react/esm/index.min'>; +} +declare module '@tippy.js/react/umd/index.js' { + declare module.exports: $Exports<'@tippy.js/react/umd/index'>; +} +declare module '@tippy.js/react/umd/index.min.js' { + declare module.exports: $Exports<'@tippy.js/react/umd/index.min'>; +} diff --git a/flow-typed/npm/apollo-server-cache-redis_vx.x.x.js b/flow-typed/npm/apollo-server-cache-redis_vx.x.x.js new file mode 100644 index 0000000000..1dbc564e93 --- /dev/null +++ b/flow-typed/npm/apollo-server-cache-redis_vx.x.x.js @@ -0,0 +1,32 @@ +// flow-typed signature: 2579e61c0f1bdea4f82d3a0ff4c70242 +// flow-typed version: <>/apollo-server-cache-redis_vx.x.x/flow_v0.66.0 + +/** + * This is an autogenerated libdef stub for: + * + * 'apollo-server-cache-redis' + * + * 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 'apollo-server-cache-redis' { + 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 'apollo-server-cache-redis/dist/index' { + declare module.exports: any; +} + +// Filename aliases +declare module 'apollo-server-cache-redis/dist/index.js' { + declare module.exports: $Exports<'apollo-server-cache-redis/dist/index'>; +} diff --git a/flow-typed/npm/apollo-server-plugin-response-cache_vx.x.x.js b/flow-typed/npm/apollo-server-plugin-response-cache_vx.x.x.js new file mode 100644 index 0000000000..de534e2240 --- /dev/null +++ b/flow-typed/npm/apollo-server-plugin-response-cache_vx.x.x.js @@ -0,0 +1,39 @@ +// flow-typed signature: 24e78321d1ce97826d55241446c65e89 +// flow-typed version: <>/apollo-server-plugin-response-cache_vx.x.x/flow_v0.66.0 + +/** + * This is an autogenerated libdef stub for: + * + * 'apollo-server-plugin-response-cache' + * + * 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 'apollo-server-plugin-response-cache' { + 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 'apollo-server-plugin-response-cache/dist/ApolloServerPluginResponseCache' { + declare module.exports: any; +} + +declare module 'apollo-server-plugin-response-cache/dist/index' { + declare module.exports: any; +} + +// Filename aliases +declare module 'apollo-server-plugin-response-cache/dist/ApolloServerPluginResponseCache.js' { + declare module.exports: $Exports<'apollo-server-plugin-response-cache/dist/ApolloServerPluginResponseCache'>; +} +declare module 'apollo-server-plugin-response-cache/dist/index.js' { + declare module.exports: $Exports<'apollo-server-plugin-response-cache/dist/index'>; +} diff --git a/flow-typed/npm/react-infinite-scroller-fork-mxstbr_vx.x.x.js b/flow-typed/npm/react-infinite-scroller-fork-mxstbr_vx.x.x.js new file mode 100644 index 0000000000..71c975db81 --- /dev/null +++ b/flow-typed/npm/react-infinite-scroller-fork-mxstbr_vx.x.x.js @@ -0,0 +1,38 @@ +// flow-typed signature: ce15133e6fc07b478594da7d33ada1a8 +// flow-typed version: <>/react-infinite-scroller-fork-mxstbr_v1.2.7/flow_v0.66.0 + +/** + * This is an autogenerated libdef stub for: + * + * 'react-infinite-scroller-fork-mxstbr' + * + * 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 'react-infinite-scroller-fork-mxstbr' { + 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 'react-infinite-scroller-fork-mxstbr/dist/InfiniteScroll' { + declare module.exports: any; +} + +// Filename aliases +declare module 'react-infinite-scroller-fork-mxstbr/dist/InfiniteScroll.js' { + declare module.exports: $Exports<'react-infinite-scroller-fork-mxstbr/dist/InfiniteScroll'>; +} +declare module 'react-infinite-scroller-fork-mxstbr/index' { + declare module.exports: $Exports<'react-infinite-scroller-fork-mxstbr'>; +} +declare module 'react-infinite-scroller-fork-mxstbr/index.js' { + declare module.exports: $Exports<'react-infinite-scroller-fork-mxstbr'>; +} diff --git a/flow-typed/npm/react_v16.8.0.js b/flow-typed/npm/react_v16.8.0.js new file mode 100644 index 0000000000..a6a7de632e --- /dev/null +++ b/flow-typed/npm/react_v16.8.0.js @@ -0,0 +1,457 @@ +// flow-typed signature: 9d52bd849b0802b1d5cbd55e71c61057 +// flow-typed version: <>/react_v16.8.0/flow_v0.66.0 + +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of the facebook/flow source tree. + */ + +// Copied from https://github.com/facebook/flow/edit/master/lib/react.js + +/** + * A UI node that can be rendered by React. React can render most primitives in + * addition to elements and arrays of nodes. + */ + +declare type React$Node = + | null + | boolean + | number + | string + | React$Element + | React$Portal + | Iterable; + +/** + * Base class of ES6 React classes, modeled as a polymorphic class whose main + * type parameters are Props and State. + */ +declare class React$Component { + // fields + + props: Props; + state: State; + + // action methods + + setState( + partialState: ?$Shape | ((State, Props) => ?$Shape), + callback?: () => mixed, + ): void; + + forceUpdate(callback?: () => void): void; + + // lifecycle methods + + constructor(props?: Props, context?: any): void; + render(): React$Node; + componentWillMount(): mixed; + UNSAFE_componentWillMount(): mixed; + componentDidMount(): mixed; + componentWillReceiveProps( + nextProps: Props, + nextContext: any, + ): mixed; + UNSAFE_componentWillReceiveProps( + nextProps: Props, + nextContext: any, + ): mixed; + shouldComponentUpdate( + nextProps: Props, + nextState: State, + nextContext: any, + ): boolean; + componentWillUpdate( + nextProps: Props, + nextState: State, + nextContext: any, + ): mixed; + UNSAFE_componentWillUpdate( + nextProps: Props, + nextState: State, + nextContext: any, + ): mixed; + componentDidUpdate( + prevProps: Props, + prevState: State, + prevContext: any, + ): mixed; + componentWillUnmount(): mixed; + componentDidCatch( + error: Error, + info: { + componentStack: string, + } + ): mixed; + + // long tail of other stuff not modeled very well + + refs: any; + context: any; + getChildContext(): any; + static displayName?: ?string; + static childContextTypes: any; + static contextTypes: any; + static propTypes: any; + + // We don't add a type for `defaultProps` so that its type may be entirely + // inferred when we diff the type for `defaultProps` with `Props`. Otherwise + // the user would need to define a type (which would be redundant) to override + // the type we provide here in the base class. + // + // static defaultProps: $Shape; +} + +declare class React$PureComponent + extends React$Component { + // TODO: Due to bugs in Flow's handling of React.createClass, some fields + // already declared in the base class need to be redeclared below. Ideally + // they should simply be inherited. + + props: Props; + state: State; +} + +/** + * Base class of legacy React classes, which extends the base class of ES6 React + * classes and supports additional methods. + */ +declare class LegacyReactComponent + extends React$Component { + // additional methods + + replaceState(state: State, callback?: () => void): void; + + isMounted(): bool; + + // TODO: Due to bugs in Flow's handling of React.createClass, some fields + // already declared in the base class need to be redeclared below. Ideally + // they should simply be inherited. + + props: Props; + state: State; +} + +declare type React$AbstractComponentStatics = { + displayName?: ?string, + // This is only on function components, but trying to access name when + // displayName is undefined is a common pattern. + name?: ?string, +}; + +/** + * The type of a stateless functional component. In most cases these components + * are a single function. However, they may have some static properties that we + * can type check. + */ +declare type React$StatelessFunctionalComponent = { + (props: Props, context: any): React$Node, + displayName?: ?string, + propTypes?: any, + contextTypes?: any +}; + +/** + * The type of a component in React. A React component may be a: + * + * - Stateless functional components. Functions that take in props as an + * argument and return a React node. + * - ES6 class component. Components with state defined either using the ES6 + * class syntax, or with the legacy `React.createClass()` helper. + */ +// $FlowIssue +declare type React$ComponentType<-Config> = React$AbstractComponent; + +/** + * The type of an element in React. A React element may be a: + * + * - String. These elements are intrinsics that depend on the React renderer + * implementation. + * - React component. See `ComponentType` for more information about its + * different variants. + */ +declare type React$ElementType = + | string + | React$AbstractComponent; + +/** + * Type of a React element. React elements are commonly created using JSX + * literals, which desugar to React.createElement calls (see below). + */ +declare type React$Element<+ElementType: React$ElementType> = {| + +type: ElementType, + +props: React$ElementProps, + +key: React$Key | null, + +ref: any, +|}; + +/** + * The type of the key that React uses to determine where items in a new list + * have moved. + */ +declare type React$Key = string | number; + +/** + * The type of the ref prop available on all React components. + */ +declare type React$Ref = + | {-current: React$ElementRef | null} + | ((React$ElementRef | null) => mixed) + | string; + +/** + * The type of a React Context. React Contexts are created by calling + * createContext() with a default value. + */ +declare type React$Context = { + Provider: React$ComponentType<{ value: T, children?: ?React$Node }>, + Consumer: React$ComponentType<{ children: (value: T) => ?React$Node }>, + + // Optional, user-specified value for custom display label in React DevTools. + displayName?: string, +} + +/** + * A React portal node. The implementation of the portal node is hidden to React + * users so we use an opaque type. + */ +declare opaque type React$Portal; + +declare module react { + declare export var DOM: any; + declare export var PropTypes: ReactPropTypes; + declare export var version: string; + + declare export function checkPropTypes( + propTypes : any, + values: V, + location: string, + componentName: string, + getStack: ?(() => ?string) + ) : void; + + declare export var createClass: React$CreateClass; + declare export function createContext( + defaultValue: T, + calculateChangedBits: ?(a: T, b: T) => number, + ): React$Context; + declare export var createElement: React$CreateElement; + declare export var cloneElement: React$CloneElement; + declare export function createFactory( + type: ElementType, + ): React$ElementFactory; + declare export function createRef( + ): {|current: null | T|}; + + declare export function isValidElement(element: any): boolean; + + declare export var Component: typeof React$Component; + declare export var PureComponent: typeof React$PureComponent; + declare export type StatelessFunctionalComponent

= + React$StatelessFunctionalComponent

; + declare export type ComponentType<-P> = React$ComponentType

; + declare export type AbstractComponent< + -Config, + +Instance = mixed, + > = React$AbstractComponent; + declare export type ElementType = React$ElementType; + declare export type Element<+C> = React$Element; + declare export var Fragment: ({children: ?React$Node}) => React$Node; + declare export type Key = React$Key; + declare export type Ref = React$Ref; + declare export type Node = React$Node; + declare export type Context = React$Context; + declare export type Portal = React$Portal; + declare export var ConcurrentMode: ({children: ?React$Node}) => React$Node; // 16.7+ + declare export var StrictMode: ({children: ?React$Node}) => React$Node; + + declare export var Suspense: React$ComponentType<{ + children?: ?React$Node, + fallback?: React$Node, + maxDuration?: number + }>; // 16.6+ + + declare export type ElementProps = React$ElementProps; + declare export type ElementConfig = React$ElementConfig; + declare export type ElementRef = React$ElementRef; + // $FlowIssue + declare export type Config = React$Config; + + declare export type ChildrenArray<+T> = $ReadOnlyArray> | T; + declare export var Children: { + map( + children: ChildrenArray, + fn: (child: $NonMaybeType, index: number) => U, + thisArg?: mixed, + ): Array<$NonMaybeType>; + forEach( + children: ChildrenArray, + fn: (child: T, index: number) => mixed, + thisArg?: mixed, + ): void; + count(children: ChildrenArray): number; + only(children: ChildrenArray): $NonMaybeType; + toArray(children: ChildrenArray): Array<$NonMaybeType>; + }; + + declare export function forwardRef( + render: ( + props: Config, + ref: {current: null | Instance} | ((null | Instance) => mixed), + ) => React$Node, + ): React$AbstractComponent; + + declare export function memo

( + component: React$ComponentType

, + equal?: (P, P) => boolean, + ): React$ComponentType

; + + declare export function lazy

( + component: () => Promise<{ default: React$ComponentType

}>, + ): React$ComponentType

; + + declare type MaybeCleanUpFn = void | (() => void); + + declare export function useContext( + context: React$Context, + observedBits: void | number | boolean, + ): T; + + declare export function useState( + initialState: (() => S) | S, + ): [S, ((S => S) | S) => void]; + + declare type Dispatch = (A) => void; + + declare export function useReducer( + reducer: (S, A) => S, + initialState: S, + ): [S, Dispatch]; + + declare export function useReducer( + reducer: (S, A) => S, + initialState: S, + init: void, + ): [S, Dispatch]; + + declare export function useReducer( + reducer: (S, A) => S, + initialArg: I, + init: (I) => S, + ): [S, Dispatch]; + + declare export function useRef(initialValue: T): {|current: T|}; + + declare export function useDebugValue(value: any): void; + + declare export function useEffect( + create: () => MaybeCleanUpFn, + inputs: ?$ReadOnlyArray, + ): void; + + declare export function useLayoutEffect( + create: () => MaybeCleanUpFn, + inputs: ?$ReadOnlyArray, + ): void; + + declare export function useCallback) => mixed>( + callback: T, + inputs: ?$ReadOnlyArray, + ): T; + + declare export function useMemo( + create: () => T, + inputs: ?$ReadOnlyArray, + ): T; + + declare export function useImperativeHandle( + ref: {current: T | null} | ((inst: T | null) => mixed) | null | void, + create: () => T, + inputs: ?$ReadOnlyArray, + ): void; + + declare export default {| + +DOM: typeof DOM, + +PropTypes: typeof PropTypes, + +version: typeof version, + +checkPropTypes: typeof checkPropTypes, + +memo: typeof memo, + +lazy: typeof lazy, + +createClass: typeof createClass, + +createContext: typeof createContext, + +createElement: typeof createElement, + +cloneElement: typeof cloneElement, + +createFactory: typeof createFactory, + +createRef: typeof createRef, + +forwardRef: typeof forwardRef, + +isValidElement: typeof isValidElement, + +Component: typeof Component, + +PureComponent: typeof PureComponent, + +Fragment: typeof Fragment, + +Children: typeof Children, + +ConcurrentMode: typeof ConcurrentMode, + +StrictMode: typeof StrictMode, + +Suspense: typeof Suspense, + +useContext: typeof useContext, + +useState: typeof useState, + +useReducer: typeof useReducer, + +useRef: typeof useRef, + +useEffect: typeof useEffect, + +useLayoutEffect: typeof useLayoutEffect, + +useCallback: typeof useCallback, + +useMemo: typeof useMemo, + +useImperativeHandle: typeof useImperativeHandle, + |}; +} + +// TODO Delete this once https://github.com/facebook/react/pull/3031 lands +// and "react" becomes the standard name for this module +declare module React { + declare module.exports: $Exports<'react'>; +} + +type ReactPropsCheckType = ( + props: any, + propName: string, + componentName: string, + href?: string) => ?Error; + +type ReactPropsChainableTypeChecker = { + isRequired: ReactPropsCheckType; + (props: any, propName: string, componentName: string, href?: string): ?Error; +}; + +type React$PropTypes$arrayOf = + (typeChecker: ReactPropsCheckType) => ReactPropsChainableTypeChecker; +type React$PropTypes$instanceOf = + (expectedClass: any) => ReactPropsChainableTypeChecker; +type React$PropTypes$objectOf = + (typeChecker: ReactPropsCheckType) => ReactPropsChainableTypeChecker; +type React$PropTypes$oneOf = + (expectedValues: Array) => ReactPropsChainableTypeChecker; +type React$PropTypes$oneOfType = + (arrayOfTypeCheckers: Array) => + ReactPropsChainableTypeChecker; +type React$PropTypes$shape = + (shapeTypes: { [key: string]: ReactPropsCheckType }) => + ReactPropsChainableTypeChecker; + +type ReactPropTypes = { + array: React$PropType$Primitive>; + bool: React$PropType$Primitive; + func: React$PropType$Primitive; + number: React$PropType$Primitive; + object: React$PropType$Primitive; + string: React$PropType$Primitive; + any: React$PropType$Primitive; + arrayOf: React$PropType$ArrayOf; + element: React$PropType$Primitive; /* TODO */ + instanceOf: React$PropType$InstanceOf; + node: React$PropType$Primitive; /* TODO */ + objectOf: React$PropType$ObjectOf; + oneOf: React$PropType$OneOf; + oneOfType: React$PropType$OneOfType; + shape: React$PropType$Shape; +} diff --git a/hermes/package.json b/hermes/package.json index 9f9e91b79c..7382d08a90 100644 --- a/hermes/package.json +++ b/hermes/package.json @@ -4,7 +4,7 @@ }, "dependencies": { "@sendgrid/mail": "^6.3.1", - "aws-sdk": "^2.409.0", + "aws-sdk": "^2.426.0", "bull": "3.3.10", "datadog-metrics": "^0.8.1", "debug": "^4.1.1", @@ -12,7 +12,7 @@ "escape-html": "^1.0.3", "faker": "^4.1.0", "ioredis": "3.2.2", - "jsonwebtoken": "^8.5.0", + "jsonwebtoken": "^8.5.1", "lodash.intersection": "^4.4.0", "node-env-file": "^0.1.8", "now-env": "^3.1.0", diff --git a/hermes/yarn.lock b/hermes/yarn.lock index 0946bbe740..977da5fba5 100644 --- a/hermes/yarn.lock +++ b/hermes/yarn.lock @@ -98,10 +98,10 @@ asynckit@^0.4.0: resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" integrity sha1-x57Zf380y48robyXkLzDZkdLS3k= -aws-sdk@^2.409.0: - version "2.409.0" - resolved "https://registry.yarnpkg.com/aws-sdk/-/aws-sdk-2.409.0.tgz#d017060ba9e005487c68dc34a592af74d916f295" - integrity sha512-QV6j9zBQq/Kz8BqVOrJ03ABjMKtErXdUT1YdYEljoLQZimpzt0ZdQwJAsoZIsxxriOJgrqeZsQUklv9AFQaldQ== +aws-sdk@^2.426.0: + version "2.426.0" + resolved "https://registry.yarnpkg.com/aws-sdk/-/aws-sdk-2.426.0.tgz#cf17361c987daf518f945218f06135fbc1a3690d" + integrity sha512-S4nmIhF/6iYeVEmKUWVG03zo1sw3zELoAPGqBKIZ3isrXbxkFXdP2cgIQxqi37zwWXSqaxt0xjeXVOMLzN6vSg== dependencies: buffer "4.9.1" events "1.1.1" @@ -624,12 +624,12 @@ json-stringify-safe@~5.0.1: resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" integrity sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus= -jsonwebtoken@^8.5.0: - version "8.5.0" - resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-8.5.0.tgz#ebd0ca2a69797816e1c5af65b6c759787252947e" - integrity sha512-IqEycp0znWHNA11TpYi77bVgyBO/pGESDh7Ajhas+u0ttkGkKYIIAjniL4Bw5+oVejVF+SYkaI7XKfwCCyeTuA== +jsonwebtoken@^8.5.1: + version "8.5.1" + resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz#00e71e0b8df54c2121a1f26137df2280673bcc0d" + integrity sha512-XjwVfRS6jTMsqYs0EsuJ4LGxXV14zQybNd4L2r0UvbVnSF9Af8x7p5MzbJ90Ioz/9TI41/hTCvznF/loiSzn8w== dependencies: - jws "^3.2.1" + jws "^3.2.2" lodash.includes "^4.3.0" lodash.isboolean "^3.0.3" lodash.isinteger "^4.0.4" @@ -650,21 +650,21 @@ jsprim@^1.2.2: json-schema "0.2.3" verror "1.10.0" -jwa@^1.2.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/jwa/-/jwa-1.3.0.tgz#061a7c3bb8ab2b3434bb2f432005a8bb7fca0efa" - integrity sha512-SxObIyzv9a6MYuZYaSN6DhSm9j3+qkokwvCB0/OTSV5ylPq1wUQiygZQcHT5Qlux0I5kmISx3J86TxKhuefItg== +jwa@^1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/jwa/-/jwa-1.4.1.tgz#743c32985cb9e98655530d53641b66c8645b039a" + integrity sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA== dependencies: buffer-equal-constant-time "1.0.1" ecdsa-sig-formatter "1.0.11" safe-buffer "^5.0.1" -jws@^3.2.1: - version "3.2.1" - resolved "https://registry.yarnpkg.com/jws/-/jws-3.2.1.tgz#d79d4216a62c9afa0a3d5e8b5356d75abdeb2be5" - integrity sha512-bGA2omSrFUkd72dhh05bIAN832znP4wOU3lfuXtRBuGTbsmNmDXMQg28f0Vsxaxgk4myF5YkKQpz6qeRpMgX9g== +jws@^3.2.2: + version "3.2.2" + resolved "https://registry.yarnpkg.com/jws/-/jws-3.2.2.tgz#001099f3639468c9414000e99995fa52fb478304" + integrity sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA== dependencies: - jwa "^1.2.0" + jwa "^1.4.1" safe-buffer "^5.0.1" lodash.assign@^4.2.0: diff --git a/hyperion/renderer/index.js b/hyperion/renderer/index.js index 7661251137..25a2b24a58 100644 --- a/hyperion/renderer/index.js +++ b/hyperion/renderer/index.js @@ -86,15 +86,7 @@ const renderer = (req: express$Request, res: express$Response) => { // Define the initial redux state const { t } = req.query; - const initialReduxState = { - dashboardFeed: { - activeThread: t ? t : '', - mountedWithActiveThread: t ? t : '', - search: { - isOpen: false, - }, - }, - }; + const initialReduxState = {}; // Create the Redux store const store = initStore(initialReduxState); let modules = []; @@ -190,7 +182,7 @@ const renderer = (req: express$Request, res: express$Response) => { }) .catch(err => { // Avoid memory leaks, see https://github.com/styled-components/styled-components/issues/1624#issuecomment-425382979 - sheet.complete(); + sheet.seal(); console.error(err); const sentryId = process.env.NODE_ENV === 'production' diff --git a/package.json b/package.json index 45327fad6b..cc490fe031 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "Spectrum", - "version": "2.8.5", + "version": "3.0.0", "license": "BSD-3-Clause", "devDependencies": { "@babel/preset-flow": "^7.0.0", @@ -30,7 +30,7 @@ "eslint-plugin-jest": "^21.25.1", "eslint-plugin-jsx-a11y": "^5.1.1", "eslint-plugin-promise": "^3.8.0", - "eslint-plugin-react": "^7.11.1", + "eslint-plugin-react": "^7.12.4", "flow-bin": "0.66", "forever": "^0.15.3", "is-html": "^1.1.0", @@ -39,7 +39,7 @@ "prettier": "^1.14.3", "raw-loader": "^0.5.1", "react-app-rewire-hot-loader": "^1.0.3", - "react-hot-loader": "^4.6.0", + "react-hot-loader": "^4.8.0", "react-scripts": "^1.1.5", "rimraf": "^2.6.1", "sw-precache-webpack-plugin": "^0.11.4", @@ -51,6 +51,7 @@ "dependencies": { "@cypress/browserify-preprocessor": "^1.1.2", "@sendgrid/mail": "^6.3.1", + "@tippy.js/react": "^2.1.1", "algoliasearch": "^3.30.0", "amplitude": "^3.5.0", "amplitude-js": "^4.4.0", @@ -61,7 +62,9 @@ "apollo-link-retry": "^2.2.6", "apollo-link-schema": "^1.1.2", "apollo-link-ws": "^1.0.10", - "apollo-server-express": "^2.2.6", + "apollo-server-cache-redis": "^0.3.1", + "apollo-server-express": "2.5.0-alpha.0", + "apollo-server-plugin-response-cache": "^0.1.0-alpha.0", "apollo-upload-client": "^9.1.0", "apollo-upload-server": "^7.1.0", "apollo-utilities": "^1.0.26", @@ -78,7 +81,7 @@ "cors": "^2.8.3", "cryptr": "^3.0.0", "css.escape": "^1.5.1", - "cypress": "3.1.5", + "cypress": "^3.2.0", "datadog-metrics": "^0.8.1", "dataloader": "^1.4.0", "debounce": "^1.2.0", @@ -153,22 +156,24 @@ "raf": "^3.4.0", "ratelimiter": "^3.2.0", "raven": "^2.6.4", - "react": "^16.7.0-alpha.2", + "react": "^16.8.4", "react-apollo": "^2.5.1", "react-app-rewire-styled-components": "^3.0.0", "react-app-rewired": "^1.6.2", "react-async-hook": "^1.0.0", "react-clipboard.js": "^2.0.1", - "react-dom": "^16.7.0-alpha.2", + "react-dom": "npm:@hot-loader/react-dom", "react-dropzone": "^8.0.3", "react-flip-move": "^3.0.2", - "react-helmet-async": "^0.1.0", + "react-helmet-async": "^0.2.0", "react-image": "^1.5.1", + "react-infinite-scroller": "^1.2.4", + "react-infinite-scroller-fork-mxstbr": "^1.2.7", "react-infinite-scroller-with-scroll-element": "2.0.2", "react-loadable": "^5.5.0", "react-mentions": "^2.4.1", "react-modal": "^3.7.1", - "react-popper": "^1.0.2", + "react-popper": "^1.3.3", "react-redux": "^5.0.2", "react-router": "^4.0.0-beta.7", "react-router-dom": "^4.3.1", @@ -199,7 +204,7 @@ "string-replace-to-array": "^1.0.3", "string-similarity": "^2.0.0", "striptags": "2.x", - "styled-components": "^3.4.10", + "styled-components": "^4.1.3", "subscriptions-transport-ws": "^0.9.15", "textversionjs": "^1.1.3", "then-queue": "^1.3.0", diff --git a/shared/algolia/index.js b/shared/algolia/index.js index 69a93abf4f..a33d5ab96b 100644 --- a/shared/algolia/index.js +++ b/shared/algolia/index.js @@ -5,6 +5,6 @@ var ALGOLIA_API_SECRET = process.env.ALGOLIA_API_SECRET; var algoliasearch = require('algoliasearch'); var algolia = algoliasearch(ALGOLIA_APP_ID, ALGOLIA_API_SECRET); var initIndex = function(index) { - return algolia.initIndex(IS_PROD ? index : 'dev_' + index); + return algolia.initIndex(IS_PROD ? index : index); }; module.exports = initIndex; diff --git a/shared/cache/redis.js b/shared/cache/redis.js index b1fd4fcc96..5a879001d6 100644 --- a/shared/cache/redis.js +++ b/shared/cache/redis.js @@ -1,7 +1,7 @@ // @flow import Redis from 'ioredis'; -const config = +export const config = process.env.NODE_ENV === 'production' && !process.env.FORCE_DEV ? { port: process.env.REDIS_CACHE_PORT, diff --git a/shared/clients/draft-js/links-decorator/core.js b/shared/clients/draft-js/links-decorator/core.js index 8c43daa584..e8d8a7b827 100644 --- a/shared/clients/draft-js/links-decorator/core.js +++ b/shared/clients/draft-js/links-decorator/core.js @@ -38,7 +38,15 @@ const createLinksDecorator = ( ); }, callback); } - linkStrategy(contentBlock, callback); + linkStrategy(contentBlock, (start, end) => { + if ( + contentBlock.entityRanges.find( + range => range.offset === start && range.length === end - start + ) + ) + return; + callback(start, end); + }); }, component: ({ decoratedText, diff --git a/shared/clients/draft-js/links-decorator/index.js b/shared/clients/draft-js/links-decorator/index.js index 048aa2c482..a538812e97 100644 --- a/shared/clients/draft-js/links-decorator/index.js +++ b/shared/clients/draft-js/links-decorator/index.js @@ -5,7 +5,7 @@ import createLinksDecorator, { } from './core'; export default createLinksDecorator((props: LinksDecoratorComponentProps) => ( - + {props.children} )); diff --git a/shared/clients/draft-js/mentions-decorator/core.js b/shared/clients/draft-js/mentions-decorator/core.js index 79733e9c96..efc05aaa0a 100644 --- a/shared/clients/draft-js/mentions-decorator/core.js +++ b/shared/clients/draft-js/mentions-decorator/core.js @@ -64,10 +64,11 @@ const createMentionsDecorator = ( component: (props: { decoratedText: string, children: Node }) => ( + > + {props.children} + ), }); diff --git a/shared/clients/draft-js/renderer/index.js b/shared/clients/draft-js/renderer/index.js index fb86faa7ba..0b2a2bd399 100644 --- a/shared/clients/draft-js/renderer/index.js +++ b/shared/clients/draft-js/renderer/index.js @@ -7,7 +7,7 @@ import { EmbedContainer, EmbedComponent, } from 'src/components/rich-text-editor/style'; -import ThreadAttachment from 'src/components/message/ThreadAttachment'; +import ThreadAttachment from 'src/components/message/threadAttachment'; import { getStringElements } from '../utils/getStringElements'; import { hasStringElements } from '../utils/hasStringElements'; import mentionsDecorator from '../mentions-decorator'; @@ -63,7 +63,7 @@ const InternalEmbed = (props: InternalEmbedData) => { }; const Embed = (props: EmbedData) => { - if (typeof props.type === 'string' && props.type === 'internal') { + if (props.type === 'internal') { return ; } @@ -112,9 +112,9 @@ export const createRenderer = (options: Options) => { {({ className, style, tokens, getLineProps, getTokenProps }) => ( {tokens.map((line, i) => ( -
+
{line.map((token, key) => ( - + ))}
))} @@ -160,7 +160,12 @@ export const createRenderer = (options: Options) => { }, entities: { LINK: (children: Array, data: DataObj, { key }: KeyObj) => ( - + {children} ), @@ -168,7 +173,7 @@ export const createRenderer = (options: Options) => { children: Array, data: { src?: string, alt?: string }, { key }: KeyObj - ) => {data.alt}, + ) => {data.alt, embed: (children: Array, data: Object, { key }: KeyObj) => ( ), diff --git a/shared/clients/draft-js/utils/getSnippet.js b/shared/clients/draft-js/utils/getSnippet.js new file mode 100644 index 0000000000..67666681e0 --- /dev/null +++ b/shared/clients/draft-js/utils/getSnippet.js @@ -0,0 +1,49 @@ +// @flow +import truncate from 'shared/truncate'; +import type { RawContentState } from 'draft-js'; + +const nthIndexOf = (string, pattern, n) => { + var i = -1; + + while (n-- && i++ < string.length) { + i = string.indexOf(pattern, i); + if (i < 0) break; + } + + return i; +}; + +export default (state: RawContentState): string => { + const textBlocks = state.blocks.filter( + ({ type }) => + type === 'unstyled' || + type.indexOf('header') === 0 || + type.indexOf('list') > -1 + ); + const text = textBlocks + .map((block, index) => { + switch (block.type) { + case 'unordered-list-item': + return `• ${block.text}`; + case 'ordered-list-item': { + const number = textBlocks.reduce((number, b, i) => { + if (i >= index) return number; + if (b.type !== 'ordered-list-item') return number; + return number + 1; + }, 1); + return `${number}. ${block.text}`; + } + default: + return block.text; + } + }) + .join('\n') + // Replace multiple line breaks with a single one + .replace(/[\r\n]+/g, '\n'); + const indexOfThirdLineBreak = nthIndexOf(text, '\n', 3); + const cut = text.substr( + 0, + indexOfThirdLineBreak > -1 ? indexOfThirdLineBreak : text.length + ); + return truncate(cut !== text ? `${cut} …` : cut, 280); +}; diff --git a/shared/clients/group-messages.js b/shared/clients/group-messages.js index 0e5da2dcc9..6b6bd46a07 100644 --- a/shared/clients/group-messages.js +++ b/shared/clients/group-messages.js @@ -13,6 +13,7 @@ export const sortAndGroupMessages = (messages: Array) => { let masterArray = []; let newArray = []; let checkId; + let checkBot; for (let i = 0; i < messages.length; i++) { // on the first message, get the user id and set it to be checked against @@ -34,12 +35,14 @@ export const sortAndGroupMessages = (messages: Array) => { if (i === 0) { checkId = messages[i].author.user.id; + checkBot = Boolean(messages[i].bot); masterArray.push(robo); } const sameUser = messages[i].author.user.id !== 'robo' && - messages[i].author.user.id === checkId; //=> boolean + messages[i].author.user.id === checkId && + Boolean(messages[i].bot) === checkBot; //=> boolean const oldMessage = (current: Object, previous: Object) => { //=> boolean /* @@ -59,7 +62,7 @@ export const sortAndGroupMessages = (messages: Array) => { */ const c = new Date(current.timestamp).getTime(); const p = new Date(previous.timestamp).getTime(); - return c > p + 3600000; // one hour; + return c > p + 3600000 * 6; // six hours; }; // if we are evaulating a bubble from the same user @@ -88,6 +91,7 @@ export const sortAndGroupMessages = (messages: Array) => { } // and maintain the checkid checkId = messages[i].author.user.id; + checkBot = Boolean(messages[i].bot); // if the next message is from a new user } else { // we push the previous user's messages to the masterarray @@ -107,6 +111,7 @@ export const sortAndGroupMessages = (messages: Array) => { // set a new checkid for the next user checkId = messages[i].author.user.id; + checkBot = Boolean(messages[i].bot); } } diff --git a/shared/draft-utils/add-embeds-to-draft-js.js b/shared/draft-utils/add-embeds-to-draft-js.js index a6f423a857..7dad71fa8a 100644 --- a/shared/draft-utils/add-embeds-to-draft-js.js +++ b/shared/draft-utils/add-embeds-to-draft-js.js @@ -2,16 +2,28 @@ import genKey from 'draft-js/lib/generateRandomKey'; import type { RawDraftContentState } from 'draft-js/lib/RawDraftContentState.js'; -const FIGMA_URLS = /\b((?:https?\/\/)?(?:www\.)?figma.com\/(file|proto)\/([0-9a-zA-Z]{22,128})(?:\/.*)?)/gi; -const YOUTUBE_URLS = /\b(?:\/\/)?(?:www\.)?youtu(?:be\.com\/watch\?v=|\.be\/)([\w\-\_]*)(&(amp;)?[\w\?=]*)?/gi; -const VIMEO_URLS = /\b(?:\/\/)?(?:www\.)?vimeo.com\/(?:channels\/[0-9a-z-_]+\/)?([0-9a-z\-_]+)/gi; +const FIGMA_URLS = /\b((?:https?:\/\/)?(?:www\.)?figma.com\/(file|proto)\/([0-9a-zA-Z]{22,128})(?:\/.*)?)/gi; +const YOUTUBE_URLS = /\b(?:https?:\/\/)?(?:www\.)?youtu(?:be\.com\/watch\?v=|\.be\/)([\w\-\_]*)(&(amp;)?[\w\?=]*)?/gi; +const VIMEO_URLS = /\b(?:https?:\/\/)?(?:www\.)?vimeo.com\/(?:channels\/[0-9a-z-_]+\/)?([0-9a-z\-_]+)/gi; const IFRAME_TAG = / { + input.blocks.forEach((block, blockIndex) => { newBlocks.push(block); if (block.type !== 'unstyled') return; @@ -49,22 +70,6 @@ export const addEmbedsToEditorState = ( embeds.forEach(embed => { lastEntityKey++; const entityKey = lastEntityKey; - newBlocks.push({ - type: 'atomic', - data: {}, - text: ' ', - depth: 0, - // TODO - entityRanges: [ - { - offset: 0, - length: 1, - key: entityKey, - }, - ], - inlineStyleRanges: [], - key: genKey(), - }); newEntityMap[entityKey] = { data: { ...embed, @@ -73,6 +78,23 @@ export const addEmbedsToEditorState = ( mutability: 'MUTABLE', type: 'embed', }; + const regexp = new RegExp(REGEXPS[embed.type], 'ig'); + const text = block.text; + var match; + while ((match = regexp.exec(text)) !== null) { + const offset = match.index; + const length = match[0].length; + newBlocks[blockIndex].entityRanges = newBlocks[ + blockIndex + ].entityRanges.filter( + entity => entity.offset !== offset || entity.length !== length + ); + newBlocks[blockIndex].entityRanges.push({ + offset, + length, + key: entityKey, + }); + } }); }); @@ -97,11 +119,12 @@ export const getEmbedsFromText = (text: string): Array => { let embeds = []; match(IFRAME_TAG, text).forEach(url => { - embeds.push({ url }); + embeds.push({ type: 'iframe', url }); }); match(FIGMA_URLS, text).forEach(url => { embeds.push({ + type: 'figma', url: `https://www.figma.com/embed?embed_host=spectrum&url=${url}`, aspectRatio: '56.25%', // 16:9 aspect ratio }); @@ -109,6 +132,7 @@ export const getEmbedsFromText = (text: string): Array => { match(YOUTUBE_URLS, text).forEach(id => { embeds.push({ + type: 'youtube', url: `https://www.youtube.com/embed/${id}`, aspectRatio: '56.25%', // 16:9 aspect ratio }); @@ -116,14 +140,16 @@ export const getEmbedsFromText = (text: string): Array => { match(VIMEO_URLS, text).forEach(id => { embeds.push({ + type: 'vimeo', url: `https://player.vimeo.com/video/${id}`, aspectRatio: '56.25%', // 16:9 aspect ratio }); }); - match(FRAMER_URLS, text).forEach(url => { + match(FRAMER_URLS, text).forEach(id => { embeds.push({ - url: `https://${url}`, + type: 'framer', + url: `https://share.framerjs.com/${id}`, width: 600, height: 800, }); @@ -131,6 +157,7 @@ export const getEmbedsFromText = (text: string): Array => { match(CODEPEN_URLS, text).forEach(path => { embeds.push({ + type: 'codepen', url: `https://codepen.io${path.replace(/(pen|full|details)/, 'embed')}`, height: 300, }); @@ -138,6 +165,7 @@ export const getEmbedsFromText = (text: string): Array => { match(CODESANDBOX_URLS, text).forEach(path => { embeds.push({ + type: 'codesandbox', url: `https://codesandbox.io${path.replace('/s/', '/embed/')}`, height: 500, }); @@ -145,6 +173,7 @@ export const getEmbedsFromText = (text: string): Array => { match(SIMPLECAST_URLS, text).forEach(path => { embeds.push({ + type: 'simplecast', url: `https://embed.simplecast.com/${path .replace('/s/', '') .replace('/', '')}`, diff --git a/shared/draft-utils/test/__snapshots__/add-embeds-to-draft-js.test.js.snap b/shared/draft-utils/test/__snapshots__/add-embeds-to-draft-js.test.js.snap index ac6667a8dc..db2a0586d6 100644 --- a/shared/draft-utils/test/__snapshots__/add-embeds-to-draft-js.test.js.snap +++ b/shared/draft-utils/test/__snapshots__/add-embeds-to-draft-js.test.js.snap @@ -6,33 +6,105 @@ Object { Object { "data": Object {}, "depth": 0, - "entityRanges": Array [], + "entityRanges": Array [ + Object { + "key": 0, + "length": 33, + "offset": 0, + }, + ], "inlineStyleRanges": Array [], "key": "g0000", "text": "https://simplecast.com/s/a1f11d11", "type": "unstyled", }, + ], + "entityMap": Object { + "0": Object { + "data": Object { + "height": 200, + "src": "https://embed.simplecast.com/a1f11d11", + "type": "simplecast", + "url": "https://embed.simplecast.com/a1f11d11", + }, + "mutability": "MUTABLE", + "type": "embed", + }, + }, +} +`; + +exports[`should add multiple embeds to text 1`] = ` +Object { + "blocks": Array [ Object { "data": Object {}, "depth": 0, "entityRanges": Array [ Object { "key": 0, - "length": 1, + "length": 33, + "offset": 13, + }, + Object { + "key": 0, + "length": 33, + "offset": 65, + }, + ], + "inlineStyleRanges": Array [], + "key": "g0000", + "text": "New podcast! https://simplecast.com/s/a1f11d11 it is really cool https://simplecast.com/s/a1f11d11", + "type": "unstyled", + }, + ], + "entityMap": Object { + "0": Object { + "data": Object { + "height": 200, + "src": "https://embed.simplecast.com/a1f11d11", + "type": "simplecast", + "url": "https://embed.simplecast.com/a1f11d11", + }, + "mutability": "MUTABLE", + "type": "embed", + }, + }, +} +`; + +exports[`should remove link entities 1`] = ` +Object { + "blocks": Array [ + Object { + "data": Object {}, + "depth": 0, + "entityRanges": Array [ + Object { + "key": 1, + "length": 33, "offset": 0, }, ], "inlineStyleRanges": Array [], - "key": "0", - "text": " ", - "type": "atomic", + "key": "g0000", + "text": "https://simplecast.com/s/a1f11d11", + "type": "unstyled", }, ], "entityMap": Object { "0": Object { + "data": Object { + "href": "https://simplecast.com/s/a1f11d11", + }, + "mutability": "MUTABLE", + "type": "link", + }, + "1": Object { "data": Object { "height": 200, "src": "https://embed.simplecast.com/a1f11d11", + "type": "simplecast", "url": "https://embed.simplecast.com/a1f11d11", }, "mutability": "MUTABLE", diff --git a/shared/draft-utils/test/add-embeds-to-draft-js.test.js b/shared/draft-utils/test/add-embeds-to-draft-js.test.js index 024a03dcd0..23e326cd70 100644 --- a/shared/draft-utils/test/add-embeds-to-draft-js.test.js +++ b/shared/draft-utils/test/add-embeds-to-draft-js.test.js @@ -17,6 +17,7 @@ describe('sites', () => { expect(getEmbedsFromText(text)).toEqual([ { url, + type: 'iframe', }, ]); }); @@ -27,6 +28,7 @@ describe('sites', () => { expect(getEmbedsFromText(text)).toEqual([ { url, + type: 'iframe', }, ]); }); @@ -40,6 +42,7 @@ describe('sites', () => { { aspectRatio: '56.25%', url: `https://player.vimeo.com/video/${id}`, + type: 'vimeo', }, ]); }); @@ -51,6 +54,7 @@ describe('sites', () => { { aspectRatio: '56.25%', url: `https://player.vimeo.com/video/${id}`, + type: 'vimeo', }, ]); }); @@ -62,6 +66,7 @@ describe('sites', () => { { aspectRatio: '56.25%', url: `https://player.vimeo.com/video/${id}`, + type: 'vimeo', }, ]); }); @@ -73,6 +78,7 @@ describe('sites', () => { { aspectRatio: '56.25%', url: `https://player.vimeo.com/video/${id}`, + type: 'vimeo', }, ]); }); @@ -84,6 +90,7 @@ describe('sites', () => { { aspectRatio: '56.25%', url: `https://player.vimeo.com/video/${id}`, + type: 'vimeo', }, ]); }); @@ -96,6 +103,7 @@ describe('sites', () => { { aspectRatio: '56.25%', url: `https://www.figma.com/embed?embed_host=spectrum&url=${text}`, + type: 'figma', }, ]); }); @@ -107,6 +115,7 @@ describe('sites', () => { { aspectRatio: '56.25%', url: `https://www.figma.com/embed?embed_host=spectrum&url=${text}`, + type: 'figma', }, ]); }); @@ -117,6 +126,7 @@ describe('sites', () => { { aspectRatio: '56.25%', url: `https://www.figma.com/embed?embed_host=spectrum&url=${text}`, + type: 'figma', }, ]); }); @@ -128,6 +138,7 @@ describe('sites', () => { { aspectRatio: '56.25%', url: `https://www.figma.com/embed?embed_host=spectrum&url=${text}`, + type: 'figma', }, ]); }); @@ -137,10 +148,8 @@ describe('sites', () => { expect(getEmbedsFromText(text)).toEqual([ { aspectRatio: '56.25%', - url: `https://www.figma.com/embed?embed_host=spectrum&url=${text.replace( - 'https://', - '' - )}`, + url: `https://www.figma.com/embed?embed_host=spectrum&url=${text}`, + type: 'figma', }, ]); }); @@ -154,6 +163,7 @@ describe('sites', () => { { aspectRatio: '56.25%', url: `https://www.youtube.com/embed/${id}`, + type: 'youtube', }, ]); }); @@ -165,6 +175,7 @@ describe('sites', () => { { aspectRatio: '56.25%', url: `https://www.youtube.com/embed/${id}`, + type: 'youtube', }, ]); }); @@ -175,7 +186,8 @@ describe('sites', () => { const text = 'https://framer.cloud/asdf123'; expect(getEmbedsFromText(text)).toEqual([ { - url: 'https://framer.cloud/asdf123', + url: 'https://share.framerjs.com/asdf123', + type: 'framer', width: 600, height: 800, }, @@ -187,6 +199,7 @@ describe('sites', () => { expect(getEmbedsFromText(text)).toEqual([ { url: 'https://share.framerjs.com/478kta5wx0wn', + type: 'framer', width: 600, height: 800, }, @@ -201,6 +214,7 @@ describe('sites', () => { { height: 300, url: 'https://codepen.io/jcoulterdesign/embed/NeOQzX', + type: 'codepen', }, ]); }); @@ -211,6 +225,7 @@ describe('sites', () => { { height: 300, url: 'https://codepen.io/jcoulterdesign/embed/NeOQzX', + type: 'codepen', }, ]); }); @@ -221,6 +236,7 @@ describe('sites', () => { { height: 300, url: 'https://codepen.io/jcoulterdesign/embed/NeOQzX', + type: 'codepen', }, ]); }); @@ -231,6 +247,7 @@ describe('sites', () => { { height: 300, url: 'https://codepen.io/jcoulterdesign/embed/NeOQzX', + type: 'codepen', }, ]); }); @@ -243,6 +260,7 @@ describe('sites', () => { { height: 500, url: 'https://codesandbox.io/embed/8lz7276xz2', + type: 'codesandbox', }, ]); }); @@ -253,6 +271,7 @@ describe('sites', () => { { height: 500, url: 'https://codesandbox.io/embed/8lz7276xz2?autoresize=true', + type: 'codesandbox', }, ]); }); @@ -263,6 +282,7 @@ describe('sites', () => { { height: 500, url: 'https://codesandbox.io/embed/8lz7276xz2', + type: 'codesandbox', }, ]); }); @@ -275,6 +295,7 @@ describe('sites', () => { { height: 200, url: 'https://embed.simplecast.com/8fb96767', + type: 'simplecast', }, ]); }); @@ -285,6 +306,7 @@ describe('sites', () => { { height: 200, url: 'https://embed.simplecast.com/8fb96767?color=000000', + type: 'simplecast', }, ]); }); @@ -295,6 +317,7 @@ describe('sites', () => { { height: 200, url: 'https://embed.simplecast.com/8fb96767', + type: 'simplecast', }, ]); }); @@ -406,6 +429,7 @@ describe('complex text', () => { { url: 'https://player.vimeo.com/video/123456', aspectRatio: '56.25%', + type: 'vimeo', }, ]); }); @@ -416,6 +440,7 @@ describe('complex text', () => { { url: 'https://player.vimeo.com/video/123456', aspectRatio: '56.25%', + type: 'vimeo', }, ]); }); @@ -427,10 +452,12 @@ describe('complex text', () => { { aspectRatio: '56.25%', url: 'https://www.youtube.com/embed/asdf123', + type: 'youtube', }, { aspectRatio: '56.25%', url: 'https://player.vimeo.com/video/123456', + type: 'vimeo', }, ]); }); @@ -440,10 +467,12 @@ describe('complex text', () => { expect(getEmbedsFromText(text)).toEqual([ { url: 'bla.com', + type: 'iframe', }, { url: 'https://player.vimeo.com/video/123456', aspectRatio: '56.25%', + type: 'vimeo', }, ]); }); @@ -459,6 +488,7 @@ describe('complex text', () => { { height: 200, url: 'https://embed.simplecast.com/a1f11d11', + type: 'simplecast', }, ]); }); @@ -499,3 +529,54 @@ it('should add embeds', () => { }; expect(addEmbedsToEditorState(input)).toMatchSnapshot(); }); + +it('should add multiple embeds to text', () => { + const input = { + blocks: [ + { + type: 'unstyled', + key: 'g0000', + data: {}, + depth: 0, + inlineStyleRanges: [], + entityRanges: [], + text: + 'New podcast! https://simplecast.com/s/a1f11d11 it is really cool https://simplecast.com/s/a1f11d11', + }, + ], + entityMap: {}, + }; + expect(addEmbedsToEditorState(input)).toMatchSnapshot(); +}); + +it('should remove link entities', () => { + const input = { + blocks: [ + { + type: 'unstyled', + key: 'g0000', + data: {}, + depth: 0, + inlineStyleRanges: [], + entityRanges: [ + { + offset: 0, + length: 33, + key: 0, + }, + ], + text: 'https://simplecast.com/s/a1f11d11', + }, + ], + entityMap: { + 0: { + type: 'link', + mutability: 'MUTABLE', + data: { + href: 'https://simplecast.com/s/a1f11d11', + }, + }, + }, + }; + expect(addEmbedsToEditorState(input)).toMatchSnapshot(); +}); diff --git a/shared/generate-meta-info.js b/shared/generate-meta-info.js index 833c6e97be..fb7dae8506 100644 --- a/shared/generate-meta-info.js +++ b/shared/generate-meta-info.js @@ -107,7 +107,7 @@ function generateMetaInfo(input /*: Input */) /*: Meta */ { ? toPlainText(JSON.parse(data.body)) : data.body); return setDefault({ - title: data && data.title + ' · ' + data.communityName, + title: data && data.title + ' · ' + data.communityName + ' community', description: body, }); } diff --git a/shared/graphql/fragments/channel/channelMetaData.js b/shared/graphql/fragments/channel/channelMetaData.js index 628af4b340..9001dab3e6 100644 --- a/shared/graphql/fragments/channel/channelMetaData.js +++ b/shared/graphql/fragments/channel/channelMetaData.js @@ -3,7 +3,6 @@ import gql from 'graphql-tag'; export type ChannelMetaDataType = { metaData: { - threads: number, members: number, onlineMembers: number, }, @@ -12,7 +11,6 @@ export type ChannelMetaDataType = { export default gql` fragment channelMetaData on Channel { metaData { - threads members onlineMembers } diff --git a/shared/graphql/fragments/community/communityInfo.js b/shared/graphql/fragments/community/communityInfo.js index c844b5f30e..a0b17b5637 100644 --- a/shared/graphql/fragments/community/communityInfo.js +++ b/shared/graphql/fragments/community/communityInfo.js @@ -12,6 +12,7 @@ export type CommunityInfoType = { coverPhoto: string, pinnedThreadId: ?string, watercoolerId: ?string, + lastActive?: Date, isPrivate: boolean, communityPermissions: { isMember: boolean, @@ -20,11 +21,13 @@ export type CommunityInfoType = { isPending: boolean, isModerator: boolean, reputation: number, + lastSeen?: Date, }, brandedLogin: { isEnabled: boolean, message: ?string, }, + watercoolerId: ?string, }; export default gql` @@ -40,6 +43,8 @@ export default gql` pinnedThreadId watercoolerId isPrivate + watercoolerId + lastActive communityPermissions { isMember isBlocked @@ -47,6 +52,7 @@ export default gql` isPending isModerator reputation + lastSeen } brandedLogin { isEnabled diff --git a/shared/graphql/fragments/community/communityMembers.js b/shared/graphql/fragments/community/communityMembers.js index f8b10235fd..01be01b05e 100644 --- a/shared/graphql/fragments/community/communityMembers.js +++ b/shared/graphql/fragments/community/communityMembers.js @@ -24,7 +24,7 @@ export type CommunityMembersType = { export default gql` fragment communityMembers on Community { members(after: $after, filter: $filter, first: $first) - @connection(key: "communityMembers") { + @connection(key: "communityMembers", filters: ["filter"]) { pageInfo { hasNextPage hasPreviousPage diff --git a/shared/graphql/fragments/community/communityMetaData.js b/shared/graphql/fragments/community/communityMetaData.js index 70d5672959..1f56fa46c9 100644 --- a/shared/graphql/fragments/community/communityMetaData.js +++ b/shared/graphql/fragments/community/communityMetaData.js @@ -3,7 +3,6 @@ import gql from 'graphql-tag'; export type CommunityMetaDataType = { metaData: { - channels: number, members: number, onlineMembers: number, }, @@ -12,7 +11,6 @@ export type CommunityMetaDataType = { export default gql` fragment communityMetaData on Community { metaData { - channels members onlineMembers } diff --git a/shared/graphql/fragments/community/communityThreadConnection.js b/shared/graphql/fragments/community/communityThreadConnection.js index 676487a311..ed476ced18 100644 --- a/shared/graphql/fragments/community/communityThreadConnection.js +++ b/shared/graphql/fragments/community/communityThreadConnection.js @@ -36,7 +36,8 @@ export default gql` watercooler { ...threadInfo } - threadConnection(first: 10, after: $after, sort: $sort) { + threadConnection(first: 10, after: $after, sort: $sort) + @connection(key: "community-thread-connection", filter: ["sort"]) { pageInfo { hasNextPage hasPreviousPage diff --git a/shared/graphql/fragments/thread/threadInfo.js b/shared/graphql/fragments/thread/threadInfo.js index e61180aeda..8e29ab64b1 100644 --- a/shared/graphql/fragments/thread/threadInfo.js +++ b/shared/graphql/fragments/thread/threadInfo.js @@ -1,11 +1,12 @@ // @flow import gql from 'graphql-tag'; import userInfoFragment from '../user/userInfo'; -import type { UserInfoType } from '../user/userInfo'; import communityInfoFragment from '../community/communityInfo'; import type { CommunityInfoType } from '../community/communityInfo'; -import channelInfoFragment from '../channel/channelInfo'; +import communityMetaDataFragment from '../community/communityMetaData'; +import type { CommunityMetaDataType } from '../community/communityMetaData'; import threadParticipantFragment from './threadParticipant'; +import channelInfoFragment from '../channel/channelInfo'; import type { ChannelInfoType } from '../channel/channelInfo'; import type { ThreadMessageConnectionType } from 'shared/graphql/fragments/thread/threadMessageConnection'; import type { ThreadParticipantType } from './threadParticipant'; @@ -24,6 +25,9 @@ export type ThreadInfoType = { lastActive: ?string, receiveNotifications: boolean, currentUserLastSeen: ?string, + editedBy?: { + ...$Exact, + }, author: { ...$Exact, }, @@ -32,6 +36,7 @@ export type ThreadInfoType = { }, community: { ...$Exact, + ...$Exact, }, // $FlowFixMe: We need to remove `messageConnection` from ThreadMessageConnectionType. This works in the meantime. ...$Exact, @@ -61,6 +66,9 @@ export default gql` lastActive receiveNotifications currentUserLastSeen + editedBy { + ...threadParticipant + } author { ...threadParticipant } @@ -69,6 +77,7 @@ export default gql` } community { ...communityInfo + ...communityMetaData } isPublished isLocked @@ -93,4 +102,5 @@ export default gql` ${userInfoFragment} ${channelInfoFragment} ${communityInfoFragment} + ${communityMetaDataFragment} `; diff --git a/shared/graphql/fragments/user/userCommunityConnection.js b/shared/graphql/fragments/user/userCommunityConnection.js index 11c20e10a5..384e973837 100644 --- a/shared/graphql/fragments/user/userCommunityConnection.js +++ b/shared/graphql/fragments/user/userCommunityConnection.js @@ -1,11 +1,14 @@ // @flow import gql from 'graphql-tag'; import communityInfoFragment from '../community/communityInfo'; +import communityMetaDataFragment from '../community/communityMetaData'; import type { CommunityInfoType } from '../community/communityInfo'; +import type { CommunityMetaDataType } from '../community/communityMetaData'; type Edge = { node: { ...$Exact, + ...$Exact, contextPermissions: { communityId: string, isOwner: boolean, @@ -21,7 +24,7 @@ export type UserCommunityConnectionType = { hasNextPage: boolean, hasPreviousPage: boolean, }, - edges: Array, + edges: Array, }, }; @@ -35,6 +38,7 @@ export default gql` edges { node { ...communityInfo + ...communityMetaData contextPermissions { communityId isOwner @@ -45,5 +49,6 @@ export default gql` } } } + ${communityMetaDataFragment} ${communityInfoFragment} `; diff --git a/shared/graphql/mutations/channel/joinChannelWithToken.js b/shared/graphql/mutations/channel/joinChannelWithToken.js index f051a6b014..16c89f8e45 100644 --- a/shared/graphql/mutations/channel/joinChannelWithToken.js +++ b/shared/graphql/mutations/channel/joinChannelWithToken.js @@ -23,7 +23,7 @@ export const joinChannelWithTokenMutation = gql` const joinChannelWithTokenOptions = { options: { - refetchQueries: ['getCurrentUserProfile', 'getEverythingThreads'], + refetchQueries: ['getCurrentUserCommunityConnection'], }, props: ({ mutate }) => ({ joinChannelWithToken: input => diff --git a/shared/graphql/mutations/channel/toggleChannelPendingUser.js b/shared/graphql/mutations/channel/toggleChannelPendingUser.js index 78c945fab1..68dada1701 100644 --- a/shared/graphql/mutations/channel/toggleChannelPendingUser.js +++ b/shared/graphql/mutations/channel/toggleChannelPendingUser.js @@ -5,8 +5,6 @@ import channelInfoFragment from '../../fragments/channel/channelInfo'; import type { ChannelInfoType } from '../../fragments/channel/channelInfo'; import userInfoFragment from '../../fragments/user/userInfo'; import type { UserInfoType } from '../../fragments/user/userInfo'; -import channelMetaDataFragment from '../../fragments/channel/channelMetaData'; -import type { ChannelMetaDataType } from '../../fragments/channel/channelMetaData'; import { getChannelMemberConnectionQuery } from '../../queries/channel/getChannelMemberConnection'; type User = { @@ -19,9 +17,6 @@ export type ToggleChannelPendingUserType = { ...$Exact, pendingUsers: Array, blockedUsers: Array, - channelMetaData: { - ...$Exact, - }, }, }, }; @@ -42,12 +37,10 @@ export const toggleChannelPendingUserMutation = gql` blockedUsers { ...userInfo } - ...channelMetaData } } ${channelInfoFragment} ${userInfoFragment} - ${channelMetaDataFragment} `; const toggleChannelPendingUserOptions = { diff --git a/shared/graphql/mutations/channel/toggleChannelSubscription.js b/shared/graphql/mutations/channel/toggleChannelSubscription.js index 1e4b96d0cb..a6aa064bf9 100644 --- a/shared/graphql/mutations/channel/toggleChannelSubscription.js +++ b/shared/graphql/mutations/channel/toggleChannelSubscription.js @@ -13,9 +13,9 @@ export type ToggleChannelSubscriptionType = { }; export type ToggleChannelSubscriptionProps = { - toggleChannelSubscription: ({ channelId: string }) => Promise< - ToggleChannelSubscriptionType - >, + toggleChannelSubscription: ({ + channelId: string, + }) => Promise, }; export const toggleChannelSubscriptionMutation = gql` @@ -29,7 +29,7 @@ export const toggleChannelSubscriptionMutation = gql` const toggleChannelSubscriptionOptions = { options: { - refetchQueries: ['getCurrentUserProfile', 'getEverythingThreads'], + refetchQueries: ['getCommunityThreadConnection'], }, props: ({ mutate }) => ({ toggleChannelSubscription: ({ channelId }: { channelId: string }) => diff --git a/shared/graphql/mutations/channel/unblockChannelBlockedUser.js b/shared/graphql/mutations/channel/unblockChannelBlockedUser.js index a9fc02ce33..1fd4fa010a 100644 --- a/shared/graphql/mutations/channel/unblockChannelBlockedUser.js +++ b/shared/graphql/mutations/channel/unblockChannelBlockedUser.js @@ -5,8 +5,6 @@ import channelInfoFragment from '../../fragments/channel/channelInfo'; import type { ChannelInfoType } from '../../fragments/channel/channelInfo'; import userInfoFragment from '../../fragments/user/userInfo'; import type { UserInfoType } from '../../fragments/user/userInfo'; -import channelMetaDataFragment from '../../fragments/channel/channelMetaData'; -import type { ChannelMetaDataType } from '../../fragments/channel/channelMetaData'; type User = { ...$Exact, @@ -18,9 +16,6 @@ export type UnblockChannelBlockedUserType = { ...$Exact, pendingUsers: Array, blockedUsers: Array, - channelMetaData: { - ...$Exact, - }, }, }, }; @@ -40,12 +35,10 @@ export const unblockChannelBlockedUserMutation = gql` blockedUsers { ...userInfo } - ...channelMetaData } } ${channelInfoFragment} ${userInfoFragment} - ${channelMetaDataFragment} `; const unblockChannelBlockedUserOptions = { diff --git a/shared/graphql/mutations/community/setCommunityLastSeen.js b/shared/graphql/mutations/community/setCommunityLastSeen.js new file mode 100644 index 0000000000..7ed3382606 --- /dev/null +++ b/shared/graphql/mutations/community/setCommunityLastSeen.js @@ -0,0 +1,56 @@ +// @flow +import gql from 'graphql-tag'; +import { graphql } from 'react-apollo'; +import communityInfoFragment from 'shared/graphql/fragments/community/communityInfo'; +import type { CommunityInfoType } from '../../fragments/community/communityInfo'; + +export type SetCommunityLastSeenType = { + data: { + setCommunityLastSeen: { + ...$Exact, + }, + }, +}; + +type SetCommunityLastSeenInput = { + lastSeen: Date, + id: string, +}; + +export const setCommunityLastSeenMutation = gql` + mutation setCommunityLastSeen($input: SetCommunityLastSeenInput!) { + setCommunityLastSeen(input: $input) { + ...communityInfo + } + } + ${communityInfoFragment} +`; + +const setCommunityLastSeenOptions = { + props: ({ mutate, ownProps }) => ({ + setCommunityLastSeen: (input: SetCommunityLastSeenInput) => + mutate({ + variables: { + input, + }, + optimisticResponse: { + __typename: 'Mutation', + setCommunityLastSeen: { + __typename: 'Community', + id: input.id, + ...ownProps.community, + communityPermissions: { + ...ownProps.community.communityPermissions, + __typename: 'CommunityPermissions', + lastSeen: input.lastSeen.toISOString(), + }, + }, + }, + }), + }), +}; + +export default graphql( + setCommunityLastSeenMutation, + setCommunityLastSeenOptions +); diff --git a/shared/graphql/mutations/communityMember/addCommunityMember.js b/shared/graphql/mutations/communityMember/addCommunityMember.js index fbdf6b2d1c..41d13e566a 100644 --- a/shared/graphql/mutations/communityMember/addCommunityMember.js +++ b/shared/graphql/mutations/communityMember/addCommunityMember.js @@ -19,9 +19,9 @@ export type AddCommunityMemberType = { }; export type AddCommunityMemberProps = { - addCommunityMember: ({ input: { communityId: string } }) => Promise< - AddCommunityMemberType - >, + addCommunityMember: ({ + input: { communityId: string }, + }) => Promise, }; export const addCommunityMemberQuery = gql` @@ -38,6 +38,9 @@ export const addCommunityMemberQuery = gql` `; const addCommunityMemberOptions = { + options: { + refetchQueries: ['getCurrentUserCommunityConnection'], + }, props: ({ mutate }) => ({ addCommunityMember: ({ input }) => mutate({ diff --git a/shared/graphql/mutations/communityMember/addCommunityMemberWithToken.js b/shared/graphql/mutations/communityMember/addCommunityMemberWithToken.js index 9af3e90969..40c337a97f 100644 --- a/shared/graphql/mutations/communityMember/addCommunityMemberWithToken.js +++ b/shared/graphql/mutations/communityMember/addCommunityMemberWithToken.js @@ -25,7 +25,7 @@ export const addCommunityMemberWithTokenMutation = gql` const addCommunityMemberWithTokenOptions = { options: { - refetchQueries: ['getCurrentUserProfile', 'getEverythingThreads'], + refetchQueries: ['getCurrentUserCommunityConnection'], }, props: ({ mutate }) => ({ addCommunityMemberWithToken: input => diff --git a/shared/graphql/mutations/communityMember/removeCommunityMember.js b/shared/graphql/mutations/communityMember/removeCommunityMember.js index a5f4697f19..b74ffd9e55 100644 --- a/shared/graphql/mutations/communityMember/removeCommunityMember.js +++ b/shared/graphql/mutations/communityMember/removeCommunityMember.js @@ -19,9 +19,9 @@ export type RemoveCommunityMemberType = { }; export type RemoveCommunityMemberProps = { - removeCommunityMember: ({ input: { communityId: string } }) => Promise< - RemoveCommunityMemberType - >, + removeCommunityMember: ({ + input: { communityId: string }, + }) => Promise, }; export const removeCommunityMemberQuery = gql` @@ -38,6 +38,9 @@ export const removeCommunityMemberQuery = gql` `; const removeCommunityMemberOptions = { + options: { + refetchQueries: ['getCurrentUserCommunityConnection'], + }, props: ({ mutate }) => ({ removeCommunityMember: ({ input }) => mutate({ diff --git a/shared/graphql/mutations/message/sendDirectMessage.js b/shared/graphql/mutations/message/sendDirectMessage.js index 7b6b756714..a4e3ae6b69 100644 --- a/shared/graphql/mutations/message/sendDirectMessage.js +++ b/shared/graphql/mutations/message/sendDirectMessage.js @@ -39,6 +39,7 @@ const sendDirectMessageOptions = { timestamp: JSON.parse(JSON.stringify(new Date())), messageType: message.messageType, modifiedAt: '', + bot: false, author: { user: { ...ownProps.currentUser, @@ -72,11 +73,12 @@ const sendDirectMessageOptions = { }, }, update: (store, { data: { addMessage } }) => { + const threadId = ownProps.threadId || ownProps.id || ownProps.thread; // Read the data from our cache for this query. const data = store.readQuery({ query: getDMThreadMessageConnectionQuery, variables: { - id: ownProps.thread || ownProps.id, + id: threadId, }, }); @@ -127,7 +129,7 @@ const sendDirectMessageOptions = { query: getDMThreadMessageConnectionQuery, data, variables: { - id: ownProps.thread, + id: threadId, }, }); }, diff --git a/shared/graphql/mutations/message/sendMessage.js b/shared/graphql/mutations/message/sendMessage.js index a5bbf3fd4d..ac281d803c 100644 --- a/shared/graphql/mutations/message/sendMessage.js +++ b/shared/graphql/mutations/message/sendMessage.js @@ -4,6 +4,7 @@ import { graphql } from 'react-apollo'; import { btoa } from 'b2a'; import snarkdown from 'snarkdown'; import messageInfoFragment from '../../fragments/message/messageInfo'; +import communityInfoFragment from '../../fragments/community/communityInfo'; import type { MessageInfoType } from '../../fragments/message/messageInfo'; import { getThreadMessageConnectionQuery } from '../../queries/thread/getThreadMessageConnection'; import { messageTypeObj } from 'shared/draft-utils/message-types'; @@ -125,9 +126,9 @@ const sendMessageOptions = { return edge; } ); - // If it's an actual duplicate because the subscription already added the message to the store then ignore + // If it's an actual duplicate because the subscription already added the message to the store + // only set lastActive and currentUserLastSeen } else if (messageInStore) { - return; // If it's a totally new message (i.e. the optimstic response) then insert it at the end } else { data.thread.messageConnection.edges.push({ @@ -140,11 +141,42 @@ const sendMessageOptions = { // Write our data back to the cache. store.writeQuery({ query: getThreadMessageConnectionQuery, - data, + data: { + ...data, + thread: { + ...data.thread, + // Optimistically update lastActive and lastSeen to make sure the + // feed ordering is the way users expect it to be + lastActive: addMessage.timestamp, + currentUserLastSeen: new Date( + new Date(addMessage.timestamp).getTime() + 1000 + ).toISOString(), + }, + }, variables: { id: message.threadId, }, }); + + const community = store.readFragment({ + fragment: communityInfoFragment, + fragmentName: 'communityInfo', + id: `Community:${data.thread.community.slug}`, + }); + + store.writeFragment({ + fragment: communityInfoFragment, + fragmentName: 'communityInfo', + id: `Community:${data.thread.community.slug}`, + data: { + ...community, + communityPermissions: { + ...community.communityPermissions, + // Forward-date lastSeen by 10 seconds + lastSeen: new Date(Date.now() + 10000).toISOString(), + }, + }, + }); }, }); }, diff --git a/shared/graphql/mutations/notification/markSingleNotificationSeen.js b/shared/graphql/mutations/notification/markSingleNotificationSeen.js index 5836cdf8e9..9923859162 100644 --- a/shared/graphql/mutations/notification/markSingleNotificationSeen.js +++ b/shared/graphql/mutations/notification/markSingleNotificationSeen.js @@ -10,12 +10,12 @@ export const markSingleNotificationSeenMutation = gql` const markSingleNotificationSeenOptions = { props: ({ mutate }: { mutate: Function }) => ({ - markSingleNotificationSeen: () => mutate(), - }), - options: ({ notification: { id } }: { notification: { id: string } }) => ({ - variables: { - id, - }, + markSingleNotificationSeen: (id: string) => + mutate({ + variables: { + id, + }, + }), }), }; diff --git a/shared/graphql/mutations/user/editUser.js b/shared/graphql/mutations/user/editUser.js index b2f127e7a6..30ec78fa90 100644 --- a/shared/graphql/mutations/user/editUser.js +++ b/shared/graphql/mutations/user/editUser.js @@ -4,6 +4,7 @@ import gql from 'graphql-tag'; import userInfoFragment from '../../fragments/user/userInfo'; import type { UserInfoType } from '../../fragments/user/userInfo'; import type { EditUserInput } from 'shared/db/queries/user'; +import { getUserByUsernameQuery } from '../../queries/user/getUser'; export type EditUserType = { ...$Exact, @@ -25,12 +26,23 @@ export const editUserMutation = gql` `; const editUserOptions = { + options: { + refetchQueries: ['getCurrentUser'], + }, props: ({ mutate }) => ({ editUser: input => mutate({ variables: { input, }, + refetchQueries: [ + { + query: getUserByUsernameQuery, + variables: { + id: input.username, + }, + }, + ], }), }), }; diff --git a/shared/graphql/queries/channel/getChannel.js b/shared/graphql/queries/channel/getChannel.js index 976239b52a..d6e7198f2f 100644 --- a/shared/graphql/queries/channel/getChannel.js +++ b/shared/graphql/queries/channel/getChannel.js @@ -3,23 +3,18 @@ import { graphql } from 'react-apollo'; import gql from 'graphql-tag'; import channelInfoFragment from '../../fragments/channel/channelInfo'; import type { ChannelInfoType } from '../../fragments/channel/channelInfo'; -import channelMetaDataFragment from '../../fragments/channel/channelMetaData'; -import type { ChannelMetaDataType } from '../../fragments/channel/channelMetaData'; export type GetChannelType = { ...$Exact, - ...$Exact, }; export const getChannelByIdQuery = gql` query getChannelById($id: ID) { channel(id: $id) { ...channelInfo - ...channelMetaData } } ${channelInfoFragment} - ${channelMetaDataFragment} `; const getChannelByIdOptions = { @@ -49,11 +44,9 @@ export const getChannelBySlugAndCommunitySlugQuery = gql` ) { channel(channelSlug: $channelSlug, communitySlug: $communitySlug) { ...channelInfo - ...channelMetaData } } ${channelInfoFragment} - ${channelMetaDataFragment} `; const getChannelBySlugAndCommunitySlugOptions = { @@ -72,7 +65,11 @@ export const getChannelBySlugAndCommunitySlug = graphql( ); const getChannelByMatchOptions = { - options: ({ match: { params: { channelSlug, communitySlug } } }) => ({ + options: ({ + match: { + params: { channelSlug, communitySlug }, + }, + }) => ({ variables: { channelSlug: channelSlug, communitySlug: communitySlug, diff --git a/shared/graphql/queries/channel/getChannelMemberConnection.js b/shared/graphql/queries/channel/getChannelMemberConnection.js index 1e128f866f..51174be703 100644 --- a/shared/graphql/queries/channel/getChannelMemberConnection.js +++ b/shared/graphql/queries/channel/getChannelMemberConnection.js @@ -3,14 +3,11 @@ import { graphql } from 'react-apollo'; import gql from 'graphql-tag'; import channelInfoFragment from '../../fragments/channel/channelInfo'; import type { ChannelInfoType } from '../../fragments/channel/channelInfo'; -import channelMetaDataFragment from '../../fragments/channel/channelMetaData'; -import type { ChannelMetaDataType } from '../../fragments/channel/channelMetaData'; import channelMemberConnectionFragment from '../../fragments/channel/channelMemberConnection'; import type { ChannelMemberConnectionType } from '../../fragments/channel/channelMemberConnection'; export type GetChannelMemberConnectionType = { ...$Exact, - ...$Exact, ...$Exact, }; @@ -18,12 +15,10 @@ export const getChannelMemberConnectionQuery = gql` query getChannelMemberConnection($id: ID, $first: Int, $after: String) { channel(id: $id) { ...channelInfo - ...channelMetaData ...channelMemberConnection } } ${channelInfoFragment} - ${channelMetaDataFragment} ${channelMemberConnectionFragment} `; @@ -31,12 +26,10 @@ const LoadMoreMembers = gql` query loadMoreChannelMembers($id: ID, $first: Int, $after: String) { channel(id: $id) { ...channelInfo - ...channelMetaData ...channelMemberConnection } } ${channelInfoFragment} - ${channelMetaDataFragment} ${channelMemberConnectionFragment} `; diff --git a/shared/graphql/queries/channel/getChannelThreadConnection.js b/shared/graphql/queries/channel/getChannelThreadConnection.js index 208399abc1..c7e3f82fed 100644 --- a/shared/graphql/queries/channel/getChannelThreadConnection.js +++ b/shared/graphql/queries/channel/getChannelThreadConnection.js @@ -64,9 +64,17 @@ const getChannelThreadConnectionOptions = { channel && channel.threadConnection ? channel.threadConnection.pageInfo.hasNextPage : false, - subscribeToUpdatedThreads: () => { + subscribeToUpdatedThreads: (channelIds?: Array) => { + const variables = channelIds + ? { + variables: { + channelIds, + }, + } + : {}; return subscribeToMore({ document: subscribeToUpdatedThreads, + ...variables, updateQuery: (prev, { subscriptionData }) => { const updatedThread = subscriptionData.data && subscriptionData.data.threadUpdated; @@ -79,8 +87,7 @@ const getChannelThreadConnectionOptions = { const newThreads = updatedThreadShouldAppearInContext ? parseRealtimeThreads( prev.channel.threadConnection.edges, - updatedThread, - ownProps.dispatch + updatedThread ).filter(thread => thread.node.channel.id === thisChannelId) : [...prev.channel.threadConnection.edges]; diff --git a/shared/graphql/queries/community/getCommunities.js b/shared/graphql/queries/community/getCommunities.js index f381d24958..a1a31b3b33 100644 --- a/shared/graphql/queries/community/getCommunities.js +++ b/shared/graphql/queries/community/getCommunities.js @@ -42,9 +42,11 @@ export const getCommunitiesBySlugsQuery = gql` query getCommunitiesBySlugs($slugs: [LowercaseString]) { communities(slugs: $slugs) { ...communityInfo + ...communityMetaData } } ${communityInfoFragment} + ${communityMetaDataFragment} `; const getCommunitiesBySlugOptions = { @@ -65,9 +67,11 @@ const getCommunitiesByCuratedContentTypeQuery = gql` query getCommunitiesCollection($curatedContentType: String) { communities(curatedContentType: $curatedContentType) { ...communityInfo + ...communityMetaData } } ${communityInfoFragment} + ${communityMetaDataFragment} `; const getCommunitiesByCuratedContentTypeOptions = { diff --git a/shared/graphql/queries/community/getCommunityChannelConnection.js b/shared/graphql/queries/community/getCommunityChannelConnection.js index f3687cd244..735b797020 100644 --- a/shared/graphql/queries/community/getCommunityChannelConnection.js +++ b/shared/graphql/queries/community/getCommunityChannelConnection.js @@ -27,7 +27,7 @@ const getCommunityChannelConnectionOptions = { variables: { id, }, - fetchPolicy: 'cache-and-network', + fetchPolicy: 'cache-first', }), }; diff --git a/shared/graphql/queries/community/getCommunityThreadConnection.js b/shared/graphql/queries/community/getCommunityThreadConnection.js index cc4c51e16a..dfb9f7ccd9 100644 --- a/shared/graphql/queries/community/getCommunityThreadConnection.js +++ b/shared/graphql/queries/community/getCommunityThreadConnection.js @@ -75,9 +75,17 @@ const getCommunityThreadConnectionOptions = { ? community.threadConnection.pageInfo.hasNextPage : false, feed: community && community.id, - subscribeToUpdatedThreads: () => { + subscribeToUpdatedThreads: (channelIds?: Array) => { + const variables = channelIds + ? { + variables: { + channelIds, + }, + } + : {}; return subscribeToMore({ document: subscribeToUpdatedThreads, + ...variables, updateQuery: (prev, { subscriptionData }) => { const updatedThread = subscriptionData.data && subscriptionData.data.threadUpdated; diff --git a/shared/graphql/queries/directMessageThread/getDirectMessageThreadByUserId.js b/shared/graphql/queries/directMessageThread/getDirectMessageThreadByUserIds.js similarity index 54% rename from shared/graphql/queries/directMessageThread/getDirectMessageThreadByUserId.js rename to shared/graphql/queries/directMessageThread/getDirectMessageThreadByUserIds.js index e33e966d7d..67663c760b 100644 --- a/shared/graphql/queries/directMessageThread/getDirectMessageThreadByUserId.js +++ b/shared/graphql/queries/directMessageThread/getDirectMessageThreadByUserIds.js @@ -4,29 +4,29 @@ import { graphql } from 'react-apollo'; import directMessageThreadInfoFragment from '../../fragments/directMessageThread/directMessageThreadInfo'; import type { DirectMessageThreadInfoType } from '../../fragments/directMessageThread/directMessageThreadInfo'; -export type GetDirectMessageThreadByUserIdType = { +export type GetDirectMessageThreadByUserIdsType = { ...$Exact, }; -export const getDirectMessageThreadByUserIdQuery = gql` - query getDirectMessageThreadByUserId($userId: ID!) { - directMessageThreadByUserId(userId: $userId) { +export const getDirectMessageThreadByUserIdsQuery = gql` + query getDirectMessageThreadByUserIds($userIds: [ID!]) { + directMessageThreadByUserIds(userIds: $userIds) { ...directMessageThreadInfo } } ${directMessageThreadInfoFragment} `; -export const getDirectMessageThreadByUserIdOptions = { - options: ({ userId }: { userId: string }) => ({ +export const getDirectMessageThreadByUserIdsOptions = { + options: ({ userIds }: { userIds: Array }) => ({ variables: { - userId, + userIds, }, fetchPolicy: 'cache-and-network', }), }; export default graphql( - getDirectMessageThreadByUserIdQuery, - getDirectMessageThreadByUserIdOptions + getDirectMessageThreadByUserIdsQuery, + getDirectMessageThreadByUserIdsOptions ); diff --git a/shared/graphql/queries/directMessageThread/getDirectMessageThreadMessageConnection.js b/shared/graphql/queries/directMessageThread/getDirectMessageThreadMessageConnection.js index 58853599ec..4d864f30e3 100644 --- a/shared/graphql/queries/directMessageThread/getDirectMessageThreadMessageConnection.js +++ b/shared/graphql/queries/directMessageThread/getDirectMessageThreadMessageConnection.js @@ -95,9 +95,6 @@ export const getDMThreadMessageConnectionOptions = { }), }, subscribeToNewMessages: () => { - if (!directMessageThread) { - return; - } return data.subscribeToMore({ document: subscribeToNewMessages, variables: { diff --git a/shared/graphql/queries/notification/getDirectMessageNotifications.js b/shared/graphql/queries/notification/getDirectMessageNotifications.js index e8de99b083..743d574522 100644 --- a/shared/graphql/queries/notification/getDirectMessageNotifications.js +++ b/shared/graphql/queries/notification/getDirectMessageNotifications.js @@ -46,7 +46,7 @@ export const getDirectMessageNotificationsOptions = { props: (props: any) => ({ ...props, refetch: () => props.data.refetch(), - subscribeToDMs: () => { + subscribeToDMs: (callback?: Function) => { return props.data.subscribeToMore({ document: subscribeToDirectMessageNotifications, updateQuery: (prev, { subscriptionData }) => { @@ -54,8 +54,10 @@ export const getDirectMessageNotificationsOptions = { subscriptionData, _ => _.data.dmNotificationAdded ); - if (!newNotification) return prev; + + if (callback) callback(newNotification); + const notificationNode = { ...newNotification, __typename: 'Notification', diff --git a/shared/graphql/queries/notification/getNotifications.js b/shared/graphql/queries/notification/getNotifications.js index fd368fd96c..dc21b5c944 100644 --- a/shared/graphql/queries/notification/getNotifications.js +++ b/shared/graphql/queries/notification/getNotifications.js @@ -105,7 +105,7 @@ export const getNotificationsOptions = { }, }), refetch: () => refetch(), - subscribeToNewNotifications: () => + subscribeToNewNotifications: (callback?: Function) => subscribeToMore({ document: subscribeToNewNotifications, updateQuery: (prev, { subscriptionData }) => { @@ -113,6 +113,8 @@ export const getNotificationsOptions = { subscriptionData.data && subscriptionData.data.notificationAdded; if (!newNotification) return prev; + if (callback) callback(newNotification); + const notificationNode = { ...newNotification, __typename: 'Notification', diff --git a/shared/graphql/queries/thread/getThreadMessageConnection.js b/shared/graphql/queries/thread/getThreadMessageConnection.js index 152ac14957..d7bf832845 100644 --- a/shared/graphql/queries/thread/getThreadMessageConnection.js +++ b/shared/graphql/queries/thread/getThreadMessageConnection.js @@ -14,6 +14,73 @@ export type GetThreadMessageConnectionType = { ...$Exact, }; +type Variables = { + id: string, + last?: number, + first?: number, + after?: string, + before?: string, +}; + +const getVariables = ({ thread, ...props }): Variables => { + // if the thread has less than 25 messages, just load all of them + if (thread && thread.messageCount <= 25) { + return { + id: props.id, + last: 25, + }; + } + + // If the user is linked to either a specific message or has pagination URL params, load those messages + if (props.location && props.location.search) { + const params = queryString.parse(props.location.search); + + if (params) { + if (params.msgsafter) { + return { + id: props.id, + after: params.msgsafter, + first: 25, + }; + } else if (params.msgsbefore) { + return { + id: props.id, + before: params.msgsbefore, + last: 25, + }; + } else if (params.m) { + return { + id: props.id, + after: params.m, + first: 25, + }; + } + } + } + + // if it's a watercooler thread load the 25 most recent messages + if (props.isWatercooler) { + return { + id: props.id, + last: 25, + }; + } + + // If a user has seen a thread, load the last 25 + if (thread.currentUserLastSeen) { + return { + id: props.id, + last: 25, + }; + } + + // In all other cases, load the first 25 + return { + id: props.id, + first: 25, + }; +}; + export const getThreadMessageConnectionQuery = gql` query getThreadMessages( $id: ID! @@ -32,70 +99,9 @@ export const getThreadMessageConnectionQuery = gql` `; export const getThreadMessageConnectionOptions = { // $FlowFixMe - options: ({ thread, ...props }) => { - let msgsafter, msgsbefore; - if (props.location && props.location.search) { - try { - const params = queryString.parse(props.location.search); - msgsafter = params.msgsafter; - msgsbefore = params.msgsbefore; - } catch (err) { - // Ignore errors in query string parsing, who cares - console.error(err); - } - } - - let variables = { - id: props.id, - after: msgsafter ? msgsafter : null, - before: msgsbefore && !msgsafter ? msgsbefore : null, - last: null, - first: null, - }; - - // if the thread has less than 25 messages, just load all of them - if (thread.messageCount <= 25) { - variables.after = null; - variables.before = null; - // $FlowFixMe - variables.last = 25; - } - - if (thread.messageCount > 25) { - //if the thread has more than 25 messages, we'll likely only want to load the latest 25 - // **unless** the current user hasn't seen the thread before - // $FlowFixMe - variables.last = 25; - if (!thread.currentUserLastSeen) { - variables.last = null; - } - } - - // if it's a watercooler thread only ever load the 25 most recent messages - if (props.isWatercooler) { - variables.before = null; - variables.after = null; - //$FlowFixMe - variables.last = 25; - } - - // if a user is visiting a url like /thread/:id#:messageId we can extract - // the messageId from the query string and start pagination from there. This - // allows users to share links to individual messages and the pagination - // will work regardless of if it's a super long thread or not - if (props.location && props.location.search) { - const params = queryString.parse(props.location.search); - - if (params && params.m) { - variables.after = params.m; - variables.last = null; - // $FlowFixMe - variables.first = 25; - } - } - + options: props => { return { - variables, + variables: getVariables(props), fetchPolicy: 'cache-and-network', }; }, @@ -124,7 +130,7 @@ export const getThreadMessageConnectionOptions = { return props.data.fetchMore({ variables: { after: cursor, - first: undefined, + first: 25, before: undefined, last: undefined, }, @@ -169,7 +175,7 @@ export const getThreadMessageConnectionOptions = { after: undefined, first: undefined, before: cursor, - last: undefined, + last: 25, }, updateQuery: (prev, { fetchMoreResult }) => { if (!fetchMoreResult || !fetchMoreResult.thread) return prev; diff --git a/shared/graphql/queries/user/getCurrentUserEverythingFeed.js b/shared/graphql/queries/user/getCurrentUserEverythingFeed.js index 1764262791..16c870ce68 100644 --- a/shared/graphql/queries/user/getCurrentUserEverythingFeed.js +++ b/shared/graphql/queries/user/getCurrentUserEverythingFeed.js @@ -68,8 +68,7 @@ const getCurrentUserEverythingOptions = { const newThreads = parseRealtimeThreads( prev.user.everything.edges, - updatedThread, - ownProps.dispatch + updatedThread ); // Add the new notification to the data diff --git a/shared/graphql/queries/user/getUser.js b/shared/graphql/queries/user/getUser.js index 1bf472a1af..5f0a8f45be 100644 --- a/shared/graphql/queries/user/getUser.js +++ b/shared/graphql/queries/user/getUser.js @@ -43,7 +43,12 @@ const getUserByUsernameOptions = { }; const getUserByMatchOptions = { - options: ({ match: { params: { username } } }) => ({ + options: ({ + match: { + params: { username }, + }, + }) => ({ + fetchPolicy: 'cache-and-network', variables: { username, }, diff --git a/shared/graphql/queries/user/getUserCommunityConnection.js b/shared/graphql/queries/user/getUserCommunityConnection.js index b8d044937c..0f6609bfcb 100644 --- a/shared/graphql/queries/user/getUserCommunityConnection.js +++ b/shared/graphql/queries/user/getUserCommunityConnection.js @@ -4,6 +4,7 @@ import gql from 'graphql-tag'; import userInfoFragment from '../../fragments/user/userInfo'; import type { UserInfoType } from '../../fragments/user/userInfo'; import userCommunityConnectionFragment from '../../fragments/user/userCommunityConnection'; +import { subscribeToUpdatedCommunities } from '../../subscriptions'; import type { UserCommunityConnectionType } from '../../fragments/user/userCommunityConnection'; export type GetUserCommunityConnectionType = { @@ -45,7 +46,27 @@ const getUserCommunityConnectionOptions = { export const getCurrentUserCommunityConnection = graphql( getCurrentUserCommunityConnectionQuery, - { options: { fetchPolicy: 'cache-and-network' } } + { + options: { fetchPolicy: 'cache-first' }, + props: props => ({ + ...props, + subscribeToUpdatedCommunities: communityIds => { + const variables = communityIds + ? { + variables: { + communityIds, + }, + } + : {}; + return props.data.subscribeToMore({ + document: subscribeToUpdatedCommunities, + ...variables, + // No need to do anything fancy here, Apollo Client will automatically update the community by its ID + updateQuery: prev => prev, + }); + }, + }), + } ); export const getUserCommunityConnection = graphql( diff --git a/shared/graphql/queries/user/getUserThreadConnection.js b/shared/graphql/queries/user/getUserThreadConnection.js index 062c1f8a7b..5ab08a5341 100644 --- a/shared/graphql/queries/user/getUserThreadConnection.js +++ b/shared/graphql/queries/user/getUserThreadConnection.js @@ -68,9 +68,17 @@ const getUserThreadConnectionOptions = { user && user.threadConnection ? user.threadConnection.pageInfo.hasNextPage : false, - subscribeToUpdatedThreads: () => { + subscribeToUpdatedThreads: (channelIds?: Array) => { + const variables = channelIds + ? { + variables: { + channelIds, + }, + } + : {}; return subscribeToMore({ document: subscribeToUpdatedThreads, + ...variables, updateQuery: (prev, { subscriptionData }) => { const updatedThread = subscriptionData.data && subscriptionData.data.threadUpdated; @@ -83,8 +91,7 @@ const getUserThreadConnectionOptions = { const newThreads = updatedThreadShouldAppearInContext ? parseRealtimeThreads( prev.user.threadConnection.edges, - updatedThread, - ownProps.dispatch + updatedThread ).filter(thread => thread.node.author.user.id === thisUserId) : [...prev.user.threadConnection.edges]; diff --git a/shared/graphql/subscriptions/index.js b/shared/graphql/subscriptions/index.js index b47b94ae86..4429fd392f 100644 --- a/shared/graphql/subscriptions/index.js +++ b/shared/graphql/subscriptions/index.js @@ -4,6 +4,7 @@ import gql from 'graphql-tag'; import messageInfoFragment from '../fragments/message/messageInfo'; import notificationInfoFragment from '../fragments/notification/notificationInfo'; import threadInfoFragment from '../fragments/thread/threadInfo'; +import communityInfoFragment from '../fragments/community/communityInfo'; import directMessageThreadInfoFragment from '../fragments/directMessageThread/directMessageThreadInfo'; export const subscribeToNewMessages = gql` @@ -51,6 +52,15 @@ export const subscribeToUpdatedThreads = gql` ${threadInfoFragment} `; +export const subscribeToUpdatedCommunities = gql` + subscription subscribeToUpdatedCommunities($communityIds: [ID!]) { + communityUpdated(communityIds: $communityIds) { + ...communityInfo + } + } + ${communityInfoFragment} +`; + const SUBSCRIBE_TO_WEB_PUSH_MUTATION = gql` mutation subscribeToWebPush($subscription: WebPushSubscription!) { subscribeWebPush(subscription: $subscription) diff --git a/shared/graphql/subscriptions/utils.js b/shared/graphql/subscriptions/utils.js index b7e852095f..cbbcfdb882 100644 --- a/shared/graphql/subscriptions/utils.js +++ b/shared/graphql/subscriptions/utils.js @@ -1,12 +1,9 @@ // @flow -import { addActivityIndicator } from '../../../src/actions/newActivityIndicator'; - // used to update feed caches with new threads in real time // takes an array of existing threads in the cache and figures out how to insert the newly updated thread export const parseRealtimeThreads = ( prevThreads: Array, - updatedThread: Object, - dispatch?: Function + updatedThread: Object ) => { // get an array of thread ids based on the threads already in cache const prevThreadIds = prevThreads.map(thread => thread.node.id); @@ -16,11 +13,6 @@ export const parseRealtimeThreads = ( // been updated with a new message const hasNewThread = prevThreadIds.indexOf(updatedThread.id) < 0; - // if the updated thread is new, show the activity indicator in the ui - if (hasNewThread && dispatch) { - dispatch(addActivityIndicator()); - } - return hasNewThread ? [ { diff --git a/shared/imgix/signThread.js b/shared/imgix/signThread.js index 1042f6e7ee..404270bb52 100644 --- a/shared/imgix/signThread.js +++ b/shared/imgix/signThread.js @@ -34,14 +34,18 @@ const signBody = (body?: string, expires?: number): string => { // transform the body inline with signed image urls const imageUrlStoredAsSigned = - src && src.indexOf('https://spectrum.imgix.net') >= 0; + src && + (src.indexOf('https://spectrum.imgix.net') >= 0 || + src.indexOf('https://spectrum-proxy.imgix.net') >= 0); // if the image was stored in the db as a signed url (eg. after the plaintext update to the thread editor) // we need to remove all query params from the src, then re-sign in order to avoid duplicate signatures // or sending down a url with an expired signature if (imageUrlStoredAsSigned) { - const pathname = url.parse(src).pathname; + const { pathname } = url.parse(src); // always attempt to use the parsed pathname, but fall back to the original src - const sanitized = decodeURIComponent(pathname || src); + const sanitized = decodeURIComponent( + pathname ? pathname.replace(/^\//, '') : src + ); returnBody.entityMap[key].data.src = signImageUrl(sanitized, { expires }); } else { returnBody.entityMap[key].data.src = signImageUrl(src, { expires }); diff --git a/shared/middlewares/thread-param.js b/shared/middlewares/thread-param.js index 3d5443fa47..c5b00082de 100644 --- a/shared/middlewares/thread-param.js +++ b/shared/middlewares/thread-param.js @@ -1,18 +1,14 @@ -// Redirect any route ?thread= to /thread/ +// Redirect any route ?thread= or ?t= to /thread/ const threadParamRedirect = (req, res, next) => { - // Redirect /?t=asdf123 if the user isn't logged in - if (req.query.t && !req.user) { - if (req.query.m) { - res.redirect(`/thread/${req.query.t}?m=${req.query.m}`); - } - res.redirect(`/thread/${req.query.t}`); - // Redirect /anything?thread=asdf123 - } else if (req.query.thread) { + const threadId = req.query.thread || req.query.t; + + if (threadId) { if (req.query.m) { - res.redirect(`/thread/${req.query.thread}?m=${req.query.m}`); + res.redirect(`/thread/${threadId}?m=${req.query.m}`); + } else { + res.redirect(`/thread/${threadId}`); } - res.redirect(`/thread/${req.query.thread}`); } else { next(); } diff --git a/shared/notification-to-text.js b/shared/notification-to-text.js index f2a8051683..6b040d05a8 100644 --- a/shared/notification-to-text.js +++ b/shared/notification-to-text.js @@ -128,9 +128,7 @@ const formatNotification = ( ); if (notification.context.type === 'DIRECT_MESSAGE_THREAD') { - title = `New ${ - entities.length > 1 ? 'replies' : 'reply' - } in a direct message thread`; + title = `${actors} replied in your direct message thread`; href = `/messages/${notification.context.id}`; } else { title = `${notification.context.payload.content.title} (${ diff --git a/shared/test/__snapshots__/notification-to-text.test.js.snap b/shared/test/__snapshots__/notification-to-text.test.js.snap index b7739c3c5d..79ab28634b 100644 --- a/shared/test/__snapshots__/notification-to-text.test.js.snap +++ b/shared/test/__snapshots__/notification-to-text.test.js.snap @@ -23,7 +23,7 @@ Max Stoiber: Testin", "data": Object { "href": "/messages/7e37e582-3a06-47c9-b799-9a9f7f330ae4", }, - "title": "New replies in a direct message thread", + "title": "Max Stoiber replied in your direct message thread", } `; diff --git a/shared/theme/index.js b/shared/theme/index.js index 9c464a81c7..615ce78d2d 100644 --- a/shared/theme/index.js +++ b/shared/theme/index.js @@ -4,8 +4,9 @@ export const theme = { default: '#FFFFFF', reverse: '#16171A', wash: '#FAFAFA', + divider: '#F6F7F8', border: '#EBECED', - inactive: '#F6F7F8', + inactive: '#DFE7EF', }, brand: { default: '#4400CC', @@ -62,14 +63,14 @@ export const theme = { border: '#9FF5D9', }, text: { - default: '#16171A', - alt: '#828C99', + default: '#24292E', + secondary: '#384047', + alt: '#67717A', + placeholder: '#7C8894', reverse: '#FFFFFF', - placeholder: '#A3AFBF', - secondary: '#494C57', }, warn: { - default: '#C21F3A', + default: '#E22F2F', alt: '#E2197A', dark: '#85000C', wash: '#FFEDF6', diff --git a/shared/types.js b/shared/types.js index bbcbe7ff8c..be0adb2755 100644 --- a/shared/types.js +++ b/shared/types.js @@ -240,6 +240,7 @@ export type DBThread = { isLocked: boolean, lockedBy?: string, lockedAt?: Date, + editedBy?: string, lastActive: Date, modifiedAt?: Date, deletedAt?: string, diff --git a/src/actions/dashboardFeed.js b/src/actions/dashboardFeed.js deleted file mode 100644 index 4a66eac5c2..0000000000 --- a/src/actions/dashboardFeed.js +++ /dev/null @@ -1,53 +0,0 @@ -// @flow -import qs from 'query-string'; -import { storeItem } from 'src/helpers/localStorage'; -import { LAST_ACTIVE_COMMUNITY_KEY } from 'src/views/dashboard/components/communityList'; - -export const changeActiveThread = (threadId: ?string) => { - return { - type: 'SELECT_FEED_THREAD', - threadId, - }; -}; - -export const changeActiveCommunity = (communityId: string) => { - storeItem(LAST_ACTIVE_COMMUNITY_KEY, communityId); - return { - type: 'SELECT_FEED_COMMUNITY', - communityId, - }; -}; - -export const changeActiveChannel = (channelId: string) => { - return { - type: 'SELECT_FEED_CHANNEL', - channelId, - }; -}; - -export const toggleComposer = () => { - return { - type: 'TOGGLE_COMPOSER', - }; -}; - -export const openSearch = () => { - return { - type: 'TOGGLE_SEARCH_OPEN', - value: true, - }; -}; - -export const closeSearch = () => { - return { - type: 'TOGGLE_SEARCH_OPEN', - value: false, - }; -}; - -export const setSearchStringVariable = (value: string) => { - return { - type: 'SET_SEARCH_VALUE_FOR_SERVER', - value, - }; -}; diff --git a/src/actions/newActivityIndicator.js b/src/actions/newActivityIndicator.js deleted file mode 100644 index ff7c4a213d..0000000000 --- a/src/actions/newActivityIndicator.js +++ /dev/null @@ -1,5 +0,0 @@ -// @flow -export const clearActivityIndicator = () => ({ - type: 'CLEAR_NEW_ACTIVITY_INDICATOR', -}); -export const addActivityIndicator = () => ({ type: 'HAS_NEW_ACTIVITY' }); diff --git a/src/actions/newUserOnboarding.js b/src/actions/newUserOnboarding.js deleted file mode 100644 index 34226e2737..0000000000 --- a/src/actions/newUserOnboarding.js +++ /dev/null @@ -1,7 +0,0 @@ -// @flow -export const addCommunityToOnboarding = (community: Object) => { - return { - type: 'ADD_COMMUNITY_TO_NEW_USER_ONBOARDING', - payload: community, - }; -}; diff --git a/src/actions/notifications.js b/src/actions/notifications.js index f7d5ddca9c..b3dcf78027 100644 --- a/src/actions/notifications.js +++ b/src/actions/notifications.js @@ -4,3 +4,8 @@ export const updateNotificationsCount = (countType: string, count: number) => ({ countType, count, }); + +export const setNotifications = (notifications: Array) => ({ + type: 'SET_NOTIFICATIONS', + notifications, +}); diff --git a/src/actions/titlebar.js b/src/actions/titlebar.js new file mode 100644 index 0000000000..ec0cd12e47 --- /dev/null +++ b/src/actions/titlebar.js @@ -0,0 +1,9 @@ +// @flow +import type { TitlebarPayloadProps } from 'src/views/globalTitlebar'; + +export const setTitlebarProps = (payload: TitlebarPayloadProps) => { + return { + type: 'SET_TITLEBAR_PROPS', + payload, + }; +}; diff --git a/src/actions/toasts.js b/src/actions/toasts.js index 77a8ffe5c1..9f353de872 100644 --- a/src/actions/toasts.js +++ b/src/actions/toasts.js @@ -1,6 +1,6 @@ // @flow import type { Dispatch } from 'redux'; -type Toasts = 'success' | 'error' | 'neutral'; +type Toasts = 'success' | 'error' | 'neutral' | 'notification'; const addToast = ( id: number, @@ -27,7 +27,10 @@ let nextToastId = 0; export const addToastWithTimeout = (kind: Toasts, message: string) => ( dispatch: Dispatch ) => { - const timeout = kind === 'success' ? 3000 : 6000; + let timeout = 6000; + if (kind === 'success') timeout = 3000; + if (kind === 'notification') timeout = 5000; + let id = nextToastId++; dispatch(addToast(id, kind, message, timeout)); diff --git a/src/components/thirdPartyContextSetting/index.js b/src/components/analyticsTracking/index.js similarity index 90% rename from src/components/thirdPartyContextSetting/index.js rename to src/components/analyticsTracking/index.js index e3ce52a88e..f927d9c3a0 100644 --- a/src/components/thirdPartyContextSetting/index.js +++ b/src/components/analyticsTracking/index.js @@ -9,7 +9,7 @@ type Props = { data: { user: GetUserType }, }; -class AuthViewHandler extends React.Component { +class AnalyticsTracking extends React.Component { componentDidMount() { const AMPLITUDE_API_KEY = process.env.NODE_ENV === 'production' @@ -42,4 +42,4 @@ class AuthViewHandler extends React.Component { } } -export default compose(getCurrentUser)(AuthViewHandler); +export default compose(getCurrentUser)(AnalyticsTracking); diff --git a/src/components/appViewWrapper/index.js b/src/components/appViewWrapper/index.js index 8aae1e1003..41e0a9001a 100644 --- a/src/components/appViewWrapper/index.js +++ b/src/components/appViewWrapper/index.js @@ -1,15 +1,81 @@ // @flow import React from 'react'; import compose from 'recompose/compose'; -import { withRouter } from 'react-router'; -import { Wrapper } from './style'; - -const AppViewWrapperPure = (props: Object): React$Element => ( - // Note(@mxstbr): This ID is needed to make infinite scrolling work - // DO NOT REMOVE IT - - {props.children} - -); - -export default compose(withRouter)(AppViewWrapperPure); +import { withRouter, type History } from 'react-router'; +import type { UserInfoType } from 'shared/graphql/fragments/user/userInfo'; +import { withCurrentUser } from 'src/components/withCurrentUser'; +import { isViewingMarketingPage } from 'src/helpers/is-viewing-marketing-page'; +import { StyledAppViewWrapper } from './style'; + +type Props = { + isModal: boolean, + currentUser: ?UserInfoType, + history: History, + location: Object, +}; + +class AppViewWrapper extends React.Component { + ref: ?HTMLElement; + prevScrollOffset: number; + + constructor(props: Props) { + super(props); + this.ref = null; + this.prevScrollOffset = 0; + } + + getSnapshotBeforeUpdate(prevProps) { + const { isModal: currModal } = this.props; + const { isModal: prevModal } = prevProps; + + /* + If the user is going to open a modal, grab the current scroll + offset of the main view the user is on and save it for now; we'll use + the value to restore the scroll position after the user closes the modal + */ + if (!prevModal && currModal && this.ref) { + const offset = this.ref.scrollTop; + this.prevScrollOffset = offset; + return null; + } + + if (prevModal && !currModal) { + // the user is closing the modal, return the previous view's scroll offset + return this.prevScrollOffset; + } + + return null; + } + + componentDidUpdate(prevProps, prevState, snapshot) { + /* + If we have a snapshot value, the user has closed a modal and we need + to return the user to where they were previously scrolled in the primary + view + */ + if (snapshot !== null && this.ref) { + this.ref.scrollTop = snapshot; + } + } + + render() { + const { currentUser, history, location } = this.props; + + const isMarketingPage = isViewingMarketingPage(history, currentUser); + const isViewingExplore = location && location.pathname === '/explore'; + const isTwoColumn = isViewingExplore || !isMarketingPage; + + return ( + (this.ref = el)} + isTwoColumn={isTwoColumn} + {...this.props} + /> + ); + } +} + +export default compose( + withRouter, + withCurrentUser +)(AppViewWrapper); diff --git a/src/components/appViewWrapper/style.js b/src/components/appViewWrapper/style.js index 24618f69da..346a038378 100644 --- a/src/components/appViewWrapper/style.js +++ b/src/components/appViewWrapper/style.js @@ -1,18 +1,23 @@ import styled from 'styled-components'; -import { FlexRow } from '../globals'; +import { + MEDIA_BREAK, + TITLEBAR_HEIGHT, + NAVBAR_WIDTH, +} from 'src/components/layout'; -export const Wrapper = styled(FlexRow)` - order: 2; - align-items: flex-start; - justify-content: center; - overflow: hidden; - overflow-y: auto; - flex: auto; +export const StyledAppViewWrapper = styled.div` + display: grid; width: 100%; + grid-template-columns: ${props => + props.isTwoColumn ? `${NAVBAR_WIDTH}px 1fr` : '1fr'}; + grid-template-areas: ${props => + props.isTwoColumn ? "'navigation main'" : "'main'"}; - @media (max-width: 768px) { - padding: 0; - justify-content: flex-start; - flex-direction: column; + @media (max-width: ${MEDIA_BREAK}px) { + grid-template-columns: 1fr; + grid-template-rows: ${props => + props.isTwoColumn ? `${TITLEBAR_HEIGHT}px 1fr` : '1fr'}; + grid-template-areas: ${props => + props.isTwoColumn ? "'titlebar' 'main'" : "'main'"}; } `; diff --git a/src/components/avatar/image.js b/src/components/avatar/image.js index 6c59c4f561..b18ac105b1 100644 --- a/src/components/avatar/image.js +++ b/src/components/avatar/image.js @@ -14,7 +14,7 @@ type Props = { export default class Image extends React.Component { render() { const { type, size, mobilesize } = this.props; - const { isClickable, ...rest } = this.props; + const { ...rest } = this.props; const fallbackSrc = type === 'user' ? '/img/default_avatar.svg' diff --git a/src/components/avatar/style.js b/src/components/avatar/style.js index 76edcd5e9c..aa5451c8b0 100644 --- a/src/components/avatar/style.js +++ b/src/components/avatar/style.js @@ -5,6 +5,7 @@ import ReactImage from 'react-image'; import { zIndex } from '../globals'; import { Link } from 'react-router-dom'; import { ProfileHeaderAction } from '../profile/style'; +import { MEDIA_BREAK } from 'src/components/layout'; export const Container = styled.div` position: relative; @@ -19,7 +20,7 @@ export const Container = styled.div` ${props => props.mobilesize && css` - @media (max-width: 768px) { + @media (max-width: ${MEDIA_BREAK}px) { width: ${props => `${props.mobilesize}px`}; height: ${props => `${props.mobilesize}px`}; } @@ -58,7 +59,7 @@ export const Img = styled(ReactImage)` ${props => props.mobilesize && css` - @media (max-width: 768px) { + @media (max-width: ${MEDIA_BREAK}px) { width: ${props => `${props.mobilesize}px`}; height: ${props => `${props.mobilesize}px`}; } @@ -77,7 +78,7 @@ export const FallbackImg = styled.img` ${props => props.mobilesize && css` - @media (max-width: 768px) { + @media (max-width: ${MEDIA_BREAK}px) { width: ${props => `${props.mobilesize}px`}; height: ${props => `${props.mobilesize}px`}; } @@ -95,7 +96,7 @@ export const LoadingImg = styled.div` ${props => props.mobilesize && css` - @media (max-width: 768px) { + @media (max-width: ${MEDIA_BREAK}px) { width: ${props => `${props.mobilesize}px`}; height: ${props => `${props.mobilesize}px`}; } diff --git a/src/components/badges/index.js b/src/components/badges/index.js index 7df828a71b..c449093791 100644 --- a/src/components/badges/index.js +++ b/src/components/badges/index.js @@ -5,6 +5,7 @@ import { connect } from 'react-redux'; import type { Dispatch } from 'redux'; import { Span, ProBadge, BlockedBadge, PendingBadge, TeamBadge } from './style'; import { withCurrentUser } from 'src/components/withCurrentUser'; +import Tooltip from 'src/components/tooltip'; type Props = { type: string, @@ -21,73 +22,60 @@ class Badge extends React.Component { switch (type) { case 'beta-supporter': return ( - - {label || 'Supporter'} - + + + {label || 'Supporter'} + + ); case 'blocked': return ( - - {label || type} - + + + {label || type} + + ); case 'pending': return ( - - {label || type} - + + + {label || type} + + ); case 'moderator': case 'admin': return ( - - Team - + + Team + + ); case 'bot': return ( - - {label || type} - + + + APP + + ); default: return ( - - {label || type} - + + + {label || type} + + ); } } diff --git a/src/components/badges/style.js b/src/components/badges/style.js index 4332d44bd1..09802d0462 100644 --- a/src/components/badges/style.js +++ b/src/components/badges/style.js @@ -1,10 +1,10 @@ // @flow import theme from 'shared/theme'; import styled from 'styled-components'; -import { Tooltip, Gradient } from '../globals'; +import { Gradient } from 'src/components/globals'; export const Span = styled.span` - display: flex; + display: inline-flex; color: ${theme.text.reverse}; background-color: ${theme.text.alt}; text-transform: uppercase; @@ -13,7 +13,6 @@ export const Span = styled.span` font-size: 9px; font-weight: 800; border-radius: 4px; - ${props => (props.tipText ? Tooltip(props) : '')}; letter-spacing: 0.6px; line-height: 1; text-shadow: 0 1px 1px rgba(0, 0, 0, 0.16); diff --git a/src/components/button/index.js b/src/components/button/index.js new file mode 100644 index 0000000000..b32ed89a43 --- /dev/null +++ b/src/components/button/index.js @@ -0,0 +1,91 @@ +// @flow +import React from 'react'; +import { + A, + StyledLink, + StyledButton, + StyledWhiteIconButton, + StyledWhiteButton, + StyledPrimaryButton, + StyledWarnButton, + StyledOutlineButton, + StyledHoverWarnOutlineButton, + StyledPrimaryOutlineButton, + StyledWhiteOutlineButton, + StyledTextButton, + StyledFacebookButton, + StyledTwitterButton, +} from './style'; + +const handleLinkWrapping = (Component, props) => { + const { href, to, target, children, disabled, isLoading, ...rest } = props; + const button = ( + + {children} + + ); + + if (href) + return ( + + {button} + + ); + if (to) return {button}; + return button; +}; + +type To = { + pathname?: string, + search?: string, + state?: Object, +}; + +type Props = { + target?: string, + href?: string, + to?: string | To, + children: React$Node, + disabled?: boolean, + isLoading?: boolean, + size?: 'small', +}; + +export const Button = (props: Props) => handleLinkWrapping(StyledButton, props); + +export const WhiteIconButton = (props: Props) => + handleLinkWrapping(StyledWhiteIconButton, props); + +export const WhiteButton = (props: Props) => + handleLinkWrapping(StyledWhiteButton, props); + +export const PrimaryButton = (props: Props) => + handleLinkWrapping(StyledPrimaryButton, props); + +export const WarnButton = (props: Props) => + handleLinkWrapping(StyledWarnButton, props); + +export const OutlineButton = (props: Props) => + handleLinkWrapping(StyledOutlineButton, props); + +export const PrimaryOutlineButton = (props: Props) => + handleLinkWrapping(StyledPrimaryOutlineButton, props); + +export const WhiteOutlineButton = (props: Props) => + handleLinkWrapping(StyledWhiteOutlineButton, props); + +export const HoverWarnOutlineButton = (props: Props) => + handleLinkWrapping(StyledHoverWarnOutlineButton, props); + +export const TextButton = (props: Props) => + handleLinkWrapping(StyledTextButton, props); + +export const FacebookButton = (props: Props) => + handleLinkWrapping(StyledFacebookButton, props); + +export const TwitterButton = (props: Props) => + handleLinkWrapping(StyledTwitterButton, props); diff --git a/src/components/button/style.js b/src/components/button/style.js new file mode 100644 index 0000000000..19ea43b7fa --- /dev/null +++ b/src/components/button/style.js @@ -0,0 +1,290 @@ +// @flow +import styled from 'styled-components'; +import theme from 'shared/theme'; +import { Link } from 'react-router-dom'; +import { tint, hexa } from 'src/components/globals'; + +export const A = styled.a` + display: flex; + align-items: center; + flex: none; +`; + +export const StyledLink = styled(Link)` + display: flex; + flex: none; + align-items: center; +`; + +export const StyledButton = styled.button` + font-size: ${props => (props.size === 'small' ? '15px' : '16px')}; + font-weight: 600; + color: ${theme.text.default}; + border-radius: 32px; + padding: ${props => (props.size === 'small' ? '6px 12px' : '10px 16px')}; + background: ${theme.bg.wash}; + display: flex; + flex: none; + align-items: center; + justify-content: center; + cursor: pointer; + -webkit-display: none; + opacity: ${props => (props.disabled ? '0.6' : '1')}; + line-height: 1.2; + transition: box-shadow 0.2s ease-in-out; + + .icon { + margin-right: 4px; + } + + &:hover { + background: ${theme.bg.border}; + } + + &:focus { + box-shadow: 0 0 0 2px ${theme.bg.default}, 0 0 0 4px ${theme.bg.border}; + transition: box-shadow 0.2s ease-in-out; + } + + &:active { + box-shadow: 0 0 0 2px ${theme.bg.default}, + 0 0 0 4px ${tint(theme.bg.border, -24)}; + transition: box-shadow 0.2s ease-in-out; + } +`; + +export const StyledWhiteIconButton = styled(StyledButton)` + background-color: transparent; + padding: 0; + color: ${theme.text.default}; + + .icon { + margin-right: 0; + } +`; + +export const StyledPrimaryButton = styled(StyledButton)` + background-color: ${theme.brand.alt}; + background-image: ${`linear-gradient(to bottom, ${theme.brand.alt}, ${tint( + theme.brand.alt, + -8 + )})`}; + color: ${theme.text.reverse}; + border: 1px solid ${tint(theme.brand.alt, -8)}; + transition: box-shadow 0.2s ease-in-out; + + &:hover { + border: 1px solid ${tint(theme.brand.alt, -16)}; + background: ${tint(theme.brand.alt, -8)}; + color: ${theme.text.reverse}; + } + + &:focus { + transition: box-shadow 0.2s ease-in-out; + box-shadow: 0 0 0 2px ${theme.bg.default}, + 0 0 0 4px ${hexa(theme.brand.alt, 0.24)}; + } + + &:active { + transition: box-shadow 0.2s ease-in-out; + box-shadow: 0 0 0 2px ${theme.bg.default}, + 0 0 0 4px ${hexa(theme.brand.alt, 0.64)}; + } +`; + +export const StyledWarnButton = styled(StyledPrimaryButton)` + background-color: ${theme.warn.default}; + background-image: ${`linear-gradient(to bottom, ${theme.warn.default}, ${tint( + theme.warn.default, + -8 + )})`}; + border: 1px solid ${tint(theme.warn.default, -8)}; + + &:hover { + border: 1px solid ${tint(theme.warn.default, -16)}; + background: ${tint(theme.warn.default, -8)}; + } + + &:focus { + box-shadow: 0 0 0 2px ${theme.bg.default}, + 0 0 0 4px ${hexa(theme.warn.default, 0.24)}; + } + + &:active { + box-shadow: 0 0 0 2px ${theme.bg.default}, + 0 0 0 4px ${hexa(theme.warn.default, 0.64)}; + } +`; + +export const StyledWhiteButton = styled(StyledButton)` + background-color: ${theme.bg.default}; + color: ${theme.text.secondary}; + border: 0; + transition: box-shadow 0.2s ease-in-out; + + &:hover { + border: 0; + background: ${theme.bg.default}; + color: ${theme.text.default}; + } + + &:focus { + transition: box-shadow 0.2s ease-in-out; + box-shadow: 0 0 0 2px ${theme.bg.default}, + 0 0 0 4px ${hexa(theme.bg.default, 0.24)}; + } + + &:active { + transition: box-shadow 0.2s ease-in-out; + box-shadow: 0 0 0 2px ${theme.bg.default}, + 0 0 0 4px ${hexa(theme.bg.default, 0.64)}; + } +`; + +export const StyledOutlineButton = styled(StyledButton)` + background: transparent; + border: 1px solid ${theme.bg.border}; + transition: box-shadow 0.2s ease-in-out; + + &:hover { + background: transparent; + border: 1px solid ${tint(theme.bg.border, -8)}; + } + + &:focus { + box-shadow: 0 0 0 2px ${theme.bg.default}, 0 0 0 4px ${theme.bg.border}; + transition: box-shadow 0.2s ease-in-out; + } + + &:active { + box-shadow: 0 0 0 2px ${theme.bg.default}, + 0 0 0 4px ${tint(theme.bg.border, -24)}; + transition: box-shadow 0.2s ease-in-out; + } +`; + +export const StyledPrimaryOutlineButton = styled(StyledOutlineButton)` + background: transparent; + border: 1px solid ${theme.brand.alt}; + color: ${theme.brand.alt}; + transition: box-shadow 0.2s ease-in-out; + + &:hover { + background: ${hexa(theme.brand.alt, 0.04)}; + border: 1px solid ${tint(theme.brand.alt, -8)}; + color: ${tint(theme.brand.alt, -8)}; + } + + &:focus { + transition: box-shadow 0.2s ease-in-out; + box-shadow: 0 0 0 2px ${theme.bg.default}, + 0 0 0 4px ${hexa(theme.brand.alt, 0.16)}; + } + + &:active { + transition: box-shadow 0.2s ease-in-out; + box-shadow: 0 0 0 2px ${theme.bg.default}, + 0 0 0 4px ${hexa(theme.brand.alt, 0.48)}; + } +`; + +export const StyledWhiteOutlineButton = styled(StyledOutlineButton)` + background: transparent; + border: 1px solid ${theme.bg.default}; + color: ${theme.bg.default}; + transition: box-shadow 0.2s ease-in-out; + + &:hover { + background: ${hexa(theme.bg.default, 0.04)}; + border: 1px solid ${theme.bg.default}; + color: ${theme.bg.default}; + } + + &:focus { + transition: box-shadow 0.2s ease-in-out; + box-shadow: 0 0 0 2px ${theme.bg.default}, + 0 0 0 4px ${hexa(theme.bg.default, 0.16)}; + } + + &:active { + transition: box-shadow 0.2s ease-in-out; + box-shadow: 0 0 0 2px ${theme.bg.default}, + 0 0 0 4px ${hexa(theme.bg.default, 0.48)}; + } +`; + +export const StyledHoverWarnOutlineButton = styled(StyledOutlineButton)` + &:hover { + background: ${theme.warn.default}; + border: 1px solid ${theme.warn.default}; + color: ${theme.text.reverse}; + } + + &:active { + transition: box-shadow 0.2s ease-in-out; + box-shadow: 0 0 0 2px ${theme.bg.default}, + 0 0 0 4px ${hexa(theme.warn.default, 0.48)}; + } +`; + +export const StyledTextButton = styled(StyledOutlineButton)` + border: 0; + + &:hover { + background: transparent; + border: 0; + } + + &:focus { + box-shadow: 0 0 0 2px ${theme.bg.default}, 0 0 0 4px ${theme.bg.border}; + transition: box-shadow 0.2s ease-in-out; + } + + &:active { + box-shadow: 0 0 0 2px ${theme.bg.default}, + 0 0 0 4px ${tint(theme.bg.border, -24)}; + transition: box-shadow 0.2s ease-in-out; + } +`; + +export const StyledFacebookButton = styled(StyledPrimaryButton)` + background-color: ${theme.social.facebook.default}; + background-image: none; + border: 1px solid ${tint(theme.social.facebook.default, -8)}; + + &:hover { + border: 1px solid ${tint(theme.social.facebook.default, -16)}; + background: ${tint(theme.social.facebook.default, -8)}; + } + + &:focus { + box-shadow: 0 0 0 2px ${theme.bg.default}, + 0 0 0 4px ${hexa(theme.social.facebook.default, 0.24)}; + } + + &:active { + box-shadow: 0 0 0 2px ${theme.bg.default}, + 0 0 0 4px ${hexa(theme.social.facebook.default, 0.64)}; + } +`; + +export const StyledTwitterButton = styled(StyledPrimaryButton)` + background-color: ${theme.social.twitter.default}; + background-image: none; + border: 1px solid ${tint(theme.social.twitter.default, -8)}; + + &:hover { + border: 1px solid ${tint(theme.social.twitter.default, -16)}; + background: ${tint(theme.social.twitter.default, -8)}; + } + + &:focus { + box-shadow: 0 0 0 2px ${theme.bg.default}, + 0 0 0 4px ${hexa(theme.social.twitter.default, 0.24)}; + } + + &:active { + box-shadow: 0 0 0 2px ${theme.bg.default}, + 0 0 0 4px ${hexa(theme.social.twitter.default, 0.64)}; + } +`; diff --git a/src/components/buttons/index.js b/src/components/buttons/index.js deleted file mode 100644 index ad2fb4db08..0000000000 --- a/src/components/buttons/index.js +++ /dev/null @@ -1,152 +0,0 @@ -import React from 'react'; -import { - Label, - StyledSolidButton, - StyledTextButton, - StyledIconButton, - StyledOutlineButton, - StyledFauxOutlineButton, - SpinnerContainer, - StyledButtonRow, -} from './style'; -import { Spinner } from '../globals'; -import Icon from '../icons'; - -type ButtonProps = {| - loading?: boolean, - disabled?: boolean, - large?: boolean, - color?: string, - gradientTheme?: string, - icon?: string, - children?: any, - dataCy?: string, -|}; - -type IconProps = { - glyph: string, - color?: string, - hoverColor?: string, - disabled?: boolean, - tipText?: string, - tipLocation?: - | 'top' - | 'top-left' - | 'top-right' - | 'bottom' - | 'bottom-right' - | 'bottom-left' - | 'left' - | 'right', -}; - -export const Button = (props: ButtonProps) => ( - - {props.icon ? ( - props.loading ? ( - - - - ) : ( - - ) - ) : ( - '' - )} - {props.loading && !props.icon && ( - - )} - - -); - -export const OutlineButton = (props: ButtonProps) => ( - - {props.icon ? ( - props.loading ? ( - - - - ) : ( - - ) - ) : ( - '' - )} - {props.loading && !props.icon && ( - - )} - - -); - -// looks like a button, but isn't a button so it won't submit forms -export const FauxOutlineButton = (props: ButtonProps) => ( - - {props.icon ? ( - props.loading ? ( - - - - ) : ( - - ) - ) : ( - '' - )} - {props.loading && !props.icon && ( - - )} - - -); - -export const TextButton = (props: ButtonProps) => ( - - {props.icon ? ( - props.loading ? ( - - - - ) : ( - - ) - ) : ( - '' - )} - {props.loading && !props.icon && ( - - )} - - -); - -export const IconButton = (props: IconProps) => ( - - - -); - -export const ButtonRow = props => ( - {props.children} -); diff --git a/src/components/buttons/style.js b/src/components/buttons/style.js deleted file mode 100644 index 86f5e31ebe..0000000000 --- a/src/components/buttons/style.js +++ /dev/null @@ -1,227 +0,0 @@ -// @flow -import theme from 'shared/theme'; -/* eslint no-eval: 0 */ -// $FlowFixMe -import styled, { css } from 'styled-components'; -import { Gradient, Shadow, Transition, hexa } from '../globals'; - -const baseButton = css` - display: flex; - flex: none; - align-self: center; - align-items: center; - justify-content: center; - border-radius: 8px; - font-weight: 600; - white-space: nowrap; - word-break: keep-all; - transition: ${Transition.hover.off}; - cursor: pointer; - font-size: ${props => (props.large ? '18px' : '14px')}; - line-height: 1; - position: relative; - text-align: center; - padding: ${props => - props.icon - ? props.large - ? '8px 12px' - : '4px 8px' - : props.large - ? '16px 32px' - : '12px 16px'}; - - &:hover { - transition: ${Transition.hover.on}; - box-shadow: ${props => - props.disabled - ? 'none' - : `${Shadow.high} ${hexa(props.theme.bg.reverse, 0.15)}`}; - opacity: ${props => (props.disabled ? '0.5' : '1')}; - } - - /* if an icon and label are both present, add space around the label*/ - div + span, - span + span { - margin: 0 8px; - } -`; - -export const Label = styled.span` - display: block; - flex: 0 0 auto; - line-height: inherit; - color: inherit; - ${props => (props.loading && !props.hasIcon ? 'opacity: 0;' : 'opacity: 1;')}; - align-self: center; - margin: auto; -`; - -export const StyledSolidButton = styled.button` - ${baseButton} background-color: ${props => - props.disabled - ? props.theme.bg.inactive - : eval(`props.theme.${props.color ? props.color : `brand.alt`}`)}; - background-image: ${props => - props.disabled || props.gradientTheme === 'none' - ? 'none' - : props.gradientTheme - ? Gradient( - eval(`props.theme.${props.gradientTheme}.alt`), - eval(`props.theme.${props.gradientTheme}.default`) - ) - : Gradient(props.theme.brand.alt, props.theme.brand.default)}; - color: ${theme.text.reverse}; - - &:hover { - background-color: ${props => - props.disabled - ? props.theme.bg.inactive - : eval( - `props.theme.${props.hoverColor ? props.hoverColor : 'brand.alt'}` - )}; - } - - &:active { - box-shadow: ${props => - props.disabled - ? 'none' - : `${Shadow.low} ${hexa(props.theme.bg.reverse, 0.15)}`}; - } -`; - -export const StyledTextButton = styled(StyledSolidButton)` - background: transparent; - background-image: none; - font-weight: 600; - color: ${props => - props.disabled - ? props.theme.bg.inactive - : eval(`props.theme.${props.color ? props.color : 'text.alt'}`)}; - transition: color 0.1s ease-out, box-shadow 0.2s ease-out 0.1s, border-radius 0.2s ease-out, padding: 0.2s ease-out; - ${props => - props.loading - ? css` - justify-content: center; - ` - : css` - justify-content: flex-start; - `} - - &:hover { - background-color: transparent; - box-shadow: none; - color: ${props => - props.disabled - ? props.theme.bg.inactive - : eval( - `props.theme.${props.hoverColor ? props.hoverColor : 'brand.alt'}` - )}; - transition: color 0.1s ease-in, box-shadow 0.2s ease-in 0.1s, padding 0.2s ease-in; - } -`; - -export const StyledOutlineButton = styled(StyledTextButton)` - box-shadow: inset 0 0 0 1px - ${props => - props.disabled - ? props.theme.bg.inactive - : eval(`props.theme.${props.color ? props.color : 'brand.default'}`)}; - color: ${props => - props.disabled - ? props.theme.bg.inactive - : eval(`props.theme.${props.color ? props.color : 'brand.default'}`)}; - transition: ${Transition.hover.on}; - - &:hover { - background-color: transparent; - color: ${props => - props.disabled - ? props.theme.bg.inactive - : eval( - `props.theme.${props.hoverColor ? props.hoverColor : 'brand.alt'}` - )}; - box-shadow: inset 0 0 0 1px - ${props => - props.disabled - ? props.theme.bg.inactive - : eval( - `props.theme.${props.hoverColor ? props.hoverColor : 'brand.alt'}` - )}; - transition: ${Transition.hover.on}; - } -`; - -export const StyledFauxOutlineButton = styled.span` - ${baseButton} box-shadow: inset 0 0 0 1px ${props => - props.disabled - ? props.theme.bg.inactive - : eval(`props.theme.${props.color ? props.color : 'brand.default'}`)}; - color: ${props => - props.disabled - ? props.theme.bg.inactive - : eval(`props.theme.${props.color ? props.color : 'brand.default'}`)}; - transition: ${Transition.hover.on}; - - &:hover { - background-color: transparent; - color: ${props => - props.disabled - ? props.theme.bg.inactive - : eval( - `props.theme.${props.hoverColor ? props.hoverColor : 'brand.alt'}` - )}; - box-shadow: inset 0 0 0 1px - ${props => - props.disabled - ? props.theme.bg.inactive - : eval( - `props.theme.${props.hoverColor ? props.hoverColor : 'brand.alt'}` - )}; - transition: ${Transition.hover.on}; - } -`; - -export const StyledIconButton = styled.button` - ${baseButton} padding: 0; - width: 32px; - height: 32px; - background-color: transparent; - color: ${props => - props.disabled - ? props.theme.bg.inactive - : props.color - ? eval(`props.theme.${props.color}`) - : props.theme.text.alt}; - opacity: ${props => (props.opacity ? props.opacity : 1)}; - - &:hover { - color: ${props => - props.disabled - ? props.theme.bg.inactive - : props.hoverColor - ? eval(`props.theme.${props.hoverColor}`) - : props.color - ? eval(`props.theme.${props.color}`) - : props.theme.brand.alt}; - transform: ${props => (props.disabled ? 'none' : 'scale(1.05)')}; - box-shadow: none; - opacity: 1; - } -`; - -export const SpinnerContainer = styled.div` - width: 32px; - height: 32px; - position: relative; -`; - -export const StyledButtonRow = styled.div` - display: flex; - justify-content: center; - flex-wrap: wrap; - align-items: center; - > button, - > a { - margin: 8px; - } -`; diff --git a/src/components/card/index.js b/src/components/card/index.js index 13cd4c38b2..0bf78a7cd4 100644 --- a/src/components/card/index.js +++ b/src/components/card/index.js @@ -1,11 +1,10 @@ // @flow import theme from 'shared/theme'; import React from 'react'; -// $FlowFixMe import compose from 'recompose/compose'; -// $FlowFixMe import styled from 'styled-components'; import { FlexCol } from '../globals'; +import { MEDIA_BREAK } from 'src/components/layout'; const StyledCard = styled(FlexCol)` background: ${theme.bg.default}; @@ -16,16 +15,7 @@ const StyledCard = styled(FlexCol)` overflow: visible; flex: none; - + div, - + span { - margin-top: 16px; - - @media (max-width: 768px) { - margin-top: 2px; - } - } - - @media (max-width: 768px) { + @media (max-width: ${MEDIA_BREAK}px) { border-radius: 0; box-shadow: none; } diff --git a/src/components/chatInput/components/mediaUploader.js b/src/components/chatInput/components/mediaUploader.js index 570b992ce2..1cc4b12eff 100644 --- a/src/components/chatInput/components/mediaUploader.js +++ b/src/components/chatInput/components/mediaUploader.js @@ -1,12 +1,13 @@ // @flow import * as React from 'react'; -import { MediaLabel, MediaInput, Form } from './style'; -import Icon from 'src/components/icons'; +import Tooltip from 'src/components/tooltip'; +import Icon from 'src/components/icon'; import { Loading } from 'src/components/loading'; import { PRO_USER_MAX_IMAGE_SIZE_STRING, PRO_USER_MAX_IMAGE_SIZE_BYTES, } from 'src/helpers/images'; +import { MediaLabel, MediaInput, Form } from './style'; type Props = { onValidated: Function, @@ -107,22 +108,20 @@ class MediaUploader extends React.Component { return (
e.preventDefault()} - innerRef={c => (this.form = c)} + ref={c => (this.form = c)} data-cy="chat-input-media-uploader" > - - - - + + + + + +
); } diff --git a/src/components/chatInput/index.js b/src/components/chatInput/index.js index 9fd9fec303..a87cc7a892 100644 --- a/src/components/chatInput/index.js +++ b/src/components/chatInput/index.js @@ -2,7 +2,7 @@ import * as React from 'react'; import compose from 'recompose/compose'; import { connect } from 'react-redux'; -import Icon from 'src/components/icons'; +import Icon from 'src/components/icon'; import { addToastWithTimeout } from 'src/actions/toasts'; import { openModal } from 'src/actions/modals'; import { replyToMessage } from 'src/actions/message'; @@ -13,11 +13,11 @@ import { ChatInputWrapper, Input, InputWrapper, - SendButton, PhotoSizeError, PreviewWrapper, RemovePreviewButton, } from './style'; +import { PrimaryButton } from 'src/components/button'; import sendMessage from 'shared/graphql/mutations/message/sendMessage'; import sendDirectMessage from 'shared/graphql/mutations/message/sendDirectMessage'; import { getMessageById } from 'shared/graphql/queries/message/getMessage'; @@ -25,6 +25,8 @@ import MediaUploader from './components/mediaUploader'; import { QuotedMessage as QuotedMessageComponent } from '../message/view'; import type { Dispatch } from 'redux'; import { MarkdownHint } from 'src/components/markdownHint'; +import { useAppScroller } from 'src/hooks/useAppScroller'; +import { MEDIA_BREAK } from 'src/components/layout'; const QuotedMessage = connect()( getMessageById(props => { @@ -56,13 +58,11 @@ type Props = { createThread: Function, sendMessage: Function, sendDirectMessage: Function, - forceScrollToBottom: Function, threadType: string, - thread: string, + threadId: string, clear: Function, websocketConnection: string, networkOnline: boolean, - threadData?: Object, refetchThread?: Function, quotedMessage: ?{ messageId: string, threadId: string }, // used to pre-populate the @mention suggestions with participants and the author of the thread @@ -81,43 +81,36 @@ export const cleanSuggestionUserObject = (user: ?Object) => { }; }; -// $FlowFixMe const ChatInput = (props: Props) => { - const cacheKey = `last-content-${props.thread}`; - // $FlowFixMe + const cacheKey = `last-content-${props.threadId}`; const [text, changeText] = React.useState(''); - // $FlowFixMe const [photoSizeError, setPhotoSizeError] = React.useState(''); - // $FlowFixMe const [inputRef, setInputRef] = React.useState(null); + const { scrollToBottom } = useAppScroller(); // On mount, set the text state to the cached value if one exists // $FlowFixMe - React.useEffect( - () => { - changeText(localStorage.getItem(cacheKey) || ''); - // NOTE(@mxstbr): We ONLY want to run this if we switch between threads, never else! - }, - [props.thread] - ); + React.useEffect(() => { + changeText(localStorage.getItem(cacheKey) || ''); + // NOTE(@mxstbr): We ONLY want to run this if we switch between threads, never else! + }, [props.threadId]); // Cache the latest text everytime it changes // $FlowFixMe - React.useEffect( - () => { - localStorage.setItem(cacheKey, text); - }, - [text] - ); + React.useEffect(() => { + localStorage.setItem(cacheKey, text); + }, [text]); // Focus chatInput when quoted message changes // $FlowFixMe - React.useEffect( - () => { - if (inputRef) inputRef.focus(); - }, - [props.quotedMessage && props.quotedMessage.messageId] - ); + React.useEffect(() => { + if (inputRef) inputRef.focus(); + }, [props.quotedMessage && props.quotedMessage.messageId]); + + React.useEffect(() => { + // autofocus the chat input on desktop + if (inputRef && window && window.innerWidth > MEDIA_BREAK) inputRef.focus(); + }, [inputRef]); const removeAttachments = () => { removeQuotedMessage(); @@ -157,7 +150,7 @@ const ChatInput = (props: Props) => { // user is creating a new directMessageThread, break the chain // and initiate a new group creation with the message being sent // in views/directMessages/containers/newThread.js - if (props.thread === 'newDirectMessageThread') { + if (props.threadId === 'newDirectMessageThread') { return props.createThread({ messageType: file ? 'media' : 'text', file, @@ -170,7 +163,7 @@ const ChatInput = (props: Props) => { ? props.sendMessage : props.sendDirectMessage; return method({ - threadId: props.thread, + threadId: props.threadId, messageType: file ? 'media' : 'text', threadType: props.threadType, parentId: props.quotedMessage, @@ -207,15 +200,14 @@ const ChatInput = (props: Props) => { if (!props.currentUser) { // user is trying to send a message without being signed in - return props.dispatch(openModal('CHAT_INPUT_LOGIN_MODAL', {})); + return props.dispatch(openModal('LOGIN_MODAL', {})); } - // If a user sends a message, force a scroll to bottom. This doesn't exist if this is a new DM thread - if (props.forceScrollToBottom) props.forceScrollToBottom(); + scrollToBottom(); if (mediaFile) { setIsSendingMediaMessage(true); - if (props.forceScrollToBottom) props.forceScrollToBottom(); + scrollToBottom(); await sendMessage({ file: mediaFile, body: '{"blocks":[],"entityMap":{}}', @@ -236,17 +228,16 @@ const ChatInput = (props: Props) => { // workaround react-mentions bug by replacing @[username] with @username // @see withspectrum/spectrum#4587 sendMessage({ body: text.replace(/@\[([a-z0-9_-]+)\]/g, '@$1') }) - .then(() => { - // If we're viewing a thread and the user sends a message as a non-member, we need to refetch the thread data - if ( - props.threadType === 'story' && - props.threadData && - !props.threadData.channel.channelPermissions.isMember && - props.refetchThread - ) { - return props.refetchThread(); - } - }) + // .then(() => { + // // If we're viewing a thread and the user sends a message as a non-member, we need to refetch the thread data + // if ( + // props.threadType === 'story' && + // props.threadId && + // props.refetchThread + // ) { + // return props.refetchThread(); + // } + // }) .catch(err => { // props.dispatch(addToastWithTimeout('error', err.message)); }); @@ -254,6 +245,7 @@ const ChatInput = (props: Props) => { // Clear the chat input now that we're sending a message for sure onChange({ target: { value: '' } }); removeQuotedMessage(); + inputRef && inputRef.focus(); }; // $FlowFixMe @@ -285,7 +277,7 @@ const ChatInput = (props: Props) => { const removeQuotedMessage = () => { if (props.quotedMessage) props.dispatch( - replyToMessage({ threadId: props.thread, messageId: null }) + replyToMessage({ threadId: props.threadId, messageId: null }) ); }; @@ -293,6 +285,7 @@ const ChatInput = (props: Props) => { !props.networkOnline || (props.websocketConnection !== 'connected' && props.websocketConnection !== 'reconnected'); + return ( @@ -333,7 +326,7 @@ const ChatInput = (props: Props) => { { value={text} onFocus={props.onFocus} onBlur={props.onBlur} + autoFocus={false} onChange={onChange} onKeyDown={handleKeyPress} inputRef={node => { @@ -359,12 +353,13 @@ const ChatInput = (props: Props) => { staticSuggestions={props.participants} /> - + style={{ flex: 'none', marginLeft: '8px' }} + > + Send + @@ -376,7 +371,7 @@ const ChatInput = (props: Props) => { const map = (state, ownProps) => ({ websocketConnection: state.connectionStatus.websocketConnection, networkOnline: state.connectionStatus.networkOnline, - quotedMessage: state.message.quotedMessage[ownProps.thread] || null, + quotedMessage: state.message.quotedMessage[ownProps.threadId] || null, }); export default compose( diff --git a/src/components/chatInput/style.js b/src/components/chatInput/style.js index 54435a08d0..167d3547c9 100644 --- a/src/components/chatInput/style.js +++ b/src/components/chatInput/style.js @@ -2,15 +2,9 @@ import theme from 'shared/theme'; import styled, { css } from 'styled-components'; import MentionsInput from '../mentionsInput'; -import { IconButton } from '../buttons'; import { QuoteWrapper } from '../message/style'; -import { - FlexRow, - hexa, - Transition, - zIndex, - monoStack, -} from 'src/components/globals'; +import { MEDIA_BREAK } from 'src/components/layout'; +import { FlexRow, hexa, zIndex, monoStack } from 'src/components/globals'; export const ChatInputContainer = styled(FlexRow)` flex: none; @@ -37,7 +31,7 @@ export const ChatInputWrapper = styled.div` border-top: 1px solid ${theme.bg.border}; box-shadow: -1px 0 0 ${theme.bg.border}, 1px 0 0 ${theme.bg.border}; - @media (max-width: 768px) { + @media (max-width: ${MEDIA_BREAK}px) { bottom: ${props => (props.focus ? '0' : 'auto')}; position: relative; z-index: ${zIndex.mobileInput}; @@ -91,7 +85,7 @@ export const InputWrapper = styled.div` transition: border-color 0.2s ease-in; } - @media (max-width: 768px) { + @media (max-width: ${MEDIA_BREAK}px) { padding-left: 16px; } `; @@ -103,13 +97,13 @@ export const Input = styled(MentionsInput).attrs({ autoComplete: 'on', autoCorrect: 'on', })` - font-size: 15px; + font-size: 16px; /* has to be 16px to avoid zoom on iOS */ font-weight: 400; line-height: 1.4; background: ${props => props.networkDisabled ? 'none' : props.theme.bg.default}; - @media (max-width: 768px) { + @media (max-width: ${MEDIA_BREAK}px) { font-size: 16px; } @@ -166,16 +160,6 @@ export const Input = styled(MentionsInput).attrs({ `}; `; -export const SendButton = styled(IconButton)` - height: 32px; - width: 32px; - bottom: 4px; - margin-left: 4px; - background-color: transparent; - transition: ${Transition.hover.off}; - align-self: flex-end; -`; - export const MediaInput = styled.input` width: 0.1px; height: 0.1px; @@ -202,17 +186,6 @@ export const MediaLabel = styled.label` } `; -export const EmojiToggle = styled(IconButton)` - position: absolute; - left: 56px; - background-color: transparent; - top: calc(50% - 16px); - - @media (max-width: 768px) { - display: none; - } -`; - export const PhotoSizeError = styled.div` display: flex; justify-content: space-between; diff --git a/src/components/column/index.js b/src/components/column/index.js index 635e68669f..c191a1c177 100644 --- a/src/components/column/index.js +++ b/src/components/column/index.js @@ -1,14 +1,14 @@ // @flow import React from 'react'; -// $FlowFixMe import styled, { css } from 'styled-components'; -import { FlexCol } from '../globals'; +import { FlexCol } from 'src/components/globals'; +import { MEDIA_BREAK } from 'src/components/layout'; const BaseColumn = styled(FlexCol)` margin: 32px 16px; align-items: stretch; - @media (max-width: 768px) { + @media (max-width: ${MEDIA_BREAK}px) { margin: 0; max-width: 100%; } @@ -16,7 +16,7 @@ const BaseColumn = styled(FlexCol)` ${p => p.hideOnMobile && css` - @media screen and (max-width: 768px) { + @media screen and (max-width: ${MEDIA_BREAK}px) { display: none; } `}; @@ -27,7 +27,7 @@ const PrimaryColumn = styled(BaseColumn)` flex: 2 1 60%; max-width: 640px; - @media (max-width: 768px) { + @media (max-width: ${MEDIA_BREAK}px) { width: 100%; max-width: 100%; margin-top: 2px; @@ -40,7 +40,7 @@ const SecondaryColumn = styled(BaseColumn)` flex: 1 1 30%; max-width: 320px; - @media (max-width: 768px) { + @media (max-width: ${MEDIA_BREAK}px) { flex: none; align-self: stretch; max-width: 100%; @@ -51,7 +51,7 @@ const OnlyColumn = styled(PrimaryColumn)` max-width: 840px; flex: 0 0 75%; - @media (max-width: 768px) { + @media (max-width: ${MEDIA_BREAK}px) { flex: 1; min-width: 100%; width: 100%; diff --git a/src/components/composer/LocationSelectors/ChannelSelector.js b/src/components/composer/LocationSelectors/ChannelSelector.js index 3df30294a1..85c5206420 100644 --- a/src/components/composer/LocationSelectors/ChannelSelector.js +++ b/src/components/composer/LocationSelectors/ChannelSelector.js @@ -8,7 +8,7 @@ import getCommunityChannelConnection, { type GetCommunityChannelConnectionType, } from 'shared/graphql/queries/community/getCommunityChannelConnection'; import { LoadingSelect, ErrorSelect } from 'src/components/loading'; -import Icon from 'src/components/icons'; +import Icon from 'src/components/icon'; import { sortChannels } from '../utils'; import { RequiredSelector, ChannelPreview } from '../style'; diff --git a/src/components/composer/index.js b/src/components/composer/index.js index 481bd386c7..276488c8fd 100644 --- a/src/components/composer/index.js +++ b/src/components/composer/index.js @@ -4,24 +4,24 @@ import compose from 'recompose/compose'; import { withRouter, type History, type Location } from 'react-router'; import { connect } from 'react-redux'; import debounce from 'debounce'; -import queryString from 'query-string'; -import Icon from '../icons'; +import Icon from 'src/components/icon'; import getThreadLink from 'src/helpers/get-thread-link'; -import { changeActiveThread } from 'src/actions/dashboardFeed'; import { addToastWithTimeout } from 'src/actions/toasts'; import getComposerCommunitiesAndChannels from 'shared/graphql/queries/composer/getComposerCommunitiesAndChannels'; import type { GetComposerType } from 'shared/graphql/queries/composer/getComposerCommunitiesAndChannels'; import publishThread from 'shared/graphql/mutations/thread/publishThread'; +import { setTitlebarProps } from 'src/actions/titlebar'; import uploadImage, { type UploadImageInput, type UploadImageType, } from 'shared/graphql/mutations/uploadImage'; -import { TextButton, Button } from '../buttons'; +import { TextButton } from 'src/components/button'; +import { PrimaryButton } from 'src/components/button'; +import Tooltip from 'src/components/tooltip'; import { MediaLabel, MediaInput, } from 'src/components/chatInput/components/style'; -import Titlebar from 'src/views/titlebar'; import type { Dispatch } from 'redux'; import { Overlay, @@ -59,11 +59,11 @@ type Props = { publishThread: Function, history: History, location: Location, - isInbox: boolean, websocketConnection: string, networkOnline: boolean, isEditing: boolean, - slider?: boolean, + isModal?: boolean, + previousLocation?: Location, }; const LS_BODY_KEY = 'last-plaintext-thread-composer-body'; @@ -143,6 +143,13 @@ class ComposerWithData extends React.Component { } componentDidMount() { + const { dispatch } = this.props; + dispatch( + setTitlebarProps({ + title: 'New post', + }) + ); + track(events.THREAD_CREATED_INITED); // $FlowIssue document.addEventListener('keydown', this.handleGlobalKeyPress, false); @@ -167,7 +174,6 @@ class ComposerWithData extends React.Component { if (esc) { e.stopPropagation(); this.closeComposer(); - this.activateLastThread(); return; } }; @@ -177,18 +183,9 @@ class ComposerWithData extends React.Component { if (cmdEnter) return this.publishThread(); }; - activateLastThread = () => { - // we get the last thread id from the query params and dispatch it - // as the active thread. - const { location } = this.props; - const { t: threadId } = queryString.parse(location.search); - - this.props.dispatch(changeActiveThread(threadId)); - }; - changeTitle = e => { const title = e.target.value; - this.persistTitleToLocalStorageWithDebounce(title); + this.persistTitleToLocalStorageWithDebounce(); if (/\n$/g.test(title)) { this.bodyEditor.focus && this.bodyEditor.focus(); return; @@ -200,15 +197,15 @@ class ComposerWithData extends React.Component { changeBody = evt => { const body = evt.target.value; - this.persistBodyToLocalStorageWithDebounce(body); + this.persistBodyToLocalStorageWithDebounce(); this.setState({ body, }); }; closeComposer = (clear?: string) => { - this.persistBodyToLocalStorage(this.state.body); - this.persistTitleToLocalStorage(this.state.title); + this.persistBodyToLocalStorage(); + this.persistTitleToLocalStorage(); // 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 @@ -216,8 +213,13 @@ class ComposerWithData extends React.Component { this.clearEditorStateAfterPublish(); } - this.props.history.goBack(); - return; + if (this.props.previousLocation) + return this.props.history.push({ + ...this.props.previousLocation, + state: { modal: false }, + }); + + return this.props.history.goBack({ state: { modal: false } }); }; clearEditorStateAfterPublish = () => { @@ -237,22 +239,22 @@ class ComposerWithData extends React.Component { localStorage.setItem(LS_COMPOSER_EXPIRE, ONE_DAY()); }; - persistBodyToLocalStorageWithDebounce = body => { + persistBodyToLocalStorageWithDebounce = () => { if (!localStorage) return; this.handleTitleBodyChange('body'); }; - persistTitleToLocalStorageWithDebounce = title => { + persistTitleToLocalStorageWithDebounce = () => { if (!localStorage) return; this.handleTitleBodyChange('title'); }; - persistTitleToLocalStorage = title => { + persistTitleToLocalStorage = () => { if (!localStorage) return; this.handleTitleBodyChange('title'); }; - persistBodyToLocalStorage = body => { + persistBodyToLocalStorage = () => { if (!localStorage) return; this.handleTitleBodyChange('body'); }; @@ -382,17 +384,14 @@ class ComposerWithData extends React.Component { }; // one last save to localstorage - this.persistBodyToLocalStorage(this.state.body); - this.persistTitleToLocalStorage(this.state.title); + this.persistBodyToLocalStorage(); + this.persistTitleToLocalStorage(); this.props .publishThread(thread) // after the mutation occurs, it will either return an error or the new // thread that was published .then(({ data }) => { - // get the thread id to redirect the user - const id = data.publishThread.id; - this.clearEditorStateAfterPublish(); // stop the loading spinner on the publish button @@ -406,14 +405,10 @@ class ComposerWithData extends React.Component { this.props.dispatch( addToastWithTimeout('success', 'Thread published!') ); - if (this.props.isInbox) { - this.props.history.replace(`/?t=${id}`); - this.props.dispatch(changeActiveThread(id)); - } else if (this.props.location.pathname === '/new/thread') { + if (this.props.location.pathname === '/new/thread') { this.props.history.replace(getThreadLink(data.publishThread)); } else { this.props.history.push(getThreadLink(data.publishThread)); - this.props.dispatch(changeActiveThread(null)); } return; }) @@ -445,7 +440,7 @@ class ComposerWithData extends React.Component { networkOnline, websocketConnection, isEditing, - slider, + isModal, } = this.props; const networkDisabled = @@ -456,14 +451,12 @@ class ComposerWithData extends React.Component { return ( - - - + { )} - - - - - - - + + + + + + + + + + + Cancel - + {isLoading ? 'Publishing...' : 'Publish'} + @@ -554,8 +541,8 @@ const mapStateToProps = state => ({ export default compose( uploadImage, - getComposerCommunitiesAndChannels, // query to get data - publishThread, // mutation to publish a thread - withRouter, // needed to use history.push() as a post-publish action + getComposerCommunitiesAndChannels, + publishThread, + withRouter, connect(mapStateToProps) )(ComposerWithData); diff --git a/src/components/composer/inputs.js b/src/components/composer/inputs.js index 6c54c35309..ca2e8606d9 100644 --- a/src/components/composer/inputs.js +++ b/src/components/composer/inputs.js @@ -77,12 +77,13 @@ export default (props: Props) => { right: '0', zIndex: '9999', background: '#FFF', + minHeight: '52px', }} > - onClick(false)}> + onClick(false)}> Write - onClick(true)}> + onClick(true)}> Preview @@ -107,7 +108,7 @@ export default (props: Props) => { {({ getRootProps, getInputProps, isDragActive }) => ( diff --git a/src/components/composer/style.js b/src/components/composer/style.js index 7ba413fb3d..8857b525e1 100644 --- a/src/components/composer/style.js +++ b/src/components/composer/style.js @@ -1,8 +1,9 @@ import React from 'react'; import styled, { css } from 'styled-components'; import theme from 'shared/theme'; -import Icon from '../icons'; -import { hexa, Shadow, FlexRow, FlexCol, zIndex } from '../globals'; +import Icon from 'src/components/icon'; +import { hexa, FlexRow, FlexCol, zIndex } from '../globals'; +import { MAX_WIDTH, MEDIA_BREAK, TITLEBAR_HEIGHT } from 'src/components/layout'; export const DropzoneWrapper = styled.div` position: sticky; @@ -23,10 +24,14 @@ export const DropImageOverlay = (props: { visible: boolean }) => { }; export const Wrapper = styled.div` - position: absolute; - width: 100vw; - height: 100vh; - bottom: 0; + grid-area: main; + display: flex; + justify-content: center; + z-index: 9995; + + @media (max-width: ${MEDIA_BREAK}px) { + height: calc(100vh - ${TITLEBAR_HEIGHT}px); + } `; export const DropImageOverlayWrapper = styled.div` @@ -57,89 +62,65 @@ export const DropImageOverlayWrapper = styled.div` `; export const Overlay = styled.div` - ${props => - props.slider && - css` - position: fixed; - top: 0; - left: 0; - right: 0; - bottom: 0; - width: 100%; - height: 100%; - z-index: 3000; - background: #000; - pointer-events: auto; - opacity: 0.4; - `} + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.24); + z-index: 9998; `; export const Container = styled(FlexCol)` - background-color: ${theme.bg.default}; - display: grid; - grid-template-rows: 50px 1fr 64px; - grid-template-columns: 100%; - grid-template-areas: 'header' 'body' 'footer'; - align-self: stretch; - flex: auto; - overflow: hidden; - height: 100vh; - position: relative; - z-index: ${zIndex.composer}; - - ${props => - props.slider && - css` - right: 0; - position: absolute; - width: 650px; - top: 0; - height: 100vh; - `} - - @media (max-width: 768px) { - grid-template-rows: 48px 64px 1fr 64px; - grid-template-areas: 'title' 'header' 'body' 'footer'; - max-width: 100vw; - width: 100%; - height: 100vh; + display: flex; + height: 100%; + max-height: 100vh; + width: 100%; + max-width: ${MAX_WIDTH + 32}px; + background: ${theme.bg.wash}; + height: calc(100vh); + z-index: 9998; + box-shadow: -4px 0 12px rgba(0, 0, 0, 0.08), 4px 0 12px rgba(0, 0, 0, 0.08); + + @media (max-width: ${MEDIA_BREAK}px) { + max-width: 100%; + max-height: calc(100vh - ${TITLEBAR_HEIGHT}px); + padding: 0; + box-shadow: 0; } `; export const DesktopLink = styled.a` display: flex; - @media (max-width: 768px) { + @media (max-width: ${MEDIA_BREAK}px) { display: none; } `; export const ButtonRow = styled(FlexRow)` - @media (max-width: 768px) { + @media (max-width: ${MEDIA_BREAK}px) { justify-content: flex-end; } `; -export const Actions = styled(FlexCol)` +export const Actions = styled.div` background: ${theme.bg.wash}; - border-top: 2px solid ${theme.bg.border}; + border-top: 1px solid ${theme.bg.border}; padding: 8px 16px; - border-radius: 0; align-self: stretch; - display: flex; - flex-direction: row; justify-content: space-between; align-items: center; - position: relative; - grid-area: footer; + position: sticky; + bottom: 0; + display: flex; + flex: 1 0 auto; + height: 56px; + max-height: 56px; - @media (max-width: 768px) { + @media (max-width: ${MEDIA_BREAK}px) { padding: 8px; z-index: ${zIndex.chrome + 1}; - border-radius: 0; - border: 0; - background: ${theme.bg.wash}; - border-top: 1px solid ${theme.bg.border}; > ${ButtonRow} { width: 100%; @@ -163,15 +144,19 @@ export const InputHints = styled(FlexRow)` export const Dropdowns = styled(FlexRow)` display: flex; + flex: 1 0 auto; + height: 48px; + max-height: 48px; align-items: center; - grid-area: header; background-color: ${theme.bg.wash}; - box-shadow: ${Shadow.low} ${props => hexa(props.theme.bg.reverse, 0.15)}; + border-bottom: 1px solid ${theme.bg.border}; z-index: 9999; - grid-area: header; border-bottom: 1px solid ${theme.bg.border}; + padding: 8px; + padding-left: 12px; + font-size: 16px; - @media (max-width: 768px) { + @media (max-width: ${MEDIA_BREAK}px) { width: 100%; justify-content: flex-start; } @@ -181,13 +166,12 @@ export const DropdownsLabel = styled.span` font-size: 14px; font-weight: 500; color: ${theme.text.secondary}; - margin-left: 16px; line-height: 1; vertical-align: middle; position: relative; top: 1px; - @media (max-width: 768px) { + @media (max-width: ${MEDIA_BREAK}px) { display: none; } `; @@ -201,6 +185,10 @@ export const CommunityPreview = styled.div` display: flex; align-items: center; position: relative; + + @media (max-width: ${MEDIA_BREAK}px) { + margin-left: 0; + } `; export const ChannelPreview = styled(CommunityPreview)` @@ -222,9 +210,10 @@ const Selector = styled.select` font-weight: 500; font-size: 14px; - @media (max-width: 768px) { + @media (max-width: ${MEDIA_BREAK}px) { flex: auto; max-width: calc(50% - 12px); + font-size: 16px; /* has to be 16px to avoid zoom on iOS */ } `; @@ -256,7 +245,7 @@ export const ThreadInputs = styled(FlexCol)` z-index: ${zIndex.composer}; height: 100%; - @media (max-width: 768px) { + @media (max-width: ${MEDIA_BREAK}px) { padding: 24px; } `; @@ -272,6 +261,7 @@ export const ThreadTitle = { width: '100%', color: '#16171A', whiteSpace: 'pre-wrap', + wordBreak: 'break-word', minHeight: '34px', flex: 'none', display: 'inline-block', @@ -280,7 +270,7 @@ export const ThreadTitle = { }; export const ThreadDescription = { - fontSize: '16px', + fontSize: '16px', // has to be 16px to avoid zoom on iOS fontWeight: '400', width: '100%', display: 'inline-block', @@ -290,11 +280,12 @@ export const ThreadDescription = { boxShadow: 'none', color: '#16171A', whiteSpace: 'pre-wrap', + wordBreak: 'break-word', overflowY: 'scroll', position: 'relative', // NOTE(@mxstbr): Magic value to make the margin between // the thread title and body match the preview - marginTop: '12px', + marginTop: '9px', }; export const DisabledWarning = styled.div` @@ -313,15 +304,11 @@ export const DisabledWarning = styled.div` export const RenderWrapper = styled.div``; export const InputsGrid = styled.div` - display: grid; - grid-template-rows: 48px 1fr; - grid-area: body; + display: flex; + flex-direction: column; + flex-grow: 1; overflow: hidden; - overflow-y: scroll; - - ${props => - props.isEditing && - css` - height: 100%; - `} + overflow-y: auto; + background: ${theme.bg.default}; + padding-bottom: 48px; `; diff --git a/src/views/dashboard/components/desktopAppUpsell/index.js b/src/components/desktopAppUpsell/index.js similarity index 94% rename from src/views/dashboard/components/desktopAppUpsell/index.js rename to src/components/desktopAppUpsell/index.js index c082a86a6c..8a01db3ce5 100644 --- a/src/views/dashboard/components/desktopAppUpsell/index.js +++ b/src/components/desktopAppUpsell/index.js @@ -1,6 +1,6 @@ // @flow import * as React from 'react'; -import Icon from 'src/components/icons'; +import Icon from 'src/components/icon'; import { track, events } from 'src/helpers/analytics'; import { hasDismissedDesktopAppUpsell, @@ -9,7 +9,7 @@ import { DESKTOP_APP_MAC_URL, } from 'src/helpers/desktop-app-utils'; import { isMac } from 'src/helpers/is-os'; -import { OutlineButton } from 'src/components/buttons'; +import { OutlineButton } from 'src/components/button'; import { Container, Card, diff --git a/src/views/dashboard/components/desktopAppUpsell/style.js b/src/components/desktopAppUpsell/style.js similarity index 100% rename from src/views/dashboard/components/desktopAppUpsell/style.js rename to src/components/desktopAppUpsell/style.js diff --git a/src/components/dropdown/index.js b/src/components/dropdown/index.js deleted file mode 100644 index 787b2c5d05..0000000000 --- a/src/components/dropdown/index.js +++ /dev/null @@ -1,41 +0,0 @@ -// @flow -import theme from 'shared/theme'; -import React from 'react'; -// $FlowFixMe -import compose from 'recompose/compose'; -// $FlowFixMe -import styled from 'styled-components'; -import { Shadow, FlexCol, hexa, Transition, zIndex } from '../globals'; -import Card from '../card'; - -const StyledDropdown = styled(FlexCol)` - background-color: transparent; - position: absolute; - width: 400px; - top: 100%; - right: 0px; - z-index: ${zIndex.dropdown}; - padding-top: 8px; - color: ${theme.text.default}; - transition: ${Transition.dropdown.off}; -`; - -const StyledCard = styled(Card)` - display: flex; - flex-direction: column; - box-shadow: ${Shadow.high} ${({ theme }) => hexa(theme.bg.reverse, 0.15)}; - max-height: 640px; - overflow: hidden; - align-items: stretch; - display: inline-block; - border-radius: 8px; -`; - -const DropdownPure = (props: Object): React$Element => ( - - {props.children} - -); - -export const Dropdown = compose()(DropdownPure); -export default Dropdown; diff --git a/src/components/editForm/style.js b/src/components/editForm/style.js index 16126c98c7..fd4e6b2b65 100644 --- a/src/components/editForm/style.js +++ b/src/components/editForm/style.js @@ -1,7 +1,8 @@ import styled from 'styled-components'; import theme from 'shared/theme'; -import Card from '../card'; -import { FlexRow, FlexCol, Truncate } from '../globals'; +import Card from 'src/components/card'; +import { FlexRow, FlexCol, Truncate } from 'src/components/globals'; +import { MEDIA_BREAK } from 'src/components/layout'; export const StyledCard = styled(Card)` padding: 16px; @@ -67,7 +68,7 @@ export const DeleteCoverButton = styled.button` height: 24px; width: 24px; cursor: pointer; - z-index: 50; + z-index: 8; &:hover { background-color: ${theme.warn.alt}; } @@ -112,11 +113,12 @@ export const ImageInputWrapper = styled(FlexCol)` flex: 0 0 auto; margin-top: 8px; margin-bottom: 24px; + max-width: 342px; > label:nth-of-type(2) { position: absolute; bottom: -24px; - left: 24px; + left: 16px; } `; @@ -140,7 +142,7 @@ export const Location = styled(FlexRow)` text-decoration: underline; } - @media (max-width: 768px) { + @media (max-width: ${MEDIA_BREAK}px) { display: none; } `; diff --git a/src/components/emailInvitationForm/index.js b/src/components/emailInvitationForm/index.js index 49bf30883a..c77562ba08 100644 --- a/src/components/emailInvitationForm/index.js +++ b/src/components/emailInvitationForm/index.js @@ -4,10 +4,10 @@ import compose from 'recompose/compose'; import { connect } from 'react-redux'; import Textarea from 'react-textarea-autosize'; import { addToastWithTimeout } from 'src/actions/toasts'; -import Icon from '../icons'; +import Icon from 'src/components/icon'; import isEmail from 'validator/lib/isEmail'; import sendCommunityEmailInvitations from 'shared/graphql/mutations/community/sendCommunityEmailInvites'; -import { Button } from '../buttons'; +import { OutlineButton } from 'src/components/button'; import { Error } from '../formElements'; import { SectionCardFooter } from 'src/components/settingsViews/style'; import { withCurrentUser } from 'src/components/withCurrentUser'; @@ -414,13 +414,13 @@ class EmailInvitationForm extends React.Component { )} - + ); diff --git a/src/components/emailInvitationForm/style.js b/src/components/emailInvitationForm/style.js index 23d05f79e8..d1f24c630d 100644 --- a/src/components/emailInvitationForm/style.js +++ b/src/components/emailInvitationForm/style.js @@ -1,5 +1,7 @@ +// @flow import styled from 'styled-components'; import theme from 'shared/theme'; +import { MEDIA_BREAK } from 'src/components/layout'; export const EmailInviteForm = styled.div` display: flex; @@ -35,7 +37,7 @@ export const EmailInviteInput = styled.input` border: 2px solid ${theme.brand.default}; } - @media screen and (max-width: 768px) { + @media screen and (max-width: ${MEDIA_BREAK}px) { display: ${props => (props.hideOnMobile ? 'none' : 'auto')}; } `; diff --git a/src/components/entities/index.js b/src/components/entities/index.js new file mode 100644 index 0000000000..bd154f849f --- /dev/null +++ b/src/components/entities/index.js @@ -0,0 +1,16 @@ +// @flow +import { UserListItem, CommunityListItem, ChannelListItem } from './listItems'; +import { + ChannelProfileCard, + CommunityProfileCard, + UserProfileCard, +} from './profileCards'; + +export { + UserListItem, + CommunityListItem, + ChannelListItem, + ChannelProfileCard, + CommunityProfileCard, + UserProfileCard, +}; diff --git a/src/components/entities/listItems/channel.js b/src/components/entities/listItems/channel.js new file mode 100644 index 0000000000..a1db3941e3 --- /dev/null +++ b/src/components/entities/listItems/channel.js @@ -0,0 +1,89 @@ +// @flow +import React from 'react'; +import compose from 'recompose/compose'; +import { Link } from 'react-router-dom'; +import type { ChannelInfoType } from 'shared/graphql/fragments/channel/channelInfo'; +import { ErrorBoundary } from 'src/components/error'; +import { withCurrentUser } from 'src/components/withCurrentUser'; +import Icon from 'src/components/icon'; +import JoinChannelWrapper from 'src/components/joinChannelWrapper'; +import LeaveChannelWrapper from 'src/components/leaveChannelWrapper'; +import { OutlineButton, PrimaryOutlineButton } from 'src/components/button'; +import { Row, Content, Label, Description, Actions } from './style'; + +type Props = { + channel: ?ChannelInfoType, + id: string, + name?: string, + description?: ?string, + currentUser: ?Object, + children?: React$Node, +}; + +const Channel = (props: Props) => { + const { channel, name, description, children, currentUser } = props; + if (!channel) return null; + + const renderAction = () => { + const chevron = ; + if (!currentUser) return chevron; + + const { community, channelPermissions } = channel; + const { communityPermissions } = community; + + const isCommunityMember = communityPermissions.isMember; + if (!isCommunityMember) return chevron; + + const { isMember } = channelPermissions; + if (isMember) + return ( + ( + + {isLoading ? 'Leaving...' : isHovering ? 'Leave' : 'Member'} + + )} + /> + ); + + return ( + ( + + {isLoading ? 'Joining...' : 'Join'} + + )} + /> + ); + }; + + return ( + + + + + {name && ( + + )} + + {description && {description}} + + + + {renderAction()} + {children} + + + + + ); +}; + +export const ChannelListItem = compose(withCurrentUser)(Channel); diff --git a/src/components/entities/listItems/community.js b/src/components/entities/listItems/community.js new file mode 100644 index 0000000000..4d580336ff --- /dev/null +++ b/src/components/entities/listItems/community.js @@ -0,0 +1,62 @@ +// @flow +import * as React from 'react'; +import { Link } from 'react-router-dom'; +import { CommunityAvatar } from 'src/components/avatar'; +import Icon from 'src/components/icon'; +import { + RowWithAvatar, + CommunityAvatarContainer, + Content, + Label, + Description, + Actions, +} from './style'; + +type Props = { + communityObject: Object, + avatarSize?: number, + profilePhoto?: string, + name?: string, + description?: ?string, + children?: React$Node, +}; + +export const CommunityListItem = (props: Props) => { + const { + communityObject, + profilePhoto, + name, + description, + avatarSize = 32, + children, + } = props; + + return ( + + + {profilePhoto && ( + + + + )} + + + {name && } + + {description && {description}} + + + + + + {children} + + + + ); +}; diff --git a/src/components/entities/listItems/index.js b/src/components/entities/listItems/index.js new file mode 100644 index 0000000000..d4e0a5233c --- /dev/null +++ b/src/components/entities/listItems/index.js @@ -0,0 +1,6 @@ +// @flow +import { ChannelListItem } from './channel'; +import { UserListItem } from './user'; +import { CommunityListItem } from './community'; + +export { ChannelListItem, UserListItem, CommunityListItem }; diff --git a/src/components/entities/listItems/style.js b/src/components/entities/listItems/style.js new file mode 100644 index 0000000000..9d43fdc7d2 --- /dev/null +++ b/src/components/entities/listItems/style.js @@ -0,0 +1,109 @@ +// @flow +import theme from 'shared/theme'; +import styled from 'styled-components'; +import { Truncate } from 'src/components/globals'; +import { Link } from 'react-router-dom'; + +export const CardLink = styled(Link)` + display: block; + position: relative; +`; + +export const Row = styled.div` + padding: 12px 16px; + align-items: center; + display: grid; + grid-template-rows: auto; + grid-template-areas: 'content actions'; + grid-template-columns: 1fr auto; + background: ${theme.bg.default}; + border-bottom: 1px solid ${theme.bg.divider}; + grid-gap: 16px; + + &:hover { + background: ${theme.bg.wash}; + cursor: pointer; + } +`; + +export const RowWithAvatar = styled.div` + padding: 12px 16px; + align-items: center; + display: grid; + grid-template-areas: 'avatar content actions'; + grid-template-columns: min-content 1fr auto; + grid-template-rows: auto; + background: ${theme.bg.default}; + border-bottom: 1px solid ${theme.bg.divider}; + grid-gap: 16px; + + &:hover { + background: ${theme.bg.wash}; + cursor: pointer; + } +`; + +export const UserAvatarContainer = styled.div` + height: 40px; + grid-area: avatar; + align-self: flex-start; +`; + +export const CommunityAvatarContainer = styled.div` + height: 32px; + grid-area: avatar; + align-self: flex-start; +`; + +export const Content = styled.div` + grid-area: content; + display: grid; +`; + +export const Label = styled.div` + color: ${theme.text.default}; + font-size: 15px; + font-weight: 600; + line-height: 1.2; + display: inline-flex; + align-items: center; + ${Truncate}; + + .icon { + color: ${theme.text.secondary}; + margin-right: 6px; + position: relative; + top: 1px; + } +`; + +export const Sublabel = styled.span` + font-size: 15px; + color: ${theme.text.alt}; + font-weight: 400; + line-height: 1.2; + display: inline-block; + ${Truncate}; +`; + +export const Description = styled.p` + font-size: 15px; + line-height: 1.3; + color: ${theme.text.default}; + margin-top: 6px; + padding-right: 24px; + word-break: break-word; +`; + +export const Actions = styled.div` + grid-area: actions; + display: flex; + flex-direction: column; + align-self: flex-start; + justify-content: flex-start; + position: relative; + z-index: 10; + color: ${theme.text.alt}; + flex: 1; + padding-top: 4px; +`; diff --git a/src/components/entities/listItems/user.js b/src/components/entities/listItems/user.js new file mode 100644 index 0000000000..8b7b545c80 --- /dev/null +++ b/src/components/entities/listItems/user.js @@ -0,0 +1,127 @@ +// @flow +import * as React from 'react'; +import { withRouter } from 'react-router'; +import compose from 'recompose/compose'; +import { connect } from 'react-redux'; +import type { UserInfoType } from 'shared/graphql/fragments/user/userInfo'; +import { UserAvatar } from 'src/components/avatar'; +import Reputation from 'src/components/reputation'; +import Badge from 'src/components/badges'; +import type { Dispatch } from 'redux'; +import InitDirectMessageWrapper from 'src/components/initDirectMessageWrapper'; +import ConditionalWrap from 'src/components/conditionalWrap'; +import { OutlineButton } from 'src/components/button'; +import { + RowWithAvatar, + UserAvatarContainer, + Content, + Label, + Sublabel, + Description, + Actions, + CardLink, +} from './style'; + +type Props = { + userObject: UserInfoType, + id: string, + avatarSize?: number, + profilePhoto?: string, + name?: string, + username?: ?string, + description?: ?string, + website?: ?string, + badges?: Array, + isCurrentUser?: boolean, + reputation?: number, + messageButton?: boolean, + multiAction?: boolean, + children?: React$Node, + history: Object, + dispatch: Dispatch, + showHoverProfile?: boolean, + isLink?: boolean, + onClick?: (user: UserInfoType) => any, +}; + +// eslint-disable-next-line +const noop = (user: UserInfoType) => {}; + +const User = (props: Props) => { + const { + userObject, + profilePhoto, + name, + username, + description, + reputation, + avatarSize = 40, + badges, + children, + messageButton, + showHoverProfile = true, + isLink = true, + onClick = noop, + } = props; + + if (!userObject.username) return null; + + return ( + ( + {children} + )} + > + onClick(userObject)}> + {profilePhoto && ( + + + + )} + + + {name && ( + + )} + + {username && @{username}} + + {typeof reputation === 'number' && ( + // $FlowIssue + + )} + + {description && {description}} + + + + {messageButton && ( + Message} + /> + )} + + {children} + + + + ); +}; + +export const UserListItem = compose( + withRouter, + connect() +)(User); diff --git a/src/components/entities/profileCards/channel.js b/src/components/entities/profileCards/channel.js new file mode 100644 index 0000000000..278e8065cd --- /dev/null +++ b/src/components/entities/profileCards/channel.js @@ -0,0 +1,29 @@ +// @flow +import React from 'react'; +import type { ChannelInfoType } from 'shared/graphql/fragments/channel/channelInfo'; +import { ChannelActions } from './components/channelActions'; +import { ChannelMeta } from './components/channelMeta'; +import { ChannelCommunityMeta } from './components/channelCommunityMeta'; +import { ProfileContainer } from './style'; + +type Props = { + channel: ChannelInfoType, + hideActions?: boolean, + hideCommunityMeta?: boolean, +}; + +export const ChannelProfileCard = (props: Props) => { + const { channel, hideActions, hideCommunityMeta } = props; + + return ( + + {!hideCommunityMeta && } + + {!hideActions ? ( + + ) : ( +
+ )} + + ); +}; diff --git a/src/components/entities/profileCards/community.js b/src/components/entities/profileCards/community.js new file mode 100644 index 0000000000..46d339ff14 --- /dev/null +++ b/src/components/entities/profileCards/community.js @@ -0,0 +1,37 @@ +// @flow +import React from 'react'; +import { Link } from 'react-router-dom'; +import { CommunityAvatar } from 'src/components/avatar'; +import { CommunityActions } from './components/communityActions'; +import { CommunityMeta } from './components/communityMeta'; +import { ProfileContainer, CoverPhoto, ProfileAvatarContainer } from './style'; +import type { CommunityInfoType } from 'shared/graphql/fragments/community/communityInfo'; +import type { CommunityMetaDataType } from 'shared/graphql/fragments/community/communityMetaData'; + +type Props = { + community: CommunityInfoType, +}; + +export const CommunityProfileCard = (props: Props) => { + const { community } = props; + + return ( + + + + + + + + + + + + + + ); +}; diff --git a/src/components/entities/profileCards/components/channelActions.js b/src/components/entities/profileCards/components/channelActions.js new file mode 100644 index 0000000000..9348158cd6 --- /dev/null +++ b/src/components/entities/profileCards/components/channelActions.js @@ -0,0 +1,88 @@ +// @flow +import React from 'react'; +import { connect } from 'react-redux'; +import type { ChannelInfoType } from 'shared/graphql/fragments/channel/channelInfo'; +import getComposerLink from 'src/helpers/get-composer-link'; +import { + OutlineButton, + PrimaryOutlineButton, + HoverWarnOutlineButton, +} from 'src/components/button'; +import JoinChannel from 'src/components/joinChannelWrapper'; +import LeaveChannel from 'src/components/leaveChannelWrapper'; +import { ActionsRowContainer } from '../style'; + +type Props = { + channel: ChannelInfoType, +}; + +export const UnconnectedChannelActions = (props: Props) => { + const { channel } = props; + const { community } = channel; + const { isOwner, isModerator } = community.communityPermissions; + const isTeamMember = isOwner || isModerator; + const { channelPermissions, isArchived } = channel; + + const { pathname, search } = getComposerLink({ + communityId: community.id, + channelId: channel.id, + }); + + if (channelPermissions.isMember) { + return ( + + {isTeamMember && ( + + Settings + + )} + + ( + + {isLoading + ? 'Leaving...' + : isHovering + ? 'Leave channel' + : 'Member'} + + )} + /> + + {!isArchived && ( + + New Post + + )} + + ); + } + + return ( + + {isTeamMember && ( + + Settings + + )} + + ( + + {isLoading ? 'Joining...' : 'Join channel'} + + )} + /> + + ); +}; + +export const ChannelActions = connect()(UnconnectedChannelActions); diff --git a/src/components/entities/profileCards/components/channelCommunityMeta.js b/src/components/entities/profileCards/components/channelCommunityMeta.js new file mode 100644 index 0000000000..6d6805ef49 --- /dev/null +++ b/src/components/entities/profileCards/components/channelCommunityMeta.js @@ -0,0 +1,24 @@ +// @flow +import React from 'react'; +import { Link } from 'react-router-dom'; +import type { ChannelInfoType } from 'shared/graphql/fragments/channel/channelInfo'; +import { CommunityAvatar } from 'src/components/avatar'; +import { ChannelCommunityMetaRow, ChannelCommunityName } from '../style'; + +type Props = { + channel: ChannelInfoType, +}; + +export const ChannelCommunityMeta = (props: Props) => { + const { channel } = props; + const { community } = channel; + + return ( + + + + {community.name} + + + ); +}; diff --git a/src/components/entities/profileCards/components/channelMeta.js b/src/components/entities/profileCards/components/channelMeta.js new file mode 100644 index 0000000000..927df82afd --- /dev/null +++ b/src/components/entities/profileCards/components/channelMeta.js @@ -0,0 +1,30 @@ +// @flow +import React from 'react'; +import { Link } from 'react-router-dom'; +import type { ChannelInfoType } from 'shared/graphql/fragments/channel/channelInfo'; +import renderTextWithLinks from 'src/helpers/render-text-with-markdown-links'; +import { MetaContainer, Name, Description, Username } from '../style'; + +type Props = { + channel: ChannelInfoType, +}; + +export const ChannelMeta = (props: Props) => { + const { channel } = props; + const { description, community, isArchived } = channel; + const formattedDescription = description && renderTextWithLinks(description); + + return ( + + + # {channel.name} + + + {isArchived && Archived} + + {formattedDescription && ( + {formattedDescription} + )} + + ); +}; diff --git a/src/components/entities/profileCards/components/communityActions.js b/src/components/entities/profileCards/components/communityActions.js new file mode 100644 index 0000000000..0af7e143bc --- /dev/null +++ b/src/components/entities/profileCards/components/communityActions.js @@ -0,0 +1,90 @@ +// @flow +import React, { useState } from 'react'; +import { connect } from 'react-redux'; +import type { Dispatch } from 'redux'; +import type { CommunityInfoType } from 'shared/graphql/fragments/community/communityInfo'; +import getComposerLink from 'src/helpers/get-composer-link'; +import { + PrimaryOutlineButton, + PrimaryButton, + OutlineButton, +} from 'src/components/button'; +import { openModal } from 'src/actions/modals'; +import JoinCommunity from 'src/components/joinCommunityWrapper'; +import { ActionsRowContainer } from '../style'; + +type Props = { + community: CommunityInfoType, + dispatch: Dispatch, +}; + +export const UnconnectedCommunityActions = (props: Props) => { + const { community, dispatch } = props; + + const [isHovering, setHover] = useState(false); + const onMouseEnter = () => setHover(true); + const onMouseLeave = () => setHover(false); + + const leaveCommunity = () => + dispatch( + openModal('DELETE_DOUBLE_CHECK_MODAL', { + id: community.id, + entity: 'team-member-leaving-community', + message: 'Are you sure you want to leave this community?', + buttonLabel: 'Leave Community', + }) + ); + + const { isMember, isOwner, isModerator } = community.communityPermissions; + const isTeamMember = isOwner || isModerator; + const { pathname, search } = getComposerLink({ communityId: community.id }); + + if (isMember) { + return ( + + {isTeamMember && ( + + Settings + + )} + + {!isOwner && ( + + {isHovering ? 'Leave community' : 'Member'} + + )} + + + New Post + + + ); + } + + return ( + + ( + + {isLoading ? 'Joining...' : 'Join community'} + + )} + /> + + ); +}; + +export const CommunityActions = connect()(UnconnectedCommunityActions); diff --git a/src/components/entities/profileCards/components/communityMeta.js b/src/components/entities/profileCards/components/communityMeta.js new file mode 100644 index 0000000000..7b5e8d858e --- /dev/null +++ b/src/components/entities/profileCards/components/communityMeta.js @@ -0,0 +1,68 @@ +// @flow +import React from 'react'; +import { Link } from 'react-router-dom'; +import type { CommunityInfoType } from 'shared/graphql/fragments/community/communityInfo'; +import type { CommunityMetaDataType } from 'shared/graphql/fragments/community/communityMetaData'; +import renderTextWithLinks from 'src/helpers/render-text-with-markdown-links'; +import addProtocolToString from 'shared/normalize-url'; +import Icon from 'src/components/icon'; +import { + MetaContainer, + Name, + Description, + MetaLinksContainer, + MetaRow, + OnlineDot, +} from '../style'; + +type Props = { + // TODO: Properly type this + community: Object, +}; + +export const CommunityMeta = (props: Props) => { + const { community } = props; + const { description, website } = community; + const formattedDescription = description && renderTextWithLinks(description); + const formattedWebsite = website && addProtocolToString(website); + + return ( + + + {community.name} + + + {formattedDescription && ( + {formattedDescription} + )} + + + {formattedWebsite && ( + + + {website} + + + )} + + {community.metaData && ( + + + {' '} + {community.metaData.members.toLocaleString()} members + + + + {community.metaData.onlineMembers.toLocaleString()}{' '} + members online + + + )} + + + ); +}; diff --git a/src/components/entities/profileCards/components/userActions.js b/src/components/entities/profileCards/components/userActions.js new file mode 100644 index 0000000000..1092a56db7 --- /dev/null +++ b/src/components/entities/profileCards/components/userActions.js @@ -0,0 +1,70 @@ +// @flow +import React from 'react'; +import compose from 'recompose/compose'; +import { connect } from 'react-redux'; +import type { Dispatch } from 'redux'; +import type { UserInfoType } from 'shared/graphql/fragments/user/userInfo'; +import { openModal } from 'src/actions/modals'; +import { withCurrentUser } from 'src/components/withCurrentUser'; +import { + PrimaryOutlineButton, + HoverWarnOutlineButton, + OutlineButton, +} from 'src/components/button'; +import InitDirectMessageWrapper from 'src/components/initDirectMessageWrapper'; +import { ActionsRowContainer } from '../style'; +import { isAdmin } from 'src/helpers/is-admin'; + +type Props = { + user: UserInfoType, + currentUser: ?UserInfoType, + dispatch: Dispatch, +}; + +export const UnconnectedUserActions = (props: Props) => { + const { user, currentUser, dispatch } = props; + + if (!user) return null; + + const initReport = () => { + return dispatch(openModal('REPORT_USER_MODAL', { user })); + }; + + const initBan = () => { + return dispatch(openModal('BAN_USER_MODAL', { user })); + }; + + return ( + + {currentUser && currentUser.id === user.id && ( + + Settings + + )} + + + Message + + } + /> + + {currentUser && user.id !== currentUser.id && ( + + Report + + )} + + {currentUser && user.id !== currentUser.id && isAdmin(currentUser.id) && ( + Ban + )} + + ); +}; + +export const UserActions = compose( + withCurrentUser, + connect() +)(UnconnectedUserActions); diff --git a/src/components/entities/profileCards/components/userMeta.js b/src/components/entities/profileCards/components/userMeta.js new file mode 100644 index 0000000000..90fdfa5f7d --- /dev/null +++ b/src/components/entities/profileCards/components/userMeta.js @@ -0,0 +1,79 @@ +// @flow +import React from 'react'; +import type { UserInfoType } from 'shared/graphql/fragments/user/userInfo'; +import renderTextWithLinks from 'src/helpers/render-text-with-markdown-links'; +import addProtocolToString from 'shared/normalize-url'; +import Icon from 'src/components/icon'; +import GithubProfile from 'src/components/githubProfile'; +import { + MetaContainer, + Name, + Description, + MetaLinksContainer, + MetaRow, + OnlineDot, + Username, +} from '../style'; + +type Props = { + user: UserInfoType, +}; + +export const UserMeta = (props: Props) => { + const { user } = props; + const { description, website, isOnline } = user; + const formattedDescription = description && renderTextWithLinks(description); + const formattedWebsite = website && addProtocolToString(website); + + return ( + + {user.name} + {user.username && @{user.username}} + + {formattedDescription && ( + {formattedDescription} + )} + + + {formattedWebsite && ( + + + {website} + + + )} + + { + if (!profile) { + return null; + } else { + return ( + + + @{profile.username} + + + ); + } + }} + /> + + {isOnline && ( + + Online now + + )} + + + ); +}; diff --git a/src/components/entities/profileCards/index.js b/src/components/entities/profileCards/index.js new file mode 100644 index 0000000000..21ee6db291 --- /dev/null +++ b/src/components/entities/profileCards/index.js @@ -0,0 +1,6 @@ +// @flow +import { ChannelProfileCard } from './channel'; +import { UserProfileCard } from './user'; +import { CommunityProfileCard } from './community'; + +export { ChannelProfileCard, CommunityProfileCard, UserProfileCard }; diff --git a/src/components/entities/profileCards/style.js b/src/components/entities/profileCards/style.js new file mode 100644 index 0000000000..19c73e6537 --- /dev/null +++ b/src/components/entities/profileCards/style.js @@ -0,0 +1,173 @@ +// @flow +import styled from 'styled-components'; +import theme from 'shared/theme'; +import { Truncate } from 'src/components/globals'; +import { MEDIA_BREAK, SECONDARY_COLUMN_WIDTH } from 'src/components/layout'; + +export const ProfileContainer = styled.div` + display: flex; + flex-direction: column; + width: 100%; + position: relative; + + @media (max-width: ${MEDIA_BREAK}px) { + border-radius: 0; + margin-top: 0; + border: 0; + } +`; + +export const CoverPhoto = styled.div` + position: relative; + width: 100%; + height: 100%; + min-height: ${SECONDARY_COLUMN_WIDTH / 3}px; + max-height: ${SECONDARY_COLUMN_WIDTH / 3}px; + background-color: ${theme.text.default}; + overflow: hidden; + background-image: url(${props => props.src}); + background-size: cover; + background-position: center center; + border-radius: 4px 4px 0 0; + + @media (max-width: ${MEDIA_BREAK}px) { + border-radius: 0; + } +`; + +export const ProfileAvatarContainer = styled.div` + position: relative; + top: -36px; + width: 68px; + height: 68px; + margin-left: 12px; + border-radius: 10px; + background: ${theme.bg.default}; + border: 4px solid ${theme.bg.default}; + margin-bottom: -44px; +`; + +export const RoundProfileAvatarContainer = styled.div` + position: relative; + top: -36px; + width: 68px; + height: 68px; + margin-left: 12px; + border-radius: 34px; + background: ${theme.bg.default}; + border: 4px solid ${theme.bg.default}; + margin-bottom: -48px; +`; + +export const ActionsRowContainer = styled.div` + display: grid; + align-items: center; + grid-gap: 12px; + padding: 16px 16px 20px; + margin-top: 8px; + + button { + flex: 1; + } + + @media (max-width: ${MEDIA_BREAK}px) { + border-bottom: 1px solid ${theme.bg.border}; + margin-top: 0; + padding-bottom: 16px; + } +`; + +export const MetaContainer = styled.div` + display: flex; + flex-direction: column; + padding: 0 16px; + margin-top: 16px; +`; + +export const Name = styled.h1` + font-size: 24px; + font-weight: 800; + color: ${theme.text.default}; + word-break: break-word; + line-height: 1.2; +`; + +export const Description = styled.p` + margin-top: 8px; + margin-bottom: 4px; + font-size: 16px; + font-weight: 400; + line-height: 1.4; + color: ${theme.text.secondary}; + word-break: break-word; +`; + +export const MetaLinksContainer = styled.div` + margin-top: 4px; +`; + +export const MetaRow = styled.div` + display: flex; + font-size: 16px; + font-weight: 400; + color: ${theme.text.secondary}; + align-items: center; + margin-top: 8px; + word-break: break-word; + + &:first-of-type { + margin-top: 8px; + } + + a { + display: flex; + align-items: center; + } + + a:hover { + color: ${theme.text.default}; + } + + .icon { + margin-right: 8px; + } +`; + +export const OnlineDot = styled.div` + width: 8px; + height: 8px; + border-radius: 4px; + background-color: ${theme.success.default}; + margin-right: 16px; + margin-left: 6px; +`; + +export const ChannelCommunityMetaRow = styled.div` + display: flex; + padding: 16px; + margin-bottom: -12px; + align-items: center; + border-bottom: 1px solid ${theme.bg.border}; + background: transparent; + + &:hover { + background: ${theme.bg.wash}; + } +`; + +export const ChannelCommunityName = styled.div` + font-size: 18px; + font-weight: 500; + color: ${theme.text.alt}; + margin-left: 16px; + ${Truncate}; +`; + +export const Username = styled.div` + font-size: 18px; + font-weight: 500; + color: ${theme.text.alt}; + margin-bottom: 4px; + word-break: break-all; + margin-top: 2px; +`; diff --git a/src/components/entities/profileCards/user.js b/src/components/entities/profileCards/user.js new file mode 100644 index 0000000000..a9e343ff15 --- /dev/null +++ b/src/components/entities/profileCards/user.js @@ -0,0 +1,41 @@ +// @flow +import React from 'react'; +import { Link } from 'react-router-dom'; +import type { UserInfoType } from 'shared/graphql/fragments/user/userInfo'; +import { UserAvatar } from 'src/components/avatar'; +import { UserActions } from './components/userActions'; +import { UserMeta } from './components/userMeta'; +import { + ProfileContainer, + CoverPhoto, + RoundProfileAvatarContainer, +} from './style'; + +type Props = { + user: UserInfoType, +}; + +export const UserProfileCard = (props: Props) => { + const { user } = props; + + return ( + + + + + + + + + + + + + + ); +}; diff --git a/src/components/error/ErrorBoundary.js b/src/components/error/ErrorBoundary.js index e6facfb70f..e8ab794dd1 100644 --- a/src/components/error/ErrorBoundary.js +++ b/src/components/error/ErrorBoundary.js @@ -16,12 +16,16 @@ class ErrorBoundary extends React.Component { componentDidCatch = (error: any, errorInfo: any) => { this.setState({ error }); + console.error({ error }); window.Raven && window.Raven.captureException(error, { extra: errorInfo }); }; render() { const { error } = this.state; - const { fallbackComponent: FallbackComponent, children } = this.props; + const { + fallbackComponent: FallbackComponent = null, + children, + } = this.props; if (error) { if (this.props.fallbackComponent) { @@ -29,7 +33,7 @@ class ErrorBoundary extends React.Component { return ; } - if (this.props.fallbackComponent === null) { + if (!this.props.fallbackComponent) { return null; } diff --git a/src/components/error/SettingsFallback.js b/src/components/error/SettingsFallback.js index 7b756abb84..01ed83471e 100644 --- a/src/components/error/SettingsFallback.js +++ b/src/components/error/SettingsFallback.js @@ -6,7 +6,7 @@ import { SectionSubtitle, SectionCardFooter, } from 'src/components/settingsViews/style'; -import { Button } from 'src/components/buttons'; +import { PrimaryButton } from 'src/components/button'; class SettingsFallback extends React.Component<{}> { render() { @@ -28,13 +28,9 @@ class SettingsFallback extends React.Component<{}> { - + ); diff --git a/src/components/formElements/index.js b/src/components/formElements/index.js index a742d7d6af..3ce8b2b26e 100644 --- a/src/components/formElements/index.js +++ b/src/components/formElements/index.js @@ -1,7 +1,7 @@ // @flow import * as React from 'react'; -import Icon from 'src/components/icons'; -import { FauxOutlineButton } from 'src/components/buttons'; +import Icon from 'src/components/icon'; +import { WhiteOutlineButton } from 'src/components/button'; import type { GetUserType } from 'shared/graphql/queries/user/getUser'; import type { GetCommunityType } from 'shared/graphql/queries/community/getCommunity'; @@ -23,7 +23,7 @@ import { } from './style'; type InputProps = { - children?: React.Node, + children?: React$Node, inputType?: string, defaultValue?: ?string, value?: ?any, @@ -117,13 +117,7 @@ export const CoverInput = (props: CoverPhotoInputProps) => { - - Add Cover Photo - + Add Cover Photo { } export const Error = (props: Object) => { - return {props.children}; + const { children, ...rest } = props; + return {children}; }; export const Success = (props: Object) => { - return {props.children}; + const { children, ...rest } = props; + return {children}; }; diff --git a/src/components/formElements/style.js b/src/components/formElements/style.js index 57da003492..bd869960da 100644 --- a/src/components/formElements/style.js +++ b/src/components/formElements/style.js @@ -238,7 +238,8 @@ export const PhotoInputImage = styled.img` export const CoverInputLabel = styled.label` position: relative; - height: 96px; + height: 114px; + max-width: 342px; z-index: ${zIndex.form}; width: 100%; margin-top: 8px; @@ -273,7 +274,7 @@ export const CoverImage = styled.div` bottom: 0; left: 0; width: 100%; - height: 96px; + height: 114px; border-radius: 8px; `; diff --git a/src/components/fullscreenView/index.js b/src/components/fullscreenView/index.js index 1c4336fe0a..5bfb17150f 100644 --- a/src/components/fullscreenView/index.js +++ b/src/components/fullscreenView/index.js @@ -5,8 +5,8 @@ import { ClusterTwo, ClusterThree, ClusterFour, -} from '../../components/illustrations'; -import Icon from '../../components/icons'; +} from 'src/components/illustrations'; +import Icon from 'src/components/icon'; import { FullscreenViewContainer, Illustrations, CloseLink } from './style'; import { ESC } from 'src/helpers/keycodes'; diff --git a/src/components/fullscreenView/style.js b/src/components/fullscreenView/style.js index cb4e318949..0d0f6c037c 100644 --- a/src/components/fullscreenView/style.js +++ b/src/components/fullscreenView/style.js @@ -1,7 +1,8 @@ // @flow import theme from 'shared/theme'; import styled from 'styled-components'; -import { zIndex } from '../globals'; +import { zIndex } from 'src/components/globals'; +import { MEDIA_BREAK } from 'src/components/layout'; export const FullscreenViewContainer = styled.div` position: fixed; @@ -24,7 +25,7 @@ export const FullscreenViewContainer = styled.div` export const Illustrations = styled.span` z-index: ${zIndex.background}; - @media screen and (max-width: 768px) { + @media screen and (max-width: ${MEDIA_BREAK}px) { display: none; } `; diff --git a/src/components/gallery/browser.js b/src/components/gallery/browser.js index b495be9e47..e9295efb9f 100644 --- a/src/components/gallery/browser.js +++ b/src/components/gallery/browser.js @@ -1,7 +1,7 @@ // @flow import * as React from 'react'; import { connect } from 'react-redux'; -import { closeGallery } from '../../actions/gallery'; +import { closeGallery } from 'src/actions/gallery'; import type { GetMediaMessagesForThreadType } from 'shared/graphql/queries/message/getMediaMessagesForThread'; import type { Dispatch } from 'redux'; import { diff --git a/src/components/gallery/index.js b/src/components/gallery/index.js index d326ce840d..3912e4d606 100644 --- a/src/components/gallery/index.js +++ b/src/components/gallery/index.js @@ -3,7 +3,7 @@ import * as React from 'react'; import { connect } from 'react-redux'; import compose from 'recompose/compose'; import getMediaMessagesForThread from 'shared/graphql/queries/message/getMediaMessagesForThread'; -import { displayLoadingGallery } from '../../components/loading'; +import { displayLoadingGallery } from 'src/components/loading'; import Browser from './browser'; const GalleryWithMedia = compose( diff --git a/src/components/gallery/style.js b/src/components/gallery/style.js index 0325fc69b8..2c95222d49 100644 --- a/src/components/gallery/style.js +++ b/src/components/gallery/style.js @@ -1,6 +1,8 @@ +// @flow import styled from 'styled-components'; import theme from 'shared/theme'; -import { Shadow, zIndex } from '../globals'; +import { Shadow, zIndex } from 'src/components/globals'; +import { MEDIA_BREAK } from 'src/components/layout'; export const GalleryWrapper = styled.div` position: fixed; @@ -32,7 +34,7 @@ export const ActiveImage = styled.img` object-fit: cover; max-height: 90%; width: 100%; - max-width: 768px; + max-width: ${MEDIA_BREAK}px; margin: auto 0 5rem; box-shadow: ${Shadow.high}; z-index: ${zIndex.fullscreen + 2}; @@ -68,7 +70,7 @@ export const MiniContainer = styled.div` display: flex; justify-content: center; - @media (max-width: 768px) { + @media (max-width: ${MEDIA_BREAK}px) { justify-content: flex-start; overflow-x: scroll; } diff --git a/src/components/globals/index.js b/src/components/globals/index.js index 4ca79fe025..b143cd62fe 100644 --- a/src/components/globals/index.js +++ b/src/components/globals/index.js @@ -5,7 +5,7 @@ import styled, { css, keyframes } from 'styled-components'; export const Gradient = (g1, g2) => css`radial-gradient(ellipse farthest-corner at top left, ${g1} 0%, ${g2} 100%)`; -export const Truncate = width => css` +export const Truncate = () => css` text-overflow: ellipsis; overflow: hidden; white-space: nowrap; @@ -142,7 +142,7 @@ export const Spinner = styled.span` margin-left: ${props => props.size !== undefined ? `-${props.size / 2}px` : '-8px'}; border-radius: 50%; - border: ${props => '2px'} solid + border: 2px solid ${props => props.color ? eval(`props.theme.${props.color}`) @@ -360,198 +360,6 @@ export const FlexCol = styled.div` align-items: stretch; `; -const returnTooltip = props => { - switch (props.tipLocation) { - case 'top-left': - return ` - &:after { - bottom: calc(100% + 4px); - right: 0; - } - &:before { - bottom: 100%; - right: 0; - transform: translateX(-100%); - border-bottom-width: 0; - border-top-color: ${ - props.onboarding ? props.theme.brand.alt : props.theme.bg.reverse - }; - } - `; - case 'top-right': - return ` - &:after { - bottom: calc(100% + 4px); - left: 0; - } - &:before { - bottom: 100%; - left: 0; - transform: translateX(100%); - border-bottom-width: 0; - border-top-color: ${ - props.onboarding ? props.theme.brand.alt : props.theme.bg.reverse - }; - } - `; - case 'top': - return ` - &:after { - bottom: calc(100% + 8px); - left: 50%; - transform: translateX(-50%); - } - &:before { - bottom: calc(100% + 3px); - left: 50%; - transform: translateX(-50%); - border-bottom-width: 0; - border-top-color: ${ - props.onboarding ? props.theme.brand.alt : props.theme.bg.reverse - }; - } - `; - case 'right': - default: - return ` - &:after { - top: 50%; - left: calc(100% + 4px); - transform: translateY(-50%); - } - &:before{ - top: calc(50% - 5px); - left: 100%; - border-left-width: 0; - border-right-color: ${ - props.onboarding ? props.theme.brand.alt : props.theme.bg.reverse - }; - } - `; - case 'bottom-left': - return ` - &:after { - top: calc(100% + 4px); - right: 0; - } - &:before { - top: 100%; - right: 0; - transform: translateX(-100%); - border-top-width: 0; - border-bottom-color: ${ - props.onboarding ? props.theme.brand.alt : props.theme.bg.reverse - }; - } - `; - case 'bottom-right': - return ` - &:after { - top: calc(100% + 4px); - left: 0; - } - &:before { - top: 100%; - left: 0; - transform: translateX(100%); - border-top-width: 0; - border-bottom-color: ${ - props.onboarding ? props.theme.brand.alt : props.theme.bg.reverse - }; - } - `; - case 'bottom': - return ` - &:after { - top: calc(100% + 8px); - left: 50%; - transform: translateX(-50%); - } - &:before { - top: calc(100% + 3px); - left: 50%; - transform: translateX(-50%); - border-top-width: 0; - border-bottom-color: ${ - props.onboarding ? props.theme.brand.alt : props.theme.bg.reverse - }; - } - `; - case 'left': - return ` - &:after { - right: calc(100% + 4px); - top: 50%; - transform: translateY(-50%); - } - &:before{ - right: 100%; - top: calc(50% - 5px); - border-right-width: 0; - border-left-color: ${ - props.onboarding ? props.theme.brand.alt : props.theme.bg.reverse - }; - } - `; - } -}; - -export const Tooltip = props => css` - position: relative; - - &:after, - &:before { - line-height: 1; - user-select: none; - pointer-events: none; - position: absolute; - opacity: 0; - display: block; - text-transform: none; - } - - &:before { - content: ''; - z-index: ${zIndex.tooltip + 1}; - border: 6px solid transparent; - } - - &:after { - content: ${props.tipText && !props.onboarding - ? `'${CSS.escape(props.tipText)}'` - : "''"}; - z-index: ${zIndex.tooltip}; - ${fontStack}; - font-size: 14px; - font-weight: 500; - min-width: 8px; - max-width: 21em; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - padding: 8px 12px; - border-radius: 8px; - box-shadow: ${Shadow.mid} ${hexa(props.theme.bg.reverse, 0.25)}; - background: ${props.theme.bg.reverse}; - color: ${props.theme.text.reverse}; - } - - ${props.tipText && !props.onboarding ? returnTooltip(props) : ''}; - - &:hover:after, - &:hover:before { - opacity: 1; - transition: opacity 0.1s ease-in 0.1s; - } - - @media (max-width: 768px) { - &:after, - &:before { - display: none; - } - } -`; - export const Onboarding = props => css` position: relative; @@ -593,8 +401,6 @@ export const Onboarding = props => css` box-shadow: 0 8px 32px rgba(23, 26, 33, 0.35); } - ${props.onboarding ? returnTooltip(props) : ''}; - &:after, &:before { opacity: 1; diff --git a/src/components/goop/index.js b/src/components/goop/index.js index d03708d79a..308647b6c2 100644 --- a/src/components/goop/index.js +++ b/src/components/goop/index.js @@ -1,6 +1,8 @@ +// @flow import React from 'react'; import styled from 'styled-components'; -import { zIndex } from '../globals'; +import { zIndex } from 'src/components/globals'; +import { MEDIA_BREAK } from 'src/components/layout'; /* eslint no-eval: 0 */ @@ -37,14 +39,20 @@ export const SvgWrapper = styled.div` color: ${props => eval(`props.theme.${props.color}`)}; pointer-events: none; - @media (max-width: 768px) { + @media (max-width: ${MEDIA_BREAK}px) { width: 150%; left: -25%; right: -25%; } `; -class Goop extends React.Component { +type Props = { + color: string, + goop: number, + goopHeight: number, +}; + +class Goop extends React.Component { returnGoop() { switch (this.props.goop) { default: @@ -100,12 +108,13 @@ class Goop extends React.Component { } render() { + const { color = 'bg.default', goopHeight, goop } = this.props; return ( , - isCurrentUser?: boolean, - reputation?: number, - messageButton?: boolean, - multiAction?: boolean, - children?: React.Node, - history: Object, - dispatch: Dispatch, - showHoverProfile?: boolean, -}; - -// Each prop both provides data AND indicates that the element should be included in the instance of the profile, -// so each instance must manually call out which pieces of the profile it wants included. - -const LinkHandler = ({ - username, - children, -}: { - username: ?string, - children: React.Node, -}) => - username ? ( - {children} - ) : ( - {children} - ); - -class GranularUserProfileHandler extends React.Component { - render() { - const { showHoverProfile = true, userObject } = this.props; - return ( - ( - - - - )} - > - - - ); - } -} - -class GranularUserProfile extends React.Component { - initMessage = () => { - const { name, username, id } = this.props; - const user = { name, username, id }; - - this.props.dispatch(initNewThreadWithUser(user)); - this.props.history.push('/messages/new'); - }; - - render() { - const { - userObject, - profilePhoto, - name, - username, - description, - reputation, - avatarSize = 32, - badges, - children, - messageButton, - multiAction, - showHoverProfile = true, - } = this.props; - - return ( - - {profilePhoto && ( - - )} - - {name && ( - - {name} - {username && @{username}} - {badges && badges.map((b, i) => )} - - )} - - {typeof reputation === 'number' && ( - - )} - - {description && {description}} - {messageButton && ( - - - - )} - {children} - - ); - } -} - -export default compose( - connect(), - withRouter -)(GranularUserProfileHandler); diff --git a/src/components/granularUserProfile/style.js b/src/components/granularUserProfile/style.js deleted file mode 100644 index acbd5ecce1..0000000000 --- a/src/components/granularUserProfile/style.js +++ /dev/null @@ -1,100 +0,0 @@ -// @flow -import theme from 'shared/theme'; -import styled from 'styled-components'; -import { Tooltip, Truncate } from '../globals'; - -export const Content = styled.div` - border-bottom: 1px solid ${theme.bg.wash}; - padding: 12px 0; - display: flex; - align-items: center; - flex-wrap: wrap; -`; - -export const Row = styled.div` - display: grid; - grid-template-columns: ${props => - props.avatarSize ? `${props.avatarSize}px` : '32px'} minmax(0px, 1fr) 32px; - grid-template-rows: ${props => - props.multiAction ? '1fr auto auto' : '1fr auto'}; - grid-template-areas: ${props => - props.multiAction - ? `'avatar name message' 'action action action' '. description .'` - : `'avatar name action' '. description .'`}; - grid-column-gap: 16px; - padding: 12px 16px; - width: 100%; - background: ${theme.bg.default}; - border-bottom: 1px solid ${theme.bg.wash}; - - &:last-of-type { - > ${Content} { - border-bottom: 0; - } - } - - > div { - align-self: center; - } - - > a { - grid-area: name; - display: flex; - flex-direction: column; - align-items: flex-start; - align-self: center; - - > span { - ${Truncate}; - max-width: 100%; - } - } -`; - -export const Name = styled.span` - color: ${theme.text.default}; - font-size: 16px; - font-weight: 600; - line-height: 1.2; - vertical-align: middle; - display: flex; - align-items: center; - - &:hover { - color: ${theme.brand.alt}; - } -`; - -export const Username = styled.span` - font-size: 14px; - color: ${theme.text.alt}; - font-weight: 400; - margin: 0px 4px; - line-height: 1; - - &:hover { - color: ${theme.text.default}; - } -`; - -export const Description = styled.p` - grid-area: description; - font-size: 14px; - color: ${theme.text.alt}; -`; - -export const MessageIcon = styled.div` - grid-area: message; - height: 32px; - color: ${theme.brand.alt}; - cursor: pointer; - ${Tooltip}; -`; - -export const Actions = styled.div` - grid-area: action; - display: flex; - flex-direction: column; - align-items: flex-end; - justify-content: flex-start; -`; diff --git a/src/components/grid/index.js b/src/components/grid/index.js deleted file mode 100644 index b28aed4178..0000000000 --- a/src/components/grid/index.js +++ /dev/null @@ -1,46 +0,0 @@ -import React from 'react'; -import styled from 'styled-components'; -import { FlexRow } from '../globals'; - -const StyledGrid = styled(FlexRow)` - flex-wrap: wrap; - justify-content: space-between; - align-items: flex-start; - - > * { - flex: 0 0 calc(50% - 8px); - margin: 0; - margin-bottom: 16px; - - + div, - + span { - margin-top: 0; - } - - @media (max-width: 768px) { - flex: none; - } - } - - @media (max-width: 768px) { - flex-direction: column; - flex-wrap: nowrap; - justify-content: flex-start; - align-items: stretch; - flex: auto; - - > * { - max-width: none; - margin: 0; - - + div, - + span { - margin-top: 2px; - } - } - } -`; - -const Grid = props => {props.children}; - -export default Grid; diff --git a/src/components/hoverProfile/channelContainer.js b/src/components/hoverProfile/channelContainer.js index 0af0e6abab..29003f3d2e 100644 --- a/src/components/hoverProfile/channelContainer.js +++ b/src/components/hoverProfile/channelContainer.js @@ -16,7 +16,7 @@ const ChannelHoverProfile = getChannelById(props => { if (props.data.channel) { return ( @@ -24,9 +24,7 @@ const ChannelHoverProfile = getChannelById(props => { } if (props.data.loading) { - return ( - - ); + return ; } return null; @@ -93,7 +91,7 @@ class ChannelHoverProfileWrapper extends React.Component { // // // {({ ref }) => ( - // + // // {children} // // )} @@ -109,7 +107,7 @@ class ChannelHoverProfileWrapper extends React.Component { // }} // > // {({ style, ref }) => ( - // + // // )} // , // document.body diff --git a/src/components/hoverProfile/channelProfile.js b/src/components/hoverProfile/channelProfile.js index 3d5c67816e..fabe32114f 100644 --- a/src/components/hoverProfile/channelProfile.js +++ b/src/components/hoverProfile/channelProfile.js @@ -5,7 +5,7 @@ import { connect } from 'react-redux'; import { withRouter } from 'react-router'; import AvatarImage from 'src/components/avatar/image'; import { Link } from 'react-router-dom'; -import { Button, OutlineButton } from 'src/components/buttons'; +import { Button, OutlineButton } from 'src/components/button'; import ToggleChannelMembership from 'src/components/toggleChannelMembership'; import renderTextWithLinks from 'src/helpers/render-text-with-markdown-links'; import type { GetChannelType } from 'shared/graphql/queries/channel/getChannel'; @@ -26,13 +26,13 @@ type ProfileProps = { channel: GetChannelType, dispatch: Dispatch, currentUser: ?Object, - innerRef: (?HTMLElement) => void, + ref: (?HTMLElement) => void, style: CSSStyleDeclaration, }; class HoverProfile extends Component { render() { - const { channel, innerRef, style } = this.props; + const { channel, ref, style } = this.props; const { isOwner: isChannelOwner, @@ -47,7 +47,7 @@ class HoverProfile extends Component { const isGlobalModerator = isCommunityModerator; return ( - + { ); } else { return ( - + ); } }} diff --git a/src/components/hoverProfile/communityContainer.js b/src/components/hoverProfile/communityContainer.js index 182eff4787..437ac0acef 100644 --- a/src/components/hoverProfile/communityContainer.js +++ b/src/components/hoverProfile/communityContainer.js @@ -16,7 +16,7 @@ const CommunityHoverProfile = getCommunityById(props => { if (props.data && props.data.community) { return ( @@ -24,9 +24,7 @@ const CommunityHoverProfile = getCommunityById(props => { } if (props.data && props.data.loading) { - return ( - - ); + return ; } return null; @@ -103,7 +101,7 @@ class CommunityHoverProfileWrapper extends React.Component { // // // {({ ref }) => ( - // + // // {children} // // )} @@ -119,7 +117,7 @@ class CommunityHoverProfileWrapper extends React.Component { // }} // > // {({ style, ref }) => ( - // + // // )} // , // document.body diff --git a/src/components/hoverProfile/communityProfile.js b/src/components/hoverProfile/communityProfile.js index e77997b15d..bdd580c2c8 100644 --- a/src/components/hoverProfile/communityProfile.js +++ b/src/components/hoverProfile/communityProfile.js @@ -5,8 +5,8 @@ import { connect } from 'react-redux'; import { withRouter } from 'react-router'; import AvatarImage from 'src/components/avatar/image'; import { Link } from 'react-router-dom'; -import { Button, OutlineButton } from 'src/components/buttons'; -import ToggleCommunityMembership from 'src/components/toggleCommunityMembership'; +import { Button, OutlineButton } from 'src/components/button'; +import JoinCommunityWrapper from 'src/components/joinCommunityWrapper'; import type { GetCommunityType } from 'shared/graphql/queries/community/getCommunity'; import renderTextWithLinks from 'src/helpers/render-text-with-markdown-links'; import type { Dispatch } from 'redux'; @@ -27,19 +27,19 @@ type ProfileProps = { community: GetCommunityType, dispatch: Dispatch, currentUser: ?Object, - innerRef: (?HTMLElement) => void, + ref: (?HTMLElement) => void, style: CSSStyleDeclaration, }; class HoverProfile extends Component { render() { - const { community, innerRef, style } = this.props; + const { community, ref, style } = this.props; const { communityPermissions } = community; - const { isMember, isOwner, isModerator } = communityPermissions; + const { isOwner, isModerator } = communityPermissions; return ( - + @@ -68,30 +68,11 @@ class HoverProfile extends Component { {!isModerator && !isOwner && ( - { - if (isMember) { - return ( - - Member - - ); - } else { - return ( - - ); - } + return ; }} /> )} diff --git a/src/components/hoverProfile/loadingHoverProfile.js b/src/components/hoverProfile/loadingHoverProfile.js index bac66c4581..dba4fd0612 100644 --- a/src/components/hoverProfile/loadingHoverProfile.js +++ b/src/components/hoverProfile/loadingHoverProfile.js @@ -4,16 +4,16 @@ import { Loading } from 'src/components/loading'; import { HoverWrapper, ProfileCard } from './style'; type Props = { - innerRef: (?HTMLElement) => void, + ref?: (?HTMLElement) => void, style: CSSStyleDeclaration, }; export default class LoadingHoverProfile extends React.Component { render() { - const { innerRef, style } = this.props; + const { ref, style } = this.props; return ( - + diff --git a/src/components/hoverProfile/style.js b/src/components/hoverProfile/style.js index 94210e81e4..947200f026 100644 --- a/src/components/hoverProfile/style.js +++ b/src/components/hoverProfile/style.js @@ -3,15 +3,16 @@ import theme from 'shared/theme'; import styled from 'styled-components'; import { zIndex } from 'src/components/globals'; import { Link } from 'react-router-dom'; +import { MEDIA_BREAK } from 'src/components/layout'; export const HoverWrapper = styled.div` - position: absolute; - padding: 8px 0; + padding: 12px; + margin-left: -12px; z-index: ${zIndex.tooltip}; width: 256px; ${props => props.popperStyle}; - @media (max-width: 768px) { + @media (max-width: ${MEDIA_BREAK}px) { display: none; pointer-events: none; } @@ -38,20 +39,29 @@ export const ProfileCard = styled.div` `; export const Content = styled.div` - padding: 0 8px; + padding: 0 12px; `; export const Title = styled.h2` font-size: 20px; font-weight: 700; color: ${theme.text.default}; + line-height: 1.2; +`; + +export const Username = styled.h3` + font-size: 16px; + font-weight: 400; + color: ${theme.text.alt}; + line-height: 1.2; + margin-top: 4px; `; export const Description = styled.p` font-size: 14px; font-weight: 400; color: ${theme.text.secondary}; - margin-top: 4px; + margin-top: 8px; line-height: 1.4; white-space: pre-wrap; @@ -66,7 +76,7 @@ export const Description = styled.p` `; export const Actions = styled.div` - padding: 16px 8px 8px; + padding: 16px 12px 12px; display: flex; flex: 1 0 auto; @@ -100,7 +110,7 @@ export const CoverPhoto = styled.div` export const ProfilePhotoContainer = styled.div` position: relative; - left: 8px; + left: 12px; top: 50%; transform: translateY(-50%); margin-bottom: -16px; diff --git a/src/components/hoverProfile/userContainer.js b/src/components/hoverProfile/userContainer.js index 9a50b8463a..d9811f213f 100644 --- a/src/components/hoverProfile/userContainer.js +++ b/src/components/hoverProfile/userContainer.js @@ -17,18 +17,12 @@ import LoadingHoverProfile from './loadingHoverProfile'; const MentionHoverProfile = getUserByUsername(props => { if (props.data && props.data.user) { return ( - + ); } if (props.data && props.data.loading) { - return ( - - ); + return ; } return null; @@ -47,93 +41,87 @@ type State = { }; class UserHoverProfileWrapper extends React.Component { - // ref: ?any; - // ref = null; - // state = { visible: false }; - // _isMounted = false; - - // componentDidMount() { - // this._isMounted = true; - // } - - // componentWillUnmount() { - // this._isMounted = false; - // } - - // handleMouseEnter = () => { - // const { username, client } = this.props; - - // if (!this._isMounted) return; - - // client - // .query({ - // query: getUserByUsernameQuery, - // variables: { username }, - // }) - // .then(() => { - // if (!this._isMounted) return; - // }); - - // const ref = setTimeout(() => { - // if (this._isMounted) { - // return this.setState({ visible: true }); - // } - // }, 500); - // this.ref = ref; - // }; - - // handleMouseLeave = () => { - // if (this.ref) { - // clearTimeout(this.ref); - // } - - // if (this._isMounted && this.state.visible) { - // this.setState({ visible: false }); - // } - // }; + ref: ?any; + ref = null; + state = { visible: false }; + _isMounted = false; + + componentDidMount() { + this._isMounted = true; + } + + componentWillUnmount() { + this._isMounted = false; + } + + handleMouseEnter = () => { + const { username, client } = this.props; + + if (!this._isMounted) return; + + client + .query({ + query: getUserByUsernameQuery, + variables: { username }, + }) + .then(() => { + if (!this._isMounted) return; + }); + + const ref = setTimeout(() => { + if (this._isMounted) { + return this.setState({ visible: true }); + } + }, 500); + this.ref = ref; + }; + + handleMouseLeave = () => { + if (this.ref) { + clearTimeout(this.ref); + } + + if (this._isMounted && this.state.visible) { + this.setState({ visible: false }); + } + }; render() { - return this.props.children; - // const { children, currentUser, username, style = {} } = this.props; - // const me = currentUser && currentUser.username === username; - // return ( - // - // - // - // {({ ref }) => ( - // - // {children} - // - // )} - // - // {this.state.visible && - // document.body && - // createPortal( - // - // {({ style, ref }) => ( - // - // )} - // , - // document.body - // )} - // - // - // ); + const { children, currentUser, username, style = {} } = this.props; + const me = currentUser && currentUser.username === username; + return ( + + + + {({ ref }) => ( + + {children} + + )} + + {this.state.visible && + document.body && + createPortal( + + {({ style, ref, placement }) => ( + + + + )} + , + document.body + )} + + + ); } } diff --git a/src/components/hoverProfile/userProfile.js b/src/components/hoverProfile/userProfile.js index cab70cc456..ce2d90ab89 100644 --- a/src/components/hoverProfile/userProfile.js +++ b/src/components/hoverProfile/userProfile.js @@ -6,13 +6,13 @@ import { withRouter } from 'react-router'; import AvatarImage from 'src/components/avatar/image'; import { Link } from 'react-router-dom'; import Badge from 'src/components/badges'; -import { Button } from 'src/components/buttons'; +import { PrimaryOutlineButton, OutlineButton } from 'src/components/button'; import ConditionalWrap from 'src/components/conditionalWrap'; import type { GetUserType } from 'shared/graphql/queries/user/getUser'; import type { Dispatch } from 'redux'; import renderTextWithLinks from 'src/helpers/render-text-with-markdown-links'; -import { initNewThreadWithUser } from 'src/actions/directMessageThreads'; import { withCurrentUser } from 'src/components/withCurrentUser'; +import InitDirectMessageWrapper from 'src/components/initDirectMessageWrapper'; import { HoverWrapper, ProfileCard, @@ -21,6 +21,7 @@ import { ProfilePhotoContainer, Content, Title, + Username, Description, Actions, } from './style'; @@ -29,22 +30,17 @@ type ProfileProps = { user: GetUserType, dispatch: Dispatch, currentUser: ?Object, - innerRef: (?HTMLElement) => void, + ref: (?HTMLElement) => void, style: CSSStyleDeclaration, }; class HoverProfile extends Component { - initMessage = () => { - const { dispatch, user } = this.props; - dispatch(initNewThreadWithUser(user)); - }; - render() { - const { user, currentUser, innerRef, style } = this.props; + const { user, currentUser, ref, style } = this.props; const me = currentUser && currentUser.id === user.id; return ( - + { )} > {user.name} + @{user.username} {user.betaSupporter && ( @@ -83,18 +80,17 @@ class HoverProfile extends Component { {!me && ( - - - + + Message + + } + /> )} - {me && ( - - - - )} + {me && My profile} diff --git a/src/components/icons/index.js b/src/components/icon/index.js similarity index 96% rename from src/components/icons/index.js rename to src/components/icon/index.js index f792056368..f6240da2ec 100644 --- a/src/components/icons/index.js +++ b/src/components/icon/index.js @@ -2,15 +2,10 @@ import theme from 'shared/theme'; import React from 'react'; import styled, { css } from 'styled-components'; -import { Tooltip } from '../globals'; - -/* eslint no-eval: 0 */ type Props = { glyph: string, size?: number | string, - tipText?: string, - tipLocation?: string, count?: ?string, onClick?: Function, onboarding?: string, @@ -39,10 +34,6 @@ export const SvgWrapper = styled.div` position: relative; color: inherit; - @media (min-width: 768px) { - ${props => (props.tipText ? Tooltip(props) : '')}; - } - ${props => props.count && css` @@ -193,6 +184,28 @@ export const Glyph = ({ glyph }: GlyphProps) => { /> ); + case 'download': + return ( + + + + ); + case 'up-caret': + return ( + + + + ); case 'down-fill': return ( @@ -310,6 +323,22 @@ export const Glyph = ({ glyph }: GlyphProps) => { ); + case 'info': + return ( + + + + + + ); case 'inserter': return ( @@ -395,6 +424,16 @@ export const Glyph = ({ glyph }: GlyphProps) => { /> ); + case 'message-simple-new': + return ( + + + + ); case 'message-fill': return ( @@ -865,23 +904,12 @@ export const Glyph = ({ glyph }: GlyphProps) => { class Icon extends React.Component { render() { - const { - size = 32, - tipText, - tipLocation, - onboarding, - count, - onClick, - glyph, - dataCy, - } = this.props; + const { size = 32, onboarding, count, onClick, glyph, dataCy } = this.props; return ( { render() { - const { thread, active, currentUser } = this.props; + const { newMessages, thread, active, currentUser } = this.props; if (!thread) return null; @@ -25,6 +33,7 @@ class ThreadActivity extends React.Component { thread={thread} active={active} /> + {newMessages && (new)} ); } diff --git a/src/views/dashboard/components/inboxThread/header/index.js b/src/components/inboxThread/header/index.js similarity index 100% rename from src/views/dashboard/components/inboxThread/header/index.js rename to src/components/inboxThread/header/index.js diff --git a/src/views/dashboard/components/inboxThread/header/style.js b/src/components/inboxThread/header/style.js similarity index 87% rename from src/views/dashboard/components/inboxThread/header/style.js rename to src/components/inboxThread/header/style.js index b6bc67cab2..490a6dd5d6 100644 --- a/src/views/dashboard/components/inboxThread/header/style.js +++ b/src/components/inboxThread/header/style.js @@ -30,10 +30,11 @@ export const TextRow = styled.span` `; const metaTitleStyles = css` - font-size: 14px; - font-weight: 500; + font-size: 15px; + font-weight: 600; + line-height: 1.2; color: ${props => - props.active ? props.theme.text.reverse : props.theme.text.secondary}; + props.active ? props.theme.text.reverse : props.theme.text.default}; pointer-events: auto; position: relative; z-index: ${zIndex.card}; @@ -58,11 +59,11 @@ export const MetaTitleText = styled.span` `; const metaSubtitleStyles = css` - font-size: 14px; + font-size: 15px; font-weight: 400; color: ${props => props.active ? props.theme.text.reverse : props.theme.text.alt}; - line-height: 1.28; + line-height: 1.2; pointer-events: auto; position: relative; z-index: ${zIndex.card}; @@ -88,16 +89,16 @@ export const MetaSubtitleText = styled.span` } `; -export const Timestamp = styled(MetaSubtitleText)``; +export const Timestamp = styled(MetaTitleText)` + color: ${props => + props.active ? props.theme.text.reverse : props.theme.text.alt}; + font-weight: 400; +`; export const NewThreadTimestamp = styled(MetaSubtitleText)` + margin-left: 4px; color: ${props => - props.active ? props.theme.text.reverse : props.theme.success.default}; - - &:hover { - color: ${props => - props.active ? props.theme.text.reverse : props.theme.success.default}; - } + props.active ? props.theme.text.reverse : props.theme.brand.default}; `; export const MetaSubtitlePinned = styled(MetaSubtitleText)` diff --git a/src/views/dashboard/components/inboxThread/header/threadHeader.js b/src/components/inboxThread/header/threadHeader.js similarity index 94% rename from src/views/dashboard/components/inboxThread/header/threadHeader.js rename to src/components/inboxThread/header/threadHeader.js index b771c4f256..9d923555dc 100644 --- a/src/views/dashboard/components/inboxThread/header/threadHeader.js +++ b/src/components/inboxThread/header/threadHeader.js @@ -25,7 +25,15 @@ class Header extends React.Component { const { active, viewContext, - thread: { author, community, channel, id, watercooler, isLocked }, + thread: { + author, + community, + channel, + id, + watercooler, + isLocked, + editedBy, + }, } = this.props; const isPinned = id === community.pinnedThreadId; @@ -96,7 +104,7 @@ class Header extends React.Component { active={active ? 'true' : undefined} to={`/${community.slug}/${channel.slug}`} > - {channel.name} + # {channel.name} )} diff --git a/src/views/dashboard/components/inboxThread/header/timestamp.js b/src/components/inboxThread/header/timestamp.js similarity index 77% rename from src/views/dashboard/components/inboxThread/header/timestamp.js rename to src/components/inboxThread/header/timestamp.js index 4dab960d6e..0cda5302f0 100644 --- a/src/views/dashboard/components/inboxThread/header/timestamp.js +++ b/src/components/inboxThread/header/timestamp.js @@ -20,18 +20,20 @@ class ThreadTimestamp extends React.Component { const createdWithinLastDay = now - createdAtTime < 86400; const isAuthor = currentUser && currentUser.id === thread.author.user.id; - if ( + const showNewPost = !isAuthor && !thread.currentUserLastSeen && createdWithinLastDay && - !active - ) { - return ( - New thread - ); - } + !active; - return {timestamp}; + return ( + + {timestamp} + {showNewPost && ( + (new) + )} + + ); } } diff --git a/src/views/dashboard/components/inboxThread/header/userProfileThreadHeader.js b/src/components/inboxThread/header/userProfileThreadHeader.js similarity index 100% rename from src/views/dashboard/components/inboxThread/header/userProfileThreadHeader.js rename to src/components/inboxThread/header/userProfileThreadHeader.js diff --git a/src/views/dashboard/components/inboxThread/index.js b/src/components/inboxThread/index.js similarity index 69% rename from src/views/dashboard/components/inboxThread/index.js rename to src/components/inboxThread/index.js index 1cf8848051..4891b12787 100644 --- a/src/views/dashboard/components/inboxThread/index.js +++ b/src/components/inboxThread/index.js @@ -3,9 +3,7 @@ import * as React from 'react'; import compose from 'recompose/compose'; import { connect } from 'react-redux'; import { withRouter } from 'react-router'; -import truncate from 'shared/truncate'; import Header from './header'; -import { changeActiveThread } from 'src/actions/dashboardFeed'; import getThreadLink from 'src/helpers/get-thread-link'; import type { ThreadInfoType } from 'shared/graphql/fragments/thread/threadInfo'; import type { Dispatch } from 'redux'; @@ -14,6 +12,7 @@ import { InboxLinkWrapper, InboxThreadContent, ThreadTitle, + ThreadSnippet, Column, AvatarLink, CommunityAvatarLink, @@ -22,6 +21,8 @@ import { UserAvatar, CommunityAvatar } from 'src/components/avatar'; import ThreadActivity from './activity'; import { ErrorBoundary } from 'src/components/error'; import { withCurrentUser } from 'src/components/withCurrentUser'; +import getSnippet from 'shared/clients/draft-js/utils/getSnippet'; +import truncate from 'shared/truncate'; type Props = { active: boolean, @@ -49,47 +50,32 @@ class InboxThread extends React.Component { active, viewContext = null, currentUser, - location, } = this.props; - const isInbox = - viewContext && - (viewContext === 'inbox' || - viewContext === 'communityInbox' || - viewContext === 'channelInbox'); - const newMessagesSinceLastViewed = !active && thread.currentUserLastSeen && thread.lastActive && thread.currentUserLastSeen < thread.lastActive; + const newUnseenThread = + !active && + currentUser && + !thread.currentUserLastSeen && + thread.community.communityPermissions.isMember && + currentUser.id !== thread.author.user.id; + return ( - - + + { - const isMobile = window.innerWidth < 768; - if (isMobile && isInbox) { - evt.preventDefault(); - this.props.history.push({ - pathname: getThreadLink(thread), - state: { modal: true }, - }); - } else if (!isMobile) { - this.props.dispatch(changeActiveThread(thread.id)); - } + to={{ + pathname: getThreadLink(thread), + state: { modal: true }, }} /> @@ -113,7 +99,7 @@ class InboxThread extends React.Component { )} - +
{ /> - + {truncate(thread.content.title, 80)} - + + {getSnippet(JSON.parse(thread.content.body))} + + + diff --git a/src/components/inboxThread/messageCount.js b/src/components/inboxThread/messageCount.js new file mode 100644 index 0000000000..a53d40fabf --- /dev/null +++ b/src/components/inboxThread/messageCount.js @@ -0,0 +1,29 @@ +// @flow +import * as React from 'react'; +import type { GetThreadType } from 'shared/graphql/queries/thread/getThread'; +import Icon from 'src/components/icon'; +import { CountWrapper } from './style'; + +type Props = { + currentUser: ?Object, + thread: GetThreadType, + active: boolean, +}; + +class MessageCount extends React.Component { + render() { + const { + thread: { messageCount }, + active, + } = this.props; + + return ( + + + {messageCount} + + ); + } +} + +export default MessageCount; diff --git a/src/views/dashboard/components/inboxThread/style.js b/src/components/inboxThread/style.js similarity index 75% rename from src/views/dashboard/components/inboxThread/style.js rename to src/components/inboxThread/style.js index e313aa92c0..212ba57c01 100644 --- a/src/views/dashboard/components/inboxThread/style.js +++ b/src/components/inboxThread/style.js @@ -2,7 +2,7 @@ import theme from 'shared/theme'; import styled, { css } from 'styled-components'; import { Link } from 'react-router-dom'; -import { zIndex, Tooltip } from 'src/components/globals'; +import { zIndex, hexa } from 'src/components/globals'; export const InboxThreadItem = styled.div` display: flex; @@ -11,20 +11,35 @@ export const InboxThreadItem = styled.div` min-width: 0; overflow-x: hidden; border-bottom: 1px solid - ${props => (props.active ? props.theme.brand.alt : props.theme.bg.border)}; + ${props => (props.active ? props.theme.brand.alt : props.theme.bg.divider)}; background: ${props => - props.active ? props.theme.brand.alt : props.theme.bg.default}; + props.active + ? props.theme.brand.alt + : props.new + ? hexa(theme.brand.default, 0.04) + : props.theme.bg.default}; position: relative; padding: 12px 20px 12px 12px; + ${props => + props.new && + css` + box-shadow: inset 2px 0 0 ${theme.brand.default}; + `} + &:hover { background: ${props => - props.active ? props.theme.brand.alt : props.theme.bg.wash}; + props.active + ? props.theme.brand.alt + : props.new + ? hexa(theme.brand.default, 0.06) + : props.theme.bg.wash}; } &:last-of-type { border-bottom: 1px solid - ${props => (props.active ? props.theme.brand.alt : props.theme.bg.border)}; + ${props => + props.active ? props.theme.brand.alt : props.theme.bg.divider}; } `; @@ -63,17 +78,29 @@ export const Column = styled.div` `; export const ThreadTitle = styled.h3` - font-size: 16px; - font-weight: ${props => (props.new ? '600' : '400')}; + font-size: 15px; + font-weight: 600; + color: ${props => + props.active ? props.theme.text.reverse : props.theme.text.default}; + max-width: 100%; + line-height: 1.4; +`; + +export const ThreadSnippet = styled.h4` + font-size: 15px; + font-weight: 400; color: ${props => props.active ? props.theme.text.reverse : props.theme.text.default}; max-width: 100%; line-height: 1.4; + margin-top: 4px; + word-break: break-word; + white-space: pre-line; `; export const ThreadActivityWrapper = styled.div` display: flex; - margin-top: 12px; + margin-top: 8px; align-items: center; .icon { @@ -112,7 +139,7 @@ export const CountWrapper = styled.div` : props.active ? props.theme.text.reverse : props.theme.text.alt}; - font-weight: 600; + font-weight: 500; align-items: center; .icon { @@ -126,14 +153,6 @@ export const CountWrapper = styled.div` a:hover { color: ${theme.text.default}; } - - ${Tooltip}; -`; - -export const NewCount = styled.span` - margin-left: 6px; - color: ${props => - props.active ? props.theme.text.reverse : props.theme.warn.alt}; `; const avatarLinkStyles = css` diff --git a/src/components/infiniteScroll/index.js b/src/components/infiniteScroll/index.js index a7afbdec5f..8c7129657b 100644 --- a/src/components/infiniteScroll/index.js +++ b/src/components/infiniteScroll/index.js @@ -1,206 +1,32 @@ // @flow -import * as React from 'react'; -import { fetchMoreOnInfiniteScrollLoad } from './tallViewports'; +import React from 'react'; +import InfiniteScroll from 'react-infinite-scroller-fork-mxstbr'; +import { useAppScroller } from 'src/hooks/useAppScroller'; type Props = { - element: string, - hasMore: boolean, - initialLoad: boolean, - loader: React.Node, loadMore: Function, - isLoadingMore: boolean, - pageStart: number, - threshold: number, - useWindow: boolean, - isReverse: boolean, - scrollElement: ?Object, - children: Array | ?React.Node, - className: string, + hasMore: boolean, + loader: React$Node, + isReverse?: boolean, }; -export default class InfiniteScroll extends React.Component { - static defaultProps = { - element: 'div', - hasMore: false, - initialLoad: true, - pageStart: 0, - threshold: 250, - useWindow: true, - isReverse: false, - scrollElement: null, - }; - - scrollListener: Function; - scrollComponent: Object; - pageLoaded: number; - _defaultLoader: React.Node; - - constructor(props: Props) { - super(props); - - this._defaultLoader = props.loader; - this.scrollListener = this.scrollListener.bind(this); - } - - componentDidMount() { - this.pageLoaded = this.props.pageStart; - this.attachScrollListener(); - } - - componentDidUpdate(prevProps: Props) { - const curr = this.props; - - /* - if the outer query is fetching more, there's no reason to re-check the scroll - position or re-attach a scroll listener - a refetch is already running! - */ - if (curr.isLoadingMore) { - return; - } - - this.attachScrollListener(); - } - - render() { - const { - children, - element, - hasMore, - initialLoad, - loader, - loadMore, - pageStart, - threshold, - useWindow, - isReverse, - scrollElement, - isLoadingMore, - ...props - } = this.props; - - if (scrollElement) { - // $FlowFixMe - props.ref = node => { - this.scrollComponent = scrollElement; - }; - } else { - // $FlowFixMe - props.ref = node => { - this.scrollComponent = node; - }; - } - - return React.createElement( - element, - props, - children, - hasMore && (loader || this._defaultLoader) - ); - } - - calculateTopPosition(el: ?any) { - if (!el) { - return 0; - } - return el.offsetTop + this.calculateTopPosition(el.offsetParent); - } - - scrollListener() { - const el = this.scrollComponent; - const scrollEl = window; - - let offset; - if (this.props.scrollElement) { - if (this.props.isReverse) { - offset = el.scrollTop; - } else offset = el.scrollHeight - el.scrollTop - el.clientHeight; - } else if (this.props.useWindow) { - let scrollTop = - scrollEl.pageYOffset !== undefined - ? scrollEl.pageYOffset - : //$FlowFixMe - ( - document.documentElement || - //$FlowFixMe - document.body.parentNode || - document.body - ).scrollTop; - if (this.props.isReverse) offset = scrollTop; - else - offset = - this.calculateTopPosition(el) + - el.offsetHeight - - scrollTop - - window.innerHeight; - } else { - if (this.props.isReverse) offset = el.parentNode.scrollTop; - else - offset = - el.scrollHeight - - el.parentNode.scrollTop - - el.parentNode.clientHeight; - } - - if ( - offset < Number(this.props.threshold) || - fetchMoreOnInfiniteScrollLoad(el, this.props.className) - ) { - this.detachScrollListener(); - // Call loadMore after detachScrollListener to allow for non-async loadMore functions - if ( - typeof this.props.loadMore === 'function' && - !this.props.isLoadingMore - ) { - this.props.loadMore((this.pageLoaded += 1)); - } - } - } - - attachScrollListener() { - if (!this.props.hasMore) { - return; - } - - let scrollEl = window; - if (this.props.scrollElement) { - scrollEl = this.scrollComponent; - } else if (this.props.useWindow === false) { - scrollEl = this.scrollComponent.parentNode; - } - - scrollEl.addEventListener('scroll', this.scrollListener); - scrollEl.addEventListener('resize', this.scrollListener); - - if ( - fetchMoreOnInfiniteScrollLoad(scrollEl, this.props.className) && - !this.props.isLoadingMore - ) { - this.props.loadMore((this.pageLoaded += 1)); - } - - if (this.props.initialLoad) { - this.scrollListener(); - } - } - - detachScrollListener() { - let scrollEl = window; - if (this.props.scrollElement) { - scrollEl = this.scrollComponent; - } else if (this.props.useWindow === false) { - scrollEl = this.scrollComponent.parentNode; - } - - scrollEl.removeEventListener('scroll', this.scrollListener); - scrollEl.removeEventListener('resize', this.scrollListener); - } - - componentWillUnmount() { - this.detachScrollListener(); - } +/* + Because route modals (like the thread modal) share the same scroll container + as all other views, we want to make sure that if a modal is open that we + aren't performing unnecessary pagination in the background +*/ +const InfiniteScroller = (props: Props) => { + const { ref } = useAppScroller(); + + return ( + ref} + {...props} + /> + ); +}; - // Set a defaut loader for all your `InfiniteScroll` components - setDefaultLoader(loader: React.Node) { - this._defaultLoader = loader; - } -} +export default InfiniteScroller; diff --git a/src/components/initDirectMessageWrapper/index.js b/src/components/initDirectMessageWrapper/index.js new file mode 100644 index 0000000000..7e21417325 --- /dev/null +++ b/src/components/initDirectMessageWrapper/index.js @@ -0,0 +1,48 @@ +// @flow +import * as React from 'react'; +import { connect } from 'react-redux'; +import compose from 'recompose/compose'; +import { withRouter, type History } from 'react-router-dom'; +import { withCurrentUser } from 'src/components/withCurrentUser'; +import type { GetUserType } from 'shared/graphql/queries/user/getUser'; +import { initNewThreadWithUser } from 'src/actions/directMessageThreads'; + +type Props = { + render: Function, + children: any, + dispatch: Dispatch, + currentUser: ?GetUserType, + user: Object, + history: History, +}; + +const InitDirectMessage = (props: Props) => { + const { dispatch, history, currentUser, render, user } = props; + + const init = (e: any) => { + e && e.preventDefault() && e.stopPropogation(); + dispatch(initNewThreadWithUser(user)); + history.push({ + pathname: currentUser ? `/new/message` : '/login', + state: { modal: !!currentUser }, + }); + }; + + if (currentUser && currentUser.id === user.id) return null; + + return ( + + {render} + + ); +}; + +export default compose( + connect(), + withCurrentUser, + withRouter +)(InitDirectMessage); diff --git a/src/components/joinChannelWrapper/index.js b/src/components/joinChannelWrapper/index.js new file mode 100644 index 0000000000..2d2edaca81 --- /dev/null +++ b/src/components/joinChannelWrapper/index.js @@ -0,0 +1,76 @@ +// @flow +import * as React from 'react'; +import { connect } from 'react-redux'; +import compose from 'recompose/compose'; +import toggleChannelSubscriptionMutation, { + type ToggleChannelSubscriptionType, +} from 'shared/graphql/mutations/channel/toggleChannelSubscription'; +import type { ChannelInfoType } from 'shared/graphql/fragments/channel/channelInfo'; +import { addToastWithTimeout } from 'src/actions/toasts'; +import { openModal } from 'src/actions/modals'; +import { withCurrentUser } from 'src/components/withCurrentUser'; +import type { GetUserType } from 'shared/graphql/queries/user/getUser'; + +type Props = { + channel: ChannelInfoType, + render: Function, + children: any, + toggleChannelSubscription: Function, + dispatch: Dispatch, + currentUser: ?GetUserType, +}; + +const JoinChannel = (props: Props) => { + const { + channel, + toggleChannelSubscription, + currentUser, + dispatch, + render, + } = props; + const [isLoading, setIsLoading] = React.useState(false); + + const join = (e: any) => { + e && e.preventDefault() && e.stopPropogation(); + + if (!currentUser || !currentUser.id) { + return dispatch(openModal('LOGIN_MODAL')); + } + + setIsLoading(true); + + return toggleChannelSubscription({ channelId: channel.id }) + .then(({ data }: ToggleChannelSubscriptionType) => { + const { toggleChannelSubscription: channel } = data; + dispatch( + addToastWithTimeout('success', `Joined the ${channel.name} channel!`) + ); + + return setIsLoading(false); + }) + .catch(err => { + dispatch(addToastWithTimeout('error', err.message)); + return setIsLoading(false); + }); + }; + + const { channelPermissions } = channel; + const { isMember } = channelPermissions; + const cy = currentUser + ? isMember + ? null + : 'channel-join-button' + : 'channel-login-join-button'; + + return ( + + {render({ isLoading })} + + ); +}; + +export default compose( + connect(), + toggleChannelSubscriptionMutation, + withCurrentUser +)(JoinChannel); diff --git a/src/components/joinCommunityWrapper/index.js b/src/components/joinCommunityWrapper/index.js new file mode 100644 index 0000000000..2e235f3324 --- /dev/null +++ b/src/components/joinCommunityWrapper/index.js @@ -0,0 +1,74 @@ +// @flow +import * as React from 'react'; +import { connect } from 'react-redux'; +import compose from 'recompose/compose'; +import addCommunityMemberMutation from 'shared/graphql/mutations/communityMember/addCommunityMember'; +import type { CommunityInfoType } from 'shared/graphql/fragments/community/communityInfo'; +import { addToastWithTimeout } from 'src/actions/toasts'; +import { withCurrentUser } from 'src/components/withCurrentUser'; +import type { AddCommunityMemberType } from 'shared/graphql/mutations/communityMember/addCommunityMember'; +import type { GetUserType } from 'shared/graphql/queries/user/getUser'; +import { openModal } from 'src/actions/modals'; + +type Props = { + community: CommunityInfoType, + render: Function, + children: any, + addCommunityMember: Function, + dispatch: Dispatch, + currentUser: ?GetUserType, +}; + +const JoinCommunity = (props: Props) => { + const { + community, + addCommunityMember, + dispatch, + currentUser, + render, + } = props; + const [isLoading, setIsLoading] = React.useState(false); + + const addMember = () => { + if (!currentUser || !currentUser.id) { + return dispatch(openModal('LOGIN_MODAL')); + } + + const input = { communityId: community.id }; + + setIsLoading(true); + + return addCommunityMember({ input }) + .then(({ data }: AddCommunityMemberType) => { + const { addCommunityMember: community } = data; + dispatch( + addToastWithTimeout( + 'success', + `Welcome to the ${community.name} community!` + ) + ); + + return setIsLoading(false); + }) + .catch(err => { + dispatch(addToastWithTimeout('error', err.message)); + return setIsLoading(false); + }); + }; + + const cy = currentUser + ? 'join-community-button' + : 'join-community-button-login'; + + return ( + + {render({ isLoading })} + + ); +}; + +export default compose( + connect(), + addCommunityMemberMutation, + withCurrentUser +)(JoinCommunity); diff --git a/src/components/layout/index.js b/src/components/layout/index.js new file mode 100644 index 0000000000..353723dcc4 --- /dev/null +++ b/src/components/layout/index.js @@ -0,0 +1,190 @@ +// @flow +import styled from 'styled-components'; +import theme from 'shared/theme'; +import { isDesktopApp } from 'src/helpers/desktop-app-utils'; + +export const NAVBAR_WIDTH = isDesktopApp() ? 80 : 72; +export const PRIMARY_COLUMN_WIDTH = 600; +export const SECONDARY_COLUMN_WIDTH = 340; +export const COL_GAP = 16; +export const TITLEBAR_HEIGHT = isDesktopApp() ? 82 : 62; +export const MAX_WIDTH = + PRIMARY_COLUMN_WIDTH + SECONDARY_COLUMN_WIDTH + COL_GAP; +export const SINGLE_COLUMN_WIDTH = MAX_WIDTH; +// add 144 (72 * 2) to account for the left side nav +export const MEDIA_BREAK = + PRIMARY_COLUMN_WIDTH + SECONDARY_COLUMN_WIDTH + COL_GAP + NAVBAR_WIDTH * 2; + +/* + do not remove this className. + see `src/routes.js` for an explanation of what's going on here +*/ +export const ViewGrid = styled.main.attrs({ + id: 'main', + className: 'view-grid', +})` + display: grid; + grid-area: main; + max-height: 100vh; + overflow: hidden; + overflow-y: auto; + + @media (max-width: ${MEDIA_BREAK}px) { + max-height: calc(100vh - ${TITLEBAR_HEIGHT}px); + } +`; + +/* +┌──┬────────┬──┐ +│ │ xx │ │ +│ │ │ │ +│ │ xx │ │ +│ │ │ │ +│ │ xx │ │ +└──┴────────┴──┘ +*/ +export const SingleColumnGrid = styled.div` + display: grid; + justify-self: center; + grid-template-columns: ${MAX_WIDTH}px; + background: ${theme.bg.default}; + + @media (max-width: ${MEDIA_BREAK}px) { + width: 100%; + max-width: 100%; + grid-template-columns: 1fr; + border-left: 0; + border-right: 0; + } +`; + +/* +┌──┬────┬─┬─┬──┐ +│ │ xx │ │x│ │ +│ │ │ │ │ │ +│ │ xx │ │x│ │ +│ │ │ │ │ │ +│ │ xx │ │x│ │ +└──┴────┴─┴─┴──┘ +*/ +export const PrimarySecondaryColumnGrid = styled.div` + display: grid; + justify-self: center; + grid-template-columns: ${PRIMARY_COLUMN_WIDTH}px ${SECONDARY_COLUMN_WIDTH}px; + grid-template-rows: 100%; + grid-template-areas: 'primary secondary'; + grid-gap: ${COL_GAP}px; + max-width: ${MAX_WIDTH}px; + + @media (max-width: ${MEDIA_BREAK}px) { + grid-template-columns: 1fr; + grid-template-rows: min-content 1fr; + grid-gap: 0; + min-width: 100%; + } +`; + +/* +┌──┬─┬─┬────┬──┐ +│ │x│ │ xx │ │ +│ │ │ │ │ │ +│ │x│ │ xx │ │ +│ │ │ │ │ │ +│ │x│ │ xx │ │ +└──┴─┴─┴────┴──┘ +*/ +export const SecondaryPrimaryColumnGrid = styled.div` + display: grid; + justify-self: center; + grid-template-columns: ${SECONDARY_COLUMN_WIDTH}px ${PRIMARY_COLUMN_WIDTH}px; + grid-template-rows: 100%; + grid-template-areas: 'secondary primary'; + grid-gap: ${COL_GAP}px; + max-width: ${MAX_WIDTH}px; + + @media (max-width: ${MEDIA_BREAK}px) { + grid-template-columns: 1fr; + grid-template-rows: 1fr; + grid-gap: 0; + min-width: 100%; + max-width: 100%; + } +`; + +/* +┌─────────────┐ +│ │ +│ ┌───┐ │ +│ │ x │ │ +│ └───┘ │ +│ │ +└─────────────┘ +*/ +export const CenteredGrid = styled.div` + display: grid; + justify-self: center; + grid-template-columns: ${MAX_WIDTH}px; + align-self: center; + max-width: ${PRIMARY_COLUMN_WIDTH}px; + grid-template-columns: ${PRIMARY_COLUMN_WIDTH}px; + + @media (max-width: ${MEDIA_BREAK}px) { + align-self: flex-start; + width: 100%; + max-width: 100%; + grid-template-columns: 1fr; + height: calc(100vh - ${TITLEBAR_HEIGHT}px); + } +`; + +export const PrimaryColumn = styled.section` + border-left: 1px solid ${theme.bg.border}; + border-right: 1px solid ${theme.bg.border}; + border-bottom: 1px solid ${theme.bg.border}; + border-radius: 0 0 4px 4px; + height: 100%; + max-width: ${PRIMARY_COLUMN_WIDTH}px; + grid-area: primary; + display: grid; + grid-template-rows: 1fr; + + @media (max-width: ${MEDIA_BREAK}px) { + border-left: 0; + border-right: 0; + border-bottom: 0; + grid-column-start: 1; + max-width: 100%; + height: calc(100vh - ${TITLEBAR_HEIGHT}px); + } +`; + +export const SecondaryColumn = styled.section` + height: 100vh; + overflow: hidden; + overflow-y: auto; + position: sticky; + top: 0; + padding-bottom: 48px; + padding-right: 12px; + grid-area: secondary; + + @media (max-width: ${MEDIA_BREAK}px) { + height: calc(100vh - ${TITLEBAR_HEIGHT}px); + display: none; + } +`; + +export const ChatInputWrapper = styled.div` + position: sticky; + bottom: 0; + left: 0; + width: 100%; + z-index: 3000; + + @media (max-width: ${MEDIA_BREAK}px) { + width: 100vw; + position: fixed; + bottom: 0; + left: 0; + } +`; diff --git a/src/components/layouts/index.js b/src/components/layouts/index.js deleted file mode 100644 index b526ab5153..0000000000 --- a/src/components/layouts/index.js +++ /dev/null @@ -1,32 +0,0 @@ -import React from 'react'; -import styled from 'styled-components'; - -const GridReset = styled.div` - display: grid; - margin: 0; - padding: 0; - max-height: 100%; - max-width: 100%; -`; - -// Describes a layout with a fixed 48px tall row called "header" at the top and a "body" row that fills the rest of the vertical space within the parent. -export const HeaderGrid = styled(GridReset)` - grid-template-rows: 48px 1fr; - grid-template-columns: 100%; - grid-template-areas: 'header' 'body'; -`; - -// Describes a layout with a fixed 48px tall row called "footer" at the bottom and a "body" row that fills the rest of the vertical space within the component. -export const FooterGrid = styled(GridReset)` - overflow: hidden; - grid-template-rows: 1fr 64px; - grid-template-columns: 100%; - grid-template-areas: 'body' 'footer'; -`; - -// Describes a layout with a 320 - 480px wide "master" column on the left and a "detail" column that fills the rest of the horizontal space within the component. -export const MasterDetailGrid = styled(GridReset)` - grid-template-rows: 100%; - grid-template-columns: minmax(320px, 480px) 1fr; - grid-template-areas: 'master detail'; -`; diff --git a/src/components/leaveChannelWrapper/index.js b/src/components/leaveChannelWrapper/index.js new file mode 100644 index 0000000000..443d9a9f03 --- /dev/null +++ b/src/components/leaveChannelWrapper/index.js @@ -0,0 +1,63 @@ +// @flow +import * as React from 'react'; +import { connect } from 'react-redux'; +import compose from 'recompose/compose'; +import toggleChannelSubscriptionMutation, { + type ToggleChannelSubscriptionType, +} from 'shared/graphql/mutations/channel/toggleChannelSubscription'; +import type { ChannelInfoType } from 'shared/graphql/fragments/channel/channelInfo'; +import { addToastWithTimeout } from 'src/actions/toasts'; + +type Props = { + channel: ChannelInfoType, + render: Function, + children: any, + toggleChannelSubscription: Function, + dispatch: Dispatch, +}; + +const LeaveChannel = (props: Props) => { + const { channel, toggleChannelSubscription, dispatch, render } = props; + const [isLoading, setIsLoading] = React.useState(false); + const [isHovering, setHover] = React.useState(false); + + const onMouseEnter = () => setHover(true); + const onMouseLeave = () => setHover(false); + + const leave = (e: any) => { + e && e.preventDefault() && e.stopPropogation(); + + setIsLoading(true); + + return toggleChannelSubscription({ channelId: channel.id }) + .then(({ data }: ToggleChannelSubscriptionType) => { + const { toggleChannelSubscription: channel } = data; + dispatch( + addToastWithTimeout('neutral', `Left the ${channel.name} channel!`) + ); + + return setIsLoading(false); + }) + .catch(err => { + dispatch(addToastWithTimeout('error', err.message)); + return setIsLoading(false); + }); + }; + + return ( + + {render({ isLoading, isHovering })} + + ); +}; + +export default compose( + connect(), + toggleChannelSubscriptionMutation +)(LeaveChannel); diff --git a/src/components/listItems/channel/index.js b/src/components/listItems/channel/index.js index d001975b34..aa2fe097ff 100644 --- a/src/components/listItems/channel/index.js +++ b/src/components/listItems/channel/index.js @@ -10,7 +10,7 @@ import type { ChannelInfoType } from 'shared/graphql/fragments/channel/channelIn import { ChannelHoverProfile } from 'src/components/hoverProfile'; type Props = { - children: React.Node, + children: React$Node, channel: ChannelInfoType, }; diff --git a/src/components/listItems/index.js b/src/components/listItems/index.js index fc050d3967..38a6ae85ab 100644 --- a/src/components/listItems/index.js +++ b/src/components/listItems/index.js @@ -46,6 +46,7 @@ export class CommunityListItem extends React.Component { {/* greater than -1 because we want to pass the 0 to the component so it returns null */} {typeof reputation === 'number' && reputation > -1 && ( + {/* $FlowIssue */} )} @@ -127,6 +128,7 @@ export const UserListItem = ({ {!hideRep && ( {(user.totalReputation || user.contextPermissions) && ( + /* $FlowIssue */ )} diff --git a/src/components/listItems/style.js b/src/components/listItems/style.js index 3a2126e622..74833976ac 100644 --- a/src/components/listItems/style.js +++ b/src/components/listItems/style.js @@ -1,6 +1,8 @@ +// @flow import styled from 'styled-components'; import theme from 'shared/theme'; import { Link } from 'react-router-dom'; +import { MEDIA_BREAK } from 'src/components/layout'; import { Truncate, FlexCol, @@ -8,7 +10,7 @@ import { H3, H4, Transition, -} from '../../components/globals'; +} from 'src/components/globals'; export const Wrapper = styled(FlexCol)` flex: 1 0 auto; @@ -95,7 +97,7 @@ export const StyledCard = styled.div` flex-direction: column; display: ${props => (props.smallOnly ? 'none' : 'flex')}; - @media (max-width: 768px) { + @media (max-width: ${MEDIA_BREAK}px) { display: ${props => (props.largeOnly ? 'none' : 'flex')}; } `; diff --git a/src/components/loading/index.js b/src/components/loading/index.js index 3b91c3152b..6b7e081335 100644 --- a/src/components/loading/index.js +++ b/src/components/loading/index.js @@ -1,17 +1,12 @@ // @flow import React from 'react'; -//$FlowFixMe import branch from 'recompose/branch'; -//$FlowFixMe import renderComponent from 'recompose/renderComponent'; -// $FlowFixMe import styled from 'styled-components'; import { Spinner, FlexCol } from '../globals'; import { Card } from '../card'; -import { Column } from '../column'; import { ThreadViewContainer, Detail, Content } from '../../views/thread/style'; import { - LoadingScreenContainer, ShimmerList, ShimmerListLite, ShimmerInboxThread, @@ -34,10 +29,6 @@ import { LoadingNavbarContainer, LogoLink, Logo, - GridProfile, - Meta, - GridContent, - LoadingCoverPhoto, } from './style'; /* @@ -71,11 +62,12 @@ const LoadingCardContainer = styled(Card)` export const Loading = ({ size, color, + ...rest }: { size?: number, color?: string, }): React$Element => ( - + ); @@ -1029,20 +1021,6 @@ export const ErrorSelect = ({ children }: Props) => ( {children} ); -export const LoadingScreen = (): React$Element => ( - - - - - - - - - - - -); - export const LoadingThreadView = (): React$Element => ( @@ -1053,14 +1031,6 @@ export const LoadingThreadView = (): React$Element => ( ); -export const LoadingNotifications = (): React$Element => ( - - - - - -); - export const displayLoadingState = branch( props => !props.data || props.data.loading, renderComponent(Loading) @@ -1085,21 +1055,11 @@ export const displayLoadingCard = branch( renderComponent(LoadingCard) ); -export const displayLoadingScreen = branch( - props => !props.data || props.data.loading, - renderComponent(LoadingScreen) -); - export const displayLoadingThreadView = branch( props => !props.data, renderComponent(LoadingThreadView) ); -export const displayLoadingNotifications = branch( - props => !props.data || props.data.loading, - renderComponent(LoadingNotifications) -); - export const displayLoadingComposer = branch( props => !props.data.user && !props.data.error, renderComponent(LoadingComposer) diff --git a/src/components/loading/style.js b/src/components/loading/style.js index ef7fe0c5ea..9ed399cfc2 100644 --- a/src/components/loading/style.js +++ b/src/components/loading/style.js @@ -1,36 +1,10 @@ // @flow import theme from 'shared/theme'; -// $FlowFixMe import styled, { keyframes } from 'styled-components'; -import { Card } from '../card'; -import { hexa, FlexCol, zIndex } from '../globals'; -// $FlowFixMe +import { Card } from 'src/components/card'; +import { hexa, FlexCol, zIndex } from 'src/components/globals'; import { Link } from 'react-router-dom'; - -const containerFadeIn = keyframes` - 0%{ - opacity: 0; - } - 99% { - opacity: 0; - } - 100%{ - opacity: 1 - } -`; - -export const LoadingScreenContainer = styled.div` - width: 100%; - height: 100%; - display: flex; - justify-content: center; - margin-top: 32px; - animation-duration: 1s; - animation-fill-mode: forwards; - animation-iteration-count: 1; - animation-timing-function: ease-out; - animation-name: ${containerFadeIn}; -`; +import { MEDIA_BREAK } from 'src/components/layout'; export const ShimmerList = styled(Card)` padding: 16px; @@ -54,7 +28,7 @@ export const ShimmerThreadDetail = styled(FlexCol)` padding: 36px 32px; display: inline-block; - @media (max-width: 768px) { + @media (max-width: ${MEDIA_BREAK}px) { padding: 16px; } @@ -146,7 +120,7 @@ export const ShimmerComposer = styled(Card)` min-height: 32px; } - @media (max-width: 768px) { + @media (max-width: ${MEDIA_BREAK}px) { display: none; } `; @@ -162,7 +136,7 @@ export const ShimmerInboxComposer = styled.div` min-height: 32px; } - @media (max-width: 768px) { + @media (max-width: ${MEDIA_BREAK}px) { display: none; } `; @@ -178,7 +152,7 @@ export const ShimmerSelect = styled.div` background: ${theme.bg.default}; border: 2px solid ${theme.bg.border}; - @media (max-width: 768px) { + @media (max-width: ${MEDIA_BREAK}px) { width: calc(50% - 12px); } @@ -283,7 +257,7 @@ export const LoadingNavbarContainer = styled.nav` position: relative; } - @media (max-width: 768px) { + @media (max-width: ${MEDIA_BREAK}px) { bottom: 0; top: auto; box-shadow: 0 -4px 8px rgba(0, 0, 0, 0.15); @@ -294,7 +268,7 @@ export const LoadingNavbarContainer = styled.nav` export const LogoLink = styled(Link)` margin-right: 32px; - @media (max-width: 768px) { + @media (max-width: ${MEDIA_BREAK}px) { display: none; } `; @@ -326,7 +300,7 @@ export const GridProfile = styled.div` grid-template-areas: 'cover cover' 'meta content'; } - @media (max-width: 768px) { + @media (max-width: ${MEDIA_BREAK}px) { grid-template-rows: 80px auto 1fr; grid-template-columns: 100%; grid-column-gap: 0; diff --git a/src/components/loginButtonSet/facebook.js b/src/components/loginButtonSet/facebook.js index eefb21bf2a..5d25f655c6 100644 --- a/src/components/loginButtonSet/facebook.js +++ b/src/components/loginButtonSet/facebook.js @@ -2,7 +2,7 @@ import * as React from 'react'; import type { ButtonProps } from './'; import { FacebookButton, Label, A } from './style'; -import Icon from '../icons'; +import Icon from 'src/components/icon'; export const FacebookSigninButton = (props: ButtonProps) => { const { href, preferred, showAfter, onClickHandler } = props; diff --git a/src/components/loginButtonSet/github.js b/src/components/loginButtonSet/github.js index 99be8438e6..8af2534cb1 100644 --- a/src/components/loginButtonSet/github.js +++ b/src/components/loginButtonSet/github.js @@ -2,7 +2,7 @@ import * as React from 'react'; import type { ButtonProps } from './'; import { GithubButton, Label, A } from './style'; -import Icon from '../icons'; +import Icon from 'src/components/icon'; export const GithubSigninButton = (props: ButtonProps) => { const { href, preferred, showAfter, onClickHandler } = props; diff --git a/src/components/loginButtonSet/google.js b/src/components/loginButtonSet/google.js index f207e249f5..e9006f85e0 100644 --- a/src/components/loginButtonSet/google.js +++ b/src/components/loginButtonSet/google.js @@ -2,7 +2,7 @@ import * as React from 'react'; import type { ButtonProps } from './'; import { GoogleButton, Label, A } from './style'; -import Icon from '../icons'; +import Icon from 'src/components/icon'; export const GoogleSigninButton = (props: ButtonProps) => { const { href, preferred, showAfter, onClickHandler } = props; diff --git a/src/components/loginButtonSet/index.js b/src/components/loginButtonSet/index.js index f8e6d3fd84..d2328bf160 100644 --- a/src/components/loginButtonSet/index.js +++ b/src/components/loginButtonSet/index.js @@ -1,6 +1,6 @@ // @flow import * as React from 'react'; -import { getItemFromStorage, storeItem } from '../../helpers/localStorage'; +import { getItemFromStorage, storeItem } from 'src/helpers/localStorage'; import { withRouter } from 'react-router'; import queryString from 'query-string'; import { SERVER_URL, CLIENT_URL } from '../../api/constants'; diff --git a/src/components/loginButtonSet/style.js b/src/components/loginButtonSet/style.js index 137f5503e2..548d1358ea 100644 --- a/src/components/loginButtonSet/style.js +++ b/src/components/loginButtonSet/style.js @@ -1,14 +1,15 @@ // @flow import theme from 'shared/theme'; import styled from 'styled-components'; -import { zIndex } from '../globals'; +import { zIndex } from 'src/components/globals'; +import { MEDIA_BREAK } from 'src/components/layout'; export const Container = styled.div` display: grid; grid-gap: 16px; align-items: flex-end; padding: 16px 0; - @media (min-width: 768px) { + @media (min-width: ${MEDIA_BREAK}px) { grid-template-columns: repeat(2, 1fr); } `; @@ -26,9 +27,8 @@ export const SigninButton = styled.div` align-items: center; justify-content: flex-start; color: ${theme.text.reverse}; - border-radius: 8px; - padding: 8px; - padding-right: 16px; + border-radius: 32px; + padding: 8px 16px; font-size: 15px; font-weight: 600; position: relative; diff --git a/src/components/loginButtonSet/twitter.js b/src/components/loginButtonSet/twitter.js index 2ad68f3bab..c05f2563e1 100644 --- a/src/components/loginButtonSet/twitter.js +++ b/src/components/loginButtonSet/twitter.js @@ -2,7 +2,7 @@ import * as React from 'react'; import type { ButtonProps } from './'; import { TwitterButton, Label, A } from './style'; -import Icon from '../icons'; +import Icon from 'src/components/icon'; export const TwitterSigninButton = (props: ButtonProps) => { const { href, preferred, showAfter, onClickHandler } = props; diff --git a/src/components/maintenance/index.js b/src/components/maintenance/index.js index b644a29079..84049e2785 100644 --- a/src/components/maintenance/index.js +++ b/src/components/maintenance/index.js @@ -1,9 +1,10 @@ // @flow import React from 'react'; import styled from 'styled-components'; -import { FlexCol } from '../globals'; +import { FlexCol } from 'src/components/globals'; import { Tagline, Copy } from 'src/views/pages/style'; -import ViewSegment from '../../components/themedSection'; +import ViewSegment from 'src/components/themedSection'; +import { MEDIA_BREAK } from 'src/components/layout'; const Emoji = styled.div` font-size: 3em; @@ -28,7 +29,7 @@ const Text = styled(Copy)` text-decoration: underline; } - @media (max-width: 768px) { + @media (max-width: ${MEDIA_BREAK}px) { font-size: 20px; text-align: center; } @@ -41,7 +42,7 @@ const MaintenanceDowntime = () => { 🛠 Spectrum is currently undergoing maintenance - We'll be back soon, check{' '} + We’ll be back soon, check{' '} @withspectrum on Twitter {' '} diff --git a/src/components/markdownHint/index.js b/src/components/markdownHint/index.js index 57fe152303..cedd8c9446 100644 --- a/src/components/markdownHint/index.js +++ b/src/components/markdownHint/index.js @@ -1,6 +1,10 @@ // @flow import React from 'react'; -import { StyledMarkdownHint, Preformatted } from './style'; +import { + MarkdownHintContainer, + StyledMarkdownHint, + Preformatted, +} from './style'; type Props = { dataCy?: string, @@ -14,12 +18,14 @@ export const MarkdownHint = ({ style = {}, }: Props) => { return ( - - **bold** - *italic* - `code` - ```codeblock``` - [name](link) - + + + **bold** + *italic* + `code` + ```codeblock``` + [name](link) + + ); }; diff --git a/src/components/markdownHint/style.js b/src/components/markdownHint/style.js index f21078e93b..55cfcfcdde 100644 --- a/src/components/markdownHint/style.js +++ b/src/components/markdownHint/style.js @@ -1,6 +1,12 @@ // @flow import styled from 'styled-components'; import theme from 'shared/theme'; +import { MEDIA_BREAK } from 'src/components/layout'; + +export const MarkdownHintContainer = styled.div` + width: 100%; + background: ${theme.bg.default}; +`; export const StyledMarkdownHint = styled.div` display: flex; @@ -22,7 +28,7 @@ export const StyledMarkdownHint = styled.div` margin-right: 3px; } - @media (max-width: 768px) { + @media (max-width: ${MEDIA_BREAK}px) { display: none; } `; diff --git a/src/components/mediaInput/index.js b/src/components/mediaInput/index.js index 82823f1711..a57c72d2be 100644 --- a/src/components/mediaInput/index.js +++ b/src/components/mediaInput/index.js @@ -1,13 +1,11 @@ import React from 'react'; import { MediaLabel, MediaInput } from './style'; -import Icon from '../icons'; +import Icon from 'src/components/icon'; export default ({ onChange, accept = '.png, .jpg, .jpeg, .gif, .mp4', multiple = false, - tipLocation = 'top-right', - tipText = 'Upload photo', glyph = 'photo', }) => ( @@ -17,6 +15,6 @@ export default ({ multiple={multiple} onChange={onChange} /> - + ); diff --git a/src/components/mentionsInput/index.js b/src/components/mentionsInput/index.js index 7f85425497..2b9b64b15e 100644 --- a/src/components/mentionsInput/index.js +++ b/src/components/mentionsInput/index.js @@ -14,11 +14,13 @@ type Props = { staticSuggestions?: Array, client: ApolloClient, placeholder?: string, + hasAttachment?: boolean, onFocus?: Function, onBlur?: Function, onKeyDown?: Function, inputRef?: Function, dataCy?: string, + networkDisabled?: boolean, }; const cleanSuggestionUserObject = (user: ?Object) => { @@ -106,7 +108,13 @@ const SpectrumMentionsInput = (props: Props) => { return callback(uniqueResults.slice(0, 8)); }; - const { dataCy, ...rest } = props; + const { + dataCy, + networkDisabled, + staticSuggestions, + hasAttachment, + ...rest + } = props; return ( { const { menuIsOpen } = this.state; return ( - this.toggleMenu()} @@ -36,7 +36,7 @@ class Menu extends React.Component { {menuIsOpen && this.props.children} - this.toggleMenu()} hasNavBar={hasNavBar} diff --git a/src/components/menu/style.js b/src/components/menu/style.js index faa91ab86d..8e90f05fba 100644 --- a/src/components/menu/style.js +++ b/src/components/menu/style.js @@ -1,7 +1,7 @@ // @flow import theme from 'shared/theme'; import styled, { css } from 'styled-components'; -import { Transition, Shadow, zIndex, hexa } from '../../components/globals'; +import { Transition, Shadow, zIndex, hexa } from 'src/components/globals'; import { isDesktopApp } from 'src/helpers/desktop-app-utils'; export const Wrapper = styled.div` diff --git a/src/components/message/authorByline.js b/src/components/message/authorByline.js index d2c59ff9d1..3e7be47bca 100644 --- a/src/components/message/authorByline.js +++ b/src/components/message/authorByline.js @@ -4,7 +4,6 @@ import { convertTimestampToTime } from 'shared/time-formatting'; import ConditionalWrap from 'src/components/conditionalWrap'; import { UserHoverProfile } from 'src/components/hoverProfile'; import { Link } from 'react-router-dom'; -import { MessagesContext } from 'src/components/messageGroup'; import Badge from '../badges'; import { BadgesContainer, @@ -20,64 +19,49 @@ type Props = { roles?: Array, bot?: boolean, messageUrl: string, - selectedMessageId: string, }; export default (props: Props) => { - const { user, roles, timestamp, messageUrl, selectedMessageId } = props; + const { user, roles, timestamp, messageUrl } = props; return ( - - {({ selectMessage }) => { - return ( - - ( - - e.stopPropagation()} - > - {children} - {user.username && `@${user.username}`} - - - )} + + ( + + e.stopPropagation()} > - {user.name} - + {children} + {user.username && `@${user.username}`} + + + )} + > + {user.name} + - - {roles && - roles.map((role, index) => ( - e.stopPropagation()} - /> - ))} - {user.betaSupporter && ( - - )} - {props.bot && } - - selectMessage(selectedMessageId)} - > - {convertTimestampToTime(new Date(timestamp))} - - - ); - }} - + + {roles && + roles.map((role, index) => ( + e.stopPropagation()} /> + ))} + {user.betaSupporter && ( + + )} + {props.bot && } + + + {convertTimestampToTime(new Date(timestamp))} + + ); }; diff --git a/src/components/message/editingBody.js b/src/components/message/editingBody.js index 7085a21f9a..0d19713223 100644 --- a/src/components/message/editingBody.js +++ b/src/components/message/editingBody.js @@ -3,7 +3,7 @@ import * as React from 'react'; import type { MessageInfoType } from 'shared/graphql/fragments/message/messageInfo.js'; import { Input } from '../chatInput/style'; import { EditorInput, EditActions } from './style'; -import { TextButton, Button } from 'src/components/buttons'; +import { TextButton, PrimaryOutlineButton } from 'src/components/button'; import type { Dispatch } from 'redux'; import { addToastWithTimeout } from 'src/actions/toasts'; import compose from 'recompose/compose'; @@ -18,12 +18,6 @@ type Props = { dispatch: Dispatch, }; -type State = { - plugins: Array, - body: string, - isSavingEdit: boolean, -}; - const EditingChatInput = (props: Props) => { const initialState = props.message.messageType === 'text' ? props.message.content.body : null; @@ -34,23 +28,20 @@ const EditingChatInput = (props: Props) => { let input = null; // $FlowIssue - React.useEffect( - () => { - if (props.message.messageType === 'text') return; - - setText(null); - fetch('https://convert.spectrum.chat/to', { - method: 'POST', - body: props.message.content.body, - }) - .then(res => res.text()) - .then(md => { - setText(md); - input && input.focus(); - }); - }, - [props.message.id] - ); + React.useEffect(() => { + if (props.message.messageType === 'text') return; + + setText(null); + fetch('https://convert.spectrum.chat/to', { + method: 'POST', + body: props.message.content.body, + }) + .then(res => res.text()) + .then(md => { + setText(md); + input && input.focus(); + }); + }, [props.message.id]); const onChange = e => { const text = e.target.value; @@ -126,13 +117,17 @@ const EditingChatInput = (props: Props) => { {!saving && ( - + Cancel )} - + ); diff --git a/src/components/message/index.js b/src/components/message/index.js index 09fa992e14..68d0cad3ca 100644 --- a/src/components/message/index.js +++ b/src/components/message/index.js @@ -3,9 +3,12 @@ import * as React from 'react'; import { btoa } from 'b2a'; import compose from 'recompose/compose'; import { connect } from 'react-redux'; +import type { Dispatch } from 'redux'; import { withRouter, type Location, type History } from 'react-router'; +import queryString from 'query-string'; import Clipboard from 'react-clipboard.js'; import { openGallery } from 'src/actions/gallery'; +import Tooltip from 'src/components/tooltip'; import Reaction from 'src/components/reaction'; import { ReactionWrapper } from 'src/components//reaction/style'; import OutsideClickHandler from 'src/components/outsideClickHandler'; @@ -15,20 +18,18 @@ import { openModal } from 'src/actions/modals'; import { replyToMessage } from 'src/actions/message'; import { CLIENT_URL } from 'src/api/constants'; import { track, events } from 'src/helpers/analytics'; -import type { Dispatch } from 'redux'; import type { MessageInfoType } from 'shared/graphql/fragments/message/messageInfo'; import type { UserInfoType } from 'shared/graphql/fragments/user/userInfo'; import { withCurrentUser } from 'src/components/withCurrentUser'; import { UserAvatar } from 'src/components/avatar'; import AuthorByline from './authorByline'; -import Icon from 'src/components/icons'; +import Icon from 'src/components/icon'; import { addToastWithTimeout } from 'src/actions/toasts'; import toggleReactionMutation from 'shared/graphql/mutations/reaction/toggleReaction'; import { convertTimestampToTime, convertTimestampToDate, } from 'shared/time-formatting'; -import { MessagesContext } from 'src/components/messageGroup'; import ConditionalWrap from 'src/components/conditionalWrap'; import { OuterMessageContainer, @@ -52,9 +53,8 @@ type Props = {| showAuthorContext: boolean, message: MessageInfoType, canModerateMessage: boolean, - threadType: 'directMessageThread' | 'story', - threadId: string, thread: GetThreadType, + threadType: 'directMessageThread' | 'story', toggleReaction: Function, location: Location, history: History, @@ -68,11 +68,11 @@ type State = { }; class Message extends React.Component { - wrapperRef: React.Node; + wrapperRef: React$Node; state = { isEditing: false }; - setWrapperRef = (node: React.Node) => { + setWrapperRef = (node: React$Node) => { this.wrapperRef = node; }; @@ -100,8 +100,8 @@ class Message extends React.Component { toggleOpenGallery = (e: any, selectedMessageId: string) => { e.stopPropagation(); - const { threadId } = this.props; - this.props.dispatch(openGallery(threadId, selectedMessageId)); + const { thread } = this.props; + this.props.dispatch(openGallery(thread.id, selectedMessageId)); }; deleteMessage = (e: any) => { @@ -130,7 +130,7 @@ class Message extends React.Component { entity: 'message', message, threadType: this.props.threadType, - threadId: this.props.threadId, + threadId: this.props.thread.id, }) ); }; @@ -138,10 +138,10 @@ class Message extends React.Component { replyToMessage = (e: any) => { e.stopPropagation(); - const { threadId, message } = this.props; + const { thread, message } = this.props; return this.props.dispatch( replyToMessage({ - threadId, + threadId: thread.id, messageId: message.id, }) ); @@ -158,6 +158,11 @@ class Message extends React.Component { initEditMessage = () => this.setState({ isEditing: true }); cancelEdit = () => this.setState({ isEditing: false }); + clearSelectedMessage = () => { + const { history, location } = this.props; + const { pathname } = location; + history.push({ pathname }); + }; render() { const { @@ -168,9 +173,9 @@ class Message extends React.Component { message, canModerateMessage, toggleReaction, - threadId, - threadType, thread, + threadType, + location, } = this.props; const { isEditing } = this.state; @@ -180,248 +185,215 @@ class Message extends React.Component { threadType === 'story' && thread ? `${getThreadLink(thread)}?m=${selectedMessageId}` : threadType === 'directMessageThread' - ? `/messages/${threadId}?m=${selectedMessageId}` - : `/thread/${threadId}?m=${selectedMessageId}`; + ? `/messages/${thread.id}?m=${selectedMessageId}` + : `/thread/${thread.id}?m=${selectedMessageId}`; + + const searchObj = queryString.parse(location.search); + const { m = null } = searchObj; + const isSelected = m && m === selectedMessageId; return ( - - {({ selectedMessage, selectMessage }) => { - const isSelected = - selectedMessage && selectedMessage === selectedMessageId; - - return ( - ( - selectMessage(null)} - style={{ width: '100%' }} - > - {children} - - )} - > - - this.handleSelectMessage(e, selectMessage, selectedMessageId) - } + ( + + {children} + + )} + > + + + {showAuthorContext ? ( + e.stopPropagation()}> + + + ) : ( + + {convertTimestampToTime(new Date(message.timestamp))} + + )} + + + + {showAuthorContext && ( + + )} + + {!isEditing ? ( + this.toggleOpenGallery(e, message.id)} + message={message} + /> + ) : ( + + )} + + {message.modifiedAt && !isEditing && ( + - - {showAuthorContext ? ( - e.stopPropagation()}> - - - ) : ( - selectMessage(selectedMessageId)} - > - {convertTimestampToTime(new Date(message.timestamp))} - - )} - - - - {showAuthorContext && ( - - )} + + Edited + + + )} - {!isEditing ? ( - 0 && ( + ( + + this.toggleOpenGallery(e, message.id)} - message={message} - /> - ) : ( - + onClick={ + me + ? (e: any) => { + e.stopPropagation(); + } + : (e: any) => { + e.stopPropagation(); + mutation(); + } + } + > + + {count} + + + )} + /> + )} + + {!isEditing && ( + + + {canEditMessage && ( + + + + + )} - {message.modifiedAt && !isEditing && ( - - Edited - + {canModerateMessage && ( + + + + + )} - {message.reactions.count > 0 && ( + + + + + + + {!me && ( ( - { - e.stopPropagation(); - } - : (e: any) => { - e.stopPropagation(); - mutation(); - } - } - tipText={ - me ? 'Likes' : hasReacted ? 'Unlike' : 'Like' - } - tipLocation={'top-right'} - > - - {count} - - )} - /> - )} - - {!isEditing && ( - - - {canEditMessage && ( - - - - )} - {canModerateMessage && ( - ( + + { + e.stopPropagation(); + mutation(); + }} > - - )} - + + + )} + /> + )} + + {threadType === 'story' && ( + + this.props.dispatch( + addToastWithTimeout('success', 'Copied to clipboard') + ) + } + > + + - - {!me && ( - ( - { - e.stopPropagation(); - mutation(); - }} - > - - - )} - /> - )} - - {threadType === 'story' && ( - - this.props.dispatch( - addToastWithTimeout( - 'success', - 'Copied to clipboard' - ) - ) - } - > - { - e.stopPropagation(); - selectMessage(selectedMessageId); - }} - > - - - - )} - - + + )} - - - - ); - }} - + + + )} + + + ); } } diff --git a/src/components/message/style.js b/src/components/message/style.js index bc8d123ee4..748b9d9139 100644 --- a/src/components/message/style.js +++ b/src/components/message/style.js @@ -2,9 +2,10 @@ import theme from 'shared/theme'; import styled, { css } from 'styled-components'; import { Link } from 'react-router-dom'; -import { SvgWrapper } from '../icons'; -import { Truncate, monoStack, hexa, Tooltip } from 'src/components/globals'; +import { SvgWrapper } from 'src/components/icon'; +import { Truncate, monoStack, hexa } from 'src/components/globals'; import { Wrapper as EditorWrapper } from '../rich-text-editor/style'; +import { MEDIA_BREAK } from 'src/components/layout'; export const Byline = styled.span` display: flex; @@ -95,8 +96,6 @@ export const Action = styled.li` &:first-child { border-left: 0; } - - ${Tooltip}; `; export const LikeAction = styled(Action)` @@ -127,8 +126,9 @@ export const GutterTimestamp = styled(Link)` `; export const OuterMessageContainer = styled.div` - display: flex; - flex: 1 0 auto; + display: grid; + grid-template-columns: 72px minmax(0, 1fr); + padding-right: 16px; align-self: stretch; position: relative; padding-right: 16px; @@ -137,7 +137,7 @@ export const OuterMessageContainer = styled.div` ? props.theme.special.wash : props.error ? props.theme.warn.wash - : props.theme.bg.default}; + : 'transparent'}; ${props => props.selected && @@ -152,23 +152,25 @@ export const OuterMessageContainer = styled.div` ${GutterTimestamp} { opacity: 1; } - `} &:hover { - @media (min-width: 768px) { - background: ${props => - props.selected - ? props.theme.special.wash - : props.error - ? props.theme.warn.border - : props.theme.bg.wash}; - - ${ActionsContainer} { - opacity: 1; - pointer-events: auto; - } + `} + + &:hover, + &:focus, + &:active { + background: ${props => + props.selected + ? props.theme.special.wash + : props.error + ? props.theme.warn.border + : props.theme.bg.wash}; + + ${ActionsContainer} { + opacity: 1; + pointer-events: auto; + } - ${GutterTimestamp} { - opacity: 1; - } + ${GutterTimestamp} { + opacity: 1; } } `; @@ -237,7 +239,7 @@ const Bubble = styled.div` `; export const Text = styled(Bubble)` - font-size: 15px; + font-size: 16px; line-height: 1.4; color: ${props => props.error ? props.theme.warn.default : props.theme.text.default}; @@ -332,14 +334,12 @@ export const Line = styled.pre` `; export const Paragraph = styled.p` + white-space: pre-wrap; + word-break: break-word; + &:not(:empty) ~ &:not(:empty) { margin-top: 8px; } - - /* hack for https://github.com/withspectrum/spectrum/issues/4829 */ - &:last-of-type:not(:empty) { - margin-top: 0px !important; - } `; export const BlockQuote = styled.blockquote` @@ -349,7 +349,7 @@ export const BlockQuote = styled.blockquote` padding: 4px 12px 4px 16px; `; -export const QuotedParagraph = Paragraph.withComponent('div').extend` +export const QuotedParagraph = styled.div` color: ${theme.text.alt}; code { @@ -444,7 +444,7 @@ export const EditorInput = styled(EditorWrapper)` max-width: 100%; word-break: break-all; - @media (max-width: 768px) { + @media (max-width: ${MEDIA_BREAK}px) { font-size: 16px; padding-left: 16px; } @@ -497,7 +497,6 @@ export const EditedIndicator = styled.span` display: block; font-size: 11px; color: ${props => props.theme.text.alt}; - ${Tooltip}; `; export const ThreadAttachmentsContainer = styled.ul``; diff --git a/src/components/message/ThreadAttachment/Attachment.js b/src/components/message/threadAttachment/attachment.js similarity index 77% rename from src/components/message/ThreadAttachment/Attachment.js rename to src/components/message/threadAttachment/attachment.js index fd5d0bac8a..39a0a39722 100644 --- a/src/components/message/ThreadAttachment/Attachment.js +++ b/src/components/message/threadAttachment/attachment.js @@ -4,9 +4,8 @@ import type { Props } from './'; import compose from 'recompose/compose'; import { Loading } from 'src/components/loading'; import { Container, LinkWrapper, AvatarWrapper, Column } from './style'; -import ThreadHeader from 'src/views/dashboard/components/inboxThread/header/threadHeader'; -import Activity from 'src/views/dashboard/components/inboxThread/activity'; -import { ThreadTitle } from 'src/views/dashboard/components/inboxThread/style'; +import Activity from 'src/components/inboxThread/activity'; +import { ThreadTitle } from 'src/components/inboxThread/style'; import { UserAvatar } from 'src/components/avatar'; import { withCurrentUser } from 'src/components/withCurrentUser'; import getThreadLink from 'src/helpers/get-thread-link'; @@ -18,12 +17,11 @@ class Attachment extends React.Component { if (loading) return ( - - - +
+ + + +
); if (error) return null; diff --git a/src/components/message/ThreadAttachment/index.js b/src/components/message/threadAttachment/index.js similarity index 94% rename from src/components/message/ThreadAttachment/index.js rename to src/components/message/threadAttachment/index.js index 57b78735f3..aa3eb07456 100644 --- a/src/components/message/ThreadAttachment/index.js +++ b/src/components/message/threadAttachment/index.js @@ -7,7 +7,7 @@ import { } from 'shared/graphql/queries/thread/getThread'; import type { MessageInfoType } from 'shared/graphql/fragments/message/messageInfo.js'; import type { UserInfoType } from 'shared/graphql/fragments/user/userInfo.js'; -import Attachment from './Attachment'; +import Attachment from './attachment'; export type Props = { currentUser: UserInfoType, diff --git a/src/components/message/ThreadAttachment/style.js b/src/components/message/threadAttachment/style.js similarity index 94% rename from src/components/message/ThreadAttachment/style.js rename to src/components/message/threadAttachment/style.js index 1ef778f608..2595b808ab 100644 --- a/src/components/message/ThreadAttachment/style.js +++ b/src/components/message/threadAttachment/style.js @@ -1,5 +1,5 @@ // @flow -import styled, { injectGlobal } from 'styled-components'; +import styled, { createGlobalStyle } from 'styled-components'; import theme from 'shared/theme'; import { Link } from 'react-router-dom'; import { tint, zIndex } from 'src/components/globals'; @@ -9,7 +9,7 @@ export const LinkWrapper = styled(Link)``; export const Column = styled.div``; export const AvatarWrapper = styled.div``; -injectGlobal` +export const GlobalThreadAttachmentStyles = createGlobalStyle` .attachment-container { h3 { diff --git a/src/components/message/view.js b/src/components/message/view.js index bf073474dd..f72a84a57e 100644 --- a/src/components/message/view.js +++ b/src/components/message/view.js @@ -1,7 +1,7 @@ // @flow import React from 'react'; import redraft from 'redraft'; -import Icon from '../icons'; +import Icon from 'src/components/icon'; import { Text, Emoji, @@ -9,7 +9,6 @@ import { QuoteWrapper, QuoteWrapperGradient, QuotedParagraph, - ThreadAttachmentsContainer, } from './style'; import { messageRenderer } from 'shared/clients/draft-js/message/renderer'; import { draftOnlyContainsEmoji } from 'shared/only-contains-emoji'; @@ -28,16 +27,6 @@ type BodyProps = { // This regexp matches /community/channel/slug~id, /?thread=id, /?t=id etc. // see https://regex101.com/r/aGamna/2/ -const MATCH_SPECTRUM_URLS = /(?:(?:https?:\/\/)?|\B)(?:spectrum\.chat|localhost:3000)\/.*?(?:~|(?:\?|&)t=|(?:\?|&)thread=)([^&\s]*)/gim; -const getSpectrumThreadIds = (text: string) => { - let ids = []; - let match; - while ((match = MATCH_SPECTRUM_URLS.exec(text))) { - ids.push(match[1]); - } - return ids; -}; - export const Body = (props: BodyProps) => { const { showParent = true, message, openGallery, me, bubble = true } = props; const emojiOnly = @@ -47,7 +36,7 @@ export const Body = (props: BodyProps) => { switch (message.messageType) { case 'optimistic': return ( -
+
@@ -56,18 +45,26 @@ export const Body = (props: BodyProps) => { case messageTypeObj.text: default: return ( - {message.content.body} + + {message.content.body} + ); case messageTypeObj.media: { if (typeof message.id === 'number' && message.id < 0) { return null; } - return ; + return ( + + ); } case messageTypeObj.draftjs: { const parsed = JSON.parse(message.content.body); return ( - + {message.parent && showParent && ( // $FlowIssue @@ -77,7 +74,9 @@ export const Body = (props: BodyProps) => { {parsed && Array.isArray(parsed.blocks) && parsed.blocks[0].text} ) : ( -
{redraft(parsed, messageRenderer)}
+
+ {redraft(parsed, messageRenderer)} +
)}
); diff --git a/src/components/messageGroup/directMessage.js b/src/components/messageGroup/directMessage.js new file mode 100644 index 0000000000..88f65a74ff --- /dev/null +++ b/src/components/messageGroup/directMessage.js @@ -0,0 +1,115 @@ +// @flow +import React from 'react'; +import { convertTimestampToDate } from 'shared/time-formatting'; +import { ErrorBoundary } from 'src/components/error'; +import Message from 'src/components/message'; +import MessageErrorFallback from '../message/messageErrorFallback'; +import type { Props } from './'; +import type { GetDirectMessageThreadType } from 'shared/graphql/queries/directMessageThread/getDirectMessageThread'; +import { + MessagesWrapper, + MessageGroupContainer, + Timestamp, + Time, + UnseenRobotext, + UnseenTime, +} from './style'; + +const DirectMessages = (props: { + ...Props, + thread: GetDirectMessageThreadType, +}) => { + const { thread, messages, threadType, currentUser } = props; + + if (!messages) return null; + + let hasInjectedUnseenRobo; + return ( + + {messages.map(group => { + // eliminate groups where there are no messages + if (!Array.isArray(group) || group.length === 0) return null; + // Since all messages in the group have the same Author and same initial timestamp, we only need to pull that data from the first message in the group. So let's get that message and then check who sent it. + const initialMessage = group[0]; + const { author } = initialMessage; + const roboText = author.user.id === 'robo'; + const me = currentUser + ? author.user && author.user.id === currentUser.id + : false; + const canModerateMessage = me; + + if (roboText) { + if (initialMessage.type === 'timestamp') { + return ( + +
+ +
+
+ ); + } else { + // Ignore unknown robo messages + return null; + } + } + + let unseenRobo = null; + // If the last message in the group was sent after the thread was seen mark the entire + // group as last seen in the UI + // NOTE(@mxstbr): Maybe we should split the group eventually + const currentUserParticipant = + currentUser && + thread && + thread.participants.find(({ userId }) => userId === currentUser.id); + if ( + currentUserParticipant && + new Date(group[group.length - 1].timestamp).getTime() > + new Date(currentUserParticipant.lastSeen).getTime() && + !me && + !hasInjectedUnseenRobo + ) { + hasInjectedUnseenRobo = true; + unseenRobo = ( + +
+ New messages +
+
+ ); + } + + return ( + + {unseenRobo} + + {group.map((message, index) => { + return ( + } + key={message.id} + > + + + ); + })} + + + ); + })} +
+ ); +}; + +export default DirectMessages; diff --git a/src/components/messageGroup/index.js b/src/components/messageGroup/index.js index 72f6c3b843..4faddb58c1 100644 --- a/src/components/messageGroup/index.js +++ b/src/components/messageGroup/index.js @@ -1,275 +1,35 @@ // @flow -import * as React from 'react'; +import React from 'react'; import compose from 'recompose/compose'; -import { withRouter, type History, type Location } from 'react-router'; -import { connect } from 'react-redux'; -import queryString from 'query-string'; -import { convertTimestampToDate } from 'shared/time-formatting'; -import Message from '../message'; -import type { Dispatch } from 'redux'; -import type { MessageInfoType } from 'shared/graphql/fragments/message/messageInfo'; +import type { UserInfoType } from 'shared/graphql/fragments/user/userInfo'; import type { GetThreadType } from 'shared/graphql/queries/thread/getThread'; -import { ErrorBoundary } from 'src/components/error'; -import MessageErrorFallback from '../message/messageErrorFallback'; +import type { GetDirectMessageThreadType } from 'shared/graphql/queries/directMessageThread/getDirectMessageThread'; +import type { MessageInfoType } from 'shared/graphql/fragments/message/messageInfo'; import { withCurrentUser } from 'src/components/withCurrentUser'; - -import { - MessagesWrapper, - MessageGroupContainer, - Timestamp, - Time, - UnseenRobotext, - UnseenTime, -} from './style'; - -type MessageGroupType = Array; - -type Props = { - messages: Array, - currentUser: ?Object, - threadType: 'directMessageThread' | 'story', - threadId: string, - thread: GetThreadType, - isModerator: boolean, - dispatch: Dispatch, - lastSeen?: number | Date, - history: History, - location: Location, +import ThreadMessages from './thread'; +import DirectMessages from './directMessage'; + +type DirectMessageThreadProps = { + threadType: 'directMessageThread', + thread?: GetDirectMessageThreadType, + messages: Array, + currentUser: ?UserInfoType, }; -type State = { - selectedMessage: ?string, +type StoryProps = { + threadType: 'story', + thread?: GetThreadType, + messages: Array, + currentUser: ?UserInfoType, }; -// $FlowFixMe -export const MessagesContext = React.createContext(); - -/* - Messages expects to receive sorted and grouped messages. - They will arrive as an array of arrays, where each top-level array is a group - of message bubbles. -*/ -class Messages extends React.Component { - constructor(props) { - super(props); - - const searchObj = queryString.parse(props.location.search); - const selectedMessageId = searchObj.m; - - let initialSelection = null; - - if (selectedMessageId) { - initialSelection = selectedMessageId; - } - - this.state = { - selectedMessage: initialSelection, - selectMessage: this.selectMessage, - }; - } - - selectMessage = (selectedMessageId: string) => { - const { - history, - location: { pathname, search, state }, - } = this.props; - const searchObj = queryString.parse(search); - const newSearchObj = { ...searchObj }; - if (selectedMessageId) { - newSearchObj.m = selectedMessageId; - } - const newSearch = queryString.stringify( - { ...newSearchObj }, - { encode: false, strict: false, sort: false } - ); - history.push({ - pathname, - search: newSearch, - state, - }); - return this.setState({ selectedMessage: selectedMessageId }); - }; - - shouldComponentUpdate(next, nextState) { - const current = this.props; - const newSelection = - nextState.selectedMessage !== this.state.selectedMessage; - - if (newSelection) { - return true; - } - - // If it's a different thread, let's re-render - const diffThread = next.threadId !== current.threadId; - if (diffThread) { - return true; - } - - // If we don't have any message groups in the next props, return if we have - // message groups in the current props - if (!next.messages) { - return !current.messages; - } +export type Props = DirectMessageThreadProps | StoryProps; - // If a message group was added - if (next.messages.length !== current.messages.length) { - return true; - } +const ChatMessages = (props: Props) => { + if (props.threadType === 'story') return ; - // Check if any message group has different messages than last time - const hasNewMessages = next.messages.some((nextGroup, groupIndex) => { - const currGroup = current.messages[groupIndex]; - // New group or more messages in group - if (!currGroup || nextGroup.length !== currGroup.length) { - return true; - } - - return nextGroup.some((nextMessage, messageIndex) => { - const currMessage = current.messages[groupIndex][messageIndex]; - // A new message was added - if (!currMessage) { - return false; - } - - return currMessage.id !== nextMessage.id; - }); - }); - - if (hasNewMessages) { - return true; - } - - const hasNewReactions = next.messages.map((nextGroup, groupIndex) => { - return nextGroup.some((nextMessage, messageIndex) => { - const currMessage = current.messages[groupIndex][messageIndex]; - if ( - !currMessage.message || - !nextMessage.message || - currMessage.message.type === 'timestamp' || - nextMessage.message.type === 'timestamp' - ) { - return false; - } - - if (currMessage.reactions.count !== nextMessage.reactions.count) - return true; - if ( - currMessage.reactions.hasReacted !== nextMessage.reactions.hasReacted - ) - return true; - - return false; - }); - }); - - if (hasNewReactions) { - return true; - } - - return false; - } - - render() { - const { - messages, - currentUser, - threadType, - threadId, - isModerator, - lastSeen, - thread, - } = this.props; - - let hasInjectedUnseenRobo; - return ( - - {messages.map((group, i) => { - // eliminate groups where there are no messages - if (group.length === 0) return null; - // Since all messages in the group have the same Author and same initial timestamp, we only need to pull that data from the first message in the group. So let's get that message and then check who sent it. - const initialMessage = group[0]; - const { author } = initialMessage; - const roboText = author.user.id === 'robo'; - const me = currentUser - ? author.user && author.user.id === currentUser.id - : false; - const canModerateMessage = me || isModerator; - - if (roboText) { - if (initialMessage.type === 'timestamp') { - return ( - -
- -
-
- ); - } else { - // Ignore unknown robo messages - return null; - } - } - - let unseenRobo = null; - // If the last message in the group was sent after the thread was seen mark the entire - // group as last seen in the UI - // NOTE(@mxstbr): Maybe we should split the group eventually - if ( - !!lastSeen && - new Date(group[group.length - 1].timestamp).getTime() > - new Date(lastSeen).getTime() && - !me && - !hasInjectedUnseenRobo - ) { - hasInjectedUnseenRobo = true; - unseenRobo = ( - -
- New messages -
-
- ); - } - - return ( - - {unseenRobo} - - {group.map((message, index) => { - return ( - } - key={message.id} - > - - - - - ); - })} - - - ); - })} -
- ); - } -} + // $FlowIssue + return ; +}; -export default compose( - withCurrentUser, - withRouter, - connect() -)(Messages); +export default compose(withCurrentUser)(ChatMessages); diff --git a/src/components/messageGroup/style.js b/src/components/messageGroup/style.js index ecdb57e857..00c9b15e7b 100644 --- a/src/components/messageGroup/style.js +++ b/src/components/messageGroup/style.js @@ -2,16 +2,20 @@ import theme from 'shared/theme'; import styled from 'styled-components'; import { Transition, HorizontalRule } from '../globals'; +import { MEDIA_BREAK } from 'src/components/layout'; export const MessagesWrapper = styled.div` flex: 1 0 auto; padding-bottom: 8px; display: flex; flex-direction: column; + width: 100%; max-width: 100%; + justify-content: flex-end; + background: ${theme.bg.default}; - @media (max-width: 768px) { - padding-bottom: 16px; + @media (max-width: ${MEDIA_BREAK}px) { + padding-bottom: 72px; } `; @@ -21,16 +25,16 @@ export const MessageGroupContainer = styled.div` flex-direction: column; align-items: flex-start; position: relative; - margin-top: 20px; + margin-top: 8px; `; export const Timestamp = styled(HorizontalRule)` - margin: 20px 0 0; + margin: 24px 0; text-align: center; user-select: none; hr { - border-color: ${theme.bg.wash}; + border-color: ${theme.bg.divider}; } `; @@ -43,16 +47,10 @@ export const UnseenRobotext = styled(Timestamp)` export const Time = styled.span` text-align: center; - color: ${theme.text.placeholder}; + color: ${theme.text.alt}; font-size: 14px; - font-weight: 400; - margin: 0 16px; - transition: ${Transition.hover.off}; - - &:hover { - color: ${theme.text.alt}; - transiton: ${Transition.hover.on}; - } + font-weight: 500; + margin: 0 24px; `; export const UnseenTime = styled(Time)` diff --git a/src/components/messageGroup/thread.js b/src/components/messageGroup/thread.js new file mode 100644 index 0000000000..e0d721b783 --- /dev/null +++ b/src/components/messageGroup/thread.js @@ -0,0 +1,113 @@ +// @flow +import React from 'react'; +import compose from 'recompose/compose'; +import { convertTimestampToDate } from 'shared/time-formatting'; +import { withCurrentUser } from 'src/components/withCurrentUser'; +import { ErrorBoundary } from 'src/components/error'; +import Message from 'src/components/message'; +import MessageErrorFallback from '../message/messageErrorFallback'; +import type { GetThreadType } from 'shared/graphql/queries/thread/getThread'; +import type { Props } from './'; +import { + MessagesWrapper, + MessageGroupContainer, + Timestamp, + Time, + UnseenRobotext, + UnseenTime, +} from './style'; + +const ChatMessages = (props: { ...Props, thread: GetThreadType }) => { + const { messages, thread, threadType, currentUser } = props; + if (!thread || !messages) return null; + const { currentUserLastSeen, community } = thread; + const { communityPermissions } = community; + const { isOwner, isModerator } = communityPermissions; + const canModerate = isOwner || isModerator; + + let hasInjectedUnseenRobo; + return ( + + {messages.map(group => { + // eliminate groups where there are no messages + if (!Array.isArray(group) || group.length === 0) return null; + // Since all messages in the group have the same Author and same initial timestamp, we only need to pull that data from the first message in the group. So let's get that message and then check who sent it. + const initialMessage = group[0]; + const { author } = initialMessage; + const roboText = author.user.id === 'robo'; + const me = currentUser + ? author.user && author.user.id === currentUser.id + : false; + const canModerateMessage = me || canModerate; + + if (roboText) { + if (initialMessage.type === 'timestamp') { + return ( + +
+ +
+
+ ); + } else { + // Ignore unknown robo messages + return null; + } + } + + let unseenRobo = null; + // If the last message in the group was sent after the thread was seen mark the entire + // group as last seen in the UI + // NOTE(@mxstbr): Maybe we should split the group eventually + if ( + !!currentUserLastSeen && + new Date(group[group.length - 1].timestamp).getTime() > + new Date(currentUserLastSeen).getTime() && + !me && + !hasInjectedUnseenRobo + ) { + hasInjectedUnseenRobo = true; + unseenRobo = ( + +
+ New messages +
+
+ ); + } + + return ( + + {unseenRobo} + + {group.map((message, index) => { + return ( + } + key={message.id} + > + + + ); + })} + + + ); + })} +
+ ); +}; + +export default compose(withCurrentUser)(ChatMessages); diff --git a/src/components/modals/BanUserModal/index.js b/src/components/modals/BanUserModal/index.js index 9013dde0c3..34b95a422e 100644 --- a/src/components/modals/BanUserModal/index.js +++ b/src/components/modals/BanUserModal/index.js @@ -3,13 +3,13 @@ import * as React from 'react'; import { connect } from 'react-redux'; import Modal from 'react-modal'; import compose from 'recompose/compose'; -import { closeModal } from '../../../actions/modals'; -import { addToastWithTimeout } from '../../../actions/toasts'; +import { closeModal } from 'src/actions/modals'; +import { addToastWithTimeout } from 'src/actions/toasts'; import type { GetUserType } from 'shared/graphql/queries/user/getUser'; import banUserMutation from 'shared/graphql/mutations/user/banUser'; import type { Dispatch } from 'redux'; import ModalContainer from '../modalContainer'; -import { TextButton, Button } from '../../buttons'; +import { TextButton, WarnButton } from 'src/components/button'; import { modalStyles } from '../styles'; import { TextArea, Error } from '../../formElements'; import { Form, Actions, Subtitle } from './style'; @@ -122,13 +122,13 @@ class BanUserModal extends React.Component { Cancel - + diff --git a/src/components/modals/ChangeChannelModal/index.js b/src/components/modals/ChangeChannelModal/index.js index e0e5a41ae0..e2d4be14f4 100644 --- a/src/components/modals/ChangeChannelModal/index.js +++ b/src/components/modals/ChangeChannelModal/index.js @@ -3,13 +3,13 @@ import * as React from 'react'; import Modal from 'react-modal'; import compose from 'recompose/compose'; import ModalContainer from '../modalContainer'; -import { closeModal } from '../../../actions/modals'; +import { closeModal } from 'src/actions/modals'; import { connect } from 'react-redux'; -import { TextButton, Button } from '../../buttons'; +import { TextButton, PrimaryOutlineButton } from 'src/components/button'; import moveThreadMutation from 'shared/graphql/mutations/thread/moveThread'; import type { MoveThreadType } from 'shared/graphql/mutations/thread/moveThread'; -import { addToastWithTimeout } from '../../../actions/toasts'; -import Icon from '../../icons'; +import { addToastWithTimeout } from 'src/actions/toasts'; +import Icon from 'src/components/icon'; import { IconContainer } from '../RepExplainerModal/style'; import { Actions, modalStyles, Section, Title, Subtitle } from './style'; import ChannelSelector from './channelSelector'; @@ -42,7 +42,10 @@ class ChangeChannelModal extends React.Component { saveNewChannel = () => { const { activeChannel } = this.state; - const { thread: { id }, dispatch } = this.props; + const { + thread: { id }, + dispatch, + } = this.props; this.setState({ isLoading: true, @@ -98,7 +101,7 @@ class ChangeChannelModal extends React.Component { - This thread can't be moved + This thread can’t be moved This thread was posted in the private channel{' '} {thread.channel.name} - threads in private channels cannot be @@ -120,17 +123,14 @@ class ChangeChannelModal extends React.Component { /> - - Cancel - - + )} @@ -141,5 +141,8 @@ class ChangeChannelModal extends React.Component { } const map = state => ({ isOpen: state.modals.isOpen }); -// $FlowIssue -export default compose(connect(map), moveThreadMutation)(ChangeChannelModal); +export default compose( + // $FlowIssue + connect(map), + moveThreadMutation +)(ChangeChannelModal); diff --git a/src/components/modals/ChangeChannelModal/style.js b/src/components/modals/ChangeChannelModal/style.js index fd67a2915d..e93d0649a5 100644 --- a/src/components/modals/ChangeChannelModal/style.js +++ b/src/components/modals/ChangeChannelModal/style.js @@ -1,7 +1,7 @@ import styled from 'styled-components'; import theme from 'shared/theme'; -import { zIndex } from '../../globals'; -import { isMobile } from '../../../helpers/utils'; +import { zIndex } from 'src/components/globals'; +import { isMobile } from 'src/helpers/utils'; const maxWidth = '460px'; const mobile = isMobile(); @@ -45,7 +45,6 @@ export const modalStyles = { export const Section = styled.section` display: flex; justify-content: center; - align-items: center; padding: 16px 32px 32px; flex-direction: column; `; @@ -55,7 +54,6 @@ export const Title = styled.h3` font-weight: 700; color: ${theme.text.default}; margin: 16px 0 8px; - text-align: center; line-height: 1.4; `; @@ -63,7 +61,6 @@ export const Subtitle = styled.h3` font-size: 16px; font-weight: 400; color: ${theme.text.alt}; - text-align: center; `; export const SelectorContainer = styled.div` @@ -82,11 +79,13 @@ export const SelectorContainer = styled.div` export const Actions = styled.div` margin-top: 24px; display: flex; - width: 100%; justify-content: center; + align-self: flex-end; + button { flex: 1; } + button + button { margin-left: 8px; } diff --git a/src/components/modals/CreateChannelModal/index.js b/src/components/modals/CreateChannelModal/index.js index bc5f5780cb..78184dd88d 100644 --- a/src/components/modals/CreateChannelModal/index.js +++ b/src/components/modals/CreateChannelModal/index.js @@ -7,9 +7,9 @@ import { withRouter } from 'react-router'; import slugg from 'slugg'; import { CHANNEL_SLUG_BLACKLIST } from 'shared/slug-blacklists'; import { withApollo } from 'react-apollo'; -import { closeModal } from '../../../actions/modals'; -import { addToastWithTimeout } from '../../../actions/toasts'; -import { throttle } from '../../../helpers/utils'; +import { closeModal } from 'src/actions/modals'; +import { addToastWithTimeout } from 'src/actions/toasts'; +import { throttle } from 'src/helpers/utils'; import { getChannelBySlugAndCommunitySlugQuery } from 'shared/graphql/queries/channel/getChannel'; import type { GetChannelType } from 'shared/graphql/queries/channel/getChannel'; import type { GetCommunityType } from 'shared/graphql/queries/community/getCommunity'; @@ -19,7 +19,7 @@ import type { Dispatch } from 'redux'; import { withCurrentUser } from 'src/components/withCurrentUser'; import ModalContainer from '../modalContainer'; -import { TextButton, Button } from '../../buttons'; +import { TextButton, PrimaryOutlineButton } from 'src/components/button'; import { modalStyles, UpsellDescription } from '../styles'; import { Input, @@ -167,7 +167,7 @@ class CreateChannelModal extends React.Component { }); } }) - .catch(err => { + .catch(() => { // do nothing }); } @@ -346,16 +346,14 @@ class CreateChannelModal extends React.Component { - - Cancel - - + {createError && ( diff --git a/src/components/modals/DeleteDoubleCheckModal/index.js b/src/components/modals/DeleteDoubleCheckModal/index.js index 7412d90777..5700c28289 100644 --- a/src/components/modals/DeleteDoubleCheckModal/index.js +++ b/src/components/modals/DeleteDoubleCheckModal/index.js @@ -3,9 +3,9 @@ import * as React from 'react'; import { connect } from 'react-redux'; import Modal from 'react-modal'; import compose from 'recompose/compose'; -import { withRouter } from 'react-router'; -import { closeModal } from '../../../actions/modals'; -import { addToastWithTimeout } from '../../../actions/toasts'; +import { withRouter, type History } from 'react-router'; +import { closeModal } from 'src/actions/modals'; +import { addToastWithTimeout } from 'src/actions/toasts'; import deleteCommunityMutation from 'shared/graphql/mutations/community/deleteCommunity'; import type { DeleteCommunityType } from 'shared/graphql/mutations/community/deleteCommunity'; import deleteChannelMutation from 'shared/graphql/mutations/channel/deleteChannel'; @@ -18,7 +18,7 @@ import archiveChannel from 'shared/graphql/mutations/channel/archiveChannel'; import removeCommunityMember from 'shared/graphql/mutations/communityMember/removeCommunityMember'; import ModalContainer from '../modalContainer'; -import { TextButton, Button } from '../../buttons'; +import { TextButton, WarnButton } from 'src/components/button'; import { modalStyles } from '../styles'; import { Actions, Message } from './style'; import type { Dispatch } from 'redux'; @@ -49,7 +49,7 @@ type Props = { redirect?: ?string, message?: ?string, buttonLabel?: string, - extraProps?: Object, + extraProps?: any, }, deleteMessage: Function, deleteCommunity: Function, @@ -59,6 +59,7 @@ type Props = { removeCommunityMember: Function, dispatch: Dispatch, isOpen: boolean, + history: History, }; export const deleteMessageWithToast = ( @@ -94,7 +95,8 @@ class DeleteDoubleCheckModal extends React.Component { triggerDelete = () => { const { - modalProps: { id, entity, redirect }, + history, + modalProps: { id, entity, redirect, extraProps }, dispatch, } = this.props; @@ -115,16 +117,14 @@ class DeleteDoubleCheckModal extends React.Component { this.close(); }); case 'thread': { + if (!extraProps) return; + const { community } = extraProps.thread; return this.props .deleteThread(id) .then(({ data }: DeleteThreadType) => { const { deleteThread } = data; if (deleteThread) { - // TODO: When we figure out the mutation reducers in apollo - // client we can just history push and trust the store to update - // eslint-disable-next-line - window.location.href = redirect ? redirect : '/'; - // history.push(redirect ? redirect : '/'); + history.replace(`/${community.slug}?tab=posts`); dispatch(addToastWithTimeout('neutral', 'Thread deleted.')); this.setState({ isLoading: false, @@ -278,17 +278,14 @@ class DeleteDoubleCheckModal extends React.Component { {message ? message : 'Are you sure?'} - - Cancel - - + diff --git a/src/components/modals/ChatInputLoginModal/index.js b/src/components/modals/LoginModal/index.js similarity index 95% rename from src/components/modals/ChatInputLoginModal/index.js rename to src/components/modals/LoginModal/index.js index c01b4e7a52..7d7eca02dc 100644 --- a/src/components/modals/ChatInputLoginModal/index.js +++ b/src/components/modals/LoginModal/index.js @@ -18,7 +18,7 @@ type Props = { modalProps: any, }; -class ChatInputLoginModal extends React.Component { +class LoginModal extends React.Component { close = () => { this.props.dispatch(closeModal()); }; @@ -87,4 +87,4 @@ const map = state => ({ }); // $FlowIssue -export default compose(connect(map))(ChatInputLoginModal); +export default compose(connect(map))(LoginModal); diff --git a/src/components/modals/ChatInputLoginModal/style.js b/src/components/modals/LoginModal/style.js similarity index 100% rename from src/components/modals/ChatInputLoginModal/style.js rename to src/components/modals/LoginModal/style.js diff --git a/src/components/modals/RepExplainerModal/index.js b/src/components/modals/RepExplainerModal/index.js index 261ad5db69..cbeca19a3f 100644 --- a/src/components/modals/RepExplainerModal/index.js +++ b/src/components/modals/RepExplainerModal/index.js @@ -4,7 +4,7 @@ import Modal from 'react-modal'; import compose from 'recompose/compose'; import Reputation from 'src/components/reputation'; import { UserAvatar } from 'src/components/avatar'; -import Icon from 'src/components/icons'; +import Icon from 'src/components/icon'; import ModalContainer from '../modalContainer'; import { closeModal } from 'src/actions/modals'; import { connect } from 'react-redux'; @@ -56,14 +56,14 @@ class RepExplainerModal extends React.Component { Spectrum Rep - Rep provides context about a person's reputation in a community. + Rep provides context about a person’s reputation in a community. Rep is earned by starting and joining productive conversations. {reputation <= 0 ? ( currentUser ? ( - You don't have any rep yet. Earn rep by starting a + You don’t have any rep yet. Earn rep by starting a conversation or replying to other people in your communities. ) : ( @@ -74,7 +74,6 @@ class RepExplainerModal extends React.Component { diff --git a/src/components/modals/RepExplainerModal/style.js b/src/components/modals/RepExplainerModal/style.js index b273d7dc77..bd97b4c54d 100644 --- a/src/components/modals/RepExplainerModal/style.js +++ b/src/components/modals/RepExplainerModal/style.js @@ -1,7 +1,7 @@ import styled from 'styled-components'; import theme from 'shared/theme'; import { zIndex } from '../../globals'; -import { isMobile } from '../../../helpers/utils'; +import { isMobile } from 'src/helpers/utils'; const maxWidth = '460px'; const mobile = isMobile(); diff --git a/src/components/modals/ReportUserModal/index.js b/src/components/modals/ReportUserModal/index.js index 5edf007736..634a7a2200 100644 --- a/src/components/modals/ReportUserModal/index.js +++ b/src/components/modals/ReportUserModal/index.js @@ -10,7 +10,7 @@ import reportUserMutation from 'shared/graphql/mutations/user/reportUser'; import { track, events } from 'src/helpers/analytics'; import type { Dispatch } from 'redux'; import ModalContainer from '../modalContainer'; -import { TextButton, Button } from '../../buttons'; +import { TextButton, PrimaryOutlineButton } from 'src/components/button'; import { modalStyles } from '../styles'; import { TextArea, Error } from '../../formElements'; import { Form, Actions } from './style'; @@ -136,13 +136,13 @@ class ReportUserModal extends React.Component { Cancel - + diff --git a/src/components/modals/RestoreChannelModal/index.js b/src/components/modals/RestoreChannelModal/index.js index 0e9d906aa0..b294e47ead 100644 --- a/src/components/modals/RestoreChannelModal/index.js +++ b/src/components/modals/RestoreChannelModal/index.js @@ -8,7 +8,7 @@ import { addToastWithTimeout } from 'src/actions/toasts'; import type { GetChannelType } from 'shared/graphql/queries/channel/getChannel'; import restoreChannel from 'shared/graphql/mutations/channel/restoreChannel'; import ModalContainer from '../modalContainer'; -import { TextButton, Button } from '../../buttons'; +import { TextButton, PrimaryOutlineButton } from 'src/components/button'; import { modalStyles, Description } from '../styles'; import { Form, Actions } from './style'; import type { Dispatch } from 'redux'; @@ -83,12 +83,10 @@ class RestoreChannelModal extends React.Component { - - Cancel - - + diff --git a/src/components/modals/modalRoot.js b/src/components/modals/modalRoot.js index 646089a1e5..9d2cc31a6a 100644 --- a/src/components/modals/modalRoot.js +++ b/src/components/modals/modalRoot.js @@ -6,7 +6,7 @@ import DeleteDoubleCheckModal from './DeleteDoubleCheckModal'; import RepExplainerModal from './RepExplainerModal'; import ChangeChannelModal from './ChangeChannelModal'; import RestoreChannelModal from './RestoreChannelModal'; -import ChatInputLoginModal from './ChatInputLoginModal'; +import LoginModal from './LoginModal'; import ReportUserModal from './ReportUserModal'; import BanUserModal from './BanUserModal'; @@ -16,7 +16,7 @@ const MODAL_COMPONENTS = { REP_EXPLAINER_MODAL: RepExplainerModal, CHANGE_CHANNEL: ChangeChannelModal, RESTORE_CHANNEL_MODAL: RestoreChannelModal, - CHAT_INPUT_LOGIN_MODAL: ChatInputLoginModal, + LOGIN_MODAL: LoginModal, REPORT_USER_MODAL: ReportUserModal, BAN_USER_MODAL: BanUserModal, }; diff --git a/src/components/modals/styles.js b/src/components/modals/styles.js index c9bf2ab31e..d5287efd14 100644 --- a/src/components/modals/styles.js +++ b/src/components/modals/styles.js @@ -1,10 +1,9 @@ // @flow import theme from 'shared/theme'; -// $FlowFixMe import styled from 'styled-components'; import { zIndex } from '../globals'; -import { isMobile } from '../../helpers/utils'; -import { IconButton } from '../buttons'; +import { isMobile } from 'src/helpers/utils'; +import Icon from 'src/components/icon'; /* This is the global stylesheet for all modal components. Its styles will wrap @@ -78,13 +77,14 @@ export const Title = styled.div` export const Header = styled.div` padding: 20px 24px 0; display: ${props => (props.noHeader ? 'none' : 'flex')}; + justify-content: space-between; `; export const ModalContent = styled.div``; export const Footer = styled.div``; -export const CloseButton = styled(IconButton)` +export const CloseButton = styled(Icon)` position: absolute; right: 8px; top: 8px; diff --git a/src/components/newActivityIndicator/index.js b/src/components/newActivityIndicator/index.js deleted file mode 100644 index b54d5c99c1..0000000000 --- a/src/components/newActivityIndicator/index.js +++ /dev/null @@ -1,136 +0,0 @@ -import React, { Component } from 'react'; -import theme from 'shared/theme'; -import { connect } from 'react-redux'; -import { clearActivityIndicator } from '../../actions/newActivityIndicator'; -import styled from 'styled-components'; -import { Gradient } from '../globals'; - -const Pill = styled.div` - padding: ${props => (props.refetching ? '8px' : '8px 16px')}; - border-radius: 20px; - color: ${theme.text.reverse}; - background: ${props => - Gradient(props.theme.brand.alt, props.theme.brand.default)};}; - font-size: 14px; - display: flex; - align-items: center; - justify-content: center; - align-self: center; - position: fixed; - top: 0; - opacity: ${props => (props.active ? '1' : '0')}; - pointer-events: ${props => (props.active ? 'auto' : 'none')}; - left: 50%; - z-index: 9999; - transform: translateX(-50%) translateY(${props => - props.active ? '80px' : '60px'}); - font-weight: 700; - box-shadow: 0 2px 8px rgba(0,0,0,0.3); - transition: transform 0.2s ease-in-out; - cursor: pointer; - - &:hover { - transform: translateX(-50%) translateY(78px); - transition: transform 0.2s ease-in-out; - } - - &:active { - transform: translateX(-50%) translateY(80px); - transition: transform 0.1s ease-in-out; - } - - @media (max-width: 768px) { - transform: translateX(-50%) translateY(${props => - props.active ? '60px' : '40px'}); - - &:hover { - transform: translateX(-50%) translateY(58px); - transition: transform 0.2s ease-in-out; - } - - &:active { - transform: translateX(-50%) translateY(60px); - transition: transform 0.1s ease-in-out; - } - } -`; - -const scrollTo = (element, to, duration) => { - if (duration < 0) return; - const difference = to - element.scrollTop; - const perTick = (difference / duration) * 2; - - setTimeout(() => { - element.scrollTop = element.scrollTop + perTick; - scrollTo(element, to, duration - 2); - }, 10); -}; - -class Indicator extends Component { - state: { - elem: any, - }; - - constructor() { - super(); - - this.state = { - elem: null, - }; - } - - componentDidUpdate(prevProps) { - if (prevProps !== this.props) { - this.setState({ - // NOTE(@mxstbr): This is super un-reacty but it works. This refers to - // the AppViewWrapper which is the scrolling part of the site. - elem: document.getElementById(this.props.elem), - }); - } - } - - componentDidMount() { - const elem = document.getElementById(this.props.elem); - this.setState({ - // NOTE(@mxstbr): This is super un-reacty but it works. This refers to - // the AppViewWrapper which is the scrolling part of the site. - elem, - }); - - // if the component mounted while the user is scrolled to the top, immediately clear the redux store of the activity indicator - since the user can see the top of the feed, they don't need an indicator - if (elem.scrollTop < window.innerHeight / 2) { - this.props.dispatch(clearActivityIndicator()); - } - } - - componentWillUnmount() { - // when the component unmounts, clear the state so that at next mount we will always get a new scrollTop position for the scroll element - this.setState({ - elem: null, - }); - } - - clearActivityIndicator = () => { - // if the user clicks on the new activity indicator, scroll them to the top of the feed and dismiss the indicator - setTimeout(() => this.props.dispatch(clearActivityIndicator()), 120); - scrollTo(this.state.elem, 0, 80); - }; - - render() { - const { elem } = this.state; - let active = false; - - // if the scroll element exists, and the user has scrolled at least half of the screen (e.g. the top of the feed is out of view), then the user should see a new activity indicator - if (elem) { - active = elem.scrollTop > window.innerHeight / 2; - } - - return ( - - New conversations! - - ); - } -} - -export default connect()(Indicator); diff --git a/src/components/nextPageButton/index.js b/src/components/nextPageButton/index.js index 883c2cb768..4d6a09987d 100644 --- a/src/components/nextPageButton/index.js +++ b/src/components/nextPageButton/index.js @@ -2,23 +2,63 @@ import React from 'react'; import { Spinner } from '../globals'; import { HasNextPage, NextPageButton } from './style'; +import VisibilitySensor from 'react-visibility-sensor'; +import { Link, type Location } from 'react-router-dom'; type Props = { isFetchingMore?: boolean, + href?: Location, fetchMore: () => any, + children?: string, + automatic?: boolean, + topOffset?: number, + bottomOffset?: number, }; const NextPageButtonWrapper = (props: Props) => { - const { isFetchingMore, fetchMore } = props; + const { + isFetchingMore, + fetchMore, + href, + children, + automatic = true, + topOffset = -250, + bottomOffset = -250, + } = props; + const onChange = (isVisible: boolean) => { + if (isFetchingMore || !isVisible) return; + return fetchMore(); + }; return ( - - fetchMore()}> - {isFetchingMore ? ( - - ) : ( - 'Load previous messages' - )} - + { + evt.preventDefault(); + onChange(true); + }} + data-cy="load-previous-messages" + > + + + {isFetchingMore ? ( + + ) : ( + children || 'Load more' + )} + + ); }; diff --git a/src/components/nextPageButton/style.js b/src/components/nextPageButton/style.js index 158455694b..ce883f861e 100644 --- a/src/components/nextPageButton/style.js +++ b/src/components/nextPageButton/style.js @@ -1,28 +1,36 @@ // @flow import theme from 'shared/theme'; import styled from 'styled-components'; +import { hexa, tint } from 'src/components/globals'; +import { Link } from 'react-router-dom'; -export const HasNextPage = styled.div` +export const HasNextPage = styled(Link)` display: flex; align-items: center; justify-content: center; + text-decoration: none; + width: 100%; `; -export const NextPageButton = styled.div` +export const NextPageButton = styled.span` display: flex; flex: 1; + margin-top: 16px; justify-content: center; padding: 8px; - background: ${theme.bg.wash}; - color: ${theme.text.alt}; - font-size: 14px; + background: ${hexa(theme.brand.default, 0.04)}; + color: ${tint(theme.brand.default, -8)}; + border-top: 1px solid ${hexa(theme.brand.default, 0.06)}; + border-bottom: 1px solid ${hexa(theme.brand.default, 0.06)}; + font-size: 15px; font-weight: 500; position: relative; min-height: 40px; + width: 100%; &:hover { color: ${theme.brand.default}; cursor: pointer; - background: rgba(56, 24, 229, 0.1); + background: ${hexa(theme.brand.default, 0.08)}; } `; diff --git a/src/components/outsideClickHandler/index.js b/src/components/outsideClickHandler/index.js index beffba65ab..cf2e61e820 100644 --- a/src/components/outsideClickHandler/index.js +++ b/src/components/outsideClickHandler/index.js @@ -2,13 +2,13 @@ import * as React from 'react'; type Props = { - children: React.Node, + children: React$Node, style?: Object, onOutsideClick: Function, }; class OutsideAlerter extends React.Component { - wrapperRef: React.Node; + wrapperRef: React$Node; // iOS bug, see: https://stackoverflow.com/questions/10165141/jquery-on-and-delegate-doesnt-work-on-ipad componentDidMount() { @@ -25,7 +25,7 @@ class OutsideAlerter extends React.Component { .removeEventListener('mousedown', this.handleClickOutside); } - setWrapperRef = (node: React.Node) => { + setWrapperRef = (node: React$Node) => { this.wrapperRef = node; }; diff --git a/src/components/profile/community.js b/src/components/profile/community.js deleted file mode 100644 index f3091c7ce2..0000000000 --- a/src/components/profile/community.js +++ /dev/null @@ -1,385 +0,0 @@ -// @flow -import * as React from 'react'; -import Card from '../card'; -import compose from 'recompose/compose'; -import { Link } from 'react-router-dom'; -import { connect } from 'react-redux'; -import addProtocolToString from 'shared/normalize-url'; -import { CLIENT_URL } from 'src/api/constants'; -import { LoadingProfile } from '../loading'; -import Icon from '../icons'; -import { CommunityAvatar } from '../avatar'; -import { Button, OutlineButton } from '../buttons'; -import type { GetCommunityType } from 'shared/graphql/queries/community/getCommunity'; -import ToggleCommunityMembership from '../toggleCommunityMembership'; -import type { Dispatch } from 'redux'; -import { withCurrentUser } from 'src/components/withCurrentUser'; -import { - ProfileHeader, - ProfileHeaderLink, - ProfileHeaderMeta, - ProfileHeaderAction, - Title, - FullTitle, - FullProfile, - Subtitle, - FullDescription, - ExtLink, - ProfileCard, - Container, - CoverPhoto, - CoverLink, - CoverTitle, - CoverDescription, - ButtonContainer, - OnlineIndicator, -} from './style'; -import renderTextWithLinks from 'src/helpers/render-text-with-markdown-links'; - -type Props = { - onJoin: Function, - onLeave: Function, - joinedCommunity?: Function, - joinedFirstCommunity?: Function, - dispatch: Dispatch, - data: { - community: GetCommunityType, - loading: boolean, - error: ?string, - }, - profileSize: ?string, - currentUser: ?Object, - showHoverProfile?: boolean, -}; - -class CommunityWithData extends React.Component { - onJoin = community => { - this.props.joinedCommunity && this.props.joinedCommunity(1, false); - this.props.onJoin && this.props.onJoin(community); - }; - - onLeave = community => { - this.props.joinedCommunity && this.props.joinedCommunity(-1, false); - this.props.onLeave && this.props.onLeave(community); - }; - - render() { - const { - data: { community, loading, error }, - profileSize, - currentUser, - showHoverProfile = true, - } = this.props; - - if (loading) { - return ; - } else if (!community || error) { - return null; - } - - const member = community.communityPermissions.isMember; - - switch (profileSize) { - case 'upsell': - return ( - - - - - {community.name} - - - {community.description && ( - - {renderTextWithLinks(community.description)} - - )} - - - {currentUser ? ( - community.communityPermissions.isMember ? ( - ( - - Joined! - - )} - /> - ) : ( - ( - - )} - /> - ) - ) : ( - - - - )} - - - ); - case 'full': - return ( - - - - {community.name} - - - {community.description && ( -

{renderTextWithLinks(community.description)}

- )} - - {community.metaData && community.metaData.members && ( - - - {community.metaData.members.toLocaleString()} - {community.metaData.members > 1 ? ' members' : ' member'} - - )} - - {community.metaData && - typeof community.metaData.onlineMembers === 'number' && ( - - - {community.metaData.onlineMembers.toLocaleString()} online - - )} - - {community.website && ( - - - - {community.website} - - - )} -
-
- ); - case 'listItemWithAction': - return ( - - - - - {community.name} - {community.metaData && ( - - {community.metaData.members.toLocaleString()} members - - )} - - - {currentUser && member && ( - ( - - )} - /> - )} - {currentUser && !member && ( - ( - - )} - /> - )} - - ); - case 'miniWithAction': - return ( - - - - - - {community.name} - {community.metaData && ( - {community.metaData.members} - )} - - - {currentUser && member && ( - ( - - )} - /> - )} - {currentUser && !member && ( - ( - - )} - /> - )} - - - ); - case 'default': - default: - return ( - - - - - - {community.name} - - - - {currentUser && !community.communityPermissions.isOwner && ( - ( - - )} - /> - )} - - {currentUser && community.communityPermissions.isOwner && ( - - - - )} - - - ); - } - } -} - -export default compose( - withCurrentUser, - connect() -)(CommunityWithData); diff --git a/src/components/profile/coverPhoto.js b/src/components/profile/coverPhoto.js index 23c03dd791..178aa9d4ab 100644 --- a/src/components/profile/coverPhoto.js +++ b/src/components/profile/coverPhoto.js @@ -1,8 +1,10 @@ +// @flow import React from 'react'; import theme from 'shared/theme'; import styled from 'styled-components'; import { Link } from 'react-router-dom'; import { ProfileHeaderAction } from './style'; +import { MEDIA_BREAK } from 'src/components/layout'; const PhotoContainer = styled.div` grid-area: cover; @@ -17,7 +19,7 @@ const PhotoContainer = styled.div` background-position: center; border-radius: ${props => (props.large ? '0' : '12px 12px 0 0')}; - @media (max-width: 768px) { + @media (max-width: ${MEDIA_BREAK}px) { flex: 0 0 ${props => (props.large ? '160px' : '64px')}; border-radius: 0; } @@ -40,8 +42,6 @@ export const CoverPhoto = (props: Object) => { color="text.reverse" opacity="0.5" hoverColor="text.reverse" - tipText={`Edit profile`} - tipLocation={'left'} /> ) : props.currentUser ? ( @@ -50,8 +50,6 @@ export const CoverPhoto = (props: Object) => { color="text.reverse" hoverColor="text.reverse" onClick={props.onClick} - tipText={`Message ${props.user.name}`} - tipLocation={'left'} /> ) : null} {props.children} diff --git a/src/components/profile/index.js b/src/components/profile/index.js index b8799eb9ca..a27879a92d 100644 --- a/src/components/profile/index.js +++ b/src/components/profile/index.js @@ -1,24 +1,16 @@ // @flow import React from 'react'; import compose from 'recompose/compose'; -import User from './user'; -import Community from './community'; import Thread from './thread'; -const ProfilePure = (props: Object): React$Element => { +const ProfilePure = (props: Object): ?React$Element => { const { type } = props; switch (type) { - case 'user': { - return ; - } - case 'community': { - return ; - } case 'thread': { return ; } default: { - return ; + return null; } } }; @@ -42,12 +34,6 @@ type ProfileProps = { then get passed to our switch statement above to return the right component. */ export const Profile = compose()(ProfilePure); -export const UserProfile = (props: ProfileProps) => ( - -); -export const CommunityProfile = (props: ProfileProps) => ( - -); export const ThreadProfile = (props: ProfileProps) => ( ); diff --git a/src/components/profile/metaData.js b/src/components/profile/metaData.js index 05b4c4e133..03ab5849e1 100644 --- a/src/components/profile/metaData.js +++ b/src/components/profile/metaData.js @@ -1,6 +1,6 @@ //@flow import React from 'react'; -import Icon from '../icons'; +import Icon from 'src/components/icon'; import { Meta, MetaList, MetaListItem, Label, Count } from './style'; /* diff --git a/src/components/profile/style.js b/src/components/profile/style.js index b8e46399d4..bf3443f0d5 100644 --- a/src/components/profile/style.js +++ b/src/components/profile/style.js @@ -10,10 +10,12 @@ import { zIndex, Shadow, hexa, -} from '../globals'; -import { Button, OutlineButton, IconButton } from '../buttons'; -import { ReputationWrapper } from '../reputation/style'; -import Card from '../card'; +} from 'src/components/globals'; +import { Button, OutlineButton } from 'src/components/button'; +import { ReputationWrapper } from 'src/components/reputation/style'; +import Icon from 'src/components/icon'; +import Card from 'src/components/card'; +import { MEDIA_BREAK } from 'src/components/layout'; export const ProfileHeader = styled(FlexRow)` padding: 16px; @@ -56,7 +58,7 @@ export const ProfileHeaderMeta = styled(FlexCol)` min-width: 0; `; -export const ProfileHeaderAction = styled(IconButton)` +export const ProfileHeaderAction = styled(Icon)` margin-left: 16px; flex: 0 0 auto; `; @@ -79,7 +81,7 @@ export const FullProfile = styled.div` margin-top: -64px; background-color: ${theme.bg.default}; - @media (max-width: 768px) { + @media (max-width: ${MEDIA_BREAK}px) { margin-top: -48px; } `; @@ -152,30 +154,6 @@ export const FullDescription = styled.div` } `; -export const ExtLink = styled(FlexRow)` - align-items: center; - font-weight: 400; - transition: ${Transition.hover.off}; - ${Truncate}; - font-size: 16px; - margin: 12px 0; - color: ${theme.text.alt}; - - a { - color: ${theme.text.secondary}; - } - - > a:hover { - color: ${theme.text.default}; - } - - > div { - color: ${theme.text.alt}; - margin-right: 4px; - margin-top: 1px; - } -`; - export const Actions = styled(FlexRow)` padding: 16px; padding-top: 0; @@ -202,7 +180,7 @@ export const Meta = styled.div` border-top: 2px solid ${theme.bg.border}; padding: 8px 16px; width: 100%; - border-radius: 0 0 12px 12px; + border-radius: 0 0 4px 4px; `; export const MetaList = styled.ul``; @@ -281,7 +259,7 @@ export const ProfileCard = styled(Card)` `; export const ThreadProfileCard = styled(ProfileCard)` - border-radius: 8px; + border-radius: 4px; box-shadow: ${Shadow.low} ${({ theme }) => hexa(theme.text.default, 0.1)}; `; @@ -294,16 +272,16 @@ export const CoverPhoto = styled.div` background-size: cover; background-repeat: no-repeat; background-position: center; - border-radius: ${props => (props.large ? '12px' : '12px 12px 0 0')}; + border-radius: ${props => (props.large ? '4px' : '4px 4px 0 0')}; `; export const Container = styled.div` background: ${theme.bg.default}; - box-shadow: ${Shadow.mid} ${props => hexa(props.theme.bg.reverse, 0.15)}; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.08); flex: 0 0 22%; display: flex; flex-direction: column; - border-radius: 12px; + border-radius: 4px; position: relative; z-index: ${zIndex.card}; margin: 16px; @@ -340,13 +318,3 @@ export const MessageButtonContainer = styled.div` text-align: center; } `; - -export const OnlineIndicator = styled.span` - width: 10px; - height: 10px; - border-radius: 5px; - background: ${props => (props.offline ? theme.text.alt : theme.success.alt)}; - margin-right: 12px; - display: inline-block; - margin-left: 6px; -`; diff --git a/src/components/profile/user.js b/src/components/profile/user.js deleted file mode 100644 index 4b4e1b02ab..0000000000 --- a/src/components/profile/user.js +++ /dev/null @@ -1,277 +0,0 @@ -// @flow -import React from 'react'; -import { Link } from 'react-router-dom'; -import Card from 'src/components/card'; -import { connect } from 'react-redux'; -import { withRouter } from 'react-router'; -import compose from 'recompose/compose'; -import addProtocolToString from 'shared/normalize-url'; -import type { GetUserType } from 'shared/graphql/queries/user/getUser'; -import { initNewThreadWithUser } from 'src/actions/directMessageThreads'; -import Icon from '../icons'; -import { CoverPhoto } from './coverPhoto'; -import GithubProfile from 'src/components/githubProfile'; -import type { ProfileSizeProps } from './index'; -import { UserAvatar } from 'src/components/avatar'; -import Badge from 'src/components/badges'; -import { displayLoadingCard } from 'src/components/loading'; -import Reputation from 'src/components/reputation'; -import renderTextWithLinks from 'src/helpers/render-text-with-markdown-links'; -import type { Dispatch } from 'redux'; -import { withCurrentUser } from 'src/components/withCurrentUser'; -import { - FullProfile, - ProfileHeader, - ProfileHeaderLink, - ProfileHeaderNoLink, - ProfileHeaderMeta, - ProfileHeaderAction, - CoverLink, - CoverTitle, - CoverSubtitle, - CoverDescription, - FullTitle, - Subtitle, - FullDescription, - Title, - ExtLink, - OnlineIndicator, -} from './style'; - -type CurrentUserProps = { - id: string, - profilePhoto: string, - displayName: string, - username: string, - name: ?string, - website: string, -}; - -type ThisUserType = { - ...$Exact, - contextPermissions: { - reputation: number, - }, -}; - -type UserWithDataProps = { - data: { user: ThisUserType }, - profileSize: ProfileSizeProps, - currentUser: CurrentUserProps, - dispatch: Dispatch, - history: Object, - showHoverProfile: boolean, -}; - -const UserWithData = ({ - data: { user }, - profileSize, - currentUser, - dispatch, - history, - showHoverProfile = true, -}: UserWithDataProps): ?React$Element => { - const componentSize = profileSize || 'mini'; - - if (!user) { - return null; - } - - const initMessage = () => { - dispatch(initNewThreadWithUser(user)); - history.push('/messages/new'); - }; - - switch (componentSize) { - case 'full': - return ( - - - {user.name} - - @{user.username} - {user.betaSupporter && } - - - - {user.description &&

{renderTextWithLinks(user.description)}

} - - {user.isOnline && ( - - Online now - - )} - - - {user.website && ( - - - - {user.website} - - - )} - { - if (!profile) { - return null; - } else { - return ( - - - - github.com/ - {profile.username} - - - ); - } - }} - /> -
-
- ); - case 'simple': - return ( - - initMessage()} - currentUser={currentUser} - /> - - - {user.name} - - - {user.username && `@${user.username}`} - - - - {user.description && ( - -

{user.description}

-
- )} -
- ); - case 'default': - default: - return ( - - - {user.username ? ( - - - - {user.name} - {user.username && @{user.username}} - - - ) : ( - - - - {user.name} - {user.username && ( - - @{user.username} - - - )} - - - )} - {currentUser && currentUser.id === user.id ? ( - - - - ) : ( - initMessage()} - tipText={`Message ${user.name}`} - tipLocation={'top-left'} - /> - )} - - - ); - } -}; - -const User = compose( - displayLoadingCard, - withRouter, - withCurrentUser -)(UserWithData); -const mapStateToProps = state => ({ - initNewThreadWithUser: state.directMessageThreads.initNewThreadWithUser, -}); -// $FlowFixMe -export default connect(mapStateToProps)(User); diff --git a/src/components/reaction/index.js b/src/components/reaction/index.js index 8077f9c2e2..40d241cacb 100644 --- a/src/components/reaction/index.js +++ b/src/components/reaction/index.js @@ -18,7 +18,7 @@ class Reaction extends React.Component { const { toggleReaction, message, dispatch, currentUser } = this.props; if (!currentUser) { - return dispatch(openModal('CHAT_INPUT_LOGIN_MODAL', {})); + return dispatch(openModal('LOGIN_MODAL', {})); } return toggleReaction({ diff --git a/src/components/reaction/style.js b/src/components/reaction/style.js index 4f42101c9f..92bbef997e 100644 --- a/src/components/reaction/style.js +++ b/src/components/reaction/style.js @@ -1,6 +1,5 @@ // @flow import styled from 'styled-components'; -import { Tooltip } from 'src/components/globals'; export const ReactionWrapper = styled.span` display: flex; @@ -34,6 +33,4 @@ export const ReactionWrapper = styled.span` margin-right: 4px; margin-top: -1px; } - - ${Tooltip}; `; diff --git a/src/components/reputation/index.js b/src/components/reputation/index.js index e589bd3a27..daab571a6e 100644 --- a/src/components/reputation/index.js +++ b/src/components/reputation/index.js @@ -1,17 +1,16 @@ // @flow import * as React from 'react'; import { connect } from 'react-redux'; +import type { Dispatch } from 'redux'; +import Tooltip from 'src/components/tooltip'; import { openModal } from 'src/actions/modals'; import { truncateNumber } from 'src/helpers/utils'; -import Icon from 'src/components/icons'; +import Icon from 'src/components/icon'; import { ReputationWrapper, ReputationLabel } from './style'; -import type { Dispatch } from 'redux'; type Props = { size?: 'mini' | 'default' | 'large', reputation: number, - tipText?: string, - tipLocation?: string, dispatch: Dispatch, ignoreClick?: boolean, }; @@ -25,28 +24,22 @@ class Reputation extends React.Component { }; render() { - const { - tipText = 'Reputation', - tipLocation = 'top-right', - reputation, - } = this.props; + const { reputation } = this.props; if (reputation === undefined || reputation === null) return null; const renderedReputation = reputation > 0 ? `${reputation}` : '0'; return ( - - - - - {truncateNumber(parseInt(renderedReputation, 10), 1)} - - + + + + + + {truncateNumber(parseInt(renderedReputation, 10), 1)} + + + ); } } diff --git a/src/components/reputation/style.js b/src/components/reputation/style.js index 06763529ba..94e6199666 100644 --- a/src/components/reputation/style.js +++ b/src/components/reputation/style.js @@ -1,7 +1,7 @@ // @flow import theme from 'shared/theme'; import styled from 'styled-components'; -import { Tooltip, zIndex } from '../globals'; +import { zIndex } from '../globals'; export const ReputationWrapper = styled.div` display: flex; @@ -11,7 +11,6 @@ export const ReputationWrapper = styled.div` cursor: pointer; position: relative; z-index: ${zIndex.fullScreen}; - ${Tooltip}; width: fit-content; `; diff --git a/src/components/rich-text-editor/style.js b/src/components/rich-text-editor/style.js index 89b0881c04..f7f739f998 100644 --- a/src/components/rich-text-editor/style.js +++ b/src/components/rich-text-editor/style.js @@ -9,6 +9,7 @@ import { Transition, zIndex } from 'src/components/globals'; import { UserHoverProfile } from 'src/components/hoverProfile'; import type { Node } from 'react'; import { withCurrentUser } from 'src/components/withCurrentUser'; +import { MEDIA_BREAK } from 'src/components/layout'; const UsernameWrapper = styled.span` color: ${props => @@ -100,7 +101,7 @@ export const MediaRow = styled.div` margin-top: 16px; width: calc(100% + 48px); - @media (max-width: 768px) { + @media (max-width: ${MEDIA_BREAK}px) { position: absolute; top: calc(100% - 90px); } diff --git a/src/components/scrollManager/index.js b/src/components/scrollManager/index.js index 0bdb7a9781..b346aeac13 100644 --- a/src/components/scrollManager/index.js +++ b/src/components/scrollManager/index.js @@ -1,14 +1,12 @@ import * as React from 'react'; -// $FlowFixMe -import { withRouter } from 'react-router'; -// $FlowFixMe +import { withRouter } from 'react-router-dom'; import debounceFn from 'debounce'; type Props = { scrollCaptureDebounce: number, scrollSyncDebounce: number, scrollSyncAttemptLimit: number, - children: React.Node, + children: React$Node, history: Object, location: Object, onLocationChange: Function, diff --git a/src/components/scrollRow/index.js b/src/components/scrollRow/index.js index cbca631dc7..fa7a0f18d5 100644 --- a/src/components/scrollRow/index.js +++ b/src/components/scrollRow/index.js @@ -45,7 +45,7 @@ class ScrollRow extends Component { return ( (this.hscroll = comp)} + ref={comp => (this.hscroll = comp)} > {this.props.children} diff --git a/src/components/segmentedControl/index.js b/src/components/segmentedControl/index.js index caa4a0667c..53676f1491 100644 --- a/src/components/segmentedControl/index.js +++ b/src/components/segmentedControl/index.js @@ -1,76 +1,54 @@ // @flow -import theme from 'shared/theme'; -import styled, { css } from 'styled-components'; -import { FlexRow } from '../globals'; - -export const SegmentedControl = styled(FlexRow)` - align-self: stretch; - margin: 0 32px; - margin-top: 16px; - border-bottom: 1px solid ${theme.bg.border}; - align-items: stretch; - min-height: 48px; - ${props => props.css}; - - @media (max-width: 768px) { - overflow-y: hidden; - overflow-x: scroll; - background-color: ${theme.bg.default}; - align-self: stretch; - margin: 0; - margin-bottom: 2px; - } -`; - -export const Segment = styled(FlexRow)` - padding: 8px 24px; - justify-content: center; - align-items: center; - text-align: center; - line-height: 1; - font-size: 18px; - font-weight: 500; - color: ${props => - props.selected ? props.theme.text.default : props.theme.text.alt}; - cursor: pointer; - position: relative; - top: 1px; - border-bottom: 1px solid - ${props => (props.selected ? props.theme.text.default : 'transparent')}; - - .icon { - margin-right: 8px; - } - - ${props => - props.selected && - css` - border-bottom: 1px solid ${theme.bg.reverse}; - `}; - - &:hover { - color: ${theme.text.default}; - } - - @media (max-width: 768px) { - flex: auto; - justify-content: center; - text-align: center; - - .icon { - margin-right: 0; - } +import React from 'react'; +import { StyledSegmentedControl, StyledSegment } from './style'; +import { Link } from 'react-router-dom'; + +type ControlProps = { + sticky?: boolean, + stickyOffset?: number, + mobileSticky?: boolean, + mobileStickyOffset?: number, +}; + +export const SegmentedControl = (props: ControlProps) => { + const { + sticky = true, + stickyOffset = 0, + mobileSticky = true, + mobileStickyOffset = 0, + ...rest + } = props; + return ( + + ); +}; + +type SegmentProps = { + isActive: boolean, + hideOnDesktop?: boolean, + to?: string, +}; + +export const Segment = (props: SegmentProps) => { + const { isActive = false, hideOnDesktop = false, to, ...rest } = props; + + const component = ( + + ); + + if (to) { + return {component}; } -`; -export const DesktopSegment = styled(Segment)` - @media (max-width: 768px) { - display: none; - } -`; - -export const MobileSegment = styled(Segment)` - @media (min-width: 768px) { - display: none; - } -`; + return component; +}; diff --git a/src/components/segmentedControl/style.js b/src/components/segmentedControl/style.js new file mode 100644 index 0000000000..1d03c1bef9 --- /dev/null +++ b/src/components/segmentedControl/style.js @@ -0,0 +1,77 @@ +// @flow +import styled, { css } from 'styled-components'; +import theme from 'shared/theme'; +import { tint } from 'src/components/globals'; +import { MEDIA_BREAK } from 'src/components/layout'; + +export const StyledSegmentedControl = styled.div` + display: flex; + width: 100%; + box-shadow: inset 0 -1px ${theme.bg.border}; + background: ${theme.bg.default}; + overflow: hidden; + overflow-x: scroll; + position: ${props => (props.sticky ? 'sticky' : 'relative')}; + z-index: ${props => (props.sticky ? '13' : '1')}; + + ${props => + props.sticky && + css` + top: ${props => (props.stickyOffset ? `${props.stickyOffset}px` : '0')}; + `}; + + &::-webkit-scrollbar { + width: 0px; + height: 0px; + background: transparent; /* make scrollbar transparent */ + } + + @media (max-width: ${MEDIA_BREAK}px) { + max-width: 100vw; + position: ${props => (props.mobileSticky ? 'sticky' : 'relative')}; + top: ${props => + props.mobileStickyOffset ? `${props.mobileStickyOffset}px` : '0'}; + } +`; + +export const StyledSegment = styled.div` + display: flex; + align-items: center; + justify-content: center; + padding: 16px; + flex: 1 0 auto; + font-weight: 600; + color: ${props => (props.isActive ? theme.text.default : theme.text.alt)}; + box-shadow: ${props => + props.isActive ? `inset 0 -2px 0 ${theme.text.default}` : 'none'}; + text-align: center; + + &:hover { + background: ${theme.bg.wash}; + box-shadow: ${props => + props.isActive + ? `inset 0 -2px 0 ${theme.text.default}` + : `inset 0 -2px 0 ${tint(theme.bg.wash, -16)}`}; + color: ${props => + props.isActive ? theme.text.default : theme.text.secondary}; + cursor: pointer; + } + + @media (max-width: ${MEDIA_BREAK}px) { + &:hover { + background: ${theme.bg.default}; + } + + &:active { + background: ${theme.bg.wash}; + } + } + + @media (min-width: ${MEDIA_BREAK}px) { + ${props => + props.hideOnDesktop && + css` + display: none; + `} + } +`; diff --git a/src/components/select/index.js b/src/components/select/index.js new file mode 100644 index 0000000000..22769d386c --- /dev/null +++ b/src/components/select/index.js @@ -0,0 +1,19 @@ +// @flow +import React from 'react'; +import Icon from 'src/components/icon'; +import { Select, Container, IconContainer } from './style'; + +type Props = { + children: React$Node, + onChange: (evt: SyntheticInputEvent) => void, + defaultValue?: ?string, +}; + +export default (props: Props) => ( + + { {subhead} {refresh && ( - + )} {children} diff --git a/src/helpers/notifications.js b/src/helpers/notifications.js index 8a1aa8c445..cf38a484b5 100644 --- a/src/helpers/notifications.js +++ b/src/helpers/notifications.js @@ -1,8 +1,6 @@ import React from 'react'; -//$FlowFixMe import { Link } from 'react-router-dom'; - -import Icon from '../components/icons'; +import Icon from '../components/icon'; import { HorizontalRuleWithIcon } from '../components/globals'; import { ChatMessage } from '../views/notifications/style'; diff --git a/src/helpers/realtimeThreads.js b/src/helpers/realtimeThreads.js index cc3b12d29a..8ecd0b1d5a 100644 --- a/src/helpers/realtimeThreads.js +++ b/src/helpers/realtimeThreads.js @@ -1,13 +1,7 @@ -import { addActivityIndicator } from '../actions/newActivityIndicator'; -import type { Dispatch } from 'redux'; - +// @flow // used to update feed caches with new threads in real time // takes an array of existing threads in the cache and figures out how to insert the newly updated thread -export default ( - prevThreads: Array, - updatedThread: Object, - dispatch: Dispatch -) => { +export default (prevThreads: Array, updatedThread: Object) => { // get an array of thread ids based on the threads already in cache const prevThreadIds = prevThreads.map(thread => thread.node.id); @@ -16,11 +10,6 @@ export default ( // been updated with a new message const hasNewThread = prevThreadIds.indexOf(updatedThread.id) < 0; - // if the updated thread is new, show the activity indicator in the ui - if (hasNewThread) { - dispatch(addActivityIndicator()); - } - return hasNewThread ? [ { diff --git a/src/helpers/signed-out-fallback.js b/src/helpers/signed-out-fallback.js index 8408ce0ebf..760da6ebf7 100644 --- a/src/helpers/signed-out-fallback.js +++ b/src/helpers/signed-out-fallback.js @@ -9,11 +9,8 @@ const Switch = props => { return ( {authed => { - if (!authed) { - return ; - } else { - return ; - } + if (!authed) return ; + return ; }} ); diff --git a/src/hooks/useAppScroller.js b/src/hooks/useAppScroller.js new file mode 100644 index 0000000000..0cb5938dc7 --- /dev/null +++ b/src/hooks/useAppScroller.js @@ -0,0 +1,27 @@ +// @flow +import { useState, useEffect } from 'react'; + +export const useAppScroller = () => { + const [ref, setRef] = useState(null); + + useEffect(() => { + if (!ref) setRef(document.getElementById('main')); + }); + + const scrollToTop = () => { + const elem = ref || document.getElementById('main'); + if (elem) return (elem.scrollTop = 0); + }; + + const scrollToBottom = () => { + const elem = ref || document.getElementById('main'); + if (elem) return (elem.scrollTop = elem.scrollHeight - elem.clientHeight); + }; + + const scrollTo = (pos: number) => { + const elem = ref || document.getElementById('main'); + if (elem) return (elem.scrollTop = pos); + }; + + return { scrollToTop, scrollTo, scrollToBottom, ref }; +}; diff --git a/src/hooks/usePrevious.js b/src/hooks/usePrevious.js new file mode 100644 index 0000000000..62d70b2b6e --- /dev/null +++ b/src/hooks/usePrevious.js @@ -0,0 +1,12 @@ +// @flow +import { useRef, useEffect } from 'react'; + +function usePrevious(value: T): ?T { + const ref = useRef(); + useEffect(() => { + ref.current = value; + }); + return ref.current; +} + +export default usePrevious; diff --git a/src/index.js b/src/index.js index 3441a49706..69dfa8c489 100644 --- a/src/index.js +++ b/src/index.js @@ -19,27 +19,18 @@ import { subscribeToDesktopPush } from 'src/subscribe-to-desktop-push'; import RedirectHandler from 'src/components/redirectHandler'; const params = queryString.parse(history.location.search); -// Redirect legacy ?thread=asdfxyz URLs to the proper /// +// Redirect legacy ?thread=asdf & ?t=asdf URLs to the proper /// // equivalents via the /thread/ shorthand -if (params.thread) { +const threadParam = params.thread || params.t; +if (threadParam) { if (params.m) { - history.replace(`/thread/${params.thread}?m=${params.m}`); + history.replace(`/thread/${threadParam}?m=${params.m}`); } else { - history.replace(`/thread/${params.thread}`); + history.replace(`/thread/${threadParam}`); } } // If the server passes an initial redux state use that, otherwise construct our own -const store = initStore( - window.__SERVER_STATE__ || { - dashboardFeed: { - activeThread: params.t || '', - mountedWithActiveThread: params.t || '', - search: { - isOpen: false, - }, - }, - } -); +const store = initStore(window.__SERVER_STATE__ || {}); const App = () => { return ( diff --git a/src/reducers/dashboardFeed.js b/src/reducers/dashboardFeed.js deleted file mode 100644 index b3ae487f4d..0000000000 --- a/src/reducers/dashboardFeed.js +++ /dev/null @@ -1,49 +0,0 @@ -const initialState = { - activeCommunity: null, - activeThread: null, - activeChannel: null, - mountedWithActiveThread: null, - search: { - isOpen: false, - queryString: '', - }, -}; - -export default function dashboardFeed(state = initialState, action) { - switch (action.type) { - case 'SELECT_FEED_THREAD': - return Object.assign({}, state, { - activeThread: action.threadId, - }); - case 'SELECT_FEED_COMMUNITY': - return Object.assign({}, state, { - activeCommunity: action.communityId, - }); - case 'SELECT_FEED_CHANNEL': - return Object.assign({}, state, { - activeChannel: action.channelId, - }); - case 'REMOVE_MOUNTED_THREAD_ID': - return Object.assign({}, state, { - mountedWithActiveThread: null, - }); - case 'TOGGLE_SEARCH_OPEN': { - return Object.assign({}, state, { - search: { - ...state.search, - isOpen: action.value, - }, - }); - } - case 'SET_SEARCH_VALUE_FOR_SERVER': { - return Object.assign({}, state, { - search: { - ...state.search, - queryString: action.value, - }, - }); - } - default: - return state; - } -} diff --git a/src/reducers/index.js b/src/reducers/index.js index ed25aa8794..158b90844d 100644 --- a/src/reducers/index.js +++ b/src/reducers/index.js @@ -4,13 +4,11 @@ import modals from './modals'; import toasts from './toasts'; import directMessageThreads from './directMessageThreads'; import gallery from './gallery'; -import newUserOnboarding from './newUserOnboarding'; -import newActivityIndicator from './newActivityIndicator'; -import dashboardFeed from './dashboardFeed'; import threadSlider from './threadSlider'; import notifications from './notifications'; import message from './message'; import connectionStatus from './connectionStatus'; +import titlebar from './titlebar'; const getReducers = () => { return combineReducers({ @@ -18,13 +16,11 @@ const getReducers = () => { toasts, directMessageThreads, gallery, - newUserOnboarding, - newActivityIndicator, - dashboardFeed, threadSlider, notifications, connectionStatus, message, + titlebar, }); }; diff --git a/src/reducers/message.js b/src/reducers/message.js index efdd498817..acc600040a 100644 --- a/src/reducers/message.js +++ b/src/reducers/message.js @@ -1,5 +1,5 @@ // @flow -import type { ReplyToMessageActionType } from '../actions/message'; +import type { ReplyToMessageActionType } from 'src/actions/message'; const initialState = { quotedMessage: {}, diff --git a/src/reducers/newActivityIndicator.js b/src/reducers/newActivityIndicator.js deleted file mode 100644 index 139c8b12d4..0000000000 --- a/src/reducers/newActivityIndicator.js +++ /dev/null @@ -1,17 +0,0 @@ -const initialState = { - hasNew: false, -}; - -export default function newActivityIndicator(state = initialState, action) { - switch (action.type) { - case 'HAS_NEW_ACTIVITY': - return { - hasNew: true, - }; - case 'CLEAR_NEW_ACTIVITY_INDICATOR': { - return initialState; - } - default: - return state; - } -} diff --git a/src/reducers/newUserOnboarding.js b/src/reducers/newUserOnboarding.js deleted file mode 100644 index 8072394210..0000000000 --- a/src/reducers/newUserOnboarding.js +++ /dev/null @@ -1,14 +0,0 @@ -const initialState = { - community: null, -}; - -export default function newUserOnboarding(state = initialState, action) { - switch (action.type) { - case 'ADD_COMMUNITY_TO_NEW_USER_ONBOARDING': - return Object.assign({}, state, { - community: action.payload, - }); - default: - return state; - } -} diff --git a/src/reducers/notifications.js b/src/reducers/notifications.js index bf5779464e..5b86859a14 100644 --- a/src/reducers/notifications.js +++ b/src/reducers/notifications.js @@ -1,6 +1,7 @@ const initialState = { directMessageNotifications: 0, notifications: 0, + notificationsData: [], }; export default function notifications(state = initialState, action) { @@ -10,6 +11,12 @@ export default function notifications(state = initialState, action) { obj[action.countType] = action.count; return obj; } + case 'SET_NOTIFICATIONS': { + return { + ...state, + notificationsData: action.notifications, + }; + } default: return state; } diff --git a/src/reducers/titlebar.js b/src/reducers/titlebar.js new file mode 100644 index 0000000000..0b0033bf9f --- /dev/null +++ b/src/reducers/titlebar.js @@ -0,0 +1,26 @@ +// @flow +const initialState = { + title: '', + titleIcon: null, + rightAction: null, + leftAction: 'menu', +}; + +export default function titlebar( + state: typeof initialState = initialState, + action: Object +) { + const { type, payload } = action; + switch (type) { + case 'SET_TITLEBAR_PROPS': { + return Object.assign({}, state, { + title: payload.title && payload.title, + titleIcon: payload.titleIcon && payload.titleIcon, + rightAction: payload.rightAction && payload.rightAction, + leftAction: payload.leftAction || 'menu', + }); + } + default: + return state; + } +} diff --git a/src/reset.css.js b/src/reset.css.js index 8b04f1163e..0fadea5b3e 100644 --- a/src/reset.css.js +++ b/src/reset.css.js @@ -1,13 +1,12 @@ // @flow -import { injectGlobal } from 'styled-components'; +import { createGlobalStyle } from 'styled-components'; // $FlowIssue import prismGlobalCSS from '!!raw-loader!./components/rich-text-editor/prism-theme.css'; import theme from 'shared/theme'; -// $FlowIssue -injectGlobal`${prismGlobalCSS}`; +export default createGlobalStyle` + ${prismGlobalCSS} -injectGlobal` * { border: 0; box-sizing: inherit; @@ -24,14 +23,12 @@ injectGlobal` html { display: flex; - height: 100%; + min-height: 100%; width: 100%; - max-height: 100%; - max-width: 100%; box-sizing: border-box; font-size: 16px; line-height: 1.5; - background-color: ${theme.bg.default}; + background-color: ${theme.bg.wash}; color: #16171a; padding: 0; margin: 0; @@ -42,15 +39,18 @@ injectGlobal` } body { - display: flex; box-sizing: border-box; - flex: auto; - align-self: stretch; - max-width: 100%; - max-height: 100%; + width: 100%; + height: 100%; + overscroll-behavior-y: none; -webkit-overflow-scrolling: touch; } + #root { + height: 100% + width: 100%; + } + a { color: currentColor; text-decoration: none; @@ -94,20 +94,6 @@ injectGlobal` color: ${theme.text.placeholder}; } - #root { - display: flex; - display: -webkit-box; - display: -webkit-flex; - display: -moz-flex; - display: -ms-flexbox; - flex-direction: column; - -ms-flex-direction: column; - -moz-flex-direction: column; - -webkit-flex-direction: column; - height: 100%; - width: 100%; - } - .fade-enter { opacity: 0; z-index: 1; @@ -119,14 +105,19 @@ injectGlobal` } .markdown { - font-size: 15px; + font-size: 16px; line-height: 1.4; color: ${theme.text.default}; } + .markdown pre { + font-size: 15px; + white-space: pre-wrap; + } + .markdown p { color: inherit; - font-size: 15px; + font-size: 16px; font-weight: 400; display: block; } @@ -172,7 +163,7 @@ injectGlobal` .markdown li { color: inherit; - font-size: 15px; + font-size: 16px; margin-bottom: 4px; line-height: 1.5; font-weight: 400; @@ -183,7 +174,7 @@ injectGlobal` border-left: 4px solid ${theme.text.secondary}; background: ${theme.bg.wash}; padding: 4px 8px 4px 16px; - font-size: 15px; + font-size: 16px; font-weight: 400; line-height: 1.4; margin: 16px 0; @@ -329,7 +320,10 @@ injectGlobal` .threadComposer textarea { line-height: 1.5; - /* account for bottom save bar when editing */ - height: calc(100% + 52px)!important; + height: calc(100% + 48px)!important; + } + + .tippy-backdrop { + background-color: ${theme.text.default}; } `; diff --git a/src/routes.js b/src/routes.js index 2cf95a3d47..1c63f3c4eb 100644 --- a/src/routes.js +++ b/src/routes.js @@ -9,102 +9,110 @@ import { type Location, type History, } from 'react-router'; -import styled, { ThemeProvider } from 'styled-components'; +import { ThemeProvider } from 'styled-components'; import Loadable from 'react-loadable'; import { ErrorBoundary } from 'src/components/error'; import { CLIENT_URL } from './api/constants'; import generateMetaInfo from 'shared/generate-meta-info'; -import './reset.css.js'; +import GlobalStyles from './reset.css.js'; +import { GlobalThreadAttachmentStyles } from 'src/components/message/threadAttachment/style'; import { theme } from 'shared/theme'; -import { FlexCol } from './components/globals'; +import AppViewWrapper from 'src/components/appViewWrapper'; import ScrollManager from 'src/components/scrollManager'; import Head from 'src/components/head'; import ModalRoot from 'src/components/modals/modalRoot'; import Gallery from 'src/components/gallery'; import Toasts from 'src/components/toasts'; -import { Loading, LoadingScreen } from 'src/components/loading'; import Composer from 'src/components/composer'; -import AuthViewHandler from 'src/views/authViewHandler'; import signedOutFallback from 'src/helpers/signed-out-fallback'; import PrivateChannelJoin from 'src/views/privateChannelJoin'; import PrivateCommunityJoin from 'src/views/privateCommunityJoin'; import ThreadSlider from 'src/views/threadSlider'; -import Navbar from 'src/views/navbar'; +import Navigation from 'src/views/navigation'; import Status from 'src/views/status'; import Login from 'src/views/login'; import DirectMessages from 'src/views/directMessages'; -import { FullscreenThreadView } from 'src/views/thread'; -import ThirdPartyContext from 'src/components/thirdPartyContextSetting'; +import { ThreadView } from 'src/views/thread'; +import AnalyticsTracking from 'src/components/analyticsTracking'; import { withCurrentUser } from 'src/components/withCurrentUser'; import Maintenance from 'src/components/maintenance'; import type { GetUserType } from 'shared/graphql/queries/user/getUser'; import RedirectOldThreadRoute from './views/thread/redirect-old-route'; import NewUserOnboarding from './views/newUserOnboarding'; import QueryParamToastDispatcher from './views/queryParamToastDispatcher'; +import { LoadingView } from 'src/views/viewHelpers'; +import GlobalTitlebar from 'src/views/globalTitlebar'; +import NoUsernameHandler from 'src/views/authViewHandler/noUsernameHandler'; const Explore = Loadable({ loader: () => import('./views/explore' /* webpackChunkName: "Explore" */), - loading: ({ isLoading }) => isLoading && , + loading: ({ isLoading }) => isLoading && , }); /* prettier-ignore */ const UserView = Loadable({ loader: () => import('./views/user'/* webpackChunkName: "UserView" */), - loading: ({ isLoading }) => isLoading && , + loading: ({ isLoading }) => isLoading && , }); /* prettier-ignore */ const CommunityView = Loadable({ loader: () => import('./views/community'/* webpackChunkName: "CommunityView" */), - loading: ({ isLoading }) => isLoading && , + loading: ({ isLoading }) => isLoading && , }); /* prettier-ignore */ const CommunityLoginView = Loadable({ loader: () => import('./views/communityLogin'/* webpackChunkName: "CommunityView" */), - loading: ({ isLoading }) => isLoading && , + loading: ({ isLoading }) => isLoading && , }); /* prettier-ignore */ const ChannelView = Loadable({ loader: () => import('./views/channel'/* webpackChunkName: "ChannelView" */), - loading: ({ isLoading }) => isLoading && , + loading: ({ isLoading }) => isLoading && , }); /* prettier-ignore */ -const Dashboard = Loadable({ - loader: () => import('./views/dashboard'/* webpackChunkName: "Dashboard" */), - loading: ({ isLoading }) => isLoading && null, +const HomeViewRedirect = Loadable({ + loader: () => import('./views/homeViewRedirect'/* webpackChunkName: "HomeViewRedirect" */), + loading: ({ isLoading }) => isLoading && , }); /* prettier-ignore */ const Notifications = Loadable({ loader: () => import('./views/notifications'/* webpackChunkName: "Notifications" */), - loading: ({ isLoading }) => isLoading && , + loading: ({ isLoading }) => isLoading && , }); /* prettier-ignore */ const UserSettings = Loadable({ loader: () => import('./views/userSettings'/* webpackChunkName: "UserSettings" */), - loading: ({ isLoading }) => isLoading && , + loading: ({ isLoading }) => isLoading && , }); /* prettier-ignore */ const CommunitySettings = Loadable({ loader: () => import('./views/communitySettings'/* webpackChunkName: "communitySettings" */), - loading: ({ isLoading }) => isLoading && , + loading: ({ isLoading }) => isLoading && , }); /* prettier-ignore */ const ChannelSettings = Loadable({ loader: () => import('./views/channelSettings'/* webpackChunkName: "channelSettings" */), - loading: ({ isLoading }) => isLoading && , + loading: ({ isLoading }) => isLoading && , }); /* prettier-ignore */ const NewCommunity = Loadable({ loader: () => import('./views/newCommunity'/* webpackChunkName: "NewCommunity" */), - loading: ({ isLoading }) => isLoading && , + loading: ({ isLoading }) => isLoading && , +}); + +/* prettier-ignore */ +const NewDirectMessage = Loadable({ + loader: () => import('./views/newDirectMessage'/* webpackChunkName: "NewDirectMessage" */), + loading: ({ isLoading }) => isLoading && , }); /* prettier-ignore */ @@ -116,25 +124,19 @@ const Pages = Loadable({ /* prettier-ignore */ const Search = Loadable({ loader: () => import('./views/search'/* webpackChunkName: "Search" */), - loading: ({ isLoading }) => isLoading && , + loading: ({ isLoading }) => isLoading && , }); /* prettier-ignore */ const ErrorFallback = Loadable({ loader: () => import('./components/error'/* webpackChunkName: "Error" */), - loading: ({ isLoading }) => isLoading && + loading: ({ isLoading }) => isLoading && }); -const Body = styled(FlexCol)` - display: flex; - width: 100vw; - height: 100vh; - max-height: 100vh; - background: ${theme.bg.wash}; -`; - -const DashboardFallback = signedOutFallback(Dashboard, Pages); -const HomeFallback = signedOutFallback(Dashboard, () => ); +const HomeViewRedirectFallback = signedOutFallback(HomeViewRedirect, Pages); +const HomeFallback = signedOutFallback(HomeViewRedirect, () => ( + +)); const LoginFallback = signedOutFallback(() => , Login); const CommunityLoginFallback = signedOutFallback( props => , @@ -143,6 +145,9 @@ const CommunityLoginFallback = signedOutFallback( const NewCommunityFallback = signedOutFallback(NewCommunity, () => ( )); +const NewDirectMessageFallback = signedOutFallback(NewDirectMessage, () => ( + +)); const MessagesFallback = signedOutFallback(DirectMessages, () => ( )); @@ -162,6 +167,15 @@ const ComposerFallback = signedOutFallback(Composer, () => ( )); +export const RouteModalContext = React.createContext({ + isModal: false, +}); + +export const NavigationContext = React.createContext({ + navigationIsOpen: false, + setNavigationIsOpen: () => {}, +}); + type Props = { currentUser: ?GetUserType, isLoadingCurrentUser: boolean, @@ -170,8 +184,13 @@ type Props = { history: History, }; -class Routes extends React.Component { +type State = { + navigationIsOpen: boolean, +}; + +class Routes extends React.Component { previousLocation = this.props.location; + state = { navigationIsOpen: false }; componentWillUpdate(nextProps) { const { location } = this.props; @@ -184,21 +203,24 @@ class Routes extends React.Component { } } + setNavigationIsOpen = (val: boolean) => + this.setState({ navigationIsOpen: val }); + render() { const { currentUser, isLoadingCurrentUser } = this.props; + const { navigationIsOpen } = this.state; const { title, description } = generateMetaInfo(); if (this.props.maintenanceMode) { return ( - - - - + + + ); @@ -211,190 +233,277 @@ class Routes extends React.Component { this.previousLocation !== location ); // not initial render + // allows any UI in the tree to open or close the side navigation on mobile + const navigationContext = { + navigationIsOpen, + setNavigationIsOpen: this.setNavigationIsOpen, + }; + + // allows any UI in the tree to know if it is existing within a modal or not + // commonly used for background views to know that they are backgrounded + const routeModalContext = { isModal }; + return ( - - - - - {/* Default meta tags, get overriden by anything further down the tree */} - - {/* Global navigation, notifications, message notifications, etc */} + + + + {/* default meta tags, get overriden by anything further down the tree */} + + + + + {/* dont let non-critical pieces of UI crash the whole app */} + + + + + + + + + + + + + + + + + + + + {/* + while users should be able to browse communities/threads + if they are signed out (eg signedOutFallback), they should not + be allowed to use the app after signing up if they dont set a username. + + otherwise we can get into a state where people are sending DMs, + sending messages, and posting threads without having a user profile + that people can report or link to. + + this global component simply listens for users without a username + to be authenticated, and if so forces a redirect to /new/user + prompting them to set a username + */} + + + + + {isModal && ( + id-123-id, easy start that works + // - /some-custom-slug~id-123-id => id-123-id, custom slug also works + // - /~id-123-id => id-123-id => id-123-id, empty custom slug also works + // - /some~custom~slug~id-123-id => id-123-id, custom slug with delimiter char in it (~) also works! :tada: + path="/:communitySlug/:channelSlug/(.*~)?:threadId" + component={props => ( + + )} + /> + )} + + {/* + this context provider allows children views to determine + how they should behave if a modal is open. For example, + you could tell a community view to not paginate the thread + feed if a thread modal is open. + */} + {/* - AuthViewHandler often returns null, but is responsible for triggering - things like the 'set username' prompt when a user auths and doesn't - have a username set. + we tell the app view wrapper any time the modal state + changes so that we can restore the scroll position to where + it was before the modal was opened */} - {() => null} - - - - + + + + +
+ {/* + switch only renders the first match. Subrouting happens downstream + https://reacttraining.com/react-router/web/api/Switch + */} + + + + + {/* Public Business Pages */} + + + + + + + + + + + + + {/* App Pages */} + + + + + - - - + } + /> - {/* - Switch only renders the first match. Subrouting happens downstream - https://reacttraining.com/react-router/web/api/Switch - */} - - - - - {/* Public Business Pages */} - - - - - - - - - - - - - {/* App Pages */} - - - - - } - /> - - - - - - - } /> - } /> - - - - - - currentUser && currentUser.username ? ( - - ) : currentUser && !currentUser.username ? ( - - ) : isLoadingCurrentUser ? null : ( - - ) - } - /> - - currentUser && currentUser.username ? ( - - ) : isLoadingCurrentUser ? null : ( - - ) - } - /> - - {/* - We check communitySlug last to ensure none of the above routes - pass. We handle null communitySlug values downstream by either - redirecting to home or showing a 404 - */} - - - - - - - id-123-id, easy start that works - // - /some-custom-slug~id-123-id => id-123-id, custom slug also works - // - /~id-123-id => id-123-id => id-123-id, empty custom slug also works - // - /some~custom~slug~id-123-id => id-123-id, custom slug with delimiter char in it (~) also works! :tada: - path="/:communitySlug/:channelSlug/(.*~)?:threadId" - component={FullscreenThreadView} - /> - - - - - {isModal && ( - id-123-id, easy start that works - // - /some-custom-slug~id-123-id => id-123-id, custom slug also works - // - /~id-123-id => id-123-id => id-123-id, empty custom slug also works - // - /some~custom~slug~id-123-id => id-123-id, custom slug with delimiter char in it (~) also works! :tada: - path="/:communitySlug/:channelSlug/(.*~)?:threadId" - component={props => ( - + + - )} - /> - )} - - {isModal && ( - - )} - - {isModal && ( - } - /> - )} - - - - + + + } /> + } + /> + + + + + + currentUser && currentUser.username ? ( + + ) : currentUser && !currentUser.username ? ( + + ) : isLoadingCurrentUser ? null : ( + + ) + } + /> + + currentUser && currentUser.username ? ( + + ) : isLoadingCurrentUser ? null : ( + + ) + } + /> + + {/* + We check communitySlug last to ensure none of the above routes + pass. We handle null communitySlug values downstream by either + redirecting to home or showing a 404 + */} + + + + + + + id-123-id, easy start that works + // - /some-custom-slug~id-123-id => id-123-id, custom slug also works + // - /~id-123-id => id-123-id => id-123-id, empty custom slug also works + // - /some~custom~slug~id-123-id => id-123-id, custom slug with delimiter char in it (~) also works! :tada: + path="/:communitySlug/:channelSlug/(.*~)?:threadId" + component={ThreadView} + /> + + + +
+ + {isModal && ( + + )} + + {isModal && ( + ( + + )} + /> + )} + + {isModal && ( + ( + + )} + /> + )} +
+
+
+
+
); } } diff --git a/src/store/index.js b/src/store/index.js index a6f9be7ca3..d034f5b0e4 100644 --- a/src/store/index.js +++ b/src/store/index.js @@ -2,7 +2,7 @@ /* eslint-disable */ import { createStore, compose, applyMiddleware } from 'redux'; import thunkMiddleware from 'redux-thunk'; -import crashReporter from '../helpers/sentry-redux-middleware'; +import crashReporter from 'src/helpers/sentry-redux-middleware'; import getReducers from '../reducers'; // this enables the chrome devtools for redux only in development diff --git a/src/views/authViewHandler/index.js b/src/views/authViewHandler/index.js index e27d32aa80..b20e4833ce 100644 --- a/src/views/authViewHandler/index.js +++ b/src/views/authViewHandler/index.js @@ -36,7 +36,7 @@ class AuthViewHandler extends React.Component { } catch (err) {} } - if (location.pathname === '/home') history.push('/'); + if (location.pathname === '/home') history.replace('/'); } } diff --git a/src/views/authViewHandler/noUsernameHandler.js b/src/views/authViewHandler/noUsernameHandler.js new file mode 100644 index 0000000000..1643842ef6 --- /dev/null +++ b/src/views/authViewHandler/noUsernameHandler.js @@ -0,0 +1,34 @@ +// @flow +import compose from 'recompose/compose'; +import { withRouter, type Location, type History } from 'react-router-dom'; +import { + getCurrentUser, + type GetUserType, +} from 'shared/graphql/queries/user/getUser'; + +type Props = { + data: { + user: ?GetUserType, + }, + location: Location, + history: History, +}; + +const NoUsernameHandler = (props: Props) => { + const { data, location, history } = props; + const { user } = data; + if (!user) return null; + if (user && user.username) return null; + const { pathname, search } = location; + if (pathname === '/new/user') return null; + history.replace({ + pathname: '/new/user', + state: { redirect: `${pathname}${search}` }, + }); + return null; +}; + +export default compose( + getCurrentUser, + withRouter +)(NoUsernameHandler); diff --git a/src/views/channel/components/MembersList.js b/src/views/channel/components/MembersList.js new file mode 100644 index 0000000000..a69b8d0693 --- /dev/null +++ b/src/views/channel/components/MembersList.js @@ -0,0 +1,110 @@ +// @flow +import * as React from 'react'; +import compose from 'recompose/compose'; +import { connect } from 'react-redux'; +import { deduplicateChildren } from 'src/components/infiniteScroll/deduplicateChildren'; +import { withRouter } from 'react-router'; +import getChannelMembersQuery, { + type GetChannelMemberConnectionType, +} from 'shared/graphql/queries/channel/getChannelMemberConnection'; +import { Card } from 'src/components/card'; +import { Loading } from 'src/components/loading'; +import NextPageButton from 'src/components/nextPageButton'; +import viewNetworkHandler from 'src/components/viewNetworkHandler'; +import ViewError from 'src/components/viewError'; +import { UserListItem } from 'src/components/entities'; +import type { Dispatch } from 'redux'; +import { withCurrentUser } from 'src/components/withCurrentUser'; + +type Props = { + data: { + channel: GetChannelMemberConnectionType, + fetchMore: Function, + networkStatus: number, + }, + dispatch: Dispatch, + isLoading: boolean, + isFetchingMore: boolean, + history: Object, + currentUser: ?Object, +}; + +class MembersList extends React.Component { + shouldComponentUpdate(nextProps) { + const curr = this.props; + // fetching more + if (curr.data.networkStatus === 7 && nextProps.data.networkStatus === 3) + return false; + return true; + } + + render() { + const { + data: { channel }, + isLoading, + currentUser, + isFetchingMore, + } = this.props; + + if (channel && channel.memberConnection) { + const { edges: members, pageInfo } = channel.memberConnection; + const nodes = members.map(member => member && member.node); + const uniqueNodes = deduplicateChildren(nodes, 'id'); + const { hasNextPage } = pageInfo; + + return ( + + {uniqueNodes.map(user => { + if (!user) return null; + + return ( + + ); + })} + {hasNextPage && ( + + Load more members + + )} + + ); + } + + if (isLoading) { + return ; + } + + return ( + + + + ); + } +} + +export default compose( + withRouter, + withCurrentUser, + getChannelMembersQuery, + viewNetworkHandler, + connect() +)(MembersList); diff --git a/src/views/channel/components/memberGrid.js b/src/views/channel/components/memberGrid.js deleted file mode 100644 index cdea4e82d5..0000000000 --- a/src/views/channel/components/memberGrid.js +++ /dev/null @@ -1,122 +0,0 @@ -// @flow -import * as React from 'react'; -import compose from 'recompose/compose'; -import { withRouter } from 'react-router'; -import { connect } from 'react-redux'; -import getChannelMembersQuery, { - type GetChannelMemberConnectionType, -} from 'shared/graphql/queries/channel/getChannelMemberConnection'; -import { FlexCol } from 'src/components/globals'; -import { Card } from 'src/components/card'; -import { LoadingList } from 'src/components/loading'; -import GranularUserProfile from 'src/components/granularUserProfile'; -import viewNetworkHandler from 'src/components/viewNetworkHandler'; -import ViewError from 'src/components/viewError'; -import { StyledButton } from '../../community/style'; -import { initNewThreadWithUser } from 'src/actions/directMessageThreads'; -import { MessageIconContainer, UserListItemContainer } from '../style'; -import Icon from 'src/components/icons'; -import { withCurrentUser } from 'src/components/withCurrentUser'; - -type Props = { - data: { - channel: GetChannelMemberConnectionType, - fetchMore: Function, - }, - isLoading: boolean, - isFetchingMore: boolean, - dispatch: Dispatch, - history: Object, - currentUser: ?Object, -}; - -class ChannelMemberGrid extends React.Component { - initMessage = user => { - this.props.dispatch(initNewThreadWithUser(user)); - return this.props.history.push('/messages/new'); - }; - - render() { - const { - data: { channel, fetchMore }, - data, - isLoading, - isFetchingMore, - currentUser, - } = this.props; - - if (data && data.channel) { - const members = - channel.memberConnection && - channel.memberConnection.edges.map(member => member && member.node); - - return ( - - {members && - members.map(user => { - if (!user) return null; - return ( - - - {currentUser && - user.id !== currentUser.id && ( - - this.initMessage(user)} - /> - - )} - - - ); - })} - - {channel.memberConnection && - channel.memberConnection.pageInfo.hasNextPage && ( - fetchMore()} - > - View more... - - )} - - ); - } - - if (isLoading) { - return ; - } - - return ( - - - - ); - } -} - -export default compose( - withRouter, - withCurrentUser, - getChannelMembersQuery, - viewNetworkHandler, - connect() -)(ChannelMemberGrid); diff --git a/src/views/channel/components/notificationsToggle.js b/src/views/channel/components/notificationsToggle.js index 9e2514efa0..e1463076d5 100644 --- a/src/views/channel/components/notificationsToggle.js +++ b/src/views/channel/components/notificationsToggle.js @@ -4,9 +4,9 @@ import compose from 'recompose/compose'; import { connect } from 'react-redux'; import toggleChannelNotificationsMutation from 'shared/graphql/mutations/channel/toggleChannelNotifications'; import type { ToggleChannelNotificationsType } from 'shared/graphql/mutations/channel/toggleChannelNotifications'; -import { Checkbox } from '../../../components/formElements'; -import { addToastWithTimeout } from '../../../actions/toasts'; -import { ListContainer } from '../../../components/listItems/style'; +import { Checkbox } from 'src/components/formElements'; +import { addToastWithTimeout } from 'src/actions/toasts'; +import { ListContainer } from 'src/components/listItems/style'; type Props = { value: boolean, @@ -37,7 +37,9 @@ class NotificationsTogglePure extends React.Component { } handleChange = () => { - const { channel: { id } } = this.props; + const { + channel: { id }, + } = this.props; const { isReceiving } = this.state; this.setState({ isReceiving: !isReceiving, @@ -66,7 +68,7 @@ class NotificationsTogglePure extends React.Component { const { channel } = this.props; return ( - + { data-cy="channel-search-input" /> - {searchString && - sendStringToServer && ( - - )} + {searchString && sendStringToServer && ( + + )} ); } diff --git a/src/views/channel/components/style.js b/src/views/channel/components/style.js index a340b6fd7f..88d18e595d 100644 --- a/src/views/channel/components/style.js +++ b/src/views/channel/components/style.js @@ -2,7 +2,7 @@ import theme from 'shared/theme'; // $FlowFixMe import styled from 'styled-components'; -import { Card } from '../../../components/card'; +import { Card } from 'src/components/card'; export const PendingUserNotificationContainer = styled(Card)` width: 100%; diff --git a/src/views/channel/index.js b/src/views/channel/index.js index 812ba95491..fe8565b430 100644 --- a/src/views/channel/index.js +++ b/src/views/channel/index.js @@ -2,62 +2,38 @@ import * as React from 'react'; import compose from 'recompose/compose'; import { connect } from 'react-redux'; +import querystring from 'query-string'; +import { withRouter, type History, type Location } from 'react-router-dom'; import generateMetaInfo from 'shared/generate-meta-info'; -import { CommunityAvatar } from 'src/components/avatar'; -import { addCommunityToOnboarding } from 'src/actions/newUserOnboarding'; -import ComposerPlaceholder from 'src/components/threadComposer/components/placeholder'; import Head from 'src/components/head'; -import AppViewWrapper from 'src/components/appViewWrapper'; import viewNetworkHandler from 'src/components/viewNetworkHandler'; -import ViewError from 'src/components/viewError'; -import { Link } from 'react-router-dom'; import ThreadFeed from 'src/components/threadFeed'; import PendingUsersNotification from './components/pendingUsersNotification'; import NotificationsToggle from './components/notificationsToggle'; import getChannelThreadConnection from 'shared/graphql/queries/channel/getChannelThreadConnection'; import { getChannelByMatch } from 'shared/graphql/queries/channel/getChannel'; import type { GetChannelType } from 'shared/graphql/queries/channel/getChannel'; -import Login from '../login'; -import { LoadingScreen } from 'src/components/loading'; -import { Upsell404Channel } from 'src/components/upsell'; -import RequestToJoinChannel from 'src/components/upsell/requestToJoinChannel'; -import Titlebar from '../titlebar'; -import Icon from 'src/components/icons'; import Search from './components/search'; -import ChannelMemberGrid from './components/memberGrid'; -import { CLIENT_URL } from 'src/api/constants'; -import CommunityLogin from 'src/views/communityLogin'; import { withCurrentUser } from 'src/components/withCurrentUser'; -import { - SegmentedControl, - DesktopSegment, - Segment, - MobileSegment, -} from 'src/components/segmentedControl'; -import { - Grid, - Meta, - Content, - Extras, - CommunityContext, - CommunityName, - ChannelName, - ChannelDescription, - MetadataContainer, -} from './style'; -import { ExtLink, OnlineIndicator } from 'src/components/profile/style'; -import { CoverPhoto } from 'src/components/profile/coverPhoto'; -import { - LoginButton, - ColumnHeading, - MidSegment, - SettingsButton, - LoginOutlineButton, -} from '../community/style'; -import ToggleChannelMembership from 'src/components/toggleChannelMembership'; +import { SegmentedControl, Segment } from 'src/components/segmentedControl'; +import { ErrorView, LoadingView } from 'src/views/viewHelpers'; +import { ChannelProfileCard } from 'src/components/entities'; +import { setTitlebarProps } from 'src/actions/titlebar'; +import { MobileChannelAction } from 'src/components/titlebar/actions'; +import { CommunityAvatar } from 'src/components/avatar'; import { track, events, transformations } from 'src/helpers/analytics'; import type { Dispatch } from 'redux'; import { ErrorBoundary } from 'src/components/error'; +import MembersList from './components/MembersList'; +import { + ViewGrid, + SecondaryPrimaryColumnGrid, + PrimaryColumn, + SecondaryColumn, +} from 'src/components/layout'; +import { SidebarSection } from 'src/views/community/style'; +import { FeedsContainer } from './style'; +import { InfoContainer } from '../community/style'; const ThreadFeedWithData = compose( connect(), @@ -78,16 +54,22 @@ type Props = { isLoading: boolean, hasError: boolean, dispatch: Dispatch, + history: History, + location: Location, }; -type State = { - selectedView: 'threads' | 'search' | 'members', -}; - -class ChannelView extends React.Component { - state = { - selectedView: 'threads', - }; +class ChannelView extends React.Component { + constructor(props) { + super(props); + const { location, history } = props; + const { search } = location; + const { tab } = querystring.parse(search); + if (!tab) + history.replace({ + ...location, + search: querystring.stringify({ tab: 'posts' }), + }); + } componentDidMount() { if (this.props.data && this.props.data.channel) { @@ -101,6 +83,25 @@ class ChannelView extends React.Component { } componentDidUpdate(prevProps) { + const { dispatch } = this.props; + + if (this.props.data.channel) { + const { channel } = this.props.data; + dispatch( + setTitlebarProps({ + title: `# ${this.props.data.channel.name}`, + titleIcon: ( + + ), + rightAction: , + }) + ); + } + if ( (!prevProps.data.channel && this.props.data.channel) || (prevProps.data.channel && @@ -115,251 +116,36 @@ class ChannelView extends React.Component { community: transformations.analyticsCommunity(channel.community), }); } - - // if the user is new and signed up through a community page, push - // the community data into the store to hydrate the new user experience - // with their first community they should join - if (this.props.currentUser) return; - this.props.dispatch( - addCommunityToOnboarding(this.props.data.channel.community) - ); } } - handleSegmentClick = label => { - if (this.state.selectedView === label) return; - - return this.setState({ - selectedView: label, + handleSegmentClick = (tab: string) => { + const { history, location } = this.props; + return history.replace({ + ...location, + search: querystring.stringify({ tab }), }); }; - renderActionButton = (channel: GetChannelType) => { - if (!channel) return null; - - const { - isOwner: isChannelOwner, - isMember: isChannelMember, - } = channel.channelPermissions; - const { communityPermissions } = channel.community; - const { - isOwner: isCommunityOwner, - isModerator: isCommunityModerator, - } = communityPermissions; - const isGlobalOwner = isChannelOwner || isCommunityOwner; - const isGlobalModerator = isCommunityModerator; - - const loginUrl = channel.community.brandedLogin.isEnabled - ? `/${channel.community.slug}/login?r=${CLIENT_URL}/${ - channel.community.slug - }/${channel.slug}` - : `/login?r=${CLIENT_URL}/${channel.community.slug}/${channel.slug}`; - - // logged in - if (!this.props.currentUser) { - // user isnt logged in, prompt a login-join - return ( - - - Join {channel.name} - - - ); - } - - // logged out - if (this.props.currentUser) { - // show settings button if owns channel or community - if (isGlobalOwner) { - return ( - - - Settings - - - ); - } - - if (isGlobalModerator) { - return ( - - { - if (isChannelMember) { - return ( - - Leave channel - - ); - } else { - return ( - - Join {channel.name} - - ); - } - }} - /> - - - - Settings - - - - ); - } - - // otherwise prompt a join - return ( - { - if (isChannelMember) { - return ( - - Leave channel - - ); - } else { - return ( - - Join {channel.name} - - ); - } - }} - /> - ); - } - }; - render() { const { - match, data: { channel }, currentUser, isLoading, - hasError, + location, } = this.props; - const { selectedView } = this.state; - const { communitySlug } = match.params; const isLoggedIn = currentUser; - + const { search } = location; + const { tab } = querystring.parse(search); + const selectedView = tab; if (channel && channel.id) { // at this point the view is no longer loading, has not encountered an error, and has returned a channel record - const { - isBlocked, - isPending, - isMember, - isOwner, - isModerator, - } = channel.channelPermissions; + const { isMember, isOwner, isModerator } = channel.channelPermissions; const { community } = channel; const userHasPermissions = isMember || isOwner || isModerator; - const isRestricted = channel.isPrivate && !userHasPermissions; - const hasCommunityPermissions = - !community.isPrivate || community.communityPermissions.isMember; const isGlobalOwner = isOwner || channel.community.communityPermissions.isOwner; - const redirectPath = `${CLIENT_URL}/${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 (community.brandedLogin.isEnabled) { - return ; - } else { - return ; - } - } - - // user has explicitly been blocked from this channel - if ( - isBlocked || - community.communityPermissions.isBlocked || - !hasCommunityPermissions - ) { - return ( - - - - - - - ); - } - - // channel is private and the user is not a member or owner - if (isRestricted) { - return ( - - - - - - - ); - } - // at this point the user has full permission to view the channel const { title, description } = generateMetaInfo({ type: 'channel', @@ -370,215 +156,144 @@ class ChannelView extends React.Component { }, }); - const actionButton = this.renderActionButton(channel); - return ( - + - - - - - - - - {community.name} - - - - - {channel.name} - {channel.isArchived && ' (Archived)'} - - {channel.description && ( - {channel.description} - )} - - - {channel.metaData && channel.metaData.members && ( - - - {channel.metaData.members.toLocaleString()} - {channel.metaData.members > 1 ? ' members' : ' member'} - - )} - {channel.metaData && - typeof channel.metaData.onlineMembers === 'number' && ( - - + + + + + + + {isLoggedIn && userHasPermissions && !channel.isArchived && ( + + + - {channel.metaData.onlineMembers.toLocaleString()} online - - )} - -
- - {actionButton} - + + + )} - {isLoggedIn && userHasPermissions && !channel.isArchived && ( - - - - )} + {/* user is signed in and has permissions to view pending users */} + {isLoggedIn && (isOwner || isGlobalOwner) && ( + + + + )} + + + + + + this.handleSegmentClick('posts')} + isActive={selectedView === 'posts'} + data-cy="channel-posts-tab" + > + Posts + - {/* user is signed in and has permissions to view pending users */} - {isLoggedIn && (isOwner || isGlobalOwner) && ( - - - - )} - - - - this.handleSegmentClick('search')} - selected={selectedView === 'search'} - data-cy="channel-search-tab" - > - - Search - - this.handleSegmentClick('threads')} - selected={selectedView === 'threads'} - > - Threads - - this.handleSegmentClick('members')} - selected={selectedView === 'members'} - > - Members ( - {channel.metaData && - channel.metaData.members && - channel.metaData.members.toLocaleString()} - ) - - this.handleSegmentClick('members')} - selected={selectedView === 'members'} - > - Members - - this.handleSegmentClick('search')} - selected={selectedView === 'search'} - > - - - + this.handleSegmentClick('members')} + isActive={selectedView === 'members'} + data-cy="channel-members-tab" + > + Members + + + this.handleSegmentClick('info')} + isActive={selectedView === 'info'} + data-cy="channel-info-tab" + hideOnDesktop + > + Info + - {/* if the user is logged in and has permissions to post, and the channel is either private + paid, or is not private, show the composer */} - {isLoggedIn && - !channel.isArchived && - selectedView === 'threads' && - userHasPermissions && - ((channel.isPrivate && !channel.isArchived) || - !channel.isPrivate) && ( - - this.handleSegmentClick('search')} + isActive={selectedView === 'search'} + data-cy="channel-search-tab" + > + Search + + + + {selectedView === 'posts' && ( + - - )} + )} - {// thread list - selectedView === 'threads' && ( - - )} + {selectedView === 'search' && ( + + + + )} - {//search - selectedView === 'search' && ( - - - - )} + {selectedView === 'members' && ( + + + + )} - {// members grid - selectedView === 'members' && ( - - - - )} - - - - Members - - - - - + {selectedView === 'info' && ( + + + + + + {isLoggedIn && userHasPermissions && !channel.isArchived && ( + + + + + + )} + + {/* user is signed in and has permissions to view pending users */} + {isLoggedIn && (isOwner || isGlobalOwner) && ( + + + + )} + + )} + + + + + ); } if (isLoading) { - return ; - } - - if (hasError) { - return ( - - - - - ); + return ; } - return ( - - - - - - - ); + return ; } } @@ -586,5 +301,6 @@ export default compose( withCurrentUser, getChannelByMatch, viewNetworkHandler, + withRouter, connect() )(ChannelView); diff --git a/src/views/channel/style.js b/src/views/channel/style.js index 8c9ad4fba6..f5ff2a1e8c 100644 --- a/src/views/channel/style.js +++ b/src/views/channel/style.js @@ -1,120 +1,9 @@ // @flow import theme from 'shared/theme'; import styled from 'styled-components'; -import Card from '../../components/card'; -import { Transition, zIndex, Truncate } from '../../components/globals'; -import { SegmentedControl } from '../../components/segmentedControl'; -import { FullProfile, FullDescription } from 'src/components/profile/style'; -import { ListContainer } from 'src/components/listItems/style'; - -export const Grid = styled.main` - display: grid; - grid-template-columns: minmax(320px, 1fr) 3fr minmax(240px, 2fr); - grid-template-rows: 160px 1fr; - grid-template-areas: 'cover cover cover' 'meta content extras'; - grid-column-gap: 32px; - width: 100%; - max-width: 1280px; - min-height: 100vh; - background-color: ${theme.bg.default}; - box-shadow: inset 1px 0 0 ${theme.bg.border}, - inset -1px 0 0 ${theme.bg.border}; - - @media (max-width: 1280px) { - grid-template-columns: 240px 1fr; - grid-template-rows: 80px 1fr; - grid-template-areas: 'cover cover' 'meta content'; - } - - @media (max-width: 768px) { - grid-template-rows: 80px auto 1fr; - grid-template-columns: 100%; - grid-column-gap: 0; - grid-row-gap: 16px; - grid-template-areas: 'cover' 'meta' 'content'; - } -`; - -const Column = styled.div` - display: flex; - flex-direction: column; -`; - -export const Meta = styled(Column)` - grid-area: meta; - - > ${FullProfile} { - margin-top: 16px; - margin-bottom: 16px; - } - - @media (max-width: 768px) { - > ${FullProfile} { - margin-top: 0; - margin-bottom: 8px; - } - - ${FullDescription} { - display: none; - } - } - - ${ListContainer} { - margin: 8px 0 0 32px; - width: auto; - - @media (max-width: 768px) { - margin-left: 0; - } - } - - > button, - > a > button { - margin-top: 16px; - margin-left: 32px; - width: calc(100% - 32px); - - @media (max-width: 768px) { - margin-left: 0; - width: 100%; - } - } - - @media (max-width: 768px) { - padding: 0 16px; - - > div { - margin-left: 0; - } - } -`; -export const Content = styled(Column)` - grid-area: content; - min-width: 0; - align-items: stretch; - - @media (max-width: 1280px) and (min-width: 768px) { - padding-right: 32px; - } - - @media (max-width: 768px) { - > ${SegmentedControl} > div { - margin-top: 0; - } - } -`; - -export const Extras = styled(Column)` - grid-area: extras; - - @media (max-width: 1280px) { - display: none; - } - - @media (min-width: 768px) { - padding-right: 32px; - } -`; +import Card from 'src/components/card'; +import { Transition, zIndex, Truncate } from 'src/components/globals'; +import { MEDIA_BREAK } from 'src/components/layout'; export const ColumnHeading = styled.div` display: flex; @@ -128,20 +17,18 @@ export const ColumnHeading = styled.div` `; export const SearchContainer = styled(Card)` - border-bottom: 2px solid ${theme.bg.border}; + border-bottom: 1px solid ${theme.bg.border}; + background: ${theme.bg.wash}; position: relative; z-index: ${zIndex.search}; width: 100%; - display: block; - min-height: 64px; + display: flex; + padding: 8px 12px; transition: ${Transition.hover.off}; + display: flex; + align-items: center; - &:hover { - transition: none; - border-bottom: 2px solid ${theme.brand.alt}; - } - - @media (max-width: 768px) { + @media (max-width: ${MEDIA_BREAK}px) { border-radius: 0; pointer-events: all; margin-bottom: 0; @@ -149,17 +36,22 @@ export const SearchContainer = styled(Card)` `; export const SearchInput = styled.input` - justify-content: flex-start; + display: flex; + flex: 1 0 auto; align-items: center; - cursor: pointer; - padding: 20px; + width: 100%; + padding: 12px 16px; color: ${theme.text.default}; transition: ${Transition.hover.off}; - font-size: 20px; - font-weight: 800; - margin-left: 8px; - width: 97%; - border-radius: 12px; + font-size: 16px; + font-weight: 500; + border-radius: 100px; + background: ${theme.bg.default}; + border: 1px solid ${theme.bg.border}; + + &:focus { + border: 1px solid ${theme.text.secondary}; + } `; export const MessageIconContainer = styled.div` @@ -185,7 +77,7 @@ export const CommunityContext = styled.div` display: flex; align-items: center; - @media (max-width: 768px) { + @media (max-width: ${MEDIA_BREAK}px) { margin-top: 16px; } `; @@ -207,7 +99,7 @@ export const ChannelName = styled.h3` margin-left: 32px; color: ${theme.text.default}; - @media (max-width: 768px) { + @media (max-width: ${MEDIA_BREAK}px) { margin-left: 0; } `; @@ -219,7 +111,7 @@ export const ChannelDescription = styled.h4` margin-bottom: 16px; color: ${theme.text.alt}; - @media (max-width: 768px) { + @media (max-width: ${MEDIA_BREAK}px) { margin-left: 0; } `; @@ -227,7 +119,13 @@ export const ChannelDescription = styled.h4` export const MetadataContainer = styled.div` margin-left: 32px; - @media (max-width: 768px;) { + @media (max-width: ${MEDIA_BREAK}px;) { margin-left: 8px; } `; + +export const FeedsContainer = styled.section` + display: flex; + flex-direction: column; + background: ${theme.bg.default}; +`; diff --git a/src/views/channelSettings/components/archiveForm.js b/src/views/channelSettings/components/archiveForm.js index f83670309f..2625e64b3e 100644 --- a/src/views/channelSettings/components/archiveForm.js +++ b/src/views/channelSettings/components/archiveForm.js @@ -2,15 +2,15 @@ import * as React from 'react'; import compose from 'recompose/compose'; import { connect } from 'react-redux'; -import { openModal } from '../../../actions/modals'; -import { Button } from '../../../components/buttons'; +import { openModal } from 'src/actions/modals'; +import { OutlineButton } from 'src/components/button'; import type { GetChannelType } from 'shared/graphql/queries/channel/getChannel'; import { SectionCard, SectionTitle, SectionSubtitle, SectionCardFooter, -} from '../../../components/settingsViews/style'; +} from 'src/components/settingsViews/style'; import { track, events, transformations } from 'src/helpers/analytics'; import type { Dispatch } from 'redux'; @@ -85,7 +85,9 @@ class Channel extends React.Component { )} - + + Archive Channel + ); @@ -100,7 +102,9 @@ class Channel extends React.Component { - + + Restore Channel + ); diff --git a/src/views/channelSettings/components/blockedUsers.js b/src/views/channelSettings/components/blockedUsers.js index b6a4ade05c..cc77ec716e 100644 --- a/src/views/channelSettings/components/blockedUsers.js +++ b/src/views/channelSettings/components/blockedUsers.js @@ -3,7 +3,7 @@ import * as React from 'react'; import compose from 'recompose/compose'; import { connect } from 'react-redux'; import { UserListItemContainer } from '../style'; -import GranularUserProfile from 'src/components/granularUserProfile'; +import { UserListItem } from 'src/components/entities'; import { Loading } from 'src/components/loading'; import getBlockedUsersQuery from 'shared/graphql/queries/channel/getChannelBlockedUsers'; import type { GetChannelBlockedUsersType } from 'shared/graphql/queries/channel/getChannelBlockedUsers'; @@ -12,6 +12,7 @@ import { SectionTitle, SectionSubtitle, } from 'src/components/settingsViews/style'; +import InitDirectMessageWrapper from 'src/components/initDirectMessageWrapper'; import viewNetworkHandler from 'src/components/viewNetworkHandler'; import ViewError from 'src/components/viewError'; import { ListContainer, Notice } from 'src/components/listItems/style'; @@ -26,7 +27,7 @@ import { DropdownSectionTitle, DropdownAction, } from 'src/components/settingsViews/style'; -import Icon from 'src/components/icons'; +import Icon from 'src/components/icon'; type Props = { data: { @@ -34,13 +35,12 @@ type Props = { }, unblock: Function, isLoading: boolean, - initMessage: Function, currentUser: ?Object, }; class BlockedUsers extends React.Component { render() { - const { data, isLoading, currentUser, unblock, initMessage } = this.props; + const { data, isLoading, currentUser, unblock } = this.props; if (data && data.channel) { const { blockedUsers } = data.channel; @@ -48,24 +48,22 @@ class BlockedUsers extends React.Component { return ( Blocked Users - {blockedUsers && - blockedUsers.length > 0 && ( - - Blocked users can not see threads or messages posted in this - channel. They will still be able to join any other public - channels in the Spectrum community and request access to other - private channels. - - )} + {blockedUsers && blockedUsers.length > 0 && ( + + Blocked users can not see threads or messages posted in this + channel. They will still be able to join any other public channels + in the Spectrum community and request access to other private + channels. + + )} - {blockedUsers && - blockedUsers.length > 0 && ( - - Unblocking a user will not add them to this channel. It - will only allow them to re-request access in the future as long - as this channel remains private. - - )} + {blockedUsers && blockedUsers.length > 0 && ( + + Unblocking a user will not add them to this channel. It + will only allow them to re-request access in the future as long as + this channel remains private. + + )} {blockedUsers && @@ -74,7 +72,7 @@ class BlockedUsers extends React.Component { return ( - { ( - initMessage(user)} - > - - - - - - Send Direct Message - - - + + + + + + + Send Direct Message + + + + } + /> @@ -124,17 +127,16 @@ class BlockedUsers extends React.Component { )} /> - + ); })} - {blockedUsers && - blockedUsers.length <= 0 && ( - - There are no blocked users in this channel. - - )} + {blockedUsers && blockedUsers.length <= 0 && ( + + There are no blocked users in this channel. + + )} ); diff --git a/src/views/channelSettings/components/channelMembers.js b/src/views/channelSettings/components/channelMembers.js index 365192470f..e72a075464 100644 --- a/src/views/channelSettings/components/channelMembers.js +++ b/src/views/channelSettings/components/channelMembers.js @@ -8,11 +8,10 @@ import type { GetChannelMemberConnectionType } from 'shared/graphql/queries/chan import { FetchMoreButton } from 'src/components/threadFeed/style'; import ViewError from 'src/components/viewError'; import viewNetworkHandler from 'src/components/viewNetworkHandler'; -import GranularUserProfile from 'src/components/granularUserProfile'; +import { UserListItem } from 'src/components/entities'; import { SectionCard, SectionTitle } from 'src/components/settingsViews/style'; -import { MessageIconContainer, UserListItemContainer } from '../style'; +import { UserListItemContainer } from '../style'; import { ListContainer, ListFooter } from 'src/components/listItems/style'; -import Icon from 'src/components/icons'; import type { Dispatch } from 'redux'; import { withCurrentUser } from 'src/components/withCurrentUser'; @@ -24,7 +23,6 @@ type Props = { isLoading: boolean, isFetchingMore: boolean, dispatch: Dispatch, - initMessage: Function, currentUser: ?Object, }; @@ -36,23 +34,16 @@ class ChannelMembers extends Component { isLoading, isFetchingMore, currentUser, - initMessage, } = this.props; if (data && data.channel) { const members = channel.memberConnection && channel.memberConnection.edges.map(member => member && member.node); - const totalCount = - channel.metaData && channel.metaData.members.toLocaleString(); return ( - - {totalCount === 1 - ? `${totalCount} member` - : `${totalCount} members`} - + Members {members && @@ -60,7 +51,7 @@ class ChannelMembers extends Component { if (!user) return null; return ( - { avatarSize={40} description={user.description} showHoverProfile={false} - > - {currentUser && user.id !== currentUser.id && ( - - initMessage(user)} - /> - - )} - + messageButton={true} + /> ); })} diff --git a/src/views/channelSettings/components/editDropdown.js b/src/views/channelSettings/components/editDropdown.js index 425c7500de..dfd4ab089b 100644 --- a/src/views/channelSettings/components/editDropdown.js +++ b/src/views/channelSettings/components/editDropdown.js @@ -3,7 +3,7 @@ import * as React from 'react'; import compose from 'recompose/compose'; import { connect } from 'react-redux'; import { EditDropdownContainer } from 'src/components/settingsViews/style'; -import Icon from 'src/components/icons'; +import Icon from 'src/components/icon'; import OutsideClickHandler from 'src/components/outsideClickHandler'; type Props = { diff --git a/src/views/channelSettings/components/editForm.js b/src/views/channelSettings/components/editForm.js index 4cd735d057..0f315ab24f 100644 --- a/src/views/channelSettings/components/editForm.js +++ b/src/views/channelSettings/components/editForm.js @@ -8,20 +8,15 @@ import editChannelMutation from 'shared/graphql/mutations/channel/editChannel'; import type { EditChannelType } from 'shared/graphql/mutations/channel/editChannel'; import type { GetChannelType } from 'shared/graphql/queries/channel/getChannel'; import deleteChannelMutation from 'shared/graphql/mutations/channel/deleteChannel'; -import { openModal } from '../../../actions/modals'; -import { addToastWithTimeout } from '../../../actions/toasts'; -import { Notice } from '../../../components/listItems/style'; -import { Button, IconButton } from '../../../components/buttons'; -import { NullCard } from '../../../components/upsell'; -import { - Input, - UnderlineInput, - TextArea, -} from '../../../components/formElements'; -import { - SectionCard, - SectionTitle, -} from '../../../components/settingsViews/style'; +import { openModal } from 'src/actions/modals'; +import Tooltip from 'src/components/tooltip'; +import { addToastWithTimeout } from 'src/actions/toasts'; +import { Notice } from 'src/components/listItems/style'; +import { PrimaryOutlineButton } from 'src/components/button'; +import Icon from 'src/components/icon'; +import { NullCard } from 'src/components/upsell'; +import { Input, UnderlineInput, TextArea } from 'src/components/formElements'; +import { SectionCard, SectionTitle } from 'src/components/settingsViews/style'; import { Form, TertiaryActionContainer, @@ -29,7 +24,7 @@ import { Actions, GeneralNotice, Location, -} from '../../../components/editForm/style'; +} from 'src/components/editForm/style'; import { track, events, transformations } from 'src/helpers/analytics'; import type { Dispatch } from 'redux'; @@ -141,12 +136,7 @@ class ChannelWithData extends React.Component { ?

- {channelData.metaData.threads > 0 && ( -

- The {channelData.metaData.threads} threads posted in this - channel will be deleted. -

- )} +

All conversations posted in this channel will be deleted.

All messages, reactions, and media shared in this channel will be deleted. @@ -184,7 +174,7 @@ class ChannelWithData extends React.Component { copy={'Want to make it?'} > {/* TODO: wire up button */} - + Create ); } else { @@ -251,24 +241,26 @@ class ChannelWithData extends React.Component { )} - + {slug !== 'general' && ( - this.triggerDeleteChannel(e, channel.id)} - dataCy="delete-channel-button" - /> + + + this.triggerDeleteChannel(e, channel.id)} + dataCy="delete-channel-button" + /> + + )} @@ -276,7 +268,7 @@ class ChannelWithData extends React.Component { {slug === 'general' && ( The General channel is the default channel for your community. - It can't be deleted or private, but you can still change the + It can’t be deleted or private, but you can still change the name and description. )} diff --git a/src/views/channelSettings/components/joinTokenToggle.js b/src/views/channelSettings/components/joinTokenToggle.js index aaa231dfe5..15b22c6bae 100644 --- a/src/views/channelSettings/components/joinTokenToggle.js +++ b/src/views/channelSettings/components/joinTokenToggle.js @@ -5,7 +5,7 @@ import { connect } from 'react-redux'; import compose from 'recompose/compose'; import enableTokenJoinMutation from 'shared/graphql/mutations/channel/enableChannelTokenJoin'; import disableTokenJoinMutation from 'shared/graphql/mutations/channel/disableChannelTokenJoin'; -import { addToastWithTimeout } from '../../../actions/toasts'; +import { addToastWithTimeout } from 'src/actions/toasts'; import type { Dispatch } from 'redux'; type Props = { diff --git a/src/views/channelSettings/components/overview.js b/src/views/channelSettings/components/overview.js index dd772b49bf..1326abe402 100644 --- a/src/views/channelSettings/components/overview.js +++ b/src/views/channelSettings/components/overview.js @@ -1,16 +1,13 @@ // @flow import * as React from 'react'; -import { - SectionsContainer, - Column, -} from '../../../components/settingsViews/style'; +import { SectionsContainer, Column } from 'src/components/settingsViews/style'; import EditForm from './editForm'; import PendingUsers from './pendingUsers'; import BlockedUsers from './blockedUsers'; import ChannelMembers from './channelMembers'; import ArchiveForm from './archiveForm'; import LoginTokenSettings from './joinTokenSettings'; -import SlackConnection from '../../communitySettings/components/slack'; +import SlackConnection from 'src/views/communitySettings/components/slack'; import { ErrorBoundary, SettingsFallback } from 'src/components/error'; type Props = { @@ -19,11 +16,10 @@ type Props = { communitySlug: string, togglePending: Function, unblock: Function, - initMessage: Function, }; class Overview extends React.Component { render() { - const { channel, initMessage, community } = this.props; + const { channel, community } = this.props; return ( @@ -55,11 +51,7 @@ class Overview extends React.Component { {channel.isPrivate && ( - + @@ -67,7 +59,6 @@ class Overview extends React.Component { togglePending={this.props.togglePending} channel={channel} id={channel.id} - initMessage={initMessage} /> @@ -76,7 +67,6 @@ class Overview extends React.Component { unblock={this.props.unblock} channel={channel} id={channel.id} - initMessage={initMessage} /> @@ -84,11 +74,7 @@ class Overview extends React.Component { {!channel.isPrivate && ( - + )} diff --git a/src/views/channelSettings/components/pendingUsers.js b/src/views/channelSettings/components/pendingUsers.js index eba96873d9..f4c28934f9 100644 --- a/src/views/channelSettings/components/pendingUsers.js +++ b/src/views/channelSettings/components/pendingUsers.js @@ -3,13 +3,14 @@ import * as React from 'react'; import compose from 'recompose/compose'; import { connect } from 'react-redux'; import { UserListItemContainer } from '../style'; -import GranularUserProfile from 'src/components/granularUserProfile'; +import { UserListItem } from 'src/components/entities'; import { Loading } from 'src/components/loading'; import viewNetworkHandler from 'src/components/viewNetworkHandler'; import getPendingUsersQuery from 'shared/graphql/queries/channel/getChannelPendingUsers'; import type { GetChannelPendingUsersType } from 'shared/graphql/queries/channel/getChannelPendingUsers'; import ViewError from 'src/components/viewError'; import { ListContainer } from 'src/components/listItems/style'; +import InitDirectMessageWrapper from 'src/components/initDirectMessageWrapper'; import { SectionCard, SectionTitle, @@ -25,7 +26,7 @@ import { DropdownSectionTitle, DropdownAction, } from 'src/components/settingsViews/style'; -import Icon from 'src/components/icons'; +import Icon from 'src/components/icon'; import { withCurrentUser } from 'src/components/withCurrentUser'; type Props = { @@ -34,19 +35,12 @@ type Props = { }, togglePending: Function, isLoading: boolean, - initMessage: Function, currentUser: ?Object, }; class PendingUsers extends React.Component { render() { - const { - data, - isLoading, - currentUser, - togglePending, - initMessage, - } = this.props; + const { data, isLoading, currentUser, togglePending } = this.props; if (data && data.channel) { const { pendingUsers } = data.channel; @@ -54,14 +48,13 @@ class PendingUsers extends React.Component { return ( Pending Members - {pendingUsers && - pendingUsers.length > 0 && ( - - Approving requests will allow a person to view all threads and - messages in this channel, as well as allow them to post their - own threads. - - )} + {pendingUsers && pendingUsers.length > 0 && ( + + Approving requests will allow a person to view all threads and + messages in this channel, as well as allow them to post their own + threads. + + )} {pendingUsers && @@ -69,7 +62,7 @@ class PendingUsers extends React.Component { if (!user) return null; return ( - { ( - initMessage(user)} - > - - - - - - Send Direct Message - - - + + + + + + + Send Direct Message + + + + } + /> @@ -139,17 +137,16 @@ class PendingUsers extends React.Component { )} /> - + ); })} - {pendingUsers && - pendingUsers.length <= 0 && ( - - There are no pending requests to join this channel. - - )} + {pendingUsers && pendingUsers.length <= 0 && ( + + There are no pending requests to join this channel. + + )} ); diff --git a/src/views/channelSettings/components/resetJoinToken.js b/src/views/channelSettings/components/resetJoinToken.js index af34434753..6b58763fb7 100644 --- a/src/views/channelSettings/components/resetJoinToken.js +++ b/src/views/channelSettings/components/resetJoinToken.js @@ -4,7 +4,7 @@ import { connect } from 'react-redux'; import compose from 'recompose/compose'; import resetJoinTokenMutation from 'shared/graphql/mutations/channel/resetChannelJoinToken'; import { addToastWithTimeout } from 'src/actions/toasts'; -import { OutlineButton } from 'src/components/buttons'; +import { OutlineButton } from 'src/components/button'; import type { Dispatch } from 'redux'; type Props = { @@ -57,7 +57,7 @@ class ResetJoinToken extends React.Component { Reset this link @@ -66,4 +66,7 @@ class ResetJoinToken extends React.Component { } } -export default compose(connect(), resetJoinTokenMutation)(ResetJoinToken); +export default compose( + connect(), + resetJoinTokenMutation +)(ResetJoinToken); diff --git a/src/views/channelSettings/index.js b/src/views/channelSettings/index.js index 2bbdc2746f..aef9c54af0 100644 --- a/src/views/channelSettings/index.js +++ b/src/views/channelSettings/index.js @@ -5,8 +5,6 @@ import { withRouter } from 'react-router'; import { connect } from 'react-redux'; import { getChannelByMatch } from 'shared/graphql/queries/channel/getChannel'; import type { GetChannelType } from 'shared/graphql/queries/channel/getChannel'; -import AppViewWrapper from 'src/components/appViewWrapper'; -import { Loading } from 'src/components/loading'; import { addToastWithTimeout } from 'src/actions/toasts'; import { Upsell404Channel } from 'src/components/upsell'; import viewNetworkHandler from 'src/components/viewNetworkHandler'; @@ -14,15 +12,15 @@ import togglePendingUserInChannelMutation from 'shared/graphql/mutations/channel import type { ToggleChannelPendingUserType } from 'shared/graphql/mutations/channel/toggleChannelPendingUser'; import unblockUserInChannelMutation from 'shared/graphql/mutations/channel/unblockChannelBlockedUser'; import type { UnblockChannelBlockedUserType } from 'shared/graphql/mutations/channel/unblockChannelBlockedUser'; -import Titlebar from '../titlebar'; import ViewError from 'src/components/viewError'; import { View } from 'src/components/settingsViews/style'; import Header from 'src/components/settingsViews/header'; import Overview from './components/overview'; -import Subnav from 'src/components/settingsViews/subnav'; -import { initNewThreadWithUser } from 'src/actions/directMessageThreads'; import { track, events, transformations } from 'src/helpers/analytics'; import type { Dispatch } from 'redux'; +import { ErrorView, LoadingView } from 'src/views/viewHelpers'; +import { ViewGrid } from 'src/components/layout'; +import { setTitlebarProps } from 'src/actions/titlebar'; type Props = { data: { @@ -40,8 +38,16 @@ type Props = { class ChannelSettings extends React.Component { componentDidMount() { - if (this.props.data && this.props.data.channel) { - const { channel } = this.props.data; + const { dispatch, data } = this.props; + + dispatch( + setTitlebarProps({ + title: 'Settings', + }) + ); + + if (data && data.channel) { + const { channel } = data; track(events.CHANNEL_SETTINGS_VIEWED, { channel: transformations.analyticsChannel(channel), @@ -61,13 +67,11 @@ class ChannelSettings extends React.Component { } } - initMessage = user => { - this.props.dispatch(initNewThreadWithUser(user)); - return this.props.history.push('/messages/new'); - }; - togglePending = (userId, action) => { - const { data: { channel }, dispatch } = this.props; + const { + data: { channel }, + dispatch, + } = this.props; const input = { channelId: channel.id, userId, @@ -96,7 +100,10 @@ class ChannelSettings extends React.Component { }; unblock = (userId: string) => { - const { data: { channel }, dispatch } = this.props; + const { + data: { channel }, + dispatch, + } = this.props; const input = { channelId: channel.id, @@ -124,9 +131,8 @@ class ChannelSettings extends React.Component { match, location, isLoading, - hasError, } = this.props; - const { communitySlug, channelSlug } = match.params; + const { communitySlug } = match.params; // this is hacky, but will tell us if we're viewing analytics or the root settings view const pathname = location.pathname; @@ -143,13 +149,7 @@ class ChannelSettings extends React.Component { if (!userHasPermissions) { return ( - - + { > - + ); } @@ -172,7 +172,6 @@ class ChannelSettings extends React.Component { communitySlug={communitySlug} togglePending={this.togglePending} unblock={this.unblock} - initMessage={this.initMessage} /> ); default: @@ -180,81 +179,31 @@ class ChannelSettings extends React.Component { } }; - const subnavItems = [ - { - to: `/${channel.community.slug}/${channel.slug}/settings`, - label: 'Overview', - activeLabel: 'settings', - }, - ]; - const subheading = { to: `/${channel.community.slug}/settings`, label: `Return to ${channel.community.name} settings`, }; return ( - - - - + +

- - - + ); } if (isLoading) { - return ; + return ; } - if (hasError) { - return ( - - - - - ); - } - - return ( - - - - - - - ); + return ; } } diff --git a/src/views/community/components/channelList.js b/src/views/community/components/channelList.js deleted file mode 100644 index 9b49c47796..0000000000 --- a/src/views/community/components/channelList.js +++ /dev/null @@ -1,217 +0,0 @@ -// @flow -import * as React from 'react'; -import { Link } from 'react-router-dom'; -import { connect } from 'react-redux'; -import compose from 'recompose/compose'; -import { OutlineButton } from 'src/components/buttons'; -import Icon from 'src/components/icons'; -import { openModal } from 'src/actions/modals'; -import viewNetworkHandler from 'src/components/viewNetworkHandler'; -import { Loading } from 'src/components/loading'; -import getCommunityChannels from 'shared/graphql/queries/community/getCommunityChannelConnection'; -import type { GetCommunityChannelConnectionType } from 'shared/graphql/queries/community/getCommunityChannelConnection'; -import { StyledCard, ListContainer } from 'src/components/listItems/style'; -import { ChannelListItem } from 'src/components/listItems'; -import ToggleChannelNotifications from 'src/components/toggleChannelNotifications'; -import type { Dispatch } from 'redux'; -import { ToggleNotificationsContainer } from '../style'; -import { withCurrentUser } from 'src/components/withCurrentUser'; - -type Props = { - data: { - community: GetCommunityChannelConnectionType, - }, - isLoading: boolean, - dispatch: Dispatch, - currentUser: Object, - communitySlug: string, -}; - -class ChannelList extends React.Component { - sortChannels = (array: Array): Array => { - if (!array || array.length === 0) return []; - - const generalChannel = array.find(channel => channel.slug === 'general'); - const withoutGeneral = array.filter(channel => channel.slug !== 'general'); - const sortedWithoutGeneral = withoutGeneral.sort((a, b) => { - if (a.slug < b.slug) return -1; - if (a.slug > b.slug) return 1; - return 0; - }); - if (generalChannel) { - sortedWithoutGeneral.unshift(generalChannel); - return sortedWithoutGeneral; - } else { - return sortedWithoutGeneral; - } - }; - render() { - const { - isLoading, - dispatch, - currentUser, - communitySlug, - data: { community }, - } = this.props; - - if (community && community.channelConnection) { - const { isMember, isOwner } = community.communityPermissions; - const channels = community.channelConnection.edges - .map(channel => channel && channel.node) - .filter(channel => { - if (!channel) return null; - if (channel.isArchived) return null; - if (channel.isPrivate && !channel.channelPermissions.isMember) - return null; - - return channel; - }) - .filter(channel => channel && !channel.channelPermissions.isBlocked); - - const sortedChannels = this.sortChannels(channels); - - return ( - - {/* - user isn't logged in, channel list is used for navigation - or - user is logged in but hasn't joined this community, channel list is used for navigation - */} - {(!currentUser || (currentUser && !isMember)) && ( - - {sortedChannels.map(channel => { - if (!channel) return null; - return ( - - - - - - ); - })} - - )} - - {isMember && !isOwner && ( - - {sortedChannels.map(channel => { - if (!channel) return null; - return ( - - ( - - {state.isLoading ? ( - - ) : ( - - )} - - )} - /> - - ); - })} - - )} - - {isOwner && ( - - {sortedChannels.map(channel => { - if (!channel) return null; - return ( - - ( - - {state.isLoading ? ( - - ) : ( - - )} - - )} - /> - -
- - - -
-
- ); - })} -
- )} - - {isOwner && ( - - dispatch(openModal('CREATE_CHANNEL_MODAL', { community })) - } - > - Create a channel - - )} -
- ); - } - - if (isLoading) { - return ( -
- -
- ); - } - - return null; - } -} - -export default compose( - getCommunityChannels, - viewNetworkHandler, - withCurrentUser, - connect() -)(ChannelList); diff --git a/src/views/community/components/channelsList.js b/src/views/community/components/channelsList.js new file mode 100644 index 0000000000..8851e3cf36 --- /dev/null +++ b/src/views/community/components/channelsList.js @@ -0,0 +1,117 @@ +// @flow +import * as React from 'react'; +import { connect } from 'react-redux'; +import compose from 'recompose/compose'; +import type { Dispatch } from 'redux'; +import { ErrorBoundary } from 'src/components/error'; +import Icon from 'src/components/icon'; +import { Loading } from 'src/components/loading'; +import viewNetworkHandler from 'src/components/viewNetworkHandler'; +import getCommunityChannels from 'shared/graphql/queries/community/getCommunityChannelConnection'; +import type { GetCommunityChannelConnectionType } from 'shared/graphql/queries/community/getCommunityChannelConnection'; +import { withCurrentUser } from 'src/components/withCurrentUser'; +import Tooltip from 'src/components/tooltip'; +import { ChannelListItem } from 'src/components/entities'; +import { WhiteIconButton } from 'src/components/button'; +import { SidebarSectionHeader, SidebarSectionHeading, List } from '../style'; + +type Props = { + data: { + community: GetCommunityChannelConnectionType, + }, + isLoading: boolean, + dispatch: Dispatch, + currentUser: Object, + communitySlug: string, +}; + +class Component extends React.Component { + sortChannels = (array: Array): Array => { + if (!array || array.length === 0) return []; + + const generalChannel = array.find(channel => channel.slug === 'general'); + const withoutGeneral = array.filter(channel => channel.slug !== 'general'); + const sortedWithoutGeneral = withoutGeneral.sort((a, b) => { + if (a.slug < b.slug) return -1; + if (a.slug > b.slug) return 1; + return 0; + }); + if (generalChannel) { + sortedWithoutGeneral.unshift(generalChannel); + return sortedWithoutGeneral; + } else { + return sortedWithoutGeneral; + } + }; + render() { + const { + isLoading, + data: { community }, + } = this.props; + + if (isLoading) { + return ( + + + Channels + + + + ); + } + + if (community && community.channelConnection) { + const { isOwner } = community.communityPermissions; + const channels = community.channelConnection.edges + .map(channel => channel && channel.node) + .filter(channel => { + if (!channel) return null; + if (channel.isArchived) return null; + if (channel.isPrivate && !channel.channelPermissions.isMember) + return null; + + return channel; + }) + .filter(channel => channel && !channel.channelPermissions.isBlocked); + + const sortedChannels = this.sortChannels(channels); + + return ( + + + Channels + {isOwner && ( + + + + + + + + )} + + + + {sortedChannels.map(channel => { + if (!channel) return null; + return ( + + + + ); + })} + + + ); + } + + return null; + } +} + +export const ChannelsList = compose( + getCommunityChannels, + viewNetworkHandler, + withCurrentUser, + connect() +)(Component); diff --git a/src/views/community/components/communityFeeds.js b/src/views/community/components/communityFeeds.js new file mode 100644 index 0000000000..68816813ff --- /dev/null +++ b/src/views/community/components/communityFeeds.js @@ -0,0 +1,207 @@ +// @flow +import React, { useEffect, useLayoutEffect } from 'react'; +import compose from 'recompose/compose'; +import { withRouter, type History, type Location } from 'react-router-dom'; +import querystring from 'query-string'; +import type { UserInfoType } from 'shared/graphql/fragments/user/userInfo'; +import type { CommunityInfoType } from 'shared/graphql/fragments/community/communityInfo'; +import MembersList from './membersList'; +import { TeamMembersList } from './teamMembersList'; +import { MobileCommunityInfoActions } from './mobileCommunityInfoActions'; +import { ChannelsList } from './channelsList'; +import { CommunityMeta } from 'src/components/entities/profileCards/components/communityMeta'; +import MessagesSubscriber from 'src/views/thread/components/messagesSubscriber'; +import { PostsFeeds } from './postsFeeds'; +import { SegmentedControl, Segment } from 'src/components/segmentedControl'; +import { useAppScroller } from 'src/hooks/useAppScroller'; +import ChatInput from 'src/components/chatInput'; +import { ChatInputWrapper } from 'src/components/layout'; +import { PrimaryOutlineButton } from 'src/components/button'; +import usePrevious from 'src/hooks/usePrevious'; +import { withCurrentUser } from 'src/components/withCurrentUser'; +import JoinCommunity from 'src/components/joinCommunityWrapper'; +import LockedMessages from 'src/views/thread/components/lockedMessages'; +import { FeedsContainer, SidebarSection, InfoContainer } from '../style'; + +type Props = { + community: CommunityInfoType, + location: Location, + history: History, + currentUser: UserInfoType, +}; + +const Feeds = (props: Props) => { + const { community, location, history, currentUser } = props; + const { search } = location; + const { tab } = querystring.parse(search); + + const changeTab = (tab: string) => { + return history.replace({ + ...location, + search: querystring.stringify({ tab }), + }); + }; + + const handleTabRedirect = () => { + const { search } = location; + const { tab } = querystring.parse(search); + + if (!tab) { + const defaultTab = community.watercoolerId ? 'chat' : 'posts'; + changeTab(defaultTab); + } + + if (tab === 'chat' && !community.watercoolerId) { + changeTab('posts'); + } + }; + + useEffect(() => { + handleTabRedirect(); + }, [tab]); + + const renderFeed = () => { + switch (tab) { + case 'chat': { + if (!community.watercoolerId) return null; + return ( + + + + {currentUser && community.communityPermissions.isMember && ( + + )} + {(!currentUser || !community.communityPermissions.isMember) && ( + ( + + + {isLoading ? 'Joining...' : 'Join community to chat'} + + + )} + /> + )} + + + ); + } + case 'posts': { + return ; + } + case 'members': { + return ( + + ); + } + case 'info': { + return ( + + + + + + + + + + + + + + + + + + ); + } + default: + return null; + } + }; + + /* + Segments preserve scroll position when switched by default. We dont want + this behavior - if you change the feed (eg threads => members) you should + always end up at the top of the list. However, if the next active segment + is chat, we want that scrolled to the bottom by default, since the behavior + of chat is to scroll up for older messages + */ + const { scrollToBottom, scrollToTop, scrollTo, ref } = useAppScroller(); + const lastTab = usePrevious(tab); + const lastScroll = ref ? ref.scrollTop : null; + useLayoutEffect(() => { + if (lastTab && lastTab !== tab && lastScroll) { + sessionStorage.setItem(`last-scroll-${lastTab}`, lastScroll.toString()); + } + const stored = + sessionStorage && sessionStorage.getItem(`last-scroll-${tab}`); + if (tab === 'chat') { + scrollToBottom(); + // If the user goes back, restore the scroll position + } else if (stored && history.action === 'POP') { + scrollTo(Number(stored)); + } else { + scrollToTop(); + } + }, [tab]); + + // Store the last scroll position on unmount + useLayoutEffect(() => { + return () => { + const elem = document.getElementById('main'); + if (!elem) return; + sessionStorage.setItem(`last-scroll-${tab}`, elem.scrollTop.toString()); + }; + }, []); + + const segments = ['posts', 'members', 'info']; + if (community.watercoolerId) segments.unshift('chat'); + + // if the community being viewed changes, and the previous community had + // a watercooler but the next one doesn't, select the posts tab on the new one + useEffect(() => { + handleTabRedirect(); + }, [community.slug]); + + return ( + + + {segments.map(segment => { + return ( + changeTab(segment)} + > + {segment[0].toUpperCase() + segment.substr(1)} + + ); + })} + + {renderFeed()} + + ); +}; + +export const CommunityFeeds = compose( + withRouter, + withCurrentUser +)(Feeds); diff --git a/src/views/community/components/memberGrid.js b/src/views/community/components/memberGrid.js deleted file mode 100644 index 6ae41f2e32..0000000000 --- a/src/views/community/components/memberGrid.js +++ /dev/null @@ -1,147 +0,0 @@ -// @flow -import * as React from 'react'; -import compose from 'recompose/compose'; -import { connect } from 'react-redux'; -import InfiniteList from 'src/components/infiniteScroll'; -import { deduplicateChildren } from 'src/components/infiniteScroll/deduplicateChildren'; -import Icon from 'src/components/icons'; -import { initNewThreadWithUser } from 'src/actions/directMessageThreads'; -import { withRouter } from 'react-router'; -import getCommunityMembersQuery, { - type GetCommunityMembersType, -} from 'shared/graphql/queries/community/getCommunityMembers'; -import { Card } from 'src/components/card'; -import { Loading, LoadingListItem } from 'src/components/loading'; -import viewNetworkHandler from 'src/components/viewNetworkHandler'; -import ViewError from 'src/components/viewError'; -import { MessageIconContainer, UserListItemContainer } from '../style'; -import GranularUserProfile from 'src/components/granularUserProfile'; -import type { Dispatch } from 'redux'; -import { withCurrentUser } from 'src/components/withCurrentUser'; - -type Props = { - data: { - community: GetCommunityMembersType, - fetchMore: Function, - }, - dispatch: Dispatch, - isLoading: boolean, - isFetchingMore: boolean, - history: Object, - currentUser: ?Object, -}; - -type State = { - scrollElement: any, -}; - -class CommunityMemberGrid extends React.Component { - state = { scrollElement: null }; - - componentDidMount() { - this.setState({ - // NOTE(@mxstbr): This is super un-reacty but it works. This refers to - // the AppViewWrapper which is the scrolling part of the site. - scrollElement: document.getElementById('scroller-for-thread-feed'), - }); - } - - initMessage = user => { - this.props.dispatch(initNewThreadWithUser(user)); - return this.props.history.push('/messages/new'); - }; - - shouldComponentUpdate(nextProps) { - const curr = this.props; - // fetching more - if (curr.data.networkStatus === 7 && nextProps.data.networkStatus === 3) - return false; - return true; - } - - render() { - const { - data: { community }, - isLoading, - currentUser, - } = this.props; - const { scrollElement } = this.state; - - if (community) { - const { edges: members } = community.members; - const nodes = members.map(member => member && member.node); - const uniqueNodes = deduplicateChildren(nodes, 'id'); - const hasNextPage = community.members.pageInfo.hasNextPage; - - return ( - - - - } - useWindow={false} - initialLoad={false} - scrollElement={scrollElement} - threshold={750} - className={'scroller-for-community-members-list'} - > - {uniqueNodes.map(node => { - if (!node) return null; - - const { user, roles, reputation } = node; - return ( - - {currentUser && user.id !== currentUser.id && ( - - this.initMessage(user)} - /> - - )} - - ); - })} - - ); - } - - if (isLoading) { - return ; - } - - return ( - - - - ); - } -} - -export default compose( - withRouter, - withCurrentUser, - getCommunityMembersQuery, - viewNetworkHandler, - connect() -)(CommunityMemberGrid); diff --git a/src/views/community/components/membersList.js b/src/views/community/components/membersList.js new file mode 100644 index 0000000000..f68d43f8b3 --- /dev/null +++ b/src/views/community/components/membersList.js @@ -0,0 +1,113 @@ +// @flow +import * as React from 'react'; +import compose from 'recompose/compose'; +import { connect } from 'react-redux'; +import { withRouter } from 'react-router'; +import { ErrorBoundary } from 'src/components/error'; +import { deduplicateChildren } from 'src/components/infiniteScroll/deduplicateChildren'; +import getCommunityMembersQuery, { + type GetCommunityMembersType, +} from 'shared/graphql/queries/community/getCommunityMembers'; +import { Card } from 'src/components/card'; +import NextPageButton from 'src/components/nextPageButton'; +import { Loading } from 'src/components/loading'; +import viewNetworkHandler from 'src/components/viewNetworkHandler'; +import ViewError from 'src/components/viewError'; +import { UserListItem } from 'src/components/entities'; +import type { Dispatch } from 'redux'; +import { withCurrentUser } from 'src/components/withCurrentUser'; + +type Props = { + data: { + community: GetCommunityMembersType, + fetchMore: Function, + networkStatus: number, + }, + dispatch: Dispatch, + isLoading: boolean, + isFetchingMore: boolean, + history: Object, + currentUser: ?Object, +}; + +class MembersList extends React.Component { + shouldComponentUpdate(nextProps) { + const curr = this.props; + // fetching more + if (curr.data.networkStatus === 7 && nextProps.data.networkStatus === 3) + return false; + return true; + } + + render() { + const { + data: { community }, + isLoading, + currentUser, + isFetchingMore, + } = this.props; + + if (isLoading) { + return ; + } + + if (community) { + const { edges: members } = community.members; + const nodes = members.map(member => member && member.node); + const uniqueNodes = deduplicateChildren(nodes, 'id'); + const hasNextPage = community.members.pageInfo.hasNextPage; + + return ( + + {uniqueNodes.map(node => { + if (!node) return null; + + const { user } = node; + return ( + + + + ); + })} + {hasNextPage && ( + + Load more members + + )} + + ); + } + + return ( + + + + ); + } +} + +export default compose( + withRouter, + withCurrentUser, + getCommunityMembersQuery, + viewNetworkHandler, + connect() +)(MembersList); diff --git a/src/views/community/components/mobileCommunityInfoActions.js b/src/views/community/components/mobileCommunityInfoActions.js new file mode 100644 index 0000000000..0a2d77eeb0 --- /dev/null +++ b/src/views/community/components/mobileCommunityInfoActions.js @@ -0,0 +1,72 @@ +// @flow +import React from 'react'; +import compose from 'recompose/compose'; +import type { Dispatch } from 'redux'; +import { connect } from 'react-redux'; +import type { CommunityInfoType } from 'shared/graphql/fragments/community/communityInfo'; +import { openModal } from 'src/actions/modals'; +import Icon from 'src/components/icon'; +import { + SidebarSectionHeader, + SidebarSectionHeading, + List, + ListItem, + ListItemLink, + ListItemLabel, + ListItemContent, + NameWarn, +} from '../style'; + +type Props = { + dispatch: Dispatch, + community: CommunityInfoType, +}; + +const Component = (props: Props) => { + const { community, dispatch } = props; + const { communityPermissions } = community; + const { isMember, isOwner, isModerator } = communityPermissions; + const isTeamMember = isOwner || isModerator; + + const leaveCommunity = () => + dispatch( + openModal('DELETE_DOUBLE_CHECK_MODAL', { + id: community.id, + entity: 'team-member-leaving-community', + message: 'Are you sure you want to leave this community?', + buttonLabel: 'Leave Community', + }) + ); + + if (!isMember && !isOwner && !isModerator) return null; + + return ( + + + More + + + + {isTeamMember && ( + + + Community settings + + + + )} + + {!isOwner && isMember && ( + + + Leave community + + + + )} + + + ); +}; + +export const MobileCommunityInfoActions = compose(connect())(Component); diff --git a/src/views/community/components/moderatorList.js b/src/views/community/components/moderatorList.js deleted file mode 100644 index d3ed13d118..0000000000 --- a/src/views/community/components/moderatorList.js +++ /dev/null @@ -1,138 +0,0 @@ -// @flow -import * as React from 'react'; -import compose from 'recompose/compose'; -import { connect } from 'react-redux'; -import Icon from 'src/components/icons'; -import { initNewThreadWithUser } from 'src/actions/directMessageThreads'; -import { withRouter } from 'react-router'; -import getCommunityMembersQuery, { - type GetCommunityMembersType, -} from 'shared/graphql/queries/community/getCommunityMembers'; -import { Card } from 'src/components/card'; -import { Loading } from 'src/components/loading'; -import viewNetworkHandler from 'src/components/viewNetworkHandler'; -import ViewError from 'src/components/viewError'; -import { MessageIconContainer, ListColumn } from '../style'; -import GranularUserProfile from 'src/components/granularUserProfile'; -import { UpsellTeamMembers } from 'src/components/upsell'; -import type { Dispatch } from 'redux'; -import { withCurrentUser } from 'src/components/withCurrentUser'; - -type Props = { - data: { - community: GetCommunityMembersType, - fetchMore: Function, - }, - dispatch: Dispatch, - isLoading: boolean, - isFetchingMore: boolean, - history: Object, - currentUser: ?Object, -}; - -class CommunityModeratorList extends React.Component { - shouldComponentUpdate(nextProps) { - // NOTE(@brian) This is needed to avoid conflicting the the members tab in - // the community view. See https://github.com/withspectrum/spectrum/pull/2613#pullrequestreview-105861623 - // for discussion - // never update once we have the list of team members - if ( - this.props.data && - this.props.data.community && - nextProps.data.community - ) { - if (this.props.data.community.id === nextProps.data.community.id) - return false; - } - return true; - } - - initMessage = user => { - this.props.dispatch(initNewThreadWithUser(user)); - return this.props.history.push('/messages/new'); - }; - - render() { - const { - data: { community }, - isLoading, - currentUser, - } = this.props; - - if (community && community.members) { - const { edges: members } = community.members; - const nodes = members - .map(member => member && member.node) - .filter(node => node && (node.isOwner || node.isModerator)) - .filter(Boolean); - - const currentUserIsOwner = - currentUser && - nodes.find(node => node.user.id === currentUser.id && node.isOwner); - - return ( - - {nodes.map(node => { - const { user } = node; - - return ( - - {currentUser && - node.user.id !== currentUser.id && ( - - this.initMessage(node.user)} - /> - - )} - - ); - })} - {currentUserIsOwner && ( - 1} - /> - )} - - ); - } - - if (isLoading) { - return ( -
- -
- ); - } - - return ( - - - - ); - } -} - -export default compose( - withRouter, - withCurrentUser, - getCommunityMembersQuery, - viewNetworkHandler, - connect() -)(CommunityModeratorList); diff --git a/src/views/community/components/postsFeeds.js b/src/views/community/components/postsFeeds.js new file mode 100644 index 0000000000..2a1bad353d --- /dev/null +++ b/src/views/community/components/postsFeeds.js @@ -0,0 +1,105 @@ +// @flow +import React, { useState, useEffect } from 'react'; +import compose from 'recompose/compose'; +import type { GetCommunityType } from 'shared/graphql/queries/community/getCommunity'; +import type { UserInfoType } from 'shared/graphql/fragments/user/userInfo'; +import getCommunityThreads from 'shared/graphql/queries/community/getCommunityThreadConnection'; +import searchThreads from 'shared/graphql/queries/search/searchThreads'; +import ThreadFeed from 'src/components/threadFeed'; +import Select from 'src/components/select'; +import { withCurrentUser } from 'src/components/withCurrentUser'; +import { PostsFeedsSelectorContainer, SearchInput } from '../style'; + +const CommunityThreadFeed = compose(getCommunityThreads)(ThreadFeed); +const SearchThreadFeed = compose(searchThreads)(ThreadFeed); + +type Props = { + community: GetCommunityType, + currentUser: ?UserInfoType, +}; + +// @see https://dev.to/gabe_ragland/debouncing-with-react-hooks-jci +function useDebounce(value, delay) { + const [debouncedValue, setDebouncedValue] = useState(value); + + useEffect(() => { + const handler = setTimeout(() => { + setDebouncedValue(value); + }, delay); + + return () => { + clearTimeout(handler); + }; + }, [value]); + + return debouncedValue; +} + +export const PostsFeeds = withCurrentUser((props: Props) => { + const { community, currentUser } = props; + const defaultFeed = !currentUser ? 'trending' : 'latest'; + const [activeFeed, setActiveFeed] = useState(defaultFeed); + const [clientSearchQuery, setClientSearchQuery] = useState(''); + const [serverSearchQuery, setServerSearchQuery] = useState(''); + + const debouncedServerSearchQuery = useDebounce(serverSearchQuery, 500); + + const search = (query: string) => { + const sanitized = query.toLowerCase().trim(); + setServerSearchQuery(sanitized); + }; + + const handleClientSearch = (e: any) => { + setClientSearchQuery(e.target.value); + search(e.target.value); + }; + + return ( + + + + + + + {debouncedServerSearchQuery && ( + + )} + + {!debouncedServerSearchQuery && ( + + )} + + ); +}); diff --git a/src/views/community/components/requestJoinCommunity.js b/src/views/community/components/requestJoinCommunity.js new file mode 100644 index 0000000000..e5b3fc0fa0 --- /dev/null +++ b/src/views/community/components/requestJoinCommunity.js @@ -0,0 +1,82 @@ +// @flow +import * as React from 'react'; +import { connect } from 'react-redux'; +import compose from 'recompose/compose'; +import addPendingCommunityMemberMutation from 'shared/graphql/mutations/communityMember/addPendingCommunityMember'; +import removePendingCommunityMemberMutation from 'shared/graphql/mutations/communityMember/removePendingCommunityMember'; +import { addToastWithTimeout } from 'src/actions/toasts'; + +type Props = { + communityId: string, + render: Function, + children: any, + addPendingCommunityMember: Function, + removePendingCommunityMember: Function, + dispatch: Dispatch, + isPending: boolean, +}; + +const RequestJoinCommunity = (props: Props) => { + const { + communityId, + dispatch, + render, + isPending, + addPendingCommunityMember, + removePendingCommunityMember, + } = props; + + const input = { communityId }; + + const [isLoading, setIsLoading] = React.useState(false); + + const sendRequest = () => { + setIsLoading(true); + + return addPendingCommunityMember({ input }) + .then(() => { + setIsLoading(false); + return dispatch(addToastWithTimeout('success', 'Request sent!')); + }) + .catch(err => { + setIsLoading(false); + return dispatch(addToastWithTimeout('error', err.message)); + }); + }; + + const cancelRequest = () => { + setIsLoading(true); + + return removePendingCommunityMember({ input }) + .then(() => { + setIsLoading(false); + + return dispatch(addToastWithTimeout('success', 'Request canceled')); + }) + .catch(err => { + setIsLoading(false); + + return dispatch(addToastWithTimeout('error', err.message)); + }); + }; + + const cy = isPending + ? 'cancel-request-to-join-private-community-button' + : 'request-to-join-private-community-button'; + + return ( + + {render({ isLoading })} + + ); +}; + +export default compose( + connect(), + addPendingCommunityMemberMutation, + removePendingCommunityMemberMutation +)(RequestJoinCommunity); diff --git a/src/views/community/components/requestToJoinCommunity.js b/src/views/community/components/requestToJoinCommunity.js deleted file mode 100644 index a65be7b389..0000000000 --- a/src/views/community/components/requestToJoinCommunity.js +++ /dev/null @@ -1,121 +0,0 @@ -// @flow -import * as React from 'react'; -import compose from 'recompose/compose'; -import { connect } from 'react-redux'; -import addPendingCommunityMemberMutation from 'shared/graphql/mutations/communityMember/addPendingCommunityMember'; -import removePendingCommunityMemberMutation from 'shared/graphql/mutations/communityMember/removePendingCommunityMember'; -import type { GetCommunityType } from 'shared/graphql/queries/community/getCommunity'; -import { addToastWithTimeout } from 'src/actions/toasts'; -import { Button, OutlineButton } from 'src/components/buttons'; - -type Props = { - community: GetCommunityType, - addPendingCommunityMember: Function, - removePendingCommunityMember: Function, - dispatch: Function, -}; - -type State = { - isLoading: boolean, -}; - -class RequestToJoinCommunity extends React.Component { - state = { - isLoading: false, - }; - - sendRequest = () => { - const { dispatch, community } = this.props; - const { id: communityId } = community; - - this.setState({ - isLoading: true, - }); - - this.props - .addPendingCommunityMember({ input: { communityId } }) - .then(() => { - this.setState({ - isLoading: false, - }); - - return dispatch(addToastWithTimeout('success', 'Request sent!')); - }) - .catch(err => { - this.setState({ - isLoading: false, - }); - - dispatch(addToastWithTimeout('error', err.message)); - }); - }; - - cancelRequest = () => { - const { dispatch, community } = this.props; - const { id: communityId } = community; - - this.setState({ - isLoading: true, - }); - - this.props - .removePendingCommunityMember({ input: { communityId } }) - .then(() => { - this.setState({ - isLoading: false, - }); - - return dispatch( - addToastWithTimeout('success', 'Request has been canceled') - ); - }) - .catch(err => { - this.setState({ - isLoading: false, - }); - - dispatch(addToastWithTimeout('error', err.message)); - }); - }; - - render() { - const { community } = this.props; - const { isLoading } = this.state; - - const { isPending } = community.communityPermissions; - - return ( - - {isPending && ( - - Cancel request - - )} - - {!isPending && ( - - )} - - ); - } -} - -export default compose( - connect(), - addPendingCommunityMemberMutation, - removePendingCommunityMemberMutation -)(RequestToJoinCommunity); diff --git a/src/views/community/components/search.js b/src/views/community/components/search.js deleted file mode 100644 index 7d20e7308c..0000000000 --- a/src/views/community/components/search.js +++ /dev/null @@ -1,87 +0,0 @@ -// @flow -import * as React from 'react'; -import compose from 'recompose/compose'; -import { throttle } from '../../../helpers/utils'; -import searchThreads from 'shared/graphql/queries/search/searchThreads'; -import ThreadFeed from '../../../components/threadFeed'; -import { SearchContainer, SearchInput } from '../style'; - -const SearchThreadFeed = compose(searchThreads)(ThreadFeed); - -type Props = { - community: Object, -}; - -type State = { - searchString: string, - sendStringToServer: string, -}; - -class Search extends React.Component { - constructor() { - super(); - - this.state = { - searchString: '', - sendStringToServer: '', - }; - - this.search = throttle(this.search, 500); - } - - search = searchString => { - // don't start searching until at least 3 characters are typed - if (searchString.length < 3) return; - - // start the input loading spinner - this.setState({ - sendStringToServer: searchString, - }); - }; - - handleChange = (e: any) => { - const searchString = e.target.value.toLowerCase().trim(); - - // set the searchstring to state - this.setState({ - searchString, - }); - - // trigger a new search based on the search input - // $FlowIssue - this.search(searchString); - }; - - render() { - const { community } = this.props; - const { searchString, sendStringToServer } = this.state; - - return ( -
- - - - {searchString && - sendStringToServer && ( - - )} -
- ); - } -} - -export default compose()(Search); diff --git a/src/views/community/components/teamMembersList.js b/src/views/community/components/teamMembersList.js new file mode 100644 index 0000000000..988208e79b --- /dev/null +++ b/src/views/community/components/teamMembersList.js @@ -0,0 +1,121 @@ +// @flow +import React from 'react'; +import compose from 'recompose/compose'; +import { connect } from 'react-redux'; +import { withRouter } from 'react-router'; +import { ErrorBoundary } from 'src/components/error'; +import type { GetCommunityType } from 'shared/graphql/queries/community/getCommunity'; +import type { UserInfoType } from 'shared/graphql/fragments/user/userInfo'; +import getCommunityMembersQuery, { + type GetCommunityMembersType, +} from 'shared/graphql/queries/community/getCommunityMembers'; +import { Loading } from 'src/components/loading'; +import viewNetworkHandler, { + type ViewNetworkHandlerType, +} from 'src/components/viewNetworkHandler'; +import { UserListItem } from 'src/components/entities'; +import Icon from 'src/components/icon'; +import { withCurrentUser } from 'src/components/withCurrentUser'; +import Tooltip from 'src/components/tooltip'; +import { WhiteIconButton } from 'src/components/button'; +import { List, SidebarSectionHeader, SidebarSectionHeading } from '../style'; + +type Props = { + ...$Exact, + currentUser: ?UserInfoType, + community: GetCommunityType, + data: { + community: GetCommunityMembersType, + }, +}; + +class Component extends React.Component { + render() { + const { + isLoading, + hasError, + queryVarIsChanging, + data, + currentUser, + } = this.props; + + const isOwner = this.props.community.communityPermissions.isOwner; + + if (isLoading || queryVarIsChanging) + return ( + + + Team + + + + ); + + if (hasError) return null; + + const { community } = data; + + const { edges: members } = community.members; + const nodes = members + .map(member => member && member.node) + .filter(node => node && (node.isOwner || node.isModerator)) + .filter(Boolean) + .sort((a, b) => { + const bc = parseInt(b.reputation, 10); + const ac = parseInt(a.reputation, 10); + + // sort same-reputation communities alphabetically + if (ac === bc) { + return a.user.name.toUpperCase() <= b.user.name.toUpperCase() + ? -1 + : 1; + } + + // otherwise sort by reputation + return bc <= ac ? -1 : 1; + }); + + return ( + + + Team + {isOwner && ( + + + + + + + + )} + + + + {nodes.map(({ user }) => ( + + + + ))} + + + ); + } +} + +export const TeamMembersList = compose( + withRouter, + withCurrentUser, + getCommunityMembersQuery, + viewNetworkHandler, + connect() +)(Component); diff --git a/src/views/community/containers/privateCommunity.js b/src/views/community/containers/privateCommunity.js new file mode 100644 index 0000000000..f3d3c62d14 --- /dev/null +++ b/src/views/community/containers/privateCommunity.js @@ -0,0 +1,61 @@ +// @flow +import React from 'react'; +import type { GetCommunityType } from 'shared/graphql/queries/community/getCommunity'; +import { OutlineButton, PrimaryButton } from 'src/components/button'; +import RequestJoinCommunity from '../components/requestJoinCommunity'; +import { + Emoji, + Heading, + Description, + ActionsRow, + PrivateCommunityWrapper, +} from '../style'; +import { ViewGrid, CenteredGrid } from 'src/components/layout'; + +type Props = { + community: GetCommunityType, +}; + +export const PrivateCommunity = (props: Props) => { + const { community } = props; + const { communityPermissions } = community; + const { isPending } = communityPermissions; + + const heading = isPending + ? 'Your request to join is pending' + : 'This community is private'; + + const description = isPending + ? 'Your request to join this community is pending. The owners have been notified, and you will be notified if your request is approved.' + : 'You can request to join this community below. The owners will be notified of your request, and you will be notified if your request is approved.'; + + const primaryAction = ({ isLoading }) => + isPending ? ( + Cancel request + ) : ( + Request to join + ); + + return ( + + + + + 🔑 + + {heading} + {description} + + Go home + + primaryAction({ isLoading })} + /> + + + + + ); +}; diff --git a/src/views/community/containers/signedIn.js b/src/views/community/containers/signedIn.js new file mode 100644 index 0000000000..c8a71c9a81 --- /dev/null +++ b/src/views/community/containers/signedIn.js @@ -0,0 +1,175 @@ +// @flow +import React, { useState, useEffect } from 'react'; +import compose from 'recompose/compose'; +import { connect } from 'react-redux'; +import type { Dispatch } from 'redux'; +import { withRouter, type Location } from 'react-router-dom'; +import type { UserInfoType } from 'shared/graphql/fragments/user/userInfo'; +import type { CommunityInfoType } from 'shared/graphql/fragments/community/communityInfo'; +import generateMetaInfo from 'shared/generate-meta-info'; +import { withCurrentUser } from 'src/components/withCurrentUser'; +import Head from 'src/components/head'; +import { CommunityProfileCard } from 'src/components/entities'; +import { CommunityAvatar } from 'src/components/avatar'; +import { ErrorBoundary } from 'src/components/error'; +import { MobileCommunityAction } from 'src/components/titlebar/actions'; +import { setTitlebarProps } from 'src/actions/titlebar'; +import { TeamMembersList } from '../components/teamMembersList'; +import { CommunityFeeds } from '../components/communityFeeds'; +import { ChannelsList } from '../components/channelsList'; +import { SidebarSection } from '../style'; +import { + ViewGrid, + SecondaryPrimaryColumnGrid, + PrimaryColumn, + SecondaryColumn, +} from 'src/components/layout'; +import setCommunityLastSeenMutation from 'shared/graphql/mutations/community/setCommunityLastSeen'; +import usePrevious from 'src/hooks/usePrevious'; + +type Props = { + community: CommunityInfoType, + currentUser: ?UserInfoType, + dispatch: Dispatch, + location: Location, + setCommunityLastSeen: Function, +}; + +const Component = (props: Props) => { + const { + community, + currentUser, + dispatch, + location, + setCommunityLastSeen, + } = props; + + const previousCommunity = usePrevious(community); + useEffect(() => { + if ( + !community.id || + !currentUser || + !community.communityPermissions.isMember + ) + return; + + if ( + previousCommunity && + community && + previousCommunity.id !== community.id + ) { + setCommunityLastSeen({ + id: previousCommunity.id, + lastSeen: new Date(), + }); + } + setCommunityLastSeen({ + id: community.id, + lastSeen: new Date(Date.now() + 10000), + }); + }, [community.id, currentUser]); + + const [metaInfo, setMetaInfo] = useState( + generateMetaInfo({ + type: 'community', + data: { + name: community.name, + description: community.description, + }, + }) + ); + + useEffect(() => { + setMetaInfo( + generateMetaInfo({ + type: 'community', + data: { + name: `${community.name} community`, + description: community.description, + }, + }) + ); + dispatch( + setTitlebarProps({ + title: community.name, + titleIcon: ( + + ), + rightAction: , + }) + ); + }, [community.id]); + + useEffect(() => { + dispatch( + setTitlebarProps({ + title: community.name, + titleIcon: ( + + ), + rightAction: , + }) + ); + }, [location]); + + const { title, description } = metaInfo; + + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); +}; + +export const SignedIn = compose( + withCurrentUser, + withRouter, + setCommunityLastSeenMutation, + connect() +)(Component); diff --git a/src/views/community/index.js b/src/views/community/index.js index 6d1606e94e..4767da4cae 100644 --- a/src/views/community/index.js +++ b/src/views/community/index.js @@ -1,482 +1,65 @@ // @flow -import * as React from 'react'; +import React from 'react'; import compose from 'recompose/compose'; +import type { Match } from 'react-router-dom'; import { connect } from 'react-redux'; -import { Link } from 'react-router-dom'; -import { Button } from 'src/components/buttons'; -import generateMetaInfo from 'shared/generate-meta-info'; -import ComposerPlaceholder from 'src/components/threadComposer/components/placeholder'; -import Head from 'src/components/head'; -import Icon from 'src/components/icons'; -import AppViewWrapper from 'src/components/appViewWrapper'; -import ThreadFeed from 'src/components/threadFeed'; -import Search from './components/search'; -import CommunityMemberGrid from './components/memberGrid'; -import ToggleCommunityMembership from 'src/components/toggleCommunityMembership'; -import { addCommunityToOnboarding } from 'src/actions/newUserOnboarding'; -import { CoverPhoto } from 'src/components/profile/coverPhoto'; -import Titlebar from '../titlebar'; -import { CommunityProfile } from 'src/components/profile'; -import viewNetworkHandler from 'src/components/viewNetworkHandler'; -import type { ViewNetworkHandlerType } from 'src/components/viewNetworkHandler'; -import ViewError from 'src/components/viewError'; -import { LoadingScreen } from 'src/components/loading'; -import { CLIENT_URL } from 'src/api/constants'; -import { Upsell404Community } from 'src/components/upsell'; -import { withCurrentUser } from 'src/components/withCurrentUser'; -import { - SegmentedControl, - Segment, - DesktopSegment, - MobileSegment, -} from 'src/components/segmentedControl'; -import { - LoginButton, - LoginOutlineButton, - SettingsButton, - Grid, - Meta, - Content, - Extras, - ColumnHeading, -} from './style'; -import getCommunityThreads from 'shared/graphql/queries/community/getCommunityThreadConnection'; +import type { UserInfoType } from 'shared/graphql/fragments/user/userInfo'; +import viewNetworkHandler, { + type ViewNetworkHandlerType, +} from 'src/components/viewNetworkHandler'; import { getCommunityByMatch, type GetCommunityType, } from 'shared/graphql/queries/community/getCommunity'; -import ChannelList from './components/channelList'; -import ModeratorList from './components/moderatorList'; -import { track, events, transformations } from 'src/helpers/analytics'; -import RequestToJoinCommunity from './components/requestToJoinCommunity'; +import { withCurrentUser } from 'src/components/withCurrentUser'; import CommunityLogin from 'src/views/communityLogin'; import Login from 'src/views/login'; -import { ErrorBoundary } from 'src/components/error'; - -const CommunityThreadFeed = compose( - connect(), - getCommunityThreads -)(ThreadFeed); +import { CLIENT_URL } from 'src/api/constants'; +import { ErrorView, LoadingView } from 'src/views/viewHelpers'; +import { SignedIn } from './containers/signedIn'; +import { PrivateCommunity } from './containers/privateCommunity'; type Props = { ...$Exact, - dispatch: Dispatch, - toggleCommunityMembership: Function, - currentUser: Object, - match: { - params: { - communitySlug: string, - }, - }, + currentUser: ?UserInfoType, + match: Match, data: { community: GetCommunityType, }, }; -type State = { - selectedView: 'trending-threads' | 'threads' | 'search' | 'members', - isLeavingCommunity: boolean, -}; - -class CommunityView extends React.Component { - constructor() { - super(); - - this.state = { - isLeavingCommunity: false, - selectedView: 'trending-threads', - }; - } - - componentDidMount() { - if (this.props.data && this.props.data.community) { - track(events.COMMUNITY_VIEWED, { - community: transformations.analyticsCommunity( - this.props.data.community - ), - }); - } - } - - componentDidUpdate(prevProps) { - const { community: prevCommunity } = prevProps.data; - const { community: currCommunity } = this.props.data; - if ( - (!prevCommunity && currCommunity && currCommunity.id) || - (prevCommunity && prevCommunity.id !== currCommunity.id) - ) { - track(events.COMMUNITY_VIEWED, { - community: transformations.analyticsCommunity(currCommunity), - }); - - // if the user is new and signed up through a community page, push - // the community data into the store to hydrate the new user experience - // with their first community they should join - if (!this.props.currentUser || !this.props.currentUser.username) { - return this.props.dispatch(addCommunityToOnboarding(currCommunity)); - } - } - } - - handleSegmentClick = label => { - if (this.state.selectedView === label) return; - - return this.setState({ - selectedView: label, - }); - }; - - render() { - const { - match: { params }, - data: { community }, - currentUser, - isLoading, - hasError, - match, - } = this.props; - const { communitySlug } = params; +const CommunityView = (props: Props) => { + const { isLoading, queryVarIsChanging, hasError, currentUser, match } = props; - if (community && community.id) { - // at this point the community exists and was fetched - const { title, description } = generateMetaInfo({ - type: 'community', - data: { - name: community.name, - description: community.description, - }, - }); + if (isLoading || queryVarIsChanging) return ; - const { selectedView } = this.state; - const { - isMember, - isOwner, - isModerator, - isPending, - isBlocked, - } = community.communityPermissions; - const userHasPermissions = isMember || isOwner || isModerator; - const isLoggedIn = currentUser; + const { community } = props.data; - if (isBlocked) { - return ( - - + if (!community || hasError) return ; - + const { isPrivate, communityPermissions } = community; + const { isMember, isBlocked } = communityPermissions; - - - - - - - ); - } + if (currentUser && !isBlocked && !isPrivate && !isMember) + return ; - const redirectPath = `${CLIENT_URL}/${community.slug}`; + if (isBlocked) return ; - if (!currentUser && community.isPrivate) { - if (community.brandedLogin.isEnabled) { - return ; - } else { - return ; - } - } - - if (community.isPrivate && (!isLoggedIn || !userHasPermissions)) { - return ( - - - - - - - - - - ); - } - - // if the person viewing the community recently created this community, - // we'll mark it as "new and owned" - this tells the downstream - // components to show nux upsells to create a thread or invite people - // to the community - const loginUrl = community.brandedLogin.isEnabled - ? `/${community.slug}/login?r=${CLIENT_URL}/${community.slug}` - : `/login?r=${CLIENT_URL}/${community.slug}`; - return ( - - - - - - - - - - - {!isLoggedIn ? ( - - - Join {community.name} - - - ) : !isOwner ? ( - { - if (isMember) { - return ( - - Leave community - - ); - } else { - return ( - - Join {community.name} - - ); - } - }} - /> - ) : null} - - {currentUser && (isOwner || isModerator) && ( - - - Settings - - - )} - - - - this.handleSegmentClick('search')} - selected={selectedView === 'search'} - > - - Search - - - this.handleSegmentClick('search')} - selected={selectedView === 'search'} - > - - - - this.handleSegmentClick('trending-threads')} - selected={selectedView === 'trending-threads'} - > - Trending - - - this.handleSegmentClick('threads')} - selected={selectedView === 'threads'} - > - Latest - - - this.handleSegmentClick('members')} - selected={selectedView === 'members'} - > - Members - - this.handleSegmentClick('members')} - selected={selectedView === 'members'} - > - Members - - - - {// if the user is logged in, is viewing the threads, - // and is a member of the community, they should see a - // new thread composer - isLoggedIn && - (selectedView === 'threads' || - selectedView === 'trending-threads') && - userHasPermissions && ( - - - - )} - - {(selectedView === 'threads' || - selectedView === 'trending-threads') && ( - - )} - - {// members grid - selectedView === 'members' && ( - - - - )} - - {//search - selectedView === 'search' && ( - - - - )} - - - - Channels - - - - - Team - - - - - - ); - } - - if (isLoading) { - return ; - } - - if (hasError) { - return ( - - - - - ); + if (isPrivate && !currentUser) { + const redirectPath = `${CLIENT_URL}/${community.slug}`; + if (community.brandedLogin.isEnabled) { + return ; + } else { + return ; } + } - return ( - - - - - - - ); + if (isPrivate && currentUser && !isMember) { + return ; } -} + + return ; +}; export default compose( withCurrentUser, diff --git a/src/views/community/style.js b/src/views/community/style.js index 38a08bb8a2..fa3e67da4f 100644 --- a/src/views/community/style.js +++ b/src/views/community/style.js @@ -1,275 +1,275 @@ // @flow +import styled, { css } from 'styled-components'; import theme from 'shared/theme'; -import styled from 'styled-components'; -import { IconButton } from '../../components/buttons'; -import Card from '../../components/card'; -import { Button, OutlineButton } from '../../components/buttons'; -import { - FlexCol, - Transition, - zIndex, - Gradient, - Tooltip, -} from '../../components/globals'; -import { - DesktopSegment, - SegmentedControl, -} from '../../components/segmentedControl'; - -export const LoginButton = styled(Button)` - width: 100%; - margin-top: 16px; - background-color: ${props => props.theme.success.default}; - background-image: ${props => - Gradient(props.theme.success.alt, props.theme.success.default)}; -`; +import { Link } from 'react-router-dom'; +import { Truncate, tint } from 'src/components/globals'; +import { MEDIA_BREAK } from 'src/components/layout'; +import { CardStyles } from 'src/views/viewHelpers'; -export const LoginOutlineButton = styled(OutlineButton)` - width: 100%; - margin-top: 16px; - color: ${props => props.theme.text.alt}; - box-shadow: 0 0 1px ${props => props.theme.text.alt}; +const listItemStyles = css` + padding: 12px 12px 12px 16px; + display: flex; + justify-content: space-between; + border-bottom: 1px solid ${theme.bg.divider}; - &:hover { - color: ${props => props.theme.warn.default}; - box-shadow: 0 0 1px ${props => props.theme.warn.default}; + &:last-of-type { + border-bottom: 0; } -`; -export const SettingsButton = styled(LoginOutlineButton)` - justify-content: center; &:hover { - color: ${props => props.theme.text.secondary}; - box-shadow: 0 0 1px ${props => props.theme.text.secondary}; + background: ${theme.bg.wash}; } -`; -export const CoverButton = styled(IconButton)` - position: absolute; - right: 16px; - top: 16px; - flex: 0 0 auto; - - @media (max-width: 768px) { - bottom: 16px; - top: auto; + .icon { + color: ${theme.text.alt}; } `; +export const ListItem = styled.div` + ${listItemStyles}; +`; +export const ListItemLink = styled(Link)` + ${listItemStyles}; +`; -export const SearchContainer = styled(Card)` - border-bottom: 2px solid ${theme.bg.border}; - position: relative; - z-index: ${zIndex.search}; - width: 100%; - display: block; - min-height: 64px; - transition: ${Transition.hover.off}; +export const ListItemContent = styled.div` + display: flex; + align-items: center; - &:hover { - transition: none; - border-bottom: 2px solid ${theme.brand.alt}; + .icon { + color: ${theme.text.secondary}; + margin-right: 6px; + position: relative; + top: 1px; } +`; - @media (max-width: 768px) { - border-radius: 0; - pointer-events: all; - margin-bottom: 0; - } +export const ListItemLabel = styled.div` + color: ${theme.text.default}; + font-size: 15px; + font-weight: 500; + line-height: 1.2; + vertical-align: middle; + display: flex; + align-items: center; + display: inline-block; + ${Truncate}; `; -export const MidSegment = styled(DesktopSegment)` - @media (min-width: 1281px) { - display: none; +export const SidebarSection = styled.section` + background: ${theme.bg.default}; + border: 1px solid ${theme.bg.border}; + margin-top: 16px; + border-radius: 4px; + + @media (max-width: ${MEDIA_BREAK}px) { + border: 0; + margin-top: 0; + + &:last-of-type { + border-bottom: 1px solid ${theme.bg.border}; + } + + &:not(:first-of-type) { + border-top: 1px solid ${theme.bg.border}; + } } `; -export const SearchInput = styled.input` - justify-content: flex-start; +export const SidebarSectionHeader = styled.div` + display: flex; + border-bottom: 1px solid ${theme.bg.border}; align-items: center; - cursor: pointer; - padding: 20px; - color: ${theme.text.default}; - transition: ${Transition.hover.off}; - font-size: 20px; - font-weight: 800; - margin-left: 8px; - width: 97%; - border-radius: 12px; -`; - -export const StyledButton = styled(Button)` - flex: none; - align-self: flex-start; - background-color: transparent; - box-shadow: none; - border: none; - padding: 0; - margin: 24px 0; - border-radius: 0; - background-image: none; - color: ${theme.text.alt}; - - &:hover { - background-color: transparent; - color: ${theme.brand.alt}; - box-shadow: none; + justify-content: space-between; + padding: 16px; + position: sticky; + top: 0; + background: ${theme.bg.default}; + z-index: 11; + border-radius: 4px 4px 0 0; + + a { + display: flex; + flex: none; + align-items: center; + color: ${theme.text.alt}; + + &:hover { + color: ${theme.text.default}; + } } - @media (max-width: 768px) { - margin: 2px 0; - padding: 16px 0; - width: 100%; - border-radius: 0; + @media (max-width: ${MEDIA_BREAK}px) { + z-index: 1; + background: ${theme.bg.wash}; + border-bottom: 1px solid ${theme.bg.border}; + padding: 24px 16px 8px 16px; + position: relative; } `; -export const Grid = styled.main` - display: grid; - grid-template-columns: minmax(320px, 1fr) 3fr minmax(240px, 2fr); - grid-template-rows: 240px 1fr; - grid-template-areas: 'cover cover cover' 'meta content extras'; - grid-column-gap: 32px; - width: 100%; - max-width: 1280px; - min-height: 100vh; - background-color: ${theme.bg.default}; - box-shadow: inset 1px 0 0 ${theme.bg.border}, - inset -1px 0 0 ${theme.bg.border}; - - @media (max-width: 1280px) { - grid-template-columns: 320px 1fr; - grid-template-rows: 160px auto 1fr; - grid-template-areas: 'cover cover' 'meta content' 'extras content'; - } +export const SidebarSectionHeading = styled.div` + font-size: 16px; + font-weight: 700; + color: ${theme.text.default}; + display: flex; + flex: 1 0 auto; + padding-right: 16px; - @media (max-width: 768px) { - grid-template-rows: 80px auto 1fr; - grid-template-columns: 100%; - grid-column-gap: 0; - grid-row-gap: 16px; - grid-template-areas: 'cover' 'meta' 'content'; + @media (max-width: ${MEDIA_BREAK}px) { + font-size: 14px; + font-weight: 600; + color: ${theme.text.secondary}; } `; -const Column = styled.div` +export const FeedsContainer = styled.section` display: flex; flex-direction: column; + background: ${theme.bg.default}; `; -export const ListColumn = styled(Column)` - align-items: stretch; - overflow: hidden; +export const Row = styled.div` + display: flex; `; -export const Meta = styled(Column)` - grid-area: meta; - - > a > button { - margin-top: 16px; - margin-left: 32px; - width: calc(100% - 32px); - - @media (max-width: 768px) { - margin-left: 0; - width: 100%; - } - } - - @media (max-width: 768px) { - padding: 0 16px; +export const ToggleNotificationsContainer = styled.div` + display: flex; + color: ${theme.text.alt}; + justify-content: center; + align-items: center; + height: 100%; + cursor: pointer; +`; - > div { - margin-left: 0; - } - } +export const Name = styled.div` + color: ${theme.text.default}; + font-size: 15px; + font-weight: 500; + line-height: 1.2; + vertical-align: middle; + display: flex; + align-items: center; + display: inline-block; + ${Truncate}; +`; - > .member-button { - margin-left: 32px; +export const NameWarn = styled.div` + color: ${theme.warn.default}; + font-size: 15px; + font-weight: 500; + line-height: 1.2; + vertical-align: middle; + display: flex; + align-items: center; + display: inline-block; + ${Truncate}; +`; - @media (max-width: 768px) { - margin-left: 0; - } - } +export const List = styled.div` + display: flex; + flex-direction: column; + border-radius: 0 0 4px 4px; + overflow: hidden; `; -export const Content = styled(Column)` - grid-area: content; - min-width: 0; - align-items: stretch; +export const PrivateCommunityWrapper = styled.div` + ${CardStyles}; + padding: 16px; +`; - @media (max-width: 1280px) and (min-width: 768px) { - padding-right: 32px; - } +export const ActionsRow = styled.div` + display: grid; + grid-gap: 16px; + grid-template-columns: repeat(2, 1fr); + margin-top: 32px; - @media (max-width: 768px) { - > ${SegmentedControl} > div { - margin-top: 0; - } + button { + display: flex; + flex: 1 0 auto; + width: 100%; } `; -export const Extras = styled(Column)` - grid-area: extras; +export const Emoji = styled.span` + font-size: 40px; + margin-bottom: 16px; +`; - > ${FlexCol} > div { - border-top: 0; - padding: 0; - padding-top: 24px; +export const Heading = styled.h3` + font-size: 24px; + font-weight: 700; + color: ${theme.text.default}; +`; +export const Description = styled.p` + margin-top: 8px; + font-size: 16px; + font-weight: 400; + line-height: 1.4; + color: ${theme.text.secondary}; +`; - h3 { - font-size: 16px; - line-height: 1.2; - } - } +export const PostsFeedsSelectorContainer = styled.div` + padding: 8px 16px; + border-bottom: 1px solid ${theme.bg.border}; + background: ${theme.bg.wash}; + display: flex; + justify-content: space-between; +`; - @media (min-width: 768px) { - padding-left: 32px; - padding-right: 0; +export const SearchInput = styled.input` + font-size: 15px; + border: none; + border: 1px solid ${theme.bg.border}; + -webkit-appearance: none; + border-radius: 32px; + padding: 8px 16px; + color: ${theme.text.default}; + font-weight: 600; + width: 120px; + transition: all 0.2s ease-in-out; + + &:focus { + width: 180px; + box-shadow: 0 0 0 2px ${theme.bg.default}, 0 0 0 4px ${theme.bg.border}; + transition: all 0.2s ease-in-out; } - @media (min-width: 1281px) { - padding-right: 32px; - padding-left: 0; + &:active { + width: 180px; + box-shadow: 0 0 0 2px ${theme.bg.default}, + 0 0 0 4px ${tint(theme.bg.border, -24)}; + transition: box-shadow 0.2s ease-in-out; + transition: all 0.2s ease-in-out; } - @media (max-width: 768px) { - display: none; + @media (max-width: ${MEDIA_BREAK}px) { + font-size: 16px; } `; -export const ColumnHeading = styled.div` +export const FeedsStretch = styled.div` + flex: 1; display: flex; align-items: center; - font-size: 18px; - line-height: 1; - font-weight: 500; - padding: 16px; - margin-top: 16px; - border-bottom: 2px solid ${theme.bg.border}; -`; - -export const ToggleNotificationsContainer = styled.div` - display: flex; - color: ${theme.text.alt}; - justify-content: center; - align-items: center; - height: 100%; - cursor: pointer; - ${Tooltip}; -`; + flex-direction: column; -export const MessageIconContainer = styled.div` - color: ${theme.text.alt}; - display: flex; - align-items: center; - justify-content: center; - cursor: pointer; + display: grid; + grid-template-columns: minmax(min-content, 1fr); + grid-template-rows: 1fr; + width: 100%; + align-items: flex-end; - &:hover { - color: ${theme.brand.alt}; + @media (max-width: ${MEDIA_BREAK}px) { + /* account for fixed position chat input */ + padding-bottom: 56px; + grid-template-rows: 1fr; } `; -export const UserListItemContainer = styled.div` - border-bottom: 1px solid ${theme.bg.wash}; +export const InfoContainer = styled.div` + display: flex; + flex-direction: column; + flex: 1; + background: ${theme.bg.wash}; + padding-bottom: 64px; `; diff --git a/src/views/communityAnalytics/components/conversationGrowth.js b/src/views/communityAnalytics/components/conversationGrowth.js index eaea4ca101..4f8c8f4799 100644 --- a/src/views/communityAnalytics/components/conversationGrowth.js +++ b/src/views/communityAnalytics/components/conversationGrowth.js @@ -1,13 +1,13 @@ // @flow import * as React from 'react'; import compose from 'recompose/compose'; -import viewNetworkHandler from '../../../components/viewNetworkHandler'; -import { Loading } from '../../../components/loading'; +import viewNetworkHandler from 'src/components/viewNetworkHandler'; +import { Loading } from 'src/components/loading'; import { SectionCard, SectionSubtitle, SectionTitle, -} from '../../../components/settingsViews/style'; +} from 'src/components/settingsViews/style'; import getCommunityConversationGrowth from 'shared/graphql/queries/community/getCommunityConversationGrowth'; import type { GetCommunityConversationGrowthType } from 'shared/graphql/queries/community/getCommunityConversationGrowth'; import { parseGrowth } from '../utils'; @@ -21,7 +21,10 @@ type Props = { class ConversationGrowth extends React.Component { render() { - const { data: { community }, isLoading } = this.props; + const { + data: { community }, + isLoading, + } = this.props; if (community) { const { @@ -55,6 +58,7 @@ class ConversationGrowth extends React.Component { } } -export default compose(getCommunityConversationGrowth, viewNetworkHandler)( - ConversationGrowth -); +export default compose( + getCommunityConversationGrowth, + viewNetworkHandler +)(ConversationGrowth); diff --git a/src/views/communityAnalytics/components/memberGrowth.js b/src/views/communityAnalytics/components/memberGrowth.js index db0cb5058f..32a9ec3f60 100644 --- a/src/views/communityAnalytics/components/memberGrowth.js +++ b/src/views/communityAnalytics/components/memberGrowth.js @@ -1,13 +1,13 @@ // @flow import * as React from 'react'; import compose from 'recompose/compose'; -import viewNetworkHandler from '../../../components/viewNetworkHandler'; -import { Loading } from '../../../components/loading'; +import viewNetworkHandler from 'src/components/viewNetworkHandler'; +import { Loading } from 'src/components/loading'; import { SectionCard, SectionSubtitle, SectionTitle, -} from '../../../components/settingsViews/style'; +} from 'src/components/settingsViews/style'; import getCommunityMemberGrowth from 'shared/graphql/queries/community/getCommunityMemberGrowth'; import type { GetCommunityMemberGrowthType } from 'shared/graphql/queries/community/getCommunityMemberGrowth'; import { parseGrowth } from '../utils'; @@ -21,7 +21,10 @@ type Props = { class MemberGrowth extends React.Component { render() { - const { data: { community }, isLoading } = this.props; + const { + data: { community }, + isLoading, + } = this.props; if (community) { const { @@ -53,6 +56,7 @@ class MemberGrowth extends React.Component { } } -export default compose(getCommunityMemberGrowth, viewNetworkHandler)( - MemberGrowth -); +export default compose( + getCommunityMemberGrowth, + viewNetworkHandler +)(MemberGrowth); diff --git a/src/views/communityAnalytics/components/topAndNewThreads.js b/src/views/communityAnalytics/components/topAndNewThreads.js index 7496df5e44..ea65136dae 100644 --- a/src/views/communityAnalytics/components/topAndNewThreads.js +++ b/src/views/communityAnalytics/components/topAndNewThreads.js @@ -1,14 +1,11 @@ // @flow import * as React from 'react'; import compose from 'recompose/compose'; -import viewNetworkHandler from '../../../components/viewNetworkHandler'; -import { Loading } from '../../../components/loading'; -import ViewError from '../../../components/viewError'; +import viewNetworkHandler from 'src/components/viewNetworkHandler'; +import { Loading } from 'src/components/loading'; +import ViewError from 'src/components/viewError'; import ThreadListItem from './threadListItem'; -import { - SectionCard, - SectionTitle, -} from '../../../components/settingsViews/style'; +import { SectionCard, SectionTitle } from 'src/components/settingsViews/style'; import getCommunityTopAndNewThreads from 'shared/graphql/queries/community/getCommunityTopAndNewThreads'; import type { GetCommunityTopAndNewThreadsType } from 'shared/graphql/queries/community/getCommunityTopAndNewThreads'; diff --git a/src/views/communityAnalytics/components/topMembers.js b/src/views/communityAnalytics/components/topMembers.js index 434f7b8151..57891fe059 100644 --- a/src/views/communityAnalytics/components/topMembers.js +++ b/src/views/communityAnalytics/components/topMembers.js @@ -2,17 +2,15 @@ import * as React from 'react'; import { connect } from 'react-redux'; import { withRouter } from 'react-router'; -import Icon from 'src/components/icons'; -import { initNewThreadWithUser } from 'src/actions/directMessageThreads'; import compose from 'recompose/compose'; import viewNetworkHandler from 'src/components/viewNetworkHandler'; import { Loading } from 'src/components/loading'; import ViewError from 'src/components/viewError'; import { SectionCard, SectionTitle } from 'src/components/settingsViews/style'; -import GranularUserProfile from 'src/components/granularUserProfile'; +import { UserListItem } from 'src/components/entities'; import getCommunityTopMembers from 'shared/graphql/queries/community/getCommunityTopMembers'; import type { GetCommunityTopMembersType } from 'shared/graphql/queries/community/getCommunityTopMembers'; -import { UserListItemContainer, MessageIconContainer } from '../style'; +import { UserListItemContainer } from '../style'; import type { Dispatch } from 'redux'; import { withCurrentUser } from 'src/components/withCurrentUser'; @@ -27,11 +25,6 @@ type Props = { }; class ConversationGrowth extends React.Component { - initMessage = user => { - this.props.dispatch(initNewThreadWithUser(user)); - this.props.history.push('/messages/new'); - }; - render() { const { data: { community }, @@ -70,7 +63,7 @@ class ConversationGrowth extends React.Component { if (!member) return null; return ( - { currentUser && member.user.id === currentUser.id } isOnline={member.user.isOnline} - reputation={member.reputation} profilePhoto={member.user.profilePhoto} avatarSize={40} - badges={member.roles} - > - {currentUser && - member.user.id !== currentUser.id && ( - - this.initMessage(member.user)} - /> - - )} - + showHoverProfile={false} + messageButton={ + currentUser && member.user.id !== currentUser.id + } + /> ); })} diff --git a/src/views/communityAnalytics/index.js b/src/views/communityAnalytics/index.js index d933830b8f..59c4231f26 100644 --- a/src/views/communityAnalytics/index.js +++ b/src/views/communityAnalytics/index.js @@ -5,7 +5,7 @@ import { connect } from 'react-redux'; import { Link } from 'react-router-dom'; import type { GetCommunitySettingsType } from 'shared/graphql/queries/community/getCommunitySettings'; import ViewError from 'src/components/viewError'; -import { Button, OutlineButton, ButtonRow } from 'src/components/buttons'; +import { Button, OutlineButton } from 'src/components/button'; import MemberGrowth from './components/memberGrowth'; import ConversationGrowth from './components/conversationGrowth'; import TopMembers from './components/topMembers'; @@ -73,7 +73,7 @@ class CommunityAnalytics extends React.Component { 'If you want to create your own community, you can get started below.' } > - +
Take me back @@ -81,7 +81,7 @@ class CommunityAnalytics extends React.Component { - +
); } diff --git a/src/views/communityAnalytics/style.js b/src/views/communityAnalytics/style.js index ca768e4056..3cf65f084d 100644 --- a/src/views/communityAnalytics/style.js +++ b/src/views/communityAnalytics/style.js @@ -67,18 +67,6 @@ export const ThreadListItemSubtitle = styled.h5` } `; -export const MessageIconContainer = styled.div` - color: ${theme.text.alt}; - display: flex; - align-items: center; - justify-content: center; - cursor: pointer; - - &:hover { - color: ${theme.brand.alt}; - } -`; - export const UserListItemContainer = styled.div` padding: 0 16px; border-bottom: 1px solid ${theme.bg.wash}; diff --git a/src/views/communityAnalytics/utils.js b/src/views/communityAnalytics/utils.js index c54c576b39..fd214148e0 100644 --- a/src/views/communityAnalytics/utils.js +++ b/src/views/communityAnalytics/utils.js @@ -3,16 +3,9 @@ import React from 'react'; import { SectionSubtitle, GrowthText, -} from '../../components/settingsViews/style'; +} from 'src/components/settingsViews/style'; -export const parseGrowth = ( - { - growth, - currentPeriodCount, - prevPeriodCount, - }: { growth: number, currentPeriodCount: number, prevPeriodCount: number }, - range: string -) => { +export const parseGrowth = ({ growth }: { growth: number }, range: string) => { if (!growth) { return null; } else if (growth > 0) { diff --git a/src/views/communityLogin/index.js b/src/views/communityLogin/index.js index de94f7ea29..5b78b1d858 100644 --- a/src/views/communityLogin/index.js +++ b/src/views/communityLogin/index.js @@ -3,7 +3,6 @@ import * as React from 'react'; import compose from 'recompose/compose'; import FullscreenView from 'src/components/fullscreenView'; import LoginButtonSet from 'src/components/loginButtonSet'; -import { Loading } from 'src/components/loading'; import { CommunityAvatar } from 'src/components/avatar'; import { CLIENT_URL } from 'src/api/constants'; import { @@ -20,9 +19,9 @@ import { getCommunityByMatch, type GetCommunityType, } from 'shared/graphql/queries/community/getCommunity'; -import ViewError from 'src/components/viewError'; import queryString from 'query-string'; import { track, events } from 'src/helpers/analytics'; +import { LoadingView, ErrorView } from 'src/views/viewHelpers'; type Props = { data: { @@ -125,25 +124,9 @@ export class Login extends React.Component { ); } - if (isLoading) { - return ( - - - - ); - } + if (isLoading) return ; - return ( - - - - ); + return ; } } diff --git a/src/views/communityLogin/style.js b/src/views/communityLogin/style.js index 5e304a2dbe..d2518d93ca 100644 --- a/src/views/communityLogin/style.js +++ b/src/views/communityLogin/style.js @@ -1,8 +1,8 @@ // @flow import theme from 'shared/theme'; -// $FlowFixMe import styled from 'styled-components'; import { zIndex } from 'src/components/globals'; +import { MEDIA_BREAK } from 'src/components/layout'; export const Title = styled.h1` color: ${theme.text.default}; @@ -69,7 +69,7 @@ export const SigninLink = styled.span` export const FullscreenContent = styled.div` width: 100%; - max-width: 768px; + max-width: ${MEDIA_BREAK}px; display: flex; align-items: center; flex-direction: column; diff --git a/src/views/communityMembers/components/communityMembers.js b/src/views/communityMembers/components/communityMembers.js index 820c44e3ac..23830c9ff7 100644 --- a/src/views/communityMembers/components/communityMembers.js +++ b/src/views/communityMembers/components/communityMembers.js @@ -15,7 +15,7 @@ import { SectionTitle, SectionCardFooter, } from 'src/components/settingsViews/style'; -import Icon from 'src/components/icons'; +import Icon from 'src/components/icon'; import { Filters, Filter, @@ -23,11 +23,11 @@ import { SearchInput, SearchForm, FetchMore, + Row, } from '../style'; import { ListContainer } from 'src/components/listItems/style'; -import { initNewThreadWithUser } from 'src/actions/directMessageThreads'; import ViewError from 'src/components/viewError'; -import GranularUserProfile from 'src/components/granularUserProfile'; +import { UserListItem } from 'src/components/entities'; import { Notice } from 'src/components/listItems/style'; import type { Dispatch } from 'redux'; @@ -134,37 +134,34 @@ class CommunityMembers extends React.Component { return this.setState({ queryString: searchString }); }; - initMessage = user => { - this.props.dispatch(initNewThreadWithUser(user)); - this.props.history.push('/messages/new'); - }; - generateUserProfile = communityMember => { - const { user, roles, reputation, ...permissions } = communityMember; + const { user, ...permissions } = communityMember; return ( - - {user.id !== this.props.currentUser.id && ( - + + - )} - + {user.id !== this.props.currentUser.id && ( + + )} + + ); }; @@ -221,60 +218,58 @@ class CommunityMembers extends React.Component { - {searchIsFocused && - queryString && ( - { - if (isLoading) { - return ; - } - - if (!searchResults || searchResults.length === 0) { - const emoji = ' '; + {searchIsFocused && queryString && ( + { + if (isLoading) { + return ; + } - const heading = - searchString.length > 1 - ? `We couldn't find anyone matching "${searchString}"` - : 'Search for people in your community'; + if (!searchResults || searchResults.length === 0) { + const emoji = ' '; - const subheading = - searchString.length > 1 - ? 'Grow your community by inviting people via email, or by importing a Slack team' - : 'Find people by name, username, and profile description - try searching for "designer" or "developer"'; + const heading = + searchString.length > 1 + ? `We couldn't find anyone matching "${searchString}"` + : 'Search for people in your community'; - return ( - - ); - } + const subheading = + searchString.length > 1 + ? 'Grow your community by inviting people via email, or by importing a Slack team' + : 'Find people by name, username, and profile description - try searching for "designer" or "developer"'; return ( - - {searchResults.map(communityMember => { - if (!communityMember) return null; - return this.generateUserProfile(communityMember); - })} - + ); - }} - /> - )} - - {searchIsFocused && - !queryString && ( - - )} + + return ( + + {searchResults.map(communityMember => { + if (!communityMember) return null; + return this.generateUserProfile(communityMember); + })} + + ); + }} + /> + )} + + {searchIsFocused && !queryString && ( + + )} {!searchIsFocused && ( { if (members && members.length > 0) { return ( - {filter && - filter.isBlocked && - !community.isPrivate && ( - - A note about blocked users: Your - community is publicly viewable (except for private - channels). This means that a blocked user may be able - to see the content and conversations in your - community. However, they will be prevented from - creating new conversations, or leaving messages in - existing conversations. - - )} + {filter && filter.isBlocked && !community.isPrivate && ( + + A note about blocked users: Your + community is publicly viewable (except for private + channels). This means that a blocked user may be able to + see the content and conversations in your community. + However, they will be prevented from creating new + conversations, or leaving messages in existing + conversations. + + )} {members.map(communityMember => { if (!communityMember) return null; return this.generateUserProfile(communityMember); })} - {community && - community.members.pageInfo.hasNextPage && ( - - - Load more - - - )} + {community && community.members.pageInfo.hasNextPage && ( + + + Load more + + + )} ); } diff --git a/src/views/communityMembers/components/editDropdown.js b/src/views/communityMembers/components/editDropdown.js index 21683fb2e0..bf07ae3dcf 100644 --- a/src/views/communityMembers/components/editDropdown.js +++ b/src/views/communityMembers/components/editDropdown.js @@ -13,11 +13,11 @@ import { DropdownSectionText, DropdownSectionTitle, DropdownAction, -} from '../../../components/settingsViews/style'; -import Icon from '../../../components/icons'; -import { Spinner } from '../../../components/globals'; -import { initNewThreadWithUser } from '../../../actions/directMessageThreads'; -import OutsideClickHandler from '../../../components/outsideClickHandler'; +} from 'src/components/settingsViews/style'; +import Icon from 'src/components/icon'; +import { Spinner } from 'src/components/globals'; +import InitDirectMessageWrapper from 'src/components/initDirectMessageWrapper'; +import OutsideClickHandler from 'src/components/outsideClickHandler'; import addCommunityModerator from 'shared/graphql/mutations/communityMember/addCommunityModerator'; import removeCommunityModerator from 'shared/graphql/mutations/communityMember/removeCommunityModerator'; import blockCommunityMember from 'shared/graphql/mutations/communityMember/blockCommunityMember'; @@ -105,11 +105,6 @@ class EditDropdown extends React.Component { }, }; - initMessage = () => { - this.props.dispatch(initNewThreadWithUser(this.props.user)); - return this.props.history.push('/messages/new'); - }; - getRolesConfiguration = () => { const { permissions } = this.props; @@ -191,9 +186,11 @@ class EditDropdown extends React.Component { }; toggleOpen = () => this.setState({ isOpen: true }); + close = () => this.setState({ isOpen: false }); render() { + const { user } = this.props; const { isOpen } = this.state; const configuration = this.getRolesConfiguration(); @@ -204,19 +201,21 @@ class EditDropdown extends React.Component { {isOpen && ( - - - - - - - Send Direct Message - - - + + + + + + + Send Direct Message + + + + } + /> diff --git a/src/views/communityMembers/components/getMembers.js b/src/views/communityMembers/components/getMembers.js index 6e168b0c2b..746e28f811 100644 --- a/src/views/communityMembers/components/getMembers.js +++ b/src/views/communityMembers/components/getMembers.js @@ -3,7 +3,7 @@ import * as React from 'react'; import compose from 'recompose/compose'; import viewNetworkHandler, { type ViewNetworkHandlerType, -} from '../../../components/viewNetworkHandler'; +} from 'src/components/viewNetworkHandler'; import getCommunityMembersQuery, { type GetCommunityMembersType, } from 'shared/graphql/queries/community/getCommunityMembers'; @@ -34,6 +34,7 @@ class CommunityMembers extends React.Component { } } -export default compose(getCommunityMembersQuery, viewNetworkHandler)( - CommunityMembers -); +export default compose( + getCommunityMembersQuery, + viewNetworkHandler +)(CommunityMembers); diff --git a/src/views/communityMembers/components/joinTokenToggle.js b/src/views/communityMembers/components/joinTokenToggle.js index d4872e57fc..87a65568b0 100644 --- a/src/views/communityMembers/components/joinTokenToggle.js +++ b/src/views/communityMembers/components/joinTokenToggle.js @@ -5,7 +5,7 @@ import { connect } from 'react-redux'; import compose from 'recompose/compose'; import enableTokenJoinMutation from 'shared/graphql/mutations/community/enableCommunityTokenJoin'; import disableTokenJoinMutation from 'shared/graphql/mutations/community/disableCommunityTokenJoin'; -import { addToastWithTimeout } from '../../../actions/toasts'; +import { addToastWithTimeout } from 'src/actions/toasts'; type Props = { id: string, diff --git a/src/views/communityMembers/components/mutationWrapper.js b/src/views/communityMembers/components/mutationWrapper.js index 1b1cdb01e0..d811b3b04d 100644 --- a/src/views/communityMembers/components/mutationWrapper.js +++ b/src/views/communityMembers/components/mutationWrapper.js @@ -1,7 +1,7 @@ // @flow import * as React from 'react'; import { connect } from 'react-redux'; -import { addToastWithTimeout } from '../../../actions/toasts'; +import { addToastWithTimeout } from 'src/actions/toasts'; import type { Dispatch } from 'redux'; type Props = { diff --git a/src/views/communityMembers/components/resetJoinToken.js b/src/views/communityMembers/components/resetJoinToken.js index d34df1faf4..3b4551c7b0 100644 --- a/src/views/communityMembers/components/resetJoinToken.js +++ b/src/views/communityMembers/components/resetJoinToken.js @@ -4,7 +4,7 @@ import { connect } from 'react-redux'; import compose from 'recompose/compose'; import resetJoinTokenMutation from 'shared/graphql/mutations/community/resetCommunityJoinToken'; import { addToastWithTimeout } from 'src/actions/toasts'; -import { OutlineButton } from 'src/components/buttons'; +import { OutlineButton } from 'src/components/button'; type Props = { id: string, @@ -56,7 +56,7 @@ class ResetJoinToken extends React.Component { Reset this link @@ -65,4 +65,7 @@ class ResetJoinToken extends React.Component { } } -export default compose(connect(), resetJoinTokenMutation)(ResetJoinToken); +export default compose( + connect(), + resetJoinTokenMutation +)(ResetJoinToken); diff --git a/src/views/communityMembers/components/search.js b/src/views/communityMembers/components/search.js index 41d008f6cd..a1a9121d84 100644 --- a/src/views/communityMembers/components/search.js +++ b/src/views/communityMembers/components/search.js @@ -5,7 +5,7 @@ import searchCommunityMembers from 'shared/graphql/queries/search/searchCommunit import type { SearchCommunityMembersType } from 'shared/graphql/queries/search/searchCommunityMembers'; import viewNetworkHandler, { type ViewNetworkHandlerType, -} from '../../../components/viewNetworkHandler'; +} from 'src/components/viewNetworkHandler'; type Props = { data: { @@ -26,4 +26,7 @@ class Search extends React.Component { } } -export default compose(searchCommunityMembers, viewNetworkHandler)(Search); +export default compose( + searchCommunityMembers, + viewNetworkHandler +)(Search); diff --git a/src/views/communityMembers/index.js b/src/views/communityMembers/index.js index 6c6b77838b..42e4f6dd6e 100644 --- a/src/views/communityMembers/index.js +++ b/src/views/communityMembers/index.js @@ -2,10 +2,7 @@ import * as React from 'react'; import compose from 'recompose/compose'; import { connect } from 'react-redux'; -import { Link } from 'react-router-dom'; import type { GetCommunityType } from 'shared/graphql/queries/community/getCommunity'; -import ViewError from 'src/components/viewError'; -import { Button, OutlineButton, ButtonRow } from 'src/components/buttons'; import { CommunityInvitationForm } from 'src/components/emailInvitationForm'; import SlackConnection from '../communitySettings/components/slack'; import CommunityMembers from './components/communityMembers'; @@ -19,6 +16,7 @@ import { Column, } from 'src/components/settingsViews/style'; import { ErrorBoundary, SettingsFallback } from 'src/components/error'; +import { ErrorView } from 'src/views/viewHelpers'; type Props = { currentUser: Object, @@ -67,24 +65,7 @@ class CommunityMembersSettings extends React.Component { ); } - return ( - - - - Take me back - - - - - - - - ); + return ; } } diff --git a/src/views/communityMembers/style.js b/src/views/communityMembers/style.js index 4f9b1af3a8..af8afd0ef3 100644 --- a/src/views/communityMembers/style.js +++ b/src/views/communityMembers/style.js @@ -1,7 +1,8 @@ // @flow import theme from 'shared/theme'; import styled from 'styled-components'; -import { TextButton } from '../../components/buttons'; +import { TextButton } from 'src/components/button'; +import { MEDIA_BREAK } from 'src/components/layout'; export const Heading = styled.h1` margin-left: 16px; @@ -151,7 +152,7 @@ export const SearchFilter = styled(Filter)` background: none; } - @media (max-width: 768px) { + @media (max-width: ${MEDIA_BREAK}px) { display: none; } `; @@ -213,3 +214,13 @@ export const TokenInputWrapper = styled.div` } } `; + +export const Row = styled.div` + display: flex; + align-items: flex-start; + + a { + display: flex; + flex: 1 1 auto; + } +`; diff --git a/src/views/communityMembers/utils.js b/src/views/communityMembers/utils.js index c54c576b39..fd214148e0 100644 --- a/src/views/communityMembers/utils.js +++ b/src/views/communityMembers/utils.js @@ -3,16 +3,9 @@ import React from 'react'; import { SectionSubtitle, GrowthText, -} from '../../components/settingsViews/style'; +} from 'src/components/settingsViews/style'; -export const parseGrowth = ( - { - growth, - currentPeriodCount, - prevPeriodCount, - }: { growth: number, currentPeriodCount: number, prevPeriodCount: number }, - range: string -) => { +export const parseGrowth = ({ growth }: { growth: number }, range: string) => { if (!growth) { return null; } else if (growth > 0) { diff --git a/src/views/communitySettings/components/brandedLogin.js b/src/views/communitySettings/components/brandedLogin.js index 9ce73dc91f..1ed6bf7f6b 100644 --- a/src/views/communitySettings/components/brandedLogin.js +++ b/src/views/communitySettings/components/brandedLogin.js @@ -17,11 +17,10 @@ import { SectionCardFooter, } from 'src/components/settingsViews/style'; import BrandedLoginToggle from './brandedLoginToggle'; -import { Link } from 'react-router-dom'; -import { Button, OutlineButton } from 'src/components/buttons'; +import { TextButton, OutlineButton } from 'src/components/button'; import { TextArea, Error } from 'src/components/formElements'; import saveBrandedLoginSettings from 'shared/graphql/mutations/community/saveBrandedLoginSettings'; -import { addToastWithTimeout } from '../../../actions/toasts'; +import { addToastWithTimeout } from 'src/actions/toasts'; import type { Dispatch } from 'redux'; type Props = { @@ -133,7 +132,7 @@ class BrandedLogin extends React.Component { justifyContent: 'flex-start', }} > - + - - - Preview - - + Preview + )} diff --git a/src/views/communitySettings/components/brandedLoginToggle.js b/src/views/communitySettings/components/brandedLoginToggle.js index 91a642442e..b330e7bf1b 100644 --- a/src/views/communitySettings/components/brandedLoginToggle.js +++ b/src/views/communitySettings/components/brandedLoginToggle.js @@ -5,7 +5,7 @@ import { connect } from 'react-redux'; import compose from 'recompose/compose'; import enableBrandedLoginMutation from 'shared/graphql/mutations/community/enableBrandedLogin'; import disableBrandedLoginMutation from 'shared/graphql/mutations/community/disableBrandedLogin'; -import { addToastWithTimeout } from '../../../actions/toasts'; +import { addToastWithTimeout } from 'src/actions/toasts'; import type { Dispatch } from 'redux'; type Props = { diff --git a/src/views/communitySettings/components/channelList.js b/src/views/communitySettings/components/channelList.js index 3e69c3522d..169e0a9409 100644 --- a/src/views/communitySettings/components/channelList.js +++ b/src/views/communitySettings/components/channelList.js @@ -3,11 +3,13 @@ import React from 'react'; import { Link } from 'react-router-dom'; import { connect } from 'react-redux'; import compose from 'recompose/compose'; -import { openModal } from '../../../actions/modals'; -import { Loading } from '../../../components/loading'; -import { IconButton, Button } from '../../../components/buttons'; -import viewNetworkHandler from '../../../components/viewNetworkHandler'; -import ViewError from '../../../components/viewError'; +import { openModal } from 'src/actions/modals'; +import { Loading } from 'src/components/loading'; +import { OutlineButton } from 'src/components/button'; +import Icon from 'src/components/icon'; +import viewNetworkHandler from 'src/components/viewNetworkHandler'; +import ViewError from 'src/components/viewError'; +import Tooltip from 'src/components/tooltip'; import getCommunityChannels from 'shared/graphql/queries/community/getCommunityChannelConnection'; import type { GetCommunityChannelConnectionType } from 'shared/graphql/queries/community/getCommunityChannelConnection'; import type { Dispatch } from 'redux'; @@ -16,7 +18,7 @@ import { SectionCard, SectionTitle, SectionCardFooter, -} from '../../../components/settingsViews/style'; +} from 'src/components/settingsViews/style'; import { ChannelListItem } from 'src/components/listItems'; type Props = { @@ -52,11 +54,11 @@ class ChannelList extends React.Component { - + + + + + ); @@ -64,7 +66,7 @@ class ChannelList extends React.Component { - + ); diff --git a/src/views/communitySettings/components/editForm.js b/src/views/communitySettings/components/editForm.js index 727d792a6c..0c2a2c2a78 100644 --- a/src/views/communitySettings/components/editForm.js +++ b/src/views/communitySettings/components/editForm.js @@ -7,10 +7,11 @@ import editCommunityMutation from 'shared/graphql/mutations/community/editCommun import type { EditCommunityType } from 'shared/graphql/mutations/community/editCommunity'; import type { GetCommunityType } from 'shared/graphql/queries/community/getCommunity'; import { openModal } from 'src/actions/modals'; +import Tooltip from 'src/components/tooltip'; import { addToastWithTimeout } from 'src/actions/toasts'; -import { Button, IconButton } from 'src/components/buttons'; +import { PrimaryOutlineButton } from 'src/components/button'; import { Notice } from 'src/components/listItems/style'; -import Icon from 'src/components/icons'; +import Icon from 'src/components/icon'; import { Input, UnderlineInput, @@ -247,9 +248,7 @@ class EditForm extends React.Component {

{' '}

{communityData.metaData.members} members will be removed from - the community and the{' '} - {communityData.metaData.channels} channels you’ve created will - be deleted. + the community and the channels you’ve created will be deleted.

All threads, messages, reactions, and media shared in your community @@ -297,7 +296,7 @@ class EditForm extends React.Component { This community doesn’t exist yet. Want to make it? - + Create ); @@ -361,25 +360,29 @@ class EditForm extends React.Component { - + {community.communityPermissions.isOwner && ( - this.triggerDeleteCommunity(e, community.id)} - /> + + + + this.triggerDeleteCommunity(e, community.id) + } + /> + + )} diff --git a/src/views/communitySettings/components/slack/channelConnection.js b/src/views/communitySettings/components/slack/channelConnection.js index a0d0240273..8dd2e69eaa 100644 --- a/src/views/communitySettings/components/slack/channelConnection.js +++ b/src/views/communitySettings/components/slack/channelConnection.js @@ -18,7 +18,7 @@ import { Loading } from 'src/components/loading'; import ViewError from 'src/components/viewError'; import ChannelSlackManager from './channelSlackManager'; import { ChannelListContainer } from './style'; -import Icon from 'src/components/icons'; +import Icon from 'src/components/icon'; import type { Dispatch } from 'redux'; type Props = { diff --git a/src/views/communitySettings/components/slack/channelSlackManager.js b/src/views/communitySettings/components/slack/channelSlackManager.js index ea6d5cca7f..7495e70380 100644 --- a/src/views/communitySettings/components/slack/channelSlackManager.js +++ b/src/views/communitySettings/components/slack/channelSlackManager.js @@ -3,13 +3,8 @@ import * as React from 'react'; import compose from 'recompose/compose'; import { connect } from 'react-redux'; import type { ChannelInfoType } from 'shared/graphql/fragments/channel/channelInfo'; -import { - SlackChannelRow, - ChannelName, - StyledSelect, - Select, - SendsTo, -} from './style'; +import { SlackChannelRow, ChannelName, SendsTo } from './style'; +import Select from 'src/components/select'; import { addToastWithTimeout } from 'src/actions/toasts'; import updateChannelSlackBotLinksMutation from 'shared/graphql/mutations/channel/updateChannelSlackBotLinks'; import type { Dispatch } from 'redux'; @@ -63,24 +58,23 @@ class ChannelSlackManager extends React.Component { {channel.name} send notifications to → - - - + ); } } -export default compose(connect(), updateChannelSlackBotLinksMutation)( - ChannelSlackManager -); +export default compose( + connect(), + updateChannelSlackBotLinksMutation +)(ChannelSlackManager); diff --git a/src/views/communitySettings/components/slack/connectSlack.js b/src/views/communitySettings/components/slack/connectSlack.js index bfd755553d..7d1aa9672e 100644 --- a/src/views/communitySettings/components/slack/connectSlack.js +++ b/src/views/communitySettings/components/slack/connectSlack.js @@ -7,8 +7,8 @@ import { SectionSubtitle, SectionCardFooter, } from 'src/components/settingsViews/style'; -import { Button } from 'src/components/buttons'; -import Icon from 'src/components/icons'; +import { OutlineButton } from 'src/components/button'; +import Icon from 'src/components/icon'; type Props = { community: GetSlackSettingsType, @@ -48,7 +48,7 @@ class ImportSlackTeam extends React.Component { - + Connect a Slack team diff --git a/src/views/communitySettings/components/slack/sendInvitations.js b/src/views/communitySettings/components/slack/sendInvitations.js index 82df55ddd5..85a417ff8d 100644 --- a/src/views/communitySettings/components/slack/sendInvitations.js +++ b/src/views/communitySettings/components/slack/sendInvitations.js @@ -9,12 +9,12 @@ import { SectionSubtitle, SectionCardFooter, } from 'src/components/settingsViews/style'; -import { Button } from 'src/components/buttons'; +import { Button } from 'src/components/button'; import { TextArea, Error } from 'src/components/formElements'; import sendSlackInvitesMutation from 'shared/graphql/mutations/community/sendSlackInvites'; import { addToastWithTimeout } from 'src/actions/toasts'; import { timeDifference } from 'shared/time-difference'; -import Icon from 'src/components/icons'; +import Icon from 'src/components/icon'; import type { Dispatch } from 'redux'; type Props = { @@ -45,7 +45,7 @@ class SendSlackInvitations extends React.Component { id: community.id, customMessage, }) - .then(result => { + .then(() => { this.setState({ isLoading: false, }); @@ -120,7 +120,7 @@ class SendSlackInvitations extends React.Component { @@ -130,6 +130,7 @@ class SendSlackInvitations extends React.Component { } } -export default compose(connect(), sendSlackInvitesMutation)( - SendSlackInvitations -); +export default compose( + connect(), + sendSlackInvitesMutation +)(SendSlackInvitations); diff --git a/src/views/communitySettings/components/watercooler.js b/src/views/communitySettings/components/watercooler.js index 0fc82496ec..ef0e8843df 100644 --- a/src/views/communitySettings/components/watercooler.js +++ b/src/views/communitySettings/components/watercooler.js @@ -18,11 +18,9 @@ import { SectionCardFooter, } from 'src/components/settingsViews/style'; import { Link } from 'react-router-dom'; -import { Button, OutlineButton } from 'src/components/buttons'; -import { TextArea, Error } from 'src/components/formElements'; +import { TextButton, OutlineButton } from 'src/components/button'; import enableCommunityWatercooler from 'shared/graphql/mutations/community/enableCommunityWatercooler'; import disableCommunityWatercooler from 'shared/graphql/mutations/community/disableCommunityWatercooler'; -import getThreadLink from 'src/helpers/get-thread-link'; import { addToastWithTimeout } from 'src/actions/toasts'; import type { Dispatch } from 'redux'; import type { History } from 'react-router'; @@ -53,7 +51,7 @@ const Watercooler = (props: Props) => { .enableCommunityWatercooler({ id: community.id, }) - .then(({ data }) => { + .then(() => { setSaving(false); dispatch(addToastWithTimeout('success', 'Open chat enabled!')); }); @@ -65,7 +63,7 @@ const Watercooler = (props: Props) => { .disableCommunityWatercooler({ id: community.id, }) - .then(({ data }) => { + .then(() => { dispatch(addToastWithTimeout('neutral', 'Open chat disabled.')); setSaving(false); }); @@ -82,18 +80,18 @@ const Watercooler = (props: Props) => { {community && community.watercoolerId && ( - Go to open chat + Go to open chat )} - + ); diff --git a/src/views/communitySettings/index.js b/src/views/communitySettings/index.js index 0b80990e10..caf71d7738 100644 --- a/src/views/communitySettings/index.js +++ b/src/views/communitySettings/index.js @@ -5,21 +5,19 @@ import { connect } from 'react-redux'; import { Route, Switch } from 'react-router-dom'; import { getCommunitySettingsByMatch } from 'shared/graphql/queries/community/getCommunitySettings'; import type { GetCommunityType } from 'shared/graphql/queries/community/getCommunity'; -import { Loading } from '../../components/loading'; -import AppViewWrapper from '../../components/appViewWrapper'; -import { Upsell404Community } from '../../components/upsell'; -import viewNetworkHandler from '../../components/viewNetworkHandler'; -import Head from '../../components/head'; -import ViewError from '../../components/viewError'; -import Analytics from '../communityAnalytics'; -import Members from '../communityMembers'; -import Overview from './components/overview'; -import Titlebar from '../titlebar'; -import Header from '../../components/settingsViews/header'; -import Subnav from '../../components/settingsViews/subnav'; +import viewNetworkHandler from 'src/components/viewNetworkHandler'; +import Head from 'src/components/head'; +import Header from 'src/components/settingsViews/header'; +import { SegmentedControl, Segment } from 'src/components/segmentedControl'; import { View } from './style'; import type { ContextRouter } from 'react-router'; import { track, events, transformations } from 'src/helpers/analytics'; +import { ErrorView, LoadingView } from 'src/views/viewHelpers'; +import { ViewGrid } from 'src/components/layout'; +import { setTitlebarProps } from 'src/actions/titlebar'; +import Analytics from '../communityAnalytics'; +import Members from '../communityMembers'; +import Overview from './components/overview'; type Props = { data: { @@ -31,6 +29,15 @@ type Props = { }; class CommunitySettings extends React.Component { + componentDidMount() { + const { dispatch } = this.props; + dispatch( + setTitlebarProps({ + title: 'Settings', + }) + ); + } + componentDidUpdate(prevProps) { if (!prevProps.data.community && this.props.data.community) { const { community } = this.props.data; @@ -46,7 +53,6 @@ class CommunitySettings extends React.Component { location, match, isLoading, - hasError, history, } = this.props; @@ -62,25 +68,7 @@ class CommunitySettings extends React.Component { community.communityPermissions.isModerator; if (!canViewCommunitySettings) { - return ( - - - - - - - - ); + return ; } const subnavItems = [ @@ -121,83 +109,56 @@ class CommunitySettings extends React.Component { title += ' Settings'; } return ( - - + - -

- - - - - {() => } - - - {() => } - - - {() => ( - - )} - - - - + + +
+ + + {subnavItems.map(item => ( + + {item.label} + + ))} + + + + + {() => } + + + {() => } + + + {() => ( + + )} + + + + + ); } if (isLoading) { - return ; + return ; } - if (hasError) { - return ( - - - - - ); - } - - return ( - - - - - - - ); + return ; } } diff --git a/src/views/communitySettings/style.js b/src/views/communitySettings/style.js index eb1c3aea90..d76b872f1a 100644 --- a/src/views/communitySettings/style.js +++ b/src/views/communitySettings/style.js @@ -1,8 +1,10 @@ +// @flow import styled from 'styled-components'; import theme from 'shared/theme'; -import Card from '../../components/card'; +import Card from 'src/components/card'; import { Link } from 'react-router-dom'; -import { FlexCol, H1, H2, H3, Span, Tooltip } from '../../components/globals'; +import { FlexCol, H1, H2, H3, Span } from 'src/components/globals'; +import { MEDIA_BREAK } from 'src/components/layout'; export const ListHeader = styled.div` display: flex; @@ -74,7 +76,7 @@ export const EmailInviteInput = styled.input` border: 2px solid ${theme.brand.default}; } - @media screen and (max-width: 768px) { + @media screen and (max-width: ${MEDIA_BREAK}px) { display: none; } `; @@ -205,7 +207,7 @@ export const View = styled.div` flex: 1; align-self: stretch; - @media (max-width: 768px) { + @media (max-width: ${MEDIA_BREAK}px) { width: 100%; } `; @@ -225,5 +227,4 @@ export const GrowthText = styled.h5` export const MessageIcon = styled.div` color: ${theme.brand.alt}; cursor: pointer; - ${Tooltip} top: 2px; `; diff --git a/src/views/dashboard/components/communityList.js b/src/views/dashboard/components/communityList.js deleted file mode 100644 index 4b10cc0be1..0000000000 --- a/src/views/dashboard/components/communityList.js +++ /dev/null @@ -1,185 +0,0 @@ -// @flow -import * as React from 'react'; -import { connect } from 'react-redux'; -import { withRouter } from 'react-router'; -import { CommunityAvatar } from 'src/components/avatar'; -import compose from 'recompose/compose'; -import { Link } from 'react-router-dom'; -import Icon from 'src/components/icons'; -import Reputation from 'src/components/reputation'; -import SidebarChannels from './sidebarChannels'; -import UpsellExploreCommunities from './upsellExploreCommunities'; -import { getItemFromStorage } from 'src/helpers/localStorage'; -import { - CommunityListItem, - CommunityListMeta, - CommunityListName, - CommunityAvatarContainer, - CommunityListScroller, - CommunityListWrapper, - Fixed, -} from '../style'; -import { - changeActiveCommunity, - changeActiveThread, - changeActiveChannel, -} from 'src/actions/dashboardFeed'; -import type { GetCommunityType } from 'shared/graphql/queries/community/getCommunity'; -import { track, events } from 'src/helpers/analytics'; -import type { Dispatch } from 'redux'; -import { ErrorBoundary } from 'src/components/error'; - -type Props = { - dispatch: Dispatch, - history: Object, - activeCommunity: ?string, - activeChannel: ?string, - communities: Array, - setActiveChannelObject: Function, -}; - -export const LAST_ACTIVE_COMMUNITY_KEY = 'last-active-inbox-community'; - -class CommunityList extends React.Component { - componentDidMount() { - const id = getItemFromStorage(LAST_ACTIVE_COMMUNITY_KEY); - if (id) return this.props.dispatch(changeActiveCommunity(id)); - this.props.dispatch(changeActiveCommunity('')); - } - - changeCommunity = id => { - track(events.INBOX_COMMUNITY_FILTERED); - this.props.dispatch(changeActiveCommunity(id)); - this.props.history.replace('/'); - this.props.dispatch(changeActiveThread('')); - - if (id !== this.props.activeCommunity) { - this.props.dispatch(changeActiveChannel('')); - } - }; - - clearActiveChannel = () => { - this.props.dispatch(changeActiveThread('')); - this.props.dispatch(changeActiveChannel('')); - }; - - handleOnClick = id => { - this.clearActiveChannel(); - if (this.props.activeCommunity !== id) { - this.changeCommunity(id); - } - }; - - shouldComponentUpdate(nextProps) { - const curr = this.props; - - const changedActiveCommunity = - curr.activeCommunity !== nextProps.activeCommunity; - const changedActiveChannel = curr.activeChannel !== nextProps.activeChannel; - const changedCommunitiesAmount = - curr.communities.length !== nextProps.communities.length; - - if ( - changedActiveCommunity || - changedActiveChannel || - changedCommunitiesAmount - ) { - return true; - } - return false; - } - - render() { - const { activeCommunity, activeChannel, communities } = this.props; - const sortedCommunities = communities.slice().sort((a, b) => { - const bc = parseInt(b.communityPermissions.reputation, 10); - const ac = parseInt(a.communityPermissions.reputation, 10); - - // sort same-reputation communities alphabetically - if (ac === bc) { - return a.name.toUpperCase() <= b.name.toUpperCase() ? -1 : 1; - } - - // otherwise sort by reputation - return bc <= ac ? -1 : 1; - }); - - return ( - - - this.changeCommunity('')} - > - - Everything - - {sortedCommunities.map(c => ( - - this.handleOnClick(c.id)} - active={c.id === activeCommunity} - > - - - - - {c.name} - - - - {c.id === activeCommunity && ( - - - - )} - - - ))} - - - - events.INBOX_FIND_MORE_COMMUNITIES_CLICKED} - > - - - Find more communities - - - {// if user has joined less than 5 communities, upsell some popular ones - communities.length < 5 && ( - - - - )} - - - ); - } -} - -export default compose( - connect(), - withRouter -)(CommunityList); diff --git a/src/views/dashboard/components/dashboardError.js b/src/views/dashboard/components/dashboardError.js deleted file mode 100644 index eeb7cfc6aa..0000000000 --- a/src/views/dashboard/components/dashboardError.js +++ /dev/null @@ -1,20 +0,0 @@ -import React, { Component } from 'react'; -import AppViewWrapper from '../../../components/appViewWrapper'; -import Head from '../../../components/head'; -import Titlebar from '../../../views/titlebar'; -import ViewError from '../../../components/viewError'; - -class DashboardError extends Component { - render() { - const { title, description } = this.props; - return ( - - - - - - ); - } -} - -export default DashboardError; diff --git a/src/views/dashboard/components/dashboardLoading.js b/src/views/dashboard/components/dashboardLoading.js deleted file mode 100644 index 7fb39f28de..0000000000 --- a/src/views/dashboard/components/dashboardLoading.js +++ /dev/null @@ -1,15 +0,0 @@ -import React, { Component } from 'react'; -import AppViewWrapper from '../../../components/appViewWrapper'; -import { Spinner } from '../../../components/globals'; - -class DashboardLoading extends Component { - render() { - return ( - - - - ); - } -} - -export default DashboardLoading; diff --git a/src/views/dashboard/components/emptySearchFeed.js b/src/views/dashboard/components/emptySearchFeed.js deleted file mode 100644 index 7eeee23137..0000000000 --- a/src/views/dashboard/components/emptySearchFeed.js +++ /dev/null @@ -1,17 +0,0 @@ -// @flow -import React from 'react'; -import { connect } from 'react-redux'; -import { Link } from 'react-router-dom'; -import { Button } from 'src/components/buttons'; -import { NullThreadFeed, NullHeading } from '../style'; - -const EmptySearchFeed = ({ dispatch, queryString }) => ( - - We couldn't find any results for "{queryString}" - - - - -); - -export default connect()(EmptySearchFeed); diff --git a/src/views/dashboard/components/emptyThreadFeed.js b/src/views/dashboard/components/emptyThreadFeed.js deleted file mode 100644 index 4dc167c0d1..0000000000 --- a/src/views/dashboard/components/emptyThreadFeed.js +++ /dev/null @@ -1,35 +0,0 @@ -// @flow -import React from 'react'; -import { connect } from 'react-redux'; -import { Link } from 'react-router-dom'; -import Icon from 'src/components/icons'; -import { Button } from 'src/components/buttons'; -import getComposerLink from 'src/helpers/get-composer-link'; -import { NullThreadFeed, NullHeading, OutlineButton, Hint } from '../style'; - -const EmptyThreadFeed = ({ dispatch, communityId, channelId }) => { - const { pathname, search } = getComposerLink({ communityId, channelId }); - - return ( - - - Your feed's a little quiet right now, but don't worry... - - We've got recommendations! - Kick your community off right! - {/* dispatch activethread to 'new'? */} - - - - Find new friends and great conversations! - - - - Join more communities - - - - ); -}; - -export default connect()(EmptyThreadFeed); diff --git a/src/views/dashboard/components/errorThreadFeed.js b/src/views/dashboard/components/errorThreadFeed.js deleted file mode 100644 index ac37ecc699..0000000000 --- a/src/views/dashboard/components/errorThreadFeed.js +++ /dev/null @@ -1,17 +0,0 @@ -import React from 'react'; -// $FlowFixMe -import { Link } from 'react-router-dom'; -import { Button } from '../../../components/buttons'; -import { NullThreadFeed, NullHeading } from '../style'; - -export default props => ( - - - There was a problem loading this feed. Please try refreshing the page. - - - - -); diff --git a/src/views/dashboard/components/inboxThread/messageCount.js b/src/views/dashboard/components/inboxThread/messageCount.js deleted file mode 100644 index f2bdeb914b..0000000000 --- a/src/views/dashboard/components/inboxThread/messageCount.js +++ /dev/null @@ -1,38 +0,0 @@ -// @flow -import * as React from 'react'; -import type { GetThreadType } from 'shared/graphql/queries/thread/getThread'; -import Icon from 'src/components/icons'; -import { CountWrapper, NewCount } from './style'; - -type Props = { - currentUser: ?Object, - thread: GetThreadType, - active: boolean, -}; - -class MessageCount extends React.Component { - render() { - const { - thread: { messageCount, currentUserLastSeen, lastActive }, - active, - } = this.props; - - const newMessagesSinceLastViewed = - currentUserLastSeen && lastActive && currentUserLastSeen < lastActive; - - return ( - - - {messageCount} - {newMessagesSinceLastViewed && - !active && (New)} - - ); - } -} - -export default MessageCount; diff --git a/src/views/dashboard/components/loadingThreadFeed.js b/src/views/dashboard/components/loadingThreadFeed.js deleted file mode 100644 index ea4ded2a54..0000000000 --- a/src/views/dashboard/components/loadingThreadFeed.js +++ /dev/null @@ -1,17 +0,0 @@ -import React from 'react'; -import { LoadingInboxThread } from '../../../components/loading'; - -export default props => ( -
- - - - - - - - - - -
-); diff --git a/src/views/dashboard/components/newActivityIndicator.js b/src/views/dashboard/components/newActivityIndicator.js deleted file mode 100644 index d89a6faed7..0000000000 --- a/src/views/dashboard/components/newActivityIndicator.js +++ /dev/null @@ -1,118 +0,0 @@ -import React, { Component } from 'react'; -import theme from 'shared/theme'; -import { connect } from 'react-redux'; -import { clearActivityIndicator } from '../../../actions/newActivityIndicator'; -import styled from 'styled-components'; - -const NewActivityBar = styled.div` - padding: ${props => (props.refetching ? '8px' : '8px 16px')}; - color: ${theme.brand.alt}; - background: ${theme.bg.wash}; - font-size: 14px; - font-weight: 600; - display: ${props => (props.active ? 'flex' : 'none')}; - align-items: center; - justify-content: center; - align-self: center; - padding: 12px 16px; - height: 40px; - border-top: 1px solid ${theme.bg.border}; - border-bottom: 1px solid ${theme.bg.border}; - z-index: 10; - position: relative; - cursor: pointer; - width: 100%; - text-align: center; - line-height: 1; - display: block; - - @media (max-width: 768px) { - padding: 20px 16px; - height: 56px; - } - - &:hover { - background: ${theme.brand.alt}; - color: ${theme.text.reverse}; - } -`; - -const scrollTo = (element, to, duration) => { - if (duration < 0) return; - const difference = to - element.scrollTop; - const perTick = (difference / duration) * 2; - - setTimeout(() => { - element.scrollTop = element.scrollTop + perTick; - scrollTo(element, to, duration - 2); - }, 10); -}; - -class Indicator extends Component { - state: { - elem: any, - }; - - constructor() { - super(); - - this.state = { - elem: null, - }; - } - - componentDidUpdate(prevProps) { - if (prevProps !== this.props) { - this.setState({ - // NOTE(@mxstbr): This is super un-reacty but it works. This refers to - // the AppViewWrapper which is the scrolling part of the site. - elem: document.getElementById(this.props.elem), - }); - } - } - - componentDidMount() { - const elem = document.getElementById(this.props.elem); - this.setState({ - // NOTE(@mxstbr): This is super un-reacty but it works. This refers to - // the AppViewWrapper which is the scrolling part of the site. - elem, - }); - - // if the component mounted while the user is scrolled to the top, immediately clear the redux store of the activity indicator - since the user can see the top of the feed, they don't need an indicator - if (elem.scrollTop < window.innerHeight / 4) { - this.props.dispatch(clearActivityIndicator()); - } - } - - componentWillUnmount() { - // when the component unmounts, clear the state so that at next mount we will always get a new scrollTop position for the scroll element - this.setState({ - elem: null, - }); - } - - clearActivityIndicator = () => { - // if the user clicks on the new activity indicator, scroll them to the top of the feed and dismiss the indicator - setTimeout(() => this.props.dispatch(clearActivityIndicator()), 120); - scrollTo(this.state.elem, 0, 80); - }; - - render() { - const { elem } = this.state; - let active = false; - - // if the scroll element exists, and the user has scrolled at least half of the screen (e.g. the top of the feed is out of view), then the user should see a new activity indicator - if (elem) { - active = elem.scrollTop > window.innerHeight / 2; - } - - return ( - - New conversations! - - ); - } -} - -export default connect()(Indicator); diff --git a/src/views/dashboard/components/sidebarChannels.js b/src/views/dashboard/components/sidebarChannels.js deleted file mode 100644 index b242851eda..0000000000 --- a/src/views/dashboard/components/sidebarChannels.js +++ /dev/null @@ -1,221 +0,0 @@ -// @flow -import * as React from 'react'; -import { Link } from 'react-router-dom'; -import getCommunityChannels from 'shared/graphql/queries/community/getCommunityChannelConnection'; -import type { GetCommunityChannelConnectionType } from 'shared/graphql/queries/community/getCommunityChannelConnection'; -import { connect } from 'react-redux'; -import Icon from '../../../components/icons'; -import { - changeActiveChannel, - changeActiveThread, -} from '../../../actions/dashboardFeed'; -import viewNetworkHandler from '../../../components/viewNetworkHandler'; -import { sortByTitle } from '../../../helpers/utils'; -import compose from 'recompose/compose'; -import { - CommunityListName, - ChannelsContainer, - ChannelListItem, - LoadingContainer, - LoadingBar, - SectionTitle, - PendingBadge, -} from '../style'; -import GetMembers from 'src/views/communityMembers/components/getMembers'; -import { track, events } from 'src/helpers/analytics'; -import type { Dispatch } from 'redux'; -import { ErrorBoundary } from 'src/components/error'; - -type Props = { - dispatch: Dispatch, - isLoading: boolean, - queryVarIsChanging: boolean, - activeChannel: ?string, - setActiveChannelObject: Function, - permissions: { - isOwner: boolean, - isModerator: boolean, - }, - slug: string, - data: { - community: GetCommunityChannelConnectionType, - }, -}; -class SidebarChannels extends React.Component { - changeChannel = id => { - track(events.INBOX_CHANNEL_FILTERED); - this.props.dispatch(changeActiveThread('')); - this.props.dispatch(changeActiveChannel(id)); - }; - - setActiveChannelObject = channel => { - if (!this.props.setActiveChannelObject || !channel) return; - return this.props.setActiveChannelObject(channel); - }; - - render() { - const { - data: { community }, - isLoading, - queryVarIsChanging, - activeChannel, - permissions, - slug, - } = this.props; - - const isOwner = permissions && permissions.isOwner; - const isModerator = permissions && permissions.isModerator; - - if (community) { - const isOwner = - community.communityPermissions && - community.communityPermissions.isOwner; - const isModerator = - community.communityPermissions && - community.communityPermissions.isModerator; - const channels = community.channelConnection.edges - .map(channel => channel && channel.node) - .filter(channel => { - if (!channel) return null; - if (channel.isPrivate && !channel.channelPermissions.isMember) { - return null; - } - - return channel; - }) - .filter(channel => { - if (!channel) return null; - if (channel.isPrivate && channel.isArchived) { - return null; - } - return channel; - }) - .filter(channel => channel && channel.channelPermissions.isMember) - .filter(channel => channel && !channel.channelPermissions.isBlocked) - .filter(channel => channel && !channel.isArchived); - - const sortedChannels = sortByTitle(channels); - - return ( - - - - - View community home - - - - {(isOwner || isModerator) && ( - - - - Settings - - - )} - - {community.isPrivate && ( - { - const members = - community && - community.members && - community.members.edges.map(member => member && member.node); - - if (members && members.length > 0) { - return ( - - - {members.length} - Pending requests - - - ); - } - - return null; - }} - /> - )} - - {(isOwner || isModerator) && ( - - - - Analytics - - - )} - - {sortedChannels && sortedChannels.length > 1 && ( - Filter by Channel - )} - {sortedChannels && - sortedChannels.length > 1 && - sortedChannels.map(channel => { - return ( - - { - evt.stopPropagation(); - this.changeChannel(channel.id); - this.setActiveChannelObject(channel); - }} - > - {channel.isPrivate ? ( - - ) : ( - - )} - - {channel.name} - - - ); - })} - - ); - } - - if (isLoading || queryVarIsChanging) { - return ( - - - - - Visit community - - - - {(isOwner || isModerator) && ( - - - - Settings - - - )} - - - - - - - ); - } - - return null; - } -} - -export default compose( - connect(), - getCommunityChannels, - viewNetworkHandler -)(SidebarChannels); diff --git a/src/views/dashboard/components/threadFeed.js b/src/views/dashboard/components/threadFeed.js deleted file mode 100644 index 3a11321da4..0000000000 --- a/src/views/dashboard/components/threadFeed.js +++ /dev/null @@ -1,361 +0,0 @@ -// @flow -import * as React from 'react'; -import compose from 'recompose/compose'; -import { withRouter, type History, type Location } from 'react-router'; -import { connect } from 'react-redux'; -// NOTE(@mxstbr): This is a custom fork published of off this (as of this writing) unmerged PR: https://github.com/CassetteRocks/react-infinite-scroller/pull/38 -// I literally took it, renamed the package.json and published to add support for scrollElement since our scrollable container is further outside -import InfiniteList from 'src/components/infiniteScroll'; -import { deduplicateChildren } from 'src/components/infiniteScroll/deduplicateChildren'; -import FlipMove from 'react-flip-move'; -import { sortByDate } from 'src/helpers/utils'; -import { LoadingInboxThread } from 'src/components/loading'; -import { changeActiveThread } from 'src/actions/dashboardFeed'; -import LoadingThreadFeed from './loadingThreadFeed'; -import ErrorThreadFeed from './errorThreadFeed'; -import EmptyThreadFeed from './emptyThreadFeed'; -import EmptySearchFeed from './emptySearchFeed'; -import InboxThread from './inboxThread'; -import DesktopAppUpsell from './desktopAppUpsell'; -import viewNetworkHandler from 'src/components/viewNetworkHandler'; -import type { ViewNetworkHandlerType } from 'src/components/viewNetworkHandler'; -import type { GetThreadType } from 'shared/graphql/queries/thread/getThread'; -import type { GetCommunityThreadConnectionType } from 'shared/graphql/queries/community/getCommunityThreadConnection'; -import type { Dispatch } from 'redux'; -import { ErrorBoundary } from 'src/components/error'; -import { useConnectionRestored } from 'src/hooks/useConnectionRestored'; -import type { WebsocketConnectionType } from 'src/reducers/connectionStatus'; - -type Node = { - node: { - ...$Exact, - }, -}; - -type Props = { - mountedWithActiveThread: ?string, - queryString?: ?string, - ...$Exact, - data: { - subscribeToUpdatedThreads: ?Function, - threads: Array, - fetchMore: Function, - loading: boolean, - community?: GetCommunityThreadConnectionType, - networkStatus: number, - hasNextPage: boolean, - feed: string, - refetch: Function, - }, - history: History, - location: Location, - dispatch: Dispatch, - selectedId: string, - activeCommunity: ?string, - activeChannel: ?string, - hasActiveCommunity: boolean, - networkOnline: boolean, - websocketConnection: WebsocketConnectionType, -}; - -type State = { - scrollElement: any, - subscription: ?Function, -}; - -class ThreadFeed extends React.Component { - innerScrollElement: any; - - constructor() { - super(); - - this.innerScrollElement = null; - - this.state = { - scrollElement: null, - subscription: null, - }; - } - - subscribe = () => { - this.setState({ - subscription: - this.props.data.subscribeToUpdatedThreads && - this.props.data.subscribeToUpdatedThreads(), - }); - }; - - shouldComponentUpdate(nextProps) { - const curr = this.props; - if (curr.networkOnline !== nextProps.networkOnline) return true; - if (curr.websocketConnection !== nextProps.websocketConnection) return true; - // fetching more - if (curr.data.networkStatus === 7 && nextProps.isFetchingMore) return false; - return true; - } - - unsubscribe = () => { - const { subscription } = this.state; - if (subscription) { - // This unsubscribes the subscription - return Promise.resolve(subscription()); - } - }; - - componentDidUpdate(prev) { - const isDesktop = window.innerWidth > 768; - const { scrollElement } = this.state; - const curr = this.props; - const { mountedWithActiveThread, queryString, location } = curr; - - const pathnameIsEmpty = location.pathname === '/'; - - const didReconnect = useConnectionRestored({ curr, prev }); - if (didReconnect && curr.data.refetch) { - curr.data.refetch(); - } - - // user is searching, don't select anything - if (queryString) { - return; - } - - // If we mount with ?t and are on mobile, we have to redirect to the full thread view - if (!isDesktop && mountedWithActiveThread) { - curr.history.replace(`/thread/${mountedWithActiveThread}`); - curr.dispatch({ type: 'REMOVE_MOUNTED_THREAD_ID' }); - return; - } - - const hasThreadsButNoneSelected = curr.data.threads && !curr.selectedId; - const justLoadedThreads = - !mountedWithActiveThread && - ((!prev.data.threads && curr.data.threads) || - (prev.data.loading && !curr.data.loading)); - - // if the app loaded with a ?t query param, it means the user was linked to a thread from the inbox view and is already logged in. In this case we want to load the thread identified in the url and ignore the fact that a feed is loading in which auto-selects a different thread. - if (justLoadedThreads && mountedWithActiveThread) { - curr.dispatch({ type: 'REMOVE_MOUNTED_THREAD_ID' }); - return; - } - - // don't select a thread if the composer is open - if (prev.selectedId === 'new') { - return; - } - - if ( - isDesktop && - (hasThreadsButNoneSelected || justLoadedThreads) && - curr.data.threads.length > 0 && - !prev.isFetchingMore && - pathnameIsEmpty - ) { - if ( - (curr.data.community && - curr.data.community.watercooler && - curr.data.community.watercooler.id) || - (curr.data.community && - curr.data.community.pinnedThread && - curr.data.community.pinnedThread.id) - ) { - const selectId = curr.data.community.watercooler - ? curr.data.community.watercooler.id - : curr.data.community.pinnedThread.id; - - curr.history.replace(`/?t=${selectId}`); - curr.dispatch(changeActiveThread(selectId)); - return; - } - - const threadNodes = curr.data.threads - .slice() - .map(thread => thread && thread.node); - const sortedThreadNodes = sortByDate(threadNodes, 'lastActive', 'desc'); - const hasFirstThread = sortedThreadNodes.length > 0; - const firstThreadId = hasFirstThread ? sortedThreadNodes[0].id : ''; - if (hasFirstThread && pathnameIsEmpty) { - curr.history.replace(`/?t=${firstThreadId}`); - curr.dispatch(changeActiveThread(firstThreadId)); - } - } - - // if the user changes the feed from all to a specific community, we need to reset the active thread in the inbox and reset our subscription for updates - if ( - (!prev.data.feed && curr.data.feed) || - (prev.data.feed && prev.data.feed !== curr.data.feed) - ) { - const threadNodes = curr.data.threads - .slice() - .map(thread => thread && thread.node); - const sortedThreadNodes = sortByDate(threadNodes, 'lastActive', 'desc'); - const hasFirstThread = sortedThreadNodes.length > 0; - const firstThreadId = hasFirstThread ? sortedThreadNodes[0].id : ''; - if (hasFirstThread && pathnameIsEmpty) { - curr.history.replace(`/?t=${firstThreadId}`); - curr.dispatch(changeActiveThread(firstThreadId)); - } - - if (scrollElement) { - scrollElement.scrollTop = 0; - } - - // $FlowFixMe - this.unsubscribe() - .then(() => this.subscribe()) - .catch(err => console.error('Error unsubscribing: ', err)); - } - } - - componentWillUnmount() { - this.unsubscribe(); - } - - componentDidMount() { - const scrollElement = document.getElementById('scroller-for-inbox'); - - this.setState({ - // NOTE(@mxstbr): This is super un-reacty but it works. This refers to - // the AppViewWrapper which is the scrolling part of the site. - scrollElement, - }); - - this.subscribe(); - } - - render() { - const { - data: { threads, community }, - selectedId, - activeCommunity, - activeChannel, - queryString, - isLoading, - hasError, - } = this.props; - const { scrollElement } = this.state; - - if (Array.isArray(threads)) { - // API returned no threads - if (threads.length === 0) { - if (isLoading) { - return ; - } - if (queryString) { - return ; - } else { - return ( - - ); - } - } - - const threadNodes = threads.slice().map(thread => thread && thread.node); - - let sortedThreadNodes = sortByDate(threadNodes, 'lastActive', 'desc'); - - if (activeCommunity) { - sortedThreadNodes = sortedThreadNodes.filter(t => !t.watercooler); - } - - // Filter the watercooler and pinned threads from the feed if we're on the community view - // since they're automatically shown at the top - let filteredThreads = sortedThreadNodes; - if (community) { - if (community.watercooler && community.watercooler.id) { - filteredThreads = filteredThreads.filter( - t => t.id !== community.watercooler.id - ); - } - if (community.pinnedThread && community.pinnedThread.id) { - filteredThreads = filteredThreads.filter( - t => t.id !== community.pinnedThread.id - ); - } - } - - const uniqueThreads = deduplicateChildren(filteredThreads, 'id'); - - let viewContext = 'inbox'; - if (activeCommunity) viewContext = 'communityInbox'; - if (activeChannel) viewContext = 'channelInbox'; - - return ( -
(this.innerScrollElement = el)} - > - - - {community && community.watercooler && community.watercooler.id && ( - - - - )} - - {community && community.pinnedThread && community.pinnedThread.id && ( - - - - )} - } - useWindow={false} - initialLoad={false} - scrollElement={scrollElement} - threshold={750} - className={'scroller-for-dashboard-threads'} - > - - {uniqueThreads.map(thread => { - return ( - - - - ); - })} - - -
- ); - } - - if (isLoading) return ; - - if (hasError) return ; - - return null; - } -} -const map = state => ({ - mountedWithActiveThread: state.dashboardFeed.mountedWithActiveThread, - activeCommunity: state.dashboardFeed.activeCommunity, - activeChannel: state.dashboardFeed.activeChannel, - networkOnline: state.connectionStatus.networkOnline, - websocketConnection: state.connectionStatus.websocketConnection, -}); -export default compose( - withRouter, - // $FlowIssue - connect(map), - viewNetworkHandler -)(ThreadFeed); diff --git a/src/views/dashboard/components/threadSearch.js b/src/views/dashboard/components/threadSearch.js deleted file mode 100644 index 3ccf64fb24..0000000000 --- a/src/views/dashboard/components/threadSearch.js +++ /dev/null @@ -1,122 +0,0 @@ -import * as React from 'react'; -import compose from 'recompose/compose'; -import { connect } from 'react-redux'; -import { SearchInput, SearchForm, SearchInputDiv } from '../style'; -import Icon from '../../../components/icons'; -import type { Dispatch } from 'redux'; -import { - closeSearch, - openSearch, - setSearchStringVariable, -} from '../../../actions/dashboardFeed'; -import { ESC } from 'src/helpers/keycodes'; -type Props = { - dispatch: Dispatch, - filter: { - communityId?: ?string, - channelId?: ?string, - everythingFeed?: ?boolean, - }, -}; -type State = { - value: string, -}; -class ThreadSearch extends React.Component { - constructor(props) { - super(props); - - this.state = { - value: props.queryString ? props.queryString : '', - }; - } - - componentDidMount() { - document.addEventListener('keydown', this.handleKeyPress, false); - } - - componentWillUnmount() { - document.removeEventListener('keydown', this.handleKeyPress, false); - } - - handleKeyPress = (e: any) => { - // escape - if (e.keyCode === ESC) { - if (!this.props.isOpen) return; - this.close(); - } - }; - - submit = e => { - e.preventDefault(); - const searchString = this.state.value.toLowerCase().trim(); - if (searchString.length > 0) { - this.props.dispatch(setSearchStringVariable(searchString)); - } - }; - - open = () => { - this.props.dispatch(openSearch()); - this.searchInput && this.searchInput.focus(); - }; - - close = () => { - if (this.state.value.length === 0) { - this.props.dispatch(closeSearch()); - this.props.dispatch(setSearchStringVariable('')); - } - this.searchInput && this.searchInput.blur(); - }; - - clearClose = () => { - this.setState({ value: '' }); - this.props.dispatch(setSearchStringVariable('')); - this.searchInput.focus(); - }; - - onChange = e => { - this.setState({ value: e.target.value }); - }; - - render() { - const { isOpen, filter, darkContext } = this.props; - const { value } = this.state; - - const placeholder = filter.communityId - ? 'Search this community...' - : filter.channelId - ? 'Search this channel...' - : 'Search for conversations...'; - - return ( - - - - { - this.searchInput = input; - }} - darkContext={darkContext} - /> - - - - ); - } -} - -const map = state => ({ - isOpen: state.dashboardFeed.search.isOpen, - queryString: state.dashboardFeed.search.queryString, -}); -// $FlowIssue -export default compose(connect(map))(ThreadSearch); diff --git a/src/views/dashboard/components/threadSelectorHeader.js b/src/views/dashboard/components/threadSelectorHeader.js deleted file mode 100644 index e35d26a602..0000000000 --- a/src/views/dashboard/components/threadSelectorHeader.js +++ /dev/null @@ -1,132 +0,0 @@ -// @flow -import * as React from 'react'; -import compose from 'recompose/compose'; -import { connect } from 'react-redux'; -import { withRouter } from 'react-router-dom'; -import type { History } from 'react-router'; -import { - HeaderWrapper, - NarrowOnly, - HeaderActiveViewTitle, - HeaderActiveViewSubtitle, - ContextHeaderContainer, -} from '../style'; -import { IconButton } from 'src/components/buttons'; -import ThreadSearch from './threadSearch'; -import Menu from 'src/components/menu'; -import CommunityList from './communityList'; -import { Link } from 'react-router-dom'; -import type { Dispatch } from 'redux'; -import getComposerLink from 'src/helpers/get-composer-link'; - -type Props = { - dispatch: Dispatch, - filter: Object, - communities: Array, - user: Object, - activeCommunity: ?string, - activeChannel: ?string, - activeCommunityObject: ?Object, - activeChannelObject: ?Object, - history: History, -}; - -class Header extends React.Component { - renderContext = () => { - const { - activeCommunity, - activeChannel, - activeCommunityObject, - activeChannelObject, - } = this.props; - - if (activeChannel && activeChannelObject && activeCommunityObject) { - return ( - - - - - {activeCommunityObject.name} - - - - - {activeChannelObject.name} - - - - - ); - } - - if (activeCommunity && activeCommunityObject) { - return ( - - - - {activeCommunityObject.name} - - - - ); - } - - return null; - }; - - render() { - const { - filter, - communities, - user, - activeCommunity, - activeChannel, - } = this.props; - - const { pathname, search } = getComposerLink({ - communityId: activeCommunity, - channelId: activeChannel, - }); - - return ( - - {this.renderContext()} - - - - - - - - - - - - - - ); - } -} - -export default compose( - withRouter, - connect() -)(Header); diff --git a/src/views/dashboard/components/upsellExploreCommunities.js b/src/views/dashboard/components/upsellExploreCommunities.js deleted file mode 100644 index ebb9bf6e42..0000000000 --- a/src/views/dashboard/components/upsellExploreCommunities.js +++ /dev/null @@ -1,137 +0,0 @@ -// @flow -import * as React from 'react'; -import { getCommunitiesByCuratedContentType } from 'shared/graphql/queries/community/getCommunities'; -import type { GetCommunitiesType } from 'shared/graphql/queries/community/getCommunities'; -import type { GetCommunityType } from 'shared/graphql/queries/community/getCommunity'; -import { connect } from 'react-redux'; -import { Link } from 'react-router-dom'; -import compose from 'recompose/compose'; -import viewNetworkHandler from 'src/components/viewNetworkHandler'; -import { CommunityAvatarContainer, UpsellRow } from '../style'; -import { track, events } from 'src/helpers/analytics'; -import { CommunityAvatar } from 'src/components/avatar'; - -const getRandom = (arr, n) => { - let result = new Array(n), - len = arr.length, - taken = new Array(len); - if (n > len) return arr; - while (n--) { - let x = Math.floor(Math.random() * len); - result[n] = arr[x in taken ? taken[x] : x]; - taken[x] = --len; - } - return result; -}; - -type Props = { - data: { - communities: GetCommunitiesType, - }, - communities: Array, - activeCommunity: ?string, -}; - -type State = { - communitiesToJoin: GetCommunitiesType, -}; - -class UpsellExploreCommunities extends React.Component { - state = { - communitiesToJoin: [], - }; - - shouldComponentUpdate(nextProps, nextState) { - if ( - nextState.communitiesToJoin.length !== this.state.communitiesToJoin.length - ) - return true; - if (!this.props.data.communities && nextProps.data.communities) return true; - if (this.props.activeCommunity !== nextProps.activeCommunity) return true; - return false; - } - - componentDidUpdate(prevProps) { - if ( - (!prevProps.data.communities && - this.props.data.communities && - this.props.data.communities.length > 0) || - (this.props.data.communities && this.state.communitiesToJoin.length === 0) - ) { - const joinedCommunityIds = this.props.communities.map(c => c && c.id); - - // don't upsell communities the user has already joined - const filteredTopCommunities = this.props.data.communities.filter( - c => c && joinedCommunityIds.indexOf(c.id) < 0 - ); - - const uniqueFiltered = filteredTopCommunities.filter( - (x, i, a) => a.indexOf(x) === i - ); - - // get five random ones - const randomTopCommunities = getRandom(uniqueFiltered, 5); - - return this.setState({ - communitiesToJoin: randomTopCommunities, - }); - } - - if ( - prevProps.data && - prevProps.data.communities && - prevProps.data.communities.length !== this.props.data.communities.length - ) { - const joinedCommunityIds = this.props.data.communities.map( - c => c && c.id - ); - const filteredStateCommunities = this.state.communitiesToJoin.filter( - c => c && joinedCommunityIds.indexOf(c.id) < 0 - ); - const filteredTopCommunities = this.props.data.communities.filter( - c => c && joinedCommunityIds.indexOf(c.id) < 0 - ); - const uniqueFiltered = filteredTopCommunities.filter( - (x, i, a) => a.indexOf(x) === i - ); - const newRandom = getRandom(uniqueFiltered, 1); - const newArr = [...filteredStateCommunities, newRandom[0]]; - return this.setState({ - communitiesToJoin: newArr, - }); - } - } - - render() { - const { communitiesToJoin } = this.state; - - if (communitiesToJoin && communitiesToJoin.length > 0) { - return ( - - {communitiesToJoin.map(c => { - if (!c) return null; - return ( - track(events.INBOX_UPSELL_COMMUNITY_CLICKED)} - > - - - - - ); - })} - - ); - } - - return null; - } -} - -export default compose( - connect(), - getCommunitiesByCuratedContentType, - viewNetworkHandler -)(UpsellExploreCommunities); diff --git a/src/views/dashboard/index.js b/src/views/dashboard/index.js deleted file mode 100644 index f9eeeca52b..0000000000 --- a/src/views/dashboard/index.js +++ /dev/null @@ -1,284 +0,0 @@ -// @flow -import * as React from 'react'; -import compose from 'recompose/compose'; -import generateMetaInfo from 'shared/generate-meta-info'; -import { connect } from 'react-redux'; -import { removeItemFromStorage } from 'src/helpers/localStorage'; -import getEverythingThreads from 'shared/graphql/queries/user/getCurrentUserEverythingFeed'; -import getCommunityThreads from 'shared/graphql/queries/community/getCommunityThreadConnection'; -import getChannelThreadConnection from 'shared/graphql/queries/channel/getChannelThreadConnection'; -import { getCurrentUserCommunityConnection } from 'shared/graphql/queries/user/getUserCommunityConnection'; -import type { GetUserCommunityConnectionType } from 'shared/graphql/queries/user/getUserCommunityConnection'; -import searchThreadsQuery from 'shared/graphql/queries/search/searchThreads'; -import Titlebar from 'src/views/titlebar'; -import NewUserOnboarding from 'src/views/newUserOnboarding'; -import DashboardThreadFeed from './components/threadFeed'; -import Head from 'src/components/head'; -import Menu from 'src/components/menu'; -import DashboardLoading from './components/dashboardLoading'; -import DashboardError from './components/dashboardError'; -import NewActivityIndicator from './components/newActivityIndicator'; -import DashboardThread from '../dashboardThread'; -import Header from './components/threadSelectorHeader'; -import CommunityList from './components/communityList'; -import viewNetworkHandler from 'src/components/viewNetworkHandler'; -import { - DashboardWrapper, - InboxWrapper, - InboxScroller, - FeedHeaderContainer, - ThreadWrapper, - ThreadScroller, - SearchStringHeader, - Sidebar, -} from './style'; -import { track, events } from 'src/helpers/analytics'; -import { ErrorBoundary } from 'src/components/error'; - -const EverythingThreadFeed = compose( - connect(), - getEverythingThreads -)(DashboardThreadFeed); - -const CommunityThreadFeed = compose( - connect(), - getCommunityThreads -)(DashboardThreadFeed); - -const ChannelThreadFeed = compose( - connect(), - getChannelThreadConnection -)(DashboardThreadFeed); - -const SearchThreadFeed = compose( - connect(), - searchThreadsQuery -)(DashboardThreadFeed); - -type State = { - activeChannelObject: ?Object, -}; - -type Props = { - data: { - user?: GetUserCommunityConnectionType, - }, - newActivityIndicator: boolean, - activeThread: ?string, - activeCommunity: ?string, - activeChannel: ?string, - isLoading: boolean, - hasError: boolean, - searchQueryString: ?string, -}; - -class Dashboard extends React.Component { - state = { - activeChannelObject: null, - }; - - setActiveChannelObject = (channel: Object) => { - this.setState({ - activeChannelObject: channel, - }); - }; - - componentDidMount() { - track(events.INBOX_EVERYTHING_VIEWED); - } - - render() { - const { - data: { user }, - newActivityIndicator, - activeThread, - activeCommunity, - activeChannel, - isLoading, - hasError, - searchQueryString, - } = this.props; - const { activeChannelObject } = this.state; - - const searchFilter = { - everythingFeed: false, - creatorId: null, - channelId: activeChannel || null, - communityId: activeChannel ? null : activeCommunity || null, - }; - const { title, description } = generateMetaInfo(); - - if (user) { - // if the user hasn't joined any communities yet, we have nothing to show them on the dashboard. So instead just render the onboarding step to upsell popular communities to join - if (user.communityConnection.edges.length === 0) { - return ; - } - - // at this point we have succesfully validated a user, and the user has both a username and joined communities - we can show their thread feed! - const communities = user.communityConnection.edges - .filter(Boolean) - .map(({ node: community }) => community); - const activeCommunityObject = communities.find( - c => c && c.id === activeCommunity - ); - - return ( - - - - - - - - - - - - - - - - - -
- - - {newActivityIndicator && ( - - )} - {searchQueryString && searchQueryString.length > 0 && ( - - Search results for “{searchQueryString}” - - )} - - {searchQueryString && - searchQueryString.length > 0 && - searchFilter && ( - - - - )} - - {// no community, no search results - !activeCommunity && !searchQueryString && ( - - - - )} - - {// community, no channel, no search results - activeCommunity && !activeChannel && !searchQueryString && ( - - - - )} - - {// channel and community, no search results - activeChannel && activeCommunity && !searchQueryString && ( - - - - )} - - - - - - - - - - - - ); - } - - // loading state - if (isLoading) - return ; - - if (hasError) - return ( - - ); - - // if the user reached here it most likely that they have a user in localstorage but we weren't able to auth them - either because of a bad session token or otherwise. In this case we should clear local storage and load the home page to get them to log in again - removeItemFromStorage('spectrum'); - window.location.href = '/'; - return null; - } -} - -const map = state => ({ - newActivityIndicator: state.newActivityIndicator.hasNew, - activeThread: state.dashboardFeed.activeThread, - activeCommunity: state.dashboardFeed.activeCommunity, - activeChannel: state.dashboardFeed.activeChannel, - searchQueryString: state.dashboardFeed.search.queryString, -}); -export default compose( - // $FlowIssue - connect(map), - getCurrentUserCommunityConnection, - viewNetworkHandler -)(Dashboard); diff --git a/src/views/dashboard/style.js b/src/views/dashboard/style.js deleted file mode 100644 index 35b29a9516..0000000000 --- a/src/views/dashboard/style.js +++ /dev/null @@ -1,574 +0,0 @@ -// @flow -import theme from 'shared/theme'; -import styled, { css, keyframes } from 'styled-components'; -import { zIndex, Truncate, Shadow, hexa } from 'src/components/globals'; - -export const DashboardWrapper = styled.main` - display: flex; - align-items: flex-start; - justify-content: flex-start; - overflow-y: hidden; - flex: auto; - width: 100%; - box-shadow: 1px 0 0 ${theme.bg.border}; - - @media (max-width: 768px) { - flex-direction: column; - } -`; - -export const InboxWrapper = styled.div` - display: flex; - flex: 0 0 440px; - width: 440px; - overflow-y: hidden; - position: relative; - align-self: stretch; - flex-direction: column; - background: ${theme.bg.default}; - border-right: 1px solid ${theme.bg.border}; - - @media (min-resolution: 120dpi) { - max-width: 440px; - min-width: 440px; - } - - @media (max-width: 768px) { - max-width: 100%; - min-width: 100%; - flex: auto; - border-right: none; - } -`; - -export const InboxScroller = styled.div` - width: 100%; - overflow-y: auto; - position: relative; - flex: 1; -`; - -export const Sidebar = styled.div` - display: flex; - width: 256px; - min-width: 256px; - max-width: 256px; - position: relative; - align-self: stretch; - flex-direction: column; - border-right: 1px solid ${theme.bg.border}; - - @media (max-width: 1280px) { - display: none; - } -`; - -export const SectionTitle = styled.span` - display: inline-block; - font-size: 12px; - font-weight: 500; - color: ${theme.text.alt}; - font-variant: small-caps; - text-transform: lowercase; - margin: 8px 16px; - letter-spacing: 1.4px; -`; - -export const ChannelsContainer = styled.div` - grid-area: menu; - display: flex; - flex-direction: column; - justify-self: stretch; - padding: 4px; - - ${SectionTitle} { - color: ${theme.text.placeholder}; - margin: 8px 8px 4px 8px; - } -`; - -export const CommunityListName = styled.p` - grid-area: title; - font-size: 14px; - font-weight: 500; - line-height: 1.28; - max-width: 164px; - - ${Truncate}; -`; - -export const ChannelListItem = styled.div` - display: grid; - font-size: 14px; - grid-template-columns: 40px 1fr; - grid-auto-rows: auto; - grid-template-areas: 'icon title'; - padding: 4px 0; - justify-items: start; - align-items: center; - cursor: pointer; - font-weight: ${props => (props.active ? '500' : '400')}; - color: ${props => - props.active ? props.theme.text.default : props.theme.text.alt}; - ${Truncate} - } - - > div:first-child { - justify-self: center; - } - - ${CommunityListName} { - margin-left: 12px; - } - - &:hover { - color: ${theme.brand.alt}; - } - -`; - -export const ChannelListDivider = styled.div``; - -const placeHolderShimmer = keyframes` - 0%{ - transform: translateX(-200%) translateY(0%); - background-size: 100%; - opacity: 1; - } - 100%{ - transform: translateX(200%) translateY(0%); - background-size: 500%; - opacity: 0; - } -`; - -export const LoadingContainer = styled.div` - display: flex; - padding: 0 8px; - flex-direction: column; - margin-left: 36px; - overflow: hidden; -`; - -export const LoadingBar = styled.div` - width: ${props => `${props.width}px`}; - height: 4px; - border-radius: 4px; - margin-top: 8px; - animation-duration: 1.5s; - animation-fill-mode: forwards; - animation-iteration-count: infinite; - animation-timing-function: ease-in-out; - background: linear-gradient( - to right, - ${theme.bg.wash} 10%, - ${({ theme }) => hexa(theme.generic.default, 0.65)} 20%, - ${theme.bg.wash} 30% - ); - animation-name: ${placeHolderShimmer}; -`; - -export const CommunityListMeta = styled.div` - grid-area: title; - display: flex; - flex-direction: column; - justify-content: center; - align-items: flex-start; - padding-left: 4px; -`; - -export const CommunityListItem = styled.div` - display: grid; - grid-template-columns: 48px 1fr; - grid-auto-rows: 48px auto; - grid-template-areas: 'icon title' 'menu menu'; - min-height: 48px; - padding: 8px 16px; - justify-items: start; - align-items: center; - cursor: pointer; - color: ${props => - props.active ? props.theme.text.default : props.theme.text.alt}; - background: ${props => - props.active ? props.theme.bg.default : 'transparent'}; - - box-shadow: ${props => - props.active - ? `inset 0 -1px 0 ${props.theme.bg.border}, 0 -1px 0 ${ - props.theme.bg.border - }` - : 'none'}; - - > ${CommunityListName} { - margin-left: 4px; - } - - > div:first-child, - > img:first-child { - justify-self: center; - } - - ${props => - props.active && - css` - img { - opacity: 1; - filter: grayscale(0%); - } - `}; - - &:hover { - background: ${theme.bg.default}; - color: ${theme.text.default}; - box-shadow: 0 1px 0 ${theme.bg.border}, 0 -1px 0 ${theme.bg.border}; - - img { - box-shadow: 0; - } - } -`; - -export const CommunityListScroller = styled.div` - grid-area: scroll; - width: 100%; - overflow: hidden; - overflow-y: auto; - position: relative; - padding-top: 1px; -`; - -export const CommunityListWrapper = styled.div` - flex: auto; - background: ${theme.bg.wash}; - display: grid; - grid-template-rows: 1fr auto; - grid-template-columns: 1fr; - grid-template-areas: 'scroll' 'fixed'; - height: 100%; - max-height: 100%; - min-height: 100%; -`; - -export const Fixed = styled.div` - grid-area: fixed; - width: 100%; - box-shadow: 0 -1px 0 ${theme.bg.border}; - - &:hover { - color: ${theme.brand.alt}; - background: ${theme.bg.default}; - - div { - color: ${theme.brand.alt}; - background: ${theme.bg.default}; - } - } -`; - -export const CommunityAvatarContainer = styled.span` - img { - box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1); - } -`; - -export const FeedHeaderContainer = styled.div` - background: ${theme.bg.default}; - padding: 16px 8px; - box-shadow: ${Shadow.low} ${props => hexa(props.theme.bg.reverse, 0.15)}; - position: relative; - z-index: ${zIndex.chrome - 1}; - - @media (max-width: 768px) { - display: none; - } -`; - -export const ThreadWrapper = styled.div` - display: flex; - flex: auto; - overflow-y: hidden; - position: relative; - align-self: stretch; - background-color: ${theme.bg.default}; - - @media (max-width: 768px) { - display: none; - } -`; - -export const ThreadScroller = styled.div` - width: 100%; - overflow-y: auto; - position: relative; -`; - -export const HeaderWrapper = styled.div` - display: grid; - grid-template-columns: 1fr auto; - grid-template-rows: 1fr; - grid-template-areas: 'center right'; - grid-column-gap: 8px; - align-items: center; - justify-items: center; - padding-left: 4px; - - > button { - grid-area: right; - } - - @media (max-width: 1280px) { - padding-left: 0; - grid-template-columns: auto 1fr auto; - grid-template-rows: 1fr; - grid-template-areas: 'left center right'; - } -`; - -export const ThreadComposerContainer = styled.div` - padding: 16px; - border-bottom: 1px solid ${theme.bg.border}; - - @media (max-width: 768px) { - margin: 0; - } -`; - -export const ComposeIconContainer = styled.div` - position: relative; - top: 2px; - color: ${theme.brand.alt}; - display: flex; - align-items: center; - font-weight: 600; - cursor: pointer; - - &:hover { - color: ${theme.brand.default}; - } - - div { - margin-right: 8px; - } -`; - -export const NullThreadFeed = styled.div` - display: flex; - flex: 1; - height: 100%; - align-items: center; - justify-content: center; - padding: 32px; - flex-direction: column; - background: ${theme.bg.default}; -`; - -export const NullHeading = styled.p` - font-size: 18px; - font-weight: 500; - color: ${theme.text.alt}; - text-align: center; - margin-bottom: 8px; -`; - -export const Lock = styled.span` - margin-right: 4px; -`; -export const PinIcon = styled.span` - margin-right: 4px; - margin-left: -2px; - display: flex; - align-items: center; -`; - -export const UpsellExploreDivider = styled.div` - border-bottom: 1px solid ${theme.bg.border}; - display: block; - width: 100%; - margin: 16px 0 16px; -`; - -export const SearchInputDiv = styled.div` - align-self: stretch; - height: 100%; - justify-self: stretch; -`; - -export const SearchInput = styled.input` - font-size: 16px; - width: 100%; - height: 100%; - color: ${props => - props.darkContext ? props.theme.text.placeholder : props.theme.text.alt}; - background-color: transparent; - border: none; - - &:placeholder { - color: ${props => - props.darkContext - ? props.theme.text.border - : props.theme.text.placeholder}; - } - - &:focus { - color: ${props => - props.darkContext ? props.theme.text.reverse : props.theme.text.default}; - } -`; - -export const ClearSearch = styled.span` - width: 16px; - height: 16px; - opacity: ${props => (props.isVisible ? '1' : '0')}; - background: ${theme.text.placeholder}; - border-radius: 50%; - font-size: 16px; - color: ${theme.text.reverse}; - font-weight: 500; - pointer-events: ${props => (props.isOpen ? 'auto' : 'none')}; - cursor: pointer; - transition: background 0.2s; - - &:hover { - background: ${theme.text.alt}; - } - - span { - position: relative; - top: -2px; - } -`; - -export const SearchForm = styled.form` - display: grid; - height: 32px; - min-height: 32px; - max-height: 32px; - color: ${theme.text.alt}; - border: 1px solid ${theme.bg.border}; - border-radius: 16px; - grid-template-columns: 32px 1fr 32px; - grid-template-rows: 1fr; - grid-template-areas: 'icon input clear'; - align-items: center; - justify-items: center; - flex: auto; - justify-self: stretch; - grid-area: center; - - ${props => - props.darkContext && - css` - border-color: transparent; - background-color: ${props => hexa(props.theme.text.alt, 0.35)}; - color: ${theme.text.reverse}; - `}; - - > div:last-of-type { - display: ${props => (props.isOpen ? 'flex' : 'none')}; - color: ${theme.text.reverse}; - background-color: ${theme.text.alt}; - border-radius: 100%; - padding: 4px; - align-items: center; - justify-content: center; - cursor: pointer; - } -`; - -export const OutlineButton = styled.button` - display: flex; - align-items: center; - background-color: transparent; - font-size: 14px; - font-weight: 700; - border: 2px solid ${theme.text.alt}; - color: ${theme.text.alt}; - border-radius: 8px; - padding: 4px 12px 4px 6px; - - span { - margin-left: 8px; - } -`; - -export const SearchStringHeader = styled.div` - background: #fff; - padding: 16px; - font-weight: 600; - border-bottom: 1px solid ${theme.bg.border}; -`; - -export const Hint = styled.span` - font-size: 16px; - color: ${theme.text.alt}; - margin-top: 32px; - margin-bottom: 8px; -`; - -export const NarrowOnly = styled.div` - display: none; - - @media (max-width: 1280px) { - display: flex; - flex: none; - grid-area: left; - } -`; - -export const UpsellRow = styled.div` - padding: 8px; - display: flex; - justify-content: space-between; -`; - -export const HeaderActiveViewTitle = styled.h2` - padding: 0 8px; - font-size: 24px; - font-weight: 700; - color: ${theme.text.default}; - max-width: 384px; - line-height: 1.2; - - ${Truncate}; - - &:hover { - color: ${theme.text.default}; - } -`; - -export const HeaderActiveViewSubtitle = styled.h3` - padding: 0 8px; - font-size: 14px; - font-weight: 400; - color: ${theme.text.alt}; - max-width: 384px; - line-height: 1.2; - - display: flex; - align-items: center; - - ${Truncate}; - - &:hover { - color: ${theme.text.default}; - } -`; - -export const ContextHeaderContainer = styled.div` - padding-top: 16px; - padding-bottom: 12px; -`; - -export const EllipsisText = styled.div` - ${Truncate}; -`; - -export const PendingBadge = styled.div` - background: ${theme.warn.alt}; - color: ${theme.text.reverse}; - padding: 0 8px; - border-radius: 24px; - font-size: 12px; - font-weight: 600; - margin-top: 2px; -`; diff --git a/src/views/dashboardThread/index.js b/src/views/dashboardThread/index.js deleted file mode 100644 index 7a2caf7617..0000000000 --- a/src/views/dashboardThread/index.js +++ /dev/null @@ -1,32 +0,0 @@ -import React, { Component } from 'react'; -import compose from 'recompose/compose'; -import { connect } from 'react-redux'; -import { InboxThreadView } from '../thread'; -import { Container, Thread } from './style'; - -class DashboardThread extends Component { - render() { - const { threadId, threadSliderIsOpen } = this.props; - - // no thread has been selected - if (!threadId) { - return null; - } - // otherwise return the thread that was selected - return ( - - - - - - ); - } -} - -const map = state => ({ threadSliderIsOpen: state.threadSlider.isOpen }); -export default compose(connect(map))(DashboardThread); diff --git a/src/views/dashboardThread/style.js b/src/views/dashboardThread/style.js deleted file mode 100644 index 146c255b59..0000000000 --- a/src/views/dashboardThread/style.js +++ /dev/null @@ -1,67 +0,0 @@ -// @flow -import theme from 'shared/theme'; -// $FlowFixMe -import styled from 'styled-components'; -import { zIndex } from '../../components/globals'; - -export const Container = styled.div` - display: flex; - flex: auto; - align-self: stretch; - overflow-y: hidden; - height: 100%; - max-height: 100%; - position: relative; -`; - -export const Thread = styled.div` - display: flex; - background: ${theme.bg.wash}; - flex: auto; - z-index: ${zIndex.chrome - 2}; - flex-direction: column; - max-width: 100%; -`; - -export const NullContainer = styled.div` - display: flex; - flex: auto; - align-self: stretch; - overflow-y: hidden; - padding: 32px; - height: 100%; -`; - -export const NullThread = styled.div` - display: flex; - flex: auto; - z-index: ${zIndex.slider + 3}; - flex-direction: column; - max-width: 100%; - border-radius: 8px; - align-items: center; - justify-content: center; - - button { - padding: 16px 24px; - font-size: 18px; - font-weight: 600; - } -`; - -export const Heading = styled.h3` - font-size: 24px; - font-weight: 700; - color: ${theme.text.default}; - margin: 48px 24px 16px; - text-align: center; -`; - -export const Subheading = styled.h4` - font-size: 18px; - font-weight: 400; - color: ${theme.text.alt}; - margin: 0 48px 32px; - max-width: 600px; - text-align: center; -`; diff --git a/src/views/directMessages/components/header.js b/src/views/directMessages/components/header.js index 91e049e377..3873d49e57 100644 --- a/src/views/directMessages/components/header.js +++ b/src/views/directMessages/components/header.js @@ -1,6 +1,6 @@ import React from 'react'; import generateMetaInfo from 'shared/generate-meta-info'; -import Head from '../../../components/head'; +import Head from 'src/components/head'; import { StyledHeader, PhotosContainer, @@ -15,6 +15,9 @@ const Header = ({ thread, currentUser }) => { user => user.userId !== currentUser.id ); + // don't show the header in a 1:1 dm because we already have the titlebar + if (trimmedUsers.length === 1) return null; + const photos = trimmedUsers.map(user => ( diff --git a/src/views/directMessages/components/loading.js b/src/views/directMessages/components/loading.js index 43cb31ed51..9b47c63d1a 100644 --- a/src/views/directMessages/components/loading.js +++ b/src/views/directMessages/components/loading.js @@ -1,14 +1,14 @@ // @flow import React from 'react'; import { Link } from 'react-router-dom'; -import Icon from '../../../components/icons'; -import { LoadingDM } from '../../../components/loading'; +import Icon from 'src/components/icon'; +import { LoadingDM } from 'src/components/loading'; import { View, MessagesList, ComposeHeader } from '../style'; export default () => ( - + diff --git a/src/views/directMessages/components/messageThreadListItem.js b/src/views/directMessages/components/messageThreadListItem.js index 45a005e9dd..c8478edcbf 100644 --- a/src/views/directMessages/components/messageThreadListItem.js +++ b/src/views/directMessages/components/messageThreadListItem.js @@ -67,16 +67,12 @@ class ListCardItemDirectMessageThread extends React.Component { {avatars} - +

{participantsArray}

- - {threadTimeDifference} - + {threadTimeDifference}
- - {thread.snippet} - + {thread.snippet}
diff --git a/src/views/directMessages/components/messages.js b/src/views/directMessages/components/messages.js index 5642889bd2..f6ee874c2e 100644 --- a/src/views/directMessages/components/messages.js +++ b/src/views/directMessages/components/messages.js @@ -2,10 +2,10 @@ import * as React from 'react'; import compose from 'recompose/compose'; import { sortAndGroupMessages } from 'shared/clients/group-messages'; -import ChatMessages from '../../../components/messageGroup'; -import { Loading } from '../../../components/loading'; -import viewNetworkHandler from '../../../components/viewNetworkHandler'; -import NextPageButton from '../../../components/nextPageButton'; +import ChatMessages from 'src/components/messageGroup'; +import { Loading } from 'src/components/loading'; +import viewNetworkHandler from 'src/components/viewNetworkHandler'; +import NextPageButton from 'src/components/nextPageButton'; import getDirectMessageThreadMessages from 'shared/graphql/queries/directMessageThread/getDirectMessageThreadMessageConnection'; import type { GetDirectMessageThreadMessageConnectionType } from 'shared/graphql/queries/directMessageThread/getDirectMessageThreadMessageConnection'; import setLastSeenMutation from 'shared/graphql/mutations/directMessageThread/setDMThreadLastSeen'; @@ -14,8 +14,6 @@ import { ErrorBoundary } from 'src/components/error'; type Props = { id: string, - forceScrollToBottom: Function, - contextualScrollToBottom: Function, data: { loading: boolean, directMessageThread: GetDirectMessageThreadMessageConnectionType, @@ -35,68 +33,122 @@ type State = { }; class MessagesWithData extends React.Component { - state = { - subscription: null, - }; + subscription: ?Function; componentDidMount() { - this.props.forceScrollToBottom(); this.subscribe(); } - componentDidUpdate(prev) { - const { contextualScrollToBottom, data, setLastSeen } = this.props; + componentWillUnmount() { + this.unsubscribe(); + } - if (this.props.data.loading) { - this.unsubscribe(); - } + subscribe = () => { + this.subscription = this.props.subscribeToNewMessages(); + }; + + unsubscribe = () => { + if (this.subscription) this.subscription(); + }; + getSnapshotBeforeUpdate(prev) { + const curr = this.props; + // First load if ( - prev.data.networkStatus === 1 && - prev.data.loading && - !this.props.data.loading + !prev.data.directMessageThread && + curr.data.directMessageThread && + curr.data.directMessageThread.messageConnection.edges.length > 0 ) { - this.subscribe(); - setTimeout(() => this.props.forceScrollToBottom()); + return { + type: 'bottom', + }; } - // force scroll to bottom when a message is sent in the same thread + // New messages if ( prev.data.directMessageThread && - data.directMessageThread && - prev.data.directMessageThread.id === data.directMessageThread.id && - (prev.data.messages && data.messages) && - prev.data.messages.length < data.messages.length && - contextualScrollToBottom + curr.data.directMessageThread && + prev.data.directMessageThread.messageConnection.edges.length < + curr.data.directMessageThread.messageConnection.edges.length ) { - // mark this thread as unread when new messages come in and i'm viewing it - if (data.directMessageThread) { - setLastSeen(data.directMessageThread.id); + const elem = document.getElementById('main'); + if (!elem) return null; + + // If we are near the bottom when new messages come in, stick to the bottom + if (elem.scrollHeight < elem.scrollTop + elem.clientHeight + 400) { + return { + type: 'bottom', + }; + } + + const prevEdges = prev.data.directMessageThread.messageConnection.edges.filter( + Boolean + ); + const currEdges = curr.data.directMessageThread.messageConnection.edges.filter( + Boolean + ); + // If messages were added at the end, keep the scroll position the same + if ( + currEdges[currEdges.length - 1].node.id === + prevEdges[prevEdges.length - 1].node.id + ) { + return null; } - contextualScrollToBottom(); + + // If messages were added at the top, persist the scroll position + return { + type: 'persist', + values: { + top: elem.scrollTop, + height: elem.scrollHeight, + }, + }; } + return null; } - componentWillUnmount() { - this.unsubscribe(); - } + componentDidUpdate(prev, _, snapshot) { + const { data, setLastSeen } = this.props; - subscribe = () => { - this.setState({ - subscription: this.props.subscribeToNewMessages(), - }); - }; + if (snapshot) { + const elem = document.getElementById('main'); + if (elem) { + switch (snapshot.type) { + case 'bottom': { + elem.scrollTop = elem.scrollHeight; + break; + } + case 'persist': { + elem.scrollTop = + elem.scrollHeight - snapshot.values.height + snapshot.values.top; + break; + } + default: { + break; + } + } + } + } - unsubscribe = () => { - const { subscription } = this.state; - if (subscription) { - // This unsubscribes the subscription - subscription(); + const firstLoad = + !prev.data.directMessageThread && data.directMessageThread; + const newThread = + prev.data.directMessageThread && + data.directMessageThread && + prev.data.directMessageThread.id !== data.directMessageThread.id; + + if (firstLoad) { + this.subscribe(); + setLastSeen(data.directMessageThread.id); + } else if (newThread) { + this.unsubscribe(); + this.subscribe(); + setLastSeen(data.directMessageThread.id); } - }; + } render() { const { - data: { messages, hasNextPage, fetchMore }, + data: { messages, directMessageThread, hasNextPage, fetchMore }, hasError, isLoading, isFetchingMore, @@ -139,10 +191,9 @@ class MessagesWithData extends React.Component { )} @@ -150,7 +201,11 @@ class MessagesWithData extends React.Component { } if (isLoading) { - return ; + return ( + + + + ); } return null; diff --git a/src/views/directMessages/components/style.js b/src/views/directMessages/components/style.js index f1d306a422..ddd026b78d 100644 --- a/src/views/directMessages/components/style.js +++ b/src/views/directMessages/components/style.js @@ -3,6 +3,7 @@ import theme from 'shared/theme'; import styled from 'styled-components'; import { Link } from 'react-router-dom'; import { UserAvatar } from 'src/components/avatar'; +import { MEDIA_BREAK, TITLEBAR_HEIGHT } from 'src/components/layout'; import { Truncate, FlexCol, @@ -12,34 +13,32 @@ import { P, hexa, zIndex, -} from '../../../components/globals'; +} from 'src/components/globals'; export const ThreadsListScrollContainer = styled.div` - display: flex; - flex-direction: column; - flex-grow: 1; - overflow-y: auto; - max-height: 100%; + height: calc(100vh - ${TITLEBAR_HEIGHT}px); + background: ${theme.bg.default}; `; export const Wrapper = styled(FlexCol)` flex: 0 0 auto; justify-content: center; max-width: 100%; - height: 64px; + min-height: 64px; position: relative; - background: ${props => (props.active ? props.theme.bg.wash : '#fff')}; + background: ${props => + props.active + ? theme.bg.wash + : props.isUnread + ? hexa(theme.brand.default, 0.04) + : theme.bg.default}; + border-bottom: 1px solid ${theme.bg.divider}; box-shadow: ${props => - props.isUnread ? `inset -2px 0 0 ${props.theme.brand.default}` : 'none'}; - - &:after { - content: ''; - position: absolute; - bottom: 0; - left: 16px; - width: calc(100% - 16px); - border-bottom: 1px solid ${theme.bg.wash}; - } + props.active + ? `inset 2px 0 0 ${props.theme.text.placeholder}` + : props.isUnread + ? `inset 2px 0 0 ${props.theme.brand.default}` + : 'none'}; &:hover { cursor: pointer; @@ -51,7 +50,7 @@ export const WrapperLink = styled(Link)` height: 100%; display: flex; align-items: center; - padding-left: 16px; + padding: 16px 12px 16px 16px; `; export const Col = styled(FlexCol)` @@ -62,7 +61,6 @@ export const Row = styled(FlexRow)` flex: 1 0 auto; align-items: center; max-width: 100%; - padding-right: 16px; a { display: flex; @@ -75,9 +73,9 @@ export const Heading = styled(H3)` `; export const Meta = styled(H4)` - font-weight: ${props => (props.isUnread ? 600 : 400)}; - color: ${props => - props.isUnread ? props.theme.text.default : props.theme.text.alt}; + font-size: 15px; + font-weight: 400; + color: ${theme.text.alt}; ${props => (props.nowrap ? Truncate() : '')}; `; @@ -112,10 +110,10 @@ export const Usernames = styled.span` white-space: nowrap; overflow: hidden; color: ${theme.text.default}; - font-weight: ${props => (props.isUnread ? 800 : 600)}; - line-height: 1.1; + font-weight: 600; + line-height: 1.2; margin-bottom: 1px; - font-size: 14px; + font-size: 15px; flex: 1 1 100%; p { @@ -124,9 +122,9 @@ export const Usernames = styled.span` `; export const Timestamp = styled.span` - font-size: 12px; + font-size: 14px; text-align: right; - color: ${props => (props.isUnread ? props.theme.brand.default : '#909aa7')}; + color: ${theme.text.alt}; padding-right: 4px; display: inline-block; flex: 1 0 auto; @@ -134,10 +132,9 @@ export const Timestamp = styled.span` `; export const Snippet = styled.p` - font-size: 13px; - font-weight: ${props => (props.unread ? 700 : 500)}; - color: ${props => - props.unread ? props.theme.text.default : props.theme.text.alt}; + font-size: 15px; + font-weight: 400; + color: ${theme.text.secondary}; padding-right: 4px; display: inline-block; line-height: 1.3; @@ -259,7 +256,7 @@ export const ComposerInput = styled.input` position: relative; z-index: ${zIndex.search}; - @media (max-width: 768px) { + @media (max-width: ${MEDIA_BREAK}px) { padding: 20px 16px; } `; @@ -286,7 +283,7 @@ export const SearchResultsDropdown = styled.ul` overflow-y: auto; z-index: ${zIndex.dropDown}; - @media (max-width: 768px) { + @media (max-width: ${MEDIA_BREAK}px) { width: 100%; left: 0; border-radius: 0 0 8px 8px; @@ -417,6 +414,5 @@ export const Username = styled.h3` export const MessagesScrollWrapper = styled.div` width: 100%; - flex: 1 0 auto; padding-top: 24px; `; diff --git a/src/views/directMessages/components/threadsList.js b/src/views/directMessages/components/threadsList.js index 279ecc136b..8c15822b8a 100644 --- a/src/views/directMessages/components/threadsList.js +++ b/src/views/directMessages/components/threadsList.js @@ -2,16 +2,14 @@ import * as React from 'react'; import { connect } from 'react-redux'; import compose from 'recompose/compose'; +import VisibilitySensor from 'react-visibility-sensor'; import DirectMessageListItem from './messageThreadListItem'; import getCurrentUserDMThreadConnection, { type GetCurrentUserDMThreadConnectionType, } from 'shared/graphql/queries/directMessageThread/getCurrentUserDMThreadConnection'; -import InfiniteList from 'src/components/infiniteScroll'; -import { NullState } from 'src/components/upsell'; import { deduplicateChildren } from 'src/components/infiniteScroll/deduplicateChildren'; import { LoadingDM } from 'src/components/loading'; import { ThreadsListScrollContainer } from './style'; -import { NoThreads } from '../style'; import { track, events } from 'src/helpers/analytics'; import { ErrorBoundary } from 'src/components/error'; import { withCurrentUser } from 'src/components/withCurrentUser'; @@ -21,6 +19,13 @@ import type { Query } from 'react-apollo'; import viewNetworkHandler, { type ViewNetworkHandlerType, } from 'src/components/viewNetworkHandler'; +import { DesktopTitlebar } from 'src/components/titlebar'; +import { PrimaryOutlineButton } from 'src/components/button'; +import { + NoCommunitySelected, + NoCommunityHeading, + NoCommunitySubheading, +} from '../style'; type Props = { currentUser: Object, @@ -38,13 +43,11 @@ type Props = { }; type State = { - scrollElement: any, subscription: ?Function, }; class ThreadsList extends React.Component { state = { - scrollElement: null, subscription: null, }; @@ -63,13 +66,6 @@ class ThreadsList extends React.Component { }; componentDidMount() { - const scrollElement = document.getElementById('scroller-for-dm-threads'); - this.setState({ - // NOTE(@mxstbr): This is super un-reacty but it works. This refers to - // the AppViewWrapper which is the scrolling part of the site. - scrollElement, - }); - this.subscribe(); track(events.DIRECT_MESSAGES_VIEWED); } @@ -102,9 +98,13 @@ class ThreadsList extends React.Component { return dmData.fetchMore(); }; + onLoadMoreVisible = (isVisible: boolean) => { + if (this.props.isFetchingMore || !isVisible) return; + return this.paginate(); + }; + render() { - const { currentUser, dmData, activeThreadId } = this.props; - const { scrollElement } = this.state; + const { currentUser, dmData, activeThreadId, isFetchingMore } = this.props; if (!dmData) return null; @@ -158,41 +158,63 @@ class ThreadsList extends React.Component { if (!uniqueThreads || uniqueThreads.length === 0) { return ( - - - - - - - - + + + +
+ No conversation selected + + Choose from an existing conversation, or start a new one. + + + New message + +
+
+
); } + const LoadingDMWithVisibility = () => ( + + + + ); + return ( - - } - useWindow={false} - scrollElement={scrollElement} - threshold={100} - className={'scroller-for-community-dm-threads-list'} - > + + + New + + } + /> + {uniqueThreads.map(thread => { if (!thread) return null; return ( - + { ); })} - - + {hasNextPage && } + + ); } } diff --git a/src/views/directMessages/containers/existingThread.js b/src/views/directMessages/containers/existingThread.js index d207acdb78..92f5c804ec 100644 --- a/src/views/directMessages/containers/existingThread.js +++ b/src/views/directMessages/containers/existingThread.js @@ -2,7 +2,10 @@ import * as React from 'react'; import compose from 'recompose/compose'; import { connect } from 'react-redux'; +import type { Dispatch } from 'redux'; import { withApollo } from 'react-apollo'; +import { Link } from 'react-router-dom'; +import Icon from 'src/components/icon'; import setLastSeenMutation from 'shared/graphql/mutations/directMessageThread/setDMThreadLastSeen'; import Messages from '../components/messages'; import Header from '../components/header'; @@ -11,13 +14,17 @@ import viewNetworkHandler from 'src/components/viewNetworkHandler'; import getDirectMessageThread, { type GetDirectMessageThreadType, } from 'shared/graphql/queries/directMessageThread/getDirectMessageThread'; +import { setTitlebarProps } from 'src/actions/titlebar'; +import { UserAvatar } from 'src/components/avatar'; import { MessagesContainer, ViewContent } from '../style'; +import { ChatInputWrapper } from 'src/components/layout'; import { Loading } from 'src/components/loading'; -import ViewError from 'src/components/viewError'; import { ErrorBoundary } from 'src/components/error'; import type { WebsocketConnectionType } from 'src/reducers/connectionStatus'; import { useConnectionRestored } from 'src/hooks/useConnectionRestored'; import { withCurrentUser } from 'src/components/withCurrentUser'; +import { LoadingView, ErrorView } from 'src/views/viewHelpers'; +import { DesktopTitlebar } from 'src/components/titlebar'; type Props = { data: { @@ -32,10 +39,10 @@ type Props = { threadSliderIsOpen: boolean, networkOnline: boolean, websocketConnection: WebsocketConnectionType, + dispatch: Dispatch, }; class ExistingThread extends React.Component { - scrollBody: ?HTMLDivElement; chatInput: ?ChatInput; componentDidMount() { @@ -45,7 +52,6 @@ class ExistingThread extends React.Component { if (!threadId) return; this.props.setLastSeen(threadId); - this.forceScrollToBottom(); // autofocus on desktop if (window && window.innerWidth > 768 && this.chatInput) { this.chatInput.focus(); @@ -54,12 +60,39 @@ class ExistingThread extends React.Component { componentDidUpdate(prev) { const curr = this.props; + const { dispatch, currentUser } = curr; const didReconnect = useConnectionRestored({ curr, prev }); if (didReconnect && curr.data.refetch) { curr.data.refetch(); } + if (curr.data.directMessageThread) { + const thread = curr.data.directMessageThread; + const trimmedUsers = thread.participants.filter( + user => user.userId !== currentUser.id + ); + const titleIcon = + trimmedUsers.length === 1 ? ( + + ) : null; + const rightAction = + trimmedUsers.length === 1 ? ( + + + + ) : null; + const names = trimmedUsers.map(user => user.name).join(', '); + dispatch( + setTitlebarProps({ + title: names, + titleIcon, + rightAction, + leftAction: 'view-back', + }) + ); + } + // if the thread slider is open, dont be focusing shit up in heyuhr if (curr.threadSliderIsOpen) return; // if the thread slider is closed and we're viewing DMs, refocus the chat input @@ -81,7 +114,6 @@ class ExistingThread extends React.Component { if (!threadId) return; curr.setLastSeen(threadId); - this.forceScrollToBottom(); // autofocus on desktop if (window && window.innerWidth > 768 && this.chatInput) { this.chatInput.focus(); @@ -89,20 +121,6 @@ class ExistingThread extends React.Component { } } - forceScrollToBottom = () => { - if (!this.scrollBody) return; - let node = this.scrollBody; - node.scrollTop = node.scrollHeight - node.clientHeight; - }; - - contextualScrollToBottom = () => { - if (!this.scrollBody) return; - let node = this.scrollBody; - if (node.scrollHeight - node.clientHeight < node.scrollTop + 140) { - node.scrollTop = node.scrollHeight - node.clientHeight; - } - }; - render() { const id = this.props.match.params.threadId; const { currentUser, data, isLoading } = this.props; @@ -110,54 +128,68 @@ class ExistingThread extends React.Component { if (id !== 'new') { if (data.directMessageThread) { const thread = data.directMessageThread; + const trimmedUsers = thread.participants.filter( + user => user.userId !== currentUser.id + ); + const titleIcon = + trimmedUsers.length === 1 ? ( + + ) : null; + const rightAction = + trimmedUsers.length === 1 ? ( + + + + ) : null; + const names = trimmedUsers.map(user => user.name).join(', '); const mentionSuggestions = thread.participants .map(cleanSuggestionUserObject) .filter(user => user && user.username !== currentUser.username); return ( - - (this.scrollBody = scrollBody)} - > - {!isLoading ? ( - - -
- - - - - ) : ( - - )} - - - (this.chatInput = chatInput)} - participants={mentionSuggestions} +
+ - + + + {!isLoading ? ( + + +
+ + + + + ) : ( + + )} + + + + (this.chatInput = chatInput)} + participants={mentionSuggestions} + /> + + +
); } if (isLoading) { - return ; + return ; } - return ( - - ); + return ; } /* diff --git a/src/views/directMessages/containers/index.js b/src/views/directMessages/containers/index.js index 8300b2bb84..1b7defbc35 100644 --- a/src/views/directMessages/containers/index.js +++ b/src/views/directMessages/containers/index.js @@ -1,17 +1,27 @@ // @flow import * as React from 'react'; -import { Link } from 'react-router-dom'; +import { connect } from 'react-redux'; +import type { Dispatch } from 'redux'; import type { Match } from 'react-router'; -import Icon from 'src/components/icons'; import ThreadsList from '../components/threadsList'; -import NewThread from './newThread'; import ExistingThread from './existingThread'; -import Titlebar from 'src/views/titlebar'; -import { View, MessagesList, ComposeHeader } from '../style'; -import { track, events } from 'src/helpers/analytics'; +import { PrimaryOutlineButton } from 'src/components/button'; +import { setTitlebarProps } from 'src/actions/titlebar'; +import { + ViewGrid, + SecondaryPrimaryColumnGrid, + PrimaryColumn, +} from 'src/components/layout'; +import { + StyledSecondaryColumn, + NoCommunitySelected, + NoCommunityHeading, + NoCommunitySubheading, +} from '../style'; type Props = { match: Match, + dispatch: Dispatch, }; type State = { @@ -19,59 +29,76 @@ type State = { }; class DirectMessages extends React.Component { - componentDidUpdate(prevProps: Props) { - const curr = this.props; - if (prevProps.match.params.threadId !== curr.match.params.threadId) { - if (curr.match.params.threadId === 'new') { - track(events.DIRECT_MESSAGE_THREAD_COMPOSER_VIEWED); - } else { - track(events.DIRECT_MESSAGE_THREAD_VIEWED); - } + componentDidMount() { + const { dispatch } = this.props; + dispatch( + setTitlebarProps({ + title: 'Messages', + rightAction: ( + + New + + ), + }) + ); + } + + componentDidUpdate() { + const { match, dispatch } = this.props; + const { params } = match; + if (!params.threadId) { + dispatch( + setTitlebarProps({ + title: 'Messages', + rightAction: ( + + New + + ), + }) + ); } } render() { const { match } = this.props; - const activeThreadId = match.params.threadId; - const isComposing = activeThreadId === 'new'; - const isViewingThread = !isComposing && !!activeThreadId; return ( - - - - - - - - - - - - + + + + + - {isViewingThread ? ( - - ) : ( - - )} - + + {activeThreadId ? ( + + ) : ( + +
+ + No conversation selected + + + Choose from an existing conversation, or start a new one. + + + New message + +
+
+ )} +
+ + ); } } -export default DirectMessages; +export default connect()(DirectMessages); diff --git a/src/views/directMessages/containers/newThread.js b/src/views/directMessages/containers/newThread.js deleted file mode 100644 index 010bbb45b7..0000000000 --- a/src/views/directMessages/containers/newThread.js +++ /dev/null @@ -1,889 +0,0 @@ -// @flow -import * as React from 'react'; -import { withApollo } from 'react-apollo'; -import { withRouter } from 'react-router'; -import compose from 'recompose/compose'; -import Head from '../../../components/head'; -import { connect } from 'react-redux'; -import generateMetaInfo from 'shared/generate-meta-info'; -import Messages from '../components/messages'; -import Header from '../components/header'; -import ChatInput from '../../../components/chatInput'; -import { NullState } from '../../../components/upsell'; -import { MessagesContainer, ViewContent, NoThreads } from '../style'; -import { getDirectMessageThreadQuery } from 'shared/graphql/queries/directMessageThread/getDirectMessageThread'; -import type { GetDirectMessageThreadType } from 'shared/graphql/queries/directMessageThread/getDirectMessageThread'; -import { debounce } from '../../../helpers/utils'; -import { searchUsersQuery } from 'shared/graphql/queries/search/searchUsers'; -import { Spinner } from '../../../components/globals'; -import { addToastWithTimeout } from '../../../actions/toasts'; -import { clearDirectMessagesComposer } from '../../../actions/directMessageThreads'; -import createDirectMessageThreadMutation from 'shared/graphql/mutations/directMessageThread/createDirectMessageThread'; -import type { Dispatch } from 'redux'; -import { withCurrentUser } from 'src/components/withCurrentUser'; -import { - BACKSPACE, - ESC, - ARROW_DOWN, - ARROW_UP, - ENTER, -} from 'src/helpers/keycodes'; -import { - ComposerInputWrapper, - Grow, - SelectedUsersPills, - Pill, - SearchSpinnerContainer, - ComposerInput, - SearchResultsDropdown, - SearchResult, - SearchResultNull, - SearchResultUsername, - SearchResultDisplayName, - SearchResultTextContainer, - SearchResultImage, -} from '../components/style'; - -type State = { - searchString: string, - searchResults: Array, - searchIsLoading: boolean, - selectedUsersForNewThread: Array, - focusedSearchResult: string, // id - focusedSelectedUser: string, // id - existingThreadBasedOnSelectedUsers: string, // id - existingThreadWithMessages: Object, - loadingExistingThreadMessages: boolean, - chatInputIsFocused: boolean, - threadIsBeingCreated: boolean, -}; - -type Props = { - client: Object, - currentUser: Object, - initNewThreadWithUser: Array, - threads: Array, - hideOnMobile: boolean, - dispatch: Dispatch, - createDirectMessageThread: Function, - threadSliderIsOpen: boolean, - history: Object, -}; - -class NewThread extends React.Component { - chatInput: React.ElementProps; - scrollBody: ?HTMLDivElement; - input: React.Node; - - constructor(props) { - super(props); - - this.state = { - // user types in a string that returns all users whose username - // or displayName contains the string - searchString: '', - // the query returns an array of user objects. this is used to populate - // the search dropdown - searchResults: [], - // if the query is still fetching, a loading indicator appears in - // the search bar - searchIsLoading: false, - // as a user selects users for the new direct message thread, we add - // them to an array. this array will be used for two functions: - // 1. Map against the user's existing DM threads to see if a thread - // with the selected users already exists - // 2. If no existing thread is found, this is the array that will be - // used in the DMThread creation mutation - selectedUsersForNewThread: [], - // represents a userId of a search result that is currently "focused" - // in the search dropdown. This allows a user to press up/down, or enter - // to quickly navigation the search results dropdown - focusedSearchResult: '', - // when users have been added to `selectedUsersForNewThread`, they can - // be removed by either backspacing them away, or a user can click on - // the person's name, and then press backspace, to remove that specific - // user - focusedSelectedUser: '', - // if an existing thread is found based on the selected users, we will - // kick off a query to get that thread's messages and load it inline - // we will also use this object to make sure the chat input sends messages - // to the existing thread and doesn't create a new one - existingThreadBasedOnSelectedUsers: '', - // after we get the messages from the server, we'll store the full object - existingThreadWithMessages: {}, - // if the query is loading, we show a centered spinner in the middle of - // the page where the messages will appear - loadingExistingThreadMessages: false, - // if the user is focused on the chat input, we want 'enter' to send - // a message and create the dm group, and ignore other logic around - // pressing the 'enter' key - chatInputIsFocused: false, - // set to true while a thread is being created, to prevent a user pressing - // enter twice and accidentally creating two threads - threadIsBeingCreated: false, - }; - - // only kick off search query every 200ms - this.search = debounce(this.search, 500, false); - } - - /* - takes a string that gets sent to the server and matched against all - user's displayNames and usernames - */ - search = (string: string) => { - // if the user has cleared the search input, make sure there are no search - // results or focused users - if (!string || string.length === 0) { - return this.setState({ - searchResults: [], - focusedSearchResult: '', - }); - } - - const { selectedUsersForNewThread } = this.state; - const { currentUser, client } = this.props; - - // start the input loading spinner - this.setState({ - searchIsLoading: true, - }); - - client - .query({ - query: searchUsersQuery, - variables: { - queryString: string, - type: 'USERS', - }, - }) - .then(({ data: { search } }) => { - if ( - !search || - !search.searchResultsConnection || - search.searchResultsConnection.edges.length === 0 - ) { - this.setState({ - searchResults: [], - searchIsLoading: false, - focusedSearchResult: '', - }); - } - // if we return users from the search query, stop the loading - // spinner, populate the searchResults array, and focus the first - // result - // create an array of user ids if the user has already selected people - // for the thread - const selectedUsersIds = - selectedUsersForNewThread && - selectedUsersForNewThread.map(user => user.id); - - let searchUsers = search.searchResultsConnection.edges.map(s => s.node); - - // filter the search results to only show users who aren't already selected - // then filter that list to remove the currentUser so you can't message yourself - - let searchResults = selectedUsersForNewThread - ? searchUsers - .filter(user => selectedUsersIds.indexOf(user.id) < 0) - .filter(user => user.id !== currentUser.id) - : searchUsers.filter(user => user.id !== currentUser.id); - - this.setState({ - // if the search results are totally filtered out of the selectedUsers, - // return an empty array - searchResults: searchResults.length > 0 ? searchResults : [], - searchIsLoading: false, - // if all results are filtered, clear the focused search result - focusedSearchResult: - searchResults.length > 0 ? searchResults[0].id : '', - }); - return; - }) - .catch(err => { - console.error('Error searching users', err); - }); - }; - - handleKeyPress = (e: any) => { - // if the thread slider is open, we shouldn't be doing anything in DMs - if (this.props.threadSliderIsOpen) return; - if (this.state.chatInputIsFocused) return; - - // destructure the whole state object - const { - searchString, - searchResults, - selectedUsersForNewThread, - focusedSearchResult, - focusedSelectedUser, - chatInputIsFocused, - } = this.state; - - // create a reference to the input - we will use this to call .focus() - // after certain events (like pressing backspace or enter) - const input = this.input; - - // create temporary arrays of IDs from the searchResults and selectedUsers - // to more easily manipulate the ids - const searchResultIds = searchResults && searchResults.map(user => user.id); - - const indexOfFocusedSearchResult = searchResultIds.indexOf( - focusedSearchResult - ); - - /* - if a user presses backspace - 1. Determine if they have focused on a selectedUser pill - if so, they - are trying to delete it - 2. Determine if there are any more characters left in the search string. - If so, they are just typing a search query as normal - 3. If there are no more characters left in the search string, we need - to check if the user has already selected people to message. If so, - we remove the last one in the array - 4. If no more characters are in the search query, and no users are - selected to be messaged, we can just return and clear out unneeded - state - */ - if (e.keyCode === BACKSPACE) { - // 0. if the chat input is focused, don't do anything - if (chatInputIsFocused) return; - - // 1. If there is a selectedUser that has been focused, delete it - if (focusedSelectedUser) { - const newSelectedUsers = selectedUsersForNewThread.filter( - user => user.id !== focusedSelectedUser - ); - - this.setState({ - selectedUsersForNewThread: newSelectedUsers, - focusedSelectedUser: '', - existingThreadBasedOnSelectedUsers: '', - existingThreadWithMessages: {}, - }); - - // recheckfor an existing direct message thread on the server - this.getMessagesForExistingDirectMessageThread(); - - // focus the search input - // $FlowFixMe - input && input.focus(); - - return; - } - - // 2. If there are more characters left in the search string - if (searchString.length > 0) return; - - // 3. The user is trying to delete selected users. If one isn't selected, - // select it. - // Note: If the user presses backspace again it will trigger step #1 - // above - if (selectedUsersForNewThread.length > 0 && !focusedSelectedUser) { - // recheck for an existing thread if the user stops searching but - // still has selected users for the new thread - this.getMessagesForExistingDirectMessageThread(); - - const focused = - selectedUsersForNewThread[selectedUsersForNewThread.length - 1].id; - - this.setState({ - focusedSelectedUser: focused, - }); - - return; - } - - // 4 - // $FlowFixMe - input && input.focus(); - return; - } - - /* - If the person presses escape: - 1. If there are focused selected users, clear them - 2. If there are search results, clear them to hide the dropdown - */ - if (e.keyCode === ESC) { - // 0. if the chat input is focused, don't do anything - if (chatInputIsFocused) return; - - this.setState({ - searchResults: [], - searchIsLoading: false, - loadingExistingThreadMessages: false, - focusedSelectedUser: '', - }); - - // $FlowFixMe input && input.focus(); - return; - } - - /* - if person presses down - 1. If the user is at the last item in the search results, don't - do anything - 2. Focus the next user in the search results - */ - if (e.keyCode === ARROW_DOWN) { - // 0. if the chat input is focused, don't do anything - if (chatInputIsFocused) return; - - // 1 - if (indexOfFocusedSearchResult === searchResults.length - 1) return; - if (searchResults.length === 1) return; - - // 2 - this.setState({ - focusedSearchResult: searchResults[indexOfFocusedSearchResult + 1].id, - }); - - return; - } - - /* - if person presses up - 1. If the user is at the first`` item in the search results, don't - do anything - 2. Focus the previous user in the search results - */ - if (e.keyCode === ARROW_UP) { - // 0. if the chat input is focused, don't do anything - if (chatInputIsFocused) return; - - // 1 - if (indexOfFocusedSearchResult === 0) return; - if (searchResults.length === 1) return; - - // 2 - this.setState({ - focusedSearchResult: searchResults[indexOfFocusedSearchResult - 1].id, - }); - - return; - } - - /* - if person presses enter - 1. If there are search results and one of them is focused, add that user - to the selectedUsers state, clear the searchString, clear the searchResults, - and stop loading. Then kick off a new search to see if there is an - existing thread containing the selected users - 2. Otherwise do nothing - */ - if (e.keyCode === ENTER) { - // 0. if the chat input is focused, don't do anything - if (chatInputIsFocused) return; - if (!searchResults || searchResults.length === 0) return; - - // 1 - this.addUserToSelectedUsersList( - searchResults[indexOfFocusedSearchResult] - ); - return; - } - }; - - setFocusedSelectedUser = (id: string) => { - this.setState({ - focusedSelectedUser: id, - }); - - return; - }; - - addUserToSelectedUsersList = (user: Object) => { - const { selectedUsersForNewThread } = this.state; - - // add the new user to the state array - selectedUsersForNewThread.push(user); - this.setState({ - selectedUsersForNewThread, - searchResults: [], - searchString: '', - focusedSearchResult: '', - searchIsLoading: false, - existingThreadBasedOnSelectedUsers: '', - existingThreadWithMessages: {}, - }); - - // trigger a new search for an existing thread - this.getMessagesForExistingDirectMessageThread(); - }; - - handleChange = (e: any) => { - const { - existingThreadBasedOnSelectedUsers, - chatInputIsFocused, - } = this.state; - if (chatInputIsFocused) return; - - // unfocus any selected user pills - this.setState({ - focusedSelectedUser: '', - }); - - // if a user keeps typing, assume they aren't trying to message a different - // set of people - if (existingThreadBasedOnSelectedUsers) { - this.setState({ - loadingExistingThreadMessages: false, - }); - } - - const string = e.target.value.toLowerCase().trim(); - - // set the searchstring to state - this.setState({ - searchIsLoading: true, - searchString: e.target.value, - }); - - // trigger a new search based on the search input - // $FlowIssue - this.search(string); - }; - - /* - This method is used to determine if the selected users in the new thread - being composed match an existing DM thread for the current user. If we - find a match, we should load the messages for that thread and prepare - the chatInput to send any messages to that existing thread. - - If no matches are found, we will return a falsey value which will tell - the chat input that it is creating a new thread based on the current - array of selectedUsers in the state - */ - getMessagesForExistingDirectMessageThread = () => { - const { threads, currentUser, client } = this.props; - const { selectedUsersForNewThread } = this.state; - - if (!threads) { - return; - } - - // user hasn't created any dm threads yet, - if (threads && threads.length === 0) { - return; - } - - // if there are no selected users in the thread - if (selectedUsersForNewThread.length === 0) { - this.setState({ - existingThreadBasedOnSelectedUsers: '', - loadingExistingThreadMessages: false, - }); - - return; - } - - /* - If we made it here it means that the user has selected people to message - in the composer and that they have some existing threads that were - already returned from the server. What we need to do now is determine - if the selectedUsers in the composer exactly match the users of an - existing thread. - - We'll do this by: - 1. Creating a new array of the user's existing DM threads with the - following shape: - { - id - users: [ id ] - } - where the users array does *not* contain the currentUser id. It has - to be cleared becaues the composer input does *not* contain the current - user. - - 2. For each of these threads, we'll sort the users, sort the composer's - selected users and look for a match. - */ - - // 1. Create a new array of cleaned up threads objects - const cleanedExistingThreads = threads.map(thread => { - return { - id: thread.id, - participants: thread.participants - .filter(user => user.userId !== currentUser.id) - .map(user => user.userId), - }; - }); - - // 2. Sort both arrays of user IDs and look for a match - const sortedSelectedUsersForNewThread = selectedUsersForNewThread - .map(user => user.id) - .sort() - .join(''); - - // will return null or an object - const existingThread = cleanedExistingThreads.filter(thread => { - const sortedUsers = thread.participants.sort().join(''); - - if (sortedUsers === sortedSelectedUsersForNewThread) { - return thread; - } else { - return null; - } - }); - - // if an existing thread was found, set it to the state and get the messages - // from the server - if (existingThread.length > 0) { - this.setState({ - loadingExistingThreadMessages: true, - existingThreadBasedOnSelectedUsers: existingThread[0].id, - }); - - client - .query({ - query: getDirectMessageThreadQuery, - variables: { - id: existingThread[0].id, - }, - }) - .then( - ({ - data: { directMessageThread }, - }: { - data: { directMessageThread: GetDirectMessageThreadType }, - }) => { - // stop loading - this.setState({ - loadingExistingThreadMessages: false, - }); - - // if messages were found - if (directMessageThread.id) { - return this.setState({ - existingThreadWithMessages: directMessageThread, - }); - // if no messages were found - } else { - return this.setState({ - existingThreadWithMessages: {}, - existingThreadBasedOnSelectedUsers: '', - }); - } - } - ) - .catch(err => { - console.error('Error finding existing conversation: ', err); - }); - } - }; - - /* - Add event listeners when the component mounts - will be listening - for up, down, backspace, escape, and enter, to trigger different - functions depending on the context or state of the composer - */ - componentDidMount() { - document.addEventListener('keydown', this.handleKeyPress, false); - - // can take an optional param of an array of user objects to automatically - // populate the new message composer - const { initNewThreadWithUser, threadSliderIsOpen } = this.props; - - // if the prop is present, add the users to the selected users state - if (initNewThreadWithUser.length > 0) { - this.setState( - { - selectedUsersForNewThread: [...initNewThreadWithUser], - }, - () => { - // clear the redux store of this inited user, in case the person - // sends more messages later in the session - this.props.dispatch(clearDirectMessagesComposer()); - - if (this.state.selectedUsersForNewThread.length > 0) { - // trigger a new search for an existing thread with these users - this.getMessagesForExistingDirectMessageThread(); - } - } - ); - } - - // if someone is viewing a thread, don't focus here - if (threadSliderIsOpen) return; - - // focus the composer input if no users were already in the composer - if (initNewThreadWithUser.length === 0) { - const input = this.input; - // $FlowFixMe - return input && input.focus(); - } - - this.chatInput.focus(); - } - - componentWillUnmount() { - document.removeEventListener('keydown', this.handleKeyPress, false); - } - - componentDidUpdate() { - this.forceScrollToBottom(); - } - - forceScrollToBottom = () => { - if (!this.scrollBody) return; - let node = this.scrollBody; - node.scrollTop = node.scrollHeight - node.clientHeight; - }; - - createThread = ({ messageBody, messageType, file }) => { - const { selectedUsersForNewThread, threadIsBeingCreated } = this.state; - - // if no users have been selected, break out of this function and throw - // an error - if (selectedUsersForNewThread.length === 0) { - this.props.dispatch( - addToastWithTimeout( - 'error', - 'Choose some people to send this message to first!' - ) - ); - return Promise.reject(); - } - - const input = { - participants: selectedUsersForNewThread.map(user => user.id), - message: { - messageType: messageType, - threadType: 'directMessageThread', - content: { - body: messageBody ? messageBody : '', - }, - file: file ? file : null, - }, - }; - - if (threadIsBeingCreated) { - return Promise.resolve(); - } else { - this.setState({ - threadIsBeingCreated: true, - }); - - return this.props - .createDirectMessageThread(input) - .then(({ data: { createDirectMessageThread } }) => { - if (!createDirectMessageThread) { - this.props.dispatch( - addToastWithTimeout( - 'error', - 'Failed to create direct message thread, please try again!' - ) - ); - return; - } - - this.setState({ - threadIsBeingCreated: false, - }); - - this.props.history.push(`/messages/${createDirectMessageThread.id}`); - return; - }) - .catch(err => { - // if an error happened, the user can try to resend the message to - // create a new thread - this.setState({ - threadIsBeingCreated: false, - }); - - this.props.dispatch(addToastWithTimeout('error', err.message)); - }); - } - }; - - onChatInputFocus = () => { - this.setState({ - chatInputIsFocused: true, - }); - }; - - onChatInputBlur = () => { - this.setState({ - chatInputIsFocused: false, - }); - }; - - render() { - const { - searchString, - selectedUsersForNewThread, - searchIsLoading, - searchResults, - focusedSelectedUser, - focusedSearchResult, - existingThreadBasedOnSelectedUsers, - loadingExistingThreadMessages, - existingThreadWithMessages, - } = this.state; - const { currentUser, hideOnMobile, threads } = this.props; - - const { title, description } = generateMetaInfo({ - type: 'directMessage', - data: { - title: 'New message', - description: null, - }, - }); - - const haveThreads = threads && threads.length > 0; - - return ( - - - - {// if users have been selected, show them as pills - selectedUsersForNewThread.length > 0 && ( - - {selectedUsersForNewThread.map(user => { - return ( - this.setFocusedSelectedUser(user.id)} - key={user.id} - data-cy="selected-user-pill" - > - {user.name} - - ); - })} - - )} - - {searchIsLoading && ( - - - - )} - - { - this.input = c; - }} - type="text" - value={searchString} - placeholder="Search for people..." - onChange={this.handleChange} - /> - - {// user has typed in a search string - searchString && ( - //if there are selected users already, we manually shift - // the search results position down - 0}> - {searchResults.length > 0 && - searchResults.map(user => { - return ( - this.addUserToSelectedUsersList(user)} - > - - - - {user.name} - - {user.username && ( - - @{user.username} - - )} - - - ); - })} - - {searchResults.length === 0 && ( - - - - {searchIsLoading - ? `Searching for "${searchString}"` - : `No users found matching “${searchString}”`} - - - - )} - - )} - - - 0} - innerRef={scrollBody => (this.scrollBody = scrollBody)} - > - {existingThreadWithMessages && existingThreadWithMessages.id && ( -
- )} - - {existingThreadBasedOnSelectedUsers && ( - - )} - - {!existingThreadBasedOnSelectedUsers && ( - - {haveThreads && ( - - - - )} - {!haveThreads && ( - - - - )} - {loadingExistingThreadMessages && ( - - )} - - )} - - (this.chatInput = chatInput)} - /> - - ); - } -} - -const mapStateToProps = state => ({ - initNewThreadWithUser: state.directMessageThreads.initNewThreadWithUser, - threadSliderIsOpen: state.threadSlider.isOpen, -}); - -export default compose( - withApollo, - withRouter, - createDirectMessageThreadMutation, - withCurrentUser, - // $FlowIssue - connect(mapStateToProps) -)(NewThread); diff --git a/src/views/directMessages/index.js b/src/views/directMessages/index.js index 7b6d994a0d..c937caf32e 100644 --- a/src/views/directMessages/index.js +++ b/src/views/directMessages/index.js @@ -1,12 +1,12 @@ // @flow import React from 'react'; import Loadable from 'react-loadable'; -import LoadingDMs from './components/loading'; +import { LoadingView } from 'src/views/viewHelpers'; /* prettier-ignore */ const DirectMessages = Loadable({ loader: () => import('./containers/index.js'/* webpackChunkName: "DirectMessages" */), - loading: ({ isLoading }) => isLoading && , + loading: ({ isLoading }) => isLoading && , }); export default DirectMessages; diff --git a/src/views/directMessages/style.js b/src/views/directMessages/style.js index d75e7d8eb0..84789f93cb 100644 --- a/src/views/directMessages/style.js +++ b/src/views/directMessages/style.js @@ -1,46 +1,26 @@ // @flow import theme from 'shared/theme'; import styled, { css } from 'styled-components'; -import { FlexCol, FlexRow } from '../../components/globals'; +import { SecondaryColumn, MEDIA_BREAK } from 'src/components/layout'; export const View = styled.main` - display: flex; - flex-direction: row; - justify-content: flex-start; - align-items: stretch; - background: #fff; - flex: auto; - height: calc(100vh - 48px); - - @media (max-width: 768px) { - flex-direction: column; - flex: auto; - } + grid-area: main; + display: grid; + grid-template-columns: minmax(320px, 400px) 1fr; `; -export const ViewContent = styled(FlexCol)` +export const ViewContent = styled.div` display: flex; flex-direction: column; - flex: auto; - overflow-y: auto; - align-items: center; - align-content: flex-start; + justify-content: flex-end; + flex: 1; `; -export const MessagesList = styled(FlexCol)` - position: relative; - overflow-y: auto; - overflow-x: hidden; - max-width: 400px; - flex: 0 0 25%; - min-width: 320px; +export const MessagesList = styled.div` background: ${theme.bg.default}; border-right: 1px solid ${theme.bg.border}; - flex: 1 0 auto; - max-height: 100%; - @media (max-width: 768px) { - max-height: calc(100% - 48px); + @media (max-width: ${MEDIA_BREAK}px) { min-width: 320px; border-right: none; max-width: 100%; @@ -48,29 +28,14 @@ export const MessagesList = styled(FlexCol)` } `; -export const MessagesContainer = styled(FlexCol)` - flex: auto; - max-height: 100%; - - @media (min-width: 768px) { - ${props => - props.hideOnDesktop && - css` - display: none; - `}; - } - - @media (max-width: 768px) { - max-height: calc(100% - 48px); - ${props => - props.hideOnMobile && - css` - display: none; - `}; - } +export const MessagesContainer = styled.div` + display: flex; + flex-direction: column; + background: ${theme.bg.default}; + flex: 1; `; -export const NoThreads = MessagesContainer.extend` +export const NoThreads = styled(MessagesContainer)` position: absolute; top: 50%; width: 100%; @@ -82,13 +47,74 @@ export const NoThreads = MessagesContainer.extend` } `; -export const ComposeHeader = styled(FlexRow)` +export const ComposeHeader = styled.div` + position: sticky; + top: 0; + z-index: 10; justify-content: flex-end; padding: 8px; border-bottom: 1px solid ${theme.bg.border}; color: ${theme.brand.default}; - @media (max-width: 768px) { + @media (max-width: ${MEDIA_BREAK}px) { display: none; } `; + +export const StyledSecondaryColumn = styled(SecondaryColumn)` + border-left: 1px solid ${theme.bg.border}; + border-right: 1px solid ${theme.bg.border}; + padding-right: 0; + padding-bottom: 0; + + @media (max-width: ${MEDIA_BREAK}px) { + border-left: 0; + border-right: 0; + display: grid; + display: ${props => (props.shouldHideThreadList ? 'none' : 'block')}; + } +`; + +export const NoCommunitySelected = styled.div` + display: flex; + height: 100vh; + align-items: center; + justify-content: center; + flex-direction: column; + text-align: center; + padding: 24px; + background: ${theme.bg.default}; + + button { + flex: 1; + } + + @media (min-width: ${MEDIA_BREAK}px) { + ${props => + props.hideOnDesktop && + css` + display: none; + `} + } + + @media (max-width: ${MEDIA_BREAK}px) { + display: none; + } +`; + +export const NoCommunityHeading = styled.h3` + font-size: 24px; + font-weight: 700; + line-height: 1.3; + margin-bottom: 8px; + color: ${theme.text.default}; +`; +export const NoCommunitySubheading = styled.p` + margin-top: 8px; + font-size: 16px; + font-weight: 400; + line-height: 1.4; + color: ${theme.text.secondary}; + padding-right: 24px; + margin-bottom: 24px; +`; diff --git a/src/views/explore/collections.js b/src/views/explore/collections.js index 6e2aa7b365..c1a49608fa 100644 --- a/src/views/explore/collections.js +++ b/src/views/explore/collections.js @@ -28,142 +28,90 @@ export const collections = [ { title: 'Design', curatedContentType: 'design-communities', - categories: [ - { - title: 'Shop Talk', - communities: [ - 'product-design', - 'icon-design', - 'typography', - 'illustrators', - 'design-management', - ], - }, - { - title: 'Resources', - communities: [ - 'specfm', - 'up-coming', - 'sketchcasts', - 'google-design', - 'design-code', - 'codepen', - 'vectors', - 'designhunt', - ], - }, - { - title: 'Tools', - communities: [ - 'figma', - 'sketch', - 'framer', - 'abstract', - 'invision', - 'compositor', - 'zeplin', - 'origami-studio', - 'fuse', - ], - }, + communities: [ + 'product-design', + 'icon-design', + 'typography', + 'illustrators', + 'design-management', + 'specfm', + 'up-coming', + 'sketchcasts', + 'google-design', + 'design-code', + 'codepen', + 'vectors', + 'designhunt', + 'figma', + 'sketch', + 'framer', + 'abstract', + 'invision', + 'compositor', + 'zeplin', + 'origami-studio', + 'fuse', ], }, { title: 'Web development', curatedContentType: 'development-communities', - categories: [ - { - title: 'Web Frameworks', - communities: [ - 'react', - 'next-js', - 'node', - 'codesandbox', - 'vue-js', - 'angular', - 'ember-js', - 'laravel', - 'elixir', - 'styled-components', - 'graphql', - 'css-in-js', - 'electron', - ], - }, - { - title: 'Native', - communities: ['android', 'swiftdev', 'react-native'], - }, - { - title: 'Resources', - communities: ['frontend', 'specfm'], - }, - { - title: 'Tools', - communities: [ - 'zeit', - 'realm', - 'expo', - 'compositor', - 'codepen', - 'bootstrap', - 'tachyons', - ], - }, + communities: [ + 'react', + 'next-js', + 'node', + 'codesandbox', + 'vue-js', + 'angular', + 'ember-js', + 'laravel', + 'elixir', + 'styled-components', + 'graphql', + 'css-in-js', + 'electron', + 'android', + 'swiftdev', + 'react-native', + 'frontend', + 'specfm', + 'zeit', + 'realm', + 'expo', + 'compositor', + 'codepen', + 'bootstrap', + 'tachyons', + 'divjoy' ], }, - // { - // title: 'For Product Managers', - // categories: [ - // { - // title: 'Product Talk', - // communities: ['product-management', 'user-research'], - // }, - // { - // title: 'Tools', - // communities: ['superhuman', 'deckset'], - // }, - // ], - // }, { title: 'Tech', curatedContentType: 'tech-communities', - categories: [ - { - title: 'Get the latest news', - communities: ['tech-tea'], - }, - { - title: 'Get on the blockchain', - communities: ['balancemymoney', 'crypto', 'btc', 'ethereum'], - }, - { - title: 'Explore the future of interfaces', - communities: ['augmented-reality', 'voice-interfaces'], - }, + communities: [ + 'tech-tea', + 'balancemymoney', + 'crypto', + 'btc', + 'ethereum', + 'augmented-reality', + 'voice-interfaces', ], }, { title: 'Life', curatedContentType: 'life-communities', - categories: [ - { - title: 'Do good, feel good', - communities: [ - 'for-good', - 'mental-health', - 'dev-fit', - // '5calls' - ], - }, - { - title: 'Share your hobbies', - communities: ['music', 'photography', 'tabletop-rpg', 'gaming'], - }, - { - title: 'Find a new gig', - communities: ['careers', 'job-opportunities', 'need-some-work'], - }, + communities: [ + 'for-good', + 'mental-health', + 'dev-fit', + 'music', + 'photography', + 'tabletop-rpg', + 'gaming', + 'careers', + 'job-opportunities', + 'need-some-work', ], }, ]; diff --git a/src/views/explore/components/communitySearchWrapper.js b/src/views/explore/components/communitySearchWrapper.js index 65d5d49852..a401a0c0f1 100644 --- a/src/views/explore/components/communitySearchWrapper.js +++ b/src/views/explore/components/communitySearchWrapper.js @@ -1,65 +1,58 @@ // @flow -import theme from 'shared/theme'; import React from 'react'; import styled from 'styled-components'; import { Link } from 'react-router-dom'; -import { Transition, zIndex, Shadow, hexa } from '../../../components/globals'; -import ViewSegment from '../../../components/themedSection'; -import { Button } from '../../../components/buttons'; -import { CLIENT_URL } from '../../../api/constants'; -import { Tagline, Copy, Content } from '../../pages/style'; +import theme from 'shared/theme'; +import { Primary } from 'src/components/themedSection'; +import { Constellations } from 'src/components/illustrations'; +import { Button } from 'src/components/button'; +import { Tagline, Copy } from 'src/views/pages/style'; import { track, events } from 'src/helpers/analytics'; +import { MEDIA_BREAK } from 'src/components/layout'; // $FlowFixMe const CommunitySearchWrapper = props => { - const ThisContent = styled(Content)` + const ThisContent = styled.div` flex-direction: column; - width: 640px; + display: flex; + max-width: 640px; + justify-self: center; + justify-content: center; align-content: center; align-self: center; margin-top: 40px; margin-bottom: 0; padding: 16px; - padding-bottom: 48px; + padding-bottom: 72px; + text-align: center; - @media (max-width: 640px) { - margin-top: 80px; + @media (max-width: ${MEDIA_BREAK}px) { margin-bottom: 0; width: 100%; - } - `; - - const PrimaryCTA = styled(Button)` - padding: 12px 16px; - font-weight: 700; - font-size: 14px; - border-radius: 12px; - background-color: ${theme.bg.default}; - background-image: none; - color: ${theme.brand.alt}; - transition: ${Transition.hover.off}; - z-index: ${zIndex.card}; - - &:hover { - background-color: ${theme.bg.default}; - color: ${theme.brand.default}; - box-shadow: ${Shadow.high} ${props => hexa(props.theme.bg.reverse, 0.5)}; - transition: ${Transition.hover.on}; + text-align: left; + justify-self: flex-start; + justify-content: flex-start; } `; const SecondaryContent = styled(ThisContent)` - margin-top: 0; + margin-top: 32px; margin-bottom: 0; + padding: 0; + + button { + flex: 1; + } `; const ThisTagline = styled(Tagline)` margin-bottom: 0; + font-weight: 800; `; const SecondaryTagline = styled(ThisTagline)` font-size: 20px; - font-weight: 900; + font-weight: 700; margin-top: 0; margin-bottom: 2px; `; @@ -71,7 +64,7 @@ const CommunitySearchWrapper = props => { text-align: center; max-width: 640px; - @media (max-width: 768px) { + @media (max-width: ${MEDIA_BREAK}px) { text-align: left; } `; @@ -81,35 +74,35 @@ const CommunitySearchWrapper = props => { `; return ( - + - Find a community for you! + Find a community - Try searching for topics like "crypto" or for products like "React" + Try searching for topics like “crypto” or for products like “React” {props.children} - ...or create your own community + Create your own community Building communities on Spectrum is easy and free! - {props.currentUser ? ( - - track(events.EXPLORE_PAGE_CREATE_COMMUNITY_CLICKED) - } - > - Get Started - - ) : ( - - Get Started - - )} + + - + + ); }; diff --git a/src/views/explore/components/search.js b/src/views/explore/components/search.js index c238a10d83..afb78cc43c 100644 --- a/src/views/explore/components/search.js +++ b/src/views/explore/components/search.js @@ -5,7 +5,7 @@ import { withRouter } from 'react-router'; import { connect } from 'react-redux'; import compose from 'recompose/compose'; import { Link } from 'react-router-dom'; -import { Button } from 'src/components/buttons'; +import { Button } from 'src/components/button'; import { debounce } from 'src/helpers/utils'; import { searchCommunitiesQuery } from 'shared/graphql/queries/search/searchCommunities'; import type { SearchCommunitiesType } from 'shared/graphql/queries/search/searchCommunities'; @@ -45,7 +45,7 @@ type Props = { }; class Search extends React.Component { - input: React.Node; + input: React$Node; constructor() { super(); @@ -241,7 +241,7 @@ class Search extends React.Component { { + ref={c => { this.input = c; }} type="text" diff --git a/src/views/explore/index.js b/src/views/explore/index.js index 17c44cb071..d0284aad39 100644 --- a/src/views/explore/index.js +++ b/src/views/explore/index.js @@ -3,23 +3,25 @@ import * as React from 'react'; import { connect } from 'react-redux'; import compose from 'recompose/compose'; import generateMetaInfo from 'shared/generate-meta-info'; -import Titlebar from '../titlebar'; -import AppViewWrapper from 'src/components/appViewWrapper'; import Head from 'src/components/head'; import Search from './components/search'; import CommunitySearchWrapper from './components/communitySearchWrapper'; -import { Wrapper } from './style'; import { Charts } from './view'; import { track, events } from 'src/helpers/analytics'; import { ErrorBoundary } from 'src/components/error'; import { withCurrentUser } from 'src/components/withCurrentUser'; +import { ViewGrid } from 'src/components/layout'; +import { setTitlebarProps } from 'src/actions/titlebar'; type Props = { currentUser?: Object, + dispatch: Function, }; class Explore extends React.Component { componentDidMount() { + const { dispatch } = this.props; + dispatch(setTitlebarProps({ title: 'Explore' })); track(events.EXPLORE_PAGE_VIEWED); } @@ -33,11 +35,10 @@ class Explore extends React.Component { // const featureNotes = `Crypto is a place to discuss crypto-currencies and tokens. As blockchain technology becomes more and more mainstream, communities like Crypto allow more people to get involved, learn, and share what they know. We're all for that, so if you're an existing investor, a newcomer to crypto-currencies, or just interested in learning about blockchain, check out Crypto!`; return ( - - - - - + + + + { - - + + ); } } diff --git a/src/views/explore/style.js b/src/views/explore/style.js index b83cb24eb7..6c65803381 100644 --- a/src/views/explore/style.js +++ b/src/views/explore/style.js @@ -1,8 +1,6 @@ // @flow import theme from 'shared/theme'; -// $FlowFixMe import styled from 'styled-components'; -// $FlowFixMe import { Link } from 'react-router-dom'; import { FlexCol, @@ -12,33 +10,17 @@ import { H3, P, Transition, - Gradient, Shadow, hexa, Truncate, zIndex, -} from '../../components/globals'; -import Card from '../../components/card'; -import { StyledCard } from '../../components/listItems/style'; -import Icon from '../../components/icons'; -import { CommunityAvatar } from '../../components/avatar'; -import ScrollRow from '../../components/scrollRow'; - -import { Button } from '../../components/buttons'; - -export const Wrapper = styled.main` - display: flex; - flex-direction: column; - justify-content: flex-start; - align-items: stretch; - flex: 1 0 auto; - width: 100%; - background-color: ${theme.bg.default}; - overflow: auto; - overflow-x: hidden; - z-index: ${zIndex.base}; - align-self: stretch; -`; +} from 'src/components/globals'; +import Card from 'src/components/card'; +import { StyledCard } from 'src/components/listItems/style'; +import Icon from 'src/components/icon'; +import { CommunityAvatar } from 'src/components/avatar'; +import ScrollRow from 'src/components/scrollRow'; +import { MEDIA_BREAK } from 'src/components/layout'; export const ViewTitle = styled(H1)` margin-left: 48px; @@ -49,7 +31,7 @@ export const ViewTitle = styled(H1)` position: relative; z-index: ${zIndex.base}; - @media (max-width: 768px) { + @media (max-width: ${MEDIA_BREAK}px) { margin-left: 16px; margin-top: 16px; } @@ -61,7 +43,7 @@ export const ViewSubtitle = styled(H2)` position: relative; z-index: ${zIndex.base}; - @media (max-width: 768px) { + @media (max-width: ${MEDIA_BREAK}px) { margin-left: 16px; font-size: 16px; line-height: 20px; @@ -70,7 +52,7 @@ export const ViewSubtitle = styled(H2)` export const ListCard = styled(StyledCard)` padding: 0; - @media (max-width: 768px) { + @media (max-width: ${MEDIA_BREAK}px) { display: flex; margin-bottom: 32px; } @@ -85,10 +67,6 @@ export const Section = styled(FlexCol)` position: relative; z-index: ${zIndex.base}; align-self: stretch; - - @media (max-width: 768px) { - padding: 0; - } `; export const SectionWrapper = styled(FlexRow)` @@ -96,7 +74,7 @@ export const SectionWrapper = styled(FlexRow)` align-items: flex-start; justify-content: center; - @media (max-width: 768px) { + @media (max-width: ${MEDIA_BREAK}px) { flex-direction: column; } `; @@ -112,7 +90,7 @@ export const ViewHeader = styled(Section)` 0.75 )}, ${theme.space.dark} )`}; - @media (max-width: 768px) { + @media (max-width: ${MEDIA_BREAK}px) { padding: 48px 24px 0 24px; } `; @@ -121,7 +99,7 @@ export const SectionWithGradientTransition = styled(Section)` background-image: ${({ theme }) => `linear-gradient(${theme.bg.default}, ${theme.bg.wash})`}; - @media (max-width: 768px) { + @media (max-width: ${MEDIA_BREAK}px) { padding: 32px; } `; @@ -133,7 +111,7 @@ export const SectionTitle = styled(H2)` margin-bottom: 16px; font-weight: 800; - @media (max-width: 768px) { + @media (max-width: ${MEDIA_BREAK}px) { font-size: 24px; } `; @@ -143,7 +121,7 @@ export const SectionSubtitle = styled(H3)` margin-bottom: 8px; margin-left: 48px; - @media (max-width: 768px) { + @media (max-width: ${MEDIA_BREAK}px) { margin-left: 16px; } `; @@ -202,32 +180,6 @@ export const ItemMeta = styled(ItemCopy)` color: ${theme.text.placeholder}; `; -export const ButtonContainer = styled(FlexRow)` - justify-content: ${props => (props.center ? 'center' : 'flex-end')}; - align-items: center; - - > div { - margin-right: 8px; - } -`; - -export const ItemButton = styled(Button)` - font-weight: 700; - color: ${theme.text.reverse}; - background-color: ${props => - props.joined ? props.theme.bg.inactive : props.theme.brand.default}; - background-image: ${props => - props.joined - ? 'none' - : Gradient(props.theme.brand.alt, props.theme.brand.default)}; - box-shadow: none; - transition: ${Transition.hover.on}; - - &:hover { - box-shadow: none; - } -`; - export const Constellations = styled.div` background-color: transparent; background: url(/img/constellations.svg) center top no-repeat; @@ -266,7 +218,7 @@ export const SearchWrapper = styled(Card)` padding: 12px 16px; box-shadow: ${Shadow.low} ${props => hexa(props.theme.bg.reverse, 0.15)}; transition: ${Transition.hover.off}; - z-index: ${zIndex.search}; + z-index: 14; border-radius: 8px; &:hover { @@ -284,13 +236,10 @@ export const SearchIcon = styled(Icon)``; export const SearchInput = styled.input` font-size: 16px; - padding: 4px 20px; + padding: 4px 20px 4px 12px; flex: auto; position: relative; z-index: ${zIndex.search}; - - &:hover { - } `; export const SearchSpinnerContainer = styled.span` @@ -315,10 +264,7 @@ export const SearchResultsDropdown = styled.ul` max-height: 400px; overflow-y: auto; background: ${theme.bg.default}; - - @media (max-width: 768px) { - border-radius: 0 0 8px 8px; - } + z-index: 12; `; export const SearchResultTextContainer = styled.div` @@ -330,31 +276,12 @@ export const SearchResultTextContainer = styled.div` export const SearchResult = styled.li` display: flex; background: ${props => - props.focused ? props.theme.brand.alt : props.theme.bg.default}; - border-bottom: 2px solid - ${props => (props.focused ? 'transparent' : props.theme.bg.border)}; + props.focused ? props.theme.bg.wash : props.theme.bg.default}; + border-bottom: 1px solid ${theme.bg.border}; &:hover { - background: ${theme.brand.alt}; + background: ${theme.bg.wash}; cursor: pointer; - - h2 { - color: ${theme.text.reverse}; - } - - p { - color: ${theme.text.reverse}; - } - } - - h2 { - color: ${props => - props.focused ? props.theme.text.reverse : props.theme.text.default}; - } - - p { - color: ${props => - props.focused ? props.theme.text.reverse : props.theme.text.alt}; } &:only-child { @@ -377,18 +304,21 @@ export const SearchResultImage = styled(CommunityAvatar)``; export const SearchResultMetaWrapper = styled(FlexCol)` margin-left: 16px; + align-items: flex-start; `; export const SearchResultName = styled.h2` font-size: 16px; font-weight: 700; line-height: 1.4; + color: ${theme.text.default}; `; export const SearchResultMetadata = styled.p` font-size: 14px; font-weight: 400; line-height: 1.4; + color: ${theme.text.secondary}; `; export const SearchResultNull = styled.div` @@ -435,15 +365,25 @@ export const ListTitle = styled(H2)` font-size: 18px; margin-top: 32px; - @media (max-width: 768px) { + @media (max-width: ${MEDIA_BREAK}px) { padding-left: 32px; } `; export const ListWrapper = styled(FlexRow)` display: grid; - grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); - align-items: stretch; + grid-template-columns: repeat(3, minmax(320px, 1fr)); + align-items: start; + grid-gap: 16px; + padding-bottom: 32px; + + @media (max-width: ${MEDIA_BREAK}px) { + grid-template-columns: repeat(2, minmax(320px, 1fr)); + } + + @media (max-width: 720px) { + grid-template-columns: 1fr; + } `; export const ListItem = styled(FlexRow)``; @@ -457,21 +397,21 @@ export const Collections = styled.div` export const CollectionWrapper = styled.div` display: flex; flex-direction: column; - padding: 0 32px; + padding: 32px; flex: auto; - @media (max-width: 768px) { - padding: 0; + @media (max-width: ${MEDIA_BREAK}px) { + padding: 16px; } `; -export const CategoryWrapper = styled.div` - display: flex; - flex-direction: column; - justify-content: flex-start; - flex: none; -`; - export const LoadingContainer = styled.div` padding: 32px; `; + +export const ProfileCardWrapper = styled.section` + background: ${theme.bg.default}; + border: 1px solid ${theme.bg.border}; + border-radius: 4px; + overflow: hidden; +`; diff --git a/src/views/explore/view.js b/src/views/explore/view.js index af2a12d4a2..c624422d24 100644 --- a/src/views/explore/view.js +++ b/src/views/explore/view.js @@ -1,10 +1,8 @@ // @flow -import theme from 'shared/theme'; import * as React from 'react'; import styled from 'styled-components'; import { connect } from 'react-redux'; import compose from 'recompose/compose'; -import { CommunityProfile } from 'src/components/profile'; import { collections } from './collections'; import viewNetworkHandler from 'src/components/viewNetworkHandler'; import { withCurrentUser } from 'src/components/withCurrentUser'; @@ -12,17 +10,18 @@ import { ListWithTitle, ListTitle, ListWrapper, - CategoryWrapper, Collections, CollectionWrapper, - LoadingContainer, + ProfileCardWrapper, } from './style'; import { getCommunitiesBySlug } from 'shared/graphql/queries/community/getCommunities'; import type { GetCommunitiesType } from 'shared/graphql/queries/community/getCommunities'; -import { Loading } from 'src/components/loading'; import { SegmentedControl, Segment } from 'src/components/segmentedControl'; import { track, transformations, events } from 'src/helpers/analytics'; import { ErrorBoundary } from 'src/components/error'; +import { Loading } from 'src/components/loading'; +import { ErrorView } from 'src/views/viewHelpers'; +import { CommunityProfileCard } from 'src/components/entities'; const ChartGrid = styled.div` display: flex; @@ -30,32 +29,26 @@ const ChartGrid = styled.div` flex: auto; `; -const ThisSegment = styled(Segment)` - @media (max-width: 768px) { - &:first-of-type { - color: ${theme.text.alt}; - border-bottom: 2px solid ${theme.bg.border}; - } - &:not(:first-of-type) { - display: none; - } - } -`; - export const Charts = () => { return {collections && }; }; -type Props = {}; type State = { selectedView: string, }; -class CollectionSwitcher extends React.Component { +class CollectionSwitcher extends React.Component<{}, State> { state = { selectedView: 'top-communities-by-members', }; + parentRef = null; + ref = null; + + componentDidMount() { + this.parentRef = document.getElementById('main'); + } + handleSegmentClick(selectedView) { if (this.state.selectedView === selectedView) return; @@ -66,44 +59,45 @@ class CollectionSwitcher extends React.Component { return this.setState({ selectedView }); } + componentDidUpdate(prevProps, prevState) { + const currState = this.state; + if (prevState.selectedView !== currState.selectedView) { + if (!this.parentRef || !this.ref) return; + return (this.parentRef.scrollTop = this.ref.offsetTop); + } + } + render() { return ( - + (this.ref = el)}> {collections.map((collection, i) => ( - this.handleSegmentClick(collection.curatedContentType) } - selected={ + isActive={ collection.curatedContentType === this.state.selectedView } > {collection.title} - + ))} {collections.map((collection, index) => { - // NOTE(@mxstbr): [].concat.apply([], ...) flattens the array - const communitySlugs = collection.categories - ? [].concat.apply( - [], - collection.categories.map(({ communities }) => communities) - ) - : collection.communities || []; + const communitySlugs = collection.communities; return ( - +
{collection.curatedContentType === this.state.selectedView && ( )} - +
); })}
@@ -120,7 +114,6 @@ type CategoryListProps = { communities?: GetCommunitiesType, }, isLoading: boolean, - categories?: Array, }; class CategoryList extends React.Component { onLeave = community => { @@ -141,8 +134,6 @@ class CategoryList extends React.Component { title, slugs, isLoading, - currentUser, - categories, } = this.props; if (communities) { @@ -155,72 +146,30 @@ class CategoryList extends React.Component { }); } - if (!categories) { - return ( - - {title ? {title} : null} - - {filteredCommunities.map((community, i) => ( - // $FlowFixMe - - - - ))} - - - ); - } - return ( -
- {categories.map((cat, i) => { - if (cat.communities) { - filteredCommunities = communities.filter(c => { - if (!c) return null; - if (cat.communities.indexOf(c.slug) > -1) return c; - return null; - }); - } - return ( - - {cat.title ? {cat.title} : null} - - {filteredCommunities.map((community, i) => ( - // $FlowFixMe - - - - ))} - - - ); - })} -
+ + {title ? {title} : null} + + {filteredCommunities.map( + (community, i) => + community && ( + + + + + + ) + )} + + ); } if (isLoading) { - return ( - - - - ); + return ; } - return null; + return ; } } diff --git a/src/views/globalTitlebar/index.js b/src/views/globalTitlebar/index.js new file mode 100644 index 0000000000..6fa1f0789e --- /dev/null +++ b/src/views/globalTitlebar/index.js @@ -0,0 +1,56 @@ +// @flow +import React from 'react'; +import compose from 'recompose/compose'; +import { withRouter, type History } from 'react-router-dom'; +import { connect } from 'react-redux'; +import { withCurrentUser } from 'src/components/withCurrentUser'; +import { MobileTitlebar } from 'src/components/titlebar'; +import { ErrorBoundary } from 'src/components/error'; +import { isViewingMarketingPage } from 'src/helpers/is-viewing-marketing-page'; + +export type TitlebarPayloadProps = { + title: string, + titleIcon?: React$Node, + rightAction?: React$Node, + leftAction?: React$Element<*> | 'menu' | 'view-back', +}; + +type TitlebarProps = { + ...$Exact, + history: History, + currentUser: ?Object, +}; + +const GlobalTitlebar = (props: TitlebarProps): React$Node => { + const { + title = 'Spectrum', + titleIcon = null, + rightAction = null, + leftAction = 'menu', + history, + currentUser, + } = props; + + if (isViewingMarketingPage(history, currentUser)) { + return null; + } + + return ( + }> + + + ); +}; + +const map = (state): * => state.titlebar; + +export default compose( + withRouter, + withCurrentUser, + connect(map) +)(GlobalTitlebar); diff --git a/src/views/homeViewRedirect/index.js b/src/views/homeViewRedirect/index.js new file mode 100644 index 0000000000..4206cdf28d --- /dev/null +++ b/src/views/homeViewRedirect/index.js @@ -0,0 +1,81 @@ +// @flow +import React from 'react'; +import { withRouter } from 'react-router'; +import compose from 'recompose/compose'; +import { + getCurrentUserCommunityConnection, + type GetUserCommunityConnectionType, +} from 'shared/graphql/queries/user/getUserCommunityConnection'; +import { SERVER_URL } from 'src/api/constants'; +import { LoadingView } from 'src/views/viewHelpers'; +import type { History } from 'react-router'; + +type Props = { + data: { + user: GetUserCommunityConnectionType, + loading: boolean, + }, + history: History, +}; + +const HomeViewRedirect = (props: Props) => { + const { data, history } = props; + const { user, loading } = data; + + if (loading) return ; + + // if the user slipped past our route fallback for signed in/out, force + // a logout and redirect back to the home page + if (!user) return history.replace(`${SERVER_URL}/auth/logout`); + + const { communityConnection } = user; + const { edges } = communityConnection; + const communities = edges.map(edge => edge && edge.node); + history.replace('/spectrum'); + // if the user hasn't joined any communities yet, help them find some + if (!communities || communities.length === 0) { + history.replace('/explore'); + return null; + } + + const recentlyActive = communities.filter(Boolean).sort((a, b) => { + if (!a.communityPermissions.lastSeen) return 1; + if (!b.communityPermissions.lastSeen) return -1; + + const x = new Date(a.communityPermissions.lastSeen).getTime(); + // $FlowIssue Flow you drunk + const y = new Date(b.communityPermissions.lastSeen).getTime(); + const val = y - x; + return val; + })[0]; + + if (recentlyActive) { + history.replace(`/${recentlyActive.slug}`); + return null; + } + + const sorted = communities + .slice() + .filter(Boolean) + .sort((a, b) => { + const bc = parseInt(b.communityPermissions.reputation, 10); + const ac = parseInt(a.communityPermissions.reputation, 10); + + // sort same-reputation communities alphabetically + if (ac === bc) { + return a.name.toUpperCase() <= b.name.toUpperCase() ? -1 : 1; + } + + // otherwise sort by reputation + return bc <= ac ? -1 : 1; + }); + + const first = sorted[0]; + history.replace(`/${first.slug}`); + return null; +}; + +export default compose( + getCurrentUserCommunityConnection, + withRouter +)(HomeViewRedirect); diff --git a/src/views/login/index.js b/src/views/login/index.js index 0cd4ae4f19..ba5a6c90b8 100644 --- a/src/views/login/index.js +++ b/src/views/login/index.js @@ -1,9 +1,10 @@ // @flow import * as React from 'react'; import { withRouter } from 'react-router'; +import { connect } from 'react-redux'; import compose from 'recompose/compose'; import { Link } from 'react-router-dom'; -import Icon from 'src/components/icons'; +import Icon from 'src/components/icon'; import FullscreenView from 'src/components/fullscreenView'; import LoginButtonSet from 'src/components/loginButtonSet'; import { @@ -16,17 +17,22 @@ import { import queryString from 'query-string'; import { track, events } from 'src/helpers/analytics'; import { CLIENT_URL } from 'src/api/constants'; +import { setTitlebarProps } from 'src/actions/titlebar'; type Props = { redirectPath: ?string, signinType?: ?string, close?: Function, location?: Object, + dispatch: Function, }; -export class Login extends React.Component { +class Login extends React.Component { componentDidMount() { let redirectPath; + const { dispatch } = this.props; + dispatch(setTitlebarProps({ title: 'Login' })); + if (this.props.location) { const searchObj = queryString.parse(this.props.location.search); redirectPath = searchObj.r; @@ -81,4 +87,7 @@ export class Login extends React.Component { } } -export default compose(withRouter)(Login); +export default compose( + withRouter, + connect() +)(Login); diff --git a/src/views/login/style.js b/src/views/login/style.js index 43a376f3f8..2d41fc496b 100644 --- a/src/views/login/style.js +++ b/src/views/login/style.js @@ -1,6 +1,5 @@ // @flow import theme from 'shared/theme'; -// $FlowFixMe import styled from 'styled-components'; import { FlexRow, @@ -9,8 +8,9 @@ import { Shadow, hexa, zIndex, -} from '../../components/globals'; -import { Button } from '../../components/buttons'; +} from 'src/components/globals'; +import { Button } from 'src/components/button'; +import { MEDIA_BREAK } from 'src/components/layout'; export const Title = styled.h1` color: ${theme.text.default}; @@ -195,7 +195,7 @@ export const SigninLink = styled.span` export const FullscreenContent = styled.div` width: 100%; - max-width: 768px; + max-width: ${MEDIA_BREAK}px; display: flex; align-items: center; flex-direction: column; diff --git a/src/views/navbar/components/messagesTab.js b/src/views/navbar/components/messagesTab.js deleted file mode 100644 index 68304cc730..0000000000 --- a/src/views/navbar/components/messagesTab.js +++ /dev/null @@ -1,269 +0,0 @@ -// @flow -import * as React from 'react'; -import { connect } from 'react-redux'; -import compose from 'recompose/compose'; -import Icon from 'src/components/icons'; -import { isDesktopApp } from 'src/helpers/desktop-app-utils'; -import viewNetworkHandler from 'src/components/viewNetworkHandler'; -import { updateNotificationsCount } from 'src/actions/notifications'; -import getUnreadDMQuery from 'shared/graphql/queries/notification/getDirectMessageNotifications'; -import type { GetDirectMessageNotificationsType } from 'shared/graphql/queries/notification/getDirectMessageNotifications'; -import markDirectMessageNotificationsSeenMutation from 'shared/graphql/mutations/notification/markDirectMessageNotificationsSeen'; -import { MessageTab, Label } from '../style'; -import { track, events } from 'src/helpers/analytics'; -import type { Dispatch } from 'redux'; -import type { WebsocketConnectionType } from 'src/reducers/connectionStatus'; -import { useConnectionRestored } from 'src/hooks/useConnectionRestored'; - -type Props = { - active: boolean, - isLoading: boolean, - hasError: boolean, - isRefetching: boolean, - markDirectMessageNotificationsSeen: Function, - data: { - directMessageNotifications: GetDirectMessageNotificationsType, - refetch: Function, - }, - subscribeToDMs: Function, - refetch: Function, - count: number, - dispatch: Dispatch, - networkOnline: boolean, - websocketConnection: WebsocketConnectionType, -}; - -type State = { - subscription: ?Function, -}; - -class MessagesTab extends React.Component { - state = { - subscription: null, - }; - - componentDidMount() { - this.subscribe(); - return this.setCount(this.props); - } - - shouldComponentUpdate(nextProps) { - const curr = this.props; - - if (curr.networkOnline !== nextProps.networkOnline) return true; - if (curr.websocketConnection !== nextProps.websocketConnection) return true; - - // if a refetch completes - if (curr.isRefetching !== nextProps.isRefetching) return true; - - // once the initial query finishes loading - if ( - !curr.data.directMessageNotifications && - nextProps.data.directMessageNotifications - ) - return true; - - // if a subscription updates the number of records returned - if ( - curr.data && - curr.data.directMessageNotifications && - curr.data.directMessageNotifications.edges && - nextProps.data && - nextProps.data.directMessageNotifications && - nextProps.data.directMessageNotifications.edges && - curr.data.directMessageNotifications.edges.length !== - nextProps.data.directMessageNotifications.edges.length - ) - return true; - // if the user clicks on the messages tab - if (curr.active !== nextProps.active) return true; - - // any time the count changes - if (curr.count !== nextProps.count) return true; - - return false; - } - - componentDidUpdate(prev: Props) { - const { data: prevData } = prev; - const curr = this.props; - - const didReconnect = useConnectionRestored({ curr, prev }); - if (didReconnect && curr.data.refetch) { - curr.data.refetch(); - } - - // if the component updates for the first time - if ( - !prevData.directMessageNotifications && - curr.data.directMessageNotifications - ) { - this.subscribe(); - return this.setCount(this.props); - } - - // never update the badge if the user is viewing the messages tab - // set the count to 0 if the tab is active so that if a user loads - // /messages view directly, the badge won't update - - // if the user is viewing /messages, mark any incoming notifications - // as seen, so that when they navigate away the message count won't shoot up - if (this.props.active) { - return this.markAllAsSeen(); - } - - if ( - curr.active && - curr.data.directMessageNotifications && - prevData.directMessageNotifications && - curr.data.directMessageNotifications.edges.length > - prevData.directMessageNotifications.edges.length - ) - return this.markAllAsSeen(); - - // if the component updates with changed or new dm notifications - // if any are unseen, set the counts - if ( - curr.data.directMessageNotifications && - curr.data.directMessageNotifications.edges.length > 0 && - curr.data.directMessageNotifications.edges.some( - n => n && n.node && !n.node.isSeen - ) - ) { - return this.setCount(this.props); - } - } - - componentWillUnmount() { - this.unsubscribe(); - } - - subscribe = () => { - this.setState({ - subscription: this.props.subscribeToDMs(), - }); - }; - - unsubscribe = () => { - const { subscription } = this.state; - if (subscription) { - // This unsubscribes the subscription - subscription(); - } - }; - - convertEdgesToNodes = notifications => { - if ( - !notifications || - !notifications.edges || - notifications.edges.length === 0 - ) - return []; - - return notifications.edges.map(n => n && n.node); - }; - - setCount(props) { - const { - data: { directMessageNotifications }, - } = props; - const { dispatch } = this.props; - const nodes = this.convertEdgesToNodes(directMessageNotifications); - // set to 0 if no notifications exist yet - if (!nodes || nodes.length === 0) { - return dispatch( - updateNotificationsCount('directMessageNotifications', 0) - ); - } - - // bundle dm notifications - const obj = {}; - nodes - .filter(n => n && !n.isSeen) - .map(o => { - if (!o) return null; - if (obj[o.context.id]) return null; - obj[o.context.id] = o; - return null; - }); - - // count of unique notifications determined by the thread id - const count = Object.keys(obj).length; - return dispatch( - updateNotificationsCount('directMessageNotifications', count) - ); - } - - markAllAsSeen = () => { - const { - data: { directMessageNotifications }, - markDirectMessageNotificationsSeen, - refetch, - dispatch, - } = this.props; - - const nodes = this.convertEdgesToNodes(directMessageNotifications); - - // force the count to 0 - dispatch(updateNotificationsCount('directMessageNotifications', 0)); - - // if there are no unread, escape - if (nodes && nodes.length === 0) return; - - // otherwise - return markDirectMessageNotificationsSeen() - .then(() => { - // notifs were marked as seen - // refetch to make sure we're keeping up with the server's state - return refetch(); - }) - .then(() => this.setCount(this.props)) - .catch(err => { - console.error('error marking dm notifications seen', err); - }); - }; - - render() { - const { active, count } = this.props; - - // Keep the dock icon notification count indicator of the desktop app in sync - if (isDesktopApp()) { - window.interop.setBadgeCount(count); - } - - return ( - { - track(events.NAVIGATION_MESSAGES_CLICKED); - this.markAllAsSeen(); - }} - data-cy="navbar-messages" - > - 0 ? 'message-fill' : 'message'} - count={count > 10 ? '10+' : count > 0 ? count.toString() : null} - size={isDesktopApp() ? 28 : 32} - /> - - - ); - } -} - -const map = state => ({ - count: state.notifications.directMessageNotifications, - networkOnline: state.connectionStatus.networkOnline, - websocketConnection: state.connectionStatus.websocketConnection, -}); -export default compose( - // $FlowIssue - connect(map), - getUnreadDMQuery, - markDirectMessageNotificationsSeenMutation, - viewNetworkHandler -)(MessagesTab); diff --git a/src/views/navbar/components/notificationDropdown.js b/src/views/navbar/components/notificationDropdown.js deleted file mode 100644 index 62b165ce65..0000000000 --- a/src/views/navbar/components/notificationDropdown.js +++ /dev/null @@ -1,107 +0,0 @@ -// @flow -import React from 'react'; -// $FlowFixMe -import compose from 'recompose/compose'; -// $FlowFixMe -import { withRouter } from 'react-router'; -// $FlowFixMe -import { Link } from 'react-router-dom'; -import Icon from '../../../components/icons'; -import Dropdown from '../../../components/dropdown'; -import { Loading } from '../../../components/loading'; -import { NullState } from '../../../components/upsell'; -import { TextButton } from '../../../components/buttons'; -import { DropdownHeader, DropdownFooter } from '../style'; -import { NotificationDropdownList } from '../../notifications/components/notificationDropdownList'; - -const NullNotifications = () => ( - -); - -const NotificationContainer = props => { - const { - rawNotifications, - currentUser, - history, - loading, - markSingleNotificationAsSeenInState, - } = props; - - if (rawNotifications && rawNotifications.length > 0) { - return ( - - ); - } - - if (loading) { - return ( -
- -
- ); - } - - return ; -}; - -const NotificationDropdownPure = props => { - const { - rawNotifications, - currentUser, - history, - markAllAsSeen, - count, - markSingleNotificationAsSeenInState, - loading, - } = props; - - return ( - - - - - - 0 ? 'brand.alt' : 'text.alt'} - onClick={markAllAsSeen} - > - Mark all as seen - - - - - - {rawNotifications && rawNotifications.length > 0 && ( - - history.push('/notifications')} - > - View all - - - )} - - ); -}; - -export const NotificationDropdown = compose(withRouter)( - NotificationDropdownPure -); diff --git a/src/views/navbar/components/notificationsTab.js b/src/views/navbar/components/notificationsTab.js deleted file mode 100644 index 3d1beaa9a4..0000000000 --- a/src/views/navbar/components/notificationsTab.js +++ /dev/null @@ -1,465 +0,0 @@ -// @flow -import * as React from 'react'; -import { withApollo } from 'react-apollo'; -import { connect } from 'react-redux'; -import compose from 'recompose/compose'; -import { isDesktopApp } from 'src/helpers/desktop-app-utils'; -import Icon from 'src/components/icons'; -import viewNetworkHandler from 'src/components/viewNetworkHandler'; -import { updateNotificationsCount } from 'src/actions/notifications'; -import { NotificationDropdown } from './notificationDropdown'; -import getNotifications from 'shared/graphql/queries/notification/getNotifications'; -import type { GetNotificationsType } from 'shared/graphql/queries/notification/getNotifications'; -import markNotificationsSeenMutation from 'shared/graphql/mutations/notification/markNotificationsSeen'; -import { markSingleNotificationSeenMutation } from 'shared/graphql/mutations/notification/markSingleNotificationSeen'; -import { Tab, NotificationTab, Label } from '../style'; -import { deduplicateChildren } from 'src/components/infiniteScroll/deduplicateChildren'; -import { track, events } from 'src/helpers/analytics'; -import type { Dispatch } from 'redux'; -import type { WebsocketConnectionType } from 'src/reducers/connectionStatus'; -import { useConnectionRestored } from 'src/hooks/useConnectionRestored'; - -type Props = { - active: boolean, - currentUser: Object, - isLoading: boolean, - hasError: boolean, - isRefetching: boolean, - markAllNotificationsSeen: Function, - activeInboxThread: ?string, - location: Object, - data: { - notifications: GetNotificationsType, - subscribeToNewNotifications: Function, - refetch: Function, - }, - refetch: Function, - client: Function, - dispatch: Dispatch, - count: number, - networkOnline: boolean, - websocketConnection: WebsocketConnectionType, - threadSlider: { - isOpen: boolean, - threadId: ?string, - }, -}; - -type State = { - notifications: ?Array, - subscription: ?Function, - shouldRenderDropdown: boolean, -}; - -class NotificationsTab extends React.Component { - state = { - notifications: null, - subscription: null, - shouldRenderDropdown: false, - }; - - shouldComponentUpdate(nextProps: Props, nextState: State) { - const curr = this.props; - const prevState = this.state; - - if (curr.networkOnline !== nextProps.networkOnline) return true; - if (curr.websocketConnection !== nextProps.websocketConnection) return true; - - const prevLocation = curr.location; - const nextLocation = nextProps.location; - const prevThreadParam = curr.threadSlider.threadId; - const nextThreadParam = nextProps.threadSlider.threadId; - const prevActiveInboxThread = curr.activeInboxThread; - const nextActiveInboxThread = nextProps.activeInboxThread; - const prevParts = prevLocation.pathname.split('/'); - const nextParts = nextLocation.pathname.split('/'); - - // changing slider - if (prevThreadParam !== nextThreadParam) return true; - - // changing inbox thread - if (prevActiveInboxThread !== nextActiveInboxThread) return true; - - // changing thread detail view - if (prevParts[2] !== nextParts[2]) return true; - - // if a refetch completes - if (curr.isRefetching !== nextProps.isRefetching) return true; - - // once the initial query finishes loading - if (!curr.data.notifications && nextProps.data.notifications) return true; - - // after refetch - if (curr.isRefetching !== nextProps.isRefetching) return true; - - // if a subscription updates the number of records returned - if ( - curr.data && - curr.data.notifications && - curr.data.notifications.edges && - nextProps.data.notifications && - nextProps.data.notifications.edges && - curr.data.notifications.edges.length !== - nextProps.data.notifications.edges.length - ) - return true; - - // if the user clicks on the notifications tab - if (curr.active !== nextProps.active) return true; - - // when the notifications get set for the first time - if (!prevState.notifications && nextState.notifications) return true; - - // when hovered - if (!prevState.shouldRenderDropdown && nextState.shouldRenderDropdown) - return true; - - // any time the count changes - if (curr.count !== nextProps.count) return true; - - // any time the count changes - if ( - prevState.notifications && - nextState.notifications && - prevState.notifications.length !== nextState.notifications.length - ) - return true; - - return false; - } - - componentDidUpdate(prev: Props) { - const { - data: prevData, - location: prevLocation, - activeInboxThread: prevActiveInboxThread, - } = prev; - const curr = this.props; - - const didReconnect = useConnectionRestored({ curr, prev }); - if (didReconnect && curr.data.refetch) { - curr.data.refetch(); - } - - const { notifications } = this.state; - - if (!notifications && curr.data.notifications) { - this.subscribe(); - return this.processAndMarkSeenNotifications(); - } - - // never update the badge if the user is viewing the notifications tab - // set the count to 0 if the tab is active so that if a user loads - // /notifications view directly, the badge won't update - if (curr.active) { - return curr.dispatch(updateNotificationsCount('notifications', 0)); - } - - // if the component updates for the first time - if (!prevData.notifications && curr.data.notifications) { - return this.processAndMarkSeenNotifications(); - } - - // if the component updates with changed or new notifications - // if any are unseen, set the counts - if ( - prevData.notifications && - prevData.notifications.edges && - curr.data.notifications && - curr.data.notifications.edges && - curr.data.notifications.edges.length > 0 && - curr.data.notifications.edges.length !== - prevData.notifications.edges.length - ) { - return this.processAndMarkSeenNotifications(); - } - - const prevThreadParam = prev.threadSlider.threadId; - const thisThreadParam = curr.threadSlider.threadId; - const prevParts = prevLocation.pathname.split('/'); - const thisParts = prevLocation.pathname.split('/'); - - // changing slider - if (prevThreadParam !== thisThreadParam) - return this.processAndMarkSeenNotifications(notifications); - - // changing inbox thread - if (prevActiveInboxThread !== curr.activeInboxThread) - return this.processAndMarkSeenNotifications(notifications); - - // changing thread detail view - if (prevParts[2] !== thisParts[2]) - return this.processAndMarkSeenNotifications(); - - // when the component finishes a refetch - if (prev.isRefetching && !curr.isRefetching) { - return this.processAndMarkSeenNotifications(); - } - } - - componentWillUnmount() { - this.unsubscribe(); - } - - subscribe = () => { - this.setState({ - subscription: this.props.data.subscribeToNewNotifications(), - }); - }; - - unsubscribe = () => { - const { subscription } = this.state; - if (subscription) { - // This unsubscribes the subscription - subscription(); - } - }; - - convertEdgesToNodes = notifications => { - if ( - !notifications || - !notifications.edges || - notifications.edges.length === 0 - ) - return []; - - return notifications.edges.map(n => n && n.node); - }; - - markAllAsSeen = () => { - const { markAllNotificationsSeen, count } = this.props; - const { notifications } = this.state; - - // don't perform a mutation is there are no unread notifs - if (count === 0) return; - - const oldNotifications = notifications && notifications.slice(); - - // Optimistically update the seen status of the notifications - const newNotifications = - notifications && - notifications.map(n => Object.assign({}, n, { isSeen: true })); - this.processAndMarkSeenNotifications(newNotifications, false); - // otherwise - return markAllNotificationsSeen().catch(err => { - console.error(err); - // Undo the optimistic update from above - this.processAndMarkSeenNotifications(oldNotifications, false); - }); - }; - - processAndMarkSeenNotifications = (stateNotifications, sync = true) => { - const { - data: { notifications }, - location, - client, - activeInboxThread, - } = this.props; - - // in componentDidUpdate, we can optionally pass in the notifications - // from state. this is useful for when a user is navigating around the site - // and we want to mark notifications as read as they view threads - // if we do not pass in notifications from the state when this method is - // invoked, it is because the incoming props have changed from the server - // i.e. a new notification was received, so we should therefore run - // the rest of this method on the incoming notifications data - const nodes = stateNotifications - ? stateNotifications - : this.convertEdgesToNodes(notifications); - - // if no notifications exist - if (!nodes || nodes.length === 0) return this.setCount(); - - // get distinct notifications by id - const distinct = deduplicateChildren(nodes, 'id'); - - /* - 1. If the user is viewing a thread in a modal, don't display a notification - badge for that thread, and mark any incoming notifications for that - thread as seen - 2. If the user is viewing a ?t= url in the inbox, same logic - 3. If the user is viewing the thread view, same logic - */ - - const filteredByContext = distinct.map(n => { - const contextId = n.context.id; - const threadParam = this.props.threadSlider.threadId; - - // 1 - const isViewingSlider = threadParam === contextId && !n.isSeen; - // 2 - const isViewingInbox = activeInboxThread - ? activeInboxThread === contextId && !n.isSeen - : false; - const parts = location.pathname.split('/'); - const isViewingThread = parts[1] === 'thread'; - // 3 - const isViewingThreadDetail = isViewingThread - ? parts[2] === contextId && !n.isSeen - : false; - - // newly published threads have a context id that is equal to the thread's channel - // we have to use different logic to mark these notifications as seen if a user views - // the thread before clicking the notification - const isNewThreadNotification = n.event === 'THREAD_CREATED'; - const isViewingANewlyPublishedThread = - isNewThreadNotification && - n.entities.some( - e => - e.id === activeInboxThread || - e.id === threadParam || - (isViewingThread && e.id === parts[2]) - ); - - if ( - isViewingSlider || - isViewingInbox || - isViewingThreadDetail || - isViewingANewlyPublishedThread - ) { - // if the user shouldn't see a new notification badge, - // mark it as seen before it ever hits the component - const newNotification = Object.assign({}, n, { - isSeen: true, - }); - - // and then mark it as seen on the server - if (sync) { - client.mutate({ - mutation: markSingleNotificationSeenMutation, - variables: { - id: n.id, - }, - }); - } - - return newNotification; - } else { - return n; - } - }); - - this.setState({ notifications: filteredByContext }); - - return this.setCount(filteredByContext); - }; - - setCount = notifications => { - const curr = this.props; - - if (!notifications || notifications.length === 0) { - return curr.dispatch(updateNotificationsCount('notifications', 0)); - } - - const distinct = deduplicateChildren(notifications, 'id'); - // set to 0 if no notifications exist yet - if (!distinct || distinct.length === 0) { - return curr.dispatch(updateNotificationsCount('notifications', 0)); - } - - // set to 0 if no notifications are unseen - const unseen = distinct.filter(n => !n.isSeen); - if (!unseen || unseen.length === 0) { - return curr.dispatch(updateNotificationsCount('notifications', 0)); - } - - // count of unique unseen notifications - const count = unseen.length; - return curr.dispatch(updateNotificationsCount('notifications', count)); - }; - - // this function gets triggered from downstream child notification components. - // in certain cases, clicking on a notification should mark it as seen - // and update the state in this parent container - // as a result, we pass this function down a few levels of children - markSingleNotificationAsSeenInState = (notificationId: string) => { - const { notifications } = this.state; - if (!notifications) return; - const newNotifications = notifications.map(n => { - if (n.id !== notificationId) return n; - return Object.assign({}, n, { - isSeen: true, - }); - }); - - this.setState({ - notifications: newNotifications, - }); - - return this.setCount(newNotifications); - }; - - setHover = () => { - return this.setState({ - shouldRenderDropdown: true, - }); - }; - - render() { - const { active, currentUser, isLoading, count } = this.props; - const { notifications, shouldRenderDropdown } = this.state; - - // Keep the dock icon notification count indicator of the desktop app in sync - if (isDesktopApp()) { - window.interop.setBadgeCount(count); - } - - return ( - - { - this.markAllAsSeen(); - track(events.NAVIGATION_NOTIFICATIONS_CLICKED); - }} - > - 0 ? 'notification-fill' : 'notification'} - count={count > 10 ? '10+' : count > 0 ? count.toString() : null} - size={isDesktopApp() ? 28 : 32} - /> - - - - {shouldRenderDropdown && ( - - )} - - ); - } -} - -const map = state => ({ - activeInboxThread: state.dashboardFeed.activeThread, - count: state.notifications.notifications, - networkOnline: state.connectionStatus.networkOnline, - websocketConnection: state.connectionStatus.websocketConnection, - threadSlider: state.threadSlider, -}); - -export default compose( - // $FlowIssue - connect(map), - withApollo, - getNotifications, - markNotificationsSeenMutation, - viewNetworkHandler -)(NotificationsTab); diff --git a/src/views/navbar/components/profileDropdown.js b/src/views/navbar/components/profileDropdown.js deleted file mode 100644 index 92249514e6..0000000000 --- a/src/views/navbar/components/profileDropdown.js +++ /dev/null @@ -1,103 +0,0 @@ -// @flow -import theme from 'shared/theme'; -import React from 'react'; -import styled from 'styled-components'; -import { connect } from 'react-redux'; -import { Link } from 'react-router-dom'; -import Dropdown from 'src/components/dropdown'; -import { SERVER_URL } from 'src/api/constants'; -import type { UserInfoType } from 'shared/graphql/fragments/user/userInfo'; -import { isMac } from 'src/helpers/is-os'; -import { isDesktopApp } from 'src/helpers/desktop-app-utils'; - -const UserProfileDropdown = styled(Dropdown)` - width: 200px; -`; - -const UserProfileDropdownList = styled.ul` - list-style-type: none; - margin: 0; - padding: 0; - display: flex; - flex-direction: column; - justify-content: center; - flex: 1; -`; - -const UserProfileDropdownListItem = styled.li` - font-size: 14px; - padding: 16px; - text-align: center; - display: flex; - flex: 1; - font-size: 14px; - font-weight: 600; - color: ${theme.text.alt}; - border-bottom: 2px solid ${theme.bg.border}; - background: ${theme.bg.default}; - justify-content: center; - - &:hover { - cursor: pointer; - color: ${theme.text.default}; - background: ${theme.bg.wash}; - } -`; - -type ProfileProps = { - user: UserInfoType, - dispatch: Function, -}; - -type State = { - didMount: boolean, -}; - -class ProfileDropdown extends React.Component { - state = { didMount: false }; - - componentDidMount() { - return this.setState({ didMount: true }); - } - - render() { - const { props } = this; - const { didMount } = this.state; - return ( - - - {props.user.username && ( - - - My Settings - - - )} - - {didMount && isMac() && !isDesktopApp() && ( - - - Desktop App - - - )} - - - - About Spectrum - - - - Support - - - - Log Out - - - - ); - } -} - -export default connect()(ProfileDropdown); diff --git a/src/views/navbar/index.js b/src/views/navbar/index.js deleted file mode 100644 index 228110fc58..0000000000 --- a/src/views/navbar/index.js +++ /dev/null @@ -1,347 +0,0 @@ -// @flow -import React from 'react'; -import { connect } from 'react-redux'; -import compose from 'recompose/compose'; -import queryString from 'query-string'; -import Icon from 'src/components/icons'; -import ProfileDropdown from './components/profileDropdown'; -import MessagesTab from './components/messagesTab'; -import NotificationsTab from './components/notificationsTab'; -import Head from 'src/components/head'; -import { withCurrentUser } from 'src/components/withCurrentUser'; -import type { GetUserType } from 'shared/graphql/queries/user/getUser'; -import { truncateNumber } from 'src/helpers/utils'; -import { - Nav, - Logo, - HomeTab, - ExploreTab, - ProfileDrop, - ProfileTab, - SupportTab, - Tab, - Label, - Navatar, - SkipLink, - SigninLink, - Reputation, -} from './style'; -import { track, events } from 'src/helpers/analytics'; -import { isViewingMarketingPage } from 'src/helpers/is-viewing-marketing-page'; -import { isDesktopApp } from 'src/helpers/desktop-app-utils'; -import type { WebsocketConnectionType } from 'src/reducers/connectionStatus'; - -type Props = { - isLoading: boolean, - hasError: boolean, - location: Object, - history: Object, - match: Object, - notificationCounts: { - notifications: number, - directMessageNotifications: number, - }, - currentUser?: GetUserType, - isLoadingCurrentUser: boolean, - activeInboxThread: ?string, - networkOnline: boolean, - websocketConnection: WebsocketConnectionType, -}; - -type State = { - isSkipLinkFocused: boolean, -}; - -class Navbar extends React.Component { - state = { - isSkipLinkFocused: false, - }; - - shouldComponentUpdate(nextProps: Props, nextState: State) { - const curr = this.props; - const isMobile = window && window.innerWidth <= 768; - - if (curr.networkOnline !== nextProps.networkOnline) return true; - if (curr.websocketConnection !== nextProps.websocketConnection) return true; - - // If the update was caused by the focus on the skip link - if (nextState.isSkipLinkFocused !== this.state.isSkipLinkFocused) - return true; - - // if route changes - if (curr.location.pathname !== nextProps.location.pathname) return true; - - // if route query params change we need to re-render on mobile - if (isMobile && curr.location.search !== nextProps.location.search) - return true; - - // Had no user, now have user or user changed - if (nextProps.currentUser !== curr.currentUser) return true; - if (nextProps.isLoadingCurrentUser !== curr.isLoadingCurrentUser) - return true; - - const newDMNotifications = - curr.notificationCounts.directMessageNotifications !== - nextProps.notificationCounts.directMessageNotifications; - const newNotifications = - curr.notificationCounts.notifications !== - nextProps.notificationCounts.notifications; - if (newDMNotifications || newNotifications) return true; - - if (isMobile) { - // if the user is mobile and is viewing a thread or DM thread, re-render - // the navbar when they exit the thread - const { thread: thisThreadParam } = queryString.parse( - curr.history.location.search - ); - const { thread: nextThreadParam } = queryString.parse( - nextProps.history.location.search - ); - if (thisThreadParam !== nextThreadParam) return true; - } - - return false; - } - - handleSkipLinkFocus = () => this.setState({ isSkipLinkFocused: true }); - handleSkipLinkBlur = () => this.setState({ isSkipLinkFocused: false }); - - getTabProps(isActive: boolean) { - return { - 'data-active': isActive, - 'aria-current': isActive ? 'page' : undefined, - }; - } - - trackNavigationClick = (route: string) => { - switch (route) { - case 'logo': - return track(events.NAVIGATION_LOGO_CLICKED); - case 'home': - return track(events.NAVIGATION_HOME_CLICKED); - case 'explore': - return track(events.NAVIGATION_EXPLORE_CLICKED); - case 'profile': - return track(events.NAVIGATION_USER_PROFILE_CLICKED); - default: - return null; - } - }; - - render() { - const { - history, - match, - currentUser, - isLoadingCurrentUser, - notificationCounts, - } = this.props; - - if (isViewingMarketingPage(history, currentUser)) { - return null; - } - - // if the user is mobile and is viewing a thread or DM thread, don't - // render a navbar - it will be replaced with a chat input - const { thread: threadParam } = queryString.parse(history.location.search); - const parts = history.location.pathname.split('/'); - const isViewingThread = parts[1] === 'thread'; - const isViewingDm = - parts[1] === 'messages' && parts[2] && parts[2] !== 'new'; - const isComposingDm = history.location.pathname === '/messages/new'; - const isComposingThread = history.location.pathname === '/new/thread'; - const isViewingThreadSlider = threadParam !== undefined; - const hideNavOnMobile = - isViewingThreadSlider || - isComposingDm || - isViewingThread || - isViewingDm || - isComposingThread; - - if (currentUser) { - return ( - - ); - } - if (!currentUser && !isLoadingCurrentUser) { - return ( - - ); - } - - return null; - } -} - -const map = state => ({ - notificationCounts: state.notifications, - networkOnline: state.connectionStatus.networkOnline, - websocketConnection: state.connectionStatus.websocketConnection, -}); -export default compose( - // $FlowIssue - connect(map), - withCurrentUser -)(Navbar); diff --git a/src/views/navbar/style.js b/src/views/navbar/style.js deleted file mode 100644 index eed94b77a8..0000000000 --- a/src/views/navbar/style.js +++ /dev/null @@ -1,438 +0,0 @@ -// @flow -import theme from 'shared/theme'; -import styled, { css } from 'styled-components'; -import { Link } from 'react-router-dom'; -import { Transition, FlexRow, hexa, zIndex } from 'src/components/globals'; -import { UserAvatar } from 'src/components/avatar'; -import { isDesktopApp } from 'src/helpers/desktop-app-utils'; - -export const Nav = styled.nav` - display: grid; - grid-template-columns: repeat(4, auto) 1fr repeat(2, auto); - grid-template-rows: 1fr; - grid-template-areas: 'logo home messages explore . notifications profile'; - align-items: stretch; - width: 100%; - flex: 0 0 48px; - padding: 0 16px; - line-height: 1; - box-shadow: 0 4px 8px ${({ theme }) => hexa(theme.bg.reverse, 0.15)}; - z-index: ${zIndex.navBar}; - ${isDesktopApp() && - css` - -webkit-app-region: drag; - user-select: none; - `} - background: ${({ theme }) => - process.env.NODE_ENV === 'production' ? theme.bg.reverse : theme.warn.alt}; - - @media (max-width: 768px) { - padding: 0; - order: 3; - position: relative; - box-shadow: 0 -4px 8px ${({ theme }) => hexa(theme.bg.reverse, 0.15)}; - grid-template-columns: repeat(5, 20%); - grid-template-areas: 'home messages explore notifications profile'; - } - - .hideOnMobile { - @media (max-width: 768px) { - display: none; - } - } - - .hideOnDesktop { - @media (min-width: 769px) { - display: none; - } - } - - ${props => - props.loggedOut && - css` - grid-template-columns: repeat(3, auto) 1fr auto; - grid-template-areas: 'logo explore support . signin'; - - @media (max-width: 768px) { - grid-template-columns: repeat(3, 1fr); - grid-template-areas: 'home explore support'; - } - `} ${props => - props.hideOnMobile && - css` - @media (max-width: 768px) { - display: none; - } - `}; -`; - -export const Label = styled.span` - font-size: 14px; - font-weight: ${isDesktopApp() ? '500' : '700'}; - margin-left: 12px; - - ${props => - props.hideOnDesktop && - css` - display: none; - `} @media (max-width: 768px) { - font-size: 10px; - font-weight: 700; - margin: 0; - display: inline-block; - } - - @media (max-width: 360px) { - display: none; - } -`; - -export const Tab = styled(Link)` - display: grid; - grid-template-columns: 'auto auto'; - grid-template-rows: 'auto'; - grid-template-areas: 'icon label'; - align-items: center; - justify-items: center; - padding: ${isDesktopApp() ? '0 12px' : '0 16px'}; - color: ${({ theme }) => - process.env.NODE_ENV === 'production' - ? theme.text.placeholder - : theme.warn.border}; - transition: ${Transition.hover.off}; - - > div { - grid-area: icon; - } - - > ${Label} { - grid-area: label; - } - - @media (min-width: 768px) { - &[data-active~='true'] { - box-shadow: inset 0 ${isDesktopApp() ? '-2px' : '-4px'} 0 - ${theme.text.reverse}; - color: ${theme.text.reverse}; - transition: ${Transition.hover.on}; - - &:hover, - &:focus { - box-shadow: inset 0 ${isDesktopApp() ? '-2px' : '-4px'} 0 - ${theme.text.reverse}; - transition: ${Transition.hover.on}; - } - } - - &:hover, - &:focus { - box-shadow: inset 0 ${isDesktopApp() ? '-2px' : '-4px'} 0 - ${({ theme }) => - process.env.NODE_ENV === 'production' - ? theme.text.placeholder - : theme.warn.border}; - color: ${theme.text.reverse}; - transition: ${Transition.hover.on}; - } - } - - @media (max-width: 768px) { - color: ${props => - process.env.NODE_ENV === 'production' - ? props.theme.text.placeholder - : props.theme.warn.border}; - padding: 0; - grid-template-columns: 'auto'; - grid-template-rows: 'auto auto'; - grid-template-areas: 'icon' 'label'; - align-content: center; - - &[data-active~='true'] { - color: ${theme.text.reverse}; - transition: ${Transition.hover.on}; - } - } -`; - -export const DropTab = styled(FlexRow)` - align-items: stretch; - align-self: stretch; - position: relative; - flex: auto; - flex: 0 1; - - &:hover { - /* - this padding left makes it so that there is a left zone on the - icon that the user can mouseover without un-hovering the dropdown - */ - ${props => - props.padOnHover && - css` - @media (min-width: 768px) { - color: ${theme.text.reverse}; - padding-left: 120px; - } - `}; - } - - @media (max-width: 768px) { - flex: auto; - justify-content: center; - ${props => - props.hideOnMobile && - css` - display: none; - `}; - } - - .dropdown { - display: none; - pointer-events: none; - position: absolute; - top: 100%; - right: 0; - padding: 8px; - - @media (max-width: 768px) { - display: none; - } - } - - &:hover .dropdown, - .dropdown:hover { - display: flex; - pointer-events: auto; - transition: ${Transition.hover.on}; - - @media (max-width: 768px) { - display: none; - } - } -`; - -export const Reputation = styled.div` - display: flex; - grid-area: icon; - align-items: center; - padding-right: 16px; - font-size: 14px; - font-weight: 700; -`; - -export const Logo = styled(Tab)` - grid-area: logo; - padding: ${isDesktopApp() ? '0 32px 0 4px' : '0 24px 0 4px'}; - color: ${theme.text.reverse}; - opacity: 1; - - ${isDesktopApp() && - css` - visibility: hidden; - `} &:hover { - box-shadow: none; - } - - @media (max-width: 768px) { - display: none; - } - - ${props => - props.ishidden && - css` - display: none; - `}; -`; - -export const HomeTab = styled(Tab)` - grid-area: home; - ${isDesktopApp() && - css` - -webkit-app-region: no-drag; - `}; -`; - -export const MessageTab = styled(Tab)` - grid-area: messages; - ${isDesktopApp() && - css` - -webkit-app-region: no-drag; - `}; -`; - -export const ExploreTab = styled(Tab)` - grid-area: explore; - - ${isDesktopApp() && - css` - -webkit-app-region: no-drag; - `}; - - ${props => - props.loggedout && - css` - grid-area: explore; - `} ${Label} { - @media (max-width: 768px) { - display: flex; - } - - @media (max-width: 360px) { - display: none; - } - } -`; - -export const SupportTab = styled(Tab)` - grid-area: support; -`; - -export const NotificationTab = styled(DropTab)` - grid-area: notifications; - - ${isDesktopApp() && - css` - -webkit-app-region: no-drag; - `}; - - > a { - &:hover { - box-shadow: none; - transition: none; - } - } -`; - -export const ProfileDrop = styled(DropTab)` - grid-area: profile; - - ${isDesktopApp() && - css` - -webkit-app-region: no-drag; - `}; - - > a { - &:hover { - box-shadow: none; - transition: none; - } - } - - @media (max-width: 768px) { - display: none; - } -`; - -export const ProfileTab = styled(Tab)` - grid-area: profile; -`; - -export const Navatar = styled(UserAvatar)` - margin-top: 0; - border-radius: 100%; - box-shadow: 0 0 0 2px ${theme.bg.default}; -`; - -export const LoggedOutSection = styled(FlexRow)` - display: flex; - flex: auto; - justify-content: flex-end; - - @media (max-width: 768px) { - flex: auto; - justify-content: center; - display: flex; - } -`; - -export const SigninLink = styled(Link)` - grid-area: signin; - font-weight: 700; - font-size: 14px; - color: ${({ theme }) => - process.env.NODE_ENV === 'production' - ? theme.text.placeholder - : theme.warn.border}; - align-self: center; - padding: 10px; - - @media (max-width: 768px) { - display: none; - } -`; - -export const DropdownHeader = styled(FlexRow)` - border-bottom: 2px solid ${theme.bg.wash}; - flex: 0 0 auto; - align-self: stretch; - justify-content: space-between; - align-items: center; - padding: 8px 16px; - font-weight: 500; - font-size: 14px; - color: ${theme.text.alt}; - - a { - display: flex; - align-items: center; - - &:hover { - color: ${theme.brand.alt}; - } - } -`; - -export const DropdownFooter = styled(FlexRow)` - border-top: 2px solid ${theme.bg.wash}; - flex: 0 0 32px; - align-self: stretch; - justify-content: center; - align-items: center; - padding: 8px; - - button { - display: flex; - flex: 1; - - &:first-of-type:not(:last-of-type) { - margin-right: 8px; - } - - &:hover { - color: ${theme.brand.alt}; - background: ${theme.bg.wash}; - } - } -`; - -export const Notification = styled.div` - color: ${theme.text.default}; - padding: 8px; - border-bottom: 1px solid ${theme.bg.border}; - overflow-x: hidden; -`; - -export const MarkAllSeen = styled.span` - color: ${props => - props.isActive ? props.theme.brand.alt : props.theme.text.alt}; - cursor: pointer; - - &:hover { - color: ${props => - props.isActive ? props.theme.brand.default : props.theme.text.alt}; - } -`; - -// We make it a real link element because anchor links don’t work properly with React Router. -// Ref: https://github.com/ReactTraining/react-router/issues/394. -export const SkipLink = Tab.withComponent('a').extend` - grid-area: logo; - overflow: hidden; - height: 1px; - width: 1px; - - &:focus { - height: auto; - width: auto; - } -`; diff --git a/src/views/navigation/accessibility.js b/src/views/navigation/accessibility.js new file mode 100644 index 0000000000..1db8ff4bdf --- /dev/null +++ b/src/views/navigation/accessibility.js @@ -0,0 +1,12 @@ +// @flow +import React from 'react'; +import { SkipLink } from './style'; + +export const Skip = () => Skip to content; + +export const getAccessibilityActiveState = (active: boolean) => { + return { + 'data-active': active, + 'aria-current': active ? 'page' : undefined, + }; +}; diff --git a/src/views/navigation/communityList.js b/src/views/navigation/communityList.js new file mode 100644 index 0000000000..70c17e46b8 --- /dev/null +++ b/src/views/navigation/communityList.js @@ -0,0 +1,181 @@ +// @flow +import React, { useEffect } from 'react'; +import compose from 'recompose/compose'; +import { Route, type History } from 'react-router-dom'; +import Tooltip from 'src/components/tooltip'; +import viewNetworkHandler from 'src/components/viewNetworkHandler'; +import { ErrorBoundary } from 'src/components/error'; +import { + getCurrentUserCommunityConnection, + type GetUserCommunityConnectionType, +} from 'shared/graphql/queries/user/getUserCommunityConnection'; +import { isDesktopApp } from 'src/helpers/desktop-app-utils'; +import { getAccessibilityActiveState } from './accessibility'; +import { + AvatarGrid, + AvatarLink, + Avatar, + Shortcut, + Label, + BlackDot, +} from './style'; +import usePrevious from 'src/hooks/usePrevious'; + +type Props = { + data: { + user: GetUserCommunityConnectionType, + }, + history: History, + sidenavIsOpen: boolean, + setNavigationIsOpen: Function, + subscribeToUpdatedCommunities: Function, +}; + +const CommunityListItem = props => { + const { isActive, community, sidenavIsOpen, index, onClick } = props; + // NOTE(@mxstbr): This is a really hacky way to ensure the "new activity" dot + // does not show up when switching from a community that new activity was in + // while you were looking at it. + const previousActive = usePrevious(usePrevious(usePrevious(isActive))); + + const appControlSymbol = '⌘'; + + return ( + + + + + {isActive === false && + previousActive === false && + community.lastActive && + community.communityPermissions.lastSeen && + new Date(community.lastActive) > + new Date(community.communityPermissions.lastSeen) && } + + + + {index < 9 && isDesktopApp() && ( + + {appControlSymbol} + {index + 1} + + )} + + + + ); +}; + +const CommunityList = (props: Props) => { + const { data, history, sidenavIsOpen, setNavigationIsOpen } = props; + const { user } = data; + + if (!user) return null; + + const { communityConnection } = user; + const { edges } = communityConnection; + const communities = edges.map(edge => edge && edge.node); + + const sorted = communities.slice().sort((a, b) => { + const bc = parseInt(b.communityPermissions.reputation, 10); + const ac = parseInt(a.communityPermissions.reputation, 10); + + // sort same-reputation communities alphabetically + if (ac === bc) { + return a.name.toUpperCase() <= b.name.toUpperCase() ? -1 : 1; + } + + // otherwise sort by reputation + return bc <= ac ? -1 : 1; + }); + + useEffect(() => { + const handleCommunitySwitch = e => { + const ONE = 49; + const TWO = 50; + const THREE = 51; + const FOUR = 52; + const FIVE = 53; + const SIX = 54; + const SEVEN = 55; + const EIGHT = 56; + const NINE = 57; + + const possibleKeys = [ + ONE, + TWO, + THREE, + FOUR, + FIVE, + SIX, + SEVEN, + EIGHT, + NINE, + ]; + + const appControlKey = e.metaKey; + + if (appControlKey) { + const index = possibleKeys.indexOf(e.keyCode); + if (index >= 0) { + const community = sorted[index]; + if (!community) return; + setNavigationIsOpen(false); + return history.push( + `/${community.slug}?tab=${ + community.watercoolerId ? 'chat' : 'posts' + }` + ); + } + } + }; + + props.subscribeToUpdatedCommunities(); + + isDesktopApp() && + window.addEventListener('keydown', handleCommunitySwitch, false); + return () => + window.removeEventListener('keydown', handleCommunitySwitch, false); + }, []); + + return sorted.map((community, index) => { + if (!community) return null; + + const { communityPermissions } = community; + const { isMember, isBlocked } = communityPermissions; + if (!isMember || isBlocked) return null; + + return ( + + + {({ match }) => { + const isActive = + match && + match.params && + match.params.communitySlug === community.slug; + + return ( + setNavigationIsOpen(false)} + /> + ); + }} + + + ); + }); +}; +export default compose( + getCurrentUserCommunityConnection, + viewNetworkHandler +)(CommunityList); diff --git a/src/views/navigation/directMessagesTab.js b/src/views/navigation/directMessagesTab.js new file mode 100644 index 0000000000..96d3e5a7d6 --- /dev/null +++ b/src/views/navigation/directMessagesTab.js @@ -0,0 +1,129 @@ +// @flow +import * as React from 'react'; +import { connect } from 'react-redux'; +import compose from 'recompose/compose'; +import { Route, withRouter } from 'react-router-dom'; +import Icon from 'src/components/icon'; +import Tooltip from 'src/components/tooltip'; +import { isDesktopApp } from 'src/helpers/desktop-app-utils'; +import viewNetworkHandler from 'src/components/viewNetworkHandler'; +import { updateNotificationsCount } from 'src/actions/notifications'; +import getUnreadDMQuery, { + type GetDirectMessageNotificationsType, +} from 'shared/graphql/queries/notification/getDirectMessageNotifications'; +import markDirectMessageNotificationsSeenMutation from 'shared/graphql/mutations/notification/markDirectMessageNotificationsSeen'; +import { getAccessibilityActiveState } from './accessibility'; +import { NavigationContext } from 'src/routes'; +import { addToastWithTimeout } from 'src/actions/toasts'; +import { withCurrentUser } from 'src/components/withCurrentUser'; +import formatNotification from 'shared/notification-to-text'; +import { AvatarGrid, AvatarLink, Label, IconWrapper, RedDot } from './style'; + +type Props = { + count: number, + data: { + directMessageNotifications?: GetDirectMessageNotificationsType, + }, + isActive: boolean, + subscribeToDMs: Function, + markDirectMessageNotificationsSeen: Function, + dispatch: Function, + refetch: Function, + currentUser?: Object, +}; + +const DirectMessagesTab = (props: Props) => { + const { count, data, isActive, currentUser } = props; + + // $FlowIssue + React.useEffect(() => { + const unsubscribe = props.subscribeToDMs(notification => { + const { title, body } = formatNotification( + notification, + currentUser && currentUser.id + ); + // TODO @mxstbr - make this clickable and not show up if the user + // is currently viewing the DM (modal or view) + // props.dispatch(addToastWithTimeout('notification', title)); + }); + return unsubscribe; + }, []); + + const unseenCount = data.directMessageNotifications + ? data.directMessageNotifications.edges + .filter(Boolean) + .reduce((count, { node }) => (node.isSeen ? count : count + 1), 0) + : 0; + // $FlowIssue Mark all as seen when the tab becomes active + React.useEffect(() => { + props.dispatch( + updateNotificationsCount( + 'directMessageNotifications', + isActive ? 0 : unseenCount + ) + ); + if (isActive) + props.markDirectMessageNotificationsSeen().then(() => props.refetch()); + }, [ + data.directMessageNotifications && + data.directMessageNotifications.edges.length, + unseenCount, + isActive, + ]); + + // Keep the dock icon notification count indicator of the desktop app in sync + if (isDesktopApp()) { + window.interop.setBadgeCount(unseenCount); + } + + return ( + + {({ setNavigationIsOpen }) => ( + + {({ match }) => ( + + + setNavigationIsOpen(false)} + {...getAccessibilityActiveState( + match && match.url.includes('/messages') + )} + > + + + {count > 0 && ( + + )} + + + + + + + )} + + )} + + ); +}; + +const map = state => ({ + count: state.notifications.directMessageNotifications, + networkOnline: state.connectionStatus.networkOnline, + websocketConnection: state.connectionStatus.websocketConnection, +}); + +export default compose( + // $FlowIssue + connect(map), + getUnreadDMQuery, + markDirectMessageNotificationsSeenMutation, + viewNetworkHandler, + withRouter, + withCurrentUser +)(DirectMessagesTab); diff --git a/src/views/navigation/globalComposerTab.js b/src/views/navigation/globalComposerTab.js new file mode 100644 index 0000000000..0e1706de78 --- /dev/null +++ b/src/views/navigation/globalComposerTab.js @@ -0,0 +1,32 @@ +// @flow +import React from 'react'; +import Icon from 'src/components/icon'; +import Tooltip from 'src/components/tooltip'; +import { NavigationContext } from 'src/routes'; +import { AvatarGrid, AvatarLink, Label, IconWrapper } from './style'; + +const GlobalComposerTab = () => { + return ( + + {({ setNavigationIsOpen }) => ( + + + setNavigationIsOpen(false)} + > + + + + + + + + + )} + + ); +}; + +export default GlobalComposerTab; diff --git a/src/views/navigation/index.js b/src/views/navigation/index.js new file mode 100644 index 0000000000..67f8a15d32 --- /dev/null +++ b/src/views/navigation/index.js @@ -0,0 +1,321 @@ +// @flow +import React from 'react'; +import compose from 'recompose/compose'; +import { withRouter, Route, type History } from 'react-router-dom'; +import Tooltip from 'src/components/tooltip'; +import { UserAvatar } from 'src/components/avatar'; +import { isViewingMarketingPage } from 'src/helpers/is-viewing-marketing-page'; +import { withCurrentUser } from 'src/components/withCurrentUser'; +import { + Overlay, + NavigationWrapper, + NavigationGrid, + AvatarGrid, + AvatarLink, + Label, + IconWrapper, + Divider, +} from './style'; +import Icon from 'src/components/icon'; +import NavHead from './navHead'; +import DirectMessagesTab from './directMessagesTab'; +import NotificationsTab from './notificationsTab'; +import GlobalComposerTab from './globalComposerTab'; +import { Skip, getAccessibilityActiveState } from './accessibility'; +import CommunityList from './communityList'; +import { NavigationContext } from 'src/routes'; + +type Props = { + history: History, + currentUser?: Object, + isLoadingCurrentUser: boolean, +}; + +const Navigation = (props: Props) => { + const { currentUser, history, isLoadingCurrentUser } = props; + const isMarketingPage = isViewingMarketingPage(history, currentUser); + if (isMarketingPage) return null; + if (!isLoadingCurrentUser && !currentUser) { + return ( + + {({ navigationIsOpen, setNavigationIsOpen }) => ( + + setNavigationIsOpen(false)} + /> + + + + {({ match }) => ( + + + setNavigationIsOpen(false)} + {...getAccessibilityActiveState(!!match)} + > + + + + + + + + + )} + + + + {({ match }) => ( + + + setNavigationIsOpen(false)} + {...getAccessibilityActiveState(!!match)} + > + + + + + + + + + )} + + + + {({ match }) => ( + + + setNavigationIsOpen(false)} + {...getAccessibilityActiveState(!!match)} + > + + + + + + + + + )} + + + + {({ match }) => ( + + + setNavigationIsOpen(false)} + {...getAccessibilityActiveState(!!match)} + > + + + + + + + + + )} + + + + {({ match }) => ( + + + setNavigationIsOpen(false)} + {...getAccessibilityActiveState(!!match)} + > + + + + + + + + + )} + + + + + + {({ match }) => ( + + + setNavigationIsOpen(false)} + {...getAccessibilityActiveState(!!match)} + > + + + + + + + + + )} + + + + )} + + ); + } + + if (currentUser) { + return ( + + {({ navigationIsOpen, setNavigationIsOpen }) => ( + + + + + setNavigationIsOpen(false)} + /> + + + + + {({ match }) => } + + + {({ match }) => } + + + + {({ match }) => ( + + + setNavigationIsOpen(false)} + {...getAccessibilityActiveState( + match && match.url === '/explore' && match.isExact + )} + > + + + + + + + + + )} + + + + {({ match }) => ( + + + setNavigationIsOpen(false)} + {...getAccessibilityActiveState( + history.location.pathname === + `/users/${currentUser.username}` + )} + > + + + + + + )} + + + + + + + {currentUser && ( + + + + {({ match }) => ( + + + + + + + + + + + + )} + + + )} + + + )} + + ); + } + + return ; +}; + +export default compose( + withCurrentUser, + withRouter +)(Navigation); diff --git a/src/views/navigation/navHead.js b/src/views/navigation/navHead.js new file mode 100644 index 0000000000..cf0728aa84 --- /dev/null +++ b/src/views/navigation/navHead.js @@ -0,0 +1,44 @@ +// @flow +import React from 'react'; +import compose from 'recompose/compose'; +import { connect } from 'react-redux'; +import Head from 'src/components/head'; + +type Props = { + notificationCounts: { + notifications: number, + directMessageNotifications: number, + }, +}; + +const NavHead = (props: Props) => { + const { notificationCounts } = props; + const { directMessageNotifications, notifications } = notificationCounts; + const showBadgedFavicon = directMessageNotifications > 0 || notifications > 0; + + return ( + + {showBadgedFavicon ? ( + + ) : ( + + )} + + ); +}; + +const map = (state): * => ({ + notificationCounts: state.notifications, +}); + +export default compose(connect(map))(NavHead); diff --git a/src/views/navigation/notificationsTab.js b/src/views/navigation/notificationsTab.js new file mode 100644 index 0000000000..04e26fe55b --- /dev/null +++ b/src/views/navigation/notificationsTab.js @@ -0,0 +1,143 @@ +// @flow +import * as React from 'react'; +import { withApollo } from 'react-apollo'; +import { withRouter } from 'react-router-dom'; +import { connect } from 'react-redux'; +import Tooltip from 'src/components/tooltip'; +import compose from 'recompose/compose'; +import { isDesktopApp } from 'src/helpers/desktop-app-utils'; +import Icon from 'src/components/icon'; +import viewNetworkHandler from 'src/components/viewNetworkHandler'; +import { + updateNotificationsCount, + setNotifications, +} from 'src/actions/notifications'; +import getNotifications, { + type GetNotificationsType, +} from 'shared/graphql/queries/notification/getNotifications'; +import markSingleNotificationSeenMutation from 'shared/graphql/mutations/notification/markSingleNotificationSeen'; +import { getAccessibilityActiveState } from './accessibility'; +import { NavigationContext } from 'src/routes'; +import formatNotification from 'shared/notification-to-text'; +import { withCurrentUser } from 'src/components/withCurrentUser'; +import { AvatarGrid, AvatarLink, Label, IconWrapper, RedDot } from './style'; + +type Props = { + isActive: boolean, + count: number, + location: Object, + data: { + notifications?: GetNotificationsType, + subscribeToNewNotifications: Function, + }, + markSingleNotificationSeen: Function, + dispatch: Function, + match: Object, + currentUser?: Object, +}; + +const NotificationsTab = (props: Props) => { + const { count, data, isActive, match, currentUser } = props; + + // $FlowIssue Subscribe on mount + React.useEffect(() => { + const unsubscribe = data.subscribeToNewNotifications(notification => { + const { title } = formatNotification( + notification, + currentUser && currentUser.id + ); + // TODO @mxstbr - make this clickable and not show up if the user + // is currently viewing the thing the notification is about - mainly + // thread view and new user joins on community + // props.dispatch(addToastWithTimeout('notification', title)); + }); + return unsubscribe; + }, []); + + const unseenCount = + data.notifications && + data.notifications.edges + .filter(Boolean) + .reduce((count, { node }) => (node.isSeen ? count : count + 1), 0); + + React.useEffect(() => { + let currentViewNotifications = []; + let unseenNotifications = []; + + if (data.notifications) { + data.notifications.edges + .filter(Boolean) + .forEach(({ node: notification }) => { + if (notification.isSeen) return; + if (props.location.pathname.indexOf(notification.context.id) > -1) { + currentViewNotifications.push(notification); + return; + } + unseenNotifications.push(notification); + }); + } + + if (currentViewNotifications.length > 0) { + // Mark notifications for current view as seen + currentViewNotifications.forEach(notification => + props.markSingleNotificationSeen(notification.id) + ); + } + const count = unseenNotifications.length; + props.dispatch(setNotifications(unseenNotifications)); + props.dispatch(updateNotificationsCount('notifications', count)); + }, [ + isActive, + data.notifications && data.notifications.edges.length, + unseenCount, + ]); + + // Keep the dock icon notification count indicator of the desktop app in sync + if (isDesktopApp()) { + window.interop.setBadgeCount(count); + } + + return ( + + {({ setNavigationIsOpen }) => ( + + + setNavigationIsOpen(false)} + {...getAccessibilityActiveState( + match && match.url.includes('/notifications') + )} + > + + + {count > 0 && } + + + + + + + )} + + ); +}; + +const map = state => ({ + count: state.notifications.notifications, + networkOnline: state.connectionStatus.networkOnline, + websocketConnection: state.connectionStatus.websocketConnection, + threadSlider: state.threadSlider, +}); + +export default compose( + // $FlowIssue + connect(map), + withApollo, + getNotifications, + markSingleNotificationSeenMutation, + viewNetworkHandler, + withRouter, + withCurrentUser +)(NotificationsTab); diff --git a/src/views/navigation/style.js b/src/views/navigation/style.js new file mode 100644 index 0000000000..c4336b0a28 --- /dev/null +++ b/src/views/navigation/style.js @@ -0,0 +1,255 @@ +// @flow +import styled, { css } from 'styled-components'; +import { Link } from 'react-router-dom'; +import theme from 'shared/theme'; +import { hexa, Truncate } from 'src/components/globals'; +import { MEDIA_BREAK, NAVBAR_WIDTH } from 'src/components/layout'; +import { isDesktopApp } from 'src/helpers/desktop-app-utils'; + +export const Overlay = styled.div` + position: fixed; + display: ${props => (props.isOpen ? 'block' : 'none')}; + width: 100%; + height: 100%; + top: 0; + left: 0; + right: 0; + bottom: 0; + z-index: 9998 /* on top of titlebar */; + background: rgba(0, 0, 0, 0.4); +`; + +export const RedDot = styled.span` + width: 16px; + height: 16px; + border-radius: 8px; + border: 3px solid ${theme.bg.default}; + position: absolute; + background: ${theme.warn.alt}; + top: 0; + right: 0px; +`; + +export const BlackDot = styled.span` + width: 16px; + height: 16px; + border-radius: 8px; + border: 3px solid ${theme.bg.default}; + position: absolute; + background: ${theme.text.placeholder}; + top: 2px; + right: 10px; + + @media (max-width: ${MEDIA_BREAK}px) { + background: ${theme.warn.alt}; + left: 40px; + top: 0px; + } +`; + +export const NavigationWrapper = styled.div` + grid-area: navigation; + position: sticky; + top: 0; + width: ${NAVBAR_WIDTH}px; + height: 100vh; + overflow: hidden; + overflow-y: auto; + + ${isDesktopApp() && + css` + -webkit-app-region: drag; + user-select: none; + `} + + + @media (max-width: ${MEDIA_BREAK}px) { + display: ${props => (props.isOpen ? 'block' : 'none')}; + position: fixed; + width: 100%; + height: 100vh; + z-index: 9997; + box-shadow: 2px 0 8px rgba(0, 0, 0, 0.16); + } +`; + +export const NavigationGrid = styled.div` + display: grid; + grid-template-columns: minmax(0, 1fr); + align-content: start; + grid-template-rows: auto; + height: 100%; + background: ${theme.bg.default}; + border-right: 1px solid ${theme.bg.border}; + position: fixed; + top: 0; + width: 100%; + max-width: ${NAVBAR_WIDTH}px; + overflow: hidden; + overflow-y: auto; + padding: 12px 0 16px; + + ${isDesktopApp() && + css` + padding-top: 40px; + `} + + &::-webkit-scrollbar { + width: 0px; + height: 0px; + background: transparent; /* make scrollbar transparent */ + } + + @media (max-width: ${MEDIA_BREAK}px) { + position: fixed; + top: 0; + z-index: 9999 /* on top of overlay and titlebar */; + width: 100%; + max-width: 256px; + grid-gap: 0px; + padding: 12px 0; + + ${isDesktopApp() && + css` + padding-top: 40px; + `} + } +`; + +export const AvatarGrid = styled.div` + display: grid; + grid-template-columns: minmax(0, 1fr); + align-content: start; + color: ${props => (props.isActive ? theme.brand.default : theme.text.alt)}; + font-weight: ${props => (props.isActive ? '600' : '500')}; + background: ${props => + props.isActive ? hexa(theme.brand.default, 0.04) : theme.bg.default}; + + a img { + opacity: ${props => (props.isActive ? '1' : '0.4')}; + filter: ${props => (props.isActive ? 'none' : 'grayscale(80%)')}; + } + + ${props => + props.isActive && + css` + box-shadow: inset 3px 0 0 ${theme.brand.default}; + + img, + a img { + filter: grayscale(0%) !important; + opacity: 1 !important; + } + `} + + &:hover { + box-shadow: inset 3px 0 0 + ${props => (props.isActive ? theme.brand.default : theme.bg.border)}; + background: ${props => + props.isActive ? hexa(theme.brand.default, 0.04) : theme.bg.wash}; + color: ${props => + props.isActive ? theme.brand.default : theme.text.secondary}; + + img, + a img { + filter: grayscale(0%); + opacity: 1; + } + + ${BlackDot} { + background-color: ${theme.warn.alt}; + } + } + + @media (max-width: ${MEDIA_BREAK}px) { + img, + a img { + filter: grayscale(0%); + opacity: 1; + } + } +`; + +export const AvatarLink = styled(Link)` + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 8px 12px; + position: relative; + + @media (max-width: ${MEDIA_BREAK}px) { + flex-direction: row; + justify-content: flex-start; + padding: 8px 20px 8px 12px; + } +`; + +export const Avatar = styled.img` + width: ${props => props.size}px; + height: ${props => props.size}px; + border-radius: 6px; + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.16); + transition: box-shadow 0.2s ease-in-out; + + &:hover { + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2); + transition: box-shadow 0.2s ease-in-out; + } +`; + +export const Shortcut = styled.span` + font-size: 12px; + font-weight: 500; + margin-top: 2px; + text-align: center; + margin-bottom: -4px; + + @media (max-width: ${MEDIA_BREAK}px) { + display: none; + } +`; + +export const Label = styled.span` + font-size: 15px; + margin-left: 12px; + padding-right: 12px; + ${Truncate}; + + @media (min-width: ${MEDIA_BREAK}px) { + display: none; + } +`; + +export const IconWrapper = styled.div` + display: flex; + align-items: center; + justify-content: center; + border-radius: 6px; + opacity: 1; + position: relative; + + &:hover { + color: ${theme.text.default}; + } +`; + +export const Divider = styled.div` + height: 1px; + background: ${theme.bg.border}; + margin: 8px 0; +`; + +// We make it a real link element because anchor links don’t work properly with React Router. +// Ref: https://github.com/ReactTraining/react-router/issues/394. +export const SkipLink = styled.a` + overflow: hidden; + position: absolute; + height: 1px; + width: 1px; + + &:focus { + height: auto; + width: auto; + } +`; diff --git a/src/views/newCommunity/components/createCommunityForm/index.js b/src/views/newCommunity/components/createCommunityForm/index.js index 067d1276fd..8260486d6b 100644 --- a/src/views/newCommunity/components/createCommunityForm/index.js +++ b/src/views/newCommunity/components/createCommunityForm/index.js @@ -15,9 +15,9 @@ import createCommunityMutation from 'shared/graphql/mutations/community/createCo import type { CreateCommunityType } from 'shared/graphql/mutations/community/createCommunity'; import { getCommunityBySlugQuery } from 'shared/graphql/queries/community/getCommunity'; import { searchCommunitiesQuery } from 'shared/graphql/queries/search/searchCommunities'; -import { Button } from 'src/components/buttons'; +import { PrimaryOutlineButton } from 'src/components/button'; import { CommunityHoverProfile } from 'src/components/hoverProfile'; -import Icon from 'src/components/icons'; +import Icon from 'src/components/icon'; import { Input, @@ -27,7 +27,7 @@ import { CoverInput, Error, Checkbox, -} from '../../../../components/formElements'; +} from 'src/components/formElements'; import { ImageInputWrapper, Spacer, @@ -668,7 +668,7 @@ class CreateCommunityForm extends React.Component {
- + ); diff --git a/src/views/newCommunity/components/createCommunityForm/style.js b/src/views/newCommunity/components/createCommunityForm/style.js index 87f551323f..e8a0b91bef 100644 --- a/src/views/newCommunity/components/createCommunityForm/style.js +++ b/src/views/newCommunity/components/createCommunityForm/style.js @@ -1,7 +1,7 @@ // @flow import theme from 'shared/theme'; import styled, { css } from 'styled-components'; -import { FlexCol, FlexRow } from '../../../../components/globals'; +import { FlexCol, FlexRow } from 'src/components/globals'; export const DeleteCoverWrapper = styled(FlexRow)` justify-content: flex-end; @@ -37,7 +37,7 @@ export const ImageInputWrapper = styled(FlexCol)` > label:nth-of-type(2) { position: absolute; bottom: -24px; - left: 24px; + left: 16px; } `; diff --git a/src/views/newCommunity/components/editCommunityForm/index.js b/src/views/newCommunity/components/editCommunityForm/index.js index 5a1811abd8..e01b8e26d5 100644 --- a/src/views/newCommunity/components/editCommunityForm/index.js +++ b/src/views/newCommunity/components/editCommunityForm/index.js @@ -6,9 +6,9 @@ import { withRouter } from 'react-router'; import editCommunityMutation from 'shared/graphql/mutations/community/editCommunity'; import deleteCommunityMutation from 'shared/graphql/mutations/community/deleteCommunity'; import type { GetCommunityType } from 'shared/graphql/queries/community/getCommunity'; -import { addToastWithTimeout } from '../../../../actions/toasts'; -import { Button } from '../../../../components/buttons'; -import { Notice } from '../../../../components/listItems/style'; +import { addToastWithTimeout } from 'src/actions/toasts'; +import { Button } from 'src/components/button'; +import { Notice } from 'src/components/listItems/style'; import { Input, UnderlineInput, @@ -16,8 +16,8 @@ import { PhotoInput, CoverInput, Error, -} from '../../../../components/formElements'; -import { ImageInputWrapper } from '../../../../components/editForm/style'; +} from 'src/components/formElements'; +import { ImageInputWrapper } from 'src/components/editForm/style'; import { Actions, FormContainer, Form } from '../../style'; import type { Dispatch } from 'redux'; diff --git a/src/views/newCommunity/components/share/index.js b/src/views/newCommunity/components/share/index.js index 461a3abc15..52252f64ae 100644 --- a/src/views/newCommunity/components/share/index.js +++ b/src/views/newCommunity/components/share/index.js @@ -2,54 +2,42 @@ import React from 'react'; import { withRouter } from 'react-router'; import compose from 'recompose/compose'; -import { OutlineButton, Button } from '../../../../components/buttons'; +import { + OutlineButton, + PrimaryButton, + FacebookButton, + TwitterButton, +} from 'src/components/button'; import { ButtonRow, InputRow, Input } from './style'; import { Description } from '../../style'; -import { Loading } from '../../../../components/loading'; +import { Loading } from 'src/components/loading'; import Clipboard from 'react-clipboard.js'; -const Share = ({ community, history, onboarding }) => { +const Share = ({ community, onboarding }) => { if (!community) return ; return (
- - - - + + - - + Share on Twitter + { {onboarding && ( - You're ready to start building your community - you can view it now, + You’re ready to start building your community - you can view it now, or manage your settings at any time - - View community settings - - - - + + View community settings + + + Go to my community + )}
diff --git a/src/views/newCommunity/components/share/style.js b/src/views/newCommunity/components/share/style.js index 2c492e86cb..36085f31f4 100644 --- a/src/views/newCommunity/components/share/style.js +++ b/src/views/newCommunity/components/share/style.js @@ -2,7 +2,7 @@ import theme from 'shared/theme'; // $FlowFixMe import styled from 'styled-components'; -import { zIndex } from '../../../../components/globals'; +import { zIndex } from 'src/components/globals'; export const ButtonRow = styled.div` display: flex; diff --git a/src/views/newCommunity/components/stepper/style.js b/src/views/newCommunity/components/stepper/style.js index ec46caffba..4bc952244e 100644 --- a/src/views/newCommunity/components/stepper/style.js +++ b/src/views/newCommunity/components/stepper/style.js @@ -2,7 +2,7 @@ import theme from 'shared/theme'; // $FlowFixMe import styled from 'styled-components'; -import { zIndex } from '../../../../components/globals'; +import { zIndex } from 'src/components/globals'; export const Container = styled.div` display: flex; diff --git a/src/views/newCommunity/index.js b/src/views/newCommunity/index.js index b8fe136e38..6be07bf1c7 100644 --- a/src/views/newCommunity/index.js +++ b/src/views/newCommunity/index.js @@ -4,24 +4,22 @@ import compose from 'recompose/compose'; import { connect } from 'react-redux'; import { withApollo } from 'react-apollo'; import queryString from 'query-string'; -import { Button, TextButton } from '../../components/buttons'; -import AppViewWrapper from '../../components/appViewWrapper'; -import Column from '../../components/column'; -import { Loading } from '../../components/loading'; +import { Button, TextButton } from 'src/components/button'; import SlackConnection from '../communitySettings/components/slack'; -import { CommunityInvitationForm } from '../../components/emailInvitationForm'; +import { CommunityInvitationForm } from 'src/components/emailInvitationForm'; import CreateCommunityForm from './components/createCommunityForm'; import EditCommunityForm from './components/editCommunityForm'; -import Titlebar from '../titlebar'; import Stepper from './components/stepper'; import Share from './components/share'; -import { Login } from '../../views/login'; +import Login from 'src/views/login'; +import { setTitlebarProps } from 'src/actions/titlebar'; import { getCommunityByIdQuery } from 'shared/graphql/queries/community/getCommunity'; import type { GetCommunityType } from 'shared/graphql/queries/community/getCommunity'; import getCurrentUserSettings, { type GetCurrentUserSettingsType, } from 'shared/graphql/queries/user/getCurrentUserSettings'; -import UserEmailConfirmation from '../../components/userEmailConfirmation'; +import UserEmailConfirmation from 'src/components/userEmailConfirmation'; +import { LoadingView } from 'src/views/viewHelpers'; import { Actions, Container, @@ -32,7 +30,8 @@ import { } from './style'; import viewNetworkHandler, { type ViewNetworkHandlerType, -} from '../../components/viewNetworkHandler'; +} from 'src/components/viewNetworkHandler'; +import { ViewGrid, SingleColumnGrid } from 'src/components/layout'; type State = { activeStep: number, @@ -43,6 +42,7 @@ type State = { }; type Props = { + dispatch: Function, ...$Exact, client: Object, history: Object, @@ -72,6 +72,10 @@ class NewCommunity extends React.Component { componentDidMount() { const { existingId } = this.state; + const { dispatch } = this.props; + + dispatch(setTitlebarProps({ title: 'New community' })); + if (!existingId) return; this.props.client @@ -177,15 +181,8 @@ class NewCommunity extends React.Component { const description = this.description(); if (user && user.email) { return ( - - - - + + {title} @@ -194,31 +191,25 @@ class NewCommunity extends React.Component { {// gather community meta info - activeStep === 1 && - !community && ( - - )} + activeStep === 1 && !community && ( + + )} - {activeStep === 1 && - community && ( - - )} + {activeStep === 1 && community && ( + + )} - {activeStep === 2 && - community && - community.id && ( - - - - - - - )} + {activeStep === 2 && community && community.id && ( + + + + + + + )} {// connect a slack team or invite via email activeStep === 2 && ( @@ -229,10 +220,7 @@ class NewCommunity extends React.Component { {hasInvitedPeople ? ( ) : ( - this.step('next')} - > + this.step('next')}> Skip this step )} @@ -246,22 +234,15 @@ class NewCommunity extends React.Component { )} - - + + ); } if (user && !user.email) { return ( - - - - + + {user.pendingEmail ? 'Confirm' : 'Add'} Your Email Address @@ -277,27 +258,19 @@ class NewCommunity extends React.Component<Props, State> { <UserEmailConfirmation user={user} /> </div> </Container> - </Column> - </AppViewWrapper> + </SingleColumnGrid> + </ViewGrid> ); } - if (isLoading) { - return ( - <AppViewWrapper> - <Titlebar - title={'Create a Community'} - provideBack={true} - backRoute={'/'} - noComposer - /> - - <Loading /> - </AppViewWrapper> - ); - } + if (isLoading) return <LoadingView />; - return <Login redirectPath={`${window.location.href}`} />; + return ( + <Login + dispatch={this.props.dispatch} + redirectPath={`${window.location.href}`} + /> + ); } } diff --git a/src/views/newCommunity/style.js b/src/views/newCommunity/style.js index dbd98e8bbb..5ce25e5370 100644 --- a/src/views/newCommunity/style.js +++ b/src/views/newCommunity/style.js @@ -1,8 +1,8 @@ // @flow import theme from 'shared/theme'; -// $FlowFixMe import styled from 'styled-components'; -import Card from '../../components/card'; +import Card from 'src/components/card'; +import { MEDIA_BREAK } from 'src/components/layout'; export const Container = styled(Card)` background-image: ${props => @@ -13,9 +13,18 @@ export const Container = styled(Card)` background-position: ${props => props.repeat ? 'center top' : 'center center'}; width: 100%; - height: auto; + border-left: 1px solid ${theme.bg.border}; + border-right: 1px solid ${theme.bg.border}; + max-width: ${MEDIA_BREAK}px; + height: 100%; + min-height: 100%; min-height: 160px; display: flex; + + @media (max-width: ${MEDIA_BREAK}px) { + border-left: 0; + border-right: 0; + } `; export const Actions = styled.div` diff --git a/src/views/newDirectMessage/components/messagesCheck.js b/src/views/newDirectMessage/components/messagesCheck.js new file mode 100644 index 0000000000..a8cd23197f --- /dev/null +++ b/src/views/newDirectMessage/components/messagesCheck.js @@ -0,0 +1,40 @@ +// @flow +import React from 'react'; +import compose from 'recompose/compose'; +import getDirectMessageThreadByUserIds, { + type GetDirectMessageThreadByUserIdsType, +} from 'shared/graphql/queries/directMessageThread/getDirectMessageThreadByUserIds'; +import viewNetworkHandler, { + type ViewNetworkHandlerType, +} from 'src/components/viewNetworkHandler'; +import MessagesSubscriber from './messagesSubscriber'; +import { LoadingMessagesWrapper, NullMessagesWrapper } from '../style'; + +type Props = { + ...$Exact<ViewNetworkHandlerType>, + data: { + directMessageThreadByUserIds: GetDirectMessageThreadByUserIdsType, + }, + onExistingThreadId: Function, +}; + +const MessagesCheck = (props: Props) => { + const { data, isLoading, hasError, onExistingThreadId } = props; + + const { directMessageThreadByUserIds: thread } = data; + + if (isLoading) return <LoadingMessagesWrapper />; + + if (!thread || hasError) return <NullMessagesWrapper />; + + if (thread && thread.id) { + onExistingThreadId(thread.id); + } + + return <MessagesSubscriber id={thread.id} />; +}; + +export default compose( + getDirectMessageThreadByUserIds, + viewNetworkHandler +)(MessagesCheck); diff --git a/src/views/newDirectMessage/components/messagesSubscriber.js b/src/views/newDirectMessage/components/messagesSubscriber.js new file mode 100644 index 0000000000..b1c6eb0df8 --- /dev/null +++ b/src/views/newDirectMessage/components/messagesSubscriber.js @@ -0,0 +1,106 @@ +// @flow +import React, { useEffect } from 'react'; +import compose from 'recompose/compose'; +import { sortAndGroupMessages } from 'shared/clients/group-messages'; +import ChatMessages from 'src/components/messageGroup'; +import { Loading } from 'src/components/loading'; +import viewNetworkHandler from 'src/components/viewNetworkHandler'; +import getDirectMessageThreadMessages from 'shared/graphql/queries/directMessageThread/getDirectMessageThreadMessageConnection'; +import type { GetDirectMessageThreadMessageConnectionType } from 'shared/graphql/queries/directMessageThread/getDirectMessageThreadMessageConnection'; +import setLastSeenMutation from 'shared/graphql/mutations/directMessageThread/setDMThreadLastSeen'; +import { ErrorBoundary } from 'src/components/error'; +import { MessagesScrollWrapper, LoadingMessagesWrapper } from '../style'; + +type Props = { + id: string, + data: { + loading: boolean, + directMessageThread: GetDirectMessageThreadMessageConnectionType, + messages: Array<Object>, + hasNextPage: boolean, + fetchMore: Function, + }, + subscribeToNewMessages: Function, + isLoading: boolean, + hasError: boolean, + isFetchingMore: boolean, + setLastSeen: Function, +}; + +const MessagesSubscriber = (props: Props) => { + const { + subscribeToNewMessages, + setLastSeen, + id, + data, + isLoading, + hasError, + } = props; + + const { messages, directMessageThread } = data; + + let ref = null; + + const scrollToBottom = () => { + if (!ref || !messages || messages.length === 0) { + return; + } + let { scrollHeight, clientHeight } = ref; + return (ref.scrollTop = scrollHeight - clientHeight); + }; + + useEffect(() => { + setLastSeen(id); + scrollToBottom(); + return subscribeToNewMessages(); + }, [id]); + + const refHeight = ref && ref.scrollHeight; + useEffect(() => { + scrollToBottom(); + }, [refHeight, id, messages, isLoading]); + + if (hasError) return <LoadingMessagesWrapper ref={el => (ref = el)} />; + if (isLoading) + return ( + <LoadingMessagesWrapper ref={el => (ref = el)}> + <Loading style={{ padding: '64px 32px' }} /> + </LoadingMessagesWrapper> + ); + + let unsortedMessages = messages.map(message => message.node); + + const unique = array => { + const processed = []; + for (let i = array.length - 1; i >= 0; i--) { + if (processed.indexOf(array[i].id) < 0) { + processed.push(array[i].id); + } else { + array.splice(i, 1); + } + } + return array; + }; + + const uniqueMessages = unique(unsortedMessages); + const sortedMessages = sortAndGroupMessages(uniqueMessages); + + return ( + <MessagesScrollWrapper ref={el => (ref = el)}> + <ErrorBoundary> + <ChatMessages + messages={sortedMessages} + uniqueMessageCount={uniqueMessages.length} + threadType={'directMessageThread'} + thread={directMessageThread} + /> + </ErrorBoundary> + </MessagesScrollWrapper> + ); +}; + +export default compose( + setLastSeenMutation, + getDirectMessageThreadMessages, + viewNetworkHandler +)(MessagesSubscriber); diff --git a/src/views/newDirectMessage/components/selectedUserPill.js b/src/views/newDirectMessage/components/selectedUserPill.js new file mode 100644 index 0000000000..8b98b781d8 --- /dev/null +++ b/src/views/newDirectMessage/components/selectedUserPill.js @@ -0,0 +1,29 @@ +// @flow +import React from 'react'; +import Icon from 'src/components/icon'; +import { Pill, PillAvatar } from '../style'; +import type { UserInfoType } from 'shared/graphql/fragments/user/userInfo'; + +type Props = { + user: UserInfoType, + usersForMessage: Array<UserInfoType>, + setUsersForMessage: Function, +}; + +const SelectedUserPill = (props: Props) => { + const { user, usersForMessage, setUsersForMessage } = props; + + const handleUserSelection = () => { + setUsersForMessage(usersForMessage.filter(u => u.id !== user.id)); + }; + + return ( + <Pill onClick={handleUserSelection}> + <PillAvatar src={user.profilePhoto} /> + {user.name} + <Icon glyph={'view-close'} size={20} /> + </Pill> + ); +}; + +export default SelectedUserPill; diff --git a/src/views/newDirectMessage/components/userSearch.js b/src/views/newDirectMessage/components/userSearch.js new file mode 100644 index 0000000000..b93ed2654d --- /dev/null +++ b/src/views/newDirectMessage/components/userSearch.js @@ -0,0 +1,4 @@ +// @flow +import React from 'react'; + +export default () => 'user search'; diff --git a/src/views/newDirectMessage/components/usersSearch.js b/src/views/newDirectMessage/components/usersSearch.js new file mode 100644 index 0000000000..54a1e5d863 --- /dev/null +++ b/src/views/newDirectMessage/components/usersSearch.js @@ -0,0 +1,41 @@ +// @flow +import React, { useState } from 'react'; +import { SearchInput, SearchInputWrapper } from '../style'; +import UsersSearchResults from './usersSearchResults'; + +const UsersSearch = (props: *) => { + let ref = null; + const [searchQuery, setSearchQuery] = useState(''); + + const onChange = (e: any) => setSearchQuery(e.target.value); + const onUserSelected = () => { + setSearchQuery(''); + ref && ref.focus(); + }; + + return ( + <React.Fragment> + <SearchInputWrapper> + <SearchInput + ref={el => (ref = el)} + value={searchQuery} + onChange={onChange} + type="text" + placeholder="Search for people..." + autoFocus + data-cy="dm-composer-search" + /> + </SearchInputWrapper> + + {searchQuery && ( + <UsersSearchResults + onUserSelected={onUserSelected} + queryString={searchQuery} + {...props} + /> + )} + </React.Fragment> + ); +}; + +export default UsersSearch; diff --git a/src/views/newDirectMessage/components/usersSearchResults.js b/src/views/newDirectMessage/components/usersSearchResults.js new file mode 100644 index 0000000000..da7855856b --- /dev/null +++ b/src/views/newDirectMessage/components/usersSearchResults.js @@ -0,0 +1,98 @@ +// @flow +import React from 'react'; +import compose from 'recompose/compose'; +import searchUsers, { + type SearchUsersType, +} from 'shared/graphql/queries/search/searchUsers'; +import viewNetworkHandler, { + type ViewNetworkHandlerType, +} from 'src/components/viewNetworkHandler'; +import { Loading } from 'src/components/loading'; +import { withCurrentUser } from 'src/components/withCurrentUser'; +import { UserListItem } from 'src/components/entities'; +import { SearchResultsWrapper } from '../style'; +import type { UserInfoType } from 'shared/graphql/fragments/user/userInfo'; + +type Props = { + ...$Exact<ViewNetworkHandlerType>, + data: { + search: SearchUsersType, + }, + queryVarIsChanging: boolean, + currentUser?: Object, + usersForMessage: Array<UserInfoType>, + setUsersForMessage: Function, + onUserSelected: Function, +}; + +const UsersSearchResults = (props: Props) => { + const { + isLoading, + hasError, + queryVarIsChanging, + data, + currentUser, + usersForMessage, + setUsersForMessage, + onUserSelected, + } = props; + const { search } = data; + + if (isLoading || queryVarIsChanging) + return <Loading style={{ padding: '32px' }} />; + if (!search || hasError) return <p>Bad news...</p>; + + const { searchResultsConnection } = search; + const { edges } = searchResultsConnection; + + if (!edges || edges.length === 0) return <p>No results...</p>; + + const results = edges + .map(edge => edge && edge.node) + .filter(Boolean) + .filter(user => user.username) + .filter(Boolean); + + if (results.length === 0) return <p>No results...</p>; + + const handleUserSelection = (selection: Object) => { + onUserSelected(); + const isAlreadySelected = usersForMessage.find( + user => user && user.id === selection.id + ); + if (isAlreadySelected) { + setUsersForMessage( + usersForMessage.filter(user => user && user.id !== selection.id) + ); + } else { + setUsersForMessage([...usersForMessage, selection]); + } + }; + + return ( + <SearchResultsWrapper> + {results.map(user => ( + <UserListItem + key={user.id} + userObject={user} + id={user.id} + name={user.name} + username={user.username} + isCurrentUser={currentUser && user.id === currentUser.id} + isOnline={user.isOnline} + profilePhoto={user.profilePhoto} + avatarSize={40} + showHoverProfile={false} + isLink={false} + onClick={handleUserSelection} + /> + ))} + </SearchResultsWrapper> + ); +}; + +export default compose( + searchUsers, + viewNetworkHandler, + withCurrentUser +)(UsersSearchResults); diff --git a/src/views/newDirectMessage/containers/index.js b/src/views/newDirectMessage/containers/index.js new file mode 100644 index 0000000000..d0f1085481 --- /dev/null +++ b/src/views/newDirectMessage/containers/index.js @@ -0,0 +1,6 @@ +// @flow +import ViewContainer from './viewContainer'; +import Search from './search'; +import Write from './write'; + +export { ViewContainer, Search, Write }; diff --git a/src/views/newDirectMessage/containers/search.js b/src/views/newDirectMessage/containers/search.js new file mode 100644 index 0000000000..875c6864d0 --- /dev/null +++ b/src/views/newDirectMessage/containers/search.js @@ -0,0 +1,77 @@ +// @flow +import React, { useEffect } from 'react'; +import { connect } from 'react-redux'; +import { setTitlebarProps } from 'src/actions/titlebar'; +import { DesktopTitlebar } from 'src/components/titlebar'; +import { PrimaryButton } from 'src/components/button'; +import UsersSearch from '../components/usersSearch'; +import SelectedUserPill from '../components/selectedUserPill'; +import { SelectedPillsWrapper } from '../style'; +import type { UserInfoType } from 'shared/graphql/fragments/user/userInfo'; + +type Props = { + usersForMessage: Array<UserInfoType>, + setUsersForMessage: Function, + setActiveStep: Function, + dispatch: Function, +}; + +const Search = (props: Props) => { + const { + usersForMessage, + setUsersForMessage, + setActiveStep, + dispatch, + } = props; + + const toWrite = () => setActiveStep('write'); + + useEffect(() => { + dispatch( + setTitlebarProps({ + title: 'New message', + leftAction: 'view-back', + rightAction: ( + <PrimaryButton + size={'small'} + disabled={!usersForMessage || usersForMessage.length === 0} + onClick={toWrite} + > + Next + </PrimaryButton> + ), + }) + ); + }); + + return ( + <React.Fragment> + <DesktopTitlebar + title={'New message'} + rightAction={ + <PrimaryButton + size={'small'} + disabled={!usersForMessage || usersForMessage.length === 0} + onClick={toWrite} + > + Next + </PrimaryButton> + } + /> + {usersForMessage && usersForMessage.length > 0 && ( + <SelectedPillsWrapper> + {usersForMessage.map( + user => + user && <SelectedUserPill key={user.id} user={user} {...props} /> + )} + </SelectedPillsWrapper> + )} + <UsersSearch + usersForMessage={usersForMessage} + setUsersForMessage={setUsersForMessage} + /> + </React.Fragment> + ); +}; + +export default connect()(Search); diff --git a/src/views/newDirectMessage/containers/viewContainer.js b/src/views/newDirectMessage/containers/viewContainer.js new file mode 100644 index 0000000000..f021905d98 --- /dev/null +++ b/src/views/newDirectMessage/containers/viewContainer.js @@ -0,0 +1,63 @@ +// @flow +import React, { useEffect } from 'react'; +import { + withRouter, + type Location, + type History, + type Match, +} from 'react-router-dom'; +import Icon from 'src/components/icon'; +import { ErrorBoundary } from 'src/components/error'; +import { ESC } from 'src/helpers/keycodes'; +import { Container, Overlay, ComposerContainer, CloseButton } from '../style'; + +type Props = { + previousLocation: Location, + history: History, + match: Match, + isModal?: boolean, + children: React$Node, +}; + +const NewDirectMessage = (props: Props) => { + const { previousLocation, history, isModal, children } = props; + + const closeComposer = (e: any) => { + e && e.stopPropagation(); + if (isModal) { + history.push(previousLocation); + } else { + history.push('/messages'); + } + }; + + useEffect(() => { + const handleKeyPress = (e: any) => { + if (e.keyCode === ESC) { + e.stopPropagation(); + closeComposer(); + } + }; + + document.addEventListener('keydown', handleKeyPress, false); + return () => { + document.removeEventListener('keydown', handleKeyPress, false); + }; + }, []); + + return ( + <ErrorBoundary> + <Container data-cy="dm-composer"> + <Overlay onClick={closeComposer} data-cy="dm-composer-overlay" /> + + <CloseButton data-cy="close-dm-composer" onClick={closeComposer}> + <Icon glyph="view-close" size={32} /> + </CloseButton> + + <ComposerContainer>{children}</ComposerContainer> + </Container> + </ErrorBoundary> + ); +}; + +export default withRouter(NewDirectMessage); diff --git a/src/views/newDirectMessage/containers/write.js b/src/views/newDirectMessage/containers/write.js new file mode 100644 index 0000000000..61640e45ea --- /dev/null +++ b/src/views/newDirectMessage/containers/write.js @@ -0,0 +1,131 @@ +// @flow +import React, { useEffect, useState } from 'react'; +import compose from 'recompose/compose'; +import { connect } from 'react-redux'; +import createDirectMessageThreadMutation from 'shared/graphql/mutations/directMessageThread/createDirectMessageThread'; +import { addToastWithTimeout } from 'src/actions/toasts'; +import { setTitlebarProps } from 'src/actions/titlebar'; +import ChatInput from 'src/components/chatInput'; +import { DesktopTitlebar } from 'src/components/titlebar'; +import { UserAvatar } from 'src/components/avatar'; +import { OutlineButton } from 'src/components/button'; +import { initNewThreadWithUser } from 'src/actions/directMessageThreads'; +import { ErrorBoundary } from 'src/components/error'; +import MessagesCheck from '../components/messagesCheck'; +import { ChatInputWrapper } from 'src/components/layout'; +import MessagesSubscriber from '../components/messagesSubscriber'; +import type { UserInfoType } from 'shared/graphql/fragments/user/userInfo'; + +type Props = { + usersForMessage: Array<UserInfoType>, + hadInitialUser?: boolean, + setActiveStep: Function, + dispatch: Function, + createDirectMessageThread: Function, +}; + +const Write = (props: Props) => { + const { + usersForMessage, + hadInitialUser, + setActiveStep, + dispatch, + createDirectMessageThread, + } = props; + + const [existingThreadId, setExistingThreadId] = useState(null); + + const titlebarTitle = + usersForMessage.length > 1 + ? usersForMessage.map(user => user && user.name).join(', ') + : usersForMessage[0].name; + const titleIcon = + usersForMessage.length === 1 ? ( + <UserAvatar user={usersForMessage[0]} size={24} /> + ) : null; + + const toSearch = () => setActiveStep('search'); + useEffect(() => { + dispatch( + setTitlebarProps({ + title: titlebarTitle, + titleIcon: titleIcon, + leftAction: 'view-back', + }) + ); + if (hadInitialUser) dispatch(initNewThreadWithUser(null)); + }, []); + + const createThread = ({ messageBody, messageType, file }) => { + const input = { + participants: usersForMessage.map(user => user && user.id), + message: { + messageType: messageType, + threadType: 'directMessageThread', + content: { + body: messageBody || '', + }, + file: file && file, + }, + }; + + return createDirectMessageThread(input) + .then(({ data: { createDirectMessageThread } }) => { + if (!createDirectMessageThread) { + return dispatch( + addToastWithTimeout( + 'error', + 'Failed to create direct message thread, please try again!' + ) + ); + } + + setExistingThreadId(createDirectMessageThread.id); + }) + .catch(err => { + dispatch(addToastWithTimeout('error', err.message)); + }); + }; + + return ( + <React.Fragment> + <DesktopTitlebar + data-cy="write-direct-message-titlebar" + title={titlebarTitle} + titleIcon={titleIcon} + rightAction={ + !hadInitialUser && ( + <OutlineButton size={'small'} onClick={toSearch}> + Edit + </OutlineButton> + ) + } + /> + + {existingThreadId ? ( + <MessagesSubscriber id={existingThreadId} /> + ) : ( + <ErrorBoundary> + <MessagesCheck + userIds={usersForMessage.map(user => user && user.id)} + onExistingThreadId={setExistingThreadId} + {...props} + /> + </ErrorBoundary> + )} + + <ChatInputWrapper> + <ChatInput + threadId={existingThreadId || 'newDirectMessageThread'} + threadType={'directMessageThread'} + createThread={createThread} + /> + </ChatInputWrapper> + </React.Fragment> + ); +}; + +export default compose( + createDirectMessageThreadMutation, + connect() +)(Write); diff --git a/src/views/newDirectMessage/index.js b/src/views/newDirectMessage/index.js new file mode 100644 index 0000000000..654d4996f3 --- /dev/null +++ b/src/views/newDirectMessage/index.js @@ -0,0 +1,54 @@ +// @flow +import React, { useState } from 'react'; +import compose from 'recompose/compose'; +import { connect } from 'react-redux'; +import { Search, Write, ViewContainer } from './containers'; + +type Props = { + initialUsers: Array<?string>, +}; + +const NewDirectMessage = (props: Props) => { + const { initialUsers } = props; + const [usersForMessage, setUsersForMessage] = useState(initialUsers); + const [activeStep, setActiveStep] = useState( + initialUsers.filter(Boolean).length > 0 ? 'write' : 'search' + ); + + if (activeStep === 'search') { + return ( + <ViewContainer {...props}> + <Search + usersForMessage={usersForMessage.filter(Boolean)} + setUsersForMessage={setUsersForMessage} + setActiveStep={setActiveStep} + /> + </ViewContainer> + ); + } + + if (activeStep === 'write') { + return ( + <ViewContainer {...props}> + <Write + /* + If the modal loaded with an initial user, it means the user is composing + a dm from somewhere in the app like a user profile. In this case, + there is no "search" step to go back to, so we let the Write + container know to not allow a back action to return to the Search + step + */ + hadInitialUser={initialUsers.length === 1} + usersForMessage={usersForMessage.filter(Boolean)} + setActiveStep={setActiveStep} + /> + </ViewContainer> + ); + } +}; + +const map = (state): * => ({ + initialUsers: state.directMessageThreads.initNewThreadWithUser, +}); + +export default compose(connect(map))(NewDirectMessage); diff --git a/src/views/newDirectMessage/style.js b/src/views/newDirectMessage/style.js new file mode 100644 index 0000000000..d6c7994e8e --- /dev/null +++ b/src/views/newDirectMessage/style.js @@ -0,0 +1,158 @@ +// @flow +import styled from 'styled-components'; +import theme from 'shared/theme'; +import { hexa } from 'src/components/globals'; +import { MEDIA_BREAK, TITLEBAR_HEIGHT } from 'src/components/layout'; + +export const Container = styled.div` + grid-area: main; + display: flex; + justify-content: center; + z-index: 9995; + position: sticky; + top: 0; + align-items: center; + justify-content: center; +`; + +export const Overlay = styled.div` + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.24); + z-index: 9996; +`; + +export const ComposerContainer = styled.div` + display: flex; + flex-direction: column; + height: 500px; + max-height: 90vh; + width: 100%; + max-width: 500px; + border-radius: 4px; + background: ${theme.bg.wash}; + z-index: 9997; + box-shadow: 4px 0 16px rgba(0, 0, 0, 0.12); + position: relative; + overflow: hidden; + + @media (max-width: ${MEDIA_BREAK}px) { + max-width: 100vw; + max-height: calc(100vh - ${TITLEBAR_HEIGHT}px); + height: calc(100vh - ${TITLEBAR_HEIGHT}px); + border-radius: 0; + } +`; + +export const CloseButton = styled.span` + position: fixed; + top: 24px; + right: 24px; + width: 60px; + height: 60px; + border-radius: 30px; + display: flex; + align-items: center; + justify-content: center; + background: ${theme.bg.reverse}; + color: ${theme.text.reverse}; + z-index: 9997; + cursor: pointer; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.12); + + @media (max-width: ${MEDIA_BREAK}px) { + display: none; + } +`; + +export const MessagesScrollWrapper = styled.div` + /* height of container minus chat input */ + height: 100%; + max-height: calc(100% - 58px); + overflow: hidden; + overflow-y: scroll; + background: ${theme.bg.default}; +`; + +export const LoadingMessagesWrapper = styled(MessagesScrollWrapper)` + display: flex; + align-items: center; + justify-content: center; + background: ${theme.bg.wash}; +`; + +export const NullMessagesWrapper = styled(MessagesScrollWrapper)` + display: flex; + align-items: center; + justify-content: center; + background: ${theme.bg.wash}; +`; + +export const SearchInputWrapper = styled.div` + background: ${theme.bg.default}; + border-bottom: 1px solid ${theme.bg.border}; +`; + +export const SearchInput = styled.input` + background: ${theme.bg.default}; + font-size: 16px; /* has to be 16px to avoid zoom on iOS */ + display: block; + width: 100%; + padding: 16px; +`; + +export const SearchResultsWrapper = styled.div` + /* height of container minus titlebart */ + height: 100%; + max-height: calc(100% - 62px); + overflow: hidden; + overflow-y: scroll; + background: ${theme.bg.default}; +`; + +export const SelectedPillsWrapper = styled.div` + display: flex; + flex-wrap: wrap; + border-bottom: 1px solid ${theme.bg.border}; + padding: 12px 16px; + padding-bottom: 4px; + align-items: center; + flex: none; +`; + +export const Pill = styled.div` + display: flex; + border-radius: 24px; + align-items: center; + justify-content: space-between; + color: ${theme.brand.default}; + padding: 2px; + padding-right: 6px; + background: ${hexa(theme.brand.default, 0.04)}; + margin-right: 8px; + margin-bottom: 8px; + font-size: 15px; + font-weight: 500; + cursor: pointer; + + &:hover { + background: ${hexa(theme.brand.default, 0.08)}; + } + + img { + margin-right: 12px; + } + + .icon { + margin-left: 8px; + } +`; + +export const PillAvatar = styled.img` + width: 24px; + height: 24px; + border-radius: 12px; +`; diff --git a/src/views/newUserOnboarding/components/appsUpsell/index.js b/src/views/newUserOnboarding/components/appsUpsell/index.js deleted file mode 100644 index e6a38e3e4b..0000000000 --- a/src/views/newUserOnboarding/components/appsUpsell/index.js +++ /dev/null @@ -1,60 +0,0 @@ -// @flow -import * as React from 'react'; -import { Button, TextButton } from 'src/components/buttons'; -import { track, events } from 'src/helpers/analytics'; -import { ActionsContainer } from './style'; -import { DESKTOP_APP_MAC_URL } from 'src/helpers/desktop-app-utils'; - -type Props = { - nextStep: (step: string) => void, - onDownload: () => void, -}; - -type State = { - didDownload: boolean, -}; - -class AppsUpsell extends React.Component<Props, State> { - state = { didDownload: false }; - - componentDidMount() { - track(events.USER_ONBOARDING_APPS_UPSELL_STEP_VIEWED); - } - - onDownload = () => { - track(events.USER_ONBOARDING_APPS_UPSELL_APP_DOWNLOADED, { app: 'mac' }); - this.props.onDownload(); - return this.setState({ didDownload: true }); - }; - - render() { - const { nextStep } = this.props; - const { didDownload } = this.state; - - if (didDownload) { - return ( - <ActionsContainer> - <Button onClick={nextStep} large> - Continue - </Button> - </ActionsContainer> - ); - } - - return ( - <ActionsContainer> - <a href={DESKTOP_APP_MAC_URL}> - <Button large icon="apple" onClick={this.onDownload}> - Download for Mac - </Button> - </a> - - <TextButton onClick={nextStep} large> - Skip for now - </TextButton> - </ActionsContainer> - ); - } -} - -export default AppsUpsell; diff --git a/src/views/newUserOnboarding/components/appsUpsell/style.js b/src/views/newUserOnboarding/components/appsUpsell/style.js deleted file mode 100644 index 354b845ff2..0000000000 --- a/src/views/newUserOnboarding/components/appsUpsell/style.js +++ /dev/null @@ -1,13 +0,0 @@ -// @flow -import styled from 'styled-components'; - -export const ActionsContainer = styled.div` - display: flex; - flex-direction: column; - align-items: center; - padding: 32px 0; - - button { - margin-bottom: 16px; - } -`; diff --git a/src/views/newUserOnboarding/components/communitySearch/index.js b/src/views/newUserOnboarding/components/communitySearch/index.js deleted file mode 100644 index 8cfff2f654..0000000000 --- a/src/views/newUserOnboarding/components/communitySearch/index.js +++ /dev/null @@ -1,384 +0,0 @@ -// @flow -import * as React from 'react'; -import { withApollo } from 'react-apollo'; -import { withRouter } from 'react-router'; -import compose from 'recompose/compose'; -import { Link } from 'react-router-dom'; -import { connect } from 'react-redux'; -import { Button, OutlineButton } from 'src/components/buttons'; -import ToggleCommunityMembership from 'src/components/toggleCommunityMembership'; -import { debounce } from 'src/helpers/utils'; -import { searchCommunitiesQuery } from 'shared/graphql/queries/search/searchCommunities'; -import { Spinner } from 'src/components/globals'; -import OutsideClickHandler from 'src/components/outsideClickHandler'; -import type { Dispatch } from 'redux'; -import { - SearchWrapper, - SearchInput, - SearchInputWrapper, - SearchSpinnerContainer, - SearchResultsDropdown, - SearchResult, - SearchResultNull, - SearchResultImage, - SearchResultMetaWrapper, - SearchResultName, - SearchResultMetadata, - SearchIcon, - SearchResultDescription, -} from './style'; -import { ESC, ARROW_DOWN, ARROW_UP } from 'src/helpers/keycodes'; - -type State = { - searchString: string, - searchResults: Array<any>, - searchIsLoading: boolean, - focusedSearchResult: string, - isFocused: boolean, -}; - -type Props = { - toggleCommunityMembership: Function, - dispatch: Dispatch<Object>, - client: Object, - joinedCommunity: Function, -}; - -class Search extends React.Component<Props, State> { - input: React.Node; - - constructor() { - super(); - - this.state = { - searchString: '', - searchResults: [], - searchIsLoading: false, - focusedSearchResult: '', - isFocused: true, - }; - - // only kick off search query every 200ms - this.search = debounce(this.search, 500, false); - } - - onJoinComplete = result => { - const { searchResults } = this.state; - - // because we are using state to display the search results, - // we can't rely on the apollo cache to automatically update the - // display of the join/leave buttons in the search results dropdown - // so we update the state manually with the new membership boolean - // returned from the mutation - const isMember = result.communityPermissions.isMember; - - if (isMember) { - this.props.joinedCommunity(1, false); - } else { - this.props.joinedCommunity(-1, false); - } - - const newSearchResults = searchResults.map(community => { - if (community.id === result.id) { - const newObj = Object.assign({}, ...community, { - ...community, - communityPermissions: { - ...community.communityPermissions, - isMember, - }, - }); - - return newObj; - } - return community; - }); - - return this.setState({ - searchResults: newSearchResults, - }); - }; - - search = (searchString: string) => { - const { client } = this.props; - - if (!searchString || searchString.length === 0) { - return this.setState({ - searchIsLoading: false, - }); - } - - // start the input loading spinner - this.setState({ - searchIsLoading: true, - }); - - // trigger the query - client - .query({ - query: searchCommunitiesQuery, - variables: { queryString: searchString, type: 'COMMUNITIES' }, - }) - .then(({ data: { search } }) => { - if ( - !search || - !search.searchResultsConnection || - search.searchResultsConnection.edges.length === 0 - ) { - return this.setState({ - searchResults: [], - searchIsLoading: false, - focusedSearchResult: '', - }); - } - - const searchResults = search.searchResultsConnection.edges.map( - c => c.node - ); - // sort by membership count - const sorted = searchResults - .slice() - .sort((a, b) => { - return b.metaData.members - a.metaData.members; - }) - // don't display communities where the user is blocked - .filter(community => !community.communityPermissions.isBlocked); - - if (!sorted || sorted.length === 0) { - return this.setState({ - searchResults: [], - searchIsLoading: false, - focusedSearchResult: '', - }); - } else { - return this.setState({ - searchResults: sorted, - searchIsLoading: false, - focusedSearchResult: sorted[0].id, - }); - } - }) - .catch(err => { - console.error('Error searching for communities: ', err); - }); - }; - - handleKeyPress = (e: any) => { - const { searchResults, focusedSearchResult } = this.state; - - const input = this.input; - const searchResultIds = - searchResults && searchResults.map(community => community.id); - const indexOfFocusedSearchResult = searchResultIds.indexOf( - focusedSearchResult - ); - - if (e.keyCode === ESC) { - this.setState({ - isFocused: false, - }); - - // $FlowFixMe - input && input.focus(); - return; - } - - if (e.keyCode === ARROW_DOWN) { - if (indexOfFocusedSearchResult === searchResults.length - 1) return; - if (searchResults.length === 1) return; - - const resultToFocus = searchResults[indexOfFocusedSearchResult + 1]; - if (!resultToFocus) return; - - return this.setState({ - focusedSearchResult: resultToFocus.id, - }); - } - - if (e.keyCode === ARROW_UP) { - if (indexOfFocusedSearchResult === 0) return; - if (searchResults.length === 1) return; - - const resultToFocus = searchResults[indexOfFocusedSearchResult - 1]; - if (!resultToFocus) return; - - return this.setState({ - focusedSearchResult: resultToFocus.id, - }); - } - }; - - handleChange = (e: any) => { - const string = e.target.value.toLowerCase().trim(); - - if (string.length === 0) { - return this.setState({ - searchIsLoading: false, - searchString: '', - }); - } - - // set the searchstring to state - this.setState({ - searchString: e.target.value, - searchIsLoading: true, - }); - - // trigger a new search based on the search input - // $FlowIssue - this.search(string); - }; - - componentDidMount() { - document.addEventListener('keydown', this.handleKeyPress, false); - } - - componentWillUnmount() { - document.removeEventListener('keydown', this.handleKeyPress, false); - } - - onFocus = (e: any) => { - const val = e.target.value; - if (!val || val.length === 0) return; - - const string = val.toLowerCase().trim(); - - // $FlowIssue - this.search(string); - - return this.setState({ - isFocused: true, - }); - }; - - hideSearchResults = () => { - return this.setState({ - isFocused: false, - }); - }; - - render() { - const { - searchString, - searchIsLoading, - searchResults, - focusedSearchResult, - isFocused, - } = this.state; - - return ( - <SearchWrapper> - {searchIsLoading && ( - <SearchSpinnerContainer> - <Spinner size={16} color={'brand.default'} /> - </SearchSpinnerContainer> - )} - <SearchInputWrapper> - <SearchIcon glyph="search" onClick={this.onFocus} /> - <SearchInput - innerRef={c => { - this.input = c; - }} - type="text" - value={searchString} - placeholder="Search for communities or topics..." - onChange={this.handleChange} - onFocus={this.onFocus} - autoFocus - /> - </SearchInputWrapper> - - {// user has typed in a search string - isFocused && searchString && ( - <OutsideClickHandler onOutsideClick={this.hideSearchResults}> - <SearchResultsDropdown> - {searchResults.length > 0 && - searchResults.map(community => { - return ( - <SearchResult - focused={focusedSearchResult === community.id} - key={community.id} - > - <SearchResultImage community={community} /> - - <SearchResultMetaWrapper> - <SearchResultName>{community.name}</SearchResultName> - {community.metaData && ( - <SearchResultMetadata> - {community.metaData.members} members - </SearchResultMetadata> - )} - <SearchResultDescription> - {community.description} - </SearchResultDescription> - </SearchResultMetaWrapper> - - <div> - {community.communityPermissions.isMember ? ( - <ToggleCommunityMembership - onJoin={this.onJoinComplete} - onLeave={this.onJoinComplete} - community={community} - render={({ isLoading }) => ( - <OutlineButton - gradientTheme="none" - color={'success.alt'} - hoverColor={'success.default'} - loading={isLoading} - > - Joined! - </OutlineButton> - )} - /> - ) : ( - <ToggleCommunityMembership - onJoin={this.onJoinComplete} - onLeave={this.onJoinComplete} - community={community} - render={({ isLoading }) => ( - <Button - loading={isLoading} - gradientTheme={'success'} - style={{ fontSize: '16px' }} - icon={'plus'} - > - Join - </Button> - )} - /> - )} - </div> - </SearchResult> - ); - })} - - {searchResults.length === 0 && !searchIsLoading && isFocused && ( - <SearchResult> - <SearchResultNull> - <p>No communities found matching “{searchString}”</p> - <Link to={'/new/community'}> - <Button>Create a Community</Button> - </Link> - </SearchResultNull> - </SearchResult> - )} - - {searchIsLoading && isFocused && ( - <SearchResult> - <SearchResultNull> - <p>Searching for “{searchString}”</p> - </SearchResultNull> - </SearchResult> - )} - </SearchResultsDropdown> - </OutsideClickHandler> - )} - </SearchWrapper> - ); - } -} - -export default compose( - withApollo, - withRouter, - connect() -)(Search); diff --git a/src/views/newUserOnboarding/components/communitySearch/style.js b/src/views/newUserOnboarding/components/communitySearch/style.js deleted file mode 100644 index b2a11142f9..0000000000 --- a/src/views/newUserOnboarding/components/communitySearch/style.js +++ /dev/null @@ -1,172 +0,0 @@ -// @flow -import theme from 'shared/theme'; -// $FlowFixMe -import styled from 'styled-components'; -import { - FlexCol, - FlexRow, - Shadow, - hexa, - zIndex, -} from '../../../../components/globals'; -import { CommunityAvatar } from '../../../../components/avatar'; -import Icon from '../../../../components/icons'; - -export const SearchWrapper = styled.div` - position: relative; - margin: 32px 16px; - padding: 12px 16px; - border: 2px solid ${theme.bg.border}; - border-radius: 12px; - width: 100%; - max-width: 640px; - - @media (max-width: 768px) { - margin: 32px 32px 16px; - } -`; - -export const SearchInputWrapper = styled(FlexRow)` - flex: auto; - color: ${theme.text.placeholder}; -`; - -export const SearchIcon = styled(Icon)``; - -export const SearchInput = styled.input` - font-size: 18px; - font-weight: 500; - padding: 4px 20px; - flex: auto; - position: relative; - z-index: ${zIndex.search}; - - @media (max-width: 768px) { - font-size: 16px; - } -`; - -export const SearchSpinnerContainer = styled.span` - position: absolute; - top: 12px; - right: 12px; - width: 32px; - height: 32px; - z-index: ${zIndex.search + 1}; -`; - -export const SearchResultsDropdown = styled.ul` - border-radius: 8px; - overflow: hidden; - box-shadow: ${Shadow.mid} ${props => hexa(props.theme.bg.reverse, 0.1)}; - position: absolute; - top: 64px; - left: 0; - display: inline-block; - width: 100%; - flex: auto; - max-height: 400px; - overflow-y: scroll; - z-index: ${zIndex.search}; - background: ${theme.bg.default}; - - @media (max-width: 768px) { - border-radius: 0 0 8px 8px; - } -`; - -export const SearchResult = styled.li` - display: flex; - flex: auto; - background: ${props => - props.focused ? props.theme.bg.wash : props.theme.bg.default}; - border-bottom: 2px solid ${theme.bg.border}; - align-items: center; - padding: 8px 16px 8px 8px; - - &:hover { - background: ${theme.bg.wash}; - cursor: pointer; - } - - h2 { - color: ${theme.text.default}; - } - - p { - color: ${theme.text.alt}; - line-height: 1.4; - } - - &:only-child { - border-bottom: none; - } - - &:last-child { - border-bottom: none; - } -`; - -export const SearchResultImage = styled(CommunityAvatar)` - margin: 4px 16px 0 4px; - width: 48px; - height: 48px; - border-radius: 8px; - align-self: flex-start; -`; - -export const SearchResultMetaWrapper = styled(FlexCol)` - margin-left: 4px; - flex: auto; - padding-right: 16px; -`; - -export const SearchResultName = styled.h2` - font-size: 16px; - font-weight: 700; - line-height: 1.4; -`; - -export const SearchResultMetadata = styled.p` - font-size: 14px; - font-weight: 400; - line-height: 1.4; -`; - -export const SearchResultNull = styled.div` - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - flex: auto; - padding: 24px; - background-color: ${theme.bg.default}; - border: 0; - - &:hover { - border: 0; - - p { - color: ${theme.text.alt}; - } - } - - a { - margin-top: 16px; - } - - p { - text-align: center; - font-size: 14px; - font-weight: 400; - color: ${theme.text.alt}; - text-align: center; - font-size: 18px; - font-weight: 600; - } -`; - -export const SearchResultDescription = styled.p` - font-size: 14px; - color: ${theme.text.alt}; -`; diff --git a/src/views/newUserOnboarding/components/discoverCommunities/index.js b/src/views/newUserOnboarding/components/discoverCommunities/index.js deleted file mode 100644 index a5fb1ca67c..0000000000 --- a/src/views/newUserOnboarding/components/discoverCommunities/index.js +++ /dev/null @@ -1,65 +0,0 @@ -// @flow -import * as React from 'react'; -import compose from 'recompose/compose'; -import { connect } from 'react-redux'; -import { getCommunitiesByCuratedContentType } from 'shared/graphql/queries/community/getCommunities'; -import type { GetCommunitiesType } from 'shared/graphql/queries/community/getCommunities'; -import { displayLoadingState } from '../../../../components/loading'; -import { Row } from './style'; -import { CommunityProfile } from '../../../../components/profile'; -import { track, events } from 'src/helpers/analytics'; - -type Props = { - data: { - communities: GetCommunitiesType, - error: ?string, - }, - hasJoined: number, - joinedCommunity: Function, -}; - -class TopCommunitiesPure extends React.Component<Props> { - componentDidMount() { - track(events.USER_ONBOARDING_JOIN_COMMUNITIES_STEP_VIEWED); - } - - render() { - const { - data: { communities, error }, - hasJoined, - joinedCommunity, - } = this.props; - // don't display communities where the user is blocked - const filteredCommunities = communities.filter( - community => community && !community.communityPermissions.isBlocked - ); - - if (!error && filteredCommunities.length > 0) { - return ( - <Row hasJoined={hasJoined > 0}> - {filteredCommunities.map(community => { - if (!community) return null; - return ( - <CommunityProfile - profileSize={'upsell'} - data={{ community }} - key={community.id} - joinedCommunity={joinedCommunity} - showHoverProfile={false} - /> - ); - })} - </Row> - ); - } - - return null; - } -} - -const TopCommunities = compose( - getCommunitiesByCuratedContentType, - displayLoadingState, - connect() -)(TopCommunitiesPure); -export default TopCommunities; diff --git a/src/views/newUserOnboarding/components/discoverCommunities/style.js b/src/views/newUserOnboarding/components/discoverCommunities/style.js deleted file mode 100644 index f0c78666a1..0000000000 --- a/src/views/newUserOnboarding/components/discoverCommunities/style.js +++ /dev/null @@ -1,109 +0,0 @@ -// @flow -import theme from 'shared/theme'; -// $FlowFixMe -import styled from 'styled-components'; -import { - Gradient, - Truncate, - Transition, - zIndex, -} from '../../../../components/globals'; - -export const Row = styled.div` - width: 100%; - flex: auto; - flex-wrap: wrap; - display: flex; - justify-content: center; - padding: 8px 16px 0 16px; - - @media (max-width: 768px) { - padding: 8px 0 0; - } -`; - -export const CoverPhoto = styled.div` - position: relative; - width: 100%; - height: ${props => (props.large ? '320px' : '96px')}; - background-color: ${theme.brand.default}; - background-image: url('${props => props.url}'); - background-size: cover; - background-repeat: no-repeat; - background-position: center; - border-radius: ${props => (props.large ? '12px' : '12px 12px 0 0')}; -`; - -export const Container = styled.div` - background: #fff; - box-shadow: inset 0 0 0 2px ${theme.bg.border}; - flex: 0 0 22%; - display: flex; - flex-direction: column; - border-radius: 12px; - position: relative; - z-index: ${zIndex.card}; - margin: 16px; - - @media (max-width: 1168px) { - flex-basis: 44%; - } - - @media (max-width: 540px) { - flex-basis: 100%; - } -`; - -export const ProfileAvatar = styled.img` - height: 40px; - width: 40px; - flex: 0 0 40px; - margin-right: 16px; - border-radius: 8px; - object-fit: cover; - background-color: ${theme.generic.default}; - background-image: ${({ theme }) => - Gradient(theme.generic.alt, theme.generic.default)}; -`; - -export const CoverAvatar = styled(ProfileAvatar)` - border: 2px solid ${theme.text.reverse}; - width: 64px; - flex: 0 0 64px; - margin-right: 0; - border-radius: 8px; -`; - -export const Title = styled.h3` - font-size: 16px; - color: ${theme.text.default}; - font-weight: 700; - line-height: 1.2; - ${Truncate} transition: ${Transition.hover.off}; -`; - -export const CoverTitle = styled(Title)` - font-size: 16px; - margin-top: 8px; - max-width: 100%; -`; - -export const CoverDescription = styled.p` - margin: 0 16px 8px 16px; - text-align: center; - font-size: 16px; - color: ${theme.text.alt}; - display: flex; - align-self: stretch; - flex-direction: column; - flex: 1; - line-height: 1.3; -`; - -export const ButtonContainer = styled.div` - padding: 8px 16px 16px; - - button { - width: 100%; - } -`; diff --git a/src/views/newUserOnboarding/components/joinFirstCommunity/index.js b/src/views/newUserOnboarding/components/joinFirstCommunity/index.js deleted file mode 100644 index e4d409812a..0000000000 --- a/src/views/newUserOnboarding/components/joinFirstCommunity/index.js +++ /dev/null @@ -1,45 +0,0 @@ -// @flow -import * as React from 'react'; -import compose from 'recompose/compose'; -import { connect } from 'react-redux'; -import { Row } from '../discoverCommunities/style'; -import { CommunityProfile } from 'src/components/profile'; -import type { Dispatch } from 'redux'; -import { withCurrentUser } from 'src/components/withCurrentUser'; - -type Props = { - toggleCommunityMembership: Function, - dispatch: Dispatch<Object>, - joinedFirstCommunity: Function, - joinedCommunity: Function, - community: { - id: string, - slug: string, - coverPhoto: string, - profilePhoto: string, - name: string, - description: string, - }, -}; - -class JoinFirstCommunityPure extends React.Component<Props> { - render() { - const { community, joinedCommunity } = this.props; - - return ( - <Row style={{ alignItems: 'flex-start' }}> - <CommunityProfile - profileSize={'upsell'} - data={{ community }} - onJoin={() => joinedCommunity(1, true)} - showHoverProfile={false} - /> - </Row> - ); - } -} - -export default compose( - withCurrentUser, - connect() -)(JoinFirstCommunityPure); diff --git a/src/views/newUserOnboarding/components/joinFirstCommunity/style.js b/src/views/newUserOnboarding/components/joinFirstCommunity/style.js deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/src/views/newUserOnboarding/components/setUsername/index.js b/src/views/newUserOnboarding/components/setUsername/index.js index 252a1be5a0..535dfe8c43 100644 --- a/src/views/newUserOnboarding/components/setUsername/index.js +++ b/src/views/newUserOnboarding/components/setUsername/index.js @@ -4,10 +4,10 @@ import slugg from 'slugg'; import { connect } from 'react-redux'; import { withApollo } from 'react-apollo'; import compose from 'recompose/compose'; -import { Error, Success } from '../../../../components/formElements'; -import UsernameSearch from '../../../../components/usernameSearch'; -import { addToastWithTimeout } from '../../../../actions/toasts'; -import { Form, Row, InputLabel, InputSubLabel } from './style'; +import { Error, Success } from 'src/components/formElements'; +import UsernameSearch from 'src/components/usernameSearch'; +import { addToastWithTimeout } from 'src/actions/toasts'; +import { Form, Row } from './style'; import editUserMutation from 'shared/graphql/mutations/user/editUser'; import { ContinueButton } from '../../style'; import type { Dispatch } from 'redux'; @@ -41,8 +41,8 @@ class SetUsername extends React.Component<Props, State> { ? user.name ? slugg(user.name) : user.firstName && user.lastName - ? `${user.firstName}-${user.lastName}` - : '' + ? `${user.firstName}-${user.lastName}` + : '' : ''; this.state = { @@ -111,21 +111,21 @@ class SetUsername extends React.Component<Props, State> { return ( <Form onSubmit={this.saveUsername}> - <InputLabel>Create your username</InputLabel> - <InputSubLabel>You can change this later - no pressure!</InputSubLabel> - <Row> <UsernameSearch - placeholder={'Set a username...'} + placeholder={'Your username...'} autoFocus={true} username={username} onValidationResult={this.handleUsernameValidation} + dataCy={'username-search'} /> </Row> - <Row> - <Error>{error ? error : <span> </span>}</Error> - <Success>{success ? success : <span> </span>}</Success> + <Row style={{ minHeight: '43px' }}> + {error && <Error data-cy="username-search-error">{error}</Error>} + {success && ( + <Success data-cy="username-search-success">{success}</Success> + )} </Row> <Row> @@ -133,8 +133,9 @@ class SetUsername extends React.Component<Props, State> { onClick={this.saveUsername} disabled={!username || error} loading={isLoading} + data-cy="save-username-button" > - Save and Continue + {isLoading ? 'Saving...' : 'Save and Continue'} </ContinueButton> </Row> </Form> diff --git a/src/views/newUserOnboarding/components/setUsername/style.js b/src/views/newUserOnboarding/components/setUsername/style.js index 78b0235700..b0eedbedb4 100644 --- a/src/views/newUserOnboarding/components/setUsername/style.js +++ b/src/views/newUserOnboarding/components/setUsername/style.js @@ -1,5 +1,4 @@ // $FlowFixMe -import theme from 'shared/theme'; import styled from 'styled-components'; export const Row = styled.div` @@ -8,33 +7,10 @@ export const Row = styled.div` position: relative; `; -export const Action = styled.div` - margin-top: 14px; - margin-left: 8px; -`; - export const Form = styled.form` display: flex; position: relative; flex-direction: column; width: 100%; - max-width: 400px; margin-top: 32px; `; - -export const InputLabel = styled.h3` - text-align: center; - font-size: 20px; - font-weight: 600; - color: ${theme.text.default}; - margin-bottom: 8px; -`; - -export const InputSubLabel = styled.h4` - text-align: center; - font-size: 16px; - font-weight: 600; - color: ${theme.text.alt}; - margin-bottom: 16px; - line-height: 1.4; -`; diff --git a/src/views/newUserOnboarding/index.js b/src/views/newUserOnboarding/index.js index 6438803cc2..efce9e9679 100644 --- a/src/views/newUserOnboarding/index.js +++ b/src/views/newUserOnboarding/index.js @@ -1,225 +1,82 @@ // @flow -import React, { Component } from 'react'; +import React from 'react'; import { connect } from 'react-redux'; +import type { Dispatch } from 'redux'; import compose from 'recompose/compose'; -import FullscreenView from '../../components/fullscreenView'; -import { UpsellCreateCommunity } from '../../components/upsell'; +import { withRouter, type History, type Location } from 'react-router-dom'; +import { withCurrentUser } from 'src/components/withCurrentUser'; import SetUsername from './components/setUsername'; -import JoinFirstCommunity from './components/joinFirstCommunity'; -import TopCommunities from './components/discoverCommunities'; -import Search from './components/communitySearch'; -import AppsUpsell from './components/appsUpsell'; -import { - OnboardingContainer, - OnboardingContent, - IconContainer, - Title, - Subtitle, - Emoji, - Container, - CreateUpsellContainer, - StickyRow, - ContinueButton, -} from './style'; import { track, events } from 'src/helpers/analytics'; import type { UserInfoType } from 'shared/graphql/fragments/user/userInfo'; -import type { CommunityInfoType } from 'shared/graphql/fragments/community/communityInfo'; -import { isDesktopApp } from 'src/helpers/desktop-app-utils'; import { SERVER_URL } from 'src/api/constants'; +import { setTitlebarProps } from 'src/actions/titlebar'; +import { ViewGrid, CenteredGrid } from 'src/components/layout'; +import { LogOutButton, Emoji, Heading, Description, Card } from './style'; -type StateProps = {| - community: CommunityInfoType, -|}; - -type Props = StateProps & {| +type Props = { currentUser: UserInfoType, -|}; - -type ActiveStep = - | 'discoverCommunities' - | 'setUsername' - | 'joinFirstCommunity' - | 'appsUpsell'; - -type State = {| - activeStep: ActiveStep, - joinedCommunities: number, - appUpsellCopy: { - title: string, - subtitle: string, - }, -|}; - -class NewUserOnboarding extends Component<Props, State> { - _isMounted = false; - - constructor(props) { - super(props); - - const { currentUser } = this.props; - - this.state = { - // if the user has a username already, we know that the onboarding - // was triggered because the user has not joined any communities yet - activeStep: - currentUser && currentUser.username - ? 'discoverCommunities' - : 'setUsername', - // we make sure to only let the user continue to their dashboard - // if they have joined one or more communities - because it's possible - // to join and then leave a community in this onboarding component, - // we keep track of the total joined count with a number, rathern than - // a boolean - joinedCommunities: 0, - appUpsellCopy: { - title: 'Download the app', - subtitle: 'A better way to keep up with your communities.', - }, - }; - } + history: History, + location: Location, + dispatch: Dispatch<Object>, +}; +class NewUserOnboarding extends React.Component<Props> { componentDidMount() { - this._isMounted = true; - } - - componentWillUnmount() { - this._isMounted = false; + const { dispatch } = this.props; + dispatch( + setTitlebarProps({ + title: 'Create username', + }) + ); } saveUsername = () => { + const { history, location } = this.props; + const { state } = location; track(events.USER_ONBOARDING_SET_USERNAME); - if (!isDesktopApp()) return this.toStep('appsUpsell'); - - return this.toStep('joinFirstCommunity'); - }; - - toStep = (step: ActiveStep) => { - if (!this._isMounted) return; - return this.setState({ - activeStep: step, - }); - }; - - joinedCommunity = (number: number, done: boolean) => { - if (number > 0) { - track(events.USER_ONBOARDING_JOINED_COMMUNITY); - } else { - track(events.USER_ONBOARDING_LEFT_COMMUNITY); - } - - const { joinedCommunities } = this.state; - // number will be either '1' or '-1' - so it will either increment - // or decrement the joinedCommunities count in state - let newCount = joinedCommunities + number; - this.setState({ joinedCommunities: newCount }); - }; - - appUpsellComplete = () => { - // if the user signed up via a community, channel, or thread view and - // has not yet joined that community, move them to that step in the onboarding - return this.toStep('joinFirstCommunity'); - }; - - onAppDownload = () => { - return this.setState({ - appUpsellCopy: { - title: 'Your download is starting!', - subtitle: 'Continue in the app, or keep going here.', - }, - }); + if (state && state.redirect) return history.replace(state.redirect); + return history.replace('/'); }; render() { - const { community, currentUser } = this.props; - const { activeStep, joinedCommunities, appUpsellCopy } = this.state; + const { currentUser } = this.props; - const steps = { - setUsername: { - title: 'Welcome to Spectrum!', - subtitle: - 'Spectrum is a place where communities can share, discuss, and grow together. To get started, create a username.', - emoji: '👋', - }, - joinFirstCommunity: { - // will be triggered if the user signed up via a community, channel, or thread view - title: 'Join your first community', - subtitle: - "You were in the middle of something. Let's get back on track and join your first community!", - emoji: '🎉', - }, - discoverCommunities: { - title: 'Find your people.', - subtitle: - 'There are hundreds of communities on Spectrum to explore. Check out some of our favorites below or search for topics.', - emoji: null, - }, - appsUpsell: { - title: appUpsellCopy.title, - subtitle: appUpsellCopy.subtitle, - emoji: null, - }, - }; + if (currentUser && currentUser.username) { + this.saveUsername(); + return null; + } + const heading = 'Create a username'; + const subheading = 'You can change this at any time, so no pressure!'; + const emoji = '👋'; return ( - <FullscreenView closePath={`${SERVER_URL}/auth/logout`}> - <OnboardingContainer> - <OnboardingContent> - <IconContainer> - {steps[activeStep].emoji && ( - <Emoji>{steps[activeStep].emoji}</Emoji> - )} - </IconContainer> - <Title>{steps[activeStep].title} - {steps[activeStep].subtitle} - - {activeStep === 'setUsername' && ( - - )} - - {activeStep === 'joinFirstCommunity' && ( - - )} - - {activeStep === 'discoverCommunities' && ( - - - 0} - curatedContentType={'top-communities-by-members'} - /> - 0}> - - - - 0}> - (window.location.href = '/')} - > - Continue to my home feed - - - - )} - - {activeStep === 'appsUpsell' && ( - - )} - - - + + + + + {emoji} + + {heading} + {subheading} + + + + + Log out + + + + ); } } -const map = (state): StateProps => ({ - community: state.newUserOnboarding.community, -}); -export default compose(connect(map))(NewUserOnboarding); +export default compose( + withRouter, + withCurrentUser, + connect() +)(NewUserOnboarding); diff --git a/src/views/newUserOnboarding/style.js b/src/views/newUserOnboarding/style.js index a49aef38d7..d14dfd60b8 100644 --- a/src/views/newUserOnboarding/style.js +++ b/src/views/newUserOnboarding/style.js @@ -1,121 +1,54 @@ // @flow -import theme from 'shared/theme'; -// $FlowFixMe import styled from 'styled-components'; -import { Button } from '../../components/buttons'; -import { Shadow, hexa, zIndex } from '../../components/globals'; +import theme from 'shared/theme'; +import { PrimaryButton, TextButton } from 'src/components/button'; +import { MEDIA_BREAK } from 'src/components/layout'; -export const OnboardingContainer = styled.div` - display: flex; - flex-direction: column; - width: 100%; +export const ContinueButton = styled(PrimaryButton)` + font-size: 17px; + font-weight: 600; + padding: 12px 16px; flex: 1; - overflow-y: scroll; - padding-top: 32px; -`; - -export const Container = styled.div` - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - flex-wrap: wrap; - width: 100%; -`; - -export const OnboardingContent = styled.div` - display: flex; - flex-direction: column; - flex: 1 0 auto; - padding: 32px; - justify-content: center; - align-items: center; - overflow-x: hidden; - - @media (max-width: 768px) { - padding: 32px 16px; - } + max-width: 100%; + margin: 16px auto 64px; `; -export const IconContainer = styled.div` - display: flex; - align-items: center; - justify-content: center; - margin-bottom: 16px; +export const LogOutButton = styled(TextButton)` + flex: 1; color: ${theme.text.alt}; `; - -export const Title = styled.h1` - color: ${theme.text.default}; - width: 100%; - line-height: 1.2; - padding: 0; - text-align: center; - letter-spacing: 0.2px; +export const Emoji = styled.span` font-size: 40px; - font-weight: 900; - letter-spacing: 0.3px; - margin-bottom: 16px; - - @media (max-width: 768px) { - font-size: 32px; - } -`; -export const Subtitle = styled.h2` - width: 100%; - max-width: 640px; - color: ${theme.text.alt}; - font-weight: 500; - font-size: 20px; - line-height: 1.4; - margin-bottom: 16px; - padding: 0 32px; - text-align: center; -`; - -export const Emoji = styled.h3` - font-size: 64px; margin-bottom: 16px; `; -export const ContinueButton = styled(Button)` - font-size: 18px; +export const Heading = styled.h3` + font-size: 24px; font-weight: 700; - color: ${theme.text.reverse}; - padding: 16px 88px; - max-width: 100%; - box-shadow: ${props => - `${Shadow.high} ${hexa(props.theme.bg.reverse, 0.15)}`}; - margin: 32px auto 0; + line-height: 1.3; + margin-bottom: 8px; + color: ${theme.text.default}; `; -export const CreateUpsellContainer = styled.div` - margin-top: 32px; - background: ${theme.bg.wash}; - padding: ${props => (props.extra ? '32px 32px 116px' : '32px')}; - border-top: 2px solid ${theme.bg.border}; - width: calc(100% + 64px); - margin-bottom: -32px; - margin-left: -32px; - margin-right: -32px; +export const Description = styled.p` + margin-top: 8px; + font-size: 16px; + font-weight: 400; + line-height: 1.4; + color: ${theme.text.secondary}; + padding-right: 24px; `; -export const StickyRow = styled.div` - width: 100%; - flex: 1 0 100%; - flex-wrap: wrap; - display: flex; - justify-content: center; +export const Card = styled.div` + background: ${theme.bg.wash}; padding: 16px; - position: fixed; - bottom: ${props => (props.hasJoined ? '0' : '-200px')}; - opacity: ${props => (props.hasJoined ? '1' : '0')}; - pointer-events: ${props => (props.hasJoined ? 'auto' : 'none')}; - left: 0; - right: 0; - background: ${theme.bg.default}; - border-top: 2px solid ${theme.bg.border}; - z-index: ${zIndex.fullscreen + 1}; - transition: bottom 0.3s ease-in-out, opacity 0.3s ease-in-out; - -webkit-transform: translate3d(0, 0, 0); + text-align: center; + max-width: 480px; + justify-self: center; + + @media (max-width: ${MEDIA_BREAK}px) { + border-radius: 0; + border: none; + border-bottom: 1px solid ${theme.bg.border}; + } `; diff --git a/src/views/notifications/components/browserNotificationRequest.js b/src/views/notifications/components/browserNotificationRequest.js deleted file mode 100644 index d02cee1c35..0000000000 --- a/src/views/notifications/components/browserNotificationRequest.js +++ /dev/null @@ -1,43 +0,0 @@ -import React from 'react'; -import { RequestCard, CloseRequest } from '../style'; -import { FlexRow } from '../../../components/globals'; -import { Button } from '../../../components/buttons'; - -const FirstRequest = ({ onSubscribe, onDismiss, loading }) => ( - -

- - 📬 - {' '} - Enable push notifications -

- - - - -
-); - -const BrowserNotificationRequest = ({ onSubscribe, onDismiss, loading }) => ( - -); - -export default BrowserNotificationRequest; diff --git a/src/views/notifications/components/communityInviteNotification.js b/src/views/notifications/components/communityInviteNotification.js index b3e67dda11..7e2c1f1639 100644 --- a/src/views/notifications/components/communityInviteNotification.js +++ b/src/views/notifications/components/communityInviteNotification.js @@ -3,31 +3,26 @@ import * as React from 'react'; import compose from 'recompose/compose'; import { getCommunityById } from 'shared/graphql/queries/community/getCommunity'; import type { GetCommunityType } from 'shared/graphql/queries/community/getCommunity'; -import { displayLoadingCard } from '../../../components/loading'; +import { displayLoadingCard } from 'src/components/loading'; import { parseNotificationDate, parseContext, parseActors } from '../utils'; -import markSingleNotificationSeenMutation from 'shared/graphql/mutations/notification/markSingleNotificationSeen'; -import Icon from '../../../components/icons'; +import Icon from 'src/components/icon'; import { SegmentedNotificationCard, TextContent, - SegmentedNotificationListRow, AttachmentsWash, CreatedContext, ContentWash, } from '../style'; -import { CommunityProfile } from '../../../components/profile'; -const CommunityInviteComponent = ({ - data, -}: { - data: { community: GetCommunityType }, -}) => { - return ; +const CommunityInviteComponent = () => { + // TODO @brian + return null; }; -const CommunityInvite = compose(getCommunityById, displayLoadingCard)( - CommunityInviteComponent -); +const CommunityInvite = compose( + getCommunityById, + displayLoadingCard +)(CommunityInviteComponent); export const CommunityInviteNotification = ({ notification, @@ -41,7 +36,7 @@ export const CommunityInviteNotification = ({ const actors = parseActors(notification.actors, currentUser, true); return ( - + @@ -57,57 +52,3 @@ export const CommunityInviteNotification = ({ ); }; - -type Props = { - notification: Object, - currentUser: Object, - markSingleNotificationSeen: Function, - markSingleNotificationAsSeenInState: Function, -}; - -class MiniCommunityInviteNotificationWithMutation extends React.Component< - Props -> { - markAsSeen = () => { - const { - markSingleNotificationSeen, - notification, - markSingleNotificationAsSeenInState, - } = this.props; - if (notification.isSeen) return; - markSingleNotificationAsSeenInState(notification.id); - markSingleNotificationSeen(notification.id); - }; - - render() { - const { notification, currentUser } = this.props; - - const date = parseNotificationDate(notification.modifiedAt); - const context = parseContext(notification.context); - const actors = parseActors(notification.actors, currentUser, true); - - return ( - - - - - {actors.asObjects[0].name} invited you to join their community,{' '} - {context.asString} {date} - - - - - - - - - ); - } -} - -export const MiniCommunityInviteNotification = compose( - markSingleNotificationSeenMutation -)(MiniCommunityInviteNotificationWithMutation); diff --git a/src/views/notifications/components/mentionMessageNotification.js b/src/views/notifications/components/mentionMessageNotification.js index 84ebcc41cd..136097166b 100644 --- a/src/views/notifications/components/mentionMessageNotification.js +++ b/src/views/notifications/components/mentionMessageNotification.js @@ -2,18 +2,14 @@ import * as React from 'react'; import { ActorsRow } from './actorsRow'; import { parseNotificationDate, parseContext, parseActors } from '../utils'; -import Icon from '../../../components/icons'; +import Icon from 'src/components/icon'; import { TextContent, Content, NotificationCard, - NotificationListRow, SpecialContext, } from '../style'; -import { - CardLink, - CardContent, -} from '../../../components/threadFeedCard/style'; +import { CardLink } from 'src/components/threadFeedCard/style'; import getThreadLink from 'src/helpers/get-thread-link'; type Props = { @@ -40,11 +36,10 @@ export class MentionMessageNotification extends React.Component { const context = parseContext(notification.context, currentUser); return ( - + @@ -61,38 +56,3 @@ export class MentionMessageNotification extends React.Component { ); } } - -export class MiniMentionMessageNotification extends React.Component< - Props, - State -> { - render() { - const { notification, currentUser } = this.props; - - const actors = parseActors(notification.actors, currentUser, false); - const date = parseNotificationDate(notification.modifiedAt); - const context = parseContext(notification.context, currentUser); - - return ( - - - - - - - - - - {actors.asString} mentioned you in {context.asString} {date} - - - - - ); - } -} diff --git a/src/views/notifications/components/mentionThreadNotification.js b/src/views/notifications/components/mentionThreadNotification.js index 537bdbe782..1f58222321 100644 --- a/src/views/notifications/components/mentionThreadNotification.js +++ b/src/views/notifications/components/mentionThreadNotification.js @@ -1,28 +1,20 @@ // @flow import * as React from 'react'; import compose from 'recompose/compose'; -import { ActorsRow } from './actorsRow'; import { getThreadById } from 'shared/graphql/queries/thread/getThread'; import type { GetThreadType } from 'shared/graphql/queries/thread/getThread'; -import { displayLoadingCard } from '../../../components/loading'; +import { displayLoadingCard } from 'src/components/loading'; import { parseNotificationDate, parseContext, parseActors } from '../utils'; -import Icon from '../../../components/icons'; -import { ThreadProfile } from '../../../components/profile'; +import Icon from 'src/components/icon'; +import { ThreadProfile } from 'src/components/profile'; import { SegmentedNotificationCard, TextContent, AttachmentsWash, ContentWash, - NotificationListRow, SpecialContext, ThreadContext, - Content, } from '../style'; -import { - CardLink, - CardContent, -} from '../../../components/threadFeedCard/style'; -import getThreadLink from 'src/helpers/get-thread-link'; type Props = { notification: Object, @@ -71,7 +63,7 @@ export class MentionThreadNotification extends React.Component { const context = parseContext(notification.context, currentUser); return ( - + @@ -90,46 +82,3 @@ export class MentionThreadNotification extends React.Component { ); } } - -export class MiniMentionThreadNotification extends React.Component< - Props, - State -> { - constructor() { - super(); - - this.state = { - communityName: '', - }; - } - - render() { - const { notification, currentUser } = this.props; - - const actors = parseActors(notification.actors, currentUser, false); - const date = parseNotificationDate(notification.modifiedAt); - const context = parseContext(notification.context, currentUser); - - return ( - - - - - - - - - - {actors.asString} mentioned you in {context.asString} {date} - - - - - ); - } -} diff --git a/src/views/notifications/components/newChannelNotification.js b/src/views/notifications/components/newChannelNotification.js index d15927b3d6..f8f36021fd 100644 --- a/src/views/notifications/components/newChannelNotification.js +++ b/src/views/notifications/components/newChannelNotification.js @@ -2,14 +2,13 @@ import * as React from 'react'; import compose from 'recompose/compose'; import { getChannelById } from 'shared/graphql/queries/channel/getChannel'; -import { displayLoadingCard } from '../../../components/loading'; +import { displayLoadingCard } from 'src/components/loading'; import { parseNotificationDate, parseContext } from '../utils'; -import Icon from '../../../components/icons'; +import Icon from 'src/components/icon'; import { Link } from 'react-router-dom'; import { SegmentedNotificationCard, TextContent, - SegmentedNotificationListRow, AttachmentsWash, CreatedContext, ContentWash, @@ -17,7 +16,6 @@ import { ChannelName, ToggleNotificationsContainer, } from '../style'; -import markSingleNotificationSeenMutation from 'shared/graphql/mutations/notification/markSingleNotificationSeen'; import type { GetChannelType } from 'shared/graphql/queries/channel/getChannel'; import ToggleChannelNotifications from 'src/components/toggleChannelNotifications'; import { Loading } from 'src/components/loading'; @@ -38,14 +36,7 @@ const NewChannelComponent = ({ ( - + {state.isLoading ? ( ) : ( @@ -89,7 +80,7 @@ export class NewChannelNotification extends React.Component { : 'A new channel was'; return ( - + @@ -109,55 +100,3 @@ export class NewChannelNotification extends React.Component { ); } } - -class MiniNewChannelNotificationWithMutation extends React.Component { - markAsSeen = () => { - const { - markSingleNotificationSeen, - notification, - markSingleNotificationAsSeenInState, - } = this.props; - if (notification.isSeen) return; - markSingleNotificationAsSeenInState && - markSingleNotificationAsSeenInState(notification.id); - markSingleNotificationSeen && markSingleNotificationSeen(notification.id); - }; - - render() { - const { notification } = this.props; - - const date = parseNotificationDate(notification.modifiedAt); - const context = parseContext(notification.context); - const newChannelCount = - notification.entities.length > 1 - ? `${notification.entities.length} new channels were` - : 'A new channel was'; - - return ( - - - - - {newChannelCount} created in {context.asString} {date} - - - - - {notification.entities.map(channel => { - return ( - - ); - })} - - - - ); - } -} - -export const MiniNewChannelNotification = compose( - markSingleNotificationSeenMutation -)(MiniNewChannelNotificationWithMutation); diff --git a/src/views/notifications/components/newMessageNotification.js b/src/views/notifications/components/newMessageNotification.js index e116b89107..bb81a08a4d 100644 --- a/src/views/notifications/components/newMessageNotification.js +++ b/src/views/notifications/components/newMessageNotification.js @@ -7,16 +7,12 @@ import { parseContext, } from '../utils'; import { ActorsRow } from './actorsRow'; -import { - CardLink, - CardContent, -} from '../../../components/threadFeedCard/style'; -import Icon from '../../../components/icons'; +import { CardLink, CardContent } from 'src/components/threadFeedCard/style'; +import Icon from 'src/components/icon'; import { sortAndGroupNotificationMessages } from './sortAndGroupNotificationMessages'; import { NotificationCard, TextContent, - NotificationListRow, SuccessContext, Content, } from '../style'; @@ -50,7 +46,6 @@ export const NewMessageNotification = ({ @@ -68,36 +63,3 @@ export const NewMessageNotification = ({ ); }; - -export const MiniNewMessageNotification = ({ - notification, - currentUser, - history, -}: Props) => { - const actors = parseActors(notification.actors, currentUser, true); - const event = parseEvent(notification.event); - const date = parseNotificationDate(notification.modifiedAt); - const context = parseContext(notification.context, currentUser); - - return ( - - - - - - - - - - {actors.asString} {event} {context.asString} {date} - - - - - ); -}; diff --git a/src/views/notifications/components/newReactionNotification.js b/src/views/notifications/components/newReactionNotification.js index 410ec31ed4..bbaa7fa668 100644 --- a/src/views/notifications/components/newReactionNotification.js +++ b/src/views/notifications/components/newReactionNotification.js @@ -10,11 +10,10 @@ import { ActorsRow } from './actorsRow'; import { NotificationCard, TextContent, - NotificationListRow, ReactionContext, Content, } from '../style'; -import Icon from 'src/components/icons'; +import Icon from 'src/components/icon'; import { CardLink, CardContent } from 'src/components/threadFeedCard/style'; type Props = { @@ -35,7 +34,7 @@ export const NewReactionNotification = ({ const context = parseContext(notification.context); return ( - + ); }; - -export const MiniNewReactionNotification = ({ - notification, - currentUser, - history, -}: Props) => { - const actors = parseActors(notification.actors, currentUser, true); - const event = parseEvent(notification.event); - const date = parseNotificationDate(notification.modifiedAt); - const context = parseContext(notification.context); - - return ( - - - - - - - - - - {' '} - {actors.asString} {event} {context.asString} {date}{' '} - - - - - ); -}; diff --git a/src/views/notifications/components/newThreadNotification.js b/src/views/notifications/components/newThreadNotification.js index 5d1a6c6373..b4e8b9c5f1 100644 --- a/src/views/notifications/components/newThreadNotification.js +++ b/src/views/notifications/components/newThreadNotification.js @@ -6,14 +6,12 @@ import type { GetThreadType } from 'shared/graphql/queries/thread/getThread'; import { sortByDate } from 'src/helpers/utils'; import viewNetworkHandler from 'src/components/viewNetworkHandler'; import { parseNotificationDate, parseContext } from '../utils'; -import markSingleNotificationSeenMutation from 'shared/graphql/mutations/notification/markSingleNotificationSeen'; -import Icon from 'src/components/icons'; +import Icon from 'src/components/icon'; import { ThreadProfile } from 'src/components/profile'; import { LoadingCard } from 'src/components/loading'; import { SegmentedNotificationCard, TextContent, - SegmentedNotificationListRow, AttachmentsWash, ThreadContext, ContentWash, @@ -100,7 +98,7 @@ export class NewThreadNotification extends React.Component { if (threads && threads.length > 0) { return ( - + @@ -129,87 +127,3 @@ export class NewThreadNotification extends React.Component { } } } - -class MiniNewThreadNotificationWithMutation extends React.Component< - Props, - State -> { - state = { - communityName: '', - deletedThreads: [], - }; - - markAsDeleted = (id: string) => { - const newArr = this.state.deletedThreads.concat(id); - setTimeout(() => { - this.setState({ deletedThreads: newArr }); - }, 0); - this.markAsSeen(); - }; - - markAsSeen = () => { - const { - markSingleNotificationSeen, - notification, - markSingleNotificationAsSeenInState, - } = this.props; - if (notification.isSeen) return; - markSingleNotificationAsSeenInState && - markSingleNotificationAsSeenInState(notification.id); - markSingleNotificationSeen && markSingleNotificationSeen(notification.id); - }; - - setCommunityName = (name: string) => this.setState({ communityName: name }); - - render() { - const { notification, currentUser } = this.props; - const { communityName, deletedThreads } = this.state; - - const date = parseNotificationDate(notification.modifiedAt); - const context = parseContext(notification.context); - - const threads = sortThreads(notification.entities, currentUser).filter( - thread => deletedThreads.indexOf(thread.id) < 0 - ); - - const newThreadCount = - threads && threads.length > 1 ? 'New threads were' : 'A new thread was'; - - if (threads && threads.length > 0) { - return ( - - - - - {newThreadCount} published in{' '} - {communityName && `${communityName}, `} {context.asString} {date} - - - - - {threads.map(thread => { - return ( - - ); - })} - - - - ); - } else { - return null; - } - } -} - -export const MiniNewThreadNotification = compose( - markSingleNotificationSeenMutation -)(MiniNewThreadNotificationWithMutation); diff --git a/src/views/notifications/components/newThreadReactionNotification.js b/src/views/notifications/components/newThreadReactionNotification.js index 138d85fc4a..c655a364c5 100644 --- a/src/views/notifications/components/newThreadReactionNotification.js +++ b/src/views/notifications/components/newThreadReactionNotification.js @@ -10,16 +10,11 @@ import { ActorsRow } from './actorsRow'; import { NotificationCard, TextContent, - NotificationListRow, ThreadReactionContext, Content, } from '../style'; -import Icon from '../../../components/icons'; -import { truncate } from '../../../helpers/utils'; -import { - CardLink, - CardContent, -} from '../../../components/threadFeedCard/style'; +import Icon from 'src/components/icon'; +import { CardLink, CardContent } from 'src/components/threadFeedCard/style'; import getThreadLink from 'src/helpers/get-thread-link'; type Props = { @@ -41,11 +36,10 @@ export const NewThreadReactionNotification = ({ ); return ( - + @@ -63,45 +57,3 @@ export const NewThreadReactionNotification = ({ ); }; - -export const MiniNewThreadReactionNotification = ({ - notification, - currentUser, - history, -}: Props) => { - const actors = parseActors(notification.actors, currentUser, true); - const event = parseEvent(notification.event); - const date = parseNotificationDate(notification.modifiedAt); - const context = parseContext( - { ...notification.context, type: 'THREAD_REACTION' }, - currentUser - ); - const isText = notification.context.payload.messageType === 'text'; - const messageStr = isText - ? truncate(notification.context.payload.content.body, 40) - : null; - - return ( - - - - - - - - - - {' '} - {actors.asString} {event} {context.asString}{' '} - {messageStr && `"${messageStr}"`} {date}{' '} - - - - - ); -}; diff --git a/src/views/notifications/components/newUserInCommunityNotification.js b/src/views/notifications/components/newUserInCommunityNotification.js index 6f87d63cf2..5de0f5b1d6 100644 --- a/src/views/notifications/components/newUserInCommunityNotification.js +++ b/src/views/notifications/components/newUserInCommunityNotification.js @@ -7,20 +7,9 @@ import { parseContext, } from '../utils'; import { ActorsRow } from './actorsRow'; -import { - NotificationCard, - TextContent, - NotificationListRow, - JoinContext, - Content, -} from '../style'; -import Icon from '../../../components/icons'; -import { - CardLink, - CardContent, -} from '../../../components/threadFeedCard/style'; -import compose from 'recompose/compose'; -import markSingleNotificationSeenMutation from 'shared/graphql/mutations/notification/markSingleNotificationSeen'; +import { NotificationCard, TextContent, JoinContext, Content } from '../style'; +import Icon from 'src/components/icon'; +import { CardLink, CardContent } from 'src/components/threadFeedCard/style'; type Props = { notification: Object, @@ -41,7 +30,7 @@ export class NewUserInCommunityNotification extends React.Component { return null; return ( - + @@ -59,56 +48,3 @@ export class NewUserInCommunityNotification extends React.Component { ); } } - -class MiniNewUserInCommunityNotificationWithMutation extends React.Component< - Props -> { - markAsSeen = () => { - const { - markSingleNotificationSeen, - notification, - markSingleNotificationAsSeenInState, - } = this.props; - if (notification.isSeen) return; - markSingleNotificationAsSeenInState && - markSingleNotificationAsSeenInState(notification.id); - markSingleNotificationSeen && markSingleNotificationSeen(notification.id); - }; - - render() { - const { notification, currentUser } = this.props; - - const actors = parseActors(notification.actors, currentUser, true); - const event = parseEvent(notification.event); - const date = parseNotificationDate(notification.modifiedAt); - const context = parseContext(notification.context); - - if (!actors.asString || !actors.asObjects || actors.asObjects.length === 0) - return null; - - return ( - - - - - - - - - - - {' '} - {actors.asString} {event} {context.asString} {date}{' '} - - - - ); - } -} - -export const MiniNewUserInCommunityNotification = compose( - markSingleNotificationSeenMutation -)(MiniNewUserInCommunityNotificationWithMutation); diff --git a/src/views/notifications/components/notificationDropdownList.js b/src/views/notifications/components/notificationDropdownList.js deleted file mode 100644 index 225bddeb34..0000000000 --- a/src/views/notifications/components/notificationDropdownList.js +++ /dev/null @@ -1,213 +0,0 @@ -// @flow -import * as React from 'react'; -import { ErrorBoundary } from 'src/components/error'; -import { NotificationListContainer } from '../style'; -import { parseNotification } from '../utils'; -import { sortByDate } from '../../../helpers/utils'; -import { MiniNewMessageNotification } from './newMessageNotification'; -import { MiniNewReactionNotification } from './newReactionNotification'; -import { MiniNewThreadReactionNotification } from './newThreadReactionNotification'; -import { MiniNewChannelNotification } from './newChannelNotification'; -import { MiniNewUserInCommunityNotification } from './newUserInCommunityNotification'; -import { MiniCommunityInviteNotification } from './communityInviteNotification'; -import { MiniMentionMessageNotification } from './mentionMessageNotification'; -import { MiniMentionThreadNotification } from './mentionThreadNotification'; -import { MiniPrivateChannelRequestSent } from './privateChannelRequestSentNotification'; -import { MiniPrivateChannelRequestApproved } from './privateChannelRequestApprovedNotification'; -import { MiniPrivateCommunityRequestSent } from './privateCommunityRequestSentNotification'; -import { MiniPrivateCommunityRequestApproved } from './privateCommunityRequestApprovedNotification'; - -type Props = { - rawNotifications: Array, - currentUser: Object, - history: Object, - markSingleNotificationAsSeenInState: Function, -}; -export class NotificationDropdownList extends React.Component { - render() { - const { - rawNotifications, - currentUser, - history, - markSingleNotificationAsSeenInState, - } = this.props; - - let notifications = rawNotifications - .map(notification => parseNotification(notification)) - .slice(0, 10) - .filter( - notification => notification.context.type !== 'DIRECT_MESSAGE_THREAD' - ); - - notifications = sortByDate(notifications, 'modifiedAt', 'desc'); - - return ( - - {notifications.map(notification => { - switch (notification.event) { - case 'MESSAGE_CREATED': { - return ( - - - - ); - } - case 'REACTION_CREATED': { - return ( - - - - ); - } - case 'THREAD_REACTION_CREATED': { - return ( - - - - ); - } - case 'CHANNEL_CREATED': { - return ( - - - - ); - } - case 'USER_JOINED_COMMUNITY': { - return ( - - - - ); - } - case 'THREAD_CREATED': { - // deprecated - we no longer show this notification type in-app - return null; - } - case 'COMMUNITY_INVITE': { - return ( - - - - ); - } - case 'MENTION_THREAD': { - return ( - - - - ); - } - case 'MENTION_MESSAGE': { - return ( - - - - ); - } - case 'PRIVATE_CHANNEL_REQUEST_SENT': { - return ( - - - - ); - } - case 'PRIVATE_CHANNEL_REQUEST_APPROVED': { - return ( - - - - ); - } - case 'PRIVATE_COMMUNITY_REQUEST_SENT': { - return ( - - - - ); - } - case 'PRIVATE_COMMUNITY_REQUEST_APPROVED': { - return ( - - - - ); - } - default: { - return null; - } - } - })} - - ); - } -} diff --git a/src/views/notifications/components/privateChannelRequestApprovedNotification.js b/src/views/notifications/components/privateChannelRequestApprovedNotification.js index b45694b77f..73d77bc6a2 100644 --- a/src/views/notifications/components/privateChannelRequestApprovedNotification.js +++ b/src/views/notifications/components/privateChannelRequestApprovedNotification.js @@ -11,17 +11,11 @@ import { ActorsRow } from './actorsRow'; import { NotificationCard, TextContent, - NotificationListRow, ApprovedContext, Content, } from '../style'; -import Icon from '../../../components/icons'; -import { - CardLink, - CardContent, -} from '../../../components/threadFeedCard/style'; -import compose from 'recompose/compose'; -import markSingleNotificationSeenMutation from 'shared/graphql/mutations/notification/markSingleNotificationSeen'; +import Icon from 'src/components/icon'; +import { CardLink, CardContent } from 'src/components/threadFeedCard/style'; type Props = { notification: Object, @@ -41,7 +35,7 @@ export class PrivateChannelRequestApproved extends React.Component { const channel = notification.entities[0].payload; return ( - + { ); } } - -class MiniPrivateChannelRequestApprovedWithMutation extends React.Component< - Props -> { - markAsSeen = () => { - const { - markSingleNotificationSeen, - notification, - markSingleNotificationAsSeenInState, - } = this.props; - if (notification.isSeen) return; - markSingleNotificationAsSeenInState && - markSingleNotificationAsSeenInState(notification.id); - markSingleNotificationSeen && markSingleNotificationSeen(notification.id); - }; - - render() { - const { notification, currentUser } = this.props; - - const actors = parseActors(notification.actors, currentUser, true); - const event = parseEvent(notification.event); - const date = parseNotificationDate(notification.modifiedAt); - const context = parseContext(notification.context); - const channel = notification.entities[0].payload; - - return ( - - - - - - - - - - - {' '} - {actors.asString} {event} the{' '} - {channel.name}{' '} - channel in {context.asString} {date}{' '} - - - - ); - } -} - -export const MiniPrivateChannelRequestApproved = compose( - markSingleNotificationSeenMutation -)(MiniPrivateChannelRequestApprovedWithMutation); diff --git a/src/views/notifications/components/privateChannelRequestSentNotification.js b/src/views/notifications/components/privateChannelRequestSentNotification.js index c3bfc417a3..f338f5945b 100644 --- a/src/views/notifications/components/privateChannelRequestSentNotification.js +++ b/src/views/notifications/components/privateChannelRequestSentNotification.js @@ -11,17 +11,11 @@ import { ActorsRow } from './actorsRow'; import { NotificationCard, TextContent, - NotificationListRow, RequestContext, Content, } from '../style'; -import Icon from '../../../components/icons'; -import { - CardLink, - CardContent, -} from '../../../components/threadFeedCard/style'; -import compose from 'recompose/compose'; -import markSingleNotificationSeenMutation from 'shared/graphql/mutations/notification/markSingleNotificationSeen'; +import Icon from 'src/components/icon'; +import { CardLink, CardContent } from 'src/components/threadFeedCard/style'; type Props = { notification: Object, @@ -41,7 +35,7 @@ export class PrivateChannelRequestSent extends React.Component { const channel = notification.entities[0].payload; return ( - + { ); } } - -class MiniPrivateChannelRequestSentWithMutation extends React.Component { - markAsSeen = () => { - const { - markSingleNotificationSeen, - notification, - markSingleNotificationAsSeenInState, - } = this.props; - if (notification.isSeen) return; - markSingleNotificationAsSeenInState && - markSingleNotificationAsSeenInState(notification.id); - markSingleNotificationSeen && markSingleNotificationSeen(notification.id); - }; - - render() { - const { notification, currentUser } = this.props; - - const actors = parseActors(notification.actors, currentUser, true); - const event = parseEvent(notification.event); - const date = parseNotificationDate(notification.modifiedAt); - const context = parseContext(notification.context); - const channel = notification.entities[0].payload; - - return ( - - - - - - - - - - - {' '} - {actors.asString} {event} the{' '} - {channel.name}{' '} - channel in {context.asString} {date}{' '} - - - - ); - } -} - -export const MiniPrivateChannelRequestSent = compose( - markSingleNotificationSeenMutation -)(MiniPrivateChannelRequestSentWithMutation); diff --git a/src/views/notifications/components/privateCommunityRequestApprovedNotification.js b/src/views/notifications/components/privateCommunityRequestApprovedNotification.js index 857d0c285d..f0228d91c8 100644 --- a/src/views/notifications/components/privateCommunityRequestApprovedNotification.js +++ b/src/views/notifications/components/privateCommunityRequestApprovedNotification.js @@ -6,17 +6,11 @@ import { ActorsRow } from './actorsRow'; import { NotificationCard, TextContent, - NotificationListRow, ApprovedContext, Content, } from '../style'; -import Icon from '../../../components/icons'; -import { - CardLink, - CardContent, -} from '../../../components/threadFeedCard/style'; -import compose from 'recompose/compose'; -import markSingleNotificationSeenMutation from 'shared/graphql/mutations/notification/markSingleNotificationSeen'; +import Icon from 'src/components/icon'; +import { CardLink, CardContent } from 'src/components/threadFeedCard/style'; type Props = { notification: Object, @@ -34,7 +28,7 @@ export class PrivateCommunityRequestApproved extends React.Component { const date = parseNotificationDate(notification.modifiedAt); return ( - + @@ -56,56 +50,3 @@ export class PrivateCommunityRequestApproved extends React.Component { ); } } - -class MiniPrivateCommunityRequestApprovedWithMutation extends React.Component< - Props -> { - markAsSeen = () => { - const { - markSingleNotificationSeen, - notification, - markSingleNotificationAsSeenInState, - } = this.props; - if (notification.isSeen) return; - markSingleNotificationAsSeenInState && - markSingleNotificationAsSeenInState(notification.id); - markSingleNotificationSeen && markSingleNotificationSeen(notification.id); - }; - - render() { - const { notification, currentUser } = this.props; - - const actors = parseActors(notification.actors, currentUser, true); - const event = parseEvent(notification.event); - const date = parseNotificationDate(notification.modifiedAt); - - return ( - - - - - - - - - - - {' '} - {actors.asString} {event} the{' '} - - {notification.context.payload.name} - {' '} - community {date}{' '} - - - - ); - } -} - -export const MiniPrivateCommunityRequestApproved = compose( - markSingleNotificationSeenMutation -)(MiniPrivateCommunityRequestApprovedWithMutation); diff --git a/src/views/notifications/components/privateCommunityRequestSentNotification.js b/src/views/notifications/components/privateCommunityRequestSentNotification.js index c23a21eca2..6c96b06784 100644 --- a/src/views/notifications/components/privateCommunityRequestSentNotification.js +++ b/src/views/notifications/components/privateCommunityRequestSentNotification.js @@ -5,21 +5,19 @@ import { parseActors, parseEvent, parseNotificationDate } from '../utils'; import { ActorsRow } from './actorsRow'; import approvePendingCommunityMember from 'shared/graphql/mutations/communityMember/approvePendingCommunityMember'; import blockPendingCommunityMember from 'shared/graphql/mutations/communityMember/blockPendingCommunityMember'; -import { Button, OutlineButton } from 'src/components/buttons'; +import { Button, OutlineButton } from 'src/components/button'; import { SegmentedNotificationCard, TextContent, - SegmentedNotificationListRow, RequestContext, Content, ContentWash, AttachmentsWash, ButtonsRow, } from '../style'; -import Icon from '../../../components/icons'; -import { CardContent } from '../../../components/threadFeedCard/style'; +import Icon from 'src/components/icon'; +import { CardContent } from 'src/components/threadFeedCard/style'; import compose from 'recompose/compose'; -import markSingleNotificationSeenMutation from 'shared/graphql/mutations/notification/markSingleNotificationSeen'; import MutationWrapper from 'src/views/communityMembers/components/mutationWrapper'; import GetCommunityMember from './getCommunityMember'; @@ -51,7 +49,7 @@ class PrivateCommunityRequestSentComponent extends React.Component { }; return ( - + { - markAsSeen = () => { - const { - markSingleNotificationSeen, - notification, - markSingleNotificationAsSeenInState, - } = this.props; - if (notification.isSeen) return; - markSingleNotificationAsSeenInState && - markSingleNotificationAsSeenInState(notification.id); - markSingleNotificationSeen && markSingleNotificationSeen(notification.id); - }; - - render() { - const { - notification, - currentUser, - approvePendingCommunityMember, - blockPendingCommunityMember, - } = this.props; - - const actors = parseActors(notification.actors, currentUser, true); - const event = parseEvent(notification.event); - const date = parseNotificationDate(notification.modifiedAt); - - const input = { - communityId: notification.context.id, - userId: notification.actors[0].id, - }; - - return ( - - - - - - - - - - - {' '} - {actors.asString} {event} the{' '} - - {notification.context.payload.name} - {' '} - community {date}{' '} - - - - { - if (!communityMember || !communityMember.isPending) return null; - return ( - - - - ( - - Block - - )} - /> - ( - - )} - /> - - - - ); - }} - /> - - ); - } -} - -export const MiniPrivateCommunityRequestSent = compose( - markSingleNotificationSeenMutation, - approvePendingCommunityMember, - blockPendingCommunityMember -)(MiniPrivateCommunityRequestSentWithMutation); diff --git a/src/views/notifications/components/sortAndGroupNotificationMessages.js b/src/views/notifications/components/sortAndGroupNotificationMessages.js index 19dc5614bf..d3aac81b9c 100644 --- a/src/views/notifications/components/sortAndGroupNotificationMessages.js +++ b/src/views/notifications/components/sortAndGroupNotificationMessages.js @@ -1,4 +1,4 @@ -import { sortByDate } from '../../../helpers/utils'; +import { sortByDate } from 'src/helpers/utils'; export const sortAndGroupNotificationMessages = messagesToSort => { if (!(messagesToSort.length > 0)) return []; diff --git a/src/views/notifications/index.js b/src/views/notifications/index.js index 2ab4d6988f..982bb36edf 100644 --- a/src/views/notifications/index.js +++ b/src/views/notifications/index.js @@ -2,9 +2,6 @@ import * as React from 'react'; import compose from 'recompose/compose'; import { connect } from 'react-redux'; -// NOTE(@mxstbr): This is a custom fork published of off this (as of this writing) unmerged PR: https://github.com/CassetteRocks/react-infinite-scroller/pull/38 -// I literally took it, renamed the package.json and published to add support for scrollElement since our scrollable container is further outside -import InfiniteList from 'src/components/infiniteScroll'; import { deduplicateChildren } from 'src/components/infiniteScroll/deduplicateChildren'; import { parseNotification } from './utils'; import { NewMessageNotification } from './components/newMessageNotification'; @@ -19,36 +16,31 @@ import { PrivateChannelRequestApproved } from './components/privateChannelReques import { PrivateChannelRequestSent } from './components/privateChannelRequestSentNotification'; import { PrivateCommunityRequestApproved } from './components/privateCommunityRequestApprovedNotification'; import { PrivateCommunityRequestSent } from './components/privateCommunityRequestSentNotification'; -import { Column } from '../../components/column'; -import AppViewWrapper from '../../components/appViewWrapper'; -import Head from '../../components/head'; -import Titlebar from '../../views/titlebar'; +import Head from 'src/components/head'; import { withCurrentUser } from 'src/components/withCurrentUser'; -import { - displayLoadingNotifications, - LoadingThread, - Loading, -} from '../../components/loading'; -import { FlexCol } from '../../components/globals'; -import { sortByDate } from '../../helpers/utils'; -import WebPushManager from '../../helpers/web-push-manager'; -import { addToastWithTimeout } from '../../actions/toasts'; +import { sortByDate } from 'src/helpers/utils'; +import WebPushManager from 'src/helpers/web-push-manager'; +import { addToastWithTimeout } from 'src/actions/toasts'; import getNotifications from 'shared/graphql/queries/notification/getNotifications'; import markNotificationsSeenMutation from 'shared/graphql/mutations/notification/markNotificationsSeen'; import { subscribeToWebPush } from 'shared/graphql/subscriptions'; -import { UpsellNullNotifications } from '../../components/upsell'; -import ViewError from '../../components/viewError'; -import BrowserNotificationRequest from './components/browserNotificationRequest'; import generateMetaInfo from 'shared/generate-meta-info'; +import { setTitlebarProps } from 'src/actions/titlebar'; import viewNetworkHandler, { type ViewNetworkHandlerType, -} from '../../components/viewNetworkHandler'; +} from 'src/components/viewNetworkHandler'; import { track, events } from 'src/helpers/analytics'; import type { Dispatch } from 'redux'; import { ErrorBoundary } from 'src/components/error'; import { isDesktopApp } from 'src/helpers/desktop-app-utils'; import { useConnectionRestored } from 'src/hooks/useConnectionRestored'; import type { WebsocketConnectionType } from 'src/reducers/connectionStatus'; +import { ViewGrid } from 'src/components/layout'; +import { ErrorView, LoadingView } from 'src/views/viewHelpers'; +import { StyledSingleColumn, StickyHeader } from './style'; +import { updateNotificationsCount } from 'src/actions/notifications'; +import NextPageButton from 'src/components/nextPageButton'; +import { PrimaryButton, OutlineButton } from 'src/components/button'; type Props = { markAllNotificationsSeen?: Function, @@ -73,7 +65,6 @@ type Props = { type State = { showWebPushPrompt: boolean, webPushPromptLoading: boolean, - scrollElement: any, }; class NotificationsPure extends React.Component { @@ -83,11 +74,11 @@ class NotificationsPure extends React.Component { this.state = { showWebPushPrompt: false, webPushPromptLoading: false, - scrollElement: null, }; } markAllNotificationsSeen = () => { + this.props.dispatch(updateNotificationsCount('notifications', 0)); this.props.markAllNotificationsSeen && this.props.markAllNotificationsSeen().catch(err => { console.error('Error marking all notifications seen: ', err); @@ -95,13 +86,17 @@ class NotificationsPure extends React.Component { }; componentDidMount() { - const scrollElement = document.getElementById('scroller-for-thread-feed'); - this.markAllNotificationsSeen(); - this.setState({ - // NOTE(@mxstbr): This is super un-reacty but it works. This refers to - // the AppViewWrapper which is the scrolling part of the site. - scrollElement, - }); + const { dispatch } = this.props; + dispatch( + setTitlebarProps({ + title: 'Notifications', + rightAction: ( + this.markAllNotificationsSeen()}> + Mark all seen + + ), + }) + ); WebPushManager.getPermissionState() .then(result => { @@ -190,7 +185,14 @@ class NotificationsPure extends React.Component { }; render() { - const { currentUser, data, isLoading } = this.props; + const { + currentUser, + data, + isLoading, + hasError, + isFetchingMore, + } = this.props; + const { title, description } = generateMetaInfo({ type: 'notifications', }); @@ -205,42 +207,36 @@ class NotificationsPure extends React.Component { notifications = deduplicateChildren(notifications, 'id'); notifications = sortByDate(notifications, 'modifiedAt', 'desc'); - const { scrollElement } = this.state; - return ( - + - - - - {!isDesktopApp() && - this.state.showWebPushPrompt && ( - - )} - } - useWindow={false} - initialLoad={false} - scrollElement={scrollElement} - threshold={750} - className={'scroller-for-notifications'} - > + + +
+ + {!isDesktopApp() && this.state.showWebPushPrompt && ( + + Enable push notifications + + )} + notification.isSeen + )} + onClick={() => this.markAllNotificationsSeen()} + > + Mark all seen + + {notifications.map(notification => { switch (notification.event) { case 'MESSAGE_CREATED': { return ( - + { } case 'REACTION_CREATED': { return ( - + { } case 'THREAD_REACTION_CREATED': { return ( - + { } case 'CHANNEL_CREATED': { return ( - + { } case 'USER_JOINED_COMMUNITY': { return ( - + { } case 'COMMUNITY_INVITE': { return ( - + { } case 'MENTION_MESSAGE': { return ( - + { } case 'MENTION_THREAD': { return ( - + { } case 'PRIVATE_CHANNEL_REQUEST_SENT': { return ( - + { } case 'PRIVATE_CHANNEL_REQUEST_APPROVED': { return ( - + { } case 'PRIVATE_COMMUNITY_REQUEST_SENT': { return ( - + + + ); } case 'PRIVATE_COMMUNITY_REQUEST_APPROVED': { return ( - + + + ); } default: { @@ -392,38 +363,38 @@ class NotificationsPure extends React.Component { } } })} - - - - + + {data.hasNextPage && ( + + Load more notifications + + )} +
+
+
+
); } if (isLoading) { - return ( - - - - - ); + return ; } - if (!data || (data && data.error)) { - return ( - - - - - ); + if (hasError) { + return ; } + // no issues loading, but the user doesnt have notifications yet return ( - - - - - - + ); } } @@ -436,7 +407,6 @@ const map = state => ({ export default compose( subscribeToWebPush, getNotifications, - displayLoadingNotifications, markNotificationsSeenMutation, viewNetworkHandler, withCurrentUser, diff --git a/src/views/notifications/style.js b/src/views/notifications/style.js index c11ecc4280..1ffaa70067 100644 --- a/src/views/notifications/style.js +++ b/src/views/notifications/style.js @@ -1,6 +1,6 @@ // @flow import theme from 'shared/theme'; -import styled from 'styled-components'; +import styled, { css } from 'styled-components'; import { FlexRow, FlexCol, @@ -10,10 +10,12 @@ import { Shadow, zIndex, Truncate, -} from '../../components/globals'; -import { HorizontalRule } from '../../components/globals'; -import Card from '../../components/card'; -import { IconButton } from '../../components/buttons'; +} from 'src/components/globals'; +import { HorizontalRule } from 'src/components/globals'; +import Card from 'src/components/card'; +import { SingleColumnGrid } from 'src/components/layout'; +import Icon from 'src/components/icon'; +import { MEDIA_BREAK } from 'src/components/layout'; export const HzRule = styled(HorizontalRule)` margin: 0; @@ -21,25 +23,21 @@ export const HzRule = styled(HorizontalRule)` export const NotificationCard = styled.div` padding: 16px; + width: 100%; padding-bottom: 24px; overflow: hidden; - transition: ${Transition.hover.off}; - border-radius: 8px; - background: ${props => props.theme.bg.default}; - margin-top: 8px; - box-shadow: ${Shadow.low} ${({ theme }) => hexa(theme.text.default, 0.1)}; position: relative; + border-bottom: 1px solid ${props => props.theme.bg.border}; + ${props => + props.isSeen === false && + css` + box-shadow: inset 2px 0 0 ${theme.brand.default}; + background: ${hexa(theme.brand.default, 0.06)}; + `} &:hover { - transition: none; - box-shadow: ${Shadow.high} ${({ theme }) => hexa(theme.text.default, 0.1)}; - } - - @media (max-width: 768px) { - border-radius: 0; - border-bottom: 1px solid ${props => props.theme.bg.border}; - box-shadow: none; - margin-top: 0; + background: ${props => + props.isSeen ? theme.bg.wash : hexa(theme.brand.default, 0.06)}; } `; @@ -47,13 +45,20 @@ export const SegmentedNotificationCard = styled(Card)` padding: 0; padding-top: 16px; transition: ${Transition.hover.off}; - border-radius: 8px; + border-radius: 0; box-shadow: ${Shadow.low} ${({ theme }) => hexa(theme.text.default, 0.1)}; &:hover { transition: none; box-shadow: ${Shadow.high} ${({ theme }) => hexa(theme.text.default, 0.1)}; } + + ${props => + props.isSeen === false && + css` + border-left: 2px solid ${theme.brand.default}; + background: ${hexa(theme.brand.default, 0.06)}; + `} `; export const ContentHeading = styled.h2` @@ -166,10 +171,11 @@ export const ActorPhotosContainer = styled(FlexRow)` margin: 0; margin-left: 4px; max-width: 100%; + flex-wrap: wrap; `; export const ActorPhotoItem = styled.div` - margin-right: 4px; + margin: 2px 4px 2px 0; `; export const ActorPhoto = styled.img` @@ -274,30 +280,25 @@ export const AttachmentsWash = styled(FlexCol)` flex: none; `; -export const RequestCard = styled.div` +export const StickyHeader = styled.div` display: flex; position: relative; flex-direction: row; align-items: center; - justify-content: space-between; - padding: 16px 16px 16px 24px; - border-radius: 8px; - box-shadow: ${Shadow.low} ${({ theme }) => hexa(theme.text.default, 0.1)}; + justify-content: flex-end; + padding: 16px; background: ${props => props.theme.bg.default}; + position: sticky; + top: 0; + border-bottom: 1px solid ${theme.bg.border}; + z-index: 10; - > p { - font-weight: 700; - font-size: 16px; - } - - @media (max-width: 768px) { - border-radius: 0; - border-bottom: 1px solid ${props => props.theme.bg.border}; - box-shadow: none; + @media (max-width: ${MEDIA_BREAK}px) { + display: none; } `; -export const CloseRequest = styled(IconButton)` +export const CloseRequest = styled(Icon)` margin-left: 8px; color: ${theme.text.placeholder}; `; @@ -357,3 +358,8 @@ export const ToggleNotificationsContainer = styled.div` height: 100%; cursor: pointer; `; + +export const StyledSingleColumn = styled(SingleColumnGrid)` + border-left: 1px solid ${theme.bg.border}; + border-right: 1px solid ${theme.bg.border}; +`; diff --git a/src/views/pages/apps/index.js b/src/views/pages/apps/index.js index 237e4447a0..d31ded35be 100644 --- a/src/views/pages/apps/index.js +++ b/src/views/pages/apps/index.js @@ -4,7 +4,8 @@ import Section from 'src/components/themedSection'; import PageFooter from '../components/footer'; import { Wrapper } from '../style'; import { Heading, Copy } from '../pricing/style'; -import { Button } from 'src/components/buttons'; +import { PrimaryButton } from 'src/components/button'; +import Icon from 'src/components/icon'; import { Intro, ActionsContainer, TextContent } from './style'; import type { ContextRouter } from 'react-router'; import { track, events } from 'src/helpers/analytics'; @@ -31,7 +32,7 @@ class Features extends React.Component { title={'Spectrum · Apps'} description={'Download Spectrum for Mac and Windows'} /> -
+
Spectrum for Mac @@ -41,15 +42,13 @@ class Features extends React.Component { - - - + track(events.APPS_PAGE_DOWNLOAD_MAC_CLICKED)} + > + + Download for Mac + diff --git a/src/views/pages/components/footer.js b/src/views/pages/components/footer.js index f0e7ee3594..60d4e27f5c 100644 --- a/src/views/pages/components/footer.js +++ b/src/views/pages/components/footer.js @@ -10,7 +10,7 @@ import { SocialLinks, } from '../style'; import { Link } from 'react-router-dom'; -import { IconButton } from 'src/components/buttons'; +import Icon from 'src/components/icon'; import { Logo } from 'src/components/logo'; import { track, events } from 'src/helpers/analytics'; @@ -28,14 +28,14 @@ export default () => { target="_blank" rel="noopener noreferrer" > - + - + diff --git a/src/views/pages/components/logos.js b/src/views/pages/components/logos.js index 7487b757ef..2e418430d2 100644 --- a/src/views/pages/components/logos.js +++ b/src/views/pages/components/logos.js @@ -1,12 +1,13 @@ // @flow import React from 'react'; import styled from 'styled-components'; +import { MEDIA_BREAK } from 'src/components/layout'; const Logo = styled.img` height: 32px; object-fit: contain; - @media (max-width: 768px) { + @media (max-width: ${MEDIA_BREAK}px) { height: 24px; } `; diff --git a/src/views/pages/components/nav.js b/src/views/pages/components/nav.js index 946b2f1f53..dbfd0464b3 100644 --- a/src/views/pages/components/nav.js +++ b/src/views/pages/components/nav.js @@ -2,9 +2,9 @@ import * as React from 'react'; import compose from 'recompose/compose'; import { connect } from 'react-redux'; -import { Button, IconButton } from 'src/components/buttons'; +import { PrimaryButton } from 'src/components/button'; +import Icon from 'src/components/icon'; import { Link } from 'react-router-dom'; -import Icon from 'src/components/icons'; import { Logo } from 'src/components/logo'; import { UserAvatar } from 'src/components/avatar'; import Head from 'src/components/head'; @@ -48,7 +48,7 @@ class Nav extends React.Component { render() { return ( - + { @@ -73,7 +73,7 @@ class Nav extends React.Component { dark={this.props.dark} selected={this.props.location === 'features'} to="/features" - data-cy="navbar-splash-features" + data-cy="navigation-splash-features" > Features @@ -81,7 +81,7 @@ class Nav extends React.Component { dark={this.props.dark} selected={this.props.location === 'apps'} to="/apps" - data-cy="navbar-splash-apps" + data-cy="navigation-splash-apps" > Apps @@ -89,7 +89,7 @@ class Nav extends React.Component { dark={this.props.dark} selected={this.props.location === 'support'} to="/support" - data-cy="navbar-splash-support" + data-cy="navigation-splash-support" > Support @@ -98,7 +98,10 @@ class Nav extends React.Component { ) : ( @@ -106,21 +109,21 @@ class Nav extends React.Component { to="/login" onClick={() => track(events.HOME_PAGE_SIGN_IN_CLICKED)} > - + Log in or sign up + )} - this.toggleMenu()} /> diff --git a/src/views/pages/features/index.js b/src/views/pages/features/index.js index dbc727bdf4..245aff9e83 100644 --- a/src/views/pages/features/index.js +++ b/src/views/pages/features/index.js @@ -2,12 +2,11 @@ import * as React from 'react'; import Section from 'src/components/themedSection'; import PageFooter from '../components/footer'; -import { Link } from 'react-router-dom'; -import Icon from 'src/components/icons'; +import Icon from 'src/components/icon'; import { Easy, Happy, Impact, Ideas } from 'src/components/illustrations'; import { Wrapper } from '../style'; import { Heading, Copy } from '../pricing/style'; -import { Button } from 'src/components/buttons'; +import { PrimaryButton } from 'src/components/button'; import { Intro, TextContent, @@ -56,16 +55,12 @@ class Features extends React.Component { organically, moderate it effectively, and measure its ROI for your organization. - - - + track(events.FEATURES_PAGE_GET_STARTED_CLICKED)} + > + Get started + @@ -84,7 +79,7 @@ class Features extends React.Component { Finally, chat that scales

- We've taken the best features of modern chat platforms and + We’ve taken the best features of modern chat platforms and old-school forums and smashed them together into a format that makes it easy scale to any size - even across timezones. @@ -146,7 +141,7 @@ class Features extends React.Component { Real members. Real reputation.

- With Spectrum's Rep system, you can see how active and + With Spectrum’s Rep system, you can see how active and constructive a member is in your community - and globally across communities - which makes it simple to figure out if an issue is a trend or an isolated incident. @@ -181,15 +176,15 @@ class Features extends React.Component { Focus on impact, not usage. - Understand your community's health + Understand your community’s health

- When you add Community Analytics to your community, you'll - get a bird's eye view of your community's overall growth and + When you add Community Analytics to your community, you’ll + get a bird’s eye view of your community’s overall growth and user engagement.

- You'll also get a heads up of what types of conversations + You’ll also get a heads up of what types of conversations are most active as well as a list of any that have gone unanswered.

@@ -211,7 +206,7 @@ class Features extends React.Component { - Visualize your community's ROI + Visualize your community’s ROI

@@ -256,7 +251,7 @@ class Features extends React.Component { Collect actionable feedback and ideas

- Spectrum's great for collecting feature requests and user + Spectrum’s great for collecting feature requests and user feedback, and the realtime nature makes it easy for the requests to adapt as you update your product.

@@ -285,7 +280,7 @@ class Features extends React.Component {
- And there's a whole lot more to love... + And there’s a whole lot more to love... Unlimited chat @@ -337,14 +332,12 @@ class Features extends React.Component { What are you waiting for? - - - + track(events.FEATURES_PAGE_GET_STARTED_CLICKED)} + > + Get started +
diff --git a/src/views/pages/features/style.js b/src/views/pages/features/style.js index c58552c7d3..43fbf11ef0 100644 --- a/src/views/pages/features/style.js +++ b/src/views/pages/features/style.js @@ -1,6 +1,6 @@ import styled, { css } from 'styled-components'; import theme from 'shared/theme'; -import { SvgWrapper } from 'src/components/icons'; +import { SvgWrapper } from 'src/components/icon'; /* eslint no-eval: 0 */ @@ -34,15 +34,6 @@ export const TextContent = styled.div` > a { display: inline-block; - - > button { - padding: 8px 16px 8px 8px; - - > span { - font-size: 16px; - margin-left: 16px; - } - } } @media (max-width: 1480px) { @@ -268,7 +259,7 @@ export const EtcCTA = styled.div` flex-direction: column; align-self: stretch; align-items: center; - margin-bottom: 80px; + margin-bottom: 108px; > span { margin-top: 32px; @@ -278,15 +269,6 @@ export const EtcCTA = styled.div` > a { display: inline-block; - - > button { - padding: 8px 16px 8px 8px; - - > span { - font-size: 16px; - margin-left: 16px; - } - } } `; diff --git a/src/views/pages/index.js b/src/views/pages/index.js index b3b25c433a..9a222ab7d0 100644 --- a/src/views/pages/index.js +++ b/src/views/pages/index.js @@ -8,7 +8,7 @@ import Terms from './terms'; import Privacy from './privacy'; import Faq from './faq'; import Apps from './apps'; -import { Page } from './style'; +import { StyledViewGrid } from './style'; type Props = { match: Object, @@ -52,13 +52,15 @@ class Pages extends React.Component { const dark = path === '/' || path === '/about'; return ( - -
diff --git a/src/views/pages/view.js b/src/views/pages/view.js index 35fe92f752..6514a294e2 100644 --- a/src/views/pages/view.js +++ b/src/views/pages/view.js @@ -3,18 +3,17 @@ import theme from 'shared/theme'; import React from 'react'; import styled from 'styled-components'; import { Link } from 'react-router-dom'; -import Icon from '../../components/icons'; -import { UserAvatar } from '../../components/avatar'; -import { - Shadow, - Gradient, - FlexCol, - Transition, - HorizontalRule, -} from '../../components/globals'; +import Icon from 'src/components/icon'; +import { UserAvatar } from 'src/components/avatar'; +import { FlexCol, Transition, HorizontalRule } from 'src/components/globals'; import Search from '../explore/components/search'; -import Section from '../../components/themedSection'; -import { Conversation, Discover } from '../../components/illustrations'; +import Section from 'src/components/themedSection'; +import { + PrimaryButton, + WhiteOutlineButton, + WhiteButton, +} from 'src/components/button'; +import { Conversation, Discover } from 'src/components/illustrations'; import { AbstractLogo, BootstrapLogo, @@ -36,14 +35,12 @@ import { BulletCopy, Flexer, PrimaryCTA, - SecondaryCTA, Content, } from './style'; import { track, events } from 'src/helpers/analytics'; +import { MEDIA_BREAK } from 'src/components/layout'; -type Props = Object; - -export const Overview = (props: Props) => { +export const Overview = () => { const ThisContent = styled(Content)` max-width: 100vw; margin-top: 92px; @@ -60,7 +57,7 @@ export const Overview = (props: Props) => { align-items: flex-start; z-index: 2; - @media (max-width: 768px) { + @media (max-width: ${MEDIA_BREAK}px) { margin-top: 0; margin-bottom: 16px; text-align: center; @@ -73,7 +70,7 @@ export const Overview = (props: Props) => { font-weight: 500; max-width: 580px; - @media (max-width: 768px) { + @media (max-width: ${MEDIA_BREAK}px) { text-align: center; } `; @@ -82,27 +79,28 @@ export const Overview = (props: Props) => { margin-bottom: 16px; font-size: 40px; - @media (max-width: 768px) { + @media (max-width: ${MEDIA_BREAK}px) { font-size: 24px; } `; - const Actions = styled(Flexer)` + const Actions = styled.div` + display: flex; margin-top: 48px; + width: 100%; align-items: flex-start; - justify-content: space-between; - @media (max-width: 768px) { + @media (max-width: ${MEDIA_BREAK}px) { + flex-direction: column; align-items: center; } `; - const ThisSecondaryCTA = styled(SecondaryCTA)` + const ThisSecondaryCTA = styled(WhiteOutlineButton)` margin-left: 16px; font-size: 16px; - border: 2px solid ${theme.text.reverse}; - @media (max-width: 768px) { + @media (max-width: ${MEDIA_BREAK}px) { margin-left: 0; margin-top: 16px; } @@ -121,8 +119,12 @@ export const Overview = (props: Props) => { } `; - const ThisPrimaryCTA = styled(PrimaryCTA)` - font-size: 16px; + const ThisPrimaryCTA = styled(WhiteButton)` + color: ${theme.brand.alt}; + + &:hover { + color: ${theme.brand.default}; + } `; const Img = styled.img` @@ -141,7 +143,7 @@ export const Overview = (props: Props) => { display: none; } - @media (max-width: 768px) { + @media (max-width: ${MEDIA_BREAK}px) { display: none; } `; @@ -162,20 +164,18 @@ export const Overview = (props: Props) => { that are built to last. - track(events.HOME_PAGE_JOIN_SPECTRUM_CLICKED)} > - Join Spectrum - - + track(events.HOME_PAGE_CREATE_COMMUNITY_CLICKED)} > - - Create your community - - + Create your community + @@ -184,7 +184,7 @@ export const Overview = (props: Props) => { ); }; -export const Centralized = (props: Props) => { +export const Centralized = () => { const ThisContent = styled(Content)` img { margin: 24px 0; @@ -194,7 +194,7 @@ export const Centralized = (props: Props) => { const Text = styled(FlexCol)` margin: 40px 16px 64px; - @media (max-width: 768px) { + @media (max-width: ${MEDIA_BREAK}px) { margin-top: 20px; margin-bottom: 44px; } @@ -205,27 +205,19 @@ export const Centralized = (props: Props) => { margin-top: 16px; `; - const ThisPrimaryCTA = styled(PrimaryCTA)` + const ThisPrimaryCTA = styled(PrimaryButton)` margin-top: 32px; - background-color: ${theme.brand.alt}; - background-image: ${props => - Gradient(props.theme.brand.alt, props.theme.brand.default)}; - color: ${theme.text.reverse}; - - &:hover { - color: ${theme.text.reverse}; - } `; const Actions = styled.div` - @media (max-width: 768px) { + @media (max-width: ${MEDIA_BREAK}px) { display: flex; justify-content: center; } `; const ThisTagline = styled(Tagline)` - @media (max-width: 768px) { + @media (max-width: ${MEDIA_BREAK}px) { margin-bottom: 0; } `; @@ -266,14 +258,12 @@ export const Centralized = (props: Props) => { wins! - - track(events.HOME_PAGE_EXPLORE_CLICKED)} - > - Explore communities - - + track(events.HOME_PAGE_EXPLORE_CLICKED)} + > + Explore communities + @@ -292,7 +282,7 @@ export const Centralized = (props: Props) => { ); }; -export const CommunitySearch = (props: Props) => { +export const CommunitySearch = () => { const ThisContent = styled(Content)` flex-direction: column; width: 640px; @@ -319,7 +309,7 @@ export const CommunitySearch = (props: Props) => { text-align: center; max-width: 640px; - @media (max-width: 768px) { + @media (max-width: ${MEDIA_BREAK}px) { text-align: left; } `; @@ -337,12 +327,12 @@ export const CommunitySearch = (props: Props) => { ); }; -export const Chat = (props: Props) => { +export const Chat = () => { const ThisContent = styled(Content)` overflow: hidden; margin: 40px 16px 80px; - @media (max-width: 768px) { + @media (max-width: ${MEDIA_BREAK}px) { margin-bottom: 40px; } `; @@ -352,27 +342,19 @@ export const Chat = (props: Props) => { margin-top: 16px; `; - const ThisPrimaryCTA = styled(PrimaryCTA)` - background-color: ${theme.brand.alt}; - background-image: ${props => - Gradient(props.theme.brand.alt, props.theme.brand.default)}; - color: ${theme.text.reverse}; - margin-top: 32px; - - &:hover { - color: ${theme.text.reverse}; - } - `; + const ThisPrimaryCTA = styled(PrimaryButton)``; const Actions = styled.div` - @media (max-width: 768px) { + margin-top: 32px; + + @media (max-width: ${MEDIA_BREAK}px) { display: flex; justify-content: center; } `; const ThisTagline = styled(Tagline)` - @media (max-width: 768px) { + @media (max-width: ${MEDIA_BREAK}px) { margin-bottom: 0; } `; @@ -394,16 +376,14 @@ export const Chat = (props: Props) => { - track(events.HOME_PAGE_EXAMPLE_CONVERSATION_CLICKED) } > - - Check out a conversation - - + Check out a conversation +
@@ -411,7 +391,7 @@ export const Chat = (props: Props) => { ); }; -export const Sell = (props: Props) => { +export const Sell = () => { const Text = styled(FlexCol)` align-items: center; margin: 40px 0; @@ -482,20 +462,18 @@ export const Sell = (props: Props) => { - track(events.HOME_PAGE_CREATE_COMMUNITY_CLICKED)} > - - Start building your community - - + Start building your community + ); }; -export const Yours = (props: Props) => { +export const Yours = () => { const ThisContent = styled(Content)` margin: 60px 16px 80px; font-size: 18px; @@ -508,26 +486,16 @@ export const Yours = (props: Props) => { align-self: center; `; - const ThisSecondaryCTA = styled(SecondaryCTA)` + const ThisSecondaryCTA = styled(WhiteOutlineButton)` margin-left: 16px; - font-size: 16px; - border: 2px solid ${theme.text.reverse}; - @media (max-width: 768px) { + @media (max-width: ${MEDIA_BREAK}px) { margin-left: 0; margin-top: 16px; } `; - const ThisPrimaryCTA = styled(PrimaryCTA)` - font-size: 16px; - color: ${theme.text.default}; - - &:hover { - color: ${theme.brand.alt}; - box-shadow: ${Shadow.high} #000; - } - `; + const ThisPrimaryCTA = styled(WhiteButton)``; const Actions = styled(Flexer)` margin-top: 32px; @@ -537,7 +505,7 @@ export const Yours = (props: Props) => { display: inline-block; } - @media (max-width: 768px) { + @media (max-width: ${MEDIA_BREAK}px) { justify-content: center; } `; @@ -552,7 +520,7 @@ export const Yours = (props: Props) => { flex-wrap: wrap; margin-left: -32px; - @media (max-width: 768px) { + @media (max-width: ${MEDIA_BREAK}px) { display: none; } `; @@ -688,20 +656,18 @@ export const Yours = (props: Props) => { - track(events.HOME_PAGE_JOIN_SPECTRUM_CLICKED)} > - Join Spectrum - - + track(events.HOME_PAGE_CREATE_COMMUNITY_CLICKED)} > - - Explore communities - - + Explore communities + diff --git a/src/views/privateChannelJoin/index.js b/src/views/privateChannelJoin/index.js index 583bc2d595..cfa26a824c 100644 --- a/src/views/privateChannelJoin/index.js +++ b/src/views/privateChannelJoin/index.js @@ -5,11 +5,10 @@ import { connect } from 'react-redux'; import joinChannelWithToken from 'shared/graphql/mutations/channel/joinChannelWithToken'; import { addToastWithTimeout } from 'src/actions/toasts'; import CommunityLogin from 'src/views/communityLogin'; -import AppViewWrapper from 'src/components/appViewWrapper'; -import { Loading } from 'src/components/loading'; import { CLIENT_URL } from 'src/api/constants'; import type { Dispatch } from 'redux'; import { withCurrentUser } from 'src/components/withCurrentUser'; +import { LoadingView, ErrorView } from 'src/views/viewHelpers'; type Props = { match: Object, @@ -59,7 +58,7 @@ class PrivateChannelJoin extends React.Component { this.setState({ isLoading: true }); joinChannelWithToken({ channelSlug, token, communitySlug }) - .then(data => { + .then(() => { this.setState({ isLoading: false }); dispatch(addToastWithTimeout('success', 'Welcome!')); return history.push(`/${communitySlug}/${channelSlug}`); @@ -85,15 +84,9 @@ class PrivateChannelJoin extends React.Component { return ; } - if (isLoading) { - return ( - - - - ); - } + if (isLoading) return ; - return null; + return ; } } diff --git a/src/views/privateCommunityJoin/index.js b/src/views/privateCommunityJoin/index.js index c47103091e..217ba89c79 100644 --- a/src/views/privateCommunityJoin/index.js +++ b/src/views/privateCommunityJoin/index.js @@ -6,10 +6,9 @@ import { connect } from 'react-redux'; import addCommunityMemberWithTokenMutation from 'shared/graphql/mutations/communityMember/addCommunityMemberWithToken'; import { addToastWithTimeout } from 'src/actions/toasts'; import CommunityLogin from 'src/views/communityLogin'; -import AppViewWrapper from 'src/components/appViewWrapper'; -import { Loading } from 'src/components/loading'; import { CLIENT_URL } from 'src/api/constants'; import { withCurrentUser } from 'src/components/withCurrentUser'; +import { ErrorView, LoadingView } from 'src/views/viewHelpers'; type Props = { match: Object, @@ -34,7 +33,7 @@ class PrivateCommunityJoin extends React.Component { const { token, communitySlug } = match.params; if (!token) { - return history.push(`/${communitySlug}`); + return history.replace(`/${communitySlug}`); } if (!currentUser) { @@ -64,15 +63,15 @@ class PrivateCommunityJoin extends React.Component { this.setState({ isLoading: true }); addCommunityMemberWithToken({ communitySlug, token }) - .then(data => { + .then(() => { this.setState({ isLoading: false }); dispatch(addToastWithTimeout('success', 'Welcome!')); - return history.push(`/${communitySlug}`); + return history.replace(`/${communitySlug}`); }) .catch(err => { this.setState({ isLoading: false }); dispatch(addToastWithTimeout('error', err.message)); - return history.push(`/${communitySlug}`); + return history.replace(`/${communitySlug}`); }); }; @@ -90,15 +89,9 @@ class PrivateCommunityJoin extends React.Component { return ; } - if (isLoading) { - return ( - - - - ); - } + if (isLoading) return ; - return null; + return ; } } diff --git a/src/views/queryParamToastDispatcher/index.js b/src/views/queryParamToastDispatcher/index.js index 703afa44f3..a1403fcadc 100644 --- a/src/views/queryParamToastDispatcher/index.js +++ b/src/views/queryParamToastDispatcher/index.js @@ -1,6 +1,7 @@ // @flow import React from 'react'; -import type { Location, History } from 'react-router'; +import compose from 'recompose/compose'; +import { withRouter, type Location, type History } from 'react-router'; import querystring from 'querystring'; import { connect } from 'react-redux'; import type { Dispatch } from 'redux'; @@ -14,7 +15,7 @@ type Props = { class QueryParamToastDispatcher extends React.Component { getParams = (props: Props) => { - return querystring.parse(this.props.location.search.replace('?', '')); + return querystring.parse(props.location.search.replace('?', '')); }; componentDidMount() { @@ -86,4 +87,7 @@ class QueryParamToastDispatcher extends React.Component { } } -export default connect()(QueryParamToastDispatcher); +export default compose( + withRouter, + connect() +)(QueryParamToastDispatcher); diff --git a/src/views/search/index.js b/src/views/search/index.js index 95496b4cea..91cc771cc3 100644 --- a/src/views/search/index.js +++ b/src/views/search/index.js @@ -2,24 +2,35 @@ import * as React from 'react'; import compose from 'recompose/compose'; import { connect } from 'react-redux'; -import Titlebar from '../titlebar'; -import { View } from './style'; +import { ViewGrid } from 'src/components/layout'; import searchThreadsQuery from 'shared/graphql/queries/search/searchThreads'; -import DashboardThreadFeed from '../dashboard/components/threadFeed'; -import { InboxScroller } from '../dashboard/style'; +import ThreadFeed from 'src/components/threadFeed'; import SearchInput from './searchInput'; +import { setTitlebarProps } from 'src/actions/titlebar'; -const SearchThreadFeed = compose(connect(), searchThreadsQuery)( - DashboardThreadFeed -); +const SearchThreadFeed = compose( + connect(), + searchThreadsQuery +)(ThreadFeed); -type Props = {}; +type Props = { + dispatch: Function, +}; type State = { searchQueryString: ?string, }; class Search extends React.Component { state = { searchQueryString: '' }; + componentDidMount() { + const { dispatch } = this.props; + return dispatch( + setTitlebarProps({ + title: 'Search', + }) + ); + } + handleSubmit = (searchQueryString: string) => { if (searchQueryString.length > 0) { this.setState({ searchQueryString }); @@ -31,27 +42,16 @@ class Search extends React.Component { const searchFilter = { everythingFeed: true }; return ( - - - + - - {searchQueryString && - searchQueryString.length > 0 && - searchFilter && ( - - )} - - + {searchQueryString && searchQueryString.length > 0 && searchFilter && ( + + )} + ); } } diff --git a/src/views/search/searchInput.js b/src/views/search/searchInput.js index 1d977e9f4b..3ab4a6e573 100644 --- a/src/views/search/searchInput.js +++ b/src/views/search/searchInput.js @@ -1,7 +1,7 @@ import * as React from 'react'; import compose from 'recompose/compose'; import { SearchWrapper, SearchInput, ClearSearch, SearchForm } from './style'; -import Icon from '../../components/icons'; +import Icon from 'src/components/icon'; type Props = {}; type State = { @@ -60,7 +60,7 @@ class SearchViewInput extends React.Component { onChange={this.onChange} value={value} placeholder={placeholder} - innerRef={input => { + ref={input => { this.searchInput = input; }} autoFocus={true} diff --git a/src/views/search/style.js b/src/views/search/style.js index f76876cc50..c7cc94853e 100644 --- a/src/views/search/style.js +++ b/src/views/search/style.js @@ -13,8 +13,9 @@ export const View = styled.div` export const SearchWrapper = styled.div` color: ${theme.text.alt}; display: flex; - align-items: center; flex: none; + align-items: center; + align-self: flex-start; transition: all 0.2s; border-bottom: 1px solid ${theme.bg.border}; position: relative; diff --git a/src/views/thread/components/actionBar.js b/src/views/thread/components/actionBar.js index a50de44888..fe272d4124 100644 --- a/src/views/thread/components/actionBar.js +++ b/src/views/thread/components/actionBar.js @@ -2,19 +2,16 @@ import * as React from 'react'; import { connect } from 'react-redux'; import Clipboard from 'react-clipboard.js'; -import { Manager, Reference, Popper } from 'react-popper'; import { CLIENT_URL } from 'src/api/constants'; import { addToastWithTimeout } from 'src/actions/toasts'; -import { openModal } from 'src/actions/modals'; -import Icon from 'src/components/icons'; +import Tooltip from 'src/components/tooltip'; +import Icon from 'src/components/icon'; import compose from 'recompose/compose'; -import { Button, TextButton, IconButton } from 'src/components/buttons'; -import Flyout from 'src/components/flyout'; +import { PrimaryOutlineButton, TextButton } from 'src/components/button'; import { LikeButton } from 'src/components/threadLikes'; import type { GetThreadType } from 'shared/graphql/queries/thread/getThread'; import toggleThreadNotificationsMutation from 'shared/graphql/mutations/thread/toggleThreadNotifications'; -import OutsideClickHandler from 'src/components/outsideClickHandler'; -import { track, events, transformations } from 'src/helpers/analytics'; +import { track, events } from 'src/helpers/analytics'; import getThreadLink from 'src/helpers/get-thread-link'; import type { Dispatch } from 'redux'; import { InputHints, DesktopLink } from 'src/components/composer/style'; @@ -23,16 +20,13 @@ import { MediaInput, } from 'src/components/chatInput/components/style'; import { - FollowButton, ShareButtons, ShareButton, ActionBarContainer, FixedBottomActionBarContainer, - FlyoutRow, - DropWrap, EditDone, - Label, } from '../style'; +import ActionsDropdown from './actionsDropdown'; type Props = { thread: GetThreadType, @@ -52,213 +46,14 @@ type Props = { isPinningThread: boolean, uploadFiles: Function, }; -type State = { - notificationStateLoading: boolean, - flyoutOpen: boolean, - isSettingsBtnHovering: boolean, -}; -class ActionBar extends React.Component { - state = { - notificationStateLoading: false, - flyoutOpen: false, - isSettingsBtnHovering: false, - }; - - toggleHover = () => { - this.setState(({ isSettingsBtnHovering }) => ({ - isSettingsBtnHovering: !isSettingsBtnHovering, - })); - }; - - toggleFlyout = val => { - if (val) { - return this.setState({ flyoutOpen: val }); - } - - if (this.state.flyoutOpen === false) { - return this.setState({ flyoutOpen: true }); - } else { - return this.setState({ flyoutOpen: false }); - } - }; - - triggerChangeChannel = () => { - const { thread, dispatch } = this.props; - - track(events.THREAD_MOVED_INITED, { - thread: transformations.analyticsThread(thread), - channel: transformations.analyticsChannel(thread.channel), - community: transformations.analyticsCommunity(thread.community), - }); - - dispatch(openModal('CHANGE_CHANNEL', { thread })); - }; - - toggleNotification = () => { - const { thread, dispatch, toggleThreadNotifications } = this.props; - const threadId = thread.id; - - this.setState({ - notificationStateLoading: true, - }); - - toggleThreadNotifications({ - threadId, - }) - .then(({ data: { toggleThreadNotifications } }) => { - this.setState({ - notificationStateLoading: false, - }); - - if (toggleThreadNotifications.receiveNotifications) { - return dispatch( - addToastWithTimeout('success', 'Notifications activated!') - ); - } else { - return dispatch( - addToastWithTimeout('neutral', 'Notifications turned off') - ); - } - }) - .catch(err => { - this.setState({ - notificationStateLoading: true, - }); - dispatch(addToastWithTimeout('error', err.message)); - }); - }; - - getThreadActionPermissions = () => { - const { currentUser, thread } = this.props; - const { - channel: { channelPermissions }, - community: { communityPermissions }, - } = thread; - - const isThreadAuthor = - currentUser && currentUser.id === thread.author.user.id; - const isChannelModerator = currentUser && channelPermissions.isModerator; - const isCommunityModerator = - currentUser && communityPermissions.isModerator; - const isChannelOwner = currentUser && channelPermissions.isOwner; - const isCommunityOwner = currentUser && communityPermissions.isOwner; - - return { - isThreadAuthor, - isChannelModerator, - isCommunityModerator, - isChannelOwner, - isCommunityOwner, - }; - }; - - shouldRenderEditThreadAction = () => { - const { isThreadAuthor } = this.getThreadActionPermissions(); - return isThreadAuthor; - }; - - shouldRenderMoveThreadAction = () => { - const { - isCommunityOwner, - isCommunityModerator, - } = this.getThreadActionPermissions(); - - return isCommunityModerator || isCommunityOwner; - }; - - shouldRenderLockThreadAction = () => { - const { - isThreadAuthor, - isChannelModerator, - isChannelOwner, - isCommunityOwner, - isCommunityModerator, - } = this.getThreadActionPermissions(); - - return ( - isThreadAuthor || - isChannelModerator || - isCommunityModerator || - isChannelOwner || - isCommunityOwner - ); - }; - - shouldRenderDeleteThreadAction = () => { - const { - isThreadAuthor, - isChannelModerator, - isChannelOwner, - isCommunityOwner, - isCommunityModerator, - } = this.getThreadActionPermissions(); - - return ( - isThreadAuthor || - isChannelModerator || - isCommunityModerator || - isChannelOwner || - isCommunityOwner - ); - }; - - shouldRenderPinThreadAction = () => { - const { thread } = this.props; - const { - isCommunityOwner, - isCommunityModerator, - } = this.getThreadActionPermissions(); - - return ( - !thread.channel.isPrivate && (isCommunityOwner || isCommunityModerator) - ); - }; - - shouldRenderActionsDropdown = () => { - const { - isThreadAuthor, - isChannelModerator, - isChannelOwner, - isCommunityOwner, - isCommunityModerator, - } = this.getThreadActionPermissions(); - - return ( - isThreadAuthor || - isChannelModerator || - isCommunityModerator || - isChannelOwner || - isCommunityOwner - ); - }; +class ActionBar extends React.Component { uploadFiles = evt => { this.props.uploadFiles(evt.target.files); }; render() { - const { - thread, - currentUser, - isEditing, - isSavingEdit, - title, - isLockingThread, - isPinningThread, - } = this.props; - const { - notificationStateLoading, - flyoutOpen, - isSettingsBtnHovering, - } = this.state; - const isPinned = thread.community.pinnedThreadId === thread.id; - - const shouldRenderActionsDropdown = this.shouldRenderActionsDropdown(); - const shouldRenderPinThreadAction = this.shouldRenderPinThreadAction(); - const shouldRenderLockThreadAction = this.shouldRenderLockThreadAction(); - const shouldRenderMoveThreadAction = this.shouldRenderMoveThreadAction(); - const shouldRenderEditThreadAction = this.shouldRenderEditThreadAction(); - const shouldRenderDeleteThreadAction = this.shouldRenderDeleteThreadAction(); + const { thread, isEditing, isSavingEdit, title } = this.props; if (isEditing) { return ( @@ -287,14 +82,14 @@ class ActionBar extends React.Component { Cancel - + {isSavingEdit ? 'Saving...' : 'Save'} + @@ -303,103 +98,66 @@ class ActionBar extends React.Component { return (
- - - {!thread.channel.isPrivate && ( - - - - - track(events.THREAD_SHARED, { method: 'facebook' }) - } - /> - - - - - - - track(events.THREAD_SHARED, { method: 'twitter' }) - } - /> - - - - - this.props.dispatch( - addToastWithTimeout('success', 'Copied to clipboard') - ) - } - > - - - - track(events.THREAD_SHARED, { method: 'link' }) - } - /> - - - - - )} - {thread.channel.isPrivate && ( - - - this.props.dispatch( - addToastWithTimeout('success', 'Copied to clipboard') - ) - } - > - + + + + {!thread.channel.isPrivate && ( + + + + + + track(events.THREAD_SHARED, { method: 'facebook' }) + } + /> + + + + + + + + + track(events.THREAD_SHARED, { method: 'twitter' }) + } + /> + + + + + )} + + + this.props.dispatch( + addToastWithTimeout('success', 'Copied to clipboard') + ) + } + > + + { /> - - - )} + + +
- {currentUser && ( - - {thread.receiveNotifications ? 'Subscribed' : 'Notify me'} - - )} - - {shouldRenderActionsDropdown && ( - - - - {({ ref }) => { - return ( - - ); - }} - - {(isSettingsBtnHovering || flyoutOpen) && ( - - - {({ style, ref, placement }) => { - return ( - - - - {thread.receiveNotifications - ? 'Subscribed' - : 'Notify me'} - - - - {shouldRenderEditThreadAction && ( - - - - - - )} - - {shouldRenderPinThreadAction && ( - - - - - - )} - - {shouldRenderMoveThreadAction && ( - - - Move thread - - - )} - - {shouldRenderLockThreadAction && ( - - - - - - )} - - {shouldRenderDeleteThreadAction && ( - - - - - - )} - - ); - }} - - - )} - - - )} +
); diff --git a/src/views/thread/components/actionsDropdown.js b/src/views/thread/components/actionsDropdown.js new file mode 100644 index 0000000000..238573ca2a --- /dev/null +++ b/src/views/thread/components/actionsDropdown.js @@ -0,0 +1,271 @@ +// @flow +import React, { useState } from 'react'; +import { Manager, Reference, Popper } from 'react-popper'; +import { connect } from 'react-redux'; +import compose from 'recompose/compose'; +import { openModal } from 'src/actions/modals'; +import { addToastWithTimeout } from 'src/actions/toasts'; +import Flyout from 'src/components/flyout'; +import OutsideClickHandler from 'src/components/outsideClickHandler'; +import Icon from 'src/components/icon'; +import { TextButton } from 'src/components/button'; +import { withCurrentUser } from 'src/components/withCurrentUser'; +import toggleThreadNotificationsMutation from 'shared/graphql/mutations/thread/toggleThreadNotifications'; +import { track, events, transformations } from 'src/helpers/analytics'; +import { FlyoutRow, DropWrap, Label } from '../style'; + +type Props = { + thread: Object, + toggleEdit: Function, + isPinningThread: boolean, + togglePinThread: Function, + isLockingThread: boolean, + lockThread: Function, + triggerDelete: Function, + // Injected + currentUser: Object, + dispatch: Function, + toggleThreadNotifications: Function, +}; + +const ActionsDropdown = (props: Props) => { + const { + thread, + dispatch, + toggleThreadNotifications, + currentUser, + toggleEdit, + isPinningThread, + togglePinThread, + isLockingThread, + lockThread, + triggerDelete, + } = props; + if (!currentUser) return null; + + const { + channel: { channelPermissions }, + community: { communityPermissions }, + } = thread; + + const isThreadAuthor = + currentUser && currentUser.id === thread.author.user.id; + const isChannelModerator = currentUser && channelPermissions.isModerator; + const isCommunityModerator = currentUser && communityPermissions.isModerator; + const isChannelOwner = currentUser && channelPermissions.isOwner; + const isCommunityOwner = currentUser && communityPermissions.isOwner; + + const shouldRenderEditThreadAction = + isThreadAuthor || + isChannelModerator || + isCommunityModerator || + isChannelOwner || + isCommunityOwner; + + const shouldRenderMoveThreadAction = isCommunityModerator || isCommunityOwner; + + const shouldRenderLockThreadAction = + isThreadAuthor || + isChannelModerator || + isCommunityModerator || + isChannelOwner || + isCommunityOwner; + + const shouldRenderDeleteThreadAction = + isThreadAuthor || + isChannelModerator || + isCommunityModerator || + isChannelOwner || + isCommunityOwner; + + const shouldRenderPinThreadAction = + !thread.channel.isPrivate && (isCommunityOwner || isCommunityModerator); + + const toggleNotification = () => { + toggleThreadNotifications({ + threadId: thread.id, + }) + .then(({ data: { toggleThreadNotifications } }) => { + if (toggleThreadNotifications.receiveNotifications) { + return dispatch( + addToastWithTimeout('success', 'Notifications activated!') + ); + } else { + return dispatch( + addToastWithTimeout('neutral', 'Notifications turned off') + ); + } + }) + .catch(err => { + dispatch(addToastWithTimeout('error', err.message)); + }); + }; + + const triggerChangeChannel = () => { + track(events.THREAD_MOVED_INITED, { + thread: transformations.analyticsThread(thread), + channel: transformations.analyticsChannel(thread.channel), + community: transformations.analyticsCommunity(thread.community), + }); + + dispatch(openModal('CHANGE_CHANNEL', { thread })); + }; + + const isPinned = thread.community.pinnedThreadId === thread.id; + + const [flyoutOpen, setFlyoutOpen] = useState(false); + + return ( + + + + {({ ref }) => { + return ( + + setFlyoutOpen(!flyoutOpen)} + dataCy="thread-actions-dropdown-trigger" + /> + + ); + }} + + {flyoutOpen && ( + setFlyoutOpen(false)}> + + {({ style, ref }) => { + return ( +
+ + + + + + + + + {shouldRenderEditThreadAction && ( + + + + + + + )} + + {shouldRenderPinThreadAction && ( + + + + + + + )} + + {shouldRenderMoveThreadAction && ( + + + + + + + )} + + {shouldRenderLockThreadAction && ( + + + + + + + )} + + {shouldRenderDeleteThreadAction && ( + + + + + + + )} + +
+ ); + }} +
+
+ )} +
+
+ ); +}; + +export default compose( + withCurrentUser, + connect(), + toggleThreadNotificationsMutation +)(ActionsDropdown); diff --git a/src/views/thread/components/desktopAppUpsell/index.js b/src/views/thread/components/desktopAppUpsell/index.js index ed03d9bb61..3b1df8d089 100644 --- a/src/views/thread/components/desktopAppUpsell/index.js +++ b/src/views/thread/components/desktopAppUpsell/index.js @@ -1,5 +1,7 @@ // @flow -import * as React from 'react'; +import React from 'react'; +import compose from 'recompose/compose'; +import { withCurrentUser } from 'src/components/withCurrentUser'; import { track, events } from 'src/helpers/analytics'; import { hasDismissedDesktopAppUpsell, @@ -8,15 +10,19 @@ import { DESKTOP_APP_MAC_URL, } from 'src/helpers/desktop-app-utils'; import { isMac } from 'src/helpers/is-os'; -import { OutlineButton } from 'src/components/buttons'; -import { SidebarSection } from '../../style'; -import { Container, Card, AppIcon, Content, Title, Subtitle } from './style'; +import { PrimaryOutlineButton } from 'src/components/button'; +import { SidebarSection } from 'src/views/community/style'; +import { Container, AppIcon, Content, Title, Subtitle } from './style'; + +type Props = { + currentUser: ?Object, +}; type State = { isVisible: boolean, }; -class DesktopAppUpsell extends React.Component<{}, State> { +class DesktopAppUpsell extends React.Component { constructor() { super(); @@ -38,34 +44,32 @@ class DesktopAppUpsell extends React.Component<{}, State> { download = () => { track(events.THREAD_VIEW_DOWNLOAD_MAC_CLICKED); dismissDesktopAppUpsell(); + return this.setState({ isVisible: false }); }; render() { + const { currentUser } = this.props; const { isVisible } = this.state; - if (!isVisible) return null; + if (!isVisible || !currentUser) return null; return ( - - + - - Download Spectrum for Mac - - A better way to keep up with your communities. - + + Download Spectrum for Mac + A better way to keep up with your communities. - - Download - - - + + Download + + ); } } -export default DesktopAppUpsell; +export default compose(withCurrentUser)(DesktopAppUpsell); diff --git a/src/views/thread/components/desktopAppUpsell/style.js b/src/views/thread/components/desktopAppUpsell/style.js index b6f5dc18da..289de988ed 100644 --- a/src/views/thread/components/desktopAppUpsell/style.js +++ b/src/views/thread/components/desktopAppUpsell/style.js @@ -2,15 +2,13 @@ import styled from 'styled-components'; import { theme } from 'shared/theme'; -export const Container = styled.div``; - -export const Card = styled.div` +export const Container = styled.div` padding: 16px; - background: ${theme.bg.default}; - border-radius: 4px; - display: flex; - flex-direction: column; - position: relative; + + a, + button { + width: 100%; + } `; export const AppIcon = styled.img` @@ -32,7 +30,7 @@ export const Title = styled.p` `; export const Subtitle = styled.p` - font-size: 14px; - color: ${theme.text.alt}; + font-size: 15px; + color: ${theme.text.secondary}; margin-bottom: 16px; `; diff --git a/src/views/thread/components/loading.js b/src/views/thread/components/loading.js deleted file mode 100644 index a5e57b452b..0000000000 --- a/src/views/thread/components/loading.js +++ /dev/null @@ -1,35 +0,0 @@ -// @flow -import React from 'react'; -import Titlebar from '../../../views/titlebar'; -import { LoadingThreadDetail } from '../../../components/loading'; -import Sidebar from './sidebar'; -import { - ThreadViewContainer, - ThreadContentView, - Content, - Detail, -} from '../style'; - -type PropTypes = { - threadViewContext?: 'fullscreen' | 'inbox' | 'slider', -}; -const LoadingView = ({ threadViewContext = 'fullscreen' }: PropTypes) => ( - - - {threadViewContext === 'fullscreen' && } - - - - - - - - -); - -export default LoadingView; diff --git a/src/views/thread/components/lockedMessages.js b/src/views/thread/components/lockedMessages.js new file mode 100644 index 0000000000..d0b3b79d23 --- /dev/null +++ b/src/views/thread/components/lockedMessages.js @@ -0,0 +1,13 @@ +// @flow +import React from 'react'; +import { LockedWrapper } from '../style'; + +type Props = { + children: React$Node, +}; + +const LockedMessages = ({ children }: Props) => ( + {children} +); + +export default LockedMessages; diff --git a/src/views/thread/components/messages.js b/src/views/thread/components/messages.js deleted file mode 100644 index 3d066ab32d..0000000000 --- a/src/views/thread/components/messages.js +++ /dev/null @@ -1,416 +0,0 @@ -// @flow -import * as React from 'react'; -import compose from 'recompose/compose'; -import { connect } from 'react-redux'; -import { withRouter } from 'react-router'; -import queryString from 'query-string'; -import idx from 'idx'; -import InfiniteList from 'src/components/infiniteScroll'; -import { deduplicateChildren } from 'src/components/infiniteScroll/deduplicateChildren'; -import { sortAndGroupMessages } from 'shared/clients/group-messages'; -import ChatMessages from 'src/components/messageGroup'; -import { Loading } from 'src/components/loading'; -import { Button } from 'src/components/buttons'; -import Icon from 'src/components/icons'; -import { NullState } from 'src/components/upsell'; -import viewNetworkHandler from 'src/components/viewNetworkHandler'; -import Head from 'src/components/head'; -import NextPageButton from 'src/components/nextPageButton'; -import { withCurrentUser } from 'src/components/withCurrentUser'; -import { - ChatWrapper, - NullMessagesWrapper, - NullCopy, - SocialShareWrapper, - A, -} from '../style'; -import getThreadMessages from 'shared/graphql/queries/thread/getThreadMessageConnection'; -import { ErrorBoundary } from 'src/components/error'; -import getThreadLink from 'src/helpers/get-thread-link'; -import type { GetThreadMessageConnectionType } from 'shared/graphql/queries/thread/getThreadMessageConnection'; -import type { GetThreadType } from 'shared/graphql/queries/thread/getThread'; -import { useConnectionRestored } from 'src/hooks/useConnectionRestored'; -import type { WebsocketConnectionType } from 'src/reducers/connectionStatus'; - -type State = { - subscription: ?Function, -}; - -type Props = { - isLoading: boolean, - location: Object, - forceScrollToBottom: Function, - forceScrollToTop: Function, - contextualScrollToBottom: Function, - id: string, - isFetchingMore: boolean, - loadPreviousPage: Function, - loadNextPage: Function, - scrollContainer: any, - subscribeToNewMessages: Function, - lastSeen: ?number | ?Date, - data: { - thread: GetThreadMessageConnectionType, - refetch: Function, - }, - thread: GetThreadType, - currentUser: ?Object, - hasError: boolean, - networkOnline: boolean, - websocketConnection: WebsocketConnectionType, - onMessagesLoaded?: Function, -}; - -class MessagesWithData extends React.Component { - state = { - subscription: null, - }; - - componentDidUpdate(prev: Props) { - const curr = this.props; - - const didReconnect = useConnectionRestored({ curr, prev }); - if (didReconnect && curr.data.refetch) { - curr.data.refetch(); - } - - if (!curr.data.thread) return; - - const previousMessagesHaveLoaded = - prev.data.thread && !!prev.data.thread.messageConnection; - const newMessagesHaveLoaded = - curr.data.thread && !!curr.data.thread.messageConnection; - const threadChanged = - previousMessagesHaveLoaded && - newMessagesHaveLoaded && - curr.data.thread.id !== prev.data.thread.id; - - const previousMessageCount = - previousMessagesHaveLoaded && - prev.data.thread.messageConnection.edges.length; - const previousOptimistic = - previousMessagesHaveLoaded && - prev.data.thread.messageConnection.edges.filter( - ({ node }) => node.messageType === 'optimistic' - ).length; - const newOptimistic = - newMessagesHaveLoaded && - curr.data.thread.messageConnection.edges.filter( - ({ node }) => node.messageType === 'optimistic' - ).length; - const newMessageCount = - newMessagesHaveLoaded && curr.data.thread.messageConnection.edges.length; - const newMessageSent = - previousMessageCount < newMessageCount || - previousOptimistic !== newOptimistic; - const messagesLoadedForFirstTime = !prev.data.thread && curr.data.thread; - - if ( - (messagesLoadedForFirstTime || threadChanged) && - this.props.onMessagesLoaded - ) { - this.props.onMessagesLoaded(curr.data.thread); - } - - if (messagesLoadedForFirstTime && this.shouldForceScrollToBottom()) { - setTimeout(() => curr.forceScrollToBottom()); - } - - if (threadChanged && this.shouldForceScrollToBottom()) { - setTimeout(() => curr.forceScrollToBottom()); - } - - // scroll to bottom when a message is sent in the same thread - if (newMessageSent && !prev.isFetchingMore) { - // If user sent a message themselves, force the scroll - if (previousOptimistic !== newOptimistic) { - curr.forceScrollToBottom(); - // otherwise only scroll to the bottom if they are near the bottom - } else { - curr.contextualScrollToBottom(); - } - } - - // if the thread changes in the inbox we have to update the subscription - if (threadChanged) { - // $FlowFixMe - this.unsubscribe() - .then(() => this.subscribe()) - .catch(err => console.error('Error unsubscribing: ', err)); - } - } - - componentDidMount() { - this.subscribe(); - - if ( - this.props.data && - this.props.data.thread && - this.props.onMessagesLoaded - ) { - this.props.onMessagesLoaded(this.props.data.thread); - } - - if (this.shouldForceScrollToBottom()) { - return setTimeout(() => this.props.forceScrollToBottom()); - } - } - - shouldForceScrollToBottom = () => { - const { currentUser, data, location } = this.props; - - if (!currentUser || !data.thread) return false; - - const { - currentUserLastSeen, - isAuthor, - watercooler, - messageCount, - } = data.thread; - - // Don't scroll empty threads to bottm - if (messageCount === 0) return false; - - const searchObj = queryString.parse(location.search); - const isLoadingMessageFromQueryParam = searchObj && searchObj.m; - if (isLoadingMessageFromQueryParam) return false; - - return !!(currentUserLastSeen || isAuthor || watercooler); - }; - - componentWillUnmount() { - this.unsubscribe(); - } - - subscribe = () => { - this.setState({ - subscription: this.props.subscribeToNewMessages(), - }); - }; - - unsubscribe = () => { - const { subscription } = this.state; - if (subscription) { - // This unsubscribes the subscription - return Promise.resolve(subscription()); - } - }; - - getIsAuthor = () => idx(this.props, _ => _.data.thread.isAuthor); - - getNonAuthorEmptyMessage = () => { - return ( - - - - No messages have been sent in this conversation yet—why don’t you kick - things off below? - - - ); - }; - - getAuthorEmptyMessage = () => { - const threadTitle = idx(this.props, _ => _.data.thread.content.title) || ''; - - return ( - - - - Nobody has replied yet—why don’t you share it with your friends? - - - - - - - - - - - ); - }; - - render() { - const { - data, - isLoading, - forceScrollToBottom, - id, - isFetchingMore, - loadPreviousPage, - loadNextPage, - scrollContainer, - location, - lastSeen, - thread, - currentUser, - hasError, - } = this.props; - - const hasMessagesToLoad = thread.messageCount > 0; - const { channelPermissions } = thread.channel; - const { communityPermissions } = thread.community; - const { isLocked } = thread; - const isChannelOwner = currentUser && channelPermissions.isOwner; - const isCommunityOwner = currentUser && communityPermissions.isOwner; - const isChannelModerator = currentUser && channelPermissions.isModerator; - const isCommunityModerator = - currentUser && communityPermissions.isModerator; - const isModerator = - isChannelOwner || - isCommunityOwner || - isChannelModerator || - isCommunityModerator; - - const messagesExist = - data && - data.thread && - data.thread.id === id && - data.thread.messageConnection && - data.thread.messageConnection.edges.length > 0; - - if (messagesExist) { - const { edges, pageInfo } = data.thread.messageConnection; - const unsortedMessages = - edges && edges.map(message => message && message.node); - - const uniqueMessages = deduplicateChildren(unsortedMessages, 'id'); - const sortedMessages = sortAndGroupMessages(uniqueMessages); - - const prevCursor = - edges && edges.length > 0 && edges[0] && edges[0].cursor; - const lastEdge = edges && edges.length > 0 && edges[edges.length - 1]; - const nextCursor = lastEdge && lastEdge.cursor; - - return ( - - - {pageInfo.hasPreviousPage && ( -
- - - {prevCursor && ( - - )} - - -
- )} - {pageInfo.hasNextPage && ( - - {nextCursor && ( - - )} - - - )} - } - useWindow={false} - initialLoad={false} - scrollElement={scrollContainer} - threshold={750} - className={'scroller-for-messages'} - > - - -
-
- ); - } - - if (isLoading && hasMessagesToLoad) { - return ( - - - - ); - } - - if ((isLoading && !hasMessagesToLoad) || (!isLoading && !messagesExist)) { - if (isLocked || !this.props.data.thread) return null; - - return this.getIsAuthor() - ? this.getAuthorEmptyMessage() - : this.getNonAuthorEmptyMessage(); - } - - if (hasError) { - return ( - - - - ); - } - - return null; - } -} - -const map = state => ({ - networkOnline: state.connectionStatus.networkOnline, - websocketConnection: state.connectionStatus.websocketConnection, -}); - -export default compose( - withRouter, - withCurrentUser, - getThreadMessages, - viewNetworkHandler, - // $FlowIssue - connect(map) -)(MessagesWithData); diff --git a/src/views/thread/components/messagesSubscriber.js b/src/views/thread/components/messagesSubscriber.js new file mode 100644 index 0000000000..0a6b9926be --- /dev/null +++ b/src/views/thread/components/messagesSubscriber.js @@ -0,0 +1,249 @@ +// @flow +import React from 'react'; +import { withRouter } from 'react-router-dom'; +import queryString from 'query-string'; +import compose from 'recompose/compose'; +import getThreadMessages, { + type GetThreadMessageConnectionType, +} from 'shared/graphql/queries/thread/getThreadMessageConnection'; +import { sortAndGroupMessages } from 'shared/clients/group-messages'; +import NextPageButton from 'src/components/nextPageButton'; +import viewNetworkHandler, { + type ViewNetworkHandlerType, +} from 'src/components/viewNetworkHandler'; +import ChatMessages from 'src/components/messageGroup'; +import { Loading } from 'src/components/loading'; +import NullMessages from './nullMessages'; +import type { Location } from 'react-router'; +import { NullMessagesWrapper } from '../style'; + +type Props = { + // Used by getThreadMessages query + isWatercooler: boolean, + data: { + loading: boolean, + thread: ?GetThreadMessageConnectionType, + }, + loadPreviousPage: Function, + loadNextPage: Function, + subscribeToNewMessages: Function, + onMessagesLoaded?: Function, + location: Location, + thread?: Object, + ...$Exact, +}; + +class Messages extends React.Component { + unsubscribe: Function; + + componentDidMount() { + const thread = this.props.data.thread || this.props.thread; + // Scroll to bottom on mount if we got cached data as getSnapshotBeforeUpdate does not fire for mounts + if (thread && (thread.watercooler || thread.currentUserLastSeen)) { + const elem = document.getElementById('main'); + if (!elem) return; + elem.scrollTop = elem.scrollHeight; + } + this.unsubscribe = this.props.subscribeToNewMessages(); + } + + componentWillUnmount() { + if (this.unsubscribe) this.unsubscribe(); + } + + getSnapshotBeforeUpdate(prev) { + const curr = this.props; + // First load + if ( + !prev.data.thread && + curr.data.thread && + (curr.data.thread.currentUserLastSeen || curr.data.thread.watercooler) + ) { + return { + type: 'bottom', + }; + } + // New messages + if ( + prev.data.thread && + curr.data.thread && + prev.data.thread.messageConnection.edges.length > 0 && + curr.data.thread.messageConnection.edges.length > 0 && + prev.data.thread.messageConnection.edges.length < + curr.data.thread.messageConnection.edges.length + ) { + const elem = document.getElementById('main'); + if (!elem || !curr.data.thread) return null; + + // If new messages were added at the top, persist the scroll position + if ( + prev.data.thread.messageConnection.edges[0].node.id !== + curr.data.thread.messageConnection.edges[0].node.id + ) { + return { + type: 'persist', + values: { + top: elem.scrollTop, + height: elem.scrollHeight, + }, + }; + } + + // If more than one new message was added at the bottom, stick to the current position + if ( + prev.data.thread.messageConnection.edges.length + 1 < + curr.data.thread.messageConnection.edges.length + ) { + return null; + } + + // If only one message came in and we are near the bottom when new messages come in, stick to the bottom + if (elem.scrollHeight < elem.scrollTop + elem.clientHeight + 400) { + return { + type: 'bottom', + }; + } + + // Otherwise stick to the current position + return null; + } + return null; + } + + componentDidUpdate(prevProps, __, snapshot) { + const { onMessagesLoaded } = this.props; + // after the messages load, pass it back to the thread container so that + // it can populate @ mention suggestions + const prevData = prevProps.data; + const currData = this.props.data; + const wasLoading = prevData && prevData.loading; + const hasPrevThread = prevData && prevData.thread; + const hasCurrThread = currData && currData.thread; + const previousMessageConnection = + // $FlowIssue + hasPrevThread && prevData.thread.messageConnection; + const currMessageConnection = + // $FlowIssue + hasCurrThread && currData.thread.messageConnection; + // thread loaded for the first time + if (!hasPrevThread && hasCurrThread && currMessageConnection) { + if (currMessageConnection.edges.length > 0) { + onMessagesLoaded && onMessagesLoaded(currData.thread); + } + } + // new messages arrived + if (previousMessageConnection && hasCurrThread && currMessageConnection) { + if ( + currMessageConnection.edges.length > + previousMessageConnection.edges.length + ) { + onMessagesLoaded && onMessagesLoaded(currData.thread); + } + // already loaded the thread, but was refetched + if (wasLoading && !currData.loading) { + onMessagesLoaded && onMessagesLoaded(currData.thread); + } + } + + if (snapshot) { + const elem = document.getElementById('main'); + if (!elem) return; + switch (snapshot.type) { + case 'bottom': { + elem.scrollTop = elem.scrollHeight; + return; + } + case 'persist': { + elem.scrollTop = + elem.scrollHeight - snapshot.values.height + snapshot.values.top; + return; + } + default: { + return; + } + } + } + } + + render() { + const { data, isLoading, isFetchingMore, hasError } = this.props; + + const { thread } = data; + if (thread && thread.messageConnection) { + const { messageConnection } = thread; + const { edges } = messageConnection; + + if (edges.length === 0) return ; + + const unsortedMessages = edges.map(message => message && message.node); + const sortedMessages = sortAndGroupMessages(unsortedMessages); + + if (!sortedMessages || sortedMessages.length === 0) + return ; + + return ( + + {messageConnection.pageInfo.hasPreviousPage && ( + + Show previous messages + + )} + + {messageConnection.pageInfo.hasNextPage && ( + + Show more messages + + )} + + ); + } + + if (isLoading) + return ( + + + + ); + + if (hasError) return null; + + return null; + } +} + +export default compose( + withRouter, + getThreadMessages, + viewNetworkHandler +)(Messages); diff --git a/src/views/thread/components/nullMessages.js b/src/views/thread/components/nullMessages.js new file mode 100644 index 0000000000..725f09a7a7 --- /dev/null +++ b/src/views/thread/components/nullMessages.js @@ -0,0 +1,13 @@ +// @flow +import React from 'react'; +import { NullMessagesWrapper, NullCopy, Stretch } from '../style'; + +const NullMessages = () => ( + + + No messages yet + + +); + +export default NullMessages; diff --git a/src/views/thread/components/sidebar.js b/src/views/thread/components/sidebar.js deleted file mode 100644 index e98cd4a7b9..0000000000 --- a/src/views/thread/components/sidebar.js +++ /dev/null @@ -1,244 +0,0 @@ -// @flow -import * as React from 'react'; -import replace from 'string-replace-to-array'; -import { withRouter } from 'react-router'; -import { Button, TextButton } from 'src/components/buttons'; -import type { GetThreadType } from 'shared/graphql/queries/thread/getThread'; -import { - LoadingProfileThreadDetail, - LoadingListThreadDetail, -} from 'src/components/loading'; -import ToggleCommunityMembership from 'src/components/toggleCommunityMembership'; -import { Link } from 'react-router-dom'; -import getCommunityThreads from 'shared/graphql/queries/community/getCommunityThreadConnection'; -import { connect } from 'react-redux'; -import compose from 'recompose/compose'; -import { CLIENT_URL } from 'src/api/constants'; -import Icon from 'src/components/icons'; -import getThreadLink from 'src/helpers/get-thread-link'; -import type { Dispatch } from 'redux'; -import { - SidebarSection, - SidebarSectionTitle, - SidebarSectionBody, - SidebarSectionActions, - SidebarSectionAuth, - ThreadSidebarView, - SidebarCommunityCover, - SidebarCommunityProfile, - SidebarCommunityName, - SidebarCommunityDescription, - SidebarRelatedThreadList, - SidebarRelatedThread, - RelatedTitle, - RelatedCount, - PillLink, - PillLabel, - Lock, - SidebarChannelPill, -} from '../style'; -import { ErrorBoundary } from 'src/components/error'; -import type { ContextRouter } from 'react-router'; -import DesktopAppUpsell from './desktopAppUpsell'; - -type RecommendedThread = { - node: GetThreadType, -}; -type Props = { - ...$Exact, - thread: GetThreadType, - currentUser: Object, - data: { - threads: Array, - }, - toggleCommunityMembership: Function, - dispatch: Dispatch, -}; - -const MARKDOWN_LINK = /(?:\[(.*?)\]\((.*?)\))/g; -const renderDescriptionWithLinks = text => { - return replace(text, MARKDOWN_LINK, (fullLink, text, url) => ( - - {text} - - )); -}; - -class Sidebar extends React.Component { - render() { - const { - thread, - currentUser, - data: { threads }, - } = this.props; - - const threadsToRender = - threads && - threads.length > 0 && - threads - .map(t => t.node) - .filter(t => t.id !== thread.id) - .sort((a, b) => b.messageCount - a.messageCount) - .slice(0, 5); - - const loginUrl = - thread && thread.community - ? thread.community.brandedLogin.isEnabled - ? `/${thread.community.slug}/login?r=${CLIENT_URL}${getThreadLink( - thread - )}` - : `/login?r=${CLIENT_URL}${getThreadLink(thread)}` - : '/login'; - - return ( - - - {thread && thread.community ? ( - - {!currentUser && ( - - - Join the conversation - - - Sign in to join this conversation, and others like it, in - the communities you care about. - - - - - - - - Log in - - - - - )} - - - - - - - {thread.community.name} - - - - - - {thread.channel.isPrivate && ( - - - - )} - - {thread.channel.name} - - - - - - {renderDescriptionWithLinks(thread.community.description)} - - - - {thread.community.communityPermissions.isMember ? ( - - - View community - - - ) : currentUser ? ( - ( - - )} - /> - ) : ( - - - - )} - - - - ) : ( - - - - )} - - - - - - {Array.isArray(threadsToRender) ? ( - threadsToRender.length > 0 && ( - - - More active conversations - - - {threadsToRender.map(t => { - return ( - - - - {t.content.title} - - {t.messageCount.toLocaleString()}{' '} - {t.messageCount === 1 ? 'message' : 'messages'} - - - - - ); - })} - - - ) - ) : ( - - - - )} - - - ); - } -} - -export default compose( - connect(), - withRouter, - getCommunityThreads -)(Sidebar); diff --git a/src/views/thread/components/stickyHeader.js b/src/views/thread/components/stickyHeader.js new file mode 100644 index 0000000000..2040b420ec --- /dev/null +++ b/src/views/thread/components/stickyHeader.js @@ -0,0 +1,62 @@ +// @flow +import * as React from 'react'; +import { connect } from 'react-redux'; +import compose from 'recompose/compose'; +import { Link } from 'react-router-dom'; +import { UserHoverProfile } from 'src/components/hoverProfile'; +import { UserAvatar } from 'src/components/avatar'; +import { LikeButton } from 'src/components/threadLikes'; +import { convertTimestampToDate } from 'shared/time-formatting'; +import type { GetThreadType } from 'shared/graphql/queries/thread/getThread'; +import getThreadLink from 'src/helpers/get-thread-link'; +import { useAppScroller } from 'src/hooks/useAppScroller'; +import { + StickyHeaderContent, + CommunityHeaderName, + CommunityHeaderMeta, + CommunityHeaderSubtitle, + CommunityHeaderMetaCol, + StickyHeaderContainer, + StickyHeaderActionsContainer, +} from '../style'; + +type Props = { + thread: GetThreadType, +}; + +const StickyHeader = (props: Props) => { + const { thread } = props; + const { scrollToTop } = useAppScroller(); + const { channel } = thread; + + const createdAt = new Date(thread.createdAt).getTime(); + const timestamp = convertTimestampToDate(createdAt); + + return ( + + + + + + {thread.content.title} + + {timestamp} + + + + + + {channel.channelPermissions.isMember && ( + + + + )} + + ); +}; + +export default compose(connect())(StickyHeader); diff --git a/src/views/thread/components/threadByline.js b/src/views/thread/components/threadByline.js index 3c8ec3b434..a58488bac5 100644 --- a/src/views/thread/components/threadByline.js +++ b/src/views/thread/components/threadByline.js @@ -49,6 +49,7 @@ class ThreadByline extends React.Component { )} + {/* $FlowIssue */} {reputation > 0 && } diff --git a/src/views/thread/components/threadCommunityBanner.js b/src/views/thread/components/threadCommunityBanner.js deleted file mode 100644 index 1f8124a92f..0000000000 --- a/src/views/thread/components/threadCommunityBanner.js +++ /dev/null @@ -1,179 +0,0 @@ -// @flow -import * as React from 'react'; -import { connect } from 'react-redux'; -import compose from 'recompose/compose'; -import { Link } from 'react-router-dom'; -import { - CommunityHoverProfile, - ChannelHoverProfile, -} from 'src/components/hoverProfile'; -import { LikeButton } from 'src/components/threadLikes'; -import { convertTimestampToDate } from 'shared/time-formatting'; -import { Button } from 'src/components/buttons'; -import toggleChannelSubscriptionMutation from 'shared/graphql/mutations/channel/toggleChannelSubscription'; -import type { GetThreadType } from 'shared/graphql/queries/thread/getThread'; -import { addToastWithTimeout } from 'src/actions/toasts'; -import { CommunityAvatar } from 'src/components/avatar'; -import { CLIENT_URL } from 'src/api/constants'; -import getThreadLink from 'src/helpers/get-thread-link'; -import type { Dispatch } from 'redux'; -import { withCurrentUser } from 'src/components/withCurrentUser'; -import { - CommunityHeader, - CommunityHeaderName, - CommunityHeaderMeta, - CommunityHeaderSubtitle, - CommunityHeaderMetaCol, - AnimatedContainer, -} from '../style'; - -type Props = { - dispatch: Dispatch, - toggleChannelSubscription: Function, - currentUser: Object, - hide: boolean, - watercooler: boolean, - thread: GetThreadType, - isVisible: boolean, - forceScrollToTop: Function, -}; -type State = { - isLoading: boolean, -}; -class ThreadCommunityBanner extends React.Component { - constructor() { - super(); - - this.state = { isLoading: false }; - } - - joinChannel = () => { - const { - thread: { channel }, - dispatch, - toggleChannelSubscription, - } = this.props; - - this.setState({ - isLoading: true, - }); - - toggleChannelSubscription({ channelId: channel.id }) - .then(({ data: { toggleChannelSubscription } }) => { - this.setState({ - isLoading: false, - }); - - const { - isMember, - isPending, - } = toggleChannelSubscription.channelPermissions; - - let str = ''; - if (isPending) { - str = `Your request to join the ${ - toggleChannelSubscription.name - } channel in ${ - toggleChannelSubscription.community.name - } has been sent.`; - } - - if (!isPending && isMember) { - str = `Joined the ${ - toggleChannelSubscription.community.name - } community!`; - } - - if (!isPending && !isMember) { - str = `Left the channel ${toggleChannelSubscription.name} in ${ - toggleChannelSubscription.community.name - }.`; - } - - const type = isMember || isPending ? 'success' : 'neutral'; - return dispatch(addToastWithTimeout(type, str)); - }) - .catch(err => { - this.setState({ - isLoading: false, - }); - dispatch(addToastWithTimeout('error', err.message)); - }); - }; - - render() { - const { - thread: { channel, community, watercooler }, - thread, - currentUser, - isVisible, - forceScrollToTop, - } = this.props; - const { isLoading } = this.state; - - const loginUrl = community.brandedLogin.isEnabled - ? `/${community.slug}/login?r=${CLIENT_URL}${getThreadLink(thread)}` - : `/login?r=${CLIENT_URL}${getThreadLink(thread)}`; - - const createdAt = new Date(thread.createdAt).getTime(); - const timestamp = convertTimestampToDate(createdAt); - - return ( - - - - - - - {watercooler - ? `${community.name} watercooler` - : thread.content.title} - - - - {community.name} - - / - - - {channel.name} - - - -   - {`· ${timestamp}`} - - - - - - {channel.channelPermissions.isMember ? ( - - ) : currentUser ? ( - - ) : ( - - - - )} - - - ); - } -} -export default compose( - withCurrentUser, - toggleChannelSubscriptionMutation, - connect() -)(ThreadCommunityBanner); diff --git a/src/views/thread/components/threadDetail.js b/src/views/thread/components/threadDetail.js index 48bf01a57d..6a52b43f71 100644 --- a/src/views/thread/components/threadDetail.js +++ b/src/views/thread/components/threadDetail.js @@ -9,7 +9,6 @@ import { convertTimestampToDate } from 'shared/time-formatting'; import { openModal } from 'src/actions/modals'; import { addToastWithTimeout } from 'src/actions/toasts'; import setThreadLockMutation from 'shared/graphql/mutations/thread/lockThread'; -import ThreadByline from './threadByline'; import deleteThreadMutation from 'shared/graphql/mutations/thread/deleteThread'; import editThreadMutation from 'shared/graphql/mutations/thread/editThread'; import pinThreadMutation from 'shared/graphql/mutations/community/pinCommunityThread'; @@ -17,19 +16,15 @@ import uploadImageMutation from 'shared/graphql/mutations/uploadImage'; import type { GetThreadType } from 'shared/graphql/queries/thread/getThread'; import ThreadRenderer from 'src/components/threadRenderer'; import ActionBar from './actionBar'; -import ConditionalWrap from 'src/components/conditionalWrap'; import ThreadEditInputs from 'src/components/composer/inputs'; import { withCurrentUser } from 'src/components/withCurrentUser'; -import { - UserHoverProfile, - CommunityHoverProfile, - ChannelHoverProfile, -} from 'src/components/hoverProfile'; +import { UserListItem } from 'src/components/entities'; import { ThreadWrapper, ThreadContent, ThreadHeading, ThreadSubtitle, + BylineContainer, } from '../style'; import { track, events, transformations } from 'src/helpers/analytics'; import getThreadLink from 'src/helpers/get-thread-link'; @@ -59,7 +54,7 @@ type Props = { currentUser: ?Object, toggleEdit: Function, uploadImage: Function, - innerRef?: any, + ref?: any, }; class ThreadDetailPure extends React.Component { @@ -77,7 +72,7 @@ class ThreadDetailPure extends React.Component { }; bodyEditor: any; - titleTextarea: React.Node; + titleTextarea: React$Node; componentWillMount() { this.setThreadState(); @@ -218,10 +213,10 @@ class ThreadDetailPure extends React.Component { isEditing, body: '', }); - this.props.toggleEdit(); + this.props.toggleEdit && this.props.toggleEdit(); }); - this.props.toggleEdit(); + this.props.toggleEdit && this.props.toggleEdit(); if (!isEditing) { track(events.THREAD_EDITED_INITED, { @@ -415,13 +410,14 @@ class ThreadDetailPure extends React.Component { const createdAt = new Date(thread.createdAt).getTime(); const timestamp = convertTimestampToDate(createdAt); + const { author } = thread; const editedTimestamp = thread.modifiedAt ? new Date(thread.modifiedAt).getTime() : null; return ( - + {isEditing ? ( { /> ) : ( - {/* $FlowFixMe */} - - ( - - - - )} - > - - - + + + + +
{thread.content.title} - - - {thread.community.name} - - -  /  - - - {thread.channel.name} - - -  ·  {timestamp} {thread.modifiedAt && ( @@ -476,6 +467,9 @@ class ThreadDetailPure extends React.Component { Date.now(), editedTimestamp ).toLowerCase()} + {thread.editedBy && + thread.editedBy.user.id !== thread.author.user.id && + ` by @${thread.editedBy.user.username}`} ) )} @@ -487,7 +481,7 @@ class ThreadDetailPure extends React.Component { )} - + { + const { thread } = props; + const { + metaImage, + type, + community, + content, + createdAt, + modifiedAt, + author, + } = thread; + const { title, description } = generateMetaInfo({ + type: 'thread', + data: { + title: content.title, + body: content.body, + type: type, + communityName: community.name, + }, + }); + + return ( + + + {metaImage && } + + + + + + ); +}; + +export default ThreadHead; diff --git a/src/views/thread/components/topBottomButtons.js b/src/views/thread/components/topBottomButtons.js new file mode 100644 index 0000000000..9284c869a2 --- /dev/null +++ b/src/views/thread/components/topBottomButtons.js @@ -0,0 +1,37 @@ +// @flow +import React, { useEffect, useState } from 'react'; +import { useAppScroller } from 'src/hooks/useAppScroller'; +import Icon from 'src/components/icon'; +import { ErrorBoundary } from 'src/components/error'; +import { TopBottomButtonContainer, TopButton, BottomButton } from '../style'; + +const TopBottomButtons = () => { + const [isVisible, setIsVisible] = useState(true); + const { scrollToTop, scrollToBottom, ref } = useAppScroller(); + + useEffect(() => { + if (ref) { + // hide on threads that don't scroll + if (ref.scrollHeight > ref.clientHeight + 100) { + setIsVisible(true); + } else { + setIsVisible(false); + } + } + }); + + return ( + + + + + + + + + + + ); +}; + +export default TopBottomButtons; diff --git a/src/views/thread/components/trendingThreads.js b/src/views/thread/components/trendingThreads.js new file mode 100644 index 0000000000..ad9d86ded1 --- /dev/null +++ b/src/views/thread/components/trendingThreads.js @@ -0,0 +1,140 @@ +// @flow +import React from 'react'; +import styled from 'styled-components'; +import { Link } from 'react-router-dom'; +import { Query } from 'react-apollo'; +import { getCommunityThreadConnectionQuery } from 'shared/graphql/queries/community/getCommunityThreadConnection'; +import { Container } from './desktopAppUpsell/style'; +import { ErrorBoundary } from 'src/components/error'; +import getThreadLink from 'src/helpers/get-thread-link'; +import theme from 'shared/theme'; +import { Truncate } from 'src/components/globals'; +import truncate from 'shared/truncate'; +import { SidebarSectionHeading } from 'src/views/community/style'; +import type { ThreadInfoType } from 'shared/graphql/fragments/thread/threadInfo'; +import { timeDifferenceShort } from 'shared/time-difference'; +import { Loading } from 'src/components/loading'; + +const ThreadListItemContainer = styled(Link)` + display: block; + padding: 12px 16px; + border-bottom: 1px solid ${theme.bg.divider}; + + &:hover { + background-color: ${theme.bg.wash}; + } + + &:last-of-type { + border-bottom: 0; + padding-bottom: 16px; + } +`; + +const ThreadContent = styled.div` + display: flex; + flex-direction: column; +`; + +const ThreadTitle = styled.div` + font-size: 15px; + font-weight: 500; + ${Truncate}; +`; + +const ThreadMeta = styled.div` + font-size: 15px; + font-weight: 400; + color: ${theme.text.alt}; + line-height: 1.2; + margin-top: 2px; +`; + +type ThreadListItemProps = { + thread: ThreadInfoType, +}; + +const ThreadListItem = (props: ThreadListItemProps) => { + const { thread } = props; + const { lastActive, createdAt, content } = thread; + + const now = new Date().getTime(); + const then = lastActive || createdAt; + let timestamp = timeDifferenceShort(now, new Date(then).getTime()); + + return ( + + + + {truncate(content.title, 80)} + + + @{thread.author.user.username} · {timestamp} + + + + ); +}; + +type Props = { + id: string, +}; + +const TrendingThreads = (props: Props) => { + return ( + + {({ data, loading }) => { + if (data.community && data.community.threadConnection) { + const threads = data.community.threadConnection.edges + .map( + ({ node }) => node + // Only show five other trending threads + ) + // Don't show watercoolers + .filter(thread => !thread.watercooler) + .slice(0, 5); + if (threads.length === 0) return null; + return ( + + + + Trending conversations + + + {threads.map(thread => ( + + + + ))} + + ); + } + + if (loading) { + return ( + + + + Trending conversations + + + + + ); + } + + return null; + }} + + ); +}; + +export default TrendingThreads; diff --git a/src/views/thread/components/watercoolerActionBar.js b/src/views/thread/components/watercoolerActionBar.js deleted file mode 100644 index c5753fc3b7..0000000000 --- a/src/views/thread/components/watercoolerActionBar.js +++ /dev/null @@ -1,167 +0,0 @@ -// @flow -import * as React from 'react'; -import { connect } from 'react-redux'; -import Clipboard from 'react-clipboard.js'; -import { addToastWithTimeout } from '../../../actions/toasts'; -import Icon from '../../../components/icons'; -import compose from 'recompose/compose'; -import type { GetThreadType } from 'shared/graphql/queries/thread/getThread'; -import toggleThreadNotificationsMutation from 'shared/graphql/mutations/thread/toggleThreadNotifications'; -import type { Dispatch } from 'redux'; -import { LikeButton } from 'src/components/threadLikes'; -import getThreadLink from 'src/helpers/get-thread-link'; -import { - FollowButton, - ShareButtons, - ShareButton, - WatercoolerActionBarContainer, -} from '../style'; - -type Props = { - thread: GetThreadType, - currentUser: Object, - dispatch: Dispatch, - toggleThreadNotifications: Function, -}; - -type State = { - notificationStateLoading: boolean, -}; - -class WatercoolerActionBar extends React.Component { - state = { notificationStateLoading: false }; - - toggleNotification = () => { - const { thread, dispatch, toggleThreadNotifications } = this.props; - const threadId = thread.id; - - this.setState({ - notificationStateLoading: true, - }); - - toggleThreadNotifications({ - threadId, - }) - .then(({ data: { toggleThreadNotifications } }) => { - this.setState({ - notificationStateLoading: false, - }); - - if (toggleThreadNotifications.receiveNotifications) { - return dispatch( - addToastWithTimeout('success', 'Notifications activated!') - ); - } else { - return dispatch( - addToastWithTimeout('neutral', 'Notifications turned off') - ); - } - }) - .catch(err => { - this.setState({ - notificationStateLoading: true, - }); - dispatch(addToastWithTimeout('error', err.message)); - }); - }; - - render() { - const { thread, currentUser } = this.props; - const { notificationStateLoading } = this.state; - - return ( - -
- - - {!thread.channel.isPrivate && ( - - - - - - - - - - - - - - - this.props.dispatch( - addToastWithTimeout('success', 'Copied to clipboard') - ) - } - > - - - - - - - - )} -
- - {currentUser && ( - - {thread.receiveNotifications ? 'Subscribed' : 'Get notifications'} - - )} -
- ); - } -} - -export default compose( - connect(), - toggleThreadNotificationsMutation -)(WatercoolerActionBar); diff --git a/src/views/thread/container.js b/src/views/thread/container.js deleted file mode 100644 index 57bfb10114..0000000000 --- a/src/views/thread/container.js +++ /dev/null @@ -1,622 +0,0 @@ -// @flow -import * as React from 'react'; -import ReactDOM from 'react-dom'; -import compose from 'recompose/compose'; -import { connect } from 'react-redux'; -import { withApollo } from 'react-apollo'; -import idx from 'idx'; -import slugg from 'slugg'; -import generateMetaInfo from 'shared/generate-meta-info'; -import { addCommunityToOnboarding } from '../../actions/newUserOnboarding'; -import Titlebar from 'src/views/titlebar'; -import ThreadDetail from './components/threadDetail'; -import Messages from './components/messages'; -import Head from 'src/components/head'; -import ChatInput from 'src/components/chatInput'; -import ViewError from 'src/components/viewError'; -import { Link } from 'react-router-dom'; -import viewNetworkHandler from 'src/components/viewNetworkHandler'; -import { withCurrentUser } from 'src/components/withCurrentUser'; -import { - getThreadByMatch, - getThreadByMatchQuery, -} from 'shared/graphql/queries/thread/getThread'; -import { NullState } from 'src/components/upsell'; -import JoinChannel from 'src/components/upsell/joinChannel'; -import LoadingThread from './components/loading'; -import ThreadCommunityBanner from './components/threadCommunityBanner'; -import Sidebar from './components/sidebar'; -import type { GetThreadType } from 'shared/graphql/queries/thread/getThread'; -import type { Dispatch } from 'redux'; -import { - ThreadViewContainer, - ThreadContentView, - Content, - Input, - Detail, - ChatInputWrapper, - WatercoolerDescription, - WatercoolerIntroContainer, - WatercoolerTitle, -} from './style'; -import { CommunityAvatar } from 'src/components/avatar'; -import WatercoolerActionBar from './components/watercoolerActionBar'; -import { ErrorBoundary } from 'src/components/error'; -import getThreadLink from 'src/helpers/get-thread-link'; - -type Props = { - data: { - thread: GetThreadType, - refetch: Function, - }, - isLoading: boolean, - hasError: boolean, - currentUser: Object, - dispatch: Dispatch, - slider: boolean, - // If this is undefined the thread is being viewed in fullscreen - threadViewContext?: 'slider' | 'inbox', - threadSliderIsOpen: boolean, - client: Object, -}; - -type State = { - scrollElement: any, - isEditing: boolean, - messagesContainer: any, - // Cache lastSeen so it doesn't jump around - // while looking at a live thread - lastSeen: ?number | ?string, - bannerIsVisible: boolean, - derivedState: Object, - // set as a callback when messages are loaded for a thread. the participants - // array is used to pre-populate @mention suggestions - participants: Array, -}; - -class ThreadContainer extends React.Component { - chatInput: any; - - // used to keep track of the height of the thread content element to determine - // whether or not to show the contextual thread header banner - threadDetailElem: any; - threadDetailElem = null; - - constructor(props) { - super(props); - - this.state = { - messagesContainer: null, - scrollElement: null, - isEditing: false, - lastSeen: null, - bannerIsVisible: false, - scrollOffset: 0, - derivedState: {}, - participants: [], - }; - } - - // to compare nextProps to a previous derivedState, we need to store - // change in local state - // see how to do this here: https://github.com/reactjs/rfcs/blob/master/text/0006-static-lifecycle-methods.md#state-derived-from-propsstate - // with implementation below - static getDerivedStateFromProps(nextProps, prevState) { - const lastSeen = idx(nextProps, _ => _.data.thread.currentUserLastSeen); - if (lastSeen === prevState.lastSeen) return null; - - return { - lastSeen, - }; - } - - toggleEdit = () => { - const { isEditing } = this.state; - this.setState({ - isEditing: !isEditing, - }); - }; - - setMessagesContainer = elem => { - if (this.state.messagesContainer) return; - this.setState({ - messagesContainer: elem, - }); - }; - - // Locally update thread.currentUserLastSeen - updateThreadLastSeen = threadId => { - const { currentUser, client } = this.props; - // No currentUser, no reason to update currentUserLastSeen - if (!currentUser || !threadId) return; - try { - const threadData = client.readQuery({ - query: getThreadByMatchQuery, - variables: { - id: threadId, - }, - }); - - client.writeQuery({ - query: getThreadByMatchQuery, - variables: { - id: threadId, - }, - data: { - ...threadData, - thread: { - ...threadData.thread, - currentUserLastSeen: new Date(), - __typename: 'Thread', - }, - }, - }); - } catch (err) { - // Errors that happen with this shouldn't crash the app - console.error(err); - } - }; - - componentDidMount() { - const elem = document.getElementById('scroller-for-inbox-thread-view'); - this.setState({ - // NOTE(@mxstbr): This is super un-reacty but it works. This refers to - // the AppViewWrapper which is the scrolling part of the site. - scrollElement: elem, - }); - } - - handleScroll = e => { - if (!e || !e.target) return; - - if (e && e.persist) { - e.persist(); - } - - // whenever the user scrolls in the thread we determine if they've scrolled - // past the thread content section - once they've scroll passed it, we - // enable the `bannerIsVisible` state to slide the thread context banner - // in from the top of the screen - const scrollOffset = e.target.scrollTop; - try { - const threadDetail = ReactDOM.findDOMNode(this.threadDetailElem); - if (!threadDetail) return; - - const { - height: threadDetailHeight, - // $FlowFixMe - } = threadDetail.getBoundingClientRect(); - const bannerShouldBeVisible = scrollOffset > threadDetailHeight; - if (bannerShouldBeVisible !== this.state.bannerIsVisible) { - this.setState({ - bannerIsVisible: bannerShouldBeVisible, - }); - } - } catch (err) { - // if theres an error finding the dom node, we should make sure - // the banner is not visible to avoid it accidentally covering content - this.setState({ - bannerIsVisible: false, - }); - } - }; - - componentDidUpdate(prevProps) { - // If we're loading the thread for the first time make sure the URL is the right one, and if not - // redirect to the right one - if ( - !this.props.threadViewContext && - (!prevProps.data || - !prevProps.data.thread || - !prevProps.data.thread.id) && - this.props.data && - this.props.data.thread && - this.props.data.thread.id - ) { - const { thread } = this.props.data; - const properUrl = `/${thread.community.slug}/${ - thread.channel.slug - }/${slugg(thread.content.title)}~${thread.id}`; - // $FlowFixMe - if (this.props.location.pathname !== properUrl) - // $FlowFixMe - return this.props.history.replace(properUrl); - } - - // if the user is in the inbox and changes threads, it should initially scroll - // to the top before continuing with logic to force scroll to the bottom - if ( - prevProps.data && - prevProps.data.thread && - this.props.data && - this.props.data.thread && - prevProps.data.thread.id !== this.props.data.thread.id - ) { - // if the user is new and signed up through a thread view, push - // the thread's community data into the store to hydrate the new user experience - // with their first community they should join - this.props.dispatch( - addCommunityToOnboarding(this.props.data.thread.community) - ); - - // Update thread.currentUserLastSeen for the last thread when we switch away from it - if (prevProps.threadId) { - this.updateThreadLastSeen(prevProps.threadId); - } - this.forceScrollToTop(); - } - - // we never autofocus on mobile - if (window && window.innerWidth < 768) return; - - const { - currentUser, - data: { thread }, - threadSliderIsOpen, - } = this.props; - - // if no thread has been returned yet from the query, we don't know whether or not to focus yet - if (!thread) return; - - // only when the thread has been returned for the first time should evaluate whether or not to focus the chat input - if (prevProps.data.thread && prevProps.data.thread.id === thread.id) return; - - const threadAndUser = currentUser && thread; - if (threadAndUser && this.chatInput) { - // if the user is viewing the inbox, opens the thread slider, and then closes it again, refocus the inbox inpu - if (prevProps.threadSliderIsOpen && !threadSliderIsOpen) { - return this.chatInput.focus(); - } - - // if the thread slider is open while in the inbox, don't focus in the inbox - if (threadSliderIsOpen) return; - - return this.chatInput.focus(); - } - } - - forceScrollToTop = () => { - const { messagesContainer } = this.state; - if (!messagesContainer) return; - messagesContainer.scrollTop = 0; - }; - - forceScrollToBottom = () => { - const { messagesContainer } = this.state; - if (!messagesContainer) return; - const node = messagesContainer; - node.scrollTop = node.scrollHeight - node.clientHeight; - }; - - contextualScrollToBottom = () => { - const { messagesContainer } = this.state; - if (!messagesContainer) return; - const node = messagesContainer; - if (node.scrollHeight - node.clientHeight < node.scrollTop + 280) { - node.scrollTop = node.scrollHeight - node.clientHeight; - } - }; - - renderChatInputOrUpsell = () => { - const { isEditing, participants } = this.state; - const { - data: { thread }, - currentUser, - } = this.props; - - if (!thread) return null; - if (thread.isLocked) return null; - if (isEditing) return null; - if (thread.channel.isArchived) return null; - - const { channelPermissions } = thread.channel; - const { communityPermissions } = thread.community; - - const isBlockedInChannelOrCommunity = - channelPermissions.isBlocked || communityPermissions.isBlocked; - - if (isBlockedInChannelOrCommunity) return null; - - const chatInputComponent = ( - - - (this.chatInput = chatInput)} - refetchThread={this.props.data.refetch} - participants={participants} - /> - - - ); - - const joinLoginUpsell = ( - - ); - - if (!currentUser || !currentUser.id) { - return joinLoginUpsell; - } - - if (channelPermissions.isMember) { - return chatInputComponent; - } - - return joinLoginUpsell; - }; - - renderPost = () => { - const { - data: { thread }, - slider, - currentUser, - } = this.props; - if (!thread || !thread.id) return null; - - if (thread.watercooler) { - return ( - - (this.threadDetailElem = c)} - > - - - - - The {thread.community.name} watercooler - - - - Welcome to the {thread.community.name} watercooler, a space for - general chat with everyone in the community. Jump in to the - conversation below or introduce yourself! - - - - - - ); - } - - return ( - - (this.threadDetailElem = c)} - /> - - ); - }; - - updateThreadParticipants = thread => { - const { messageConnection, author } = thread; - - if (!messageConnection || messageConnection.edges.length === 0) - return this.setState({ - participants: [author.user], - }); - - const participants = messageConnection.edges - .map(edge => edge.node) - .map(node => node.author.user); - - const participantsWithAuthor = [...participants, author.user].filter( - (user, index, array) => array.indexOf(user) === index - ); - - return this.setState({ participants: participantsWithAuthor }); - }; - - render() { - const { - data: { thread }, - currentUser, - isLoading, - hasError, - slider, - threadViewContext = 'fullscreen', - } = this.props; - const { isEditing, lastSeen } = this.state; - - if (thread && thread.id) { - // successful network request to get a thread - const { title, description } = generateMetaInfo({ - type: 'thread', - data: { - title: thread.content.title, - body: thread.content.body, - type: thread.type, - communityName: thread.community.name, - }, - }); - - // get the data we need to render the view - const { channelPermissions } = thread.channel; - const { communityPermissions } = thread.community; - const { isLocked } = thread; - const shouldRenderThreadSidebar = threadViewContext === 'fullscreen'; - - if (channelPermissions.isBlocked || communityPermissions.isBlocked) { - return ( - - - - - - - - ); - } - - const isWatercooler = thread.watercooler; - const headTitle = isWatercooler - ? `The Watercooler · ${thread.community.name}` - : title; - const headDescription = isWatercooler - ? `Watercooler chat for the ${thread.community.name} community` - : description; - const metaImage = thread.metaImage; - - return ( - - - {shouldRenderThreadSidebar && ( - - )} - - - - - {metaImage && ( - - )} - - - - - - - - - - - - {this.renderPost()} - - {!isEditing && ( - - )} - - {!isEditing && isLocked && ( - - )} - - - - {this.renderChatInputOrUpsell()} - - - - ); - } - - if (isLoading) { - return ; - } - - return ( - - - - - - - - ); - } -} - -export default compose( - withCurrentUser, - getThreadByMatch, - viewNetworkHandler, - withApollo, - connect() -)(ThreadContainer); diff --git a/src/views/thread/container/index.js b/src/views/thread/container/index.js new file mode 100644 index 0000000000..a8c1de9df5 --- /dev/null +++ b/src/views/thread/container/index.js @@ -0,0 +1,315 @@ +// @flow +import React, { useEffect, useState } from 'react'; +import compose from 'recompose/compose'; +import { connect } from 'react-redux'; +import { withRouter } from 'react-router-dom'; +import { withApollo } from 'react-apollo'; +import { + getThreadByMatch, + getThreadByMatchQuery, +} from 'shared/graphql/queries/thread/getThread'; +import { markSingleNotificationSeenMutation } from 'shared/graphql/mutations/notification/markSingleNotificationSeen'; +import { withCurrentUser } from 'src/components/withCurrentUser'; +import viewNetworkHandler, { + type ViewNetworkHandlerType, +} from 'src/components/viewNetworkHandler'; +import { LoadingView, ErrorView } from 'src/views/viewHelpers'; +import JoinCommunity from 'src/components/joinCommunityWrapper'; +import Icon from 'src/components/icon'; +import { PrimaryOutlineButton } from 'src/components/button'; +import { + ViewGrid, + SecondaryPrimaryColumnGrid, + PrimaryColumn, + SecondaryColumn, +} from 'src/components/layout'; +import { + CommunityProfileCard, + ChannelProfileCard, +} from 'src/components/entities'; +import { SidebarSection } from 'src/views/community/style'; +import ChatInput from 'src/components/chatInput'; +import { setTitlebarProps } from 'src/actions/titlebar'; +import MessagesSubscriber from '../components/messagesSubscriber'; +import TrendingThreads from '../components/trendingThreads'; +import StickyHeader from '../components/stickyHeader'; +import ThreadDetail from '../components/threadDetail'; +import ThreadHead from '../components/threadHead'; +import LockedMessages from '../components/lockedMessages'; +import DesktopAppUpsell from '../components/desktopAppUpsell'; +import TopBottomButtons from '../components/topBottomButtons'; +import { ChatInputWrapper } from 'src/components/layout'; +import { Stretch, LockedText } from '../style'; +import { deduplicateChildren } from 'src/components/infiniteScroll/deduplicateChildren'; +import type { GetThreadType } from 'shared/graphql/queries/thread/getThread'; +import { ErrorBoundary } from 'src/components/error'; + +type Props = { + ...$Exact, + data: { + thread: GetThreadType, + }, + client: Object, + className?: string, + currentUser?: Object, + dispatch: Function, + notifications: Array, + isModal: boolean, + children: React$Node, +}; + +const ThreadContainer = (props: Props) => { + const { + data, + isLoading, + children, + client, + currentUser, + dispatch, + className, + isModal = false, + } = props; + + if (isLoading) return ; + + const { thread } = data; + if (!thread) return ; + + const { id } = thread; + + /* + update the last seen timestamp of the current thread whenever it first + loads, as well as when it unmounts as the user closes the thread. This + should provide the effect of locally marking the thread as "seen" while + athena handles storing the actual lastSeen timestamp update in the background + asynchronously. + */ + const updateThreadLastSeen = () => { + if (!currentUser || !thread) return; + try { + const threadData = client.readQuery({ + query: getThreadByMatchQuery, + variables: { + id, + }, + }); + + client.writeQuery({ + query: getThreadByMatchQuery, + variables: { + id, + }, + data: { + ...threadData, + thread: { + ...threadData.thread, + currentUserLastSeen: new Date(), + __typename: 'Thread', + }, + }, + }); + } catch (err) { + // Errors that happen with this shouldn't crash the app + console.error(err); + } + }; + + const [mentionSuggestions, setMentionSuggestions] = useState([ + thread.author.user, + ]); + const [isEditing, setEditing] = useState(false); + const updateMentionSuggestions = (thread: GetThreadType) => { + const { messageConnection, author } = thread; + + if (!messageConnection || messageConnection.edges.length === 0) + return setMentionSuggestions([author.user]); + + const participants = messageConnection.edges + .map(edge => edge.node) + .map(node => node.author.user); + + const participantsWithAuthor = [...participants, author.user]; + const filtered = deduplicateChildren(participantsWithAuthor, 'id'); + return setMentionSuggestions(filtered); + }; + + const markCurrentThreadNotificationsAsSeen = () => { + if (!currentUser || !thread) return; + try { + props.notifications.forEach(notification => { + if (notification.isSeen) return; + + const notificationContextIds = + notification.type === 'THREAD_CREATED' + ? notification.entities.map(entity => entity.id) + : [notification.context.id]; + + if (notificationContextIds.indexOf(id) === -1) return; + + props.client.mutate({ + mutation: markSingleNotificationSeenMutation, + variables: { + id: notification.id, + }, + }); + }); + } catch (err) { + console.error(err); + } + }; + + useEffect(() => { + markCurrentThreadNotificationsAsSeen(); + }, [id, props.notifications.length]); + + useEffect(() => { + updateThreadLastSeen(); + return () => updateThreadLastSeen(); + }, [id]); + + useEffect(() => { + dispatch( + setTitlebarProps({ + title: 'Conversation', + leftAction: 'view-back', + }) + ); + }, []); + + const { community, channel, isLocked } = thread; + const { communityPermissions } = community; + const { isMember } = communityPermissions; + const canChat = !isLocked && !channel.isArchived && isMember; + + return ( + + + + + {children} + + + + + + + + + + + + + + + + + + + + + + + + + {/* + This container makes sure that the thread detail and messages + component are always at least the height of the screen, minus the + height of the chat input. This is necessary because we always want + the chat input at the bottom of the view, so it must always be tricked + into thinking that its preceeding sibling is full-height. + */} + + + + + + setEditing(!isEditing)} + /> + + {!isEditing && ( + + + + {canChat && ( + + + + )} + + {!canChat && !isLocked && ( + + ( + + + {isLoading + ? 'Joining...' + : 'Join community to chat'} + + + )} + /> + + )} + + {isLocked && ( + + + + + This conversation has been locked + + + + )} + + {channel.isArchived && ( + + + + This channel has been archived + + + )} + + )} + + + + + + ); +}; + +const mapStateToProps = (state): * => ({ + notifications: state.notifications.notificationsData, +}); + +export default compose( + getThreadByMatch, + viewNetworkHandler, + withRouter, + withApollo, + withCurrentUser, + connect(mapStateToProps) +)(ThreadContainer); diff --git a/src/views/thread/index.js b/src/views/thread/index.js index 0bdd270b6f..15424698ff 100644 --- a/src/views/thread/index.js +++ b/src/views/thread/index.js @@ -1,42 +1,22 @@ // @flow import React from 'react'; import Loadable from 'react-loadable'; -import LoadingThread from './components/loading'; -import ViewError from 'src/components/viewError'; +import { ErrorView, LoadingView } from 'src/views/viewHelpers'; /* prettier-ignore */ const loader = () => import('./container'/* webpackChunkName: "Thread" */); -const getLoading = (threadViewContext: 'fullscreen' | 'inbox' | 'slider') => ({ - error, - pastDelay, -}) => { +const getLoading = () => ({ error, pastDelay }) => { if (error) { - console.error(error); - return ( - - ); + return ; } else if (pastDelay) { - return ; + return ; } return null; }; -export const InboxThreadView = Loadable({ +export const ThreadView = Loadable({ loader, - loading: getLoading('inbox'), -}); - -export const SliderThreadView = Loadable({ - loader, - loading: getLoading('slider'), -}); - -export const FullscreenThreadView = Loadable({ - loader, - loading: getLoading('fullscreen'), + loading: getLoading(), }); diff --git a/src/views/thread/redirect-old-route.js b/src/views/thread/redirect-old-route.js index 0ff838a558..bae51587fc 100644 --- a/src/views/thread/redirect-old-route.js +++ b/src/views/thread/redirect-old-route.js @@ -2,36 +2,36 @@ // Redirect the old thread route (/thread/:threadId) to the new one (/:community/:channel/:threadId) import React from 'react'; import { Redirect } from 'react-router'; -import slugg from 'slugg'; -import idx from 'idx'; import { getThreadByMatch } from 'shared/graphql/queries/thread/getThread'; -import { FullscreenThreadView } from 'src/views/thread'; -import LoadingThread from 'src/views/thread/components/loading'; +import { ErrorView, LoadingView } from 'src/views/viewHelpers'; import getThreadLink from 'src/helpers/get-thread-link'; export default getThreadByMatch(props => { - if (props.data && props.data.thread && props.data.thread.id) { - const { thread } = props.data; - return ( - - ); - } + const { data, location } = props; + if (data) { + const { thread, loading, error } = data; - if (props.loading) { - return ; - } + if (thread && thread.id) { + return ( + + ); + } + + if (loading) { + return ; + } + + // If we don't have a thread, but also aren't loading anymore it's either a private or a non-existant thread + if (error) { + return ; + } - // If we don't have a thread, but also aren't loading anymore it's either a private or a non-existant thread - // In that case, or if it's an error, render the thread container empty state - if (!props.loading || props.error) { - return ( - - ); + return ; } return null; diff --git a/src/views/thread/style.js b/src/views/thread/style.js index 84a3c04ac3..6b62774108 100644 --- a/src/views/thread/style.js +++ b/src/views/thread/style.js @@ -2,8 +2,9 @@ import theme from 'shared/theme'; import styled, { css } from 'styled-components'; import { Link } from 'react-router-dom'; -import { Button } from 'src/components/buttons'; +import { OutlineButton } from 'src/components/button'; import Column from 'src/components/column'; +import { MEDIA_BREAK, PRIMARY_COLUMN_WIDTH } from 'src/components/layout'; import { FlexCol, FlexRow, @@ -11,9 +12,6 @@ import { H3, Transition, zIndex, - Tooltip, - Shadow, - hexa, Truncate, } from 'src/components/globals'; @@ -21,7 +19,6 @@ export const ThreadViewContainer = styled.div` display: flex; width: 100%; height: 100%; - max-height: ${props => (props.constrain ? 'calc(100% - 48px)' : '100%')}; max-width: 1024px; background-color: ${theme.bg.wash}; margin: ${props => @@ -92,7 +89,7 @@ export const Input = styled(FlexRow)` max-width: 100%; align-self: stretch; - @media (max-width: 768px) { + @media (max-width: ${MEDIA_BREAK}px) { z-index: ${zIndex.mobileInput}; } `; @@ -109,15 +106,6 @@ export const Detail = styled(Column)` `} `; -export const ChatInputWrapper = styled(FlexCol)` - align-self: stretch; - align-items: stretch; - margin: 0; - flex: auto; - position: relative; - max-width: 100%; -`; - export const DetailViewWrapper = styled(FlexCol)` background-image: ${({ theme }) => `linear-gradient(to right, ${theme.bg.wash}, ${theme.bg.default} 15%, ${ @@ -127,7 +115,7 @@ export const DetailViewWrapper = styled(FlexCol)` justify-content: flex-start; align-items: center; - @media (max-width: 768px) { + @media (max-width: ${MEDIA_BREAK}px) { background-color: ${theme.bg.default}; background-image: none; } @@ -141,7 +129,7 @@ export const Container = styled(FlexCol)` flex: auto; overflow-y: scroll; - @media (max-width: 768px) { + @media (max-width: ${MEDIA_BREAK}px) { padding-top: 16px; } `; @@ -150,6 +138,14 @@ export const ThreadWrapper = styled(FlexCol)` font-size: 16px; flex: none; min-width: 320px; + position: relative; + z-index: 4; + background: ${theme.bg.default}; + width: 100%; + max-width: 100%; + /* manually nudge up 60px to cover the sliding header in the thread view */ + top: -68px; + margin-bottom: -68px; ${props => props.isEditing && @@ -159,20 +155,25 @@ export const ThreadWrapper = styled(FlexCol)` position: relative; display: block; overflow: hidden; - overflow-y: scroll; + overflow-y: auto; `} + + @media (max-width: ${MEDIA_BREAK}px) { + top: 0; + margin-bottom: 0; + } `; export const ThreadContent = styled.div` height: 100%; - padding: ${props => (props.isEditing ? '0' : '32px')}; + padding: ${props => (props.isEditing ? '0' : '16px')}; ${props => props.isEditing && css` - max-height: calc(100% - 52px); + max-height: calc(100% - 55px); overflow: hidden; - overflow-y: scroll; + overflow-y: auto; `} @media (max-width: 1024px) { @@ -184,6 +185,7 @@ export const ThreadHeading = styled(H1)` font-size: 28px; font-weight: 600; word-break: break-word; + margin-bottom: 16px; `; export const A = styled.a` @@ -227,7 +229,8 @@ export const DropWrap = styled(FlexCol)` margin: 0 8px; &:hover { - color: ${theme.bg.border}; + color: ${theme.text.secondary}; + cursor: pointer; transition: ${Transition.hover.on}; } @@ -244,13 +247,14 @@ export const FlyoutRow = styled(FlexRow)` button { width: 100%; justify-content: flex-start; - border-top: 1px solid ${theme.bg.wash}; + border-top: 1px solid ${theme.bg.divider}; border-radius: 0; transition: none; } button:hover { background: ${theme.bg.wash}; + border-top: 1px solid ${theme.bg.divider}; transition: none; } @@ -288,7 +292,6 @@ export const Byline = styled.div` font-weight: 400; color: ${theme.brand.alt}; display: flex; - margin-bottom: 24px; align-items: center; flex: auto; font-size: 14px; @@ -324,6 +327,7 @@ export const AuthorUsername = styled.span` font-weight: 400; margin-right: 4px; align-self: flex-end; + word-break: break-all; `; export const ReputationRow = styled.div``; @@ -355,7 +359,7 @@ export const Location = styled(FlexRow)` text-decoration: underline; } - @media (max-width: 768px) { + @media (max-width: ${MEDIA_BREAK}px) { display: none; } `; @@ -374,7 +378,7 @@ export const ChatWrapper = styled.div` flex: none; margin-top: 16px; - @media (max-width: 768px) { + @media (max-width: ${MEDIA_BREAK}px) { overflow-x: hidden; } `; @@ -383,12 +387,17 @@ export const NullMessagesWrapper = styled.div` display: flex; align-items: center; justify-content: center; - padding: 32px; - padding-top: 64px; + padding: 64px 32px; flex: 1; color: ${theme.text.alt}; flex-direction: column; opacity: 0.8; + width: 100%; + background: ${theme.bg.default}; + + @media (max-width: ${MEDIA_BREAK}px) { + padding-bottom: 128px; + } > .icon { opacity: 0.4; @@ -415,11 +424,12 @@ export const ThreadTitle = { width: '100%', color: '#171A21', whiteSpace: 'pre-wrap', + wordBreak: 'break-word', borderRadius: '12px 12px 0 0', }; export const ThreadDescription = { - fontSize: '16px', + fontSize: '16px', // has to be 16px to avoid zoom on iOS fontWeight: '500', width: '100%', display: 'inline-block', @@ -430,6 +440,7 @@ export const ThreadDescription = { boxShadow: 'none', color: '#171A21', whiteSpace: 'pre-wrap', + wordBreak: 'break-word', }; export const ShareButtons = styled.div` @@ -459,25 +470,27 @@ export const ShareButton = styled.span` ? props.theme.social.twitter.default : props.theme.text.default}; } - - ${Tooltip}; `; -export const CommunityHeader = styled.div` - display: ${props => (props.hide ? 'none' : 'flex')}; - align-items: center; - justify-content: space-between; - padding: 11px 32px; - box-shadow: ${Shadow.low} ${props => hexa(props.theme.bg.reverse, 0.15)}; - flex: 0 0 64px; - align-self: stretch; - background: ${theme.bg.default}; +export const StickyHeaderContent = styled.div` + display: flex; + padding: 12px 16px; + cursor: pointer; + max-width: 70%; @media (max-width: 728px) { padding: 16px; display: flex; } `; + +export const StickyHeaderActionsContainer = styled.div` + padding: 12px 0; + display: flex; + align-items: center; + flex: 0 1 auto; +`; + export const CommunityHeaderName = styled.h3` font-size: 16px; font-weight: 600; @@ -495,6 +508,7 @@ export const CommunityHeaderSubtitle = styled.span` margin-top: 4px; line-height: 12px; color: ${theme.text.alt}; + ${Truncate}; > a { display: flex; @@ -542,7 +556,7 @@ export const CommunityHeaderChannelTag = styled.div` export const CommunityHeaderMeta = styled.div` display: flex; align-items: center; - max-width: 80%; + max-width: 100%; `; export const CommunityHeaderMetaCol = styled.div` @@ -560,7 +574,7 @@ export const PillLink = styled(Link)` font-size: 12px; box-shadow: 0 0 0 1px ${theme.bg.border}; background: ${theme.bg.wash}; - font-weight: ${props => '400'}; + font-weight: 400; color: ${theme.text.alt}; display: flex; flex: none; @@ -627,9 +641,9 @@ export const ActionBarContainer = styled.div` border: 1px solid ${theme.bg.border}; border-left: 0; border-right: 0; - padding: 6px 32px; + padding: 6px 16px; - @media (max-width: 768px) { + @media (max-width: ${MEDIA_BREAK}px) { margin: 0; margin-top: 0; border-radius: 0; @@ -640,29 +654,15 @@ export const ActionBarContainer = styled.div` } `; -export const WatercoolerActionBarContainer = styled(ActionBarContainer)` - margin-bottom: 16px; -`; - export const FixedBottomActionBarContainer = styled(ActionBarContainer)` z-index: 1; width: 100%; position: sticky; `; -export const FollowButton = styled(Button)` +export const FollowButton = styled(OutlineButton)` background: ${theme.bg.default}; - border: 1px solid ${theme.bg.border}; - color: ${theme.text.alt}; - padding: 4px; - margin-left: 24px; - - &:hover { - background: ${theme.bg.default}; - color: ${theme.text.default}; - } - - @media (max-width: 768px) { + @media (max-width: ${MEDIA_BREAK}px) { display: ${props => (props.currentUser ? 'none' : 'flex')}; } `; @@ -801,46 +801,109 @@ export const Label = styled.p` font-size: 14px; `; -export const WatercoolerDescription = styled.h4` - font-size: 18px; - font-weight: 400; - color: ${theme.text.alt}; - text-align: center; - line-height: 1.4; - margin: 0; - max-width: 600px; +export const StickyHeaderContainer = styled.div` + position: sticky; + top: 0; + width: 100%; + z-index: ${zIndex.card}; + display: flex; + align-items: center; + justify-content: space-between; + border-bottom: 1px solid ${theme.bg.border}; + flex: 0 0 64px; + align-self: stretch; + background: ${theme.bg.wash}; + padding-right: 16px; + max-width: ${PRIMARY_COLUMN_WIDTH - 2}px; + + @media (max-width: ${MEDIA_BREAK}px) { + display: none; + } `; -export const WatercoolerIntroContainer = styled.div` +export const Stretch = styled.div` + flex: 1; display: flex; align-items: center; - justify-content: center; - padding: 32px 32px 36px; - background: ${theme.bg.default}; - flex: auto; flex-direction: column; + width: 100%; + position: relative; + ${props => + props.isModal && + css` + border-left: 1px solid ${theme.bg.border}; + border-right: 1px solid ${theme.bg.border}; + `} `; -export const WatercoolerTitle = styled.h3` - text-align: center; - font-size: 22px; +export const LockedWrapper = styled.div` + display: flex; + width: 100%; + justify-content: center; + align-items: center; + padding: 16px; + color: ${theme.text.secondary}; + background: ${theme.bg.wash}; + border-top: 1px solid ${theme.bg.border}; + + button { + flex: 1; + } +`; + +export const LockedText = styled.div` + font-size: 15px; font-weight: 500; - color: ${theme.text.default}; - margin-bottom: 8px; + margin-left: 16px; `; -export const AnimatedContainer = styled.div` - transform: translateY(${props => (props.isVisible ? '0' : '-64px')}); +export const TopBottomButtonContainer = styled.div` + position: fixed; + bottom: 24px; + right: 24px; opacity: ${props => (props.isVisible ? '1' : '0')}; - transition: transform 0.3s ease-in-out, opacity 0.3s ease-in-out; - width: 100%; - position: absolute; - top: 0; - left: 0; - right: 0; - z-index: ${zIndex.card}; + transform: translateY(${props => (props.isVisible ? '0' : '8px')}); + transition: transform opacity 0.2s ease-in-out; + display: flex; + flex-direction: column; + justify-items: center; + background: ${theme.bg.default}; + border: 1px solid ${theme.bg.border}; + color: ${theme.text.alt}; + border-radius: 24px; + cursor: pointer; + box-shadow: 0 1px 4px rgba(0, 0, 0, 0.08); + z-index: 3000; + + &:hover { + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.12); + } - @media (max-width: 768px) { + @media (max-width: ${MEDIA_BREAK + 72}px) { + bottom: 84px; + } + + @media (max-width: ${MEDIA_BREAK}px) { display: none; } `; +export const TopButton = styled.div` + display: flex; + align-items: center; + justify-content: center; + padding: 8px; + + &:hover { + color: ${theme.text.secondary}; + } +`; +export const BottomButton = styled(TopButton)` + border-top: 1px solid ${theme.bg.border}; +`; + +export const BylineContainer = styled.div` + width: calc(100% + 32px); + margin-left: -16px; + margin-right: -16px; + margin-top: -16px; +`; diff --git a/src/views/threadSlider/index.js b/src/views/threadSlider/index.js index 01be70f017..435ddc1556 100644 --- a/src/views/threadSlider/index.js +++ b/src/views/threadSlider/index.js @@ -1,84 +1,70 @@ -import React, { Component } from 'react'; +import React, { useEffect, useRef } from 'react'; import { connect } from 'react-redux'; -import { openThreadSlider, closeThreadSlider } from 'src/actions/threadSlider'; +import type { Location, History, Match } from 'react-router'; +import Icon from 'src/components/icon'; +import { ThreadView } from 'src/views/thread'; +import { ErrorBoundary } from 'src/components/error'; +import { ESC } from 'src/helpers/keycodes'; +import { setTitlebarProps } from 'src/actions/titlebar'; import { Container, Overlay, - Thread, - Close, + ThreadContainer, CloseButton, - CloseLabel, + ThreadBackground, } from './style'; -import Icon from 'src/components/icons'; -import { SliderThreadView } from '../thread'; -import { ErrorBoundary } from 'src/components/error'; -import { ESC } from 'src/helpers/keycodes'; - -class ThreadSlider extends Component { - componentDidMount() { - document.addEventListener('keydown', this.handleKeyPress, false); - this.props.dispatch(openThreadSlider(this.props.match.params.threadId)); - } - componentWillUnmount() { - document.removeEventListener('keydown', this.handleKeyPress, false); - this.props.dispatch(closeThreadSlider()); - } +type Props = { + previousLocation: Location, + history: History, + match: Match, +}; - // Sync the currently open thread to the Redux state - componentDidUpdate(prev) { - const curr = this.props; +const ThreadSlider = (props: Props) => { + const { previousLocation, history, match, titlebar, dispatch } = props; + const prevTitlebarProps = useRef(titlebar); + const { params } = match; + const { threadId } = params; - const prevId = prev.match.params.threadId; - const currId = curr.match.params.threadId; - - if (prevId !== currId) - this.props.dispatch(openThreadSlider(this.props.match.params.threadId)); - } - - handleKeyPress = e => { - if (e.keyCode === ESC) { - this.closeSlider(e); - } - }; - - closeSlider = e => { + const closeSlider = (e: any) => { e && e.stopPropagation(); - this.props.history.push(this.props.previousLocation); - this.props.dispatch(closeThreadSlider()); + history.push({ ...previousLocation, state: { modal: false } }); }; - render() { - const { threadId } = this.props.match.params; - const { threadSlider } = this.props; + useEffect(() => { + const handleKeyPress = (e: any) => { + if (e.keyCode === ESC) { + e.stopPropagation(); + closeSlider(); + } + }; + + document.addEventListener('keydown', handleKeyPress, false); + return () => { + const prev = prevTitlebarProps.current; + dispatch(setTitlebarProps({ ...prev })); + document.removeEventListener('keydown', handleKeyPress, false); + }; + }, []); - if (!threadSlider.isOpen) return null; + return ( + + + + + - return ( - - -
- -
- - - Close - - - - + +
+
- - -
-
- ); - } -} + + + + + + ); +}; -const map = state => ({ threadSlider: state.threadSlider }); +const map = state => ({ titlebar: state.titlebar }); export default connect(map)(ThreadSlider); diff --git a/src/views/threadSlider/style.js b/src/views/threadSlider/style.js index b62dfc7109..be9acaa2f0 100644 --- a/src/views/threadSlider/style.js +++ b/src/views/threadSlider/style.js @@ -1,27 +1,28 @@ // @flow +import styled from 'styled-components'; import theme from 'shared/theme'; -import styled, { css } from 'styled-components'; import { zIndex } from 'src/components/globals'; - -const animation = css` - opacity: 0; - transform: translateX(1em) translate3d(0, 0, 0); - transition: opacity ${props => props.duration}ms ease-out, - transform ${props => props.duration}ms ease-in-out; - - ${props => - props.entering || props.entered - ? css` - opacity: 1; - transform: translateX(0em) translate3d(0, 0, 0); - ` - : ''}; -`; +import { + MEDIA_BREAK, + MAX_WIDTH, + TITLEBAR_HEIGHT, + NAVBAR_WIDTH, +} from 'src/components/layout'; export const Container = styled.div` - width: 100vw; - height: 100vh; + display: flex; + justify-content: center; + z-index: ${zIndex.slider + 1}; position: absolute; + left: ${NAVBAR_WIDTH}px; + right: 0; + top: 0; + bottom: 0; + + @media (max-width: ${MEDIA_BREAK}px) { + top: ${TITLEBAR_HEIGHT}px; + left: 0; + } `; export const Overlay = styled.div` @@ -30,59 +31,48 @@ export const Overlay = styled.div` left: 0; right: 0; bottom: 0; - background: rgba(0, 0, 0, 0.15); - z-index: ${zIndex.slider + 2}; - - ${animation}; + background: rgba(0, 0, 0, 0.24); `; -export const Thread = styled.div` - display: flex; - position: absolute; - right: 0; +export const ThreadBackground = styled.div` + position: fixed; top: 0; bottom: 0; - width: 650px; - background: #fff; - z-index: ${zIndex.slider + 3}; - box-shadow: -4px 0 12px rgba(0, 0, 0, 0.1); - flex-direction: column; - max-width: 100%; - - @media (max-width: 768px) { - top: -48px; - width: 100%; - } - - @media (min-width: 768px) { - ${animation}; - } + background: ${theme.bg.wash}; + width: ${MAX_WIDTH + 32}px; + left: 50%; + transform: translateX(calc(-50% + 32px)); `; -export const Close = styled.div` +export const ThreadContainer = styled.div` display: flex; - align-items: center; - flex: 1; - border-bottom: 1px solid ${theme.bg.border}; - padding: 8px 16px; - flex: 1 0 auto; - background: ${theme.bg.wash}; - max-height: 48px; - justify-content: flex-end; + justify-content: center; + width: 100%; + z-index: ${zIndex.slider + 4}; + + @media (max-width: ${MEDIA_BREAK}px) { + max-width: 100%; + box-shadow: 0; + } `; export const CloseButton = styled.span` + position: fixed; + top: 24px; + right: 24px; + width: 60px; + height: 60px; + border-radius: 30px; display: flex; align-items: center; justify-content: center; - width: 32px; - height: 32px; - border-radius: 32px; - color: ${theme.text.alt}; -`; + background: ${theme.bg.reverse}; + color: ${theme.text.reverse}; + z-index: ${zIndex.slider + 4}; + cursor: pointer; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.12); -export const CloseLabel = styled.span` - font-size: 14px; - font-weight: 500; - color: ${theme.text.alt}; + @media (max-width: ${MEDIA_BREAK}px) { + display: none; + } `; diff --git a/src/views/titlebar/index.js b/src/views/titlebar/index.js deleted file mode 100644 index 72914f38ac..0000000000 --- a/src/views/titlebar/index.js +++ /dev/null @@ -1,102 +0,0 @@ -import React, { Component } from 'react'; -import compose from 'recompose/compose'; -import { withRouter } from 'react-router'; -import { Link } from 'react-router-dom'; -import ThreadSearch from '../dashboard/components/threadSearch'; -import Icon from '../../components/icons'; -import { IconButton } from '../../components/buttons'; -import { TitleBar, Text, Subtitle, Title } from './style'; - -const TextHeading = ({ title, subtitle }) => ( - - {subtitle && {subtitle}} - {title && {title}} - -); - -class Titlebar extends Component { - handleBack = () => { - const { history, backRoute } = this.props; - const length = history.length; - - if (backRoute) { - return history.push(this.props.backRoute); - } - - /* - We don't have a reliable way to know exactly where a user should navigate - to when they hit the back button. For example, suppose you see a link - to Spectrum on Twitter - you open Spectrum and read the story. If you - click the 'back' button in the titlebar, where should you go? To the - channel the thread was posted in? To the home page? - - What we can do - roughly - is look at the length provided by the - router history. If it is greater than 3, we can assume the user has - already navigated on the site before and would expect the back behavior - to only move back one step in the history. If the location length is less - than 3, we might assume that the user *just* landed on a page, in which - case we need to provide an explicit backRoute via a prop. - - This backRoute will often be just a link to '/' although in some cases - we can make good assumptions (i.e. if a user lands directly on a channel - page, the back button can take them to the community for that channel). - */ - return history.goBack(); - }; - - render() { - const { - title, - subtitle, - provideBack, - noComposer, - hasChildren, - hasSearch, - filter, - children, - messageComposer, - activeCommunityId, - activeChannelId, - } = this.props; - const composerQuery = `${ - activeCommunityId ? `?composerCommunityId=${activeCommunityId}` : '' - }${activeChannelId ? `&composerChannelId=${activeChannelId}` : ''}`; - return ( - - {provideBack ? ( - - ) : hasChildren ? ( - children - ) : null} - {title || subtitle ? ( - - ) : hasSearch ? ( - - ) : ( - - )} - - {noComposer ? null : messageComposer ? ( - - - - ) : ( - - - - )} - - ); - } -} - -export default compose(withRouter)(Titlebar); diff --git a/src/views/titlebar/style.js b/src/views/titlebar/style.js deleted file mode 100644 index aa81c16d33..0000000000 --- a/src/views/titlebar/style.js +++ /dev/null @@ -1,69 +0,0 @@ -// @flow -import theme from 'shared/theme'; -// $FlowFixMe -import styled from 'styled-components'; -import { hexa, Shadow, FlexRow, zIndex } from '../../components/globals'; -import { isDesktopApp } from 'src/helpers/desktop-app-utils'; - -export const TitleBar = styled(FlexRow)` - grid-area: title; - width: 100%; - display: grid; - grid-template-columns: 32px 1fr 32px; - grid-template-rows: 1fr; - grid-template-areas: 'left center right'; - grid-column-gap: 16px; - padding: ${isDesktopApp() ? '32px 8px 0' : '0 8px'}; - background-color: ${theme.bg.reverse}; - color: ${theme.text.reverse}; - min-height: ${isDesktopApp() ? '80px' : '48px'}; - height: ${isDesktopApp() ? '80px' : '48px'}; - max-height: ${isDesktopApp() ? '80px' : '48px'}; - order: 0; - flex: 0 0 ${isDesktopApp() ? '80px' : '48px'}; - z-index: ${zIndex.chrome}; - box-shadow: ${Shadow.mid} ${({ theme }) => hexa(theme.bg.reverse, 0.15)}; - justify-items: center; - - @media (min-width: 768px) { - display: none; - } -`; - -export const Text = styled.div` - display: flex; - justify-content: center; - flex-direction: column; - text-align: center; - align-content: center; - align-items: center; - flex: auto; - justify-self: center; - align-self: center; - grid-area: center; - max-width: calc(100vw - 96px); - overflow: hidden; -`; - -export const Title = styled.h3` - font-size: ${props => (props.large ? '18px' : '14px')}; - font-weight: 800; - max-width: 100%; - text-overflow: ellipsis; - overflow: hidden; - word-break: break-word; - white-space: nowrap; -`; - -export const Subtitle = styled.p` - color: ${({ theme }) => hexa(theme.text.reverse, 0.75)}; - font-size: 12px; - letter-spacing: 0.2px; - font-weight: 600; - line-height: 1.4; - max-width: 100%; - text-overflow: ellipsis; - overflow: hidden; - word-break: break-word; - white-space: nowrap; -`; diff --git a/src/views/user/components/communityList.js b/src/views/user/components/communityList.js index a22698d5eb..4b3ec0b11f 100644 --- a/src/views/user/components/communityList.js +++ b/src/views/user/components/communityList.js @@ -1,16 +1,15 @@ //@flow import * as React from 'react'; -import { Link } from 'react-router-dom'; import { connect } from 'react-redux'; import { withRouter } from 'react-router'; import compose from 'recompose/compose'; -import { CommunityListItem } from 'src/components/listItems'; -import Icon from 'src/components/icons'; +import { CommunityListItem } from 'src/components/entities'; +import { ErrorBoundary } from 'src/components/error'; +import { Loading } from 'src/components/loading'; +import { PrimaryOutlineButton } from 'src/components/button'; import { getUserCommunityConnection } from 'shared/graphql/queries/user/getUserCommunityConnection'; import type { GetUserCommunityConnectionType } from 'shared/graphql/queries/user/getUserCommunityConnection'; -import { ListContainer } from 'src/components/listItems/style'; - type Props = { data: { user: GetUserCommunityConnectionType, @@ -23,13 +22,23 @@ class CommunityList extends React.Component { render() { const { data } = this.props; + if (data.loading) { + return ; + } + if ( !data.user || !data.user.communityConnection || !data.user.communityConnection.edges || data.user.communityConnection.edges.length === 0 ) { - return null; + return ( +
+ + Explore communities + +
+ ); } const communities = data.user.communityConnection.edges.map( @@ -49,25 +58,20 @@ class CommunityList extends React.Component { } return ( - +
{sortedCommunities.map(community => { if (!community) return null; return ( - + - - - + communityObject={community} + profilePhoto={community.profilePhoto} + name={community.name} + /> + ); })} - +
); } } diff --git a/src/views/user/components/search.js b/src/views/user/components/search.js index 997107292f..37a0a567bb 100644 --- a/src/views/user/components/search.js +++ b/src/views/user/components/search.js @@ -1,9 +1,9 @@ // @flow import * as React from 'react'; import compose from 'recompose/compose'; -import { throttle } from '../../../helpers/utils'; +import { throttle } from 'src/helpers/utils'; import searchThreadsQuery from 'shared/graphql/queries/search/searchThreads'; -import ThreadFeed from '../../../components/threadFeed'; +import ThreadFeed from 'src/components/threadFeed'; import { SearchContainer, SearchInput } from '../style'; const SearchThreadFeed = compose(searchThreadsQuery)(ThreadFeed); @@ -68,17 +68,16 @@ class Search extends React.Component { onChange={this.handleChange} /> - {searchString && - sendStringToServer && ( - - )} + {searchString && sendStringToServer && ( + + )} ); } diff --git a/src/views/user/index.js b/src/views/user/index.js index 9f09c8bd66..2b007bb0fc 100644 --- a/src/views/user/index.js +++ b/src/views/user/index.js @@ -1,48 +1,53 @@ // @flow import * as React from 'react'; import compose from 'recompose/compose'; -import { type History, type Match } from 'react-router'; +import querystring from 'query-string'; +import { + withRouter, + type History, + type Location, + type Match, +} from 'react-router'; import { connect } from 'react-redux'; import generateMetaInfo from 'shared/generate-meta-info'; -import { Link } from 'react-router-dom'; -import AppViewWrapper from 'src/components/appViewWrapper'; import Head from 'src/components/head'; import ThreadFeed from 'src/components/threadFeed'; -import { initNewThreadWithUser } from 'src/actions/directMessageThreads'; -import { UserProfile } from 'src/components/profile'; -import { LoadingScreen } from 'src/components/loading'; -import { NullState } from 'src/components/upsell'; -import { Button, ButtonRow, TextButton } from 'src/components/buttons'; +import { UserProfileCard } from 'src/components/entities'; +import { setTitlebarProps } from 'src/actions/titlebar'; import CommunityList from './components/communityList'; import Search from './components/search'; import { withCurrentUser } from 'src/components/withCurrentUser'; +import { UserAvatar } from 'src/components/avatar'; import { getUserByMatch, type GetUserType, } from 'shared/graphql/queries/user/getUser'; import getUserThreads from 'shared/graphql/queries/user/getUserThreadConnection'; -import ViewError from 'src/components/viewError'; -import Titlebar from '../titlebar'; -import { CoverPhoto } from 'src/components/profile/coverPhoto'; -import { LoginButton, SettingsButton } from '../community/style'; +import { ErrorView, LoadingView } from 'src/views/viewHelpers'; import viewNetworkHandler from 'src/components/viewNetworkHandler'; import type { Dispatch } from 'redux'; +import { SegmentedControl, Segment } from 'src/components/segmentedControl'; +import { + ViewGrid, + SecondaryPrimaryColumnGrid, + PrimaryColumn, + SecondaryColumn, +} from 'src/components/layout'; import { - Grid, - Meta, - Content, - Extras, - ColumnHeading, - MetaMemberships, -} from './style'; + SidebarSection, + SidebarSectionHeader, + SidebarSectionHeading, +} from 'src/views/community/style'; import { - SegmentedControl, - DesktopSegment, - MobileSegment, -} from 'src/components/segmentedControl'; -import { ErrorBoundary } from 'src/components/error'; -import { openModal } from 'src/actions/modals'; -import { isAdmin } from 'src/helpers/is-admin'; + NullColumn, + NullColumnHeading, + NullColumnSubheading, +} from 'src/components/threadFeed/style'; +import { PrimaryOutlineButton } from 'src/components/button'; +import Icon from 'src/components/icon'; +import { MobileUserAction } from 'src/components/titlebar/actions'; +import { FeedsContainer } from './style'; +import { InfoContainer } from 'src/views/community/style'; const ThreadFeedWithData = compose( connect(), @@ -63,64 +68,112 @@ type Props = { queryVarIsChanging: boolean, dispatch: Dispatch, history: History, + location: Location, }; type State = { hasNoThreads: boolean, - selectedView: string, hasThreads: boolean, }; class UserView extends React.Component { state = { hasNoThreads: false, - selectedView: 'participant', hasThreads: true, }; - componentDidMount() {} + componentDidMount() { + const { dispatch } = this.props; + + if (this.props.data && this.props.data.user) { + this.setDefaultTab(); + + return dispatch( + setTitlebarProps({ + title: this.props.data.user.name, + titleIcon: ( + + ), + rightAction: , + }) + ); + } + } + + setDefaultTab = () => { + const { location, history } = this.props; + const { search } = location; + const { tab } = querystring.parse(search); + if (!tab) + history.replace({ + ...location, + search: querystring.stringify({ tab: 'posts' }), + }); + }; componentDidUpdate(prevProps: Props) { - if (!prevProps.data.user) return; - if (!this.props.data.user) return; + const { dispatch } = this.props; + + if (!prevProps.data || !this.props.data) return; + + if (!prevProps.data.user && this.props.data.user) { + this.setDefaultTab(); + + return dispatch( + setTitlebarProps({ + title: this.props.data.user.name, + titleIcon: ( + + ), + rightAction: , + }) + ); + } // track when a new profile is viewed without the component having been remounted - if (prevProps.data.user.id !== this.props.data.user.id) { + if ( + prevProps.data.user && + this.props.data.user && + prevProps.data.user.id !== this.props.data.user.id + ) { + this.setDefaultTab(); + return dispatch( + setTitlebarProps({ + title: this.props.data.user.name, + titleIcon: ( + + ), + rightAction: , + }) + ); } } hasNoThreads = () => this.setState({ hasThreads: false }); hasThreads = () => this.setState({ hasThreads: true }); - handleSegmentClick = (label: string) => { - if (this.state.selectedView === label) return; - - return this.setState({ - selectedView: label, - hasThreads: true, + handleSegmentClick = (tab: string) => { + const { history, location } = this.props; + return history.replace({ + ...location, + search: querystring.stringify({ tab }), }); }; - initMessage = user => { - this.props.dispatch(initNewThreadWithUser(user)); - this.props.history.push('/messages/new'); - }; - - initReport = () => { - const { - data: { user }, - dispatch, - } = this.props; - return dispatch(openModal('REPORT_USER_MODAL', { user })); - }; - - initBan = () => { - const { - data: { user }, - dispatch, - } = this.props; - return dispatch(openModal('BAN_USER_MODAL', { user })); - }; - render() { const { data: { user }, @@ -129,15 +182,21 @@ class UserView extends React.Component { match: { params: { username }, }, + location, currentUser, } = this.props; - const { hasThreads, selectedView } = this.state; + const { hasThreads } = this.state; + + const { search } = location; + const { tab } = querystring.parse(search); + const selectedView = tab; if (queryVarIsChanging) { - return ; + return ; } if (user && user.id) { + const isCurrentUser = currentUser && user.id === currentUser.id; const { title, description } = generateMetaInfo({ type: 'user', data: { @@ -147,17 +206,13 @@ class UserView extends React.Component { }, }); - const nullHeading = `${user.name} hasn’t ${ - selectedView === 'creator' ? 'created' : 'joined' - } any conversations yet.`; - const Feed = - selectedView === 'creator' + selectedView === 'posts' ? ThreadFeedWithData : ThreadParticipantFeedWithData; return ( - + { - - - - - - - - - {currentUser && user.id !== currentUser.id && ( - - this.initMessage(user)} - > - Message {user.name} - - Report - - )} - - {currentUser && - user.id !== currentUser.id && - isAdmin(currentUser.id) && ( - Ban - )} - - {currentUser && user.id === currentUser.id && ( - - Settings - - )} - - - - Member of + + + + + + + + + + + Communities + + - - - - - - this.handleSegmentClick('search')} - selected={selectedView === 'search'} - > - Search - - - this.handleSegmentClick('participant')} - selected={selectedView === 'participant'} - > - Replies - - - this.handleSegmentClick('creator')} - selected={selectedView === 'creator'} - > - Threads - - this.handleSegmentClick('search')} - selected={selectedView === 'search'} - > - Search - - this.handleSegmentClick('participant')} - selected={selectedView === 'participant'} - > - Replies - - this.handleSegmentClick('creator')} - selected={selectedView === 'creator'} - > - Threads - - - - {hasThreads && - (selectedView === 'creator' || - selectedView === 'participant') && ( - - )} - - {selectedView === 'search' && } - - {!hasThreads && } - - - - Member of - - - - - + + + + + + this.handleSegmentClick('posts')} + isActive={selectedView === 'posts'} + data-cy="user-posts-tab" + > + Posts + + + this.handleSegmentClick('activity')} + isActive={selectedView === 'activity'} + data-cy="user-activity-tab" + > + Activity + + + this.handleSegmentClick('info')} + hideOnDesktop + isActive={selectedView === 'info'} + data-cy="user-info-tab" + > + Info + + + this.handleSegmentClick('search')} + isActive={selectedView === 'search'} + data-cy="user-search-tab" + > + Search + + + + {hasThreads && + (selectedView === 'posts' || + selectedView === 'activity') && ( + + )} + + {selectedView === 'search' && } + + {selectedView === 'info' && ( + + + + + + + + + Communities + + + + + + + )} + + {!hasThreads && + (selectedView === 'posts' || + selectedView === 'activity') && ( + + + No posts yet + + Posts will show up here as they are published and + when conversations are joined. + + {isCurrentUser && ( + + + New post + + )} + + + )} + + + + + ); } if (isLoading) { - return ; + return ; } if (!user) { return ( - - - - - - - - - - + ); } - return ( - - - - - ); + return ; } } @@ -354,5 +378,6 @@ export default compose( getUserByMatch, withCurrentUser, viewNetworkHandler, + withRouter, connect() )(UserView); diff --git a/src/views/user/style.js b/src/views/user/style.js index 6bcbcd365e..ef0ba5f8e5 100644 --- a/src/views/user/style.js +++ b/src/views/user/style.js @@ -1,10 +1,10 @@ // @flow import theme from 'shared/theme'; import styled from 'styled-components'; -import { FlexRow, FlexCol } from '../../components/globals'; -import Card from '../../components/card'; -import { Transition, zIndex } from '../../components/globals'; -import { SegmentedControl } from '../../components/segmentedControl'; +import { FlexRow, FlexCol } from 'src/components/globals'; +import Card from 'src/components/card'; +import { Transition, zIndex } from 'src/components/globals'; +import { MEDIA_BREAK } from 'src/components/layout'; export const Row = styled(FlexRow)` padding: 8px 16px; @@ -42,20 +42,18 @@ export const RowLabel = styled.span` `; export const SearchContainer = styled(Card)` - border-bottom: 2px solid ${theme.bg.border}; + border-bottom: 1px solid ${theme.bg.border}; + background: ${theme.bg.wash}; position: relative; z-index: ${zIndex.search}; width: 100%; - display: block; - min-height: 64px; + display: flex; + padding: 8px 12px; transition: ${Transition.hover.off}; + display: flex; + align-items: center; - &:hover { - transition: none; - border-bottom: 2px solid ${theme.brand.alt}; - } - - @media (max-width: 768px) { + @media (max-width: ${MEDIA_BREAK}px) { border-radius: 0; pointer-events: all; margin-bottom: 0; @@ -63,44 +61,21 @@ export const SearchContainer = styled(Card)` `; export const SearchInput = styled.input` - justify-content: flex-start; + display: flex; + flex: 1 0 auto; align-items: center; - cursor: pointer; - padding: 20px; + width: 100%; + padding: 12px 16px; color: ${theme.text.default}; transition: ${Transition.hover.off}; - font-size: 20px; - font-weight: 800; - margin-left: 8px; - width: 97%; - border-radius: 12px; -`; - -export const Grid = styled.main` - display: grid; - grid-template-columns: minmax(320px, 1fr) 3fr minmax(240px, 2fr); - grid-template-rows: 240px 1fr; - grid-template-areas: 'cover cover cover' 'meta content extras'; - grid-column-gap: 32px; - width: 100%; - max-width: 1280px; - min-height: 100vh; - background-color: ${theme.bg.default}; - box-shadow: inset 1px 0 0 ${theme.bg.border}, - inset -1px 0 0 ${theme.bg.border}; - - @media (max-width: 1028px) { - grid-template-columns: 320px 1fr; - grid-template-rows: 160px 1fr; - grid-template-areas: 'cover cover' 'meta content'; - } + font-size: 16px; + font-weight: 500; + border-radius: 100px; + background: ${theme.bg.default}; + border: 1px solid ${theme.bg.border}; - @media (max-width: 768px) { - grid-template-rows: 80px auto 1fr; - grid-template-columns: 100%; - grid-column-gap: 0; - grid-row-gap: 16px; - grid-template-areas: 'cover' 'meta' 'content'; + &:focus { + border: 1px solid ${theme.text.secondary}; } `; @@ -118,13 +93,13 @@ export const Meta = styled(Column)` margin-left: 32px; width: calc(100% - 32px); - @media (max-width: 768px) { + @media (max-width: ${MEDIA_BREAK}px) { margin-left: 0; width: 100%; } } - @media (max-width: 768px) { + @media (max-width: ${MEDIA_BREAK}px) { padding: 0 16px; > div { @@ -140,48 +115,9 @@ export const MetaMemberships = styled.div` display: none; } - @media (max-width: 768px) { - display: none; - } -`; - -export const Content = styled(Column)` - grid-area: content; - min-width: 0; - align-items: stretch; - - @media (max-width: 1028px) and (min-width: 768px) { - padding-right: 32px; - } - - @media (max-width: 768px) { - > ${SegmentedControl} > div { - margin-top: 0; - } - } -`; - -export const Extras = styled(Column)` - grid-area: extras; - - > ${FlexCol} > div { - border-top: 0; - padding: 0; - padding-top: 24px; - - h3 { - font-size: 16px; - line-height: 1.2; - } - } - - @media (max-width: 1280px) { + @media (max-width: ${MEDIA_BREAK}px) { display: none; } - - @media (min-width: 768px) { - padding-right: 32px; - } `; export const ColumnHeading = styled.div` @@ -193,8 +129,10 @@ export const ColumnHeading = styled.div` padding: 8px 16px 12px; margin-top: 24px; border-bottom: 2px solid ${theme.bg.border}; +`; - + div { - padding: 8px 16px; - } +export const FeedsContainer = styled.section` + display: flex; + flex-direction: column; + background: ${theme.bg.default}; `; diff --git a/src/views/userSettings/components/deleteAccountForm.js b/src/views/userSettings/components/deleteAccountForm.js index d20fe062ad..471a0f93f5 100644 --- a/src/views/userSettings/components/deleteAccountForm.js +++ b/src/views/userSettings/components/deleteAccountForm.js @@ -15,7 +15,11 @@ import { type GetUserCommunityConnectionType, } from 'shared/graphql/queries/user/getUserCommunityConnection'; import viewNetworkHandler from 'src/components/viewNetworkHandler'; -import { Button, TextButton, OutlineButton } from 'src/components/buttons'; +import { + HoverWarnOutlineButton, + WarnButton, + OutlineButton, +} from 'src/components/button'; import deleteCurrentUserMutation from 'shared/graphql/mutations/user/deleteCurrentUser'; import { SERVER_URL } from 'src/api/constants'; import { Link } from 'react-router-dom'; @@ -118,31 +122,30 @@ class DeleteAccountForm extends React.Component { > {!isLoading && ( Cancel )} - + ) : ( - Delete my account - + )} diff --git a/src/views/userSettings/components/editForm.js b/src/views/userSettings/components/editForm.js index 780562c139..a4f04c8ef2 100644 --- a/src/views/userSettings/components/editForm.js +++ b/src/views/userSettings/components/editForm.js @@ -5,8 +5,8 @@ import { withApollo } from 'react-apollo'; import compose from 'recompose/compose'; import { connect } from 'react-redux'; import { Link } from 'react-router-dom'; -import { Button } from 'src/components/buttons'; -import Icon from 'src/components/icons'; +import { PrimaryOutlineButton } from 'src/components/button'; +import Icon from 'src/components/icon'; import { SERVER_URL, CLIENT_URL } from 'src/api/constants'; import GithubProfile from 'src/components/githubProfile'; import { GithubSigninButton } from 'src/components/loginButtonSet/github'; @@ -370,6 +370,7 @@ class UserWithData extends React.Component { label="Username" size={'small'} username={username} + placeholder="Set a username..." onValidationResult={this.handleUsernameValidation} onError={this.handleOnError} dataCy="user-username-input" @@ -452,21 +453,21 @@ class UserWithData extends React.Component { /> - + {createError && ( diff --git a/src/views/userSettings/components/emailSettings.js b/src/views/userSettings/components/emailSettings.js index 6001ac120b..1b8d6a5f1f 100644 --- a/src/views/userSettings/components/emailSettings.js +++ b/src/views/userSettings/components/emailSettings.js @@ -6,7 +6,7 @@ import { addToastWithTimeout } from 'src/actions/toasts'; import updateUserEmailMutation from 'shared/graphql/mutations/user/updateUserEmail'; import toggleUserNotificationSettingsMutation from 'shared/graphql/mutations/user/toggleUserNotificationSettings'; import { Checkbox } from 'src/components/formElements'; -import Icon from 'src/components/icons'; +import Icon from 'src/components/icon'; import { ListContainer, Notice, @@ -110,7 +110,12 @@ class EmailSettings extends React.Component { }; render() { - const { user: { settings: { notifications } }, user } = this.props; + const { + user: { + settings: { notifications }, + }, + user, + } = this.props; const settings = parseNotificationTypes(notifications).filter( notification => notification.hasOwnProperty('emailValue') diff --git a/src/views/userSettings/components/logout.js b/src/views/userSettings/components/logout.js index 218300e335..546143d6a2 100644 --- a/src/views/userSettings/components/logout.js +++ b/src/views/userSettings/components/logout.js @@ -1,16 +1,16 @@ // @flow import React from 'react'; import { SERVER_URL } from 'src/api/constants'; -import { Button } from 'src/components/buttons'; +import { OutlineButton } from 'src/components/button'; import { LogoutWrapper } from '../style'; import { SectionCard } from 'src/components/settingsViews/style'; export default () => ( - - - + + Log out + ); diff --git a/src/views/userSettings/index.js b/src/views/userSettings/index.js index 85d0487df8..11d299cadd 100644 --- a/src/views/userSettings/index.js +++ b/src/views/userSettings/index.js @@ -6,19 +6,17 @@ import { Route } from 'react-router-dom'; import getCurrentUserSettings, { type GetCurrentUserSettingsType, } from 'shared/graphql/queries/user/getCurrentUserSettings'; -import { Loading } from 'src/components/loading'; -import AppViewWrapper from 'src/components/appViewWrapper'; import viewNetworkHandler from 'src/components/viewNetworkHandler'; import { withCurrentUser } from 'src/components/withCurrentUser'; import Head from 'src/components/head'; -import ViewError from 'src/components/viewError'; import { View } from './style'; import Overview from './components/overview'; -import Titlebar from '../titlebar'; import Header from 'src/components/settingsViews/header'; -import Subnav from 'src/components/settingsViews/subnav'; import type { ContextRouter } from 'react-router'; import { track, events } from 'src/helpers/analytics'; +import { ErrorView, LoadingView } from 'src/views/viewHelpers'; +import { ViewGrid } from 'src/components/layout'; +import { setTitlebarProps } from 'src/actions/titlebar'; type Props = { data: { @@ -32,61 +30,33 @@ type Props = { class UserSettings extends React.Component { componentDidMount() { track(events.USER_SETTINGS_VIEWED); + const { dispatch } = this.props; + return dispatch( + setTitlebarProps({ + title: 'Settings', + }) + ); } render() { const { data: { user }, - location, match, isLoading, - hasError, currentUser, } = this.props; - // this is hacky, but will tell us if we're viewing analytics or the root settings view - const pathname = location.pathname; - const lastIndex = pathname.lastIndexOf('/'); - const activeTab = pathname.substr(lastIndex + 1); - if (isLoading) { - return ; + return ; } // the user is logged in but somehow a user wasnt fetched from the server prompt a refresh to reauth the user if ((currentUser && !user) || (currentUser && user && !user.id)) { - return ( - - - - - - - ); + return ; } // user is viewing their own settings, validated on the server if (user && user.id && currentUser.id === user.id) { - const subnavItems = [ - { - to: `/users/${user.username}/settings`, - label: 'Overview', - activeLabel: 'settings', - }, - ]; - const subheading = { to: `/users/${user.username}`, label: `Return to profile`, @@ -98,61 +68,26 @@ class UserSettings extends React.Component { }; return ( - - + - - -
- - - - - {() => } - - - - ); - } - - if (hasError) { - return ( - - - - + + +
+ + + {() => } + + + + ); } - return ( - - - - - ); + return ; } } diff --git a/src/views/userSettings/style.js b/src/views/userSettings/style.js index 0789777ca2..a6814ce8e0 100644 --- a/src/views/userSettings/style.js +++ b/src/views/userSettings/style.js @@ -2,6 +2,7 @@ import theme from 'shared/theme'; import styled from 'styled-components'; import { FlexRow, FlexCol } from 'src/components/globals'; +import { MEDIA_BREAK } from 'src/components/layout'; export const EmailListItem = styled.div` padding: 8px 0 16px; @@ -27,7 +28,7 @@ export const View = styled.div` flex: 1; align-self: stretch; - @media (max-width: 768px) { + @media (max-width: ${MEDIA_BREAK}px) { width: 100%; } `; @@ -98,7 +99,7 @@ export const ImageInputWrapper = styled(FlexCol)` > label:nth-of-type(2) { position: absolute; bottom: -24px; - left: 24px; + left: 16px; } `; @@ -122,7 +123,7 @@ export const Location = styled(FlexRow)` text-decoration: underline; } - @media (max-width: 768px) { + @media (max-width: ${MEDIA_BREAK}px) { display: none; } `; @@ -139,13 +140,9 @@ export const GithubSignin = styled.div` `; export const LogoutWrapper = styled.div` - display: none; + display: block; button { width: 100%; } - - @media (max-width: 768px) { - display: block; - } `; diff --git a/src/views/viewHelpers/errorView.js b/src/views/viewHelpers/errorView.js new file mode 100644 index 0000000000..c12f67b58d --- /dev/null +++ b/src/views/viewHelpers/errorView.js @@ -0,0 +1,40 @@ +// @flow +import React from 'react'; +import { OutlineButton, PrimaryButton } from 'src/components/button'; +import { Emoji, Heading, Description, ActionsRow, Card } from './style'; +import { ViewGrid, CenteredGrid } from 'src/components/layout'; + +type Props = { + emoji?: string, + heading?: string, + subheading?: string, +}; + +export const ErrorView = (props: Props) => { + const { + emoji = '😣', + heading = 'We ran into trouble loading this page', + subheading = 'You may be trying to view something that is deleted, or Spectrum is just having a hiccup. If you think something has gone wrong, please contact us.', + ...rest + } = props; + + return ( + + + + + {emoji} + + {heading} + {subheading} + + + Contact us + + Go home + + + + + ); +}; diff --git a/src/views/viewHelpers/index.js b/src/views/viewHelpers/index.js new file mode 100644 index 0000000000..22821c33c0 --- /dev/null +++ b/src/views/viewHelpers/index.js @@ -0,0 +1,6 @@ +// @flow +import { ErrorView } from './errorView'; +import { LoadingView } from './loadingView'; +import { CardStyles } from './style'; + +export { ErrorView, LoadingView, CardStyles }; diff --git a/src/views/viewHelpers/loadingView.js b/src/views/viewHelpers/loadingView.js new file mode 100644 index 0000000000..8293cd23ec --- /dev/null +++ b/src/views/viewHelpers/loadingView.js @@ -0,0 +1,19 @@ +// @flow +import React from 'react'; +import { Loading } from 'src/components/loading'; +import { ViewGrid, CenteredGrid } from 'src/components/layout'; +import { Stretch } from './style'; + +export const LoadingView = () => { + return ( + + + + + + + + + + ); +}; diff --git a/src/views/viewHelpers/style.js b/src/views/viewHelpers/style.js new file mode 100644 index 0000000000..b4036e4205 --- /dev/null +++ b/src/views/viewHelpers/style.js @@ -0,0 +1,63 @@ +// @flow +import styled, { css } from 'styled-components'; +import theme from 'shared/theme'; +import { MEDIA_BREAK } from 'src/components/layout'; + +export const Emoji = styled.span` + font-size: 40px; + margin-bottom: 16px; +`; + +export const Heading = styled.h3` + font-size: 24px; + font-weight: 700; + line-height: 1.3; + margin-bottom: 8px; + color: ${theme.text.default}; +`; +export const Description = styled.p` + margin-top: 8px; + font-size: 16px; + font-weight: 400; + line-height: 1.4; + color: ${theme.text.secondary}; + padding-right: 24px; +`; + +export const ActionsRow = styled.div` + display: grid; + grid-gap: 16px; + grid-template-columns: repeat(2, 1fr); + margin-top: 32px; + + button { + display: flex; + flex: 1 0 auto; + width: 100%; + } +`; + +export const CardStyles = css` + background: ${theme.bg.default}; + border: 1px solid ${theme.bg.border}; + border-radius: 4px; +`; + +export const Card = styled.div` + ${CardStyles}; + padding: 16px; + + @media (max-width: ${MEDIA_BREAK}px) { + border-radius: 0; + border: none; + border-bottom: 1px solid ${theme.bg.border}; + } +`; + +export const Stretch = styled.div` + display: flex; + flex-direction: column; + flex: 1; + justify-content: center; + align-items: center; +`; diff --git a/vulcan/package.json b/vulcan/package.json index 8eb63e599b..ecfb41c8ad 100644 --- a/vulcan/package.json +++ b/vulcan/package.json @@ -3,8 +3,8 @@ "start": "NODE_ENV=production node main.js" }, "dependencies": { - "algoliasearch": "^3.32.0", - "aws-sdk": "^2.409.0", + "algoliasearch": "^3.32.1", + "aws-sdk": "^2.426.0", "bull": "^3.7.0", "datadog-metrics": "^0.8.1", "debug": "^4.1.1", @@ -25,7 +25,7 @@ "rethinkhaberdashery": "^2.3.32", "sanitize-filename": "^1.6.1", "source-map-support": "^0.4.15", - "stopword": "^0.1.15", + "stopword": "^0.1.17", "toobusy-js": "^0.5.1" }, "devDependencies": { diff --git a/vulcan/yarn.lock b/vulcan/yarn.lock index 22b66e5c56..f4b0f05576 100644 --- a/vulcan/yarn.lock +++ b/vulcan/yarn.lock @@ -7,13 +7,13 @@ agentkeepalive@^2.2.0: resolved "https://registry.yarnpkg.com/agentkeepalive/-/agentkeepalive-2.2.0.tgz#c5d1bd4b129008f1163f236f86e5faea2026e2ef" integrity sha1-xdG9SxKQCPEWPyNvhuX66iAm4u8= -algoliasearch@^3.32.0: - version "3.32.0" - resolved "https://registry.yarnpkg.com/algoliasearch/-/algoliasearch-3.32.0.tgz#5818168c26ff921bd0346a919071bac928b747ce" - integrity sha512-C8oQnPTf0wPuyD2jSZwtBAPvz+lHOE7zRIPpgXGBuNt6ZNcC4omsbytG26318rT77a8h4759vmIp6n9p8iw4NA== +algoliasearch@^3.32.1: + version "3.32.1" + resolved "https://registry.yarnpkg.com/algoliasearch/-/algoliasearch-3.32.1.tgz#605f8a2c17ab8da2af4456110f4d0a02b384e3d0" + integrity sha512-NaaHMboU9tKwrU3aim7LlzSDqKb+1TGaC+Lx3NOttSnuMHbPpaf+7LtJL4KlosbRWEwqb9t5wSYMVDrPTH2dNA== dependencies: agentkeepalive "^2.2.0" - debug "^2.6.8" + debug "^2.6.9" envify "^4.0.0" es6-promise "^4.1.0" events "^1.1.0" @@ -33,10 +33,10 @@ asap@~2.0.3: resolved "https://registry.yarnpkg.com/asap/-/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46" integrity sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY= -aws-sdk@^2.409.0: - version "2.409.0" - resolved "https://registry.yarnpkg.com/aws-sdk/-/aws-sdk-2.409.0.tgz#d017060ba9e005487c68dc34a592af74d916f295" - integrity sha512-QV6j9zBQq/Kz8BqVOrJ03ABjMKtErXdUT1YdYEljoLQZimpzt0ZdQwJAsoZIsxxriOJgrqeZsQUklv9AFQaldQ== +aws-sdk@^2.426.0: + version "2.426.0" + resolved "https://registry.yarnpkg.com/aws-sdk/-/aws-sdk-2.426.0.tgz#cf17361c987daf518f945218f06135fbc1a3690d" + integrity sha512-S4nmIhF/6iYeVEmKUWVG03zo1sw3zELoAPGqBKIZ3isrXbxkFXdP2cgIQxqi37zwWXSqaxt0xjeXVOMLzN6vSg== dependencies: buffer "4.9.1" events "1.1.1" @@ -149,7 +149,7 @@ debug@3.1.0: dependencies: ms "2.0.0" -debug@^2.6.8, debug@^2.6.9: +debug@^2.6.9: version "2.6.9" resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== @@ -890,10 +890,10 @@ standard-as-callback@^1.0.0: resolved "https://registry.yarnpkg.com/standard-as-callback/-/standard-as-callback-1.0.1.tgz#2e9e1e9d278d7d77580253faaec42269015e3c1d" integrity sha512-izxEITSyc7S+5oOiF/URiYaNkemPUxIndCNv66jJ548Y1TVxhBvioNMSPrZIQdaZDlhnguOdUzHA/7hJ3xFhuQ== -stopword@^0.1.15: - version "0.1.15" - resolved "https://registry.yarnpkg.com/stopword/-/stopword-0.1.15.tgz#e5a9ac5a3936eb2c54deb2615847f49619f41fcd" - integrity sha512-UoattsZj2D6D00p5K1yKss/rIULftvUp8yrNKm76suiBjzCC6ER0V+IBJvMO3CJwUpaooy3GPgUeHU4CHn/9mA== +stopword@^0.1.17: + version "0.1.17" + resolved "https://registry.yarnpkg.com/stopword/-/stopword-0.1.17.tgz#658ce1532b257700ec909178b05f4142730be16f" + integrity sha512-RSml9ta7Ce0LyN8VUsbIxtwtg4S5suyeXMg2KNFX6xFTtMes3ICNj7EiG0xdaMn+j5oD00lZvO8VPqRpWMP1dQ== strip-json-comments@~2.0.1: version "2.0.1" diff --git a/yarn.lock b/yarn.lock index 4bd2435c18..b4e40e1efc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,12 +2,17 @@ # yarn lockfile v1 -"@apollographql/apollo-tools@^0.2.6": - version "0.2.9" - resolved "https://registry.yarnpkg.com/@apollographql/apollo-tools/-/apollo-tools-0.2.9.tgz#1e20999d11728ef47f8f812f2be0426b5dde1a51" - integrity sha512-AEIQwPkS0QLbkpb6WyRhV4aOMxuErasp47ABv5niDKOasQH8mrD8JSGKJAHuQxVe4kB8DE9sLRoc5qeQ0KFCHA== +"@amplitude/ua-parser-js@0.7.11": + version "0.7.11" + resolved "https://registry.yarnpkg.com/@amplitude/ua-parser-js/-/ua-parser-js-0.7.11.tgz#e3e411912aa88b1832ce3fb4dd4996839bd39243" + integrity sha512-uBYLbl5dRh0w7yWATTiKwfzae4EU6B/jHK6xsY8vRgbNEfwJZLG44Z18B1sBGjeaUYCk2nP8lWNehKGeQf3jgw== + +"@apollographql/apollo-tools@^0.3.3": + version "0.3.4" + resolved "https://registry.yarnpkg.com/@apollographql/apollo-tools/-/apollo-tools-0.3.4.tgz#5f88a0ad87dcbc8e16b17336db063e599797b083" + integrity sha512-2UhFsn9EV57iHaGlkpof4qti3r/ZUqZZ6ELZeq6cliIjO2kdprkVljGAzu4WN9kynTnEm/Cw+yaLtcL7urjWbw== dependencies: - apollo-env "0.2.5" + apollo-env "0.3.4" "@apollographql/graphql-playground-html@^1.6.6": version "1.6.6" @@ -59,14 +64,14 @@ source-map "^0.5.0" trim-right "^1.0.1" -"@babel/generator@^7.0.0", "@babel/generator@^7.2.2": - version "7.2.2" - resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.2.2.tgz#18c816c70962640eab42fe8cae5f3947a5c65ccc" - integrity sha512-I4o675J/iS8k+P38dvJ3IBGqObLXyQLTxtrR4u9cSUJOURvafeEWb/pFMOTwtNrmq73mJzyF6ueTbO1BtN0Zeg== +"@babel/generator@^7.0.0", "@babel/generator@^7.3.4": + version "7.3.4" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.3.4.tgz#9aa48c1989257877a9d971296e5b73bfe72e446e" + integrity sha512-8EXhHRFqlVVWXPezBW5keTiQi/rJMQTg/Y9uVCEZ0CAF3PKtCCaVRnp64Ii1ujhkoDhhF1fVsImoN4yJ2uz4Wg== dependencies: - "@babel/types" "^7.2.2" + "@babel/types" "^7.3.4" jsesc "^2.5.1" - lodash "^4.17.10" + lodash "^4.17.11" source-map "^0.5.0" trim-right "^1.0.1" @@ -85,12 +90,12 @@ "@babel/helper-explode-assignable-expression" "^7.1.0" "@babel/types" "^7.0.0" -"@babel/helper-builder-react-jsx@^7.0.0": - version "7.0.0" - resolved "https://registry.yarnpkg.com/@babel/helper-builder-react-jsx/-/helper-builder-react-jsx-7.0.0.tgz#fa154cb53eb918cf2a9a7ce928e29eb649c5acdb" - integrity sha512-ebJ2JM6NAKW0fQEqN8hOLxK84RbRz9OkUhGS/Xd5u56ejMfVbayJ4+LykERZCOUM6faa6Fp3SZNX3fcT16MKHw== +"@babel/helper-builder-react-jsx@^7.3.0": + version "7.3.0" + resolved "https://registry.yarnpkg.com/@babel/helper-builder-react-jsx/-/helper-builder-react-jsx-7.3.0.tgz#a1ac95a5d2b3e88ae5e54846bf462eeb81b318a4" + integrity sha512-MjA9KgwCuPEkQd9ncSXvSyJ5y+j2sICHyrI0M3L+6fnS4wMSNDc1ARXsbTfbb2cXHn17VisSnU/sHFTCxVxSMw== dependencies: - "@babel/types" "^7.0.0" + "@babel/types" "^7.3.0" esutils "^2.0.0" "@babel/helper-call-delegate@^7.1.0": @@ -214,15 +219,15 @@ "@babel/traverse" "^7.1.0" "@babel/types" "^7.0.0" -"@babel/helper-replace-supers@^7.1.0": - version "7.1.0" - resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.1.0.tgz#5fc31de522ec0ef0899dc9b3e7cf6a5dd655f362" - integrity sha512-BvcDWYZRWVuDeXTYZWxekQNO5D4kO55aArwZOTFXw6rlLQA8ZaDicJR1sO47h+HrnCiDFiww0fSPV0d713KBGQ== +"@babel/helper-replace-supers@^7.1.0", "@babel/helper-replace-supers@^7.3.4": + version "7.3.4" + resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.3.4.tgz#a795208e9b911a6eeb08e5891faacf06e7013e13" + integrity sha512-pvObL9WVf2ADs+ePg0jrqlhHoxRXlOa+SHRHzAXIz2xkYuOHfGl+fKxPMaS4Fq+uje8JQPobnertBBvyrWnQ1A== dependencies: "@babel/helper-member-expression-to-functions" "^7.0.0" "@babel/helper-optimise-call-expression" "^7.0.0" - "@babel/traverse" "^7.1.0" - "@babel/types" "^7.0.0" + "@babel/traverse" "^7.3.4" + "@babel/types" "^7.3.4" "@babel/helper-simple-access@^7.1.0": version "7.1.0" @@ -257,13 +262,13 @@ "@babel/types" "^7.2.0" "@babel/helpers@^7.1.0": - version "7.2.0" - resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.2.0.tgz#8335f3140f3144270dc63c4732a4f8b0a50b7a21" - integrity sha512-Fr07N+ea0dMcMN8nFpuK6dUIT7/ivt9yKQdEEnjVS83tG2pHwPi03gYmk/tyuwONnZ+sY+GFFPlWGgCtW1hF9A== + version "7.3.1" + resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.3.1.tgz#949eec9ea4b45d3210feb7dc1c22db664c9e44b9" + integrity sha512-Q82R3jKsVpUV99mgX50gOPCWwco9Ec5Iln/8Vyu4osNIOQgSrd9RFrQeUvmvddFNoLwMyOUWU+5ckioEKpDoGA== dependencies: "@babel/template" "^7.1.2" "@babel/traverse" "^7.1.5" - "@babel/types" "^7.2.0" + "@babel/types" "^7.3.0" "@babel/highlight@7.0.0-beta.44": version "7.0.0-beta.44" @@ -283,10 +288,10 @@ esutils "^2.0.2" js-tokens "^4.0.0" -"@babel/parser@^7.1.0", "@babel/parser@^7.2.2": - version "7.2.2" - resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.2.2.tgz#37ebdbc88a2e1ebc6c8dd3d35ea9436e3e39e477" - integrity sha512-UNTmQ5cSLDeBGBl+s7JeowkqIHgmFAGBnLDdIzFmUNSuS5JF0XBcN59jsh/vJO/YjfsBqMxhMjoFGmNExmf0FA== +"@babel/parser@^7.1.0", "@babel/parser@^7.2.2", "@babel/parser@^7.3.4": + version "7.3.4" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.3.4.tgz#a43357e4bbf4b92a437fb9e465c192848287f27c" + integrity sha512-tXZCqWtlOOP4wgCp6RjRvLmfuhnqTLy9VHwRochJBCP2nDm27JnnuFEnXFASVyQNHk36jD1tAammsCEEqgscIQ== "@babel/plugin-proposal-async-generator-functions@^7.1.0": version "7.2.0" @@ -326,9 +331,9 @@ "@babel/plugin-syntax-object-rest-spread" "^7.0.0" "@babel/plugin-proposal-object-rest-spread@^7.0.0": - version "7.2.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.2.0.tgz#88f5fec3e7ad019014c97f7ee3c992f0adbf7fb8" - integrity sha512-1L5mWLSvR76XYUQJXkd/EEQgjq8HHRP6lQuZTTg0VA4tTGPpGemmCdAfQIz1rzEuWAm+ecP8PyyEm30jC1eQCg== + version "7.3.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.3.4.tgz#47f73cf7f2a721aad5c0261205405c642e424654" + integrity sha512-j7VQmbbkA+qrzNqbKHrBsW3ddFnOeva6wzSe/zB7T+xaxGc+RCpwo44wCmRixAIGRoIpmVgvzFzNJqQcO3/9RA== dependencies: "@babel/helper-plugin-utils" "^7.0.0" "@babel/plugin-syntax-object-rest-spread" "^7.2.0" @@ -407,9 +412,9 @@ "@babel/helper-plugin-utils" "^7.0.0" "@babel/plugin-transform-async-to-generator@^7.1.0": - version "7.2.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.2.0.tgz#68b8a438663e88519e65b776f8938f3445b1a2ff" - integrity sha512-CEHzg4g5UraReozI9D4fblBYABs7IM6UerAVG7EJVrTLC5keh00aEuLUT+O40+mJCEzaXkYfTCUKIyeDfMOFFQ== + version "7.3.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.3.4.tgz#4e45408d3c3da231c0e7b823f407a53a7eb3048c" + integrity sha512-Y7nCzv2fw/jEZ9f678MuKdMo99MFDJMT/PvD9LisrR5JDFcJH6vYeH6RnjVt3p5tceyGRvTtEN0VOlU+rgHZjA== dependencies: "@babel/helper-module-imports" "^7.0.0" "@babel/helper-plugin-utils" "^7.0.0" @@ -423,24 +428,24 @@ "@babel/helper-plugin-utils" "^7.0.0" "@babel/plugin-transform-block-scoping@^7.0.0": - version "7.2.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.2.0.tgz#f17c49d91eedbcdf5dd50597d16f5f2f770132d4" - integrity sha512-vDTgf19ZEV6mx35yiPJe4fS02mPQUUcBNwWQSZFXSzTSbsJFQvHt7DqyS3LK8oOWALFOsJ+8bbqBgkirZteD5Q== + version "7.3.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.3.4.tgz#5c22c339de234076eee96c8783b2fed61202c5c4" + integrity sha512-blRr2O8IOZLAOJklXLV4WhcEzpYafYQKSGT3+R26lWG41u/FODJuBggehtOwilVAcFu393v3OFj+HmaE6tVjhA== dependencies: "@babel/helper-plugin-utils" "^7.0.0" - lodash "^4.17.10" + lodash "^4.17.11" "@babel/plugin-transform-classes@^7.1.0": - version "7.2.2" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-classes/-/plugin-transform-classes-7.2.2.tgz#6c90542f210ee975aa2aa8c8b5af7fa73a126953" - integrity sha512-gEZvgTy1VtcDOaQty1l10T3jQmJKlNVxLDCs+3rCVPr6nMkODLELxViq5X9l+rfxbie3XrfrMCYYY6eX3aOcOQ== + version "7.3.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-classes/-/plugin-transform-classes-7.3.4.tgz#dc173cb999c6c5297e0b5f2277fdaaec3739d0cc" + integrity sha512-J9fAvCFBkXEvBimgYxCjvaVDzL6thk0j0dBvCeZmIUDBwyt+nv6HfbImsSrWsYXfDNDivyANgJlFXDUWRTZBuA== dependencies: "@babel/helper-annotate-as-pure" "^7.0.0" "@babel/helper-define-map" "^7.1.0" "@babel/helper-function-name" "^7.1.0" "@babel/helper-optimise-call-expression" "^7.0.0" "@babel/helper-plugin-utils" "^7.0.0" - "@babel/helper-replace-supers" "^7.1.0" + "@babel/helper-replace-supers" "^7.3.4" "@babel/helper-split-export-declaration" "^7.0.0" globals "^11.1.0" @@ -452,9 +457,9 @@ "@babel/helper-plugin-utils" "^7.0.0" "@babel/plugin-transform-destructuring@^7.0.0": - version "7.2.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.2.0.tgz#e75269b4b7889ec3a332cd0d0c8cff8fed0dc6f3" - integrity sha512-coVO2Ayv7g0qdDbrNiadE4bU7lvCd9H539m2gMknyVjjMdwF/iCOM7R+E8PkntoqLkltO0rk+3axhpp/0v68VQ== + version "7.3.2" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.3.2.tgz#f2f5520be055ba1c38c41c0e094d8a461dd78f2d" + integrity sha512-Lrj/u53Ufqxl/sGxyjsJ2XNtNuEjDyjpqdhMNh5aZ+XFOdThL46KBj27Uem4ggoezSYBxKWAil6Hu8HtwqesYw== dependencies: "@babel/helper-plugin-utils" "^7.0.0" @@ -483,9 +488,9 @@ "@babel/helper-plugin-utils" "^7.0.0" "@babel/plugin-transform-flow-strip-types@^7.0.0": - version "7.2.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-flow-strip-types/-/plugin-transform-flow-strip-types-7.2.0.tgz#db6180d098caaabdd609a8da3800f5204e66b69b" - integrity sha512-xhQp0lyXA5vk8z1kJitdMozQYEWfo4MgC6neNXrb5euqHiTIGhj5ZWfFPkVESInQSk9WZz1bbNmIRz6zKfWGVA== + version "7.3.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-flow-strip-types/-/plugin-transform-flow-strip-types-7.3.4.tgz#00156236defb7dedddc2d3c9477dcc01a4494327" + integrity sha512-PmQC9R7DwpBFA+7ATKMyzViz3zCaMNouzZMPZN2K5PnbBbtL3AXFYTkDk+Hey5crQq2A90UG5Uthz0mel+XZrA== dependencies: "@babel/helper-plugin-utils" "^7.0.0" "@babel/plugin-syntax-flow" "^7.2.0" @@ -530,9 +535,9 @@ "@babel/helper-simple-access" "^7.1.0" "@babel/plugin-transform-modules-systemjs@^7.0.0": - version "7.2.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.2.0.tgz#912bfe9e5ff982924c81d0937c92d24994bb9068" - integrity sha512-aYJwpAhoK9a+1+O625WIjvMY11wkB/ok0WClVwmeo3mCjcNRjt+/8gHWrB5i+00mUju0gWsBkQnPpdvQ7PImmQ== + version "7.3.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.3.4.tgz#813b34cd9acb6ba70a84939f3680be0eb2e58861" + integrity sha512-VZ4+jlGOF36S7TjKs8g4ojp4MEI+ebCQZdswWb/T9I4X84j8OtFAyjXjt/M16iIm5RIZn0UMQgg/VgIwo/87vw== dependencies: "@babel/helper-hoist-variables" "^7.0.0" "@babel/helper-plugin-utils" "^7.0.0" @@ -561,9 +566,9 @@ "@babel/helper-replace-supers" "^7.1.0" "@babel/plugin-transform-parameters@^7.1.0": - version "7.2.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.2.0.tgz#0d5ad15dc805e2ea866df4dd6682bfe76d1408c2" - integrity sha512-kB9+hhUidIgUoBQ0MsxMewhzr8i60nMa2KgeJKQWYrqQpqcBYtnpR+JgkadZVZoaEZ/eKu9mclFaVwhRpLNSzA== + version "7.3.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.3.3.tgz#3a873e07114e1a5bee17d04815662c8317f10e30" + integrity sha512-IrIP25VvXWu/VlBWTpsjGptpomtIkYrN/3aDp4UKm7xK6UxZY88kcJ1UwETbzHAlwN21MnNfwlar0u8y3KpiXw== dependencies: "@babel/helper-call-delegate" "^7.1.0" "@babel/helper-get-function-arity" "^7.0.0" @@ -593,20 +598,20 @@ "@babel/plugin-syntax-jsx" "^7.2.0" "@babel/plugin-transform-react-jsx@^7.0.0": - version "7.2.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.2.0.tgz#ca36b6561c4d3b45524f8efb6f0fbc9a0d1d622f" - integrity sha512-h/fZRel5wAfCqcKgq3OhbmYaReo7KkoJBpt8XnvpS7wqaNMqtw5xhxutzcm35iMUWucfAdT/nvGTsWln0JTg2Q== + version "7.3.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.3.0.tgz#f2cab99026631c767e2745a5368b331cfe8f5290" + integrity sha512-a/+aRb7R06WcKvQLOu4/TpjKOdvVEKRLWFpKcNuHhiREPgGRB4TQJxq07+EZLS8LFVYpfq1a5lDUnuMdcCpBKg== dependencies: - "@babel/helper-builder-react-jsx" "^7.0.0" + "@babel/helper-builder-react-jsx" "^7.3.0" "@babel/helper-plugin-utils" "^7.0.0" "@babel/plugin-syntax-jsx" "^7.2.0" "@babel/plugin-transform-regenerator@^7.0.0": - version "7.0.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.0.0.tgz#5b41686b4ed40bef874d7ed6a84bdd849c13e0c1" - integrity sha512-sj2qzsEx8KDVv1QuJc/dEfilkg3RRPvPYx/VnKLtItVQRWt1Wqf5eVCOLZm29CiGFfYYsA3VPjfizTCV0S0Dlw== + version "7.3.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.3.4.tgz#1601655c362f5b38eead6a52631f5106b29fa46a" + integrity sha512-hvJg8EReQvXT6G9H2MvNPXkv9zK36Vxa1+csAVTpE1J3j0zlHplw76uudEbJxgvqZzAq9Yh45FLD4pk5mKRFQA== dependencies: - regenerator-transform "^0.13.3" + regenerator-transform "^0.13.4" "@babel/plugin-transform-runtime@7.1.0": version "7.1.0" @@ -738,9 +743,9 @@ regenerator-runtime "^0.12.0" "@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2": - version "7.2.0" - resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.2.0.tgz#b03e42eeddf5898e00646e4c840fa07ba8dcad7f" - integrity sha512-oouEibCbHMVdZSDlJBO6bZmID/zA/G/Qx3H1d3rSNPTD+L8UNKvCat7aKWSJ74zYbm5zWGh0GQN0hKj8zYFTCg== + version "7.3.4" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.3.4.tgz#73d12ba819e365fcf7fd152aed56d6df97d21c83" + integrity sha512-IvfvnMdSaLBateu0jfsYIpZTxAc2cKEXEMiezGGN75QcBcecDUKd3PgLAncT0oOgxKy8dd8hrJKj9MfzgfZd6g== dependencies: regenerator-runtime "^0.12.0" @@ -779,20 +784,20 @@ invariant "^2.2.0" lodash "^4.2.0" -"@babel/traverse@^7.1.0", "@babel/traverse@^7.1.5": - version "7.2.2" - resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.2.2.tgz#961039de1f9bcb946d807efe2dba9c92e859d188" - integrity sha512-E5Bn9FSwHpSkUhthw/XEuvFZxIgrqb9M8cX8j5EUQtrUG5DQUy6bFyl7G7iQ1D1Czudor+xkmp81JbLVVM0Sjg== +"@babel/traverse@^7.1.0", "@babel/traverse@^7.1.5", "@babel/traverse@^7.3.4": + version "7.3.4" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.3.4.tgz#1330aab72234f8dea091b08c4f8b9d05c7119e06" + integrity sha512-TvTHKp6471OYEcE/91uWmhR6PrrYywQntCHSaZ8CM8Vmp+pjAusal4nGB2WCCQd0rvI7nOMKn9GnbcvTUz3/ZQ== dependencies: "@babel/code-frame" "^7.0.0" - "@babel/generator" "^7.2.2" + "@babel/generator" "^7.3.4" "@babel/helper-function-name" "^7.1.0" "@babel/helper-split-export-declaration" "^7.0.0" - "@babel/parser" "^7.2.2" - "@babel/types" "^7.2.2" + "@babel/parser" "^7.3.4" + "@babel/types" "^7.3.4" debug "^4.1.0" globals "^11.1.0" - lodash "^4.17.10" + lodash "^4.17.11" "@babel/types@7.0.0-beta.44": version "7.0.0-beta.44" @@ -803,13 +808,13 @@ lodash "^4.2.0" to-fast-properties "^2.0.0" -"@babel/types@^7.0.0", "@babel/types@^7.2.0", "@babel/types@^7.2.2": - version "7.2.2" - resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.2.2.tgz#44e10fc24e33af524488b716cdaee5360ea8ed1e" - integrity sha512-fKCuD6UFUMkR541eDWL+2ih/xFZBXPOg/7EQFeTluMDebfqR4jrpaCjLhkWlQS4hT6nRa2PMEgXKbRB5/H2fpg== +"@babel/types@^7.0.0", "@babel/types@^7.2.0", "@babel/types@^7.2.2", "@babel/types@^7.3.0", "@babel/types@^7.3.4": + version "7.3.4" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.3.4.tgz#bf482eaeaffb367a28abbf9357a94963235d90ed" + integrity sha512-WEkp8MsLftM7O/ty580wAmZzN1nDmCACc5+jFzUt+GUFNNIi3LdRlueYz0YIlmJhlZx1QYDMZL5vdWCL0fNjFQ== dependencies: esutils "^2.0.2" - lodash "^4.17.10" + lodash "^4.17.11" to-fast-properties "^2.0.0" "@cypress/browserify-preprocessor@^1.1.2": @@ -843,14 +848,31 @@ date-fns "^1.27.2" figures "^1.7.0" -"@cypress/xvfb@1.2.3": - version "1.2.3" - resolved "https://registry.yarnpkg.com/@cypress/xvfb/-/xvfb-1.2.3.tgz#6319afdcdcff7d1505daeeaa84484d0596189860" - integrity sha512-yYrK+/bgL3hwoRHMZG4r5fyLniCy1pXex5fimtewAY6vE/jsVs8Q37UsEO03tFlcmiLnQ3rBNMaZBYTi/+C1cw== +"@cypress/xvfb@1.2.4": + version "1.2.4" + resolved "https://registry.yarnpkg.com/@cypress/xvfb/-/xvfb-1.2.4.tgz#2daf42e8275b39f4aa53c14214e557bd14e7748a" + integrity sha512-skbBzPggOVYCbnGgV+0dmBdW/s77ZkAOXIC1knS8NagwDjBrNC1LuXtQJeiN6l+m7lzmHtaoUw/ctJKdqkG57Q== dependencies: debug "^3.1.0" lodash.once "^4.1.1" +"@emotion/is-prop-valid@^0.7.3": + version "0.7.3" + resolved "https://registry.yarnpkg.com/@emotion/is-prop-valid/-/is-prop-valid-0.7.3.tgz#a6bf4fa5387cbba59d44e698a4680f481a8da6cc" + integrity sha512-uxJqm/sqwXw3YPA5GXX365OBcJGFtxUVkB6WyezqFHlNe9jqUWH5ur2O2M8dGBz61kn1g3ZBlzUunFQXQIClhA== + dependencies: + "@emotion/memoize" "0.7.1" + +"@emotion/memoize@0.7.1": + version "0.7.1" + resolved "https://registry.yarnpkg.com/@emotion/memoize/-/memoize-0.7.1.tgz#e93c13942592cf5ef01aa8297444dc192beee52f" + integrity sha512-Qv4LTqO11jepd5Qmlp3M1YEjBumoTHcHFdgPTQ+sFlIL5myi/7xu/POwP7IRu6odBdmLXdtIs1D6TuW6kbwbbg== + +"@emotion/unitless@^0.7.0": + version "0.7.3" + resolved "https://registry.yarnpkg.com/@emotion/unitless/-/unitless-0.7.3.tgz#6310a047f12d21a1036fb031317219892440416f" + integrity sha512-4zAPlpDEh2VwXswwr/t8xGNDGg8RQiPxtxZ3qQEXyQsBV39ptTdESCjuBvGze1nLMVrxmTIKmnO/nAV8Tqjjzg== + "@mrmlnc/readdir-enhanced@^2.2.1": version "2.2.1" resolved "https://registry.yarnpkg.com/@mrmlnc/readdir-enhanced/-/readdir-enhanced-2.2.1.tgz#524af240d1a360527b730475ecfa1344aa540dde" @@ -932,14 +954,6 @@ resolved "https://registry.yarnpkg.com/@protobufjs/utf8/-/utf8-1.1.0.tgz#a777360b5b39a1a2e5106f8e858f2fd2d060c570" integrity sha1-p3c2C1s5oaLlEG+OhY8v0tBgxXA= -"@segment/top-domain@^3.0.0": - version "3.0.0" - resolved "https://registry.yarnpkg.com/@segment/top-domain/-/top-domain-3.0.0.tgz#02e5a5a4fd42a9f6cf886b05e82f104012a3c3a7" - integrity sha1-AuWlpP1CqfbPiGsF6C8QQBKjw6c= - dependencies: - component-cookie "^1.1.2" - component-url "^0.2.1" - "@sendgrid/client@^6.3.0": version "6.3.0" resolved "https://registry.yarnpkg.com/@sendgrid/client/-/client-6.3.0.tgz#25c34b11bec392ab43ca7e52fb35e4105fb00901" @@ -965,6 +979,14 @@ "@sendgrid/client" "^6.3.0" "@sendgrid/helpers" "^6.3.0" +"@tippy.js/react@^2.1.1": + version "2.1.1" + resolved "https://registry.yarnpkg.com/@tippy.js/react/-/react-2.1.1.tgz#3d106963ec81d55098b6ac6f85c9f7b1a76b9fc0" + integrity sha512-rIM5nHuaTek5uEDDD+AjkUEtVCiU6m6Boc5F9bFMiUMp8zEaodZUG7kn/xCD3jR5SfFius3gCdYq0VOZpyypQQ== + dependencies: + prop-types "^15.6.2" + tippy.js "^4.0.3" + "@types/accepts@^1.3.5": version "1.3.5" resolved "https://registry.yarnpkg.com/@types/accepts/-/accepts-1.3.5.tgz#c34bec115cfc746e04fe5a059df4ce7e7b391575" @@ -972,16 +994,6 @@ dependencies: "@types/node" "*" -"@types/blob-util@1.3.3": - version "1.3.3" - resolved "https://registry.yarnpkg.com/@types/blob-util/-/blob-util-1.3.3.tgz#adba644ae34f88e1dd9a5864c66ad651caaf628a" - integrity sha512-4ahcL/QDnpjWA2Qs16ZMQif7HjGP2cw3AGjHabybjw7Vm1EKu+cfQN1D78BaZbS1WJNa1opSMF5HNMztx7lR0w== - -"@types/bluebird@3.5.18": - version "3.5.18" - resolved "https://registry.yarnpkg.com/@types/bluebird/-/bluebird-3.5.18.tgz#6a60435d4663e290f3709898a4f75014f279c4d6" - integrity sha512-OTPWHmsyW18BhrnG5x8F7PzeZ2nFxmHGb42bZn79P9hl+GI5cMzyPgQTwNjbem0lJhoru/8vtjAFCUOu3+gE2w== - "@types/body-parser@*", "@types/body-parser@1.17.0": version "1.17.0" resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.17.0.tgz#9f5c9d9bd04bb54be32d5eb9fc0d8c974e6cf58c" @@ -991,27 +1003,14 @@ "@types/node" "*" "@types/caseless@*": - version "0.12.1" - resolved "https://registry.yarnpkg.com/@types/caseless/-/caseless-0.12.1.tgz#9794c69c8385d0192acc471a540d1f8e0d16218a" - integrity sha512-FhlMa34NHp9K5MY1Uz8yb+ZvuX0pnvn3jScRSNAb75KHGB8d3rEU6hqMs3Z2vjuytcMfRg6c5CHMc3wtYyD2/A== - -"@types/chai-jquery@1.1.35": - version "1.1.35" - resolved "https://registry.yarnpkg.com/@types/chai-jquery/-/chai-jquery-1.1.35.tgz#9a8f0a39ec0851b2768a8f8c764158c2a2568d04" - integrity sha512-7aIt9QMRdxuagLLI48dPz96YJdhu64p6FCa6n4qkGN5DQLHnrIjZpD9bXCvV2G0NwgZ1FAmfP214dxc5zNCfgQ== - dependencies: - "@types/chai" "*" - "@types/jquery" "*" + version "0.12.2" + resolved "https://registry.yarnpkg.com/@types/caseless/-/caseless-0.12.2.tgz#f65d3d6389e01eeb458bd54dc8f52b95a9463bc8" + integrity sha512-6ckxMjBBD8URvjB6J3NcnuAn5Pkl7t3TizAg+xdlzzQGSPSmBcXf8KoIH0ua/i+tio+ZRUHEXp0HEmvaR4kt0w== -"@types/chai@*": - version "4.1.7" - resolved "https://registry.yarnpkg.com/@types/chai/-/chai-4.1.7.tgz#1b8e33b61a8c09cbe1f85133071baa0dbf9fa71a" - integrity sha512-2Y8uPt0/jwjhQ6EiluT0XCri1Dbplr0ZxfFXUz+ye13gaqE8u5gL5ppao1JrUYr9cIip5S6MvQzBS7Kke7U9VA== - -"@types/chai@4.0.8": - version "4.0.8" - resolved "https://registry.yarnpkg.com/@types/chai/-/chai-4.0.8.tgz#d27600e9ba2f371e08695d90a0fe0408d89c7be7" - integrity sha512-m812CONwdZn/dMzkIJEY0yAs4apyTkTORgfB2UsMOxgkUbC205AHnm4T8I0I5gPg9MHrFc1dJ35iS75c0CJkjg== +"@types/clipboard@^2.0.1": + version "2.0.1" + resolved "https://registry.yarnpkg.com/@types/clipboard/-/clipboard-2.0.1.tgz#75a74086c293d75b12bc93ff13bc7797fef05a40" + integrity sha512-gJJX9Jjdt3bIAePQRRjYWG20dIhAgEqonguyHxXuqALxsoDsDLimihqrSg8fXgVTJ4KZCzkfglKtwsh/8dLfbA== "@types/connect@*": version "3.4.32" @@ -1028,23 +1027,22 @@ "@types/express" "*" "@types/events@*": - version "1.2.0" - resolved "https://registry.yarnpkg.com/@types/events/-/events-1.2.0.tgz#81a6731ce4df43619e5c8c945383b3e62a89ea86" - integrity sha512-KEIlhXnIutzKwRbQkGWb/I4HFqBuUykAdHgDED6xqwXJfONCjF5VoE0cXEiurh3XauygxzeDzgtXUqvLkxFzzA== + version "3.0.0" + resolved "https://registry.yarnpkg.com/@types/events/-/events-3.0.0.tgz#2862f3f58a9a7f7c3e78d79f130dd4d71c25c2a7" + integrity sha512-EaObqwIvayI5a8dCzhFrjKzVwKLxjoG9T6Ppd5CEo07LRKfQ8Yokw54r5+Wq7FaBQ+yXRvQAYPrHwya1/UFt9g== "@types/express-serve-static-core@*": - version "4.16.0" - resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.16.0.tgz#fdfe777594ddc1fe8eb8eccce52e261b496e43e7" - integrity sha512-lTeoCu5NxJU4OD9moCgm0ESZzweAx0YqsAcab6OB0EB3+As1OaHtKnaGJvcngQxYsi9UNv0abn4/DRavrRxt4w== + version "4.16.1" + resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.16.1.tgz#35df7b302299a4ab138a643617bd44078e74d44e" + integrity sha512-QgbIMRU1EVRry5cIu1ORCQP4flSYqLM1lS5LYyGWfKnFT3E58f0gKto7BR13clBFVrVZ0G0rbLZ1hUpSkgQQOA== dependencies: - "@types/events" "*" "@types/node" "*" "@types/range-parser" "*" -"@types/express@*", "@types/express@4.16.0": - version "4.16.0" - resolved "https://registry.yarnpkg.com/@types/express/-/express-4.16.0.tgz#6d8bc42ccaa6f35cf29a2b7c3333cb47b5a32a19" - integrity sha512-TtPEYumsmSTtTetAPXlJVf3kEqb6wZK0bZojpJQrnD/djV4q1oB6QQ8aKvKqwNPACoe02GNiy5zDzcYivR5Z2w== +"@types/express@*", "@types/express@4.16.1": + version "4.16.1" + resolved "https://registry.yarnpkg.com/@types/express/-/express-4.16.1.tgz#d756bd1a85c34d87eaf44c888bad27ba8a4b7cf0" + integrity sha512-V0clmJow23WeyblmACoxbHBu2JKlE5TiIme6Lem14FnPW9gsttyHtk6wq7njcdIWH1njAaFgR8gW09lgY98gQg== dependencies: "@types/body-parser" "*" "@types/express-serve-static-core" "*" @@ -1057,22 +1055,14 @@ dependencies: "@types/node" "*" -"@types/jquery@*": - version "3.3.28" - resolved "https://registry.yarnpkg.com/@types/jquery/-/jquery-3.3.28.tgz#e4e4e1a72a890b9d5fce34019d961265dd1394c4" - integrity sha512-6+0asQBU38H5kdoKvxVGE7fY8JREBgQsxONw0na0noV9D3JLN2+odBPKkTvRcLW20xSNiP8BH0nyl+8PcIHYNw== +"@types/glob@^7.1.1": + version "7.1.1" + resolved "https://registry.yarnpkg.com/@types/glob/-/glob-7.1.1.tgz#aa59a1c6e3fbc421e07ccd31a944c30eba521575" + integrity sha512-1Bh06cbWJUHMC97acuD6UMG29nMt0Aqz1vF3guLfG+kHHJhy3AyohZFFxYk2f7Q1SQIrNwvncxAE0N/9s70F2w== dependencies: - "@types/sizzle" "*" - -"@types/jquery@3.3.6": - version "3.3.6" - resolved "https://registry.yarnpkg.com/@types/jquery/-/jquery-3.3.6.tgz#5932ead926307ca21e5b36808257f7c926b06565" - integrity sha512-403D4wN95Mtzt2EoQHARf5oe/jEPhzBOBNrunk+ydQGW8WmkQ/E8rViRAEB1qEt/vssfGfNVD6ujP4FVeegrLg== - -"@types/lodash@4.14.87": - version "4.14.87" - resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.87.tgz#55f92183b048c2c64402afe472f8333f4e319a6b" - integrity sha512-AqRC+aEF4N0LuNHtcjKtvF9OTfqZI0iaBoe3dA6m/W+/YZJBZjBmW/QIZ8fBeXC6cnytSY9tBoFBqZ9uSCeVsw== + "@types/events" "*" + "@types/minimatch" "*" + "@types/node" "*" "@types/long@^4.0.0": version "4.0.0" @@ -1080,24 +1070,24 @@ integrity sha512-1w52Nyx4Gq47uuu0EVcsHBxZFJgurQ+rTKS3qMHxR1GY2T8c2AJYd6vZoZ9q1rupaDjU0yT+Jc2XTyXkjeMA+Q== "@types/mime@*": - version "2.0.0" - resolved "https://registry.yarnpkg.com/@types/mime/-/mime-2.0.0.tgz#5a7306e367c539b9f6543499de8dd519fac37a8b" - integrity sha512-A2TAGbTFdBw9azHbpVd+/FkdW2T6msN1uct1O9bH3vTerEHKZhTXJUQXy+hNq1B0RagfU8U+KBdqiZpxjhOUQA== + version "2.0.1" + resolved "https://registry.yarnpkg.com/@types/mime/-/mime-2.0.1.tgz#dc488842312a7f075149312905b5e3c0b054c79d" + integrity sha512-FwI9gX75FgVBJ7ywgnq/P7tw+/o1GUbtP0KzbtusLigAOgIgNISRK0ZPl4qertvXSIE8YbsVJueQ90cDt9YYyw== -"@types/minimatch@3.0.3": +"@types/minimatch@*": version "3.0.3" resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.3.tgz#3dca0e3f33b200fc7d1139c0cd96c1268cadfd9d" integrity sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA== -"@types/mocha@2.2.44": - version "2.2.44" - resolved "https://registry.yarnpkg.com/@types/mocha/-/mocha-2.2.44.tgz#1d4a798e53f35212fd5ad4d04050620171cd5b5e" - integrity sha512-k2tWTQU8G4+iSMvqKi0Q9IIsWAp/n8xzdZS4Q4YVIltApoMA00wFBFdlJnmoaK1/z7B0Cy0yPe6GgXteSmdUNw== +"@types/node@*": + version "11.11.3" + resolved "https://registry.yarnpkg.com/@types/node/-/node-11.11.3.tgz#7c6b0f8eaf16ae530795de2ad1b85d34bf2f5c58" + integrity sha512-wp6IOGu1lxsfnrD+5mX6qwSwWuqsdkKKxTN4aQc4wByHAKZJf9/D4KXPQ1POUjEbnCP5LMggB0OEFNY9OTsMqg== -"@types/node@*", "@types/node@^10.1.0": - version "10.12.18" - resolved "https://registry.yarnpkg.com/@types/node/-/node-10.12.18.tgz#1d3ca764718915584fcd9f6344621b7672665c67" - integrity sha512-fh+pAqt4xRzPfqA6eh3Z2y6fyZavRIumvjhaCL753+TVkGKGhpPeyrJG2JftD0T9q4GF00KjefsQ+PQNDdWQaQ== +"@types/node@^10.1.0": + version "10.14.1" + resolved "https://registry.yarnpkg.com/@types/node/-/node-10.14.1.tgz#8701cd760acc20beba5ffe0b7a1b879f39cb8c41" + integrity sha512-Rymt08vh1GaW4vYB6QP61/5m/CFLGnFZP++bJpWbiNxceNa6RBipDmb413jvtSf/R1gg5a/jQVl2jY4XVRscEA== "@types/range-parser@*": version "1.2.3" @@ -1136,33 +1126,10 @@ "@types/express-serve-static-core" "*" "@types/mime" "*" -"@types/sinon-chai@3.2.2": - version "3.2.2" - resolved "https://registry.yarnpkg.com/@types/sinon-chai/-/sinon-chai-3.2.2.tgz#5cfdbda70bae30f79a9423334af9e490e4cce793" - integrity sha512-5zSs2AslzyPZdOsbm2NRtuSNAI2aTWzNKOHa/GRecKo7a5efYD7qGcPxMZXQDayVXT2Vnd5waXxBvV31eCZqiA== - dependencies: - "@types/chai" "*" - "@types/sinon" "*" - -"@types/sinon@*": - version "7.0.1" - resolved "https://registry.yarnpkg.com/@types/sinon/-/sinon-7.0.1.tgz#031a95fa22c802b98de5dc039719a8e0b32c3079" - integrity sha512-pSv29992MQ+V/95qbbH3dQIs+V19enjR/NhjnUVV4PDAP6buMxNFRvh56KpqSenb1so8zsXk73wkiYunxMi1bQ== - -"@types/sinon@7.0.0": - version "7.0.0" - resolved "https://registry.yarnpkg.com/@types/sinon/-/sinon-7.0.0.tgz#84e707e157ec17d3e4c2a137f41fc3f416c0551e" - integrity sha512-kcYoPw0uKioFVC/oOqafk2yizSceIQXCYnkYts9vJIwQklFRsMubTObTDrjQamUyBRd47332s85074cd/hCwxg== - -"@types/sizzle@*": - version "2.3.2" - resolved "https://registry.yarnpkg.com/@types/sizzle/-/sizzle-2.3.2.tgz#a811b8c18e2babab7d542b3365887ae2e4d9de47" - integrity sha512-7EJYyKTL7tFR8+gDbB6Wwz/arpGa0Mywk1TJbNzKzHtzbwVmY4HR9WqS5VV7dsBUKQmPNr192jHr/VpBluj/hg== - "@types/tough-cookie@*": - version "2.3.4" - resolved "https://registry.yarnpkg.com/@types/tough-cookie/-/tough-cookie-2.3.4.tgz#821878b81bfab971b93a265a561d54ea61f9059f" - integrity sha512-Set5ZdrAaKI/qHdFlVMgm/GsAv/wkXhSTuZFkJ+JI7HK+wIkIlOaUXSXieIvJ0+OvGIqtREFoE+NHJtEq0gtEw== + version "2.3.5" + resolved "https://registry.yarnpkg.com/@types/tough-cookie/-/tough-cookie-2.3.5.tgz#9da44ed75571999b65c37b60c9b2b88db54c585d" + integrity sha512-SCcK7mvGi3+ZNz833RRjFIxrn4gI1PPR3NtuIS+6vMkvmsGjosqTJwRt5bAEFLRz+wtJMWv8+uOnZf2hi2QXTg== "@types/ws@^6.0.0": version "6.0.1" @@ -1230,7 +1197,7 @@ acorn-jsx@^3.0.0: dependencies: acorn "^3.0.4" -acorn-node@^1.2.0, acorn-node@^1.3.0, acorn-node@^1.5.2: +acorn-node@^1.2.0, acorn-node@^1.3.0, acorn-node@^1.5.2, acorn-node@^1.6.1: version "1.6.2" resolved "https://registry.yarnpkg.com/acorn-node/-/acorn-node-1.6.2.tgz#b7d7ceca6f22e6417af933a62cad4de01048d5d2" integrity sha512-rIhNEZuNI8ibQcL7ANm/mGyPukIaZsRNX9psFNQURyJW0nu6k8wjSDld20z6v2mDBWqX13pIEnk9gGZJHIlEXg== @@ -1261,9 +1228,9 @@ acorn@^5.0.0, acorn@^5.3.0, acorn@^5.5.0, acorn@^5.5.3: integrity sha512-T/zvzYRfbVojPWahDsE5evJdHb3oJoQfFbsrKM7w5Zcs++Tr257tia3BmMP8XYVjp1S9RZXQMh7gao96BlqZOw== acorn@^6.0.1, acorn@^6.0.2: - version "6.0.4" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.0.4.tgz#77377e7353b72ec5104550aa2d2097a2fd40b754" - integrity sha512-VY4i5EKSKkofY2I+6QLTbTTN/UvEQPCo6eiwzzSaSWfpaDhOmStMCMod6wmuPciNq+XS0faCglFu2lHZpdHUtg== + version "6.1.1" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.1.1.tgz#7d25ae05bb8ad1f9b699108e1094ecd7884adc1f" + integrity sha512-jPTiwtOxaHNaAPg/dmrJ/beuzLRnXtB0kQPQ8JpotKJgTB6rX6c8mlf315941pyjBSaPg8NHXS9fhP4u17DpGA== address@1.0.3, address@^1.0.1: version "1.0.3" @@ -1288,11 +1255,11 @@ ajv-keywords@^2.0.0, ajv-keywords@^2.1.0: integrity sha1-YXmX/F9gV2iUxDX5QNgZ4TW4B2I= ajv-keywords@^3.0.0, ajv-keywords@^3.1.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.2.0.tgz#e86b819c602cf8821ad637413698f1dec021847a" - integrity sha1-6GuBnGAs+IIa1jdBNpjx3sAhhHo= + version "3.4.0" + resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.4.0.tgz#4b831e7b531415a7cc518cd404e73f6193c6349d" + integrity sha512-aUjdRFISbuFOl0EIZc+9e4FfZp0bDZgAdOOf30bJmw8VM9v84SHyVyxDfbWxpGYbdZD/9XoKxfHVNmxPkhwyGw== -ajv@^5.0.0, ajv@^5.1.0, ajv@^5.1.5, ajv@^5.2.0, ajv@^5.2.3, ajv@^5.3.0: +ajv@^5.0.0, ajv@^5.1.5, ajv@^5.2.0, ajv@^5.2.3, ajv@^5.3.0: version "5.5.2" resolved "https://registry.yarnpkg.com/ajv/-/ajv-5.5.2.tgz#73b5eeca3fab653e3d3f9422b341ad42205dc965" integrity sha1-c7Xuyj+rZT49P5Qis0GtQiBdyWU= @@ -1303,9 +1270,9 @@ ajv@^5.0.0, ajv@^5.1.0, ajv@^5.1.5, ajv@^5.2.0, ajv@^5.2.3, ajv@^5.3.0: json-schema-traverse "^0.3.0" ajv@^6.0.1, ajv@^6.1.0, ajv@^6.5.5: - version "6.6.2" - resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.6.2.tgz#caceccf474bf3fc3ce3b147443711a24063cc30d" - integrity sha512-FBHEW6Jf5TB9MGBgUUA9XHkTbjXYfAUjY43ACMfmdMRHniyoMHjHjzD50OK8LGDWQwp4rWEsIq5kEqq7rvIM1g== + version "6.10.0" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.10.0.tgz#90d0d54439da587cd7e843bfb7045f50bd22bdf1" + integrity sha512-nffhOpkymDECQyR0mnsUtoCE8RlX38G0rYP+wgLWFyZuUyuuojSSvi/+euOiQBIn63whYwYVIIH1TvE3tu4OEg== dependencies: fast-deep-equal "^2.0.1" fast-json-stable-stringify "^2.0.0" @@ -1348,15 +1315,14 @@ alphanum-sort@^1.0.1, alphanum-sort@^1.0.2: integrity sha1-l6ERlkmyEa0zaR2fn0hqjsn74KM= amplitude-js@^4.4.0: - version "4.5.2" - resolved "https://registry.yarnpkg.com/amplitude-js/-/amplitude-js-4.5.2.tgz#17fd0f1c6a8a1afdbde1a36944dc9dbaa24805c1" - integrity sha512-J075hRBuhCuBqwrhmuGIXg7zCLRO6TvONTJUESpTkM1LVL5bMcTx9BczW4Hh6p6kjBQjs2fD4rNbhPs48kYZwA== + version "4.7.0" + resolved "https://registry.yarnpkg.com/amplitude-js/-/amplitude-js-4.7.0.tgz#82a3f23d0dbe0e7ad3e319a974bd102389713fbb" + integrity sha512-4ZlJjZafznb6TDh/Bwr2DYWDDmivsrayqXoCK5p/1T59eqNHuEea41QwWde4S7kKz+CEeq4piaCS3sTQT1jJRg== dependencies: - "@segment/top-domain" "^3.0.0" + "@amplitude/ua-parser-js" "0.7.11" blueimp-md5 "^2.10.0" json3 "^3.3.2" - lodash "^4.17.4" - ua-parser-js "github:amplitude/ua-parser-js#ed538f1" + query-string "5" amplitude@^3.5.0: version "3.5.0" @@ -1378,9 +1344,9 @@ ansi-escapes@^1.0.0: integrity sha1-06ioOzGapneTZisT52HHkRQiMG4= ansi-escapes@^3.0.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-3.1.0.tgz#f73207bb81207d75fd6c83f125af26eea378ca30" - integrity sha512-UgAb8H9D41AQnu/PbWlCofQVcnV4Gs2bBJi9eZPxfU/hgglFh3SMDMENRIqdr7H6XFnXdoknctFByVsCOotTVw== + version "3.2.0" + resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-3.2.0.tgz#8780b98ff9dbf5638152d1f1fe5c1d7b4442976b" + integrity sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ== ansi-html@0.0.7: version "0.0.7" @@ -1410,9 +1376,9 @@ ansi-styles@^3.0.0, ansi-styles@^3.2.0, ansi-styles@^3.2.1: color-convert "^1.9.0" ansy@^1.0.0: - version "1.0.13" - resolved "https://registry.yarnpkg.com/ansy/-/ansy-1.0.13.tgz#972cbd54c525112f36814fdefe26269ef993810f" - integrity sha512-zO9+N/z1cEDQOjJSaqeUQTorTfl1sTfjx7qlQ/IMIE//UgDfmVqrRy9Aop+m0gFZe2+1VpqXw5PZXSbS/LQRVA== + version "1.0.14" + resolved "https://registry.yarnpkg.com/ansy/-/ansy-1.0.14.tgz#7df7d0194a4cc2d97fd0204bb7faa0e6b29e7eb0" + integrity sha512-6EZU3oFiAFR5KdxMfBC7L9A5WtMSO9rXietMuQ0STnHx2n2qgrDld+7JkT2j9FHWRVOHekdH5nMs5Hry4oJzyg== dependencies: ansi-styles "^3.0.0" custom-return "^1.0.0" @@ -1435,13 +1401,13 @@ anymatch@^2.0.0: micromatch "^3.1.4" normalize-path "^2.1.1" -apollo-cache-control@0.4.0: - version "0.4.0" - resolved "https://registry.yarnpkg.com/apollo-cache-control/-/apollo-cache-control-0.4.0.tgz#fec343e6ec95aa4f1b88e07e62f067bee0c48397" - integrity sha512-WuriaNQIugTE8gYwfBWWCbbQTSKul/cV4JMi5UgqNIUvjHvnKZQLKbt5uYWow6QQNMkLT9hey8QPYkWpogkeSA== +apollo-cache-control@0.6.0-alpha.0: + version "0.6.0-alpha.0" + resolved "https://registry.npmjs.org/apollo-cache-control/-/apollo-cache-control-0.6.0-alpha.0.tgz#ec9bc985b16150bb35a5d2ea874ac8c1e6ff415f" + integrity sha512-38FF+0kGkN6/efPWYda+CNQhwnY7Ee3k0am9SepI395VBKO7eXdLv1tBttwLh/Sn6sIeP7OT+DVhYBcrxdqKKA== dependencies: - apollo-server-env "2.2.0" - graphql-extensions "0.4.0" + apollo-server-env "2.3.0-alpha.0" + graphql-extensions "0.6.0-alpha.0" apollo-cache-inmemory@1.3.12: version "1.3.12" @@ -1452,7 +1418,7 @@ apollo-cache-inmemory@1.3.12: apollo-utilities "^1.0.27" optimism "^0.6.8" -apollo-cache@1.2.1: +apollo-cache@1.2.1, apollo-cache@^1.1.22: version "1.2.1" resolved "https://registry.yarnpkg.com/apollo-cache/-/apollo-cache-1.2.1.tgz#aae71eb4a11f1f7322adc343f84b1a39b0693644" integrity sha512-nzFmep/oKlbzUuDyz6fS6aYhRmfpcHWqNkkA9Bbxwk18RD6LXC4eZkuE0gXRX0IibVBHNjYVK+Szi0Yied4SpQ== @@ -1460,13 +1426,6 @@ apollo-cache@1.2.1: apollo-utilities "^1.2.1" tslib "^1.9.3" -apollo-cache@^1.1.22: - version "1.1.22" - resolved "https://registry.yarnpkg.com/apollo-cache/-/apollo-cache-1.1.22.tgz#d4682ea6e8b2508a934f61c2fd9e36b4a65041d9" - integrity sha512-8PoxhQLISj2oHwT7i/r4l+ly4y3RKZls+dtXzAewu3U77P9dNZKhYkRNAhx9iEfsrNoHgXBV8vMp64hb1uYh+g== - dependencies: - apollo-utilities "^1.0.27" - apollo-client@^2.5.1: version "2.5.1" resolved "https://registry.yarnpkg.com/apollo-client/-/apollo-client-2.5.1.tgz#36126ed1d32edd79c3713c6684546a3bea80e6d1" @@ -1482,122 +1441,166 @@ apollo-client@^2.5.1: tslib "^1.9.3" zen-observable "^0.8.0" -apollo-datasource@0.2.1: - version "0.2.1" - resolved "https://registry.yarnpkg.com/apollo-datasource/-/apollo-datasource-0.2.1.tgz#3ecef4efe64f7a04a43862f32027d38ac09e142c" - integrity sha512-r185+JTa5KuF1INeTAk7AEP76zwMN6c8Ph1lmpzJMNwBUEzTGnLClrccCskCBx4SxfnkdKbuQdwn9JwCJUWrdg== +apollo-datasource@0.4.0-alpha.0: + version "0.4.0-alpha.0" + resolved "https://registry.npmjs.org/apollo-datasource/-/apollo-datasource-0.4.0-alpha.0.tgz#4f5a6d1e6ab50b4ab6f2878fb3815c8be5abf0f6" + integrity sha512-vAe/zFRLX8JdIXp1oHioYy6Kx4+19tWYMgRYu2/PdUaC3P3SbBGBEBBdm1HXPiVWBZkw+uBeoVv5MiwgtwyNFQ== dependencies: - apollo-server-caching "0.2.1" - apollo-server-env "2.2.0" + apollo-server-caching "0.4.0-alpha.0" + apollo-server-env "2.3.0-alpha.0" -apollo-engine-reporting-protobuf@0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/apollo-engine-reporting-protobuf/-/apollo-engine-reporting-protobuf-0.2.0.tgz#2aaf4d2eddefe7924d469cf1135267bc0deadf73" - integrity sha512-qI+GJKN78UMJ9Aq/ORdiM2qymZ5yswem+/VDdVFocq+/e1QqxjnpKjQWISkswci5+WtpJl9SpHBNxG98uHDKkA== +apollo-engine-reporting-protobuf@0.3.0-alpha.0: + version "0.3.0-alpha.0" + resolved "https://registry.npmjs.org/apollo-engine-reporting-protobuf/-/apollo-engine-reporting-protobuf-0.3.0-alpha.0.tgz#9aca6b57c6fb0f9f5c4c1a1ba1944ec32a50886d" + integrity sha512-zmoZiqjLJ8ZI5hu7+TJoeWAUDjNJEFGPlLDXiXaEFz0hx9kMCmuskJp27lVt3T7FtfyBvVJcwJz6mIGugq7ZMg== dependencies: protobufjs "^6.8.6" -apollo-engine-reporting@0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/apollo-engine-reporting/-/apollo-engine-reporting-0.2.0.tgz#e71816b1f46e782f8538c5a118148d4c0e628e25" - integrity sha512-Q6FfVb10v/nrv8FaFsPjIYlWh62jaYav3LuMgM9PsHWGK/zRQFXOEwLxcY2UCvG7O1moxF3XGmfBhMgo54py+Q== +apollo-engine-reporting@1.1.0-alpha.0: + version "1.1.0-alpha.0" + resolved "https://registry.npmjs.org/apollo-engine-reporting/-/apollo-engine-reporting-1.1.0-alpha.0.tgz#4129c035a7325bade5cd04cf88f12c985df28dac" + integrity sha512-4qWGF7FoedbFumgmdAa1DKWUjByOD7BMmP/o1p0QoGP3sXGuw0hlRKYTtrZhAg7AsIGi+HYcWTKUjd5wJRuMRQ== dependencies: - apollo-engine-reporting-protobuf "0.2.0" - apollo-server-env "2.2.0" + apollo-engine-reporting-protobuf "0.3.0-alpha.0" + apollo-graphql "^0.2.0" + apollo-server-core "2.5.0-alpha.0" + apollo-server-env "2.3.0-alpha.0" async-retry "^1.2.1" - graphql-extensions "0.4.0" - lodash "^4.17.10" + graphql-extensions "0.6.0-alpha.0" -apollo-env@0.2.5: - version "0.2.5" - resolved "https://registry.yarnpkg.com/apollo-env/-/apollo-env-0.2.5.tgz#162c785bccd2aea69350a7600fab4b7147fc9da5" - integrity sha512-Gc7TEbwCl7jJVutnn8TWfzNSkrrqyoo0DP92BQJFU9pZbJhpidoXf2Sw1YwOJl82rRKH3ujM3C8vdZLOgpFcFA== +apollo-env@0.3.4: + version "0.3.4" + resolved "https://registry.yarnpkg.com/apollo-env/-/apollo-env-0.3.4.tgz#c66d7fc295c01c997cfab9ada1f5e450016e9fc0" + integrity sha512-Hq3BQFhINpLB4iY1HTgT/c6+4g9JXB0Otiz+w2gVBKSdfsratRKbpBxBsIgGlAomk92iQ4/YkZB78wsQNd69Rw== dependencies: - core-js "^3.0.0-beta.3" + core-js "3.0.0-beta.13" node-fetch "^2.2.0" +apollo-env@0.4.0: + version "0.4.0" + resolved "https://registry.npmjs.org/apollo-env/-/apollo-env-0.4.0.tgz#f26c8570cc66edc3606d0cf9b66dbc1770b99353" + integrity sha512-TZpk59RTbXd8cEqwmI0KHFoRrgBRplvPAP4bbRrX4uDSxXvoiY0Y6tQYUlJ35zi398Hob45mXfrZxeRDzoFMkQ== + dependencies: + core-js "3.0.0-beta.13" + node-fetch "^2.2.0" + sha.js "^2.4.11" + +apollo-graphql@^0.2.0: + version "0.2.0" + resolved "https://registry.npmjs.org/apollo-graphql/-/apollo-graphql-0.2.0.tgz#74d3a84b84fa745716363a38e4ff1022f90ab5e1" + integrity sha512-wwKynD31Yw1L93IAtnEyhSxBhK4X7NXqkY6wBKWRQ4xph5uJKGgmcQmq3sPieKJT91BGL4AQBv+cwGD3blbLNA== + dependencies: + apollo-env "0.4.0" + lodash.sortby "^4.7.0" + apollo-link-dedup@^1.0.0: - version "1.0.13" - resolved "https://registry.yarnpkg.com/apollo-link-dedup/-/apollo-link-dedup-1.0.13.tgz#bb22957e18b6125ae8bfb46cab6bda8d33ba8046" - integrity sha512-i4NuqT3DSFczFcC7NMUzmnYjKX7NggLY+rqYVf+kE9JjqKOQhT6wqhaWsVIABfIUGE/N0DTgYJBCMu/18aXmYA== + version "1.0.16" + resolved "https://registry.yarnpkg.com/apollo-link-dedup/-/apollo-link-dedup-1.0.16.tgz#7d1a883329ca04edf925746b5a46f72bdd0c96b6" + integrity sha512-ckPrS0jU2ad8qTXw8GXh3YHZkbnjtsjbCOPT0QmLRM0GpESFTqhoTGl2gBxIlHZE5/Vw/L6NY8u2JAMw6tcy/A== dependencies: - apollo-link "^1.2.6" + apollo-link "^1.2.9" + tslib "^1.9.3" -apollo-link-http-common@^0.2.5, apollo-link-http-common@^0.2.8: - version "0.2.8" - resolved "https://registry.yarnpkg.com/apollo-link-http-common/-/apollo-link-http-common-0.2.8.tgz#c6deedfc2739db8b11013c3c2d2ccd657152941f" - integrity sha512-gGmXZN8mr7e9zjopzKQfZ7IKnh8H12NxBDzvp9nXI3U82aCVb72p+plgoYLcpMY8w6krvoYjgicFmf8LO20TCQ== +apollo-link-http-common@^0.2.11, apollo-link-http-common@^0.2.5: + version "0.2.11" + resolved "https://registry.yarnpkg.com/apollo-link-http-common/-/apollo-link-http-common-0.2.11.tgz#d4e494ed1e45ea0e0c0ed60f3df64541d0de682d" + integrity sha512-FjtzEDiG6blH/2MR4fpVNoxdZUFmddP0sez34qnoLaYz6ABFbTDlmRE/dVN79nPExM4Spfs/DtW7KRqyjJ3tOg== dependencies: - apollo-link "^1.2.6" + apollo-link "^1.2.9" + ts-invariant "^0.3.2" + tslib "^1.9.3" apollo-link-http@^1.5.7: - version "1.5.9" - resolved "https://registry.yarnpkg.com/apollo-link-http/-/apollo-link-http-1.5.9.tgz#9046f5640a94c8a8b508a39e0f2c628b781baecc" - integrity sha512-9tJy2zGm4Cm/1ycScDNZJe51dgnTSfKx7pKIgPZmcxkdDpgUY2DZitDH6ZBv4yp9z8MC9Xr9wgwc29s6hcadUQ== + version "1.5.12" + resolved "https://registry.yarnpkg.com/apollo-link-http/-/apollo-link-http-1.5.12.tgz#878d48bf9d8ae091752710529a222c4a5548118e" + integrity sha512-2tS36RIU6OdxzoWYTPrjvDTF2sCrnlaJ6SL7j0ILPn1Lmw4y6YLwKDsv/SWLwtodtVe9v1dLCGKIGMRMM/SdyA== dependencies: - apollo-link "^1.2.6" - apollo-link-http-common "^0.2.8" + apollo-link "^1.2.9" + apollo-link-http-common "^0.2.11" + tslib "^1.9.3" apollo-link-retry@^2.2.6: - version "2.2.8" - resolved "https://registry.yarnpkg.com/apollo-link-retry/-/apollo-link-retry-2.2.8.tgz#84be877118687285a346cad60af6a9e707afbf2f" - integrity sha512-bwGAjhYwuQuim6QhQYZ4LeHA+1bXmdGmd6s1LpTziR46sZIuPEEuaxTwvyZqrjbj8CkMCx5shcZeheQ8D/ZTmA== + version "2.2.11" + resolved "https://registry.yarnpkg.com/apollo-link-retry/-/apollo-link-retry-2.2.11.tgz#f9b8cb252a711f7bcd45c0e08337128c68f9b95b" + integrity sha512-D0sxoV8Yy3YikWAwpq0vvKE0vnVZMaduuq3m0CYVqkHFB4cx/sdWPBxLIciCoKYUmMqBZPji5zPY0VFlvBTRbw== dependencies: "@types/zen-observable" "0.8.0" - apollo-link "^1.2.6" + apollo-link "^1.2.9" + tslib "^1.9.3" apollo-link-schema@^1.1.2: - version "1.1.4" - resolved "https://registry.yarnpkg.com/apollo-link-schema/-/apollo-link-schema-1.1.4.tgz#23877dd4f513a0403cecc561b5a9b7886f2f0a45" - integrity sha512-X6fW5ZxG64iOMosejwkdKn6NCVTYsbIO/jvvr2WqqMzUKgU4qzhOfYAFPYpoJf11HO7oRMQj6HRS6rrteisX2A== + version "1.2.0" + resolved "https://registry.yarnpkg.com/apollo-link-schema/-/apollo-link-schema-1.2.0.tgz#6ca0fbc909fab3c722807136f7cd8cf6f0f6c836" + integrity sha512-cuf9ZHSiMZrBpT9SzW2uRJFgLOirv9x07AB+RdXDi93nhhJFlZrAyFoEtW0IGd9rVBPfm6B97KEvXxst/pq51Q== dependencies: - apollo-link "^1.2.6" + apollo-link "^1.2.9" + tslib "^1.9.3" apollo-link-ws@^1.0.10: - version "1.0.12" - resolved "https://registry.yarnpkg.com/apollo-link-ws/-/apollo-link-ws-1.0.12.tgz#e343bb0c071f2db0ae147a2327f03cc1740d0c2d" - integrity sha512-BjbskhfuuIgk9e4XHdrqmjxkY+RkD1tuerrs4PLiPTkJYcQrvA8t27lGBSrDUKHWH4esCdhQF1UhKPwhlouEHw== + version "1.0.15" + resolved "https://registry.yarnpkg.com/apollo-link-ws/-/apollo-link-ws-1.0.15.tgz#1a0132ee3c700640d64c5b00ad8cff5cb974ff62" + integrity sha512-zXYfvKBpgf7QXIIEO11qgKLYyodo1mDkJr2IojcvOqbGGtqi7+DtOxX21/mrM2pzcwczYGCSDrtFhas4A7RnNA== dependencies: - apollo-link "^1.2.6" + apollo-link "^1.2.9" + tslib "^1.9.3" -apollo-link@^1.0.0, apollo-link@^1.2.3, apollo-link@^1.2.4, apollo-link@^1.2.6: - version "1.2.6" - resolved "https://registry.yarnpkg.com/apollo-link/-/apollo-link-1.2.6.tgz#d9b5676d79c01eb4e424b95c7171697f6ad2b8da" - integrity sha512-sUNlA20nqIF3gG3F8eyMD+mO80fmf3dPZX+GUOs3MI9oZR8ug09H3F0UsWJMcpEg6h55Yy5wZ+BMmAjrbenF/Q== +apollo-link@^1.0.0, apollo-link@^1.2.3, apollo-link@^1.2.4, apollo-link@^1.2.9: + version "1.2.9" + resolved "https://registry.yarnpkg.com/apollo-link/-/apollo-link-1.2.9.tgz#40a8f0b90716ce3fd6beb27b7eae1108b92e0054" + integrity sha512-ZLUwthOFZq4lxchQ2jeBfVqS/UDdcVmmh8aUw6Ar9awZH4r+RgkcDeu2ooFLUfodWE3mZr7wIZuYsBas/MaNVA== dependencies: - apollo-utilities "^1.0.0" - zen-observable-ts "^0.8.13" + apollo-utilities "^1.2.1" + ts-invariant "^0.3.2" + tslib "^1.9.3" + zen-observable-ts "^0.8.16" -apollo-server-caching@0.2.1: - version "0.2.1" - resolved "https://registry.yarnpkg.com/apollo-server-caching/-/apollo-server-caching-0.2.1.tgz#7e67f8c8cac829e622b394f0fb82579cabbeadfd" - integrity sha512-+U9F3X297LL8Gqy6ypfDNEv/DfV/tDht9Dr2z3AMaEkNW1bwO6rmdDL01zYxDuVDVq6Z3qSiNCSO2pXE2F0zmA== +apollo-server-cache-redis@^0.3.1: + version "0.3.1" + resolved "https://registry.yarnpkg.com/apollo-server-cache-redis/-/apollo-server-cache-redis-0.3.1.tgz#207fea925df7ad237918a8ce6978499e444377c4" + integrity sha512-1Wgxu3oOnraox/0vQ2BVszPm20FdZDXEVu6yCoHNkT/zFOYMR/KjqufF7ddlttdSsaQsXqlu0127fWHvX6NDAA== + dependencies: + apollo-server-caching "0.3.1" + apollo-server-env "2.2.0" + dataloader "^1.4.0" + redis "^2.8.0" + +apollo-server-caching@0.3.1: + version "0.3.1" + resolved "https://registry.yarnpkg.com/apollo-server-caching/-/apollo-server-caching-0.3.1.tgz#63fcb2aaa176e1e101b36a8450e6b4c593d2767a" + integrity sha512-mfxzikYXbB/OoEms77AGYwRh7FF3Oim5v5XWAL+VL49FrkbZt5lopVa4bABi7Mz8Nt3Htl9EBJN8765s/yh8IA== + dependencies: + lru-cache "^5.0.0" + +apollo-server-caching@0.4.0-alpha.0: + version "0.4.0-alpha.0" + resolved "https://registry.npmjs.org/apollo-server-caching/-/apollo-server-caching-0.4.0-alpha.0.tgz#24425b0081deb871e45e0f0b16fe6c3f3e8bed7f" + integrity sha512-E8YfrUgw7xzI7lPxJ9DdLBKP6zVoGyn+h57liMMasmbdWqc8R7VixNzkskYivq83R5wGiIPjYP9iKuotJGmTaA== dependencies: lru-cache "^5.0.0" -apollo-server-core@2.3.1: - version "2.3.1" - resolved "https://registry.yarnpkg.com/apollo-server-core/-/apollo-server-core-2.3.1.tgz#cbdc0020a0dfecf2220cf5062dbb304fdf56edf2" - integrity sha512-8jMWYOQIZi9mDJlHe2rXg8Cp4xKYogeRu23jkcNy+k5UjZL+eO+kHXbNFiTaP4HLYYEpe2XE3asxp6q5YUEQeQ== +apollo-server-core@2.5.0-alpha.0: + version "2.5.0-alpha.0" + resolved "https://registry.npmjs.org/apollo-server-core/-/apollo-server-core-2.5.0-alpha.0.tgz#4e007c07e9b59329723241751b4c6eb28d925796" + integrity sha512-2c0OxKyV3nQDNxLeSApaSEzIXnzcgFqOXlsV4Jr+cNffzgKocoTDOnkMHHuI/QqAIDn3BmdmLTNLJx5cOCahOA== dependencies: - "@apollographql/apollo-tools" "^0.2.6" + "@apollographql/apollo-tools" "^0.3.3" "@apollographql/graphql-playground-html" "^1.6.6" "@types/ws" "^6.0.0" - apollo-cache-control "0.4.0" - apollo-datasource "0.2.1" - apollo-engine-reporting "0.2.0" - apollo-server-caching "0.2.1" - apollo-server-env "2.2.0" - apollo-server-errors "2.2.0" - apollo-server-plugin-base "0.2.1" - apollo-tracing "0.4.0" - graphql-extensions "0.4.1" + apollo-cache-control "0.6.0-alpha.0" + apollo-datasource "0.4.0-alpha.0" + apollo-engine-reporting "1.1.0-alpha.0" + apollo-server-caching "0.4.0-alpha.0" + apollo-server-env "2.3.0-alpha.0" + apollo-server-errors "2.2.1" + apollo-server-plugin-base "0.4.0-alpha.0" + apollo-tracing "0.6.0-alpha.0" + fast-json-stable-stringify "^2.0.0" + graphql-extensions "0.6.0-alpha.0" graphql-subscriptions "^1.0.0" graphql-tag "^2.9.2" graphql-tools "^4.0.0" graphql-upload "^8.0.2" - json-stable-stringify "^1.0.1" - lodash "^4.17.10" + sha.js "^2.4.11" subscriptions-transport-ws "^0.9.11" ws "^6.0.0" @@ -1609,41 +1612,59 @@ apollo-server-env@2.2.0: node-fetch "^2.1.2" util.promisify "^1.0.0" -apollo-server-errors@2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/apollo-server-errors/-/apollo-server-errors-2.2.0.tgz#5b452a1d6ff76440eb0f127511dc58031a8f3cb5" - integrity sha512-gV9EZG2tovFtT1cLuCTavnJu2DaKxnXPRNGSTo+SDI6IAk6cdzyW0Gje5N2+3LybI0Wq5KAbW6VLei31S4MWmg== +apollo-server-env@2.3.0-alpha.0: + version "2.3.0-alpha.0" + resolved "https://registry.npmjs.org/apollo-server-env/-/apollo-server-env-2.3.0-alpha.0.tgz#0abe5bdb814c68ae735d32c6f81918ed1abb757b" + integrity sha512-ml35SHu3SGsbohpl23Hk7mFpEWPGR9hmalSJ0ek1mFLuWOn2oRqyU+FRGW+UOA1jOcxs8U+J3Al6RKIfR8Aasg== + dependencies: + node-fetch "^2.1.2" + util.promisify "^1.0.0" -apollo-server-express@^2.2.6: - version "2.3.1" - resolved "https://registry.yarnpkg.com/apollo-server-express/-/apollo-server-express-2.3.1.tgz#0598e2fa0a0d9e6eb570c0bb6ce65c31810a9c09" - integrity sha512-J+rObr4GdT/5j6qTByUJoSvZSjTAX/7VqIkr2t+GxwcVUFGet2MdOHuV6rtWKc8CRgvVKfKN6iBrb2EOFcp2LQ== +apollo-server-errors@2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/apollo-server-errors/-/apollo-server-errors-2.2.1.tgz#f68a3f845929768057da7e1c6d30517db5872205" + integrity sha512-wY/YE3iJVMYC+WYIf8QODBjIP4jhI+oc7kiYo9mrz7LdYPKAgxr/he+NteGcqn/0Ea9K5/ZFTGJDbEstSMeP8g== + +apollo-server-express@2.5.0-alpha.0: + version "2.5.0-alpha.0" + resolved "https://registry.npmjs.org/apollo-server-express/-/apollo-server-express-2.5.0-alpha.0.tgz#4932e8b40f5bca4f884cbe5454be53821de82f31" + integrity sha512-KJhEyVhWYad5gL9qZMRIwc5Tbzu1/744FGyShJbMONuGEguqOsrc3ChjAwxRcUhvfTT2iNrdzVb48mQEVW56hg== dependencies: "@apollographql/graphql-playground-html" "^1.6.6" "@types/accepts" "^1.3.5" "@types/body-parser" "1.17.0" "@types/cors" "^2.8.4" - "@types/express" "4.16.0" + "@types/express" "4.16.1" accepts "^1.3.5" - apollo-server-core "2.3.1" + apollo-server-core "2.5.0-alpha.0" body-parser "^1.18.3" cors "^2.8.4" graphql-subscriptions "^1.0.0" graphql-tools "^4.0.0" type-is "^1.6.16" -apollo-server-plugin-base@0.2.1: - version "0.2.1" - resolved "https://registry.yarnpkg.com/apollo-server-plugin-base/-/apollo-server-plugin-base-0.2.1.tgz#d08c9576f7f11ab6e212f352d482faaa4059a31e" - integrity sha512-497NIY9VWRYCrMSkgR11IrIUO4Fsy6aGgnpOJoTdLQAnkDD9SJDSRzwKj4gypUoTT2unfKDng4eMxXVZlHvjOw== +apollo-server-plugin-base@0.4.0-alpha.0: + version "0.4.0-alpha.0" + resolved "https://registry.npmjs.org/apollo-server-plugin-base/-/apollo-server-plugin-base-0.4.0-alpha.0.tgz#165d12056f4cc3a4c9ed1ac8b08e25fcac1b4f39" + integrity sha512-L8HMdOOddy6mUkYopNVzx3YgU83FKeNM/pFdfAVft3Y2v4p9Fyu5cdoBijRHO4+gEfpJOdaSlZBqHlCg8wnw/g== -apollo-tracing@0.4.0: - version "0.4.0" - resolved "https://registry.yarnpkg.com/apollo-tracing/-/apollo-tracing-0.4.0.tgz#4b939063f4292422ac5a3564b76d1d88dec0a916" - integrity sha512-BlM8iQUQva4fm0xD/pLwkcz0degfB9a/aAn4k4cK36eLVD8XUkl7ptEB0c+cwcj7tOYpV1r5QX1XwdayBzlHSg== +apollo-server-plugin-response-cache@^0.1.0-alpha.0: + version "0.1.0-alpha.0" + resolved "https://registry.npmjs.org/apollo-server-plugin-response-cache/-/apollo-server-plugin-response-cache-0.1.0-alpha.0.tgz#c26c6502641daaeff17e5ebd896afd14049ed900" + integrity sha512-1p6mLlG24y+mg2iPPpJLIGHYVTE5oPSoWt7Pxd0kWu32n89sviQlok40LUc3Bb7nnq4uW1UxJRwRqGBRFiGrdw== dependencies: - apollo-server-env "2.2.0" - graphql-extensions "0.4.0" + apollo-cache-control "0.6.0-alpha.0" + apollo-server-caching "0.4.0-alpha.0" + apollo-server-env "2.3.0-alpha.0" + apollo-server-plugin-base "0.4.0-alpha.0" + +apollo-tracing@0.6.0-alpha.0: + version "0.6.0-alpha.0" + resolved "https://registry.npmjs.org/apollo-tracing/-/apollo-tracing-0.6.0-alpha.0.tgz#d8e393fdbd16b0635b496ebb8438c0081397a961" + integrity sha512-fec4S+Clpfj2zS1PyLSDh9LTYBc6eZzlNM4eA4NC0dNon51flEB1HeZkzFaAPSXbmnsc4mi7pv++sFxvxqFDyA== + dependencies: + apollo-server-env "2.3.0-alpha.0" + graphql-extensions "0.6.0-alpha.0" apollo-upload-client@^9.1.0: version "9.1.0" @@ -1664,7 +1685,7 @@ apollo-upload-server@^7.1.0: http-errors "^1.7.0" object-path "^0.11.4" -apollo-utilities@1.2.1, apollo-utilities@^1.2.1: +apollo-utilities@1.2.1, apollo-utilities@^1.0.1, apollo-utilities@^1.0.26, apollo-utilities@^1.0.27, apollo-utilities@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/apollo-utilities/-/apollo-utilities-1.2.1.tgz#1c3a1ebf5607d7c8efe7636daaf58e7463b41b3c" integrity sha512-Zv8Udp9XTSFiN8oyXOjf6PMHepD4yxxReLsl6dPUy5Ths7jti3nmlBzZUOxuTWRwZn0MoclqL7RQ5UEJN8MAxg== @@ -1673,20 +1694,6 @@ apollo-utilities@1.2.1, apollo-utilities@^1.2.1: ts-invariant "^0.2.1" tslib "^1.9.3" -apollo-utilities@^1.0.0, apollo-utilities@^1.0.1, apollo-utilities@^1.0.26: - version "1.0.26" - resolved "https://registry.yarnpkg.com/apollo-utilities/-/apollo-utilities-1.0.26.tgz#589c66bf4d16223531351cf667a230c787def1da" - integrity sha512-URw7o3phymliqYCYatcird2YRPUU2eWCNvip64U9gQrX56mEfK4m99yBIDCMTpmcvOFsKLii1sIEZsHIs/bvnw== - dependencies: - fast-json-stable-stringify "^2.0.0" - -apollo-utilities@^1.0.27: - version "1.0.27" - resolved "https://registry.yarnpkg.com/apollo-utilities/-/apollo-utilities-1.0.27.tgz#77c550f9086552376eca3a48e234a1466b5b057e" - integrity sha512-nzrMQ89JMpNmYnVGJ4t8zN75gQbql27UDhlxNi+3OModp0Masx5g+fQmQJ5B4w2dpRuYOsdwFLmj3lQbwOKV1Q== - dependencies: - fast-json-stable-stringify "^2.0.0" - app-root-path@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/app-root-path/-/app-root-path-2.1.0.tgz#98bf6599327ecea199309866e8140368fd2e646a" @@ -1792,7 +1799,7 @@ array-reduce@~0.0.0: resolved "https://registry.yarnpkg.com/array-reduce/-/array-reduce-0.0.0.tgz#173899d3ffd1c7d9383e4479525dbe278cab5f2b" integrity sha1-FziZ0//Rx9k4PkR5Ul2+J4yrXys= -array-union@^1.0.1: +array-union@^1.0.1, array-union@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/array-union/-/array-union-1.0.2.tgz#9a34410e4f4e3da23dea375be5be70f24778ec39" integrity sha1-mjRBDk9OPaI96jdb5b5w8kd47Dk= @@ -1830,17 +1837,17 @@ asap@~2.0.3: integrity sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY= asciify-pixel-matrix@^1.0.8: - version "1.0.12" - resolved "https://registry.yarnpkg.com/asciify-pixel-matrix/-/asciify-pixel-matrix-1.0.12.tgz#8a7d36cf37757861af0c8c218044f7ecab6ad278" - integrity sha512-huTyfgwdGbbvz7JoQFpuRiunoeW6wzcP69/vL+kT+QRiV09SpGxd7qozfKgd2PvgVi9kzYYZlcdYsiD7+lOrtQ== + version "1.0.13" + resolved "https://registry.yarnpkg.com/asciify-pixel-matrix/-/asciify-pixel-matrix-1.0.13.tgz#23e708600ed77e7f7fedb154715eb55d0b3d9108" + integrity sha512-x7SCpYX76K2Ano4nSEpTD0die9FD58HGlUVI2JA/xiIzjV15UCZ2BO+BMebj6UIiNjhroDd80eZT4at5pa78Bg== dependencies: asciify-pixel "^1.0.0" ul "^5.2.1" asciify-pixel@^1.0.0: - version "1.2.12" - resolved "https://registry.yarnpkg.com/asciify-pixel/-/asciify-pixel-1.2.12.tgz#08adbc5bd09a48da4bada726ccb6d46ade9ae8c9" - integrity sha512-fGNWJ6p/djfjlU4hqIs6LpoaVoTEATB1dZLRJ0EGoAycf4MWWzWCu8fLI3449JJC7XhDTJ5J0USKTKz7wkYZGw== + version "1.2.13" + resolved "https://registry.yarnpkg.com/asciify-pixel/-/asciify-pixel-1.2.13.tgz#f74a85caa7c06bf864adf0e77d1bf2e6db625a44" + integrity sha512-WzynlA81mEYVspO1i5lb+LuY3mh7AKguWTEYQjwp+zDTMEROuNt6T9MFZYBBC/SBgwji8nx6JA9MxRPLblJeSA== dependencies: couleurs "^6.0.0" deffy "^2.2.1" @@ -1909,7 +1916,7 @@ astral-regex@^1.0.0: resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-1.0.0.tgz#6c8c3fb827dd43ee3918f27b82782ab7658a6fd9" integrity sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg== -async-each@^1.0.0: +async-each@^1.0.0, async-each@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/async-each/-/async-each-1.0.1.tgz#19d386a1d9edc6e7c1c85d388aedbcc56d33602d" integrity sha1-GdOGodntxufByF04iu28xW0zYC0= @@ -1949,11 +1956,11 @@ async@^1.5.2: integrity sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo= async@^2.1.2, async@^2.1.4, async@^2.4.1, async@^2.5.0: - version "2.6.1" - resolved "https://registry.yarnpkg.com/async/-/async-2.6.1.tgz#b245a23ca71930044ec53fa46aa00a3e87c6a610" - integrity sha512-fNEiL2+AZt6AlAw/29Cr0UDe4sRAHCpEHh54WMz+Bb7QfNcFw4h3loofyJpLeQs4Yx7yuqu/2dLgM5hKOs6HlQ== + version "2.6.2" + resolved "https://registry.yarnpkg.com/async/-/async-2.6.2.tgz#18330ea7e6e313887f5d2f2a904bac6fe4dd5381" + integrity sha512-H1qVYh1MYhEEFLsP97cVKqCGo7KfCyTt6uEWqsTBr9SO84oK9Uwbyd/yCW+6rKJLHksBNUVWZDAjfS+Ccx0Bbg== dependencies: - lodash "^4.17.10" + lodash "^4.17.11" asynckit@^0.4.0: version "0.4.0" @@ -2016,7 +2023,7 @@ aws-sign2@~0.7.0: resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8" integrity sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg= -aws4@^1.6.0, aws4@^1.8.0: +aws4@^1.8.0: version "1.8.0" resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.8.0.tgz#f0e003d9ca9e7f59c7a508945d7b2ef9a04a542f" integrity sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ== @@ -2377,7 +2384,7 @@ babel-plugin-jest-hoist@^22.4.4: resolved "https://registry.yarnpkg.com/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-22.4.4.tgz#b9851906eab34c7bf6f8c895a2b08bea1a844c0b" integrity sha512-DUvGfYaAIlkdnygVIEl0O4Av69NtuQWcrjMOv6DODPuhuGLDnbsARz3AwiiI/EkIMMlxQDUcrZ9yoyJvTNjcVQ== -babel-plugin-styled-components@^1.1.4, babel-plugin-styled-components@^1.8.0: +"babel-plugin-styled-components@>= 1", babel-plugin-styled-components@^1.1.4, babel-plugin-styled-components@^1.8.0: version "1.10.0" resolved "https://registry.yarnpkg.com/babel-plugin-styled-components/-/babel-plugin-styled-components-1.10.0.tgz#ff1f42ad2cc78c21f26b62266b8f564dbc862939" integrity sha512-sQVKG8irFXx14ZfaK1bBePirfkacl3j8nZwSZK+ZjsbnadRHKQTbhXbe/RB1vT6Vgkz45E+V95LBq4KqdhZUNw== @@ -3030,9 +3037,9 @@ bcrypt-pbkdf@^1.0.0: tweetnacl "^0.14.3" before-after-hook@^1.1.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/before-after-hook/-/before-after-hook-1.2.0.tgz#1079c10312cd4d4ad0d1676d37951ef8bfc3a563" - integrity sha512-wI3QtdLppHNkmM1VgRVLCrlWCKk/YexlPicYbXPs4eYdd1InrUCTFsx5bX1iUQzzMsoRXXPpM1r+p7JEJJydag== + version "1.4.0" + resolved "https://registry.yarnpkg.com/before-after-hook/-/before-after-hook-1.4.0.tgz#2b6bf23dca4f32e628fd2747c10a37c74a4b484d" + integrity sha512-l5r9ir56nda3qu14nAXIlyq1MmUSs0meCIaFAh8HwkFwP1F8eToOuS3ah2VAHHcY04jaYD7FpJC5JTXHYRbkzg== bfj-node4@^5.2.0: version "5.3.1" @@ -3044,24 +3051,29 @@ bfj-node4@^5.2.0: tryer "^1.0.0" big-integer@^1.6.17: - version "1.6.40" - resolved "https://registry.yarnpkg.com/big-integer/-/big-integer-1.6.40.tgz#02e4cd4d6e266c4d9ece2469c05cb6439149fc78" - integrity sha512-CjhtJp0BViLzP1ZkEnoywjgtFQXS2pomKjAJtIISTCnuHILkLcAXLdFLG/nxsHc4s9kJfc+82Xpg8WNyhfACzQ== + version "1.6.42" + resolved "https://registry.yarnpkg.com/big-integer/-/big-integer-1.6.42.tgz#91623ae5ceeff9a47416c56c9440a66f12f534f1" + integrity sha512-3UQFKcRMx+5Z+IK5vYTMYK2jzLRJkt+XqyDdacgWgtMjjuifKpKTFneJLEgeBElOE2/lXZ1LcMcb5s8pwG2U8Q== big.js@^3.1.3: version "3.2.0" resolved "https://registry.yarnpkg.com/big.js/-/big.js-3.2.0.tgz#a5fc298b81b9e0dca2e458824784b65c52ba588e" integrity sha512-+hN/Zh2D08Mx65pZ/4g5bsmNiZUuChDiQfTUQ7qJr4/kuopCr88xZsAXv6mBoZEsUI4OuGHlX59qE94K2mMW8Q== +big.js@^5.2.2: + version "5.2.2" + resolved "https://registry.yarnpkg.com/big.js/-/big.js-5.2.2.tgz#65f0af382f578bcdc742bd9c281e9cb2d7768328" + integrity sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ== + bignumber.js@~1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/bignumber.js/-/bignumber.js-1.1.1.tgz#1a415d9ac014c13256af1feed9d1a3e5717a8cf7" integrity sha1-GkFdmsAUwTJWrx/u2dGj5XF6jPc= binary-extensions@^1.0.0: - version "1.12.0" - resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-1.12.0.tgz#c2d780f53d45bba8317a8902d4ceeaf3a6385b14" - integrity sha512-DYWGk01lDcxeS/K9IHPGWfT8PsJmbXRtRd2Sx72Tnb8pcYZQFF1oSDb8hJtS1vhp212q1Rzi5dUf9+nq0o9UIg== + version "1.13.0" + resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-1.13.0.tgz#9523e001306a32444b907423f1de2164222f6ab1" + integrity sha512-EgmjVLMn22z7eGGv3kcnHwSnJXmFHjISTY9E/S5lIcTD3Oxw05QTcBLNkJFzcb3cNueUdF/IN4U+d78V0zO8Hw== binary@~0.3.0: version "0.3.0" @@ -3086,7 +3098,7 @@ bluebird@3.5.2: resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-2.11.0.tgz#534b9033c022c9579c56ba3b3e5a5caafbb650e1" integrity sha1-U0uQM8AiyVecVro7Plpcqvu2UOE= -"bluebird@>= 3.0.1", bluebird@^3.3.4, bluebird@^3.4.7, bluebird@^3.5.1, bluebird@^3.5.3: +"bluebird@>= 3.0.1", bluebird@^3.3.4, bluebird@^3.4.7, bluebird@^3.5.1: version "3.5.3" resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.5.3.tgz#7d01c6f9616c9a51ab0f8c549a79dfe6ec33efa7" integrity sha512-/qKPUQlaW1OyR51WeCPBvRnAlnZFUJkCSG5HzGnuIqhgyJtF+T94lFnn33eiazjRm2LAHVy2guNnaq48X9SJuw== @@ -3169,7 +3181,7 @@ braces@^1.8.2: preserve "^0.2.0" repeat-element "^1.1.2" -braces@^2.3.0, braces@^2.3.1: +braces@^2.3.1, braces@^2.3.2: version "2.3.2" resolved "https://registry.yarnpkg.com/braces/-/braces-2.3.2.tgz#5979fd3f14cd531565e5fa2df1abfff1dfaee729" integrity sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w== @@ -3422,13 +3434,13 @@ browserslist@^3.2.6: electron-to-chromium "^1.3.47" browserslist@^4.1.0: - version "4.3.6" - resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.3.6.tgz#0f9d9081afc66b36f477c6bdf3813f784f42396a" - integrity sha512-kMGKs4BTzRWviZ8yru18xBpx+CyHG9eqgRbj9XbE3IMgtczf4aiA0Y1YCpVdvUieKGZ03kolSPXqTcscBCb9qw== + version "4.4.2" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.4.2.tgz#6ea8a74d6464bb0bd549105f659b41197d8f0ba2" + integrity sha512-ISS/AIAiHERJ3d45Fz0AVYKkgcy+F/eJHzKEvv1j0wwKGKD9T3BrwKr/5g45L+Y4XIK5PlTqefHciRFcfE1Jxg== dependencies: - caniuse-lite "^1.0.30000921" - electron-to-chromium "^1.3.92" - node-releases "^1.1.1" + caniuse-lite "^1.0.30000939" + electron-to-chromium "^1.3.113" + node-releases "^1.1.8" bser@^2.0.0: version "2.0.0" @@ -3499,7 +3511,7 @@ buffers@~0.1.1: resolved "https://registry.yarnpkg.com/buffers/-/buffers-0.1.1.tgz#b24579c3bed4d6d396aeee6d9a8ae7f5482ab7bb" integrity sha1-skV5w77U1tOWru5tmorn9Ugqt7s= -builtin-modules@^1.0.0, builtin-modules@^1.1.1: +builtin-modules@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-1.1.1.tgz#270f076c5a72c02f5b65a47df94c5fe3a278892f" integrity sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8= @@ -3510,16 +3522,19 @@ builtin-status-codes@^3.0.0: integrity sha1-hZgoeOIbmOHGZCXgPQF0eI9Wnug= bull@^3.5.2: - version "3.5.2" - resolved "https://registry.yarnpkg.com/bull/-/bull-3.5.2.tgz#9c85f205b17686efab2ee28aaa4388887360de32" - integrity sha512-tuL4Uj0kUeaQ7Cow3POkca20fk+VSsR8AiTFeNkyMmuicBnE1ZMwvF1NRDY7vIH43pD9PiMCSEP4Li/934Pw1w== + version "3.7.0" + resolved "https://registry.yarnpkg.com/bull/-/bull-3.7.0.tgz#ec9a8721a2cfb0421c501d28553ac1f9f025414d" + integrity sha512-DHCALp+OOahK+q2hB3sZQew0CJn4W3zYIQsdMlnBCy7JYCnJ/bdj0MFHjo5k0ZhNZxzwhLErXt1yd3llV494UQ== dependencies: - bluebird "^3.5.3" - cron-parser "^2.5.0" + cron-parser "^2.7.3" debuglog "^1.0.0" - ioredis "^3.1.4" + get-port latest + ioredis "^4.5.1" lodash "^4.17.11" + p-timeout "^2.0.1" + promise.prototype.finally "^3.1.0" semver "^5.6.0" + util.promisify "^1.0.0" uuid "^3.2.1" bundle-buddy-webpack-plugin@^0.3.0: @@ -3553,6 +3568,13 @@ busboy@^0.2.14: dicer "0.2.5" readable-stream "1.1.x" +busboy@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/busboy/-/busboy-0.3.0.tgz#6ee3cb1c844fc1f691d8f9d824f70128b3b5e485" + integrity sha512-e+kzZRAbbvJPLjQz2z+zAyr78BSi9IFeBTyLwF76g78Q2zRt/RZ1NtS3MS17v2yLqYfLz69zHdC+1L4ja8PwqQ== + dependencies: + dicer "0.3.0" + bytes@3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.0.0.tgz#d32815404d689699f85a4ea4fa8755dd13a96048" @@ -3680,7 +3702,7 @@ camelcase@^4.0.0, camelcase@^4.1.0: resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-4.1.0.tgz#d545635be1e33c542649c69173e5de6acfae34dd" integrity sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0= -camelize@1.0.0: +camelize@1.0.0, camelize@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/camelize/-/camelize-1.0.0.tgz#164a5483e630fa4321e5af07020e531831b2609b" integrity sha1-FkpUg+Yw+kMh5a8HAg5TGDGyYJs= @@ -3696,14 +3718,14 @@ caniuse-api@^1.5.2: lodash.uniq "^4.5.0" caniuse-db@^1.0.30000529, caniuse-db@^1.0.30000634, caniuse-db@^1.0.30000639: - version "1.0.30000921" - resolved "https://registry.yarnpkg.com/caniuse-db/-/caniuse-db-1.0.30000921.tgz#2aa78193e23539634abcf0248919d5901506c53b" - integrity sha512-sAvmRuxZ457rlTK+ydUMpmeXjVfkiXQXv0POTdpHEdKrVwEQaeZqJgQA5MH7sKAGTGxzlLcDpfoNkpVXw09X5Q== + version "1.0.30000946" + resolved "https://registry.yarnpkg.com/caniuse-db/-/caniuse-db-1.0.30000946.tgz#c8fd9a53ccd23e26d87b6e22a4bdf257b7aff36c" + integrity sha512-qjpdekHW9uyy1U/VxuoR9ppn8HjoBxsR5dRTpumUeoYgL8IWUeos+QpJh9DaDdjaKitkNzgAFMXCZLDYKWWyEQ== -caniuse-lite@^1.0.30000748, caniuse-lite@^1.0.30000792, caniuse-lite@^1.0.30000844, caniuse-lite@^1.0.30000921: - version "1.0.30000921" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30000921.tgz#7a607c1623444b22351d834e093aedda3c42fbe8" - integrity sha512-Bu09ciy0lMWLgpYC77I0YGuI8eFRBPPzaSOYJK1jTI64txCphYCqnWbxJYjHABYVt/TYX/p3jNjLBR87u1Bfpw== +caniuse-lite@^1.0.30000748, caniuse-lite@^1.0.30000792, caniuse-lite@^1.0.30000844, caniuse-lite@^1.0.30000939: + version "1.0.30000946" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30000946.tgz#ac50a3331bb805b483478bbc26a0ab71bb6d0509" + integrity sha512-ZVXtMoZ3Mfq69Ikv587Av+5lwGVJsG98QKUucVmtFBf0tl1kOCfLQ5o6Z2zBNis4Mx3iuH77WxEUpdP6t7f2CQ== capture-exit@^1.2.0: version "1.2.0" @@ -3728,9 +3750,9 @@ caseless@~0.12.0: integrity sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw= casual@^1.5.19: - version "1.5.19" - resolved "https://registry.yarnpkg.com/casual/-/casual-1.5.19.tgz#66fac46f7ae463f468f5913eb139f9c41c58bbf2" - integrity sha512-zMWzIs7y4grU0mkb6ZAWyHVJFHRZ2V/zi9GTrVv9v9PcS4Ur3AHaKocl4DLjhLR0J5LnMhMsMiAjGooqYo2t1Q== + version "1.6.0" + resolved "https://registry.yarnpkg.com/casual/-/casual-1.6.0.tgz#30dff7d3202a60ec5da9e36296a58dac62f14ae4" + integrity sha512-UmOIqc09KMrGIOKbLsb8GfPHDW8E+hdI1AjmJ8BYhQazTj8BHX3y6zEXj5lT7v7T4ZBkO180HXXF8ZACf3qANA== dependencies: mersenne-twister "^1.0.1" moment "^2.15.2" @@ -3761,10 +3783,10 @@ chalk@1.1.3, chalk@^1.0.0, chalk@^1.1.1, chalk@^1.1.3: strip-ansi "^3.0.0" supports-color "^2.0.0" -chalk@2.4.1, chalk@^2.0.0, chalk@^2.0.1, chalk@^2.1.0, chalk@^2.3.0, chalk@^2.4.0, chalk@^2.4.1: - version "2.4.1" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.1.tgz#18c49ab16a037b6eb0152cc83e3471338215b66e" - integrity sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ== +chalk@2.4.2, chalk@^2.0.0, chalk@^2.0.1, chalk@^2.1.0, chalk@^2.3.0, chalk@^2.4.0, chalk@^2.4.1: + version "2.4.2" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" + integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== dependencies: ansi-styles "^3.2.1" escape-string-regexp "^1.0.5" @@ -3845,32 +3867,31 @@ chokidar@^1.0.0, chokidar@^1.0.1, chokidar@^1.6.1: optionalDependencies: fsevents "^1.0.0" -chokidar@^2.0.0, chokidar@^2.0.2, chokidar@^2.0.4: - version "2.0.4" - resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-2.0.4.tgz#356ff4e2b0e8e43e322d18a372460bbcf3accd26" - integrity sha512-z9n7yt9rOvIJrMhvDtDictKrkFHeihkNl6uWMmZlmL6tJtX9Cs+87oK+teBx+JIgzvbX3yZHT3eF8vpbDxHJXQ== +chokidar@^2.0.0, chokidar@^2.0.2, chokidar@^2.0.4, chokidar@^2.1.0: + version "2.1.2" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-2.1.2.tgz#9c23ea40b01638439e0513864d362aeacc5ad058" + integrity sha512-IwXUx0FXc5ibYmPC2XeEj5mpXoV66sR+t3jqu2NS2GYwCktt3KF1/Qqjws/NkegajBA4RbZ5+DDwlOiJsxDHEg== dependencies: anymatch "^2.0.0" - async-each "^1.0.0" - braces "^2.3.0" + async-each "^1.0.1" + braces "^2.3.2" glob-parent "^3.1.0" - inherits "^2.0.1" + inherits "^2.0.3" is-binary-path "^1.0.0" is-glob "^4.0.0" - lodash.debounce "^4.0.8" - normalize-path "^2.1.1" + normalize-path "^3.0.0" path-is-absolute "^1.0.0" - readdirp "^2.0.0" - upath "^1.0.5" + readdirp "^2.2.1" + upath "^1.1.0" optionalDependencies: - fsevents "^1.2.2" + fsevents "^1.2.7" chownr@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.1.tgz#54726b8b8fff4df053c42187e801fb4412df1494" integrity sha512-j38EvO5+LHX84jlo6h4UzmOwi0UgW61WRyPtJz4qaadK5eY3BTS5TY/S1Stc3Uk2lIM6TPevAlULiEJwie860g== -ci-info@^1.0.0, ci-info@^1.5.0: +ci-info@^1.5.0: version "1.6.0" resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-1.6.0.tgz#2ca20dbb9ceb32d4524a683303313f0304b1e497" integrity sha512-vsGdkwSCDpWmP80ncATX7iea5DWQemg1UgCW5J8tqjU3lYw4FBYuj89J0CTVomA7BEfvSZd84GmHko+MxFQU2A== @@ -4130,17 +4151,17 @@ combine-source-map@^0.8.0, combine-source-map@~0.8.0: lodash.memoize "~3.0.3" source-map "~0.5.3" -combined-stream@^1.0.6, combined-stream@~1.0.5, combined-stream@~1.0.6: +combined-stream@^1.0.6, combined-stream@~1.0.6: version "1.0.7" resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.7.tgz#2d1d24317afb8abe95d6d2c0b07b57813539d828" integrity sha512-brWl9y6vOB1xYPZcpZde3N9zDByXTosAeMDo4p1wzo6UMOX4vumB+TP1RZ76sfE6Md68Q0NJSrE/gbezd4Ul+w== dependencies: delayed-stream "~1.0.0" -commander@2.11.0: - version "2.11.0" - resolved "https://registry.yarnpkg.com/commander/-/commander-2.11.0.tgz#157152fd1e7a6c8d98a5b715cf376df928004563" - integrity sha512-b0553uYA5YAEGgyYIGYROzKQ7X5RAqedkfjiZxwi0kL1g3bOaBNNZfYkzt/CL0umgD5wc9Jec2FbB98CjkMRvQ== +commander@2.15.1: + version "2.15.1" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.15.1.tgz#df46e867d0fc2aec66a34662b406a9ccafff5b0f" + integrity sha512-VlfT9F3V0v+jr4yxPc5gg9s62/fIVWsd2Bk2iD435um1NlGMYdVCq+MjcXnhYq2icNOizHr1kK+5TI6H0Hy0ag== commander@2.17.x, commander@~2.17.1: version "2.17.1" @@ -4157,14 +4178,7 @@ commander@~2.13.0: resolved "https://registry.yarnpkg.com/commander/-/commander-2.13.0.tgz#6964bca67685df7c1f1430c584f07d7597885b9c" integrity sha512-MVuS359B+YzaWqjCL/c+22gfryv+mCBPHAv3zyVI2GN8EY6IRP8VwtasXn8jyyhvvq84R4ImN1OKRtcbIasjYA== -common-tags@1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/common-tags/-/common-tags-1.4.0.tgz#1187be4f3d4cf0c0427d43f74eef1f73501614c0" - integrity sha1-EYe+Tz1M8MBCfUP3Tu8fc1AWFMA= - dependencies: - babel-runtime "^6.18.0" - -common-tags@^1.8.0: +common-tags@1.8.0, common-tags@^1.8.0: version "1.8.0" resolved "https://registry.yarnpkg.com/common-tags/-/common-tags-1.8.0.tgz#8e3153e542d4a39e9b10554434afaaf98956a937" integrity sha512-6P6g0uetGpW/sdyUy/iQQCbFF0kWVMSIVSyYz7Zgjcgh8mgw8PQzDNZeyZ5DQ2gM7LBoZPHmnjz8rUthkBG5tw== @@ -4174,29 +4188,17 @@ commondir@^1.0.1: resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b" integrity sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs= -component-cookie@^1.1.2: - version "1.1.4" - resolved "https://registry.yarnpkg.com/component-cookie/-/component-cookie-1.1.4.tgz#1b88b3dda4953d890163dd52fa53df374247cf8d" - integrity sha512-j6rzl+vHDTowvYz7Al3V0ud84O2l4YqGdA9qMj1W1nlZ5yWi7EhOd7ZSPzWFM25gZgv2OxWh6JlJYfsz2+XYow== - dependencies: - debug "2.2.0" - component-emitter@^1.2.0, component-emitter@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.2.1.tgz#137918d6d78283f7df7a6b7c5a63e140e69425e6" integrity sha1-E3kY1teCg/ffemt8WmPhQOaUJeY= -component-url@^0.2.1: - version "0.2.1" - resolved "https://registry.yarnpkg.com/component-url/-/component-url-0.2.1.tgz#4e4f4799c43ead9fd3ce91b5a305d220208fee47" - integrity sha1-Tk9HmcQ+rZ/TzpG1owXSICCP7kc= - compressible@~2.0.14: - version "2.0.15" - resolved "https://registry.yarnpkg.com/compressible/-/compressible-2.0.15.tgz#857a9ab0a7e5a07d8d837ed43fe2defff64fe212" - integrity sha512-4aE67DL33dSW9gw4CI2H/yTxqHLNcxp0yS6jB+4h+wr3e43+1z7vm0HU9qXOH8j+qjKuL8+UtkOxYQSMq60Ylw== + version "2.0.16" + resolved "https://registry.yarnpkg.com/compressible/-/compressible-2.0.16.tgz#a49bf9858f3821b64ce1be0296afc7380466a77f" + integrity sha512-JQfEOdnI7dASwCuSPWIeVYwc/zMsu/+tRhoUvEfXz2gxOA2DNjmG5vhtFdBlhWPPGo+RdT9S3tgc/uH5qgDiiA== dependencies: - mime-db ">= 1.36.0 < 2" + mime-db ">= 1.38.0 < 2" compression@^1.5.2: version "1.7.3" @@ -4216,16 +4218,7 @@ concat-map@0.0.1: resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= -concat-stream@1.6.0: - version "1.6.0" - resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.0.tgz#0aac662fd52be78964d5532f694784e70110acf7" - integrity sha1-CqxmL9Ur54lk1VMvaUeE5wEQrPc= - dependencies: - inherits "^2.0.3" - readable-stream "^2.2.2" - typedarray "^0.0.6" - -concat-stream@^1.4.7, concat-stream@^1.6.0, concat-stream@^1.6.1, concat-stream@~1.6.0: +concat-stream@1.6.2, concat-stream@^1.4.7, concat-stream@^1.6.0, concat-stream@^1.6.1, concat-stream@~1.6.0: version "1.6.2" resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.2.tgz#904bdf194cd3122fc675c77fc4ac3d4ff0fd1a34" integrity sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw== @@ -4248,9 +4241,9 @@ configstore@^3.0.0: xdg-basedir "^3.0.0" connect-history-api-fallback@^1.3.0: - version "1.5.0" - resolved "https://registry.yarnpkg.com/connect-history-api-fallback/-/connect-history-api-fallback-1.5.0.tgz#b06873934bc5e344fef611a196a6faae0aee015a" - integrity sha1-sGhzk0vF40T+9hGhlqb6rgruAVo= + version "1.6.0" + resolved "https://registry.yarnpkg.com/connect-history-api-fallback/-/connect-history-api-fallback-1.6.0.tgz#8b32089359308d111115d81cad3fceab888f97bc" + integrity sha512-e54B99q/OUoH64zYYRf3HBP5z24G38h5D3qXu23JGRoigpX5Ss4r9ZnDk3g0Z8uQC2x2lPaJ+UlWBc1ZWBWdLg== console-browserify@^1.1.0: version "1.1.0" @@ -4307,9 +4300,9 @@ convert-source-map@~1.1.0: integrity sha1-SCnId+n+SbMWHzvzZziI4gRpmGA= cookie-parser@^1.4.3: - version "1.4.3" - resolved "https://registry.yarnpkg.com/cookie-parser/-/cookie-parser-1.4.3.tgz#0fe31fa19d000b95f4aadf1f53fdc2b8a203baa5" - integrity sha1-D+MfoZ0AC5X0qt8fU/3CuKIDuqU= + version "1.4.4" + resolved "https://registry.yarnpkg.com/cookie-parser/-/cookie-parser-1.4.4.tgz#e6363de4ea98c3def9697b93421c09f30cf5d188" + integrity sha512-lo13tqF3JEtFO7FyA49CqbhaFkskRJ0u/UAiINgrIXeRCY41c88/zxtrECl8AKH3B0hj9q10+h3Kt8I7KlW4tw== dependencies: cookie "0.3.1" cookie-signature "1.0.6" @@ -4352,20 +4345,20 @@ copy-descriptor@^0.1.0: resolved "https://registry.yarnpkg.com/copy-descriptor/-/copy-descriptor-0.1.1.tgz#676f6eb3c39997c2ee1ac3a924fd6124748f578d" integrity sha1-Z29us8OZl8LuGsOpJP1hJHSPV40= +core-js@3.0.0-beta.13: + version "3.0.0-beta.13" + resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.0.0-beta.13.tgz#7732c69be5e4758887917235fe7c0352c4cb42a1" + integrity sha512-16Q43c/3LT9NyePUJKL8nRIQgYWjcBhjJSMWg96PVSxoS0PeE0NHitPI3opBrs9MGGHjte1KoEVr9W63YKlTXQ== + core-js@^1.0.0: version "1.2.7" resolved "https://registry.yarnpkg.com/core-js/-/core-js-1.2.7.tgz#652294c14651db28fa93bd2d5ff2983a4f08c636" integrity sha1-ZSKUwUZR2yj6k70tX/KYOk8IxjY= core-js@^2.4.0, core-js@^2.4.1, core-js@^2.5.0: - version "2.6.1" - resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.1.tgz#87416ae817de957a3f249b3b5ca475d4aaed6042" - integrity sha512-L72mmmEayPJBejKIWe2pYtGis5r0tQ5NaJekdhyXgeMQTpJoBsH0NL4ElY2LfSoV15xeQWKQ+XTTOZdyero5Xg== - -core-js@^3.0.0-beta.3: - version "3.0.0-beta.6" - resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.0.0-beta.6.tgz#f1ee6c8bd9c1941f992fda01f886b3b40ceb1510" - integrity sha512-06k0SnRTdYGlTNek5vAqfxbQjTtMM0zC2xJ79T1QM5UkZS0JQegrOgDiGh43n1QICnOe5+bcvS0zOGTm2C7rBA== + version "2.6.5" + resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.5.tgz#44bc8d249e7fb2ff5d00e0341a7ffb94fbf67895" + integrity sha512-klh/kDpwX8hryYL14M9w/xei6vrv6sE8gTHDG7/T/+SEovB/G4ejwcfE/CBzO6Edsu+OETZMZ3wcX/EjUkrl5A== core-util-is@1.0.2, core-util-is@~1.0.0: version "1.0.2" @@ -4413,9 +4406,9 @@ cosmiconfig@^2.1.0, cosmiconfig@^2.1.1: require-from-string "^1.1.0" couleurs@^6.0.0: - version "6.0.9" - resolved "https://registry.yarnpkg.com/couleurs/-/couleurs-6.0.9.tgz#b2b2a3ee37dae51875c9efd243ec7e7894afbc9e" - integrity sha1-srKj7jfa5Rh1ye/SQ+x+eJSvvJ4= + version "6.0.10" + resolved "https://registry.yarnpkg.com/couleurs/-/couleurs-6.0.10.tgz#a4a89a456f53ee98e65f106f2e69c6cb852e19fd" + integrity sha512-16ZvhVjVhEP75sMflsPtXcwbly+79os1zhBVcpRWNmnwifEbZChW+0URYING/A2ehBwp8i0pOXJYzdpiGO3Ivw== dependencies: ansy "^1.0.0" color-convert "^1.0.0" @@ -4433,15 +4426,15 @@ cp-file@^4.1.1: pify "^2.3.0" safe-buffer "^5.0.1" -cp-file@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/cp-file/-/cp-file-6.0.0.tgz#f38477ece100b403fcf780fd34d030486beb693e" - integrity sha512-OtHMgPugkgwHlbph25wlMKd358lZNhX1Y2viUpPoFmlBPlEiPIRhztYWha11grbGPnlM+urp5saVmwsChCIOEg== +cp-file@^6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/cp-file/-/cp-file-6.1.0.tgz#b48d2d80577d4c5025c68eb597a38093c1dc9ccf" + integrity sha512-an34I0lJwKncRKjxe3uGWUuiIIVYsHHjBGKld3OQB56hfoPCYom31VysvfuysKqHLbz6drnqP5YrCfLw17I2kw== dependencies: graceful-fs "^4.1.2" - make-dir "^1.0.0" + make-dir "^2.0.0" nested-error-stacks "^2.0.0" - pify "^3.0.0" + pify "^4.0.1" safe-buffer "^5.0.1" cpy-cli@^2.0.0: @@ -4453,13 +4446,13 @@ cpy-cli@^2.0.0: meow "^5.0.0" cpy@^7.0.0: - version "7.0.1" - resolved "https://registry.yarnpkg.com/cpy/-/cpy-7.0.1.tgz#d817e4d81bd7f0f25ff812796c5f1392dc0fb485" - integrity sha512-Zo52tXKLJcgy/baacn6KaNoRAakkl2wb+R4u6qJ4wlD0uchncwRQcIk66PlGlkzuToCJO6A6PWX27Tdwc8LU2g== + version "7.1.0" + resolved "https://registry.yarnpkg.com/cpy/-/cpy-7.1.0.tgz#085aa6077b28d211d585521ad7d8f3d05234a31d" + integrity sha512-HT6xnKeHwACUObD3LEFAsjeQ9IUVhC1Pn6Qbk0q6CEWy0WG061khT3ZxQU6IuMXPEEyb+vvluyUOyTdl+9EPWQ== dependencies: arrify "^1.0.1" - cp-file "^6.0.0" - globby "^8.0.1" + cp-file "^6.1.0" + globby "^9.1.0" nested-error-stacks "^2.0.0" crc@3.4.4: @@ -4529,10 +4522,10 @@ create-react-context@<=0.2.2: fbjs "^0.8.0" gud "^1.0.0" -cron-parser@^2.5.0: - version "2.7.3" - resolved "https://registry.yarnpkg.com/cron-parser/-/cron-parser-2.7.3.tgz#12603f89f5375af353a9357be2543d3172eac651" - integrity sha512-t9Kc7HWBWPndBzvbdQ1YG9rpPRB37Tb/tTviziUOh1qs3TARGh3b1p+tnkOHNe1K5iI3oheBPgLqwotMM7+lpg== +cron-parser@^2.7.3: + version "2.9.0" + resolved "https://registry.yarnpkg.com/cron-parser/-/cron-parser-2.9.0.tgz#7358377dee4b77c1a2b6a319efaf09ae13040e0d" + integrity sha512-WkHhWssz4OxEdepOIt3dXYQiKZgPwgeF1yzBMlLwnUCwv0ziNeINNbMs5haoG10ikM/j0LMrrhEsuK2Blt638w== dependencies: is-nan "^1.2.1" moment-timezone "^0.5.23" @@ -4654,19 +4647,19 @@ css-selector-tokenizer@^0.7.0: fastparse "^1.1.1" regexpu-core "^1.0.0" -css-to-react-native@^2.0.3: - version "2.2.2" - resolved "https://registry.yarnpkg.com/css-to-react-native/-/css-to-react-native-2.2.2.tgz#c077d0f7bf3e6c915a539e7325821c9dd01f9965" - integrity sha512-w99Fzop1FO8XKm0VpbQp3y5mnTnaS+rtCvS+ylSEOK76YXO5zoHQx/QMB1N54Cp+Ya9jB9922EHrh14ld4xmmw== +css-to-react-native@^2.0.3, css-to-react-native@^2.2.2: + version "2.3.0" + resolved "https://registry.yarnpkg.com/css-to-react-native/-/css-to-react-native-2.3.0.tgz#bf80d24ec4a08e430306ef429c0586e6ed5485f7" + integrity sha512-IhR7bNIrCFwbJbKZOAjNDZdwpsbjTN6f1agXeELHDqg1wHPA8c2QLruttKOW7hgMGetkfraRJCIEMrptifBfVw== dependencies: + camelize "^1.0.0" css-color-keywords "^1.0.0" - fbjs "^0.8.5" postcss-value-parser "^3.3.0" css-what@2.1: - version "2.1.2" - resolved "https://registry.yarnpkg.com/css-what/-/css-what-2.1.2.tgz#c0876d9d0480927d7d4920dcd72af3595649554d" - integrity sha512-wan8dMWQ0GUeF7DGEPVjhHemVW/vy6xUYmFzRY8RYqgA0JtXC9rJmbScBjqSu6dg9q0lwPQy6ZAmJVr3PPTvqQ== + version "2.1.3" + resolved "https://registry.yarnpkg.com/css-what/-/css-what-2.1.3.tgz#a6d7604573365fe74686c3f311c56513d88285f2" + integrity sha512-a+EPoD+uZiNfh+5fxw2nO9QwFa6nJe2Or35fGY6Ipw1R3R4AGz1d1TEZrCegvw2YTmZ0jXirGYlzxxpYSHwpEg== css.escape@^1.5.1: version "1.5.1" @@ -4725,14 +4718,14 @@ csso@~2.3.1: source-map "^0.5.3" cssom@0.3.x, "cssom@>= 0.3.2 < 0.4.0": - version "0.3.4" - resolved "https://registry.yarnpkg.com/cssom/-/cssom-0.3.4.tgz#8cd52e8a3acfd68d3aed38ee0a640177d2f9d797" - integrity sha512-+7prCSORpXNeR4/fUP3rL+TzqtiFfhMvTd7uEqMdgPvLPt4+uzFUeufx5RHjGTACCargg/DiEt/moMQmvnfkog== + version "0.3.6" + resolved "https://registry.yarnpkg.com/cssom/-/cssom-0.3.6.tgz#f85206cee04efa841f3c5982a74ba96ab20d65ad" + integrity sha512-DtUeseGk9/GBW0hl0vVPpU22iHL6YB5BUX7ml1hB+GMpo0NX5G4voX3kdWiMSEguFtcW3Vh3djqNF4aIe6ne0A== cssstyle@^1.0.0: - version "1.1.1" - resolved "https://registry.yarnpkg.com/cssstyle/-/cssstyle-1.1.1.tgz#18b038a9c44d65f7a8e428a653b9f6fe42faf5fb" - integrity sha512-364AI1l/M5TYcFH83JnOH/pSqgaNnKmYgKrm0didZMGKWjQB60dymwWy1rKUgL3J1ffdq9xVi2yGLHdSjjSNog== + version "1.2.1" + resolved "https://registry.yarnpkg.com/cssstyle/-/cssstyle-1.2.1.tgz#3aceb2759eaf514ac1a21628d723d6043a819495" + integrity sha512-7DYm8qe+gPx/h77QlCyFmX80+fGaE/6A/Ekl0zaszYOubvySO2saYFdQ78P29D0UsULxFKCetDGNaNRUdSF+2A== dependencies: cssom "0.3.x" @@ -4751,9 +4744,9 @@ curry2@^1.0.0: fast-bind "^1.0.0" custom-return@^1.0.0: - version "1.0.10" - resolved "https://registry.yarnpkg.com/custom-return/-/custom-return-1.0.10.tgz#ba875b2a97c9fba1fc12729ce1c21eeaa5de6a0c" - integrity sha512-WF07K2QwOIb6+mHYmiFP7oAlbVL+fkNgCGvjMMFuiVn5HExz75HWOyXslk+GgzEF72JiDlih8MyD1WJB8SYw7w== + version "1.0.11" + resolved "https://registry.yarnpkg.com/custom-return/-/custom-return-1.0.11.tgz#38461ed33435a641b27cdf6523ffd399e3edf91c" + integrity sha512-CM64m2bV2IQ1MiKRErXR3mRlIiFkIjkQGktTMVPXbmmKCZEiO/YGUlrg69/Gg2tGTSNv+Kyd3+bNJEJIlxhZbA== dependencies: noop6 "^1.0.0" @@ -4767,51 +4760,41 @@ cypress-plugin-retries@^1.2.0: resolved "https://registry.yarnpkg.com/cypress-plugin-retries/-/cypress-plugin-retries-1.2.0.tgz#a4e120c1bc417d1be525632e7d38e52a87bc0578" integrity sha512-seQFI/0j5WCqX7IVN2k0tbd3FLdhbPuSCWdDtdzDmU9oJfUkRUlluV47TYD+qQ/l+fJYkQkpw8csLg8/LohfRg== -cypress@3.1.5: - version "3.1.5" - resolved "https://registry.yarnpkg.com/cypress/-/cypress-3.1.5.tgz#5227b2ce9306c47236d29e703bad9055d7218042" - integrity sha512-jzYGKJqU1CHoNocPndinf/vbG28SeU+hg+4qhousT/HDBMJxYgjecXOmSgBX/ga9/TakhqSrIrSP2r6gW/OLtg== +cypress@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/cypress/-/cypress-3.2.0.tgz#c2d5befd5077dab6fb52ad70721e0868ac057001" + integrity sha512-PN0wz6x634QyNL56/voTzJoeScDfwtecvSfFTHfv5MkHuECVSR4VQcEZTvYtKWln3CMBMUkWbBKPIwwu2+a/kw== dependencies: "@cypress/listr-verbose-renderer" "0.4.1" - "@cypress/xvfb" "1.2.3" - "@types/blob-util" "1.3.3" - "@types/bluebird" "3.5.18" - "@types/chai" "4.0.8" - "@types/chai-jquery" "1.1.35" - "@types/jquery" "3.3.6" - "@types/lodash" "4.14.87" - "@types/minimatch" "3.0.3" - "@types/mocha" "2.2.44" - "@types/sinon" "7.0.0" - "@types/sinon-chai" "3.2.2" + "@cypress/xvfb" "1.2.4" bluebird "3.5.0" cachedir "1.3.0" - chalk "2.4.1" + chalk "2.4.2" check-more-types "2.24.0" - commander "2.11.0" - common-tags "1.4.0" + commander "2.15.1" + common-tags "1.8.0" debug "3.1.0" execa "0.10.0" executable "4.1.1" - extract-zip "1.6.6" + extract-zip "1.6.7" fs-extra "4.0.1" getos "3.1.0" - glob "7.1.2" - is-ci "1.0.10" + glob "7.1.3" + is-ci "1.2.1" is-installed-globally "0.1.0" lazy-ass "1.6.0" listr "0.12.0" lodash "4.17.11" log-symbols "2.2.0" minimist "1.2.0" - moment "2.22.2" + moment "2.24.0" ramda "0.24.1" - request "2.87.0" - request-progress "0.3.1" - supports-color "5.1.0" - tmp "0.0.31" + request "2.88.0" + request-progress "0.4.0" + supports-color "5.5.0" + tmp "0.0.33" url "0.11.0" - yauzl "2.8.0" + yauzl "2.10.0" d@1: version "1.0.0" @@ -4825,6 +4808,11 @@ damerau-levenshtein@^1.0.0: resolved "https://registry.yarnpkg.com/damerau-levenshtein/-/damerau-levenshtein-1.0.4.tgz#03191c432cb6eea168bb77f3a55ffdccb8978514" integrity sha1-AxkcQyy27qFou3fzpV/9zLiXhRQ= +dash-ast@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/dash-ast/-/dash-ast-1.0.0.tgz#12029ba5fb2f8aa6f0a861795b23c1b4b6c27d37" + integrity sha512-Vy4dx7gquTeMcQR/hDkYLGUnwVil6vk4FOOct+djUnHOUWt+zJPJAaRIXaAFkPXtJjvlY7o3rfRu0/3hpnwoUA== + dashdash@^1.12.0: version "1.14.1" resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0" @@ -4874,13 +4862,6 @@ debounce@^1.2.0: resolved "https://registry.yarnpkg.com/debounce/-/debounce-1.2.0.tgz#44a540abc0ea9943018dc0eaa95cce87f65cd131" integrity sha512-mYtLl1xfZLi1m4RtQYlZgJUNQjl4ZxVnHzIR8nLLgi4q1YT8o/WM+MK/f8yfcc9s5Ir5zRaPZyZU6xs1Syoocg== -debug@2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/debug/-/debug-2.2.0.tgz#f87057e995b1a1f6ae6a4960664137bc56f039da" - integrity sha1-+HBX6ZWxofauaklgZkE3vFbwOdo= - dependencies: - ms "0.7.1" - debug@2.6.9, debug@^2.1.2, debug@^2.2.0, debug@^2.3.2, debug@^2.3.3, debug@^2.5.2, debug@^2.6.0, debug@^2.6.6, debug@^2.6.8, debug@^2.6.9: version "2.6.9" resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" @@ -4888,7 +4869,7 @@ debug@2.6.9, debug@^2.1.2, debug@^2.2.0, debug@^2.3.2, debug@^2.3.3, debug@^2.5. dependencies: ms "2.0.0" -debug@3.1.0, debug@=3.1.0: +debug@3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261" integrity sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g== @@ -4902,7 +4883,7 @@ debug@4.0.1: dependencies: ms "^2.1.1" -debug@^3.0.1, debug@^3.1.0: +debug@^3.0.1, debug@^3.1.0, debug@^3.2.6: version "3.2.6" resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.6.tgz#e83d17de16d8a7efb7717edbe5fb10135eee629b" integrity sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ== @@ -4910,9 +4891,9 @@ debug@^3.0.1, debug@^3.1.0: ms "^2.1.1" debug@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/debug/-/debug-4.1.0.tgz#373687bffa678b38b1cd91f861b63850035ddc87" - integrity sha512-heNPJUJIqC+xB6ayLAMHaIrmN9HKa7aQO8MGqKpvCA+uJYVcvR6l5kgdrhRuwPFHU7P5/A1w0BjByPHwpfTDKg== + version "4.1.1" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.1.1.tgz#3b72260255109c6b589cee050f1d516139664791" + integrity sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw== dependencies: ms "^2.1.1" @@ -4996,9 +4977,9 @@ default-require-extensions@^1.0.0: strip-bom "^2.0.0" deffy@^2.2.1, deffy@^2.2.2: - version "2.2.2" - resolved "https://registry.yarnpkg.com/deffy/-/deffy-2.2.2.tgz#088f40913cb47078653fa6f697c206e03471d523" - integrity sha1-CI9AkTy0cHhlP6b2l8IG4DRx1SM= + version "2.2.3" + resolved "https://registry.yarnpkg.com/deffy/-/deffy-2.2.3.tgz#16671c969a8fc447c76dd6bb0d265dd2d1b9c361" + integrity sha512-c5JD8Z6V1aBWVzn1+aELL97R1pHCwEjXeU3hZXdigkZkxb9vhgFP162kAxGXl992TtAg0btwQyx7d54CqcQaXQ== dependencies: typpy "^2.0.0" @@ -5086,6 +5067,11 @@ denque@^1.1.0: resolved "https://registry.yarnpkg.com/denque/-/denque-1.4.0.tgz#79e2f0490195502107f24d9553f374837dabc916" integrity sha512-gh513ac7aiKrAgjiIBWZG0EASyDF9p4JMWwKA8YU5s9figrL5SRNEMT6FDynsegakuhWd1wVqTvqvqAoDxw7wQ== +depd@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df" + integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw== + depd@~1.1.1, depd@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9" @@ -5150,11 +5136,11 @@ detect-port-alt@1.1.6: debug "^2.6.0" detective@^5.0.2: - version "5.1.0" - resolved "https://registry.yarnpkg.com/detective/-/detective-5.1.0.tgz#7a20d89236d7b331ccea65832e7123b5551bb7cb" - integrity sha512-TFHMqfOvxlgrfVzTEkNBSh9SvSNX/HfF4OFI2QFGCyPm02EsyILqnUeb5P6q7JZ3SFNTBL5t2sePRgrN4epUWQ== + version "5.2.0" + resolved "https://registry.yarnpkg.com/detective/-/detective-5.2.0.tgz#feb2a77e85b904ecdea459ad897cc90a99bd2a7b" + integrity sha512-6SsIx+nUUbuK0EthKjv0zrdnajCCXVYGmbYYiYjFVpzcjwEs/JMDZ8tPRG29J/HhN56t3GJp2cGSWDRjjot8Pg== dependencies: - acorn-node "^1.3.0" + acorn-node "^1.6.1" defined "^1.0.0" minimist "^1.1.1" @@ -5166,6 +5152,13 @@ dicer@0.2.5: readable-stream "1.1.x" streamsearch "0.1.2" +dicer@0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/dicer/-/dicer-0.3.0.tgz#eacd98b3bfbf92e8ab5c2fdb71aaac44bb06b872" + integrity sha512-MdceRRWqltEG2dZqO769g27N/3PXfcKl04VhYnBlo2YhH7zPi88VebsjTKclaOyiuMaGU72hTfw3VkUitGcVCA== + dependencies: + streamsearch "0.1.2" + diff@^3.2.0: version "3.5.0" resolved "https://registry.yarnpkg.com/diff/-/diff-3.5.0.tgz#800c0dd1e0a8bfbc95835c202ad220fe317e5a12" @@ -5180,12 +5173,11 @@ diffie-hellman@^5.0.0: miller-rabin "^4.0.0" randombytes "^2.0.0" -dir-glob@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-2.0.0.tgz#0b205d2b6aef98238ca286598a8204d29d0a0034" - integrity sha512-37qirFDz8cA5fimp9feo43fSuRo2gHwaIn6dXL8Ber1dGwUosDrGZeCCXq57WnIqE4aQ+u3eQZzsk1yOzhdwag== +dir-glob@^2.2.1: + version "2.2.2" + resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-2.2.2.tgz#fa09f0694153c8918b18ba0deafae94769fc50c4" + integrity sha512-f9LBi5QWzIW3I6e//uxZoLBlUt9kcp66qo0sSCxL6YZKc75R1c4MFCoe/LaZiBGmgujvQdxc5Bn3QhfyvK5Hsw== dependencies: - arrify "^1.0.1" path-type "^3.0.0" direction@^0.1.5: @@ -5248,7 +5240,7 @@ dogapi@1.1.0: minimist "^1.1.1" rc "^1.0.0" -dom-converter@~0.2: +dom-converter@^0.2: version "0.2.0" resolved "https://registry.yarnpkg.com/dom-converter/-/dom-converter-0.2.0.tgz#6721a9daee2e293682955b6afe416771627bb768" integrity sha512-gd3ypIPfOMr9h5jIKq8E3sHOTCjeirnl0WK5ZdS1AW0Odt0b1PaWaHdJ4Qk4klv+YB9aJBS7mESXjFoDQPu6DA== @@ -5263,12 +5255,12 @@ dom-helpers@^3.3.1: "@babel/runtime" "^7.1.2" dom-serializer@0, dom-serializer@~0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.1.0.tgz#073c697546ce0780ce23be4a28e293e40bc30c82" - integrity sha1-BzxpdUbOB4DOI75KKOKT5AvDDII= + version "0.1.1" + resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.1.1.tgz#1ec4059e284babed36eec2941d4a970a189ce7c0" + integrity sha512-l0IU0pPzLWSHBcieZbpOKgkIn3ts3vAh7ZuFyXNwJxJXk/c4Gwj9xaTJwIDVQCXawWD0qb3IzMGH5rglQaO0XA== dependencies: - domelementtype "~1.1.1" - entities "~1.1.1" + domelementtype "^1.3.0" + entities "^1.1.1" dom-urls@^1.1.0: version "1.1.0" @@ -5287,16 +5279,11 @@ domain-browser@^1.1.1, domain-browser@^1.2.0: resolved "https://registry.yarnpkg.com/domain-browser/-/domain-browser-1.2.0.tgz#3d31f50191a6749dd1375a7f522e823d42e54eda" integrity sha512-jnjyiM6eRyZl2H+W8Q/zLMA481hzi0eszAaBUzIVnmYVDBbnLxVNnfu1HgEBvCbL+71FrxMl3E6lpKH7Ge3OXA== -domelementtype@1, domelementtype@^1.3.0: +domelementtype@1, domelementtype@^1.3.0, domelementtype@^1.3.1: version "1.3.1" resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-1.3.1.tgz#d048c44b37b0d10a7f2a3d5fee3f4333d790481f" integrity sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w== -domelementtype@~1.1.1: - version "1.1.3" - resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-1.1.3.tgz#bd28773e2642881aec51544924299c5cd822185b" - integrity sha1-vSh3PiZCiBrsUVRJJCmcXNgiGFs= - domexception@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/domexception/-/domexception-1.0.1.tgz#937442644ca6a31261ef36e3ec677fe805582c90" @@ -5304,13 +5291,6 @@ domexception@^1.0.1: dependencies: webidl-conversions "^4.0.2" -domhandler@2.1: - version "2.1.0" - resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-2.1.0.tgz#d2646f5e57f6c3bab11cf6cb05d3c0acf7412594" - integrity sha1-0mRvXlf2w7qxHPbLBdPArPdBJZQ= - dependencies: - domelementtype "1" - domhandler@^2.3.0: version "2.4.2" resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-2.4.2.tgz#8805097e933d65e85546f726d60f5eb88b44f803" @@ -5318,13 +5298,6 @@ domhandler@^2.3.0: dependencies: domelementtype "1" -domutils@1.1: - version "1.1.6" - resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.1.6.tgz#bddc3de099b9a2efacc51c623f28f416ecc57485" - integrity sha1-vdw94Jm5ou+sxRxiPyj0FuzFdIU= - dependencies: - domelementtype "1" - domutils@1.5.1: version "1.5.1" resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.5.1.tgz#dcd8488a26f563d61079e48c9f7b7e32373682cf" @@ -5368,6 +5341,11 @@ dotsplit.js@^1.0.3: resolved "https://registry.yarnpkg.com/dotsplit.js/-/dotsplit.js-1.1.0.tgz#25a239eabe922a91ffa5d2a172d6c9fb82451e02" integrity sha1-JaI56r6SKpH/pdKhctbJ+4JFHgI= +double-ended-queue@^2.1.0-0: + version "2.1.0-0" + resolved "https://registry.yarnpkg.com/double-ended-queue/-/double-ended-queue-2.1.0-0.tgz#103d3527fd31528f40188130c841efdd78264e5c" + integrity sha1-ED01J/0xUo9AGIEwyEHv3XgmTlw= + draft-js-checkable-list-item@^2.0.6: version "2.0.6" resolved "https://registry.yarnpkg.com/draft-js-checkable-list-item/-/draft-js-checkable-list-item-2.0.6.tgz#19dfb99421e07ac1a93736f4a5d04e222de2a647" @@ -5413,11 +5391,11 @@ draft-js-embed-plugin@^1.2.0: decorate-component-with-props "^1.0.2" draft-js-export-markdown@^1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/draft-js-export-markdown/-/draft-js-export-markdown-1.3.0.tgz#e7f0d7aabc1f787c24e1c6dc524b681463aae8c8" - integrity sha512-kOiDGQ9KehcbYYcwzlkR+Gja6svEwIgId1gz3EtEVsZ09cxZaV13Qlkydm0J5wPy5Omthvdpj0Iw1B2E4BZRZQ== + version "1.3.3" + resolved "https://registry.yarnpkg.com/draft-js-export-markdown/-/draft-js-export-markdown-1.3.3.tgz#fcdfdd408b6d380ff9cc788aecf4ef352c0efae3" + integrity sha512-sp3zJQMSnQ97eAZXWjbFSDifgZAVNM5KTiTRldNNel9/K1IzKyMfAaMoxsIEL0fXTxDwfpFSaE7dKAwkdROayA== dependencies: - draft-js-utils "^1.2.0" + draft-js-utils "^1.3.3" draft-js-focus-plugin@^2.2.0: version "2.2.0" @@ -5441,21 +5419,21 @@ draft-js-image-plugin@^2.0.6: prop-types "^15.5.8" union-class-names "^1.0.0" -draft-js-import-element@^1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/draft-js-import-element/-/draft-js-import-element-1.3.0.tgz#64ca3b32e770cc0227f563cfb362b19bd5c60e4e" - integrity sha512-asRZSsMbqzpQ3xlUX9+HMuOn/DR8OyWYVV9wdcnPw7QwVUqb3L4XGj0XbDCbdQte0aX8W1DU4BV0nhC2KcSCEQ== +draft-js-import-element@^1.3.3: + version "1.3.3" + resolved "https://registry.yarnpkg.com/draft-js-import-element/-/draft-js-import-element-1.3.3.tgz#e4b5e87f72bc57adb459d786a3d0d58b498b2fa1" + integrity sha512-E1hiuWuhM9XmX5QFBzKY0blkuPxz+yuK88R1y/xIfLgJtVyfQOnc8vfkGH6tEPbNtdbIcnKI8aCGfCkqgvt5CA== dependencies: - draft-js-utils "^1.3.0" - synthetic-dom "^1.2.0" + draft-js-utils "^1.3.3" + synthetic-dom "^1.3.3" draft-js-import-markdown@^1.3.1: - version "1.3.1" - resolved "https://registry.npmjs.org/draft-js-import-markdown/-/draft-js-import-markdown-1.3.1.tgz#7ed4f95c7f16acdd4703a5d608497857ed4a4ffa" - integrity sha512-TTV+X7X5ixPeNWqpGk6CFLYO+5ORCrUENaJPGDOzps7gyT/SMfuILM8DP2FwWjFt3ys2WmFZMlMGKk/M4l/N/Q== + version "1.3.3" + resolved "https://registry.yarnpkg.com/draft-js-import-markdown/-/draft-js-import-markdown-1.3.3.tgz#cc5e703dc31888757743e94a4eafd25bf012f9ab" + integrity sha512-O9wQPAVB4TQLZGPYUQDHD1XGK/H7DZKNrSuwDn7RTadWWFycZV4sP46TZPFD4D240xas/h9QwjbxfAg8ZLQwrw== dependencies: - draft-js-import-element "^1.3.0" - synthetic-dom "^1.2.0" + draft-js-import-element "^1.3.3" + synthetic-dom "^1.3.3" draft-js-linkify-plugin@^2.0.0-beta1: version "2.0.1" @@ -5516,15 +5494,10 @@ draft-js-prism@^1.0.6: extend "^3.0.0" immutable "*" -draft-js-utils@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/draft-js-utils/-/draft-js-utils-1.2.0.tgz#f5cb23eb167325ffed3d79882fdc317721d2fd12" - integrity sha1-9csj6xZzJf/tPXmIL9wxdyHS/RI= - -draft-js-utils@^1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/draft-js-utils/-/draft-js-utils-1.3.0.tgz#9102ca34450da3de2097a074c6cc9c92f72a312e" - integrity sha512-wZaY6HQ/jZTh0wshkPzXNrPu0qcE6PuZwIYJp/q8nFAc3elWma7EBtGLXDPkSwvAXEl596mD6GPfk3jnPDuA+w== +draft-js-utils@^1.3.3: + version "1.3.3" + resolved "https://registry.yarnpkg.com/draft-js-utils/-/draft-js-utils-1.3.3.tgz#2330ab98218c835d58dc08eacdba6708b201eb27" + integrity sha512-ZIF3KE2+dRD6zEoqQyu9HzeG56NWtktfazRnDZFC9GD4NnHgJE5qI+TqGgmjRjSrKzuSQonl5rsx+D4N5m6yhQ== draft-js@0.x, draft-js@^0.10.4, "draft-js@npm:draft-js-fork-mxstbr", draft-js@~0.10.0: version "0.10.4" @@ -5565,10 +5538,10 @@ ecc-jsbn@~0.1.1: jsbn "~0.1.0" safer-buffer "^2.1.0" -ecdsa-sig-formatter@1.0.10: - version "1.0.10" - resolved "https://registry.yarnpkg.com/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.10.tgz#1c595000f04a8897dfb85000892a0f4c33af86c3" - integrity sha1-HFlQAPBKiJffuFAAiSoPTDOvhsM= +ecdsa-sig-formatter@1.0.11: + version "1.0.11" + resolved "https://registry.yarnpkg.com/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz#ae0f0fa2d85045ef14a817daa3ce9acd0489e5bf" + integrity sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ== dependencies: safe-buffer "^5.0.1" @@ -5592,10 +5565,10 @@ ejs@^2.3.4, ejs@^2.5.7: resolved "https://registry.yarnpkg.com/ejs/-/ejs-2.6.1.tgz#498ec0d495655abc6f23cd61868d926464071aa0" integrity sha512-0xy4A/twfrRCnkhfk8ErDi5DqdAsAqeGxht4xkCUrsvhhbQNs7E+4jV0CN7+NKIY0aHE72+XvqtBIXzD31ZbXQ== -electron-to-chromium@^1.2.7, electron-to-chromium@^1.3.30, electron-to-chromium@^1.3.47, electron-to-chromium@^1.3.92: - version "1.3.95" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.95.tgz#79fac438813ca7f3db182a525c2ab432934f6484" - integrity sha512-0JZEDKOQAE05EO/4rk3vLAE+PYFI9OLCVLAS4QAq1y+Bb2y1N6MyQJz62ynzHN/y0Ka/nO5jVJcahbCEdfiXLQ== +electron-to-chromium@^1.2.7, electron-to-chromium@^1.3.113, electron-to-chromium@^1.3.30, electron-to-chromium@^1.3.47: + version "1.3.115" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.115.tgz#fdaa56c19b9f7386dbf29abc1cc632ff5468ff3b" + integrity sha512-mN2qeapQWdi2B9uddxTZ4nl80y46hbyKY5Wt9Yjih+QZFQLdaujEDK4qJky35WhyxMzHF3ZY41Lgjd2BPDuBhg== elegant-spinner@^1.0.1: version "1.0.1" @@ -5686,18 +5659,19 @@ error-stack-parser@^2.0.0: dependencies: stackframe "^1.0.4" -es-abstract@^1.5.1, es-abstract@^1.7.0: - version "1.12.0" - resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.12.0.tgz#9dbbdd27c6856f0001421ca18782d786bf8a6165" - integrity sha512-C8Fx/0jFmV5IPoMOFPA9P9G5NtqW+4cOPit3MIuvR2t7Ag2K15EJTpxnHAYTzL+aYQJIESYeXZmDBfOBE1HcpA== +es-abstract@^1.11.0, es-abstract@^1.5.1, es-abstract@^1.7.0, es-abstract@^1.9.0: + version "1.13.0" + resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.13.0.tgz#ac86145fdd5099d8dd49558ccba2eaf9b88e24e9" + integrity sha512-vDZfg/ykNxQVwup/8E1BZhVzFfBxs9NqMzGcvIJrqg5k2/5Za2bWo40dK2J1pgLngZ7c+Shh8lwYtLGyrwPutg== dependencies: - es-to-primitive "^1.1.1" + es-to-primitive "^1.2.0" function-bind "^1.1.1" - has "^1.0.1" - is-callable "^1.1.3" + has "^1.0.3" + is-callable "^1.1.4" is-regex "^1.0.4" + object-keys "^1.0.12" -es-to-primitive@^1.1.1: +es-to-primitive@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.2.0.tgz#edf72478033456e8dda8ef09e00ad9650707f377" integrity sha512-qZryBOJjV//LaxLTV6UC//WewneB3LcXOL9NP++ozKVXsIIIpm/2c13UDiD9Jp2eThsecw9m3jPqDwTyobcdbg== @@ -5707,13 +5681,13 @@ es-to-primitive@^1.1.1: is-symbol "^1.0.2" es5-ext@^0.10.14, es5-ext@^0.10.35, es5-ext@^0.10.9, es5-ext@~0.10.14: - version "0.10.46" - resolved "https://registry.yarnpkg.com/es5-ext/-/es5-ext-0.10.46.tgz#efd99f67c5a7ec789baa3daa7f79870388f7f572" - integrity sha512-24XxRvJXNFwEMpJb3nOkiRJKRoupmjYmOPVlI65Qy2SrtxwOTB+g6ODjBKOtwEHbYrhWRty9xxOWLNdClT2djw== + version "0.10.49" + resolved "https://registry.yarnpkg.com/es5-ext/-/es5-ext-0.10.49.tgz#059a239de862c94494fec28f8150c977028c6c5e" + integrity sha512-3NMEhi57E31qdzmYp2jwRArIUsj1HI/RxbQ4bgnSB+AIKIxsAmTiK83bYMifIcpWvEc3P1X30DhUKOqEtF/kvg== dependencies: es6-iterator "~2.0.3" es6-symbol "~3.1.1" - next-tick "1" + next-tick "^1.0.0" es6-iterator@^2.0.1, es6-iterator@~2.0.1, es6-iterator@~2.0.3: version "2.0.3" @@ -5737,9 +5711,9 @@ es6-map@^0.1.3, es6-map@^0.1.4: event-emitter "~0.3.5" es6-promise@^4.0.3, es6-promise@^4.0.5, es6-promise@^4.1.0: - version "4.2.5" - resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.2.5.tgz#da6d0d5692efb461e082c14817fe2427d8f5d054" - integrity sha512-n6wvpdE43VFtJq+lUDYDBFUwV8TZbuGXLV4D6wKafg13ldznKsyEvatubnmUe31zcvelSzOHF+XbaT+Bl9ObDg== + version "4.2.6" + resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.2.6.tgz#b685edd8258886365ea62b57d30de28fadcd974f" + integrity sha512-aRVgGdnmW2OiySVPUC9e6m+plolMAJKjZnQlCwNSuK5yQ0JN61DZSO1X1Ufd1foqWRAlig0rhduTCHe7sVtK5Q== es6-promisify@^5.0.0: version "5.0.0" @@ -5788,9 +5762,9 @@ escape-string-regexp@1.0.5, escape-string-regexp@^1.0.2, escape-string-regexp@^1 integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ= escodegen@^1.9.1: - version "1.11.0" - resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-1.11.0.tgz#b27a9389481d5bfd5bec76f7bb1eb3f8f4556589" - integrity sha512-IeMV45ReixHS53K/OmfKAIztN/igDHzTJUhZM3k1jMhIZWjk45SMwAtBsEXiJp3vSPmTcu6CXn7mDvFHRN66fw== + version "1.11.1" + resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-1.11.1.tgz#c485ff8d6b4cdb89e27f4a856e91f118401ca510" + integrity sha512-JwiqFD9KdGVVpeuRa68yU3zZnBEOcPs0nKW7wZzXky8Z7tffdYUHbe11bPCV5jYlK6DVdKLWLm0f5I/QlL0Kmw== dependencies: esprima "^3.1.3" estraverse "^4.2.0" @@ -5814,7 +5788,7 @@ eslint-config-react-app@^2.1.0: resolved "https://registry.yarnpkg.com/eslint-config-react-app/-/eslint-config-react-app-2.1.0.tgz#23c909f71cbaff76b945b831d2d814b8bde169eb" integrity sha512-8QZrKWuHVC57Fmu+SsKAVxnI9LycZl7NFQ4H9L+oeISuCXhYdXqsOOIVSjQFW6JF5MXZLFE+21Syhd7mF1IRZQ== -eslint-import-resolver-node@^0.3.1: +eslint-import-resolver-node@^0.3.1, eslint-import-resolver-node@^0.3.2: version "0.3.2" resolved "https://registry.yarnpkg.com/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.2.tgz#58f15fb839b8d0576ca980413476aab2472db66a" integrity sha512-sfmTqJfPSizWu4aymbPr4Iidp5yKm8yDkHp+Ir3YiTHiiDfxh69mOUsmiqW6RZ9zRXFaF64GtYmN7e+8GHBv6Q== @@ -5833,13 +5807,13 @@ eslint-loader@1.9.0: object-hash "^1.1.4" rimraf "^2.6.1" -eslint-module-utils@^2.1.1, eslint-module-utils@^2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/eslint-module-utils/-/eslint-module-utils-2.2.0.tgz#b270362cd88b1a48ad308976ce7fa54e98411746" - integrity sha1-snA2LNiLGkitMIl2zn+lTphBF0Y= +eslint-module-utils@^2.1.1, eslint-module-utils@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/eslint-module-utils/-/eslint-module-utils-2.3.0.tgz#546178dab5e046c8b562bbb50705e2456d7bda49" + integrity sha512-lmDJgeOOjk8hObTysjqH7wyMi+nsHwwvfBykwfhjR1LNdd7C2uFJBvx4OpWYpXOw4df1yE1cDEVd1yLHitk34w== dependencies: debug "^2.6.8" - pkg-dir "^1.0.0" + pkg-dir "^2.0.0" eslint-plugin-flowtype@2.39.1: version "2.39.1" @@ -5872,20 +5846,20 @@ eslint-plugin-import@2.8.0: read-pkg-up "^2.0.0" eslint-plugin-import@^2.14.0: - version "2.14.0" - resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.14.0.tgz#6b17626d2e3e6ad52cfce8807a845d15e22111a8" - integrity sha512-FpuRtniD/AY6sXByma2Wr0TXvXJ4nA/2/04VPlfpmUDPOpOY264x+ILiwnrk/k4RINgDAyFZByxqPUbSQ5YE7g== + version "2.16.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.16.0.tgz#97ac3e75d0791c4fac0e15ef388510217be7f66f" + integrity sha512-z6oqWlf1x5GkHIFgrSvtmudnqM6Q60KM4KvpWi5ubonMjycLjndvd5+8VAZIsTlHC03djdgJuyKG6XO577px6A== dependencies: contains-path "^0.1.0" - debug "^2.6.8" + debug "^2.6.9" doctrine "1.5.0" - eslint-import-resolver-node "^0.3.1" - eslint-module-utils "^2.2.0" - has "^1.0.1" - lodash "^4.17.4" - minimatch "^3.0.3" + eslint-import-resolver-node "^0.3.2" + eslint-module-utils "^2.3.0" + has "^1.0.3" + lodash "^4.17.11" + minimatch "^3.0.4" read-pkg-up "^2.0.0" - resolve "^1.6.0" + resolve "^1.9.0" eslint-plugin-jest@^21.25.1: version "21.27.2" @@ -5930,16 +5904,18 @@ eslint-plugin-react@7.4.0: jsx-ast-utils "^2.0.0" prop-types "^15.5.10" -eslint-plugin-react@^7.11.1: - version "7.11.1" - resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.11.1.tgz#c01a7af6f17519457d6116aa94fc6d2ccad5443c" - integrity sha512-cVVyMadRyW7qsIUh3FHp3u6QHNhOgVrLQYdQEB1bPWBsgbNCHdFAeNMquBMCcZJu59eNthX053L70l7gRt4SCw== +eslint-plugin-react@^7.12.4: + version "7.12.4" + resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.12.4.tgz#b1ecf26479d61aee650da612e425c53a99f48c8c" + integrity sha512-1puHJkXJY+oS1t467MjbqjvX53uQ05HXwjqDgdbGBqf5j9eeydI54G3KwiJmWciQ0HTBacIKw2jgwSBSH3yfgQ== dependencies: array-includes "^3.0.3" doctrine "^2.1.0" has "^1.0.3" jsx-ast-utils "^2.0.1" + object.fromentries "^2.0.0" prop-types "^15.6.2" + resolve "^1.9.0" eslint-plugin-standard@^3.1.0: version "3.1.0" @@ -6142,7 +6118,7 @@ eventemitter3@^3.0.0, eventemitter3@^3.1.0: resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-3.1.0.tgz#090b4d6cdbd645ed10bf750d4b5407942d7ba163" integrity sha512-ivIvhpq/Y0uSjcHDcOIccjmYjGLcP09MFGE7ysAwkAvkXfpZlC985pH2/ui64DKazbTW/4kN3yqozUxlXzI6cA== -events@1.1.1, events@^1.0.0, events@^1.1.0: +events@1.1.1, events@^1.1.0: version "1.1.1" resolved "https://registry.yarnpkg.com/events/-/events-1.1.1.tgz#9ebdb7635ad099c70dcc4c2a1f5004288e8bd924" integrity sha1-nr23Y1rQmccNzEwqH1AEKI6L2SQ= @@ -6152,6 +6128,11 @@ events@^2.0.0: resolved "https://registry.yarnpkg.com/events/-/events-2.1.0.tgz#2a9a1e18e6106e0e812aa9ebd4a819b3c29c0ba5" integrity sha512-3Zmiobend8P9DjmKAty0Era4jV8oJ0yGYe2nJJAxgymF9+N8F2m0hhZiMoWtcfepExzNKZumFU3ksdQbInGWCg== +events@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/events/-/events-3.0.0.tgz#9a0a0dfaf62893d92b875b8f2698ca4114973e88" + integrity sha512-Dc381HFWJzEOhQ+d8pkNon++bk9h6cdAoAj4iE6Q4y6xgTzySWXlKn05/TVNpjnfRqi/X0EpJEJohPjNI3zpVA== + eventsource@0.1.6: version "0.1.6" resolved "https://registry.yarnpkg.com/eventsource/-/eventsource-0.1.6.tgz#0acede849ed7dd1ccc32c811bb11b944d4f29232" @@ -6352,7 +6333,7 @@ extend-shallow@^3.0.0, extend-shallow@^3.0.2: assign-symbols "^1.0.0" is-extendable "^1.0.1" -extend@^3.0.0, extend@~3.0.1, extend@~3.0.2: +extend@^3.0.0, extend@~3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== @@ -6402,14 +6383,14 @@ extract-text-webpack-plugin@3.0.2: schema-utils "^0.3.0" webpack-sources "^1.0.1" -extract-zip@1.6.6: - version "1.6.6" - resolved "https://registry.yarnpkg.com/extract-zip/-/extract-zip-1.6.6.tgz#1290ede8d20d0872b429fd3f351ca128ec5ef85c" - integrity sha1-EpDt6NINCHK0Kf0/NRyhKOxe+Fw= +extract-zip@1.6.7: + version "1.6.7" + resolved "https://registry.yarnpkg.com/extract-zip/-/extract-zip-1.6.7.tgz#a840b4b8af6403264c8db57f4f1a74333ef81fe9" + integrity sha1-qEC0uK9kAyZMjbV/Txp0Mz74H+k= dependencies: - concat-stream "1.6.0" + concat-stream "1.6.2" debug "2.6.9" - mkdirp "0.5.0" + mkdirp "0.5.1" yauzl "2.4.1" extsprintf@1.3.0: @@ -6447,10 +6428,10 @@ fast-deep-equal@^2.0.1: resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz#7b05218ddf9667bf7f370bf7fdb2cb15fdd0aa49" integrity sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk= -fast-glob@^2.0.2: - version "2.2.4" - resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-2.2.4.tgz#e54f4b66d378040e0e4d6a68ec36bbc5b04363c0" - integrity sha512-FjK2nCGI/McyzgNtTESqaWP3trPvHyRyoyY70hxjc3oKPNmDe8taohLZpoVKoUjW85tbU5txaYUZCNtVzygl1g== +fast-glob@^2.2.6: + version "2.2.6" + resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-2.2.6.tgz#a5d5b697ec8deda468d85a74035290a025a95295" + integrity sha512-0BvMaZc1k9F+MeWWMe8pL6YltFzZYcJsYU7D4JyDA6PAczaXvxqQQ/z+mDF7/4Mw01DeUc+i3CTKajnkANkV4w== dependencies: "@mrmlnc/readdir-enhanced" "^2.2.1" "@nodelib/fs.stat" "^1.1.2" @@ -6495,7 +6476,7 @@ fb-watchman@^2.0.0: dependencies: bser "^2.0.0" -fbjs@0.8.16, fbjs@^0.8.0, fbjs@^0.8.1, fbjs@^0.8.15, fbjs@^0.8.16, fbjs@^0.8.5, fbjs@^0.8.9: +fbjs@0.8.16, fbjs@^0.8.0, fbjs@^0.8.1, fbjs@^0.8.15, fbjs@^0.8.9: version "0.8.16" resolved "https://registry.yarnpkg.com/fbjs/-/fbjs-0.8.16.tgz#5e67432f550dc41b572bf55847b8aca64e5337db" integrity sha1-XmdDL1UNxBtXK/VYR7ispk5TN9s= @@ -6515,6 +6496,13 @@ fd-slicer@~1.0.1: dependencies: pend "~1.2.0" +fd-slicer@~1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/fd-slicer/-/fd-slicer-1.1.0.tgz#25c7c89cb1f9077f8891bbe61d8f390eae256f1e" + integrity sha1-JcfInLH5B3+IkbvmHY85Dq4lbx4= + dependencies: + pend "~1.2.0" + feature-policy@0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/feature-policy/-/feature-policy-0.2.0.tgz#22096de49ab240176878ffe2bde2f6ff04d48c43" @@ -6551,6 +6539,13 @@ file-loader@1.1.5: loader-utils "^1.0.2" schema-utils "^0.3.0" +file-selector@^0.1.8: + version "0.1.11" + resolved "https://registry.yarnpkg.com/file-selector/-/file-selector-0.1.11.tgz#4648d1303fc594afe8638d0f35caed38697d32cf" + integrity sha512-NopCegJ7QuoqVzUdSLcZb0M9IFO69CSFZzuZhZBasfQxepNwa1ehL6L9UKe3EyBof3EUeraccfJocLCRAvtxdg== + dependencies: + tslib "^1.9.0" + filename-regex@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/filename-regex/-/filename-regex-2.0.1.tgz#c1c4b9bee3e09725ddb106b75c1e301fe2f18b26" @@ -6703,11 +6698,11 @@ flow-typed@^2.5.1: yargs "^4.2.0" follow-redirects@^1.0.0, follow-redirects@^1.2.5: - version "1.5.10" - resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.5.10.tgz#7b7a9f9aea2fdff36786a94ff643ed07f4ff5e2a" - integrity sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ== + version "1.7.0" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.7.0.tgz#489ebc198dc0e7f64167bd23b03c4c19b5784c76" + integrity sha512-m/pZQy4Gj287eNy94nivy5wchN3Kp+Q5WgUPNy5lJSZ3sgkVKSYV/ZChMAQVIgx1SqfZ2zBZtPA2YlXIWxxJOQ== dependencies: - debug "=3.1.0" + debug "^3.2.6" for-in@^1.0.1, for-in@^1.0.2: version "1.0.2" @@ -6763,7 +6758,7 @@ forever@^0.15.3: utile "~0.2.1" winston "~0.8.1" -form-data@^2.3.1, form-data@~2.3.1, form-data@~2.3.2: +form-data@^2.3.1, form-data@~2.3.2: version "2.3.3" resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.3.tgz#dcce52c05f644f298c6a7ab936bd724ceffbf3a6" integrity sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ== @@ -6818,6 +6813,11 @@ fs-capacitor@^1.0.0: resolved "https://registry.yarnpkg.com/fs-capacitor/-/fs-capacitor-1.0.1.tgz#ff9dbfa14dfaf4472537720f19c3088ed9278df0" integrity sha512-XdZK0Q78WP29Vm3FGgJRhRhrBm51PagovzWtW2kJ3Q6cYJbGtZqWSGTSPwvtEkyjIirFd7b8Yes/dpOYjt4RRQ== +fs-capacitor@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/fs-capacitor/-/fs-capacitor-2.0.1.tgz#8b27ce79979a4ed2427e7bb6bf3d781344f7ea56" + integrity sha512-kyV2oaG1/pu9NPosfGACmBym6okgzyg6hEtA5LSUq0dGpGLe278MVfMwVnSHDA/OBcTCHkPNqWL9eIwbPN6dDg== + fs-extra@3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-3.0.1.tgz#3794f378c58b342ea7dbbb23095109c4b3b62291" @@ -6882,10 +6882,10 @@ fs.realpath@^1.0.0: resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8= -fsevents@^1.0.0, fsevents@^1.1.3, fsevents@^1.2.2, fsevents@^1.2.3: - version "1.2.4" - resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-1.2.4.tgz#f41dcb1af2582af3692da36fc55cbd8e1041c426" - integrity sha512-z8H8/diyk76B7q5wg+Ud0+CqzcAF3mBBI/bA5ne5zrRUUIvNkJY//D3BqyH571KuAC4Nr7Rw7CjWX4r0y9DvNg== +fsevents@^1.0.0, fsevents@^1.1.3, fsevents@^1.2.3, fsevents@^1.2.7: + version "1.2.7" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-1.2.7.tgz#4851b664a3783e52003b3c66eb0eee1074933aa4" + integrity sha512-Pxm6sI2MeBD7RdD12RYsqaP0nMiwx8eZBXCa6z2L+mRHm2DYrOYwihmhjpkdjUHwQhslWQjRpEgNq4XvBmaAuw== dependencies: nan "^2.9.2" node-pre-gyp "^0.10.0" @@ -6906,9 +6906,9 @@ function-bind@^1.1.1: integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== function.name@^1.0.3: - version "1.0.11" - resolved "https://registry.yarnpkg.com/function.name/-/function.name-1.0.11.tgz#445df70938bf88b3ad23413d1ad611b192abee9a" - integrity sha512-dR60geqE4iI/siyBiWGcUjrQNtMqvToGXR/RkVkSSm4iH8FiHkIx2ljUFFiOZMbgBl/N9o9+VzikNp2hVV6y2Q== + version "1.0.12" + resolved "https://registry.yarnpkg.com/function.name/-/function.name-1.0.12.tgz#34eec84476d9fb67977924a4cdcb98ec85695726" + integrity sha512-C7Tu+rAFrWW5RjXqtKtXp2xOdCujq+4i8ZH3w0uz/xrYHBwXZrPt96x8cDAEHrIjeyEv/Jm6iDGyqupbaVQTlw== dependencies: noop6 "^1.0.1" @@ -6946,6 +6946,11 @@ get-document@1: resolved "https://registry.yarnpkg.com/get-document/-/get-document-1.0.0.tgz#4821bce66f1c24cb0331602be6cb6b12c4f01c4b" integrity sha1-SCG85m8cJMsDMWAr5strEsTwHEs= +get-port@latest: + version "4.2.0" + resolved "https://registry.yarnpkg.com/get-port/-/get-port-4.2.0.tgz#e37368b1e863b7629c43c5a323625f95cf24b119" + integrity sha512-/b3jarXkH8KJoOMQc3uVGHASwGLPq3gSFJ7tgJm2diza+bydJPTGOibin2steecKeOylE8oY2JERlVWkAJO6yw== + get-stdin@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-4.0.1.tgz#b968c6b0a04384324902e8bf1a5df32579a450fe" @@ -7010,19 +7015,7 @@ glob-to-regexp@^0.3.0: resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.3.0.tgz#8c5a1494d2066c570cc3bfe4496175acc4d502ab" integrity sha1-jFoUlNIGbFcMw7/kSWF1rMTVAqs= -glob@7.1.2: - version "7.1.2" - resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.2.tgz#c19c9df9a028702d678612384a6552404c636d15" - integrity sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ== - dependencies: - fs.realpath "^1.0.0" - inflight "^1.0.4" - inherits "2" - minimatch "^3.0.4" - once "^1.3.0" - path-is-absolute "^1.0.0" - -glob@^7.0.3, glob@^7.0.5, glob@^7.1.0, glob@^7.1.1, glob@^7.1.2: +glob@7.1.3, glob@^7.0.3, glob@^7.1.0, glob@^7.1.1, glob@^7.1.2, glob@^7.1.3: version "7.1.3" resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.3.tgz#3960832d3f1574108342dafd3a67b332c0969df1" integrity sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ== @@ -7070,9 +7063,9 @@ global@^4.3.0, global@^4.3.2: process "~0.5.1" globals@^11.0.1, globals@^11.1.0: - version "11.9.0" - resolved "https://registry.yarnpkg.com/globals/-/globals-11.9.0.tgz#bde236808e987f290768a93d065060d78e6ab249" - integrity sha512-5cJVtyXWH8PiJPVLZzzoIizXx944O4OmRro5MWKx5fT4MgcN7OfaMutPeaTdJCCURwbWdhhcCWcKIffPnmTzBg== + version "11.11.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-11.11.0.tgz#dcf93757fa2de5486fbeed7118538adf789e9c2e" + integrity sha512-WHq43gS+6ufNOEqlrDBxVEbb8ntfXrfAUU2ZOpCxrBdGKW3gyv8mCxAfIBD0DroPKGrJ2eSsXsLtY9MPntsyTw== globals@^9.17.0, globals@^9.18.0: version "9.18.0" @@ -7102,18 +7095,19 @@ globby@^6.1.0: pify "^2.0.0" pinkie-promise "^2.0.0" -globby@^8.0.1: - version "8.0.1" - resolved "https://registry.yarnpkg.com/globby/-/globby-8.0.1.tgz#b5ad48b8aa80b35b814fc1281ecc851f1d2b5b50" - integrity sha512-oMrYrJERnKBLXNLVTqhm3vPEdJ/b2ZE28xN4YARiix1NOIOBPEpOUnm844K1iu/BkphCaf2WNFwMszv8Soi1pw== - dependencies: - array-union "^1.0.1" - dir-glob "^2.0.0" - fast-glob "^2.0.2" - glob "^7.1.2" - ignore "^3.3.5" - pify "^3.0.0" - slash "^1.0.0" +globby@^9.1.0: + version "9.1.0" + resolved "https://registry.yarnpkg.com/globby/-/globby-9.1.0.tgz#e90f4d5134109e6d855abdd31bdb1b085428592e" + integrity sha512-VtYjhHr7ncls724Of5W6Kaahz0ag7dB4G62/2HsN+xEKG6SrPzM1AJMerGxQTwJGnN9reeyxdvXbuZYpfssCvg== + dependencies: + "@types/glob" "^7.1.1" + array-union "^1.0.2" + dir-glob "^2.2.1" + fast-glob "^2.2.6" + glob "^7.1.3" + ignore "^4.0.3" + pify "^4.0.1" + slash "^2.0.0" good-listener@^1.2.2: version "1.2.2" @@ -7165,9 +7159,9 @@ graceful-fs@^4.1.11, graceful-fs@^4.1.2, graceful-fs@^4.1.4, graceful-fs@^4.1.6, integrity sha512-6uHUhOPEBgQ24HM+r6b/QwWfZq+yiFcipKFrOFiBEnWdy5sdzYoi+pJeQaPI5qOLRFqWmAXUPQNsielzdLoecA== graphql-cost-analysis@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/graphql-cost-analysis/-/graphql-cost-analysis-1.0.2.tgz#680f7b99137e5e21f279f76e01f779955258de0a" - integrity sha512-U6TaSSarx4WbCwT0520xsGvhNwYu5GWfn4bEHMiVgdWvWvdfscVp0n8zShTwzoEoO0gEYT2fXTg1O2KadDbg/A== + version "1.0.3" + resolved "https://registry.yarnpkg.com/graphql-cost-analysis/-/graphql-cost-analysis-1.0.3.tgz#25b97c8e638c7e538af5ba9bcf6012cda74420ce" + integrity sha512-2kogZrc3iPVW5Lf2cSadVfufNx440XMoqKbMjNRi96HV80jCk9is1AI7CwizT5CSGzKlsnGQmaSqjeR1dJB0Gw== dependencies: selectn "^1.1.2" @@ -7185,19 +7179,12 @@ graphql-depth-limit@^1.1.0: dependencies: arrify "^1.0.1" -graphql-extensions@0.4.0: - version "0.4.0" - resolved "https://registry.yarnpkg.com/graphql-extensions/-/graphql-extensions-0.4.0.tgz#5857c7b7b9f20dbccbfd88730fffa5963b3c61ee" - integrity sha512-8TUgIIUVpXWOcqq9RdmTSHUrhc3a/s+saKv9cCl8TYWHK9vyJIdea7ZaSKHGDthZNcsN+C3LulZYRL3Ah8ukoA== - dependencies: - "@apollographql/apollo-tools" "^0.2.6" - -graphql-extensions@0.4.1: - version "0.4.1" - resolved "https://registry.yarnpkg.com/graphql-extensions/-/graphql-extensions-0.4.1.tgz#92c49a8409ffbfb24559d7661ab60cc90d6086e4" - integrity sha512-Xei4rBxbsTHU6dYiq9y1xxbpRMU3+Os7yD3vXV5W4HbTaxRMizDmu6LAvV4oBEi0ttwICHARQjYTjDTDhHnxrQ== +graphql-extensions@0.6.0-alpha.0: + version "0.6.0-alpha.0" + resolved "https://registry.npmjs.org/graphql-extensions/-/graphql-extensions-0.6.0-alpha.0.tgz#4e0b0e2c1962e98e12730bc23cefd5881b68525e" + integrity sha512-SY4mUxY0Q+GElKMjHtNsYYQ0ypHiuvky5roNh0CbOWqxTo0HNQp4vkjLKN4yu9QX1nCk02v5hFxivE0NqOj/sg== dependencies: - "@apollographql/apollo-tools" "^0.2.6" + "@apollographql/apollo-tools" "^0.3.3" graphql-log@0.1.3: version "0.1.3" @@ -7225,14 +7212,14 @@ graphql-subscriptions@^1.0.0: iterall "^1.2.1" graphql-tag@^2.10.0, graphql-tag@^2.9.2: - version "2.10.0" - resolved "https://registry.yarnpkg.com/graphql-tag/-/graphql-tag-2.10.0.tgz#87da024be863e357551b2b8700e496ee2d4353ae" - integrity sha512-9FD6cw976TLLf9WYIUPCaaTpniawIjHWZSwIRZSjrfufJamcXbVVYfN2TWvJYbw0Xf2JjYbl1/f2+wDnBVw3/w== + version "2.10.1" + resolved "https://registry.yarnpkg.com/graphql-tag/-/graphql-tag-2.10.1.tgz#10aa41f1cd8fae5373eaf11f1f67260a3cad5e02" + integrity sha512-jApXqWBzNXQ8jYa/HLkZJaVw9jgwNqZkywa2zfFn16Iv1Zb7ELNHkJaXHR7Quvd5SIGsy6Ny7SUKATgnu05uEg== graphql-tools@^4.0.0, graphql-tools@^4.0.3: - version "4.0.3" - resolved "https://registry.yarnpkg.com/graphql-tools/-/graphql-tools-4.0.3.tgz#23b5cb52c519212b1b2e4630a361464396ad264b" - integrity sha512-NNZM0WSnVLX1zIMUxu7SjzLZ4prCp15N5L2T2ro02OVyydZ0fuCnZYRnx/yK9xjGWbZA0Q58yEO//Bv/psJWrg== + version "4.0.4" + resolved "https://registry.yarnpkg.com/graphql-tools/-/graphql-tools-4.0.4.tgz#ca08a63454221fdde825fe45fbd315eb2a6d566b" + integrity sha512-chF12etTIGVVGy3fCTJ1ivJX2KB7OSG4c6UOJQuqOHCmBQwTyNgCDuejZKvpYxNZiEx7bwIjrodDgDe9RIkjlw== dependencies: apollo-link "^1.2.3" apollo-utilities "^1.0.1" @@ -7241,12 +7228,12 @@ graphql-tools@^4.0.0, graphql-tools@^4.0.3: uuid "^3.1.0" graphql-upload@^8.0.2: - version "8.0.2" - resolved "https://registry.yarnpkg.com/graphql-upload/-/graphql-upload-8.0.2.tgz#1c1f116f15b7f8485cf40ff593a21368f0f58856" - integrity sha512-u8a5tKPfJ0rU4MY+B3skabL8pEjMkm3tUzq25KBx6nT0yEWmqUO7Z5tdwvwYLFpkLwew94Gue0ARbZtar3gLTw== + version "8.0.4" + resolved "https://registry.yarnpkg.com/graphql-upload/-/graphql-upload-8.0.4.tgz#ed7cbde883b5cca493de77e39f95cddf40dfd514" + integrity sha512-jsTfVYXJ5mU6BXiiJ20CUCAcf41ICCQJ2ltwQFUuaFKiY4JhlG99uZZp5S3hbpQ/oA1kS7hz4pRtsnxPCa7Yfg== dependencies: - busboy "^0.2.14" - fs-capacitor "^1.0.0" + busboy "^0.3.0" + fs-capacitor "^2.0.0" http-errors "^1.7.1" object-path "^0.11.4" @@ -7288,9 +7275,9 @@ handle-thing@^1.2.5: integrity sha1-/Xqtcmvxpf0W38KbL3pmAdJxOcQ= handlebars@^4.0.3: - version "4.0.12" - resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.0.12.tgz#2c15c8a96d46da5e266700518ba8cb8d919d5bc5" - integrity sha512-RhmTekP+FZL+XNhwS1Wf+bTTZpdLougwt5pcgA1tuz6Jcx0fpH/7z0qd71RKnZHBCxIRBHfBOnio4gViPemNzA== + version "4.1.0" + resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.1.0.tgz#0d6a6f34ff1f63cecec8423aa4169827bf787c3a" + integrity sha512-l2jRuU1NAWK6AW5qqcTATWQJvNPEwkM7NEKSiv/gqOsoSQbVoWyqVEY5GS+XPQ88zLNmqASRpzfdm8d79hJS+w== dependencies: async "^2.5.0" optimist "^0.6.1" @@ -7303,14 +7290,6 @@ har-schema@^2.0.0: resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92" integrity sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI= -har-validator@~5.0.3: - version "5.0.3" - resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-5.0.3.tgz#ba402c266194f15956ef15e0fcf242993f6a7dfd" - integrity sha1-ukAsJmGU8VlW7xXg/PJCmT9qff0= - dependencies: - ajv "^5.1.0" - har-schema "^2.0.0" - har-validator@~5.1.0: version "5.1.3" resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-5.1.3.tgz#1ef89ebd3e4996557675eed9893110dc350fa080" @@ -7438,10 +7417,11 @@ helmet-csp@2.7.1: platform "1.3.5" helmet@^3.14.0: - version "3.15.0" - resolved "https://registry.yarnpkg.com/helmet/-/helmet-3.15.0.tgz#fe0bb80e05d9eec589e3cbecaf5384409a3a64c9" - integrity sha512-j9JjtAnWJj09lqe/PEICrhuDaX30TeokXJ9tW6ZPhVH0+LMoihDeJ58CdWeTGzM66p6EiIODmgAaWfdeIWI4Gg== + version "3.16.0" + resolved "https://registry.yarnpkg.com/helmet/-/helmet-3.16.0.tgz#7df41a4bfe4c83d90147c1e30d70893f92a9d97c" + integrity sha512-rsTKRogc5OYGlvSHuq5QsmOsOzF6uDoMqpfh+Np8r23+QxDq+SUx90Rf8HyIKQVl7H6NswZEwfcykinbAeZ6UQ== dependencies: + depd "2.0.0" dns-prefetch-control "0.1.0" dont-sniff-mimetype "1.0.0" expect-ct "0.1.1" @@ -7451,8 +7431,8 @@ helmet@^3.14.0: helmet-csp "2.7.1" hide-powered-by "1.0.0" hpkp "2.0.0" - hsts "2.1.0" - ienoopen "1.0.0" + hsts "2.2.0" + ienoopen "1.1.0" nocache "2.0.0" referrer-policy "1.1.0" x-xss-protection "1.1.0" @@ -7463,9 +7443,9 @@ hide-powered-by@1.0.0: integrity sha1-SoWtZYgfYoV/xwr3F0oRhNzM4ys= highlight.js@^9.13.1: - version "9.13.1" - resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-9.13.1.tgz#054586d53a6863311168488a0f58d6c505ce641e" - integrity sha512-Sc28JNQNDzaH6PORtRLMvif9RSn1mYuOoX3omVjnb0+HbpPygU2ALBI0R/wsiqCb4/fcp07Gdo8g+fhtFrQl6A== + version "9.15.6" + resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-9.15.6.tgz#72d4d8d779ec066af9a17cb14360c3def0aa57c4" + integrity sha512-zozTAWM1D6sozHo8kqhfYgsac+B+q0PmsjXeyDrYIHHcBN0zTVT66+s2GW1GZv7DbyaROdLXKdabwS/WqPyIdQ== history@^4.6.1, history@^4.7.2: version "4.7.2" @@ -7512,12 +7492,12 @@ hoist-non-react-statics@^2.1.1, hoist-non-react-statics@^2.3.1, hoist-non-react- resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-2.5.5.tgz#c5903cf409c0dfd908f388e619d86b9c1174cb47" integrity sha512-rqcy4pJo55FTTLWt+bU8ukscqHeE/e9KWvsOW2b/a3afxQZhwkQdT1rPPCJ0rYXdj4vNcasY8zHTH+jF/qStxw== -hoist-non-react-statics@^3.0.0, hoist-non-react-statics@^3.1.0: - version "3.2.1" - resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.2.1.tgz#c09c0555c84b38a7ede6912b61efddafd6e75e1e" - integrity sha512-TFsu3TV3YLY+zFTZDrN8L2DTFanObwmBLpWvJs1qfUuEQ5bTAdFcwfx2T/bsCXfM9QHSLvjfP+nihEl0yvozxw== +hoist-non-react-statics@^3.0.0, hoist-non-react-statics@^3.1.0, hoist-non-react-statics@^3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.0.tgz#b09178f0122184fb95acf525daaecb4d8f45958b" + integrity sha512-0XsbTXxgiaCDYDIWFcwkmerZPSwywfUqYmwT4jzewKTQSWoE6FCMoUVOeBJWK3E/CrWbxRG3m5GzY4lnIwGRBA== dependencies: - react-is "^16.3.2" + react-is "^16.7.0" home-or-tmp@^2.0.0: version "2.0.0" @@ -7528,9 +7508,9 @@ home-or-tmp@^2.0.0: os-tmpdir "^1.0.1" homedir-polyfill@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/homedir-polyfill/-/homedir-polyfill-1.0.1.tgz#4c2bbc8a758998feebf5ed68580f76d46768b4bc" - integrity sha1-TCu8inWJmP7r9e1oWA921GdotLw= + version "1.0.3" + resolved "https://registry.yarnpkg.com/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz#743298cef4e5af3e194161fbadcc2151d3a058e8" + integrity sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA== dependencies: parse-passwd "^1.0.0" @@ -7572,10 +7552,12 @@ hpp@^0.2.2: lodash "^4.7.0" type-is "^1.6.12" -hsts@2.1.0, hsts@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/hsts/-/hsts-2.1.0.tgz#cbd6c918a2385fee1dd5680bfb2b3a194c0121cc" - integrity sha512-zXhh/DqgrTXJ7erTN6Fh5k/xjMhDGXCqdYN3wvxUvGUQvnxcFfUd8E+6vLg/nk3ss1TYMb+DhRl25fYABioTvA== +hsts@2.2.0, hsts@^2.1.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/hsts/-/hsts-2.2.0.tgz#09119d42f7a8587035d027dda4522366fe75d964" + integrity sha512-ToaTnQ2TbJkochoVcdXYm4HOCliNozlviNsg+X2XQLQvZNI/kCHR9rZxVYpJB3UPcHz80PgxRyWQ7PdU1r+VBQ== + dependencies: + depd "2.0.0" html-comment-regex@^1.1.0: version "1.1.2" @@ -7629,27 +7611,17 @@ htmlescape@^1.1.0: resolved "https://registry.yarnpkg.com/htmlescape/-/htmlescape-1.1.1.tgz#3a03edc2214bca3b66424a3e7959349509cb0351" integrity sha1-OgPtwiFLyjtmQko+eVk0lQnLA1E= -htmlparser2@^3.9.1: - version "3.10.0" - resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-3.10.0.tgz#5f5e422dcf6119c0d983ed36260ce9ded0bee464" - integrity sha512-J1nEUGv+MkXS0weHNWVKJJ+UrLfePxRWpN3C9bEi9fLxL2+ggW94DQvgYVXsaT30PGwYRIZKNZXuyMhp3Di4bQ== +htmlparser2@^3.3.0, htmlparser2@^3.9.1: + version "3.10.1" + resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-3.10.1.tgz#bd679dc3f59897b6a34bb10749c855bb53a9392f" + integrity sha512-IgieNijUMbkDovyoKObU1DUhm1iwNYE/fuifEoEHfd1oZKZDaONBSkal7Y01shxsM49R4XaMdGez3WnF9UfiCQ== dependencies: - domelementtype "^1.3.0" + domelementtype "^1.3.1" domhandler "^2.3.0" domutils "^1.5.1" entities "^1.1.1" inherits "^2.0.1" - readable-stream "^3.0.6" - -htmlparser2@~3.3.0: - version "3.3.0" - resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-3.3.0.tgz#cc70d05a59f6542e43f0e685c982e14c924a9efe" - integrity sha1-zHDQWln2VC5D8OaFyYLhTJJKnv4= - dependencies: - domelementtype "1" - domhandler "2.1" - domutils "1.1" - readable-stream "1.0" + readable-stream "^3.1.1" http-deceiver@^1.2.7: version "1.2.7" @@ -7667,13 +7639,13 @@ http-errors@1.6.3, http-errors@~1.6.2, http-errors@~1.6.3: statuses ">= 1.4.0 < 2" http-errors@^1.7.0, http-errors@^1.7.1: - version "1.7.1" - resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.7.1.tgz#6a4ffe5d35188e1c39f872534690585852e1f027" - integrity sha512-jWEUgtZWGSMba9I1N3gc1HmvpBUaNC9vDdA46yScAdp+C5rdEuKWUBLWTQpW9FwSWSbYYs++b6SDCxf9UEJzfw== + version "1.7.2" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.7.2.tgz#4f5029cf13239f31036e5b2e55292bcfbcc85c8f" + integrity sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg== dependencies: depd "~1.1.2" inherits "2.0.3" - setprototypeof "1.1.0" + setprototypeof "1.1.1" statuses ">= 1.5.0 < 2" toidentifier "1.0.0" @@ -7784,9 +7756,9 @@ icss-utils@^2.1.0: postcss "^6.0.1" idx@^2.4.0: - version "2.5.2" - resolved "https://registry.yarnpkg.com/idx/-/idx-2.5.2.tgz#4b405c2e6d68d04136e0a368a7ab35b9caa0595f" - integrity sha512-MLoGF4lQU5q/RqJJjRsuid52emu7tPVtSSZaYXsqRvSjvXdBEmIwk2urvbNvPBRU9Ox9I4WYnxiz2GjhU34Lrw== + version "2.5.4" + resolved "https://registry.yarnpkg.com/idx/-/idx-2.5.4.tgz#4ce79359b085e1b75dda7521de894a518093bc42" + integrity sha512-TMxVRvWR/0LLb1HHVJ5oGtrUNjyHevjLtGAaGnV9/LdLuljfUS1KT6zBWB9XeUaZfloPMenC51MqoJmNnj3DHw== ieee754@1.1.8: version "1.1.8" @@ -7798,10 +7770,10 @@ ieee754@^1.1.4: resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.12.tgz#50bf24e5b9c8bb98af4964c941cdb0918da7b60b" integrity sha512-GguP+DRY+pJ3soyIiGPTvdiVXjZ+DbXOxGpXn3eMvNW4x4irjqXm4wHKscC+TfxSJ0yw/S1F24tqdMNsMZTiLA== -ienoopen@1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/ienoopen/-/ienoopen-1.0.0.tgz#346a428f474aac8f50cf3784ea2d0f16f62bda6b" - integrity sha1-NGpCj0dKrI9QzzeE6i0PFvYr2ms= +ienoopen@1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/ienoopen/-/ienoopen-1.1.0.tgz#411e5d530c982287dbdc3bb31e7a9c9e32630974" + integrity sha512-MFs36e/ca6ohEKtinTJ5VvAJ6oDRAYFdYXweUnGY9L9vcoqFOU4n2ZhmJ0C4z/cwGZ3YIQRSB3XZ1+ghZkY5NQ== ignore-by-default@^1.0.1: version "1.0.1" @@ -7815,24 +7787,29 @@ ignore-walk@^3.0.1: dependencies: minimatch "^3.0.4" -ignore@^3.3.3, ignore@^3.3.5, ignore@^3.3.6: +ignore@^3.3.3, ignore@^3.3.6: version "3.3.10" resolved "https://registry.yarnpkg.com/ignore/-/ignore-3.3.10.tgz#0a97fb876986e8081c631160f8f9f389157f0043" integrity sha512-Pgs951kaMm5GXP7MOvxERINe3gsaVjUWFm+UZPSq9xYriQAksyhg0csnS0KXSNRD5NmNdapXEpjxG49+AKh/ug== +ignore@^4.0.3: + version "4.0.6" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-4.0.6.tgz#750e3db5862087b4737ebac8207ffd1ef27b25fc" + integrity sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg== + imgix-core-js@^1.0.6: - version "1.2.0" - resolved "https://registry.yarnpkg.com/imgix-core-js/-/imgix-core-js-1.2.0.tgz#72163ebd312b25cdae077340d13cf94dc09327f2" - integrity sha512-Eq8IabyhZwwP1m+E26L4AJLCznqckFuL2nvOFmtYESmyFEv4IXa/UU58DHegnmMYoPqHunmG9ttL6NDdB0ddbg== + version "1.2.1" + resolved "https://registry.yarnpkg.com/imgix-core-js/-/imgix-core-js-1.2.1.tgz#069244b6cb53d70ffcba7560fd883b72cfeb3862" + integrity sha512-5lQTZGQislkRl0zdOQfZsKxlkn/d8YJJXhnmbLUvGX5XJX1iEVGWPlWxsj2iJDqMWzMmxarBtxMtnD4PJvM1ww== dependencies: crc "^3.5.0" js-base64 "^2.1.9" md5 "^2.2.1" immutable-tuple@^0.4.9: - version "0.4.9" - resolved "https://registry.yarnpkg.com/immutable-tuple/-/immutable-tuple-0.4.9.tgz#473ebdd6c169c461913a454bf87ef8f601a20ff0" - integrity sha512-LWbJPZnidF8eczu7XmcnLBsumuyRBkpwIRPCZxlojouhBo5jEBO4toj6n7hMy6IxHU/c+MqDSWkvaTpPlMQcyA== + version "0.4.10" + resolved "https://registry.yarnpkg.com/immutable-tuple/-/immutable-tuple-0.4.10.tgz#e0b1625384f514084a7a84b749a3bb26e9179929" + integrity sha512-45jheDbc3Kr5Cw8EtDD+4woGRUV0utIrJBZT8XH0TPZRfm8tzT0/sLGGzyyCCFqFMG5Pv5Igf3WY/arn6+8V9Q== immutable@*, immutable@3.7.4, immutable@3.x, immutable@^3.8.1, immutable@~3.7.4: version "3.7.4" @@ -7953,9 +7930,9 @@ internal-ip@1.2.0: meow "^3.3.0" interpret@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.1.0.tgz#7ed1b1410c6a0e0f78cf95d3b8440c63f78b8614" - integrity sha1-ftGxQQxqDg94z5XTuEQMY/eLhhQ= + version "1.2.0" + resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.2.0.tgz#d5061a6224be58e8083985f5014d844359576296" + integrity sha512-mT34yGKMNceBQUoVn7iCDKDntA7SC6gycMAWzGx1z/CMCTV7b2AAtXlo3nRyHZ1FelRkQbQjprHSYGwzLtkVbw== invariant@^2.2.0, invariant@^2.2.1, invariant@^2.2.2, invariant@^2.2.4: version "2.2.4" @@ -7969,7 +7946,7 @@ invert-kv@^1.0.0: resolved "https://registry.yarnpkg.com/invert-kv/-/invert-kv-1.0.0.tgz#104a8e4aaca6d3d8cd157a8ef8bfab2d7a3ffdb6" integrity sha1-EEqOSqym09jNFXqO+L+rLXo//bY= -ioredis@3.2.2, ioredis@^3.1.4: +ioredis@3.2.2: version "3.2.2" resolved "https://registry.yarnpkg.com/ioredis/-/ioredis-3.2.2.tgz#b7d5ff3afd77bb9718bb2821329b894b9a44c00b" integrity sha512-g+ShTQYLsCcOUkNOK6CCEZbj3aRDVPw3WOwXk+LxlUKvuS9ujEqP2MppBHyRVYrNNFW/vcPaTBUZ2ctGNSiOCA== @@ -7998,10 +7975,10 @@ ioredis@3.2.2, ioredis@^3.1.4: redis-commands "^1.2.0" redis-parser "^2.4.0" -ioredis@^4.0.0: - version "4.3.0" - resolved "https://registry.yarnpkg.com/ioredis/-/ioredis-4.3.0.tgz#a92850dd8794eaee4f38a265c830ca823a09d345" - integrity sha512-TwTp93UDKlKVQeg9ThuavNh4Vs31JTlqn+cI/J6z21OtfghyJm5I349ZlsKobOeEyS4INITMLQ1fhR7xwf9Fxg== +ioredis@^4.0.0, ioredis@^4.5.1: + version "4.6.2" + resolved "https://registry.yarnpkg.com/ioredis/-/ioredis-4.6.2.tgz#840847117fe0190a9309085847311a07183fc385" + integrity sha512-zlc/LeoeriHTXm5z3rakPcfRcUV9x+xr0E+7/L7KH0D5z7sI5ngEQWR2RUxnwFcxUcCkvrXMztRIdBP3DhqMAQ== dependencies: cluster-key-slot "^1.0.6" debug "^3.1.0" @@ -8060,26 +8037,12 @@ is-buffer@^1.1.0, is-buffer@^1.1.5, is-buffer@~1.1.1: resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w== -is-builtin-module@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-builtin-module/-/is-builtin-module-1.0.0.tgz#540572d34f7ac3119f8f76c30cbc1b1e037affbe" - integrity sha1-VAVy0096wxGfj3bDDLwbHgN6/74= - dependencies: - builtin-modules "^1.0.0" - -is-callable@^1.1.3, is-callable@^1.1.4: +is-callable@^1.1.4: version "1.1.4" resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.1.4.tgz#1e1adf219e1eeb684d691f9d6a05ff0d30a24d75" integrity sha512-r5p9sxJjYnArLjObpjA4xu5EKI3CuKHkJXMhT7kwbpUyIFD1n5PMAsoPvWnvtZiNz7LjkYDRZhd7FlI0eMijEA== -is-ci@1.0.10: - version "1.0.10" - resolved "https://registry.yarnpkg.com/is-ci/-/is-ci-1.0.10.tgz#f739336b2632365061a9d48270cd56ae3369318e" - integrity sha1-9zkzayYyNlBhqdSCcM1WrjNpMY4= - dependencies: - ci-info "^1.0.0" - -is-ci@^1.0.10: +is-ci@1.2.1, is-ci@^1.0.10: version "1.2.1" resolved "https://registry.yarnpkg.com/is-ci/-/is-ci-1.2.1.tgz#e3779c8ee17fccf428488f6e281187f2e632841c" integrity sha512-s6tfsaQaQi3JNciBH6shVqEDvhGut0SUXr31ag8Pd8BBbVVlcGfWhpPmEOoM6RJ5TFhbypvf5yyRw/VXW1IiWg== @@ -8551,9 +8514,9 @@ iterall@^1.1.3, iterall@^1.2.1: integrity sha512-yynBb1g+RFUPY64fTrFv7nsjRrENBQJaX2UL+2Szc9REFrSNm1rpSXHGzhmAy7a9uv3vlvgBlXnf9RqmPH1/DA== iterate-object@^1.3.1: - version "1.3.2" - resolved "https://registry.yarnpkg.com/iterate-object/-/iterate-object-1.3.2.tgz#24ec15affa5d0039e8839695a21c2cae1f45b66b" - integrity sha1-JOwVr/pdADnog5aVohwsrh9Ftms= + version "1.3.3" + resolved "https://registry.yarnpkg.com/iterate-object/-/iterate-object-1.3.3.tgz#c58e60f7f0caefa2d382027a484b215988a7a296" + integrity sha512-DximWbkke36cnrSfNJv6bgcB2QOMV9PRD2FiowwzCoMsh8RupFLdbNIzWe+cVDWT+NIMNJgGlB1dGxP6kpzGtA== jest-changed-files@^22.2.0: version "22.4.3" @@ -8865,14 +8828,14 @@ joi@^9.2.0: topo "2.x.x" js-base64@^2.1.9: - version "2.5.0" - resolved "https://registry.yarnpkg.com/js-base64/-/js-base64-2.5.0.tgz#42255ba183ab67ce59a0dee640afdc00ab5ae93e" - integrity sha512-wlEBIZ5LP8usDylWbDNhKPEFVFdI5hCHpnVoT/Ysvoi/PRhJENm/Rlh9TvjYB38HFfKZN7OzEbRjmjvLkFw11g== + version "2.5.1" + resolved "https://registry.yarnpkg.com/js-base64/-/js-base64-2.5.1.tgz#1efa39ef2c5f7980bb1784ade4a8af2de3291121" + integrity sha512-M7kLczedRMYX4L8Mdh4MzyAMM9O5osx+4FcOQuTvr3A9F2D9S5JXheN0ewNbrvK2UatkTRhL5ejGmGSjNMiZuw== js-levenshtein@^1.1.3: - version "1.1.4" - resolved "https://registry.yarnpkg.com/js-levenshtein/-/js-levenshtein-1.1.4.tgz#3a56e3cbf589ca0081eb22cd9ba0b1290a16d26e" - integrity sha512-PxfGzSs0ztShKrUYPIn5r0MtyAhYcCwmndozzpz8YObbPnD1jFxzlBGbRnX2mIu6Z13xN6+PTu05TQFnZFlzow== + version "1.1.6" + resolved "https://registry.yarnpkg.com/js-levenshtein/-/js-levenshtein-1.1.6.tgz#c6cee58eb3550372df8deb85fad5ce66ce01d59d" + integrity sha512-X2BB11YZtrRqY4EnQcLX5Rh373zbK4alC1FW7D7MBhL2gtcC17cTnr6DmfHZeS0s2rTHjUTMMHfG7gO8SSdw+g== js-tokens@^3.0.0, js-tokens@^3.0.2: version "3.0.2" @@ -8885,9 +8848,9 @@ js-tokens@^3.0.0, js-tokens@^3.0.2: integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== js-yaml@^3.4.3, js-yaml@^3.7.0, js-yaml@^3.8.2, js-yaml@^3.9.1: - version "3.12.0" - resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.12.0.tgz#eaed656ec8344f10f527c6bfa1b6e2244de167d1" - integrity sha512-PIt2cnwmPfL4hKNwqeiuz4bKfnzHTBv6HyVgjahA6mPLwPDzjDWrplJBMjHUFxku/N3FlmrbyPclad+I+4mJ3A== + version "3.12.2" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.12.2.tgz#ef1d067c5a9d9cb65bd72f285b5d8105c77f14fc" + integrity sha512-QHn/Lh/7HhZ/Twc7vJYQTkjuCa0kaCcDcjK5Zlk2rvnUpy7DxMJ23+Jc2dcyvltwQVg1nygAVlB2oRDFHoRS5Q== dependencies: argparse "^1.0.7" esprima "^4.0.0" @@ -9028,6 +8991,13 @@ json5@^0.5.0, json5@^0.5.1: resolved "https://registry.yarnpkg.com/json5/-/json5-0.5.1.tgz#1eade7acc012034ad84e2396767ead9fa5495821" integrity sha1-Hq3nrMASA0rYTiOWdn6tn6VJWCE= +json5@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.1.tgz#779fb0018604fa854eacbf6252180d83543e3dbe" + integrity sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow== + dependencies: + minimist "^1.2.0" + jsonfile@^2.1.0: version "2.4.0" resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-2.4.0.tgz#3736a2b428b87bbda0cc83b53fa3d633a35c2ae8" @@ -9060,11 +9030,11 @@ jsonparse@^1.2.0: integrity sha1-P02uSpH6wxX3EGL4UhzCOfE2YoA= jsonwebtoken@^8.3.0: - version "8.4.0" - resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-8.4.0.tgz#8757f7b4cb7440d86d5e2f3becefa70536c8e46a" - integrity sha512-coyXjRTCy0pw5WYBpMvWOMN+Kjaik2MwTUIq9cna/W7NpO9E+iYbumZONAz3hcr+tXFJECoQVrtmIoC3Oz0gvg== + version "8.5.0" + resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-8.5.0.tgz#ebd0ca2a69797816e1c5af65b6c759787252947e" + integrity sha512-IqEycp0znWHNA11TpYi77bVgyBO/pGESDh7Ajhas+u0ttkGkKYIIAjniL4Bw5+oVejVF+SYkaI7XKfwCCyeTuA== dependencies: - jws "^3.1.5" + jws "^3.2.1" lodash.includes "^4.3.0" lodash.isboolean "^3.0.3" lodash.isinteger "^4.0.4" @@ -9073,6 +9043,7 @@ jsonwebtoken@^8.3.0: lodash.isstring "^4.0.1" lodash.once "^4.0.0" ms "^2.1.1" + semver "^5.6.0" jsprim@^1.2.2: version "1.4.1" @@ -9096,21 +9067,21 @@ jsx-ast-utils@^2.0.0, jsx-ast-utils@^2.0.1: dependencies: array-includes "^3.0.3" -jwa@^1.1.5: - version "1.1.6" - resolved "https://registry.yarnpkg.com/jwa/-/jwa-1.1.6.tgz#87240e76c9808dbde18783cf2264ef4929ee50e6" - integrity sha512-tBO/cf++BUsJkYql/kBbJroKOgHWEigTKBAjjBEmrMGYd1QMBC74Hr4Wo2zCZw6ZrVhlJPvoMrkcOnlWR/DJfw== +jwa@^1.2.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/jwa/-/jwa-1.4.0.tgz#8f61dc799acf0309f2d4b22a91ce73d6d2bb206c" + integrity sha512-mt6IHaq0ZZWDBspg0Pheu3r9sVNMEZn+GJe1zcdYyhFcDSclp3J8xEdO4PjZolZ2i8xlaVU1LetHM0nJejYsEw== dependencies: buffer-equal-constant-time "1.0.1" - ecdsa-sig-formatter "1.0.10" + ecdsa-sig-formatter "1.0.11" safe-buffer "^5.0.1" -jws@^3.1.3, jws@^3.1.5: - version "3.1.5" - resolved "https://registry.yarnpkg.com/jws/-/jws-3.1.5.tgz#80d12d05b293d1e841e7cb8b4e69e561adcf834f" - integrity sha512-GsCSexFADNQUr8T5HPJvayTjvPIfoyJPtLQBwn5a4WZQchcrPMPMAWcC1AzJVRDKyD6ZPROPAxgv6rfHViO4uQ== +jws@^3.1.3, jws@^3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/jws/-/jws-3.2.1.tgz#d79d4216a62c9afa0a3d5e8b5356d75abdeb2be5" + integrity sha512-bGA2omSrFUkd72dhh05bIAN832znP4wOU3lfuXtRBuGTbsmNmDXMQg28f0Vsxaxgk4myF5YkKQpz6qeRpMgX9g== dependencies: - jwa "^1.1.5" + jwa "^1.2.0" safe-buffer "^5.0.1" keycode@^2.1.2: @@ -9338,9 +9309,9 @@ loader-fs-cache@^1.0.0: mkdirp "0.5.1" loader-runner@^2.3.0: - version "2.3.1" - resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-2.3.1.tgz#026f12fe7c3115992896ac02ba022ba92971b979" - integrity sha512-By6ZFY7ETWOc9RFaAIb23IjJVcM4dvJC/N57nmdz9RSkMXvAXGI7SyVlAw3v8vjtDRlqThgVDVmTnr9fqMlxkw== + version "2.4.0" + resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-2.4.0.tgz#ed47066bfe534d7e84c4c7b9998c2a75607d9357" + integrity sha512-Jsmr89RcXGIwivFY21FcRrisYZfvLMTWx5kOLc+JTxtpBOG6xML0vzbc6SEQG2FO9/4Fc3wW4LVcB5DmGflaRw== loader-utils@0.2.x, loader-utils@^0.2.16: version "0.2.17" @@ -9353,13 +9324,13 @@ loader-utils@0.2.x, loader-utils@^0.2.16: object-assign "^4.0.1" loader-utils@^1.0.2, loader-utils@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-1.1.0.tgz#c98aef488bcceda2ffb5e2de646d6a754429f5cd" - integrity sha1-yYrvSIvM7aL/teLeZG1qdUQp9c0= + version "1.2.3" + resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-1.2.3.tgz#1ff5dc6911c9f0a062531a4c04b609406108c2c7" + integrity sha512-fkpz8ejdnEMG3s37wGL07iSBDg99O9D5yflE9RGNH3hRdx9SOwYfnGYdZOUIZitN8E+E2vkq3MUMYMvPYl5ZZA== dependencies: - big.js "^3.1.3" + big.js "^5.2.2" emojis-list "^2.0.0" - json5 "^0.5.0" + json5 "^1.0.1" locate-path@^2.0.0: version "2.0.0" @@ -9419,11 +9390,6 @@ lodash.cond@^4.3.0: resolved "https://registry.yarnpkg.com/lodash.cond/-/lodash.cond-4.5.2.tgz#f471a1da486be60f6ab955d17115523dd1d255d5" integrity sha1-9HGh2khr5g9quVXRcRVSPdHSVdU= -lodash.debounce@^4.0.8: - version "4.0.8" - resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af" - integrity sha1-gteb/zCmfEAF/9XiUVMArZyk168= - lodash.defaults@^4.0.1, lodash.defaults@^4.2.0: version "4.2.0" resolved "https://registry.yarnpkg.com/lodash.defaults/-/lodash.defaults-4.2.0.tgz#d09178716ffea4dde9e5fb7b37f6f0802274580c" @@ -9519,7 +9485,7 @@ lodash.memoize@~3.0.3: resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-3.0.4.tgz#2dcbd2c287cbc0a55cc42328bd0c736150d53e3f" integrity sha1-LcvSwofLwKVcxCMovQxzYVDVPj8= -lodash.merge@^4.4.0, lodash.merge@^4.6.1: +lodash.merge@^4.4.0: version "4.6.1" resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.1.tgz#adc25d9cb99b9391c59624f379fbba60d7111d54" integrity sha512-AOYza4+Hf5z1/0Hztxpm2/xiPZgi/cjMqdnKTUWTBSKchJlxXXuUSxCCl8rJlf4g6yww/j6mA8nC8Hw/EZWxKQ== @@ -9599,7 +9565,7 @@ lodash.values@^4.3.0: resolved "https://registry.yarnpkg.com/lodash.values/-/lodash.values-4.3.0.tgz#a3a6c2b0ebecc5c2cba1c17e6e620fe81b53d347" integrity sha1-o6bCsOvsxcLLocF+bmIP6BtT00c= -lodash@4.17.11, "lodash@>=3.5 <5", lodash@^4.13.1, lodash@^4.14.0, lodash@^4.15.0, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.2, lodash@^4.17.3, lodash@^4.17.4, lodash@^4.17.5, lodash@^4.2.0, lodash@^4.2.1, lodash@^4.3.0, lodash@^4.5.1, lodash@^4.7.0: +lodash@4.17.11, "lodash@>=3.5 <5", lodash@^4.14.0, lodash@^4.15.0, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.2, lodash@^4.17.3, lodash@^4.17.4, lodash@^4.17.5, lodash@^4.2.0, lodash@^4.2.1, lodash@^4.3.0, lodash@^4.5.1, lodash@^4.7.0: version "4.17.11" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.11.tgz#b39ea6229ef607ecd89e2c8df12536891cac9b8d" integrity sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg== @@ -9693,6 +9659,14 @@ make-dir@^1.0.0: dependencies: pify "^3.0.0" +make-dir@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-2.1.0.tgz#5f0310e18b8be898cc07009295a30ae41e91e6f5" + integrity sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA== + dependencies: + pify "^4.0.1" + semver "^5.6.0" + makeerror@1.0.x: version "1.0.11" resolved "https://registry.yarnpkg.com/makeerror/-/makeerror-1.0.11.tgz#e01a5c9109f2af79660e4e8b9587790184f5a96c" @@ -9733,9 +9707,9 @@ math-expression-evaluator@^1.2.14: integrity sha1-3oGf282E3M2PrlnGrreWFbnSZqw= math-random@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/math-random/-/math-random-1.0.1.tgz#8b3aac588b8a66e4975e3cdea67f7bb329601fac" - integrity sha1-izqsWIuKZuSXXjzepn97sylgH6w= + version "1.0.4" + resolved "https://registry.yarnpkg.com/math-random/-/math-random-1.0.4.tgz#5dd6943c938548267016d4e34f057583080c514c" + integrity sha512-rUxjysqif/BZQH2yhd5Aaq7vXMSx9NdEsQcyA07uEzIvxgI7zIr33gGsh+RU0/XjmQpCW7RsVof1vlkvQVCK5A== md5.js@^1.3.4: version "1.3.5" @@ -9767,6 +9741,11 @@ mem@^1.1.0: dependencies: mimic-fn "^1.0.0" +memoize-one@^4.0.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-4.1.0.tgz#a2387c58c03fff27ca390c31b764a79addf3f906" + integrity sha512-2GApq0yI/b22J2j9rhbrAlsHb0Qcz+7yWxeLG8h+95sl1XPUgeLimQSOdur4Vw7cUhrBHwaUZxWFZueojqNRzA== + memory-fs@^0.4.0, memory-fs@~0.4.1: version "0.4.1" resolved "https://registry.yarnpkg.com/memory-fs/-/memory-fs-0.4.1.tgz#3a9a20b8462523e447cfbc7e8bb80ed667bfc552" @@ -9884,17 +9863,17 @@ miller-rabin@^4.0.0: bn.js "^4.0.0" brorand "^1.0.1" -"mime-db@>= 1.36.0 < 2", mime-db@~1.37.0: - version "1.37.0" - resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.37.0.tgz#0b6a0ce6fdbe9576e25f1f2d2fde8830dc0ad0d8" - integrity sha512-R3C4db6bgQhlIhPU48fUtdVmKnflq+hRdad7IyKhtFj06VPNVdk2RhiYL3UjQIlso8L+YxAtFkobT0VK+S/ybg== +"mime-db@>= 1.38.0 < 2", mime-db@~1.38.0: + version "1.38.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.38.0.tgz#1a2aab16da9eb167b49c6e4df2d9c68d63d8e2ad" + integrity sha512-bqVioMFFzc2awcdJZIzR3HjZFX20QhilVS7hytkKrv7xFAn8bM1gzc/FOX2awLISvWe0PV8ptFKcon+wZ5qYkg== mime-types@^2.1.12, mime-types@~2.1.17, mime-types@~2.1.18, mime-types@~2.1.19: - version "2.1.21" - resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.21.tgz#28995aa1ecb770742fe6ae7e58f9181c744b3f96" - integrity sha512-3iL6DbwpyLzjR3xHSFNFeb9Nz/M8WDkX33t1GFQnFOllWk8pOrh/LSrB5OXlnlW5P9LH73X6loW/eogc+F5lJg== + version "2.1.22" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.22.tgz#fe6b355a190926ab7698c9a0556a11199b2199bd" + integrity sha512-aGl6TZGnhm/li6F7yx82bJiBZwgiEa4Hf6CNr8YO+r5UHr53tSTYZb102zyU50DOWWKeOv0uQLRL0/9EiKWCog== dependencies: - mime-db "~1.37.0" + mime-db "~1.38.0" mime@1.4.1: version "1.4.1" @@ -9993,13 +9972,6 @@ mixin-deep@^1.2.0: for-in "^1.0.2" is-extendable "^1.0.1" -mkdirp@0.5.0: - version "0.5.0" - resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.0.tgz#1d73076a6df986cd9344e15e71fcc05a4c9abf12" - integrity sha1-HXMHam35hs2TROFecfzAWkyavxI= - dependencies: - minimist "0.0.8" - mkdirp@0.5.1, mkdirp@0.5.x, mkdirp@0.x.x, "mkdirp@>=0.5 0", mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@~0.5.0, mkdirp@~0.5.1: version "0.5.1" resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903" @@ -10035,15 +10007,10 @@ moment-timezone@^0.5.23: dependencies: moment ">= 2.9.0" -moment@2.22.2: - version "2.22.2" - resolved "https://registry.yarnpkg.com/moment/-/moment-2.22.2.tgz#3c257f9839fc0e93ff53149632239eb90783ff66" - integrity sha1-PCV/mDn8DpP/UxSWMiOeuQeD/2Y= - -moment@2.x.x, "moment@>= 2.9.0", moment@^2.15.2, moment@^2.20.1, moment@^2.22.1, moment@^2.22.2: - version "2.23.0" - resolved "https://registry.yarnpkg.com/moment/-/moment-2.23.0.tgz#759ea491ac97d54bac5ad776996e2a58cc1bc225" - integrity sha512-3IE39bHVqFbWWaPOMHZF98Q9c3LDKGTmypMiTM2QygGXXElkFWIH7GxfmlwmY2vwa+wmNsoYZmG2iusf1ZjJoA== +moment@2.24.0, moment@2.x.x, "moment@>= 2.9.0", moment@^2.15.2, moment@^2.20.1, moment@^2.22.1, moment@^2.22.2: + version "2.24.0" + resolved "https://registry.yarnpkg.com/moment/-/moment-2.24.0.tgz#0d055d53f5052aa653c9f6eb68bb5d12bf5c2b5b" + integrity sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg== mrm-core@^1.1.0: version "1.1.0" @@ -10064,11 +10031,6 @@ mrm-core@^1.1.0: webpack-merge "^4.0.0" yarn-install "^0.2.1" -ms@0.7.1: - version "0.7.1" - resolved "https://registry.yarnpkg.com/ms/-/ms-0.7.1.tgz#9cd13c03adbff25b65effde7ce864ee952017098" - integrity sha1-nNE8A62/8ltl7/3nzoZO6VIBcJg= - ms@2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" @@ -10092,11 +10054,16 @@ multicast-dns@^6.0.1: dns-packet "^1.3.1" thunky "^1.0.2" -mute-stream@0.0.7, mute-stream@~0.0.4: +mute-stream@0.0.7: version "0.0.7" resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.7.tgz#3075ce93bc21b8fab43e1bc4da7e8115ed1e7bab" integrity sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s= +mute-stream@~0.0.4: + version "0.0.8" + resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.8.tgz#1630c42b2251ff81e2a283de96a5497ea92e5e0d" + integrity sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA== + nan@^2.9.2: version "2.12.1" resolved "https://registry.yarnpkg.com/nan/-/nan-2.12.1.tgz#7b1aa193e9aa86057e3c7bbd0ac448e770925552" @@ -10162,7 +10129,7 @@ nested-error-stacks@^2.0.0: resolved "https://registry.yarnpkg.com/nested-error-stacks/-/nested-error-stacks-2.1.0.tgz#0fbdcf3e13fe4994781280524f8b96b0cdff9c61" integrity sha512-AO81vsIO1k1sM4Zrd6Hu7regmJN1NSiAja10gc4bX3F0wd+9rQmcuHQaHVQCYIEC8iFXnE+mavh23GOt7wBgug== -next-tick@1: +next-tick@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/next-tick/-/next-tick-1.0.0.tgz#ca86d1fe8828169b0120208e3dc8424b9db8342c" integrity sha1-yobR/ogoFpsBICCOPchCS524NCw= @@ -10189,6 +10156,11 @@ node-env-file@^0.1.8: resolved "https://registry.yarnpkg.com/node-env-file/-/node-env-file-0.1.8.tgz#fccb7b050f735b5a33da9eb937cf6f1ab457fb69" integrity sha1-/Mt7BQ9zW1oz2p65N89vGrRX+2k= +node-eta@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/node-eta/-/node-eta-0.1.1.tgz#4066109b39371c761c72b7ebda9a9ea0a5de121f" + integrity sha1-QGYQmzk3HHYccrfr2pqeoKXeEh8= + node-fetch@^1.0.1: version "1.7.3" resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-1.7.3.tgz#980f6f72d85211a5347c6b2bc18c5b84c3eb47ef" @@ -10213,9 +10185,9 @@ node-int64@^0.4.0: integrity sha1-h6kGXNs1XTGC2PlM4RGIuCXGijs= node-libs-browser@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/node-libs-browser/-/node-libs-browser-2.1.0.tgz#5f94263d404f6e44767d726901fff05478d600df" - integrity sha512-5AzFzdoIMb89hBGMZglEegffzgRg+ZFoUmisQ8HI4j1KDdpx13J0taNp2y9xPbur6W61gepGDDotGBVQ7mfUCg== + version "2.2.0" + resolved "https://registry.yarnpkg.com/node-libs-browser/-/node-libs-browser-2.2.0.tgz#c72f60d9d46de08a940dedbb25f3ffa2f9bbaa77" + integrity sha512-5MQunG/oyOaBdttrL40dA7bUfPORLRWMUJLQtMg7nluxUvk5XwnLdL9twQHFAjRx/y7mIMkLKT9++qPbbk6BZA== dependencies: assert "^1.1.1" browserify-zlib "^0.2.0" @@ -10224,7 +10196,7 @@ node-libs-browser@^2.0.0: constants-browserify "^1.0.0" crypto-browserify "^3.11.0" domain-browser "^1.1.1" - events "^1.0.0" + events "^3.0.0" https-browserify "^1.0.0" os-browserify "^0.3.0" path-browserify "0.0.0" @@ -10238,15 +10210,16 @@ node-libs-browser@^2.0.0: timers-browserify "^2.0.4" tty-browserify "0.0.0" url "^0.11.0" - util "^0.10.3" + util "^0.11.0" vm-browserify "0.0.4" node-notifier@^5.2.1: - version "5.3.0" - resolved "https://registry.yarnpkg.com/node-notifier/-/node-notifier-5.3.0.tgz#c77a4a7b84038733d5fb351aafd8a268bfe19a01" - integrity sha512-AhENzCSGZnZJgBARsUjnQ7DnZbzyP+HxlVXuD0xqAnvL8q+OqtSX7lGg9e8nHzwXkMMXNdVeqq4E2M3EUAqX6Q== + version "5.4.0" + resolved "https://registry.yarnpkg.com/node-notifier/-/node-notifier-5.4.0.tgz#7b455fdce9f7de0c63538297354f3db468426e6a" + integrity sha512-SUDEb+o71XR5lXSTyivXd9J7fCloE3SyP4lSgt3lU2oSANiox+SxlNRGPjDKrwU1YN3ix2KN/VGGCg0t01rttQ== dependencies: growly "^1.3.0" + is-wsl "^1.1.0" semver "^5.5.0" shellwords "^0.1.1" which "^1.3.0" @@ -10267,19 +10240,19 @@ node-pre-gyp@^0.10.0: semver "^5.3.0" tar "^4" -node-releases@^1.1.1: - version "1.1.2" - resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.2.tgz#93c17fba5eec8650ad908de5433fa8763baebe4d" - integrity sha512-j1gEV/zX821yxdWp/1vBMN0pSUjuH9oGUdLCb4PfUko6ZW7KdRs3Z+QGGwDUhYtSpQvdVVyLd2V0YvLsmdg5jQ== +node-releases@^1.1.8: + version "1.1.10" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.10.tgz#5dbeb6bc7f4e9c85b899e2e7adcc0635c9b2adf7" + integrity sha512-KbUPCpfoBvb3oBkej9+nrU0/7xPlVhmhhUJ1PZqwIP5/1dJkRWKWD3OONjo6M2J7tSCBtDCumLwwqeI+DWWaLQ== dependencies: semver "^5.3.0" nodemon@^1.11.0: - version "1.18.9" - resolved "https://registry.yarnpkg.com/nodemon/-/nodemon-1.18.9.tgz#90b467efd3b3c81b9453380aeb2a2cba535d0ead" - integrity sha512-oj/eEVTEI47pzYAjGkpcNw0xYwTl4XSTUQv2NPQI6PpN3b75PhpuYk3Vb3U80xHCyM2Jm+1j68ULHXl4OR3Afw== + version "1.18.10" + resolved "https://registry.yarnpkg.com/nodemon/-/nodemon-1.18.10.tgz#3ba63f64eb4c283cf3e4f75f30817e9d4f393afe" + integrity sha512-we51yBb1TfEvZamFchRgcfLbVYgg0xlGbyXmOtbBzDwxwgewYS/YbZ5tnlnsH51+AoSTTsT3A2E/FloUbtH8cQ== dependencies: - chokidar "^2.0.4" + chokidar "^2.1.0" debug "^3.1.0" ignore-by-default "^1.0.1" minimatch "^3.0.4" @@ -10291,9 +10264,9 @@ nodemon@^1.11.0: update-notifier "^2.5.0" noop6@^1.0.0, noop6@^1.0.1: - version "1.0.7" - resolved "https://registry.yarnpkg.com/noop6/-/noop6-1.0.7.tgz#96767bf2058ba59ca8cb91559347ddc80239fa8e" - integrity sha1-lnZ78gWLpZyoy5FVk0fdyAI5+o4= + version "1.0.8" + resolved "https://registry.yarnpkg.com/noop6/-/noop6-1.0.8.tgz#eff06e2e5b3621e9e5618f389d6a2294f76e64ad" + integrity sha512-+Al5csMVc40I8xRfJsyBcN1IbpyvebOuQmMfxdw+AL6ECELey12ANgNTRhMfTwNIDU4W9W0g8EHLcsb3+3qPFA== nopt@^4.0.1: version "4.0.1" @@ -10311,12 +10284,12 @@ nopt@~1.0.10: abbrev "1" normalize-package-data@^2.3.2, normalize-package-data@^2.3.4: - version "2.4.0" - resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.4.0.tgz#12f95a307d58352075a04907b84ac8be98ac012f" - integrity sha512-9jjUFbTPfEy3R/ad/2oNbKtW9Hgovl5O1FvFWKkKblNXoN/Oou6+9+KKohPK13Yc3/TyunyWhJp6gvRNR/PPAw== + version "2.5.0" + resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.5.0.tgz#e66db1838b200c1dfc233225d12cb36520e234a8" + integrity sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA== dependencies: hosted-git-info "^2.1.4" - is-builtin-module "^1.0.0" + resolve "^1.10.0" semver "2 || 3 || 4 || 5" validate-npm-package-license "^3.0.1" @@ -10327,6 +10300,11 @@ normalize-path@^2.0.0, normalize-path@^2.0.1, normalize-path@^2.1.1: dependencies: remove-trailing-separator "^1.0.1" +normalize-path@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" + integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== + normalize-range@^0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/normalize-range/-/normalize-range-0.1.2.tgz#2d10c06bdfd312ea9777695a4d28439456b75942" @@ -10348,14 +10326,14 @@ now-env@^3.1.0: integrity sha512-f+jXC+UkoxD/g9Nlig99Bxswoh7UUuQxw0EsPfuueHnVpVE0LfgQ4el5dxY4TSXwrL9mEF9GGm0gb7r3K8r/ug== npm-bundled@^1.0.1: - version "1.0.5" - resolved "https://registry.yarnpkg.com/npm-bundled/-/npm-bundled-1.0.5.tgz#3c1732b7ba936b3a10325aef616467c0ccbcc979" - integrity sha512-m/e6jgWu8/v5niCUKQi9qQl8QdeEduFA96xHDDzFGqly0OOjI7c+60KM/2sppfnUU9JJagf+zs+yGhqSOFj71g== + version "1.0.6" + resolved "https://registry.yarnpkg.com/npm-bundled/-/npm-bundled-1.0.6.tgz#e7ba9aadcef962bb61248f91721cd932b3fe6bdd" + integrity sha512-8/JCaftHwbd//k6y2rEWp6k1wxVfpFzB6t1p825+cUb7Ym2XQfhwIC5KwhrvzZRJu+LtDE585zVaS32+CGtf0g== npm-packlist@^1.1.6: - version "1.1.12" - resolved "https://registry.yarnpkg.com/npm-packlist/-/npm-packlist-1.1.12.tgz#22bde2ebc12e72ca482abd67afc51eb49377243a" - integrity sha512-WJKFOVMeAlsU/pjXuqVdzU0WfgtIBCupkEVwn+1Y0ERAbUfWw8R4GjgVbaKnUjRoD2FoQbHOCbOyT5Mbs9Lw4g== + version "1.4.1" + resolved "https://registry.yarnpkg.com/npm-packlist/-/npm-packlist-1.4.1.tgz#19064cdf988da80ea3cee45533879d90192bbfbc" + integrity sha512-+TcdO7HJJ8peiiYhvPxsEDhF3PJFGUGRcFsGve3vxvxdcpO2Z4Z7rkosRM0kWj6LfbK/P0gu3dzk5RU1ffvFcw== dependencies: ignore-walk "^3.0.1" npm-bundled "^1.0.1" @@ -10419,14 +10397,9 @@ number-is-nan@^1.0.0: integrity sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0= nwsapi@^2.0.7: - version "2.0.9" - resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.0.9.tgz#77ac0cdfdcad52b6a1151a84e73254edc33ed016" - integrity sha512-nlWFSCTYQcHk/6A9FFnfhKc14c3aFhfdNBXgo8Qgi9QTBu/qg3Ww+Uiz9wMzXd1T8GFxPc2QIHB6Qtf2XFryFQ== - -oauth-sign@~0.8.2: - version "0.8.2" - resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.8.2.tgz#46a6ab7f0aead8deae9ec0565780b7d4efeb9d43" - integrity sha1-Rqarfwrq2N6unsBWV4C31O/rnUM= + version "2.1.1" + resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.1.1.tgz#08d6d75e69fd791bdea31507ffafe8c843b67e9c" + integrity sha512-T5GaA1J/d34AC8mkrFD2O0DR17kwJ702ZOtJOsS8RpbsQZVOC2/xYFb1i/cw+xdM54JIlMuojjDOYct8GIWtwg== oauth-sign@~0.9.0: version "0.9.0" @@ -10467,10 +10440,10 @@ object-hash@^1.1.4: resolved "https://registry.yarnpkg.com/object-hash/-/object-hash-1.3.1.tgz#fde452098a951cb145f039bb7d455449ddc126df" integrity sha512-OSuu/pU4ENM9kmREg0BdNrUDIl1heYa4mBZacJc+vVWz4GtAwu7jO8s4AIt2aGRUTqxykpWzI3Oqnsm13tTMDA== -object-keys@^1.0.11, object-keys@^1.0.12, object-keys@~1.0.0: - version "1.0.12" - resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.0.12.tgz#09c53855377575310cca62f55bb334abff7b3ed2" - integrity sha512-FTMyFUm2wBcGHnH2eXmz7tC6IwlqQZ6mVZ+6dm6vZ4IQIHjs6FdNsQBuKGPuUUUY6NfJw2PshC08Tn6LzLDOag== +object-keys@^1.0.11, object-keys@^1.0.12, object-keys@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.0.tgz#11bd22348dd2e096a045ab06f6c85bcc340fa032" + integrity sha512-6OO5X1+2tYkNyNEx6TsCxEqFfRWaqx6EtMiSbGrw8Ob8v9Ne+Hl8rBAgLBZn5wjEz3s/s6U1WXFUFOcxxAwUpg== object-path@^0.11.4: version "0.11.4" @@ -10484,6 +10457,16 @@ object-visit@^1.0.0: dependencies: isobject "^3.0.0" +object.fromentries@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/object.fromentries/-/object.fromentries-2.0.0.tgz#49a543d92151f8277b3ac9600f1e930b189d30ab" + integrity sha512-9iLiI6H083uiqUuvzyY6qrlmc/Gz8hLQFOcb/Ri/0xXFkSNS3ctV+CbE6yM2+AnkYfOB3dGjdzC0wrMLIhQICA== + dependencies: + define-properties "^1.1.2" + es-abstract "^1.11.0" + function-bind "^1.1.1" + has "^1.0.1" + object.getownpropertydescriptors@^2.0.3: version "2.0.3" resolved "https://registry.yarnpkg.com/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.0.3.tgz#8758c846f5b407adab0f236e0986f14b051caa16" @@ -10531,9 +10514,9 @@ on-finished@~2.3.0: ee-first "1.1.1" on-headers@~1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/on-headers/-/on-headers-1.0.1.tgz#928f5d0f470d49342651ea6794b0857c100693f7" - integrity sha1-ko9dD0cNSTQmUepnlLCFfBAGk/c= + version "1.0.2" + resolved "https://registry.yarnpkg.com/on-headers/-/on-headers-1.0.2.tgz#772b0ae6aaa525c399e489adfad90c403eb3c28f" + integrity sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA== once@^1.3.0, once@^1.4.0: version "1.4.0" @@ -10584,9 +10567,9 @@ opn@^5.1.0: is-wsl "^1.1.0" optimism@^0.6.8: - version "0.6.8" - resolved "https://registry.yarnpkg.com/optimism/-/optimism-0.6.8.tgz#0780b546da8cd0a72e5207e0c3706c990c8673a6" - integrity sha512-bN5n1KCxSqwBDnmgDnzMtQTHdL+uea2HYFx1smvtE+w2AMl0Uy31g0aXnP/Nt85OINnMJPRpJyfRQLTCqn5Weg== + version "0.6.9" + resolved "https://registry.yarnpkg.com/optimism/-/optimism-0.6.9.tgz#19258ff8b3be0cea29ac35f06bff818e026e30bb" + integrity sha512-xoQm2lvXbCA9Kd7SCx6y713Y7sZ6fUc5R6VYpoL5M6svKJbTuvtNopexK8sO8K4s0EOUYHuPN2+yAEsNyRggkQ== dependencies: immutable-tuple "^0.4.9" @@ -10674,7 +10657,7 @@ os-shim@^0.1.2: resolved "https://registry.yarnpkg.com/os-shim/-/os-shim-0.1.3.tgz#6b62c3791cf7909ea35ed46e17658bb417cb3917" integrity sha1-a2LDeRz3kJ6jXtRuF2WLtBfLORc= -os-tmpdir@^1.0.0, os-tmpdir@^1.0.1, os-tmpdir@~1.0.1, os-tmpdir@~1.0.2: +os-tmpdir@^1.0.0, os-tmpdir@^1.0.1, os-tmpdir@~1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" integrity sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ= @@ -10739,6 +10722,13 @@ p-timeout@^1.1.1: dependencies: p-finally "^1.0.0" +p-timeout@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/p-timeout/-/p-timeout-2.0.1.tgz#d8dd1979595d2dc0139e1fe46b8b646cb3cdf038" + integrity sha512-88em58dDVB/KzPEx1X0N3LwFfYZPyDc4B6eF38M1rk9VTZMbxXXgjugz8mmwpS9Ox4BDZ+t6t3QP5+/gazweIA== + dependencies: + p-finally "^1.0.0" + p-try@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/p-try/-/p-try-1.0.0.tgz#cbc79cdbaf8fd4228e13f621f2b1a237c1b207b3" @@ -10755,9 +10745,9 @@ package-json@^4.0.0: semver "^5.1.0" pako@~1.0.5: - version "1.0.7" - resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.7.tgz#2473439021b57f1516c82f58be7275ad8ef1bb27" - integrity sha512-3HNK5tW4x8o5mO8RuHZp3Ydw9icZXx0RANAOMzlMzx7LVXhMJ4mo3MOBpzyd7r/+RUu8BmndP47LXT+vzjtWcQ== + version "1.0.10" + resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.10.tgz#4328badb5086a426aa90f541977d4955da5c9732" + integrity sha512-0DTvPVU3ed8+HNXOu5Bs+o//Mbdj9VNQMUOe9oKCwh8l0GNwpTDMKCWbRjgtD291AWnkAgkqA/LOnQS8AmS1tw== param-case@2.1.x: version "2.1.1" @@ -10774,15 +10764,16 @@ parents@^1.0.0, parents@^1.0.1: path-platform "~0.11.15" parse-asn1@^5.0.0: - version "5.1.1" - resolved "https://registry.yarnpkg.com/parse-asn1/-/parse-asn1-5.1.1.tgz#f6bf293818332bd0dab54efb16087724745e6ca8" - integrity sha512-KPx7flKXg775zZpnp9SxJlz00gTd4BmJ2yJufSc44gMCRrRQ7NSzAcSJQfifuOLgW6bEi+ftrALtsgALeB2Adw== + version "5.1.4" + resolved "https://registry.yarnpkg.com/parse-asn1/-/parse-asn1-5.1.4.tgz#37f6628f823fbdeb2273b4d540434a22f3ef1fcc" + integrity sha512-Qs5duJcuvNExRfFZ99HDD3z4mAi3r9Wl/FOjEOijlxwCZs7E7mW2vjTpgQ4J8LpTF8x5v+1Vn5UQFejmWT11aw== dependencies: asn1.js "^4.0.0" browserify-aes "^1.0.0" create-hash "^1.1.0" evp_bytestokey "^1.0.0" pbkdf2 "^3.0.3" + safe-buffer "^5.1.1" parse-glob@^3.0.4: version "3.0.4" @@ -11027,6 +11018,11 @@ pify@^3.0.0: resolved "https://registry.yarnpkg.com/pify/-/pify-3.0.0.tgz#e5a4acd2c101fdf3d9a4d07f0dbc4db49dd28176" integrity sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY= +pify@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/pify/-/pify-4.0.1.tgz#4b2cd25c50d598735c50292224fd8c6df41e3231" + integrity sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g== + pinkie-promise@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/pinkie-promise/-/pinkie-promise-2.0.1.tgz#2135d6dfa7a358c069ac9b178776288228450ffa" @@ -11040,23 +11036,23 @@ pinkie@^2.0.0: integrity sha1-clVrgM+g1IqXToDnckjoDtT3+HA= pixel-bg@^1.0.0: - version "1.0.8" - resolved "https://registry.yarnpkg.com/pixel-bg/-/pixel-bg-1.0.8.tgz#24292649c50566558fbf030890f6919e14366c58" - integrity sha1-JCkmScUFZlWPvwMIkPaRnhQ2bFg= + version "1.0.9" + resolved "https://registry.yarnpkg.com/pixel-bg/-/pixel-bg-1.0.9.tgz#f7b7c47fa0802d6e6d82e55525ffa758b52cfe20" + integrity sha512-Tf5lY9SCT6xquaxykKB8y+WLeRZs9Nz/RG8dr7W2a9BMR/JwbGc2tcrv+2+d8iCP6UipM5FY62wuXHhjdqtk4w== dependencies: pixel-class "^1.0.0" pixel-class@^1.0.0: - version "1.0.7" - resolved "https://registry.yarnpkg.com/pixel-class/-/pixel-class-1.0.7.tgz#904a858f1d4a0cc032d461a1e47e7bc0ec7def23" - integrity sha1-kEqFjx1KDMAy1GGh5H57wOx97yM= + version "1.0.8" + resolved "https://registry.yarnpkg.com/pixel-class/-/pixel-class-1.0.8.tgz#5ee243f41e1332887ebd94a61a120fd65e484d6e" + integrity sha512-tvT5sqSCn/UGLNl/0g6vtto4QzDF9BTEDR57m3cHhMdSgxQp15FYRm3nel4fwCWwjgp23s0sG+p7PMyOiyLIKg== dependencies: deffy "^2.2.1" pixel-white-bg@^1.0.0: - version "1.0.7" - resolved "https://registry.yarnpkg.com/pixel-white-bg/-/pixel-white-bg-1.0.7.tgz#0cfbc5b385e4cd8e5da33662f896bc9958399587" - integrity sha1-DPvFs4XkzY5dozZi+Ja8mVg5lYc= + version "1.0.8" + resolved "https://registry.yarnpkg.com/pixel-white-bg/-/pixel-white-bg-1.0.8.tgz#3538537ddf2b2e0194222020a9c6931a35f19166" + integrity sha512-CFuUeb/aDv8ocGgjxpmPNsorfVRQyCbPGZ6cuXqT7ylHp26MA3j1s92Oy5zVSj9HAc3Ax9iEX7k5CK5O1SKXtw== dependencies: pixel-bg "^1.0.0" @@ -11099,10 +11095,10 @@ pn@^1.1.0: resolved "https://registry.yarnpkg.com/pn/-/pn-1.1.0.tgz#e2f4cef0e219f463c179ab37463e4e1ecdccbafb" integrity sha512-2qHaIQr2VLRFoxe2nASzsV6ef4yOOH+Fi9FBOVH6cqeSgUnoyySPZkxzLuzd+RYOQTRpROA0ztTMqxROKSb/nA== -popper.js@^1.14.4: - version "1.14.6" - resolved "https://registry.yarnpkg.com/popper.js/-/popper.js-1.14.6.tgz#ab20dd4edf9288b8b3b6531c47c361107b60b4b0" - integrity sha512-AGwHGQBKumlk/MDfrSOf0JHhJCImdDMcGNoqKmKkU+68GFazv3CQ6q9r7Ja1sKDZmYWTckY/uLyEznheTDycnA== +popper.js@^1.14.4, popper.js@^1.14.7: + version "1.14.7" + resolved "https://registry.yarnpkg.com/popper.js/-/popper.js-1.14.7.tgz#e31ec06cfac6a97a53280c3e55e4e0c860e7738e" + integrity sha512-4q1hNvoUre/8srWsH7hnoSJ5xVmIL4qgz+s4qf2TnJIMyZFUFMGH+9vE7mXynAlHSZ/NdTmmow86muD0myUkVQ== portfinder@^1.0.13, portfinder@^1.0.9: version "1.0.20" @@ -11465,9 +11461,9 @@ preserve@^0.2.0: integrity sha1-gV7R9uvGWSb4ZbMQwHE7yzMVzks= prettier@^1.14.3: - version "1.15.3" - resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.15.3.tgz#1feaac5bdd181237b54dbe65d874e02a1472786a" - integrity sha512-gAU9AGAPMaKb3NNSUUuhhFAS7SCO4ALTN4nRIn6PJ075Qd28Yn2Ig2ahEJWdJwJmlEBTUfC7mMUSFy8MwsOCfg== + version "1.16.4" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.16.4.tgz#73e37e73e018ad2db9c76742e2647e21790c9717" + integrity sha512-ZzWuos7TI5CKUeQAtFd6Zhm2s6EpAD/ZLApIhsF9pRvRtM1RFo61dM/4MSRUA0SuLugA/zgrZD8m0BaY46Og7g== pretty-bytes@^4.0.2: version "4.0.2" @@ -11499,9 +11495,9 @@ prettyjson@^1.1.2: minimist "^1.2.0" prism-react-renderer@^0.1.5: - version "0.1.5" - resolved "https://registry.yarnpkg.com/prism-react-renderer/-/prism-react-renderer-0.1.5.tgz#1475ced5cfc7f47b7c1611e1cf649b32df2eb1f2" - integrity sha512-OoFeWfTYwXP9bdsJItDKK2hCnGZ78AGHism6up7wzBxBuO/0Hcn3nvA8K2LJOb5UxYjXqMOedNhHQuU7ypDdnw== + version "0.1.6" + resolved "https://registry.yarnpkg.com/prism-react-renderer/-/prism-react-renderer-0.1.6.tgz#c9216baa234fab1c234209fcdaf0cd23a01c50a9" + integrity sha512-uZJn5wrygCH0ZMue+2JRd0qJharrmpxa6/uK7deKgvCtJFFE+VsyvJ49LS8/ATt0mlAJS6vFQTDvhXBEXsda+A== prismjs@^1.15.0, prismjs@^1.6.0: version "1.15.0" @@ -11540,6 +11536,15 @@ progress@^2.0.0: resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8" integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA== +promise.prototype.finally@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/promise.prototype.finally/-/promise.prototype.finally-3.1.0.tgz#66f161b1643636e50e7cf201dc1b84a857f3864e" + integrity sha512-7p/K2f6dI+dM8yjRQEGrTQs5hTQixUAdOGpMEA3+pVxpX5oHKRSKAXyLw9Q9HUWDTdwtoo39dSHGQtN90HcEwQ== + dependencies: + define-properties "^1.1.2" + es-abstract "^1.9.0" + function-bind "^1.1.1" + promise@8.0.1: version "8.0.1" resolved "https://registry.yarnpkg.com/promise/-/promise-8.0.1.tgz#e45d68b00a17647b6da711bf85ed6ed47208f450" @@ -11579,7 +11584,15 @@ prop-ini@^0.0.2: dependencies: extend "^3.0.0" -prop-types@15.6.2, prop-types@^15.5.0, prop-types@^15.5.10, prop-types@^15.5.4, prop-types@^15.5.8, prop-types@^15.6.0, prop-types@^15.6.1, prop-types@^15.6.2: +prop-types-extra@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/prop-types-extra/-/prop-types-extra-1.1.0.tgz#32609910ea2dcf190366bacd3490d5a6412a605f" + integrity sha512-QFyuDxvMipmIVKD2TwxLVPzMnO4e5oOf1vr3tJIomL8E7d0lr6phTHd5nkPhFIzTD1idBLLEPeylL9g+rrTzRg== + dependencies: + react-is "^16.3.2" + warning "^3.0.0" + +prop-types@15.6.2: version "15.6.2" resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.6.2.tgz#05d5ca77b4453e985d60fc7ff8c859094a497102" integrity sha512-3pboPvLiWD7dkI3qf3KbUe6hKFKa52w+AE0VCqECtf+QHAKgOL37tTaNCnuX1nAAQ4ZhyP+kYVKf8rLmJ/feDQ== @@ -11587,6 +11600,15 @@ prop-types@15.6.2, prop-types@^15.5.0, prop-types@^15.5.10, prop-types@^15.5.4, loose-envify "^1.3.1" object-assign "^4.1.1" +prop-types@^15.5.0, prop-types@^15.5.10, prop-types@^15.5.4, prop-types@^15.5.8, prop-types@^15.6.0, prop-types@^15.6.1, prop-types@^15.6.2: + version "15.7.2" + resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5" + integrity sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ== + dependencies: + loose-envify "^1.4.0" + object-assign "^4.1.1" + react-is "^16.8.1" + protobufjs@^6.8.6: version "6.8.8" resolved "https://registry.yarnpkg.com/protobufjs/-/protobufjs-6.8.8.tgz#c8b4f1282fd7a90e6f5b109ed11c84af82908e7c" @@ -11673,7 +11695,7 @@ q@^1.1.2: resolved "https://registry.yarnpkg.com/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7" integrity sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc= -qs@6.5.2, qs@~6.5.1, qs@~6.5.2: +qs@6.5.2, qs@~6.5.2: version "6.5.2" resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36" integrity sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA== @@ -11688,7 +11710,7 @@ qs@~2.3.3: resolved "https://registry.yarnpkg.com/qs/-/qs-2.3.3.tgz#e9e85adbe75da0bbe4c8e0476a086290f863b404" integrity sha1-6eha2+ddoLvkyOBHaghikPhjtAQ= -query-string@5.1.1: +query-string@5, query-string@5.1.1: version "5.1.1" resolved "https://registry.yarnpkg.com/query-string/-/query-string-5.1.1.tgz#a78c012b71c17e05f2e3fa2319dd330682efb3cb" integrity sha512-gjWOsm2SoGlgLEdAGt7a6slVOk9mGiXmPFMqrEhLQ68rhQuBnpfs3+EmlvqKyxnCo9/PPlF+9MtY02S1aFg+Jw== @@ -11764,9 +11786,9 @@ randomatic@^3.0.0: math-random "^1.0.1" randombytes@^2.0.0, randombytes@^2.0.1, randombytes@^2.0.5: - version "2.0.6" - resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.0.6.tgz#d302c522948588848a8d300c932b44c24231da80" - integrity sha512-CIQ5OFxf4Jou6uOKe9t1AOgqpeU5fd70A8NPdHSGeYXqXsPe6peOwI0cUl88RWZ6sP1vPMV3avd/R6cZ5/sP1A== + version "2.1.0" + resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a" + integrity sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ== dependencies: safe-buffer "^5.1.0" @@ -11784,9 +11806,9 @@ range-parser@^1.0.3, range-parser@~1.2.0: integrity sha1-9JvmtIeJTdxA3MlKMi9hEJLgDV4= ratelimiter@^3.2.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/ratelimiter/-/ratelimiter-3.2.0.tgz#ae74cf9629daae4cc8900ec126ab28d3794070f1" - integrity sha512-zMc9X4FNmOk3RBxV95lvp13sZRtf43UJJN1FficbYiusBB09zB6gcJtK2X18dKmH+Gq0C7W6qNCHE+UJx0YwVg== + version "3.3.0" + resolved "https://registry.yarnpkg.com/ratelimiter/-/ratelimiter-3.3.0.tgz#bed9882f552e1aff4d7d3281bd1ab380f94cbf74" + integrity sha512-dDax7d0XosqzOrrQyMYEiu87tHDT6Wqm9LtGlxnQVAQJtEG6bmbvgPZ6E2/g1XrIpikJyec9hrOHKZ+DuVWxgQ== raven@^2.6.4: version "2.6.4" @@ -11825,15 +11847,15 @@ rc@^1.0.0, rc@^1.0.1, rc@^1.1.6, rc@^1.2.7: strip-json-comments "~2.0.1" react-apollo@^2.5.1: - version "2.5.1" - resolved "https://registry.yarnpkg.com/react-apollo/-/react-apollo-2.5.1.tgz#85934e1cf41ba88e2e96c921b44588f29d5daad6" - integrity sha512-obXPcmjJ5O75UkoizcsIbe0PXAXKRqm+SwNu059HQInB3FAab9PIn4wd/KglpPNiB9JXHe8OJDov0lFMcBbHEQ== + version "2.5.2" + resolved "https://registry.yarnpkg.com/react-apollo/-/react-apollo-2.5.2.tgz#6732c6af55e6adc9ebf97bf189e867a893c449d3" + integrity sha512-lmglhG6NQ+lfAUDzx8ZgelWKUbvxBhhy1l7Z2ksgtQ8+FVqwX7i6p5O3zicAZZlIdKzdq82V0kqq5WkxEsffrA== dependencies: apollo-utilities "^1.2.1" hoist-non-react-statics "^3.0.0" lodash.isequal "^4.5.0" prop-types "^15.6.0" - ts-invariant "^0.2.1" + ts-invariant "^0.3.0" tslib "^1.9.3" react-app-rewire-hot-loader@^1.0.3: @@ -11870,10 +11892,11 @@ react-click-outside@^3.0.1: hoist-non-react-statics "^2.1.1" react-clipboard.js@^2.0.1: - version "2.0.2" - resolved "https://registry.yarnpkg.com/react-clipboard.js/-/react-clipboard.js-2.0.2.tgz#bceb55e9cd00e2b937c36158f2a7b7038311bd24" - integrity sha512-F4VyDVgHetULOpIbn2DuXSM9vBN2gGuk8MQovTdfMaXanE3zOGmR07FNBycoYC4QmMt4QD3Ve29FqbrLm21RXA== + version "2.0.6" + resolved "https://registry.yarnpkg.com/react-clipboard.js/-/react-clipboard.js-2.0.6.tgz#2c304a6709832b1eaf69cc3ebbba03ae913539bf" + integrity sha512-Xls69sDUNrhA0ZQkVD8G439VpNnNlzoZrHRi4Xb7cKXHdFYFBiGSvHl+51gusekVpq0PsKM3IX+bpblKWJPQww== dependencies: + "@types/clipboard" "^2.0.1" clipboard "^2.0.0" prop-types "^15.5.0" @@ -11901,54 +11924,61 @@ react-dev-utils@^5.0.2: strip-ansi "3.0.1" text-table "0.2.0" -react-dom@^16.7.0-alpha.2: - version "16.7.0-alpha.2" - resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.7.0-alpha.2.tgz#16632880ed43676315991d8b412cce6975a30282" - integrity sha512-o0mMw8jBlwHjGZEy/vvKd/6giAX0+skREMOTs3/QHmgi+yAhUClp4My4Z9lsKy3SXV+03uPdm1l/QM7NTcGuMw== +"react-dom@npm:@hot-loader/react-dom": + version "16.8.4" + resolved "https://registry.yarnpkg.com/@hot-loader/react-dom/-/react-dom-16.8.4.tgz#aefe598ca26005c1ec3ac11d92d54ff3c1cd219c" + integrity sha512-S+5x4HJEMV7p2FzgUBEzrgiNJJeN4m5auTL3rZbmGsuyGt3Tgg8zgz6Mt1Udj8dKDxaryixDSNh0d/63TMqizg== dependencies: loose-envify "^1.1.0" object-assign "^4.1.1" prop-types "^15.6.2" - scheduler "^0.12.0-alpha.2" + scheduler "^0.13.4" react-dropzone@^8.0.3: - version "8.0.3" - resolved "https://registry.yarnpkg.com/react-dropzone/-/react-dropzone-8.0.3.tgz#1fe14a5ce58c390327273c6e766fb0429d51300e" - integrity sha512-3zvEH4szxTHjpNIbuSXY3aIByv6fUao8ZYOqtYucmgduRR+r0c94xMJvrPKyAj9hgWZTJ/0I//PMq+SDZp3MCw== + version "8.2.0" + resolved "https://registry.yarnpkg.com/react-dropzone/-/react-dropzone-8.2.0.tgz#e889455c4259c018bc5595ab0e2a5f7d958d173a" + integrity sha512-G25lAPDn5QSEUUCgAsUyKqaqa/pV2B39jAdRl5bjl89DyOpK7uFPnF1GDY5io7wUj0Bn8txj44qU8Xglp8CHcw== dependencies: attr-accept "^1.1.3" + file-selector "^0.1.8" prop-types "^15.6.2" + prop-types-extra "^1.1.0" react-error-overlay@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-4.0.1.tgz#417addb0814a90f3a7082eacba7cee588d00da89" integrity sha512-xXUbDAZkU08aAkjtUvldqbvI04ogv+a1XdHxvYuHPYKIVk/42BIOD0zSKTHAWV4+gDy3yGm283z2072rA2gdtw== +react-fast-compare@^2.0.2: + version "2.0.4" + resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-2.0.4.tgz#e84b4d455b0fec113e0402c329352715196f81f9" + integrity sha512-suNP+J1VU1MWFKcyt7RtjiSWUjvidmQSlqu+eHslq+342xCbGTYmC0mEhPCOHxlW0CywylOC1u2DFAT+bv4dBw== + react-flip-move@^3.0.2: version "3.0.3" resolved "https://registry.yarnpkg.com/react-flip-move/-/react-flip-move-3.0.3.tgz#3065b0b9e622ae73953aba725f8feed8e4643667" integrity sha512-gR2jvjUgIXI7ceFWJkr8owX4vKhV0IJoXIf/Dt7gESFe5OKiSz2H6d10mKTW8fN134NDI16J4HgEgq9pKqJd5A== -react-helmet-async@^0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/react-helmet-async/-/react-helmet-async-0.1.0.tgz#d5fa350b74cf8b690c401dfe74d55ffd9fdd9119" - integrity sha512-hnSOFBDRllV9O6K5u9JSsLbDn5ZpO08aFr08sftHTAbC+oqCq+/fWUtixPlgtVlHxVRbIk2G8qDL8DHR+0+ZaQ== +react-helmet-async@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/react-helmet-async/-/react-helmet-async-0.2.0.tgz#d20d8725c1dcdcc95d54e281a1040af47c3abffa" + integrity sha512-xo8oN+SUt0YkgQscKPTqhZZIOn5ni18FMv/H3KuBDt5+yAXTGktPEf3HU2EyufbHAF0TQ8qI+JrA3ILnjVfqNA== dependencies: - deep-equal "^1.0.1" invariant "^2.2.4" prop-types "^15.6.1" + react-fast-compare "^2.0.2" shallowequal "^1.0.2" -react-hot-loader@^4.6.0: - version "4.6.3" - resolved "https://registry.yarnpkg.com/react-hot-loader/-/react-hot-loader-4.6.3.tgz#d9c8923c45b35fd51538ba4297081a00be6bccb1" - integrity sha512-FUvRO8dwbeLnc3mgLn8ARuSh8NnLBYJyiRjFn+grY/5GupSyPqv0U7ixgwXro77hwDplhm8z9wU7FlJ8kZqiAQ== +react-hot-loader@^4.8.0: + version "4.8.0" + resolved "https://registry.yarnpkg.com/react-hot-loader/-/react-hot-loader-4.8.0.tgz#0b7c7dd9407415e23eb8246fdd28b0b839f54cb6" + integrity sha512-HY9F0vITYSVmXhAR6tPkMk240nxmoH8+0rca9iO2B82KVguiCiBJkieS0Wb4CeSIzLWecYx3iOcq8dcbnp0bxA== dependencies: fast-levenshtein "^2.0.6" global "^4.3.0" - hoist-non-react-statics "^2.5.0" + hoist-non-react-statics "^3.3.0" loader-utils "^1.1.0" - lodash.merge "^4.6.1" + lodash "^4.17.11" prop-types "^15.6.1" react-lifecycles-compat "^3.0.4" shallowequal "^1.0.2" @@ -11962,6 +11992,13 @@ react-image@^1.5.1: "@babel/runtime" "^7.0.0" prop-types "15.6.2" +react-infinite-scroller-fork-mxstbr@^1.2.7: + version "1.2.7" + resolved "https://registry.yarnpkg.com/react-infinite-scroller-fork-mxstbr/-/react-infinite-scroller-fork-mxstbr-1.2.7.tgz#8704bf036d94abeefe345bd1aeb2b7d2929d9dce" + integrity sha512-LX9pxQTD+9gVzahap0xSoqw3YM+LDWN1ygd1Sek13zZv3prq8R6BYm5R5vX18NYl0hmXImBHGXNtLBH3Va8NTg== + dependencies: + prop-types "^15.5.8" + react-infinite-scroller-with-scroll-element@2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/react-infinite-scroller-with-scroll-element/-/react-infinite-scroller-with-scroll-element-2.0.2.tgz#1a59ee022cd798260593c1322794ed809cd5e2a5" @@ -11969,10 +12006,17 @@ react-infinite-scroller-with-scroll-element@2.0.2: dependencies: prop-types "^15.5.8" -react-is@^16.3.1, react-is@^16.3.2, react-is@^16.6.0: - version "16.6.3" - resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.6.3.tgz#d2d7462fcfcbe6ec0da56ad69047e47e56e7eac0" - integrity sha512-u7FDWtthB4rWibG/+mFbVd5FvdI20yde86qKGx4lVUTWmPlSWQ4QxbBIrrs+HnXGbxOUlUzTAP/VDmvCwaP2yA== +react-infinite-scroller@^1.2.4: + version "1.2.4" + resolved "https://registry.yarnpkg.com/react-infinite-scroller/-/react-infinite-scroller-1.2.4.tgz#f67eaec4940a4ce6417bebdd6e3433bfc38826e9" + integrity sha512-/oOa0QhZjXPqaD6sictN2edFMsd3kkMiE19Vcz5JDgHpzEJVqYcmq+V3mkwO88087kvKGe1URNksHEOt839Ubw== + dependencies: + prop-types "^15.5.8" + +react-is@^16.3.2, react-is@^16.6.0, react-is@^16.7.0, react-is@^16.8.1: + version "16.8.4" + resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.8.4.tgz#90f336a68c3a29a096a3d648ab80e87ec61482a2" + integrity sha512-PVadd+WaUDOAciICm/J1waJaSvgq+4rHE/K70j0PFqKhkTBsPv/82UGQJNXAngz1fOQLLxI6z1sEDmJDQhCTAA== react-lifecycles-compat@^3.0.0, react-lifecycles-compat@^3.0.2, react-lifecycles-compat@^3.0.4: version "3.0.4" @@ -11987,9 +12031,9 @@ react-loadable@^5.5.0: prop-types "^15.5.0" react-mentions@^2.4.1: - version "2.4.1" - resolved "https://registry.yarnpkg.com/react-mentions/-/react-mentions-2.4.1.tgz#08f64ba9450370ab70cb9ca6b84dc60f43edb0c9" - integrity sha512-irw5V5IJ5NU+Lr1QVgQd8395gh3Lyrgz9/Uhyp6S2aDR4X4E2gz0Hf4saLvk+bBDJpNmV/j+oJsn28Me0rPIqg== + version "2.4.2" + resolved "https://registry.yarnpkg.com/react-mentions/-/react-mentions-2.4.2.tgz#d88eddb85643381352c6021e62ffe94c09066b98" + integrity sha512-il9PuG0lXRoMBjm1JDMEjRaRDmHBFd0O4fOANFZmX/8dIBJhBieO5ii+imNNvVb16/ft4MGxsQq8MgCdLobnTA== dependencies: lodash "^4.5.1" prop-types "^15.5.8" @@ -12005,10 +12049,10 @@ react-modal@^3.7.1: react-lifecycles-compat "^3.0.0" warning "^3.0.0" -react-popper@^1.0.2: - version "1.3.2" - resolved "https://registry.yarnpkg.com/react-popper/-/react-popper-1.3.2.tgz#e723a0a7fe1c42099a13e5d494e9d7d74b352af4" - integrity sha512-UbFWj55Yt9uqvy0oZ+vULDL2Bw1oxeZF9/JzGyxQ5ypgauRH/XlarA5+HLZWro/Zss6Ht2kqpegtb6sYL8GUGw== +react-popper@^1.3.3: + version "1.3.3" + resolved "https://registry.yarnpkg.com/react-popper/-/react-popper-1.3.3.tgz#2c6cef7515a991256b4f0536cd4bdcb58a7b6af6" + integrity sha512-ynMZBPkXONPc5K4P5yFWgZx5JGAUIP3pGGLNs58cfAPgK67olx7fmLp+AdpZ0+GoQ+ieFDa/z4cdV6u7sioH6w== dependencies: "@babel/runtime" "^7.1.2" create-react-context "<=0.2.2" @@ -12116,9 +12160,9 @@ react-textarea-autosize@^6.1.0: prop-types "^15.6.0" react-transition-group@^2.5.0: - version "2.5.1" - resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-2.5.1.tgz#67fbd8d30ebb1c57a149d554dbb82eabefa61f0d" - integrity sha512-8x/CxUL9SjYFmUdzsBPTgtKeCxt7QArjNSte0wwiLtF/Ix/o1nWNJooNy5o9XbHIKS31pz7J5VF2l41TwlvbHQ== + version "2.6.0" + resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-2.6.0.tgz#3c41cbdd9c044c5f8604d4e8d319e860919c9fae" + integrity sha512-VzZ+6k/adL3pJHo4PU/MHEPjW59/TGQtRsXC+wnxsx2mxjQKNHnDdJL/GpYuPJIsyHGjYbBQfIJ2JNOAdPc8GQ== dependencies: dom-helpers "^3.3.1" loose-envify "^1.4.0" @@ -12139,15 +12183,15 @@ react-visibility-sensor@^5.0.1: dependencies: prop-types "^15.6.2" -react@*: - version "16.6.3" - resolved "https://registry.yarnpkg.com/react/-/react-16.6.3.tgz#25d77c91911d6bbdd23db41e70fb094cc1e0871c" - integrity sha512-zCvmH2vbEolgKxtqXL2wmGCUxUyNheYn/C+PD1YAjfxHC54+MhdruyhO7QieQrYsYeTxrn93PM2y0jRH1zEExw== +react@*, react@^16.8.4: + version "16.8.4" + resolved "https://registry.yarnpkg.com/react/-/react-16.8.4.tgz#fdf7bd9ae53f03a9c4cd1a371432c206be1c4768" + integrity sha512-0GQ6gFXfUH7aZcjGVymlPOASTuSjlQL4ZtVC5YKH+3JL6bBLCVO21DknzmaPlI90LN253ojj02nsapy+j7wIjg== dependencies: loose-envify "^1.1.0" object-assign "^4.1.1" prop-types "^15.6.2" - scheduler "^0.11.2" + scheduler "^0.13.4" "react@^0.14.0 || ^15.0.0": version "15.6.2" @@ -12160,16 +12204,6 @@ react@*: object-assign "^4.1.0" prop-types "^15.5.10" -react@^16.7.0-alpha.2: - version "16.7.0-alpha.2" - resolved "https://registry.yarnpkg.com/react/-/react-16.7.0-alpha.2.tgz#924f2ae843a46ea82d104a8def7a599fbf2c78ce" - integrity sha512-Xh1CC8KkqIojhC+LFXd21jxlVtzoVYdGnQAi/I2+dxbmos9ghbx5TQf9/nDxc4WxaFfUQJkya0w1k6rMeyIaxQ== - dependencies: - loose-envify "^1.1.0" - object-assign "^4.1.1" - prop-types "^15.6.2" - scheduler "^0.12.0-alpha.2" - read-only-stream@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/read-only-stream/-/read-only-stream-2.0.0.tgz#2724fd6a8113d73764ac288d4386270c1dbf17f0" @@ -12235,16 +12269,6 @@ read@1.0.x: dependencies: mute-stream "~0.0.4" -readable-stream@1.0: - version "1.0.34" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.0.34.tgz#125820e34bc842d2f2aaafafe4c2916ee32c157c" - integrity sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw= - dependencies: - core-util-is "~1.0.0" - inherits "~2.0.1" - isarray "0.0.1" - string_decoder "~0.10.x" - readable-stream@1.1.x: version "1.1.14" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.1.14.tgz#7cf4c54ef648e3813084c636dd2079e166c081d9" @@ -12268,10 +12292,10 @@ readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.6, readable string_decoder "~1.1.1" util-deprecate "~1.0.1" -readable-stream@^3.0.6: - version "3.1.0" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.1.0.tgz#19c2e9c1ce43507c53f6eefbcf1ee3d4aaa786f5" - integrity sha512-vpydAvIJvPODZNagCPuHG87O9JNPtvFEtjHHRVwNVsVVRBqemvPJkc2SYbxJsiZXawJdtZNmkmnsPuE3IgsG0A== +readable-stream@^3.1.1: + version "3.2.0" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.2.0.tgz#de17f229864c120a9f56945756e4f32c4045245d" + integrity sha512-RV20kLjdmpZuTF1INEb9IA3L68Nmi+Ri7ppZqo78wj//Pn62fCoJyV9zalccNzDD/OuJpMG4f+pfMl8+L6QdGw== dependencies: inherits "^2.0.3" string_decoder "^1.1.1" @@ -12290,7 +12314,7 @@ readable-stream@~2.1.5: string_decoder "~0.10.x" util-deprecate "~1.0.1" -readdirp@^2.0.0: +readdirp@^2.0.0, readdirp@^2.2.1: version "2.2.1" resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-2.2.1.tgz#0e87622a3325aa33e892285caf8b4e846529a525" integrity sha512-1JU/8q+VgFZyxwrJ+SVIOsh+KywWGpds3NTqikiKpDMZWScmAYyKIgqkO+ARvNWJfXeXR1zxz7aHF4u4CyH6vQ== @@ -12305,9 +12329,9 @@ readme-badger@^0.1.2: integrity sha1-gbE435cjxzPfaifHvZyuvTg+CKU= realpath-native@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/realpath-native/-/realpath-native-1.0.2.tgz#cd51ce089b513b45cf9b1516c82989b51ccc6560" - integrity sha512-+S3zTvVt9yTntFrBpm7TQmQ3tzpCrnA1a/y+3cUHAc9ZR6aIjG0WNLR+Rj79QpJktY+VeW/TQtFlQ1bzsehI8g== + version "1.1.0" + resolved "https://registry.yarnpkg.com/realpath-native/-/realpath-native-1.1.0.tgz#2003294fea23fb0672f2476ebe22fcf498a2d65c" + integrity sha512-wlgPA6cCIIg9gKz0fgAPjnzh4yR/LnXovwuo9hvyGvx3h8nX4+/iLZplfUWasXpqD8BdnGnP5njOFjkUwPzvjA== dependencies: util.promisify "^1.0.0" @@ -12356,7 +12380,7 @@ redis-errors@^1.0.0, redis-errors@^1.2.0: resolved "https://registry.yarnpkg.com/redis-errors/-/redis-errors-1.2.0.tgz#eb62d2adb15e4eaf4610c04afe1529384250abad" integrity sha1-62LSrbFeTq9GEMBK/hUpOEJQq60= -redis-parser@^2.4.0: +redis-parser@^2.4.0, redis-parser@^2.6.0: version "2.6.0" resolved "https://registry.yarnpkg.com/redis-parser/-/redis-parser-2.6.0.tgz#52ed09dacac108f1a631c07e9b69941e7a19504b" integrity sha1-Uu0J2srBCPGmMcB+m2mUHnoZUEs= @@ -12375,6 +12399,15 @@ redis-tag-cache@^1.2.1: dependencies: ioredis "^4.0.0" +redis@^2.8.0: + version "2.8.0" + resolved "https://registry.yarnpkg.com/redis/-/redis-2.8.0.tgz#202288e3f58c49f6079d97af7a10e1303ae14b02" + integrity sha512-M1OkonEQwtRmZv4tEWF2VgpG0JWJ8Fv1PhlgT5+B+uNq2cA3Rt1Yt/ryoR+vQNOQcIEgdCdfH0jr3bDpihAw1A== + dependencies: + double-ended-queue "^2.1.0-0" + redis-commands "^1.2.0" + redis-parser "^2.6.0" + redraft@0.10.2: version "0.10.2" resolved "https://registry.yarnpkg.com/redraft/-/redraft-0.10.2.tgz#e8cbc477bb65cebe2231257199262cbd555db19e" @@ -12399,11 +12432,11 @@ reduce-function-call@^1.0.1: balanced-match "^0.4.2" reduce@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/reduce/-/reduce-1.0.1.tgz#14fa2e5ff1fc560703a020cbb5fbaab691565804" - integrity sha1-FPouX/H8VgcDoCDLtfuqtpFWWAQ= + version "1.0.2" + resolved "https://registry.yarnpkg.com/reduce/-/reduce-1.0.2.tgz#0cd680ad3ffe0b060e57a5c68bdfce37168d361b" + integrity sha512-xX7Fxke/oHO5IfZSk77lvPa/7bjMh9BuCk4OOoX5XTXrM7s0Z+MkPfSDfz0q7r91BhhGSs8gii/VEN/7zhCPpQ== dependencies: - object-keys "~1.0.0" + object-keys "^1.1.0" redux-thunk@^2.3.0: version "2.3.0" @@ -12425,10 +12458,10 @@ referrer-policy@1.1.0: resolved "https://registry.yarnpkg.com/referrer-policy/-/referrer-policy-1.1.0.tgz#35774eb735bf50fb6c078e83334b472350207d79" integrity sha1-NXdOtzW/UPtsB46DM0tHI1AgfXk= -regenerate-unicode-properties@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-7.0.0.tgz#107405afcc4a190ec5ed450ecaa00ed0cafa7a4c" - integrity sha512-s5NGghCE4itSlUS+0WUj88G6cfMVMmH8boTPNvABf8od+2dhT9WDlWu8n01raQAJZMOK8Ch6jSexaRO7swd6aw== +regenerate-unicode-properties@^8.0.2: + version "8.0.2" + resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-8.0.2.tgz#7b38faa296252376d363558cfbda90c9ce709662" + integrity sha512-SbA/iNrBUf6Pv2zU8Ekv1Qbhv92yxL4hiDa2siuxs4KKn4oOoMDHXjAf7+Nz9qinUQ46B1LcWEi/PhJfPWpZWQ== dependencies: regenerate "^1.4.0" @@ -12461,10 +12494,10 @@ regenerator-transform@^0.10.0: babel-types "^6.19.0" private "^0.1.6" -regenerator-transform@^0.13.3: - version "0.13.3" - resolved "https://registry.yarnpkg.com/regenerator-transform/-/regenerator-transform-0.13.3.tgz#264bd9ff38a8ce24b06e0636496b2c856b57bcbb" - integrity sha512-5ipTrZFSq5vU2YoGoww4uaRVAK4wyYC4TSICibbfEPOruUu8FFP7ErV0BjmbIOEpn3O/k9na9UEdYR/3m7N6uA== +regenerator-transform@^0.13.4: + version "0.13.4" + resolved "https://registry.yarnpkg.com/regenerator-transform/-/regenerator-transform-0.13.4.tgz#18f6763cf1382c69c36df76c6ce122cc694284fb" + integrity sha512-T0QMBjK3J0MtxjPmdIMXm72Wvj2Abb0Bd4HADdfijwMdoIsyQZ6fWC7kDFhk2YinBBEMZDL7Y7wh0J1sGx3S4A== dependencies: private "^0.1.6" @@ -12507,16 +12540,16 @@ regexpu-core@^2.0.0: regjsparser "^0.1.4" regexpu-core@^4.1.3, regexpu-core@^4.2.0: - version "4.4.0" - resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-4.4.0.tgz#8d43e0d1266883969720345e70c275ee0aec0d32" - integrity sha512-eDDWElbwwI3K0Lo6CqbQbA6FwgtCz4kYTarrri1okfkRLZAqstU+B3voZBCjg8Fl6iq0gXrJG6MvRgLthfvgOA== + version "4.5.4" + resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-4.5.4.tgz#080d9d02289aa87fe1667a4f5136bc98a6aebaae" + integrity sha512-BtizvGtFQKGPUcTy56o3nk1bGRp4SZOTYrDtGNlqCQufptV5IkkLN6Emw+yunAJjzf+C9FQFtvq7IoA3+oMYHQ== dependencies: regenerate "^1.4.0" - regenerate-unicode-properties "^7.0.0" + regenerate-unicode-properties "^8.0.2" regjsgen "^0.5.0" regjsparser "^0.6.0" unicode-match-property-ecmascript "^1.0.4" - unicode-match-property-value-ecmascript "^1.0.2" + unicode-match-property-value-ecmascript "^1.1.0" registry-auth-token@^3.0.1: version "3.3.2" @@ -12568,13 +12601,13 @@ remove-trailing-separator@^1.0.1: integrity sha1-wkvOKig62tW8P1jg1IJJuSN52O8= renderkid@^2.0.1: - version "2.0.2" - resolved "https://registry.yarnpkg.com/renderkid/-/renderkid-2.0.2.tgz#12d310f255360c07ad8fde253f6c9e9de372d2aa" - integrity sha512-FsygIxevi1jSiPY9h7vZmBFUbAOcbYm9UwyiLNdVsLRs/5We9Ob5NMPbGYUTWiLq5L+ezlVdE0A8bbME5CWTpg== + version "2.0.3" + resolved "https://registry.yarnpkg.com/renderkid/-/renderkid-2.0.3.tgz#380179c2ff5ae1365c522bf2fcfcff01c5b74149" + integrity sha512-z8CLQp7EZBPCwCnncgf9C4XAi3WR0dv+uWu/PjIyhhAb5d6IJ/QZqlHFprHeKT+59//V6BNUsLbvN8+2LarxGA== dependencies: css-select "^1.1.0" - dom-converter "~0.2" - htmlparser2 "~3.3.0" + dom-converter "^0.2" + htmlparser2 "^3.3.0" strip-ansi "^3.0.0" utila "^0.4.0" @@ -12602,56 +12635,31 @@ request-ip@^2.1.3: dependencies: is_js "^0.9.0" -request-progress@0.3.1: - version "0.3.1" - resolved "https://registry.yarnpkg.com/request-progress/-/request-progress-0.3.1.tgz#0721c105d8a96ac6b2ce8b2c89ae2d5ecfcf6b3a" - integrity sha1-ByHBBdipasayzossia4tXs/Pazo= +request-progress@0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/request-progress/-/request-progress-0.4.0.tgz#c1954e39086aa85269c5660bcee0142a6a70d7e7" + integrity sha1-wZVOOQhqqFJpxWYLzuAUKmpw1+c= dependencies: - throttleit "~0.0.2" + node-eta "^0.1.1" + throttleit "^0.0.2" -request-promise-core@1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/request-promise-core/-/request-promise-core-1.1.1.tgz#3eee00b2c5aa83239cfb04c5700da36f81cd08b6" - integrity sha1-Pu4AssWqgyOc+wTFcA2jb4HNCLY= +request-promise-core@1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/request-promise-core/-/request-promise-core-1.1.2.tgz#339f6aababcafdb31c799ff158700336301d3346" + integrity sha512-UHYyq1MO8GsefGEt7EprS8UrXsm1TxEvFUX1IMTuSLU2Rh7fTIdFtl8xD7JiEYiWU2dl+NYAjCTksTehQUxPag== dependencies: - lodash "^4.13.1" + lodash "^4.17.11" request-promise-native@^1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/request-promise-native/-/request-promise-native-1.0.5.tgz#5281770f68e0c9719e5163fd3fab482215f4fda5" - integrity sha1-UoF3D2jgyXGeUWP9P6tIIhX0/aU= - dependencies: - request-promise-core "1.1.1" - stealthy-require "^1.1.0" - tough-cookie ">=2.3.3" - -request@2.87.0: - version "2.87.0" - resolved "https://registry.yarnpkg.com/request/-/request-2.87.0.tgz#32f00235cd08d482b4d0d68db93a829c0ed5756e" - integrity sha512-fcogkm7Az5bsS6Sl0sibkbhcKsnyon/jV1kF3ajGmF0c8HrttdKTPRT9hieOaQHA5HEq6r8OyWOo/o781C1tNw== + version "1.0.7" + resolved "https://registry.yarnpkg.com/request-promise-native/-/request-promise-native-1.0.7.tgz#a49868a624bdea5069f1251d0a836e0d89aa2c59" + integrity sha512-rIMnbBdgNViL37nZ1b3L/VfPOpSi0TqVDQPAvO6U14lMzOLrt5nilxCQqtDKhZeDiW0/hkCXGoQjhgJd/tCh6w== dependencies: - aws-sign2 "~0.7.0" - aws4 "^1.6.0" - caseless "~0.12.0" - combined-stream "~1.0.5" - extend "~3.0.1" - forever-agent "~0.6.1" - form-data "~2.3.1" - har-validator "~5.0.3" - http-signature "~1.2.0" - is-typedarray "~1.0.0" - isstream "~0.1.2" - json-stringify-safe "~5.0.1" - mime-types "~2.1.17" - oauth-sign "~0.8.2" - performance-now "^2.1.0" - qs "~6.5.1" - safe-buffer "^5.1.1" - tough-cookie "~2.3.3" - tunnel-agent "^0.6.0" - uuid "^3.1.0" + request-promise-core "1.1.2" + stealthy-require "^1.1.1" + tough-cookie "^2.3.3" -request@^2.78.0, request@^2.81.0, request@^2.87.0, request@^2.88.0: +request@2.88.0, request@^2.78.0, request@^2.81.0, request@^2.87.0, request@^2.88.0: version "2.88.0" resolved "https://registry.yarnpkg.com/request/-/request-2.88.0.tgz#9c2fca4f7d35b592efe57c7f0a55e81052124fef" integrity sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg== @@ -12752,10 +12760,10 @@ resolve@1.6.0: dependencies: path-parse "^1.0.5" -resolve@^1.1.4, resolve@^1.3.2, resolve@^1.3.3, resolve@^1.4.0, resolve@^1.5.0, resolve@^1.6.0, resolve@^1.8.1: - version "1.9.0" - resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.9.0.tgz#a14c6fdfa8f92a7df1d996cb7105fa744658ea06" - integrity sha512-TZNye00tI67lwYvzxCxHGjwTNlUV70io54/Ed4j6PscB8xVfuBJpRenI/o6dVk0cY0PYTY27AgCoGGxRnYuItQ== +resolve@^1.1.4, resolve@^1.10.0, resolve@^1.3.2, resolve@^1.3.3, resolve@^1.4.0, resolve@^1.5.0, resolve@^1.8.1, resolve@^1.9.0: + version "1.10.0" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.10.0.tgz#3bdaaeaf45cc07f375656dfd2e54ed0810b101ba" + integrity sha512-3sUr9aq5OfSg2S9pNtPA9hL1FVEAjvfOC4leW0SNf/mpnaakz2a9femSd6LqAww2RaFctwyf1lCqnTHuF1rxDg== dependencies: path-parse "^1.0.6" @@ -12851,11 +12859,11 @@ right-align@^0.1.1: align-text "^0.1.1" rimraf@2, rimraf@2.x.x, rimraf@^2.2.8, rimraf@^2.5.4, rimraf@^2.6.1, rimraf@^2.6.2, rimraf@~2.6.2: - version "2.6.2" - resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.2.tgz#2ed8150d24a16ea8651e6d6ef0f47c4158ce7a36" - integrity sha512-lreewLK/BlghmxtfH36YYVg1i8IAce4TI7oao75I1g245+6BctqTVQiBP3YUJ9C6DQOXJmkYR9X9fCLtCOJc5w== + version "2.6.3" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.3.tgz#b2d104fe0d8fb27cf9e0a1cda8262dd3833c6cab" + integrity sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA== dependencies: - glob "^7.0.5" + glob "^7.1.3" ripemd160@^2.0.0, ripemd160@^2.0.1: version "2.0.2" @@ -12956,18 +12964,10 @@ sax@>=0.6.0, sax@^1.2.4, sax@~1.2.1: resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9" integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw== -scheduler@^0.11.2: - version "0.11.3" - resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.11.3.tgz#b5769b90cf8b1464f3f3cfcafe8e3cd7555a2d6b" - integrity sha512-i9X9VRRVZDd3xZw10NY5Z2cVMbdYg6gqFecfj79USv1CFN+YrJ3gIPRKf1qlY+Sxly4djoKdfx1T+m9dnRB8kQ== - dependencies: - loose-envify "^1.1.0" - object-assign "^4.1.1" - -scheduler@^0.12.0-alpha.2: - version "0.12.0-alpha.3" - resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.12.0-alpha.3.tgz#59afcaba1cb79e3e8bee91de94eb8f42c9152c2b" - integrity sha512-KADuBlOWSrT/DCt/oA+NgsNamRCsfz7wj+leaeGjGHipNClsqhjOPogKkJgem6WLAv/QzxW8bE7zlGc9OxiYSQ== +scheduler@^0.13.4: + version "0.13.4" + resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.13.4.tgz#8fef05e7a3580c76c0364d2df5e550e4c9140298" + integrity sha512-cvSOlRPxOHs5dAhP9yiS/6IDmVAVxmk33f0CtTJRkmUWcb1Us+t7b1wqdzoC0REw2muC9V5f1L/w5R5uKGaepA== dependencies: loose-envify "^1.1.0" object-assign "^4.1.1" @@ -13043,9 +13043,9 @@ send@0.16.2: statuses "~1.4.0" serialize-javascript@^1.5.0: - version "1.5.0" - resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-1.5.0.tgz#1aa336162c88a890ddad5384baebc93a655161fe" - integrity sha512-Ga8c8NjAAp46Br4+0oZ2WxJCwIzwP60Gq1YPgU+39PiTVxyed/iKE/zyZI6+UlVYH5Q4PaQdHhcegIFPZTUfoQ== + version "1.6.1" + resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-1.6.1.tgz#4d1f697ec49429a847ca6f442a2a755126c4d879" + integrity sha512-A5MOagrPFga4YaKQSWHryl7AXvbQkEqpw4NNYMTNYUNV51bA8ABHgYFpqKx+YFFrw59xMV1qGH1R4AgoNIVgCw== serve-index@^1.7.2: version "1.9.1" @@ -13117,7 +13117,12 @@ setprototypeof@1.1.0: resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.0.tgz#d0bd85536887b6fe7c0d818cb962d9d91c54e656" integrity sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ== -sha.js@^2.4.0, sha.js@^2.4.8, sha.js@~2.4.4: +setprototypeof@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.1.tgz#7e95acb24aa92f5885e0abef5ba131330d4ae683" + integrity sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw== + +sha.js@^2.4.0, sha.js@^2.4.11, sha.js@^2.4.8, sha.js@~2.4.4: version "2.4.11" resolved "https://registry.yarnpkg.com/sha.js/-/sha.js-2.4.11.tgz#37a5cf0b81ecbc6943de109ba2960d1b26584ae7" integrity sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ== @@ -13196,6 +13201,11 @@ slash@^1.0.0: resolved "https://registry.yarnpkg.com/slash/-/slash-1.0.0.tgz#c41f2f6c39fc16d1cd17ad4b5d896114ae470d55" integrity sha1-xB8vbDn8FtHNF61LXYlhFK5HDVU= +slash@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/slash/-/slash-2.0.0.tgz#de552851a1759df3a8f206535442f5ec4ddeab44" + integrity sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A== + slate-markdown@0.1.1: version "0.1.1" resolved "https://registry.yarnpkg.com/slate-markdown/-/slate-markdown-0.1.1.tgz#dc4d8bce0c133661678248108c28ff2637a8ce78" @@ -13329,9 +13339,9 @@ source-map-support@^0.4.15: source-map "^0.5.6" source-map-support@^0.5.0: - version "0.5.9" - resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.9.tgz#41bc953b2534267ea2d605bccfa7bfa3111ced5f" - integrity sha512-gR6Rw4MvUlYy83vP0vxoVNzM6t8MUXqNuRsuBmBHQDu1Fh6X015FrLdgoDKcNdkwGubozq0P4N0Q37UyFVr1EA== + version "0.5.11" + resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.11.tgz#efac2ce0800355d026326a0ca23e162aeac9a4e2" + integrity sha512-//sajEx/fGL3iw6fltKMdPvy8kL3kJ2O3iuYlRoT3k9Kb4BjOoZ+BZzaNHeuaruSt+Kf3Zk9tnfAQg9/AJqUVQ== dependencies: buffer-from "^1.0.0" source-map "^0.6.0" @@ -13386,9 +13396,9 @@ spdx-expression-parse@^3.0.0: spdx-license-ids "^3.0.0" spdx-license-ids@^3.0.0: - version "3.0.2" - resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.2.tgz#a59efc09784c2a5bada13cfeaf5c75dd214044d2" - integrity sha512-qky9CVt0lVIECkEsYbNILVnPvycuEBkXoMFLRWsREkomQLevYhtRKC+R91a5TOAQ3bCMjikRwhyaRqj1VYatYg== + version "3.0.3" + resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.3.tgz#81c0ce8f21474756148bbb5f3bfc0f36bf15d76e" + integrity sha512-uBIcIl3Ih6Phe3XHK1NqboJLdGfwr1UN3k6wSD1dZpmPsIkb8AGNbZYJ1fOBk834+Gxy8rpfDxrS6XLEMZMY2g== spdy-transport@^2.0.18: version "2.1.1" @@ -13440,9 +13450,9 @@ sprintf-js@~1.0.2: integrity sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw= sshpk@^1.7.0: - version "1.15.2" - resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.15.2.tgz#c946d6bd9b1a39d0e8635763f5242d6ed6dcb629" - integrity sha512-Ra/OXQtuh0/enyl4ETZAfTaeksa6BXks5ZcjpSUNrjBr0DvrJKX+1fsKDPpT9TBXgHAFsa4510aNVgI8g/+SzA== + version "1.16.1" + resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.16.1.tgz#fb661c0bef29b39db40769ee39fa70093d6f6877" + integrity sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg== dependencies: asn1 "~0.2.3" assert-plus "^1.0.0" @@ -13475,9 +13485,9 @@ staged-git-files@0.0.4: integrity sha1-15fhtVHKemOd7AI33G60u5vhfTU= standard-as-callback@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/standard-as-callback/-/standard-as-callback-1.0.1.tgz#2e9e1e9d278d7d77580253faaec42269015e3c1d" - integrity sha512-izxEITSyc7S+5oOiF/URiYaNkemPUxIndCNv66jJ548Y1TVxhBvioNMSPrZIQdaZDlhnguOdUzHA/7hJ3xFhuQ== + version "1.0.2" + resolved "https://registry.yarnpkg.com/standard-as-callback/-/standard-as-callback-1.0.2.tgz#d0813289db00f8bd5e0f29e74744cb63706707c8" + integrity sha512-1Qrah+2Vmj8DiftcXR9gfUe/gFmOukdnxF5v7G/apCZbLtjh3rjss8Eu6Qlprm6zerrl+qDmvm7KXpJedqpoAQ== static-extend@^0.1.1: version "0.1.2" @@ -13497,20 +13507,20 @@ statuses@~1.4.0: resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.4.0.tgz#bb73d446da2796106efcc1b601a253d6c46bd087" integrity sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew== -stealthy-require@^1.1.0: +stealthy-require@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/stealthy-require/-/stealthy-require-1.1.1.tgz#35b09875b4ff49f26a777e509b3090a3226bf24b" integrity sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks= stopword@^0.1.13: - version "0.1.13" - resolved "https://registry.yarnpkg.com/stopword/-/stopword-0.1.13.tgz#92b26491c443b1a8d9709142ad64ae1af10cea55" - integrity sha512-WteqDiQpRyn2yBAm1pwbkXn+SMa8L+WDesd33ovgs0JoheFcrrazMSet4P9LoTRteuUoqVxtEIwEKVZ/z/WHKg== + version "0.1.15" + resolved "https://registry.yarnpkg.com/stopword/-/stopword-0.1.15.tgz#e5a9ac5a3936eb2c54deb2615847f49619f41fcd" + integrity sha512-UoattsZj2D6D00p5K1yKss/rIULftvUp8yrNKm76suiBjzCC6ER0V+IBJvMO3CJwUpaooy3GPgUeHU4CHn/9mA== stream-browserify@^2.0.0, stream-browserify@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/stream-browserify/-/stream-browserify-2.0.1.tgz#66266ee5f9bdb9940a4e4514cafb43bb71e5c9db" - integrity sha1-ZiZu5fm9uZQKTkUUyvtDu3Hlyds= + version "2.0.2" + resolved "https://registry.yarnpkg.com/stream-browserify/-/stream-browserify-2.0.2.tgz#87521d38a44aa7ee91ce1cd2a47df0cb49dd660b" + integrity sha512-nX6hmklHs/gr2FuxYDltq8fJA1GDlxKQCz8O/IM4atRqBH8OORmBNgfvW5gG10GT/qQ9u0CzIvr2X5Pkt6ntqg== dependencies: inherits "~2.0.1" readable-stream "^2.0.2" @@ -13702,20 +13712,22 @@ styled-components@^2.0.0: stylis "^3.4.0" supports-color "^3.2.3" -styled-components@^3.4.10: - version "3.4.10" - resolved "https://registry.yarnpkg.com/styled-components/-/styled-components-3.4.10.tgz#9a654c50ea2b516c36ade57ddcfa296bf85c96e1" - integrity sha512-TA8ip8LoILgmSAFd3r326pKtXytUUGu5YWuqZcOQVwVVwB6XqUMn4MHW2IuYJ/HAD81jLrdQed8YWfLSG1LX4Q== +styled-components@^4.1.3: + version "4.1.3" + resolved "https://registry.yarnpkg.com/styled-components/-/styled-components-4.1.3.tgz#4472447208e618b57e84deaaeb6acd34a5e0fe9b" + integrity sha512-0quV4KnSfvq5iMtT0RzpMGl/Dg3XIxIxOl9eJpiqiq4SrAmR1l1DLzNpMzoy3DyzdXVDMJS2HzROnXscWA3SEw== dependencies: - buffer "^5.0.3" - css-to-react-native "^2.0.3" - fbjs "^0.8.16" - hoist-non-react-statics "^2.5.0" + "@babel/helper-module-imports" "^7.0.0" + "@emotion/is-prop-valid" "^0.7.3" + "@emotion/unitless" "^0.7.0" + babel-plugin-styled-components ">= 1" + css-to-react-native "^2.2.2" + memoize-one "^4.0.0" prop-types "^15.5.4" - react-is "^16.3.1" + react-is "^16.6.0" stylis "^3.5.0" stylis-rule-sheet "^0.0.10" - supports-color "^3.2.3" + supports-color "^5.5.0" stylis-rule-sheet@^0.0.10: version "0.0.10" @@ -13735,9 +13747,9 @@ subarg@^1.0.0: minimist "^1.1.0" subscriptions-transport-ws@^0.9.11, subscriptions-transport-ws@^0.9.15: - version "0.9.15" - resolved "https://registry.yarnpkg.com/subscriptions-transport-ws/-/subscriptions-transport-ws-0.9.15.tgz#68a8b7ba0037d8c489fb2f5a102d1494db297d0d" - integrity sha512-f9eBfWdHsePQV67QIX+VRhf++dn1adyC/PZHP6XI5AfKnZ4n0FW+v5omxwdHVpd4xq2ZijaHEcmlQrhBY79ZWQ== + version "0.9.16" + resolved "https://registry.yarnpkg.com/subscriptions-transport-ws/-/subscriptions-transport-ws-0.9.16.tgz#90a422f0771d9c32069294c08608af2d47f596ec" + integrity sha512-pQdoU7nC+EpStXnCfh/+ho0zE0Z+ma+i7xvj7bkXKb1dvYHSZxgRPaU6spRP+Bjzow67c/rRDoix5RT0uU9omw== dependencies: backo2 "^1.0.2" eventemitter3 "^3.1.0" @@ -13776,12 +13788,12 @@ superagent@^3.3.1: qs "^6.5.1" readable-stream "^2.3.5" -supports-color@5.1.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.1.0.tgz#058a021d1b619f7ddf3980d712ea3590ce7de3d5" - integrity sha512-Ry0AwkoKjDpVKK4sV4h6o3UJmNRbjYm2uXhwfj3J56lMVdvnUNqzQVRztOOMGQ++w1K/TjNDFvpJk0F/LoeBCQ== +supports-color@5.5.0, supports-color@^5.1.0, supports-color@^5.2.0, supports-color@^5.3.0, supports-color@^5.4.0, supports-color@^5.5.0: + version "5.5.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" + integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== dependencies: - has-flag "^2.0.0" + has-flag "^3.0.0" supports-color@^2.0.0: version "2.0.0" @@ -13802,13 +13814,6 @@ supports-color@^4.2.1: dependencies: has-flag "^2.0.0" -supports-color@^5.1.0, supports-color@^5.2.0, supports-color@^5.3.0, supports-color@^5.4.0: - version "5.5.0" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" - integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== - dependencies: - has-flag "^3.0.0" - svgo@^0.7.0: version "0.7.2" resolved "https://registry.yarnpkg.com/svgo/-/svgo-0.7.2.tgz#9f5772413952135c6fefbf40afe6a4faa88b4bb5" @@ -13886,10 +13891,10 @@ syntax-error@^1.1.1: dependencies: acorn-node "^1.2.0" -synthetic-dom@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/synthetic-dom/-/synthetic-dom-1.2.0.tgz#f3589aafe2b5e299f337bb32973a9be42dd5625e" - integrity sha1-81iar+K14pnzN7sylzqb5C3VYl4= +synthetic-dom@^1.3.3: + version "1.3.3" + resolved "https://registry.yarnpkg.com/synthetic-dom/-/synthetic-dom-1.3.3.tgz#a01865fef513c76c6936f968f90140eb6959a034" + integrity sha512-ILjWWiiHIAYphm+F3w0V+A4a79HHF3ELZc6v7H3/kVzCvoxqHWTySN10M7vUOMpZFeRkwz/LmXsW10eFu79bdQ== table@4.0.2: version "4.0.2" @@ -13985,7 +13990,7 @@ throat@^4.0.0: resolved "https://registry.yarnpkg.com/throat/-/throat-4.1.0.tgz#89037cbc92c56ab18926e6ba4cbb200e15672a6a" integrity sha1-iQN8vJLFarGJJua6TLsgDhVnKmo= -throttleit@~0.0.2: +throttleit@^0.0.2: version "0.0.2" resolved "https://registry.yarnpkg.com/throttleit/-/throttleit-0.0.2.tgz#cfedf88e60c00dd9697b61fdd2a8343a9b680eaf" integrity sha1-z+34jmDADdlpe2H90qg0OptoDq8= @@ -14038,23 +14043,23 @@ timespan@~2.3.0: integrity sha1-SQLOBAvRPYRcj1myfp1ZutbzmSk= tiny-emitter@^2.0.0: - version "2.0.2" - resolved "https://registry.yarnpkg.com/tiny-emitter/-/tiny-emitter-2.0.2.tgz#82d27468aca5ade8e5fd1e6d22b57dd43ebdfb7c" - integrity sha512-2NM0auVBGft5tee/OxP4PI3d8WItkDM+fPnaRAVo6xTDI2knbz9eC5ArWGqtGlYqiH3RU5yMpdyTTO7MguC4ow== + version "2.1.0" + resolved "https://registry.yarnpkg.com/tiny-emitter/-/tiny-emitter-2.1.0.tgz#1d1a56edfc51c43e863cbb5382a72330e3555423" + integrity sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q== + +tippy.js@^4.0.3: + version "4.1.0" + resolved "https://registry.yarnpkg.com/tippy.js/-/tippy.js-4.1.0.tgz#aea4f7d983d6714e0521975798e216eac8dbfa51" + integrity sha512-dNtBNbv40SXw9oUUOfSRUev9Ljj/xRcxr6LlwnF96SGH7SouEIMYIx4q8nO2ZfJBN+WTd9P2EBmS6vMxMr43Bw== + dependencies: + popper.js "^1.14.7" tlds@^1.189.0: version "1.203.1" resolved "https://registry.yarnpkg.com/tlds/-/tlds-1.203.1.tgz#4dc9b02f53de3315bc98b80665e13de3edfc1dfc" integrity sha512-7MUlYyGJ6rSitEZ3r1Q1QNV8uSIzapS8SmmhSusBuIc7uIxPPwsKllEP0GRp1NS6Ik6F+fRZvnjDWm3ecv2hDw== -tmp@0.0.31: - version "0.0.31" - resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.31.tgz#8f38ab9438e17315e5dbd8b3657e8bfb277ae4a7" - integrity sha1-jzirlDjhcxXl29izZX6L+yd65Kc= - dependencies: - os-tmpdir "~1.0.1" - -tmp@^0.0.33: +tmp@0.0.33, tmp@^0.0.33: version "0.0.33" resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9" integrity sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw== @@ -14142,7 +14147,7 @@ touch@^3.1.0: dependencies: nopt "~1.0.10" -tough-cookie@>=2.3.3, tough-cookie@^2.3.4: +tough-cookie@^2.3.3, tough-cookie@^2.3.4: version "2.5.0" resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.5.0.tgz#cd9fb2a0aa1d5a12b473bd9fb96fa3dcff65ade2" integrity sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g== @@ -14150,13 +14155,6 @@ tough-cookie@>=2.3.3, tough-cookie@^2.3.4: psl "^1.1.28" punycode "^2.1.1" -tough-cookie@~2.3.3: - version "2.3.4" - resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.3.4.tgz#ec60cee38ac675063ffc97a5c18970578ee83655" - integrity sha512-TZ6TTfI5NtZnuyy/Kecv+CnoROnyXn2DN97LontgQpCwsX2XyLYCC0ENhYkehSOwAp8rTQKc/NUIF7BkQ5rKLA== - dependencies: - punycode "^1.4.1" - tough-cookie@~2.4.3: version "2.4.3" resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.4.3.tgz#53f36da3f47783b0925afa06ff9f3b165280f781" @@ -14211,7 +14209,14 @@ ts-invariant@^0.2.1: dependencies: tslib "^1.9.3" -tslib@^1.9.3: +ts-invariant@^0.3.0, ts-invariant@^0.3.2: + version "0.3.2" + resolved "https://registry.yarnpkg.com/ts-invariant/-/ts-invariant-0.3.2.tgz#89a2ffeb70879b777258df1df1c59383c35209b0" + integrity sha512-QsY8BCaRnHiB5T6iE4DPlJMAKEG3gzMiUco9FEt1jUXQf0XP6zi0idT0i0rMTu8A326JqNSDsmlkA9dRSh1TRg== + dependencies: + tslib "^1.9.3" + +tslib@^1.9.0, tslib@^1.9.3: version "1.9.3" resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.9.3.tgz#d7e4dd79245d85428c4d7e4822a79917954ca286" integrity sha512-4krF8scpejhaOgqzBEcGM7yDIEfi0/8+8zDRZhNZZ2kjmHJ4hv3zCbQWxoJGz1iw5U0Jl0nma13xzHXcncMavQ== @@ -14269,9 +14274,9 @@ typedarray@^0.0.6: integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c= typpy@^2.0.0, typpy@^2.3.1, typpy@^2.3.4: - version "2.3.10" - resolved "https://registry.yarnpkg.com/typpy/-/typpy-2.3.10.tgz#63a39e4171cbbb4cdefb590009228a3de9a22b2f" - integrity sha512-DKiSmYeXF4q+K0H999sVROLjwsngad5AloblLo72No+xVT9W09ytUIOCC/3puHsf+Dsf8M2hoPds0H1HwJgQqg== + version "2.3.11" + resolved "https://registry.yarnpkg.com/typpy/-/typpy-2.3.11.tgz#21a0d22c96fb646306e08b6c669ad43608e1b3b9" + integrity sha512-Jh/fykZSaxeKO0ceMAs6agki9T5TNA9kiIR6fzKbvafKpIw8UlNlHhzuqKyi5lfJJ5VojJOx9tooIbyy7vHV/g== dependencies: function.name "^1.0.3" @@ -14280,14 +14285,10 @@ ua-parser-js@^0.7.9: resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.19.tgz#94151be4c0a7fb1d001af7022fdaca4642659e4b" integrity sha512-T3PVJ6uz8i0HzPxOF9SWzWAlfN/DavlpQqepn22xgve/5QecC+XMCAtmUNnY7C9StehaV6exjUCI801lOI7QlQ== -"ua-parser-js@github:amplitude/ua-parser-js#ed538f1": - version "0.7.10" - resolved "https://codeload.github.com/amplitude/ua-parser-js/tar.gz/ed538f16f5c6ecd8357da989b617d4f156dcf35d" - uc.micro@^1.0.1: - version "1.0.5" - resolved "https://registry.yarnpkg.com/uc.micro/-/uc.micro-1.0.5.tgz#0c65f15f815aa08b560a61ce8b4db7ffc3f45376" - integrity sha512-JoLI4g5zv5qNyT09f4YAvEZIIV1oOjqnewYg5D38dkQljIzpPT296dbIGvKro3digYI1bkb7W6EP1y4uDlmzLg== + version "1.0.6" + resolved "https://registry.yarnpkg.com/uc.micro/-/uc.micro-1.0.6.tgz#9c411a802a409a91fc6cf74081baba34b24499ac" + integrity sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA== uglify-es@^3.3.9: version "3.3.9" @@ -14342,9 +14343,9 @@ uid2@0.0.x: integrity sha1-SDEm4Rd03y9xuLY53NeZw3YWK4I= ul@^5.2.1: - version "5.2.13" - resolved "https://registry.yarnpkg.com/ul/-/ul-5.2.13.tgz#9ff0504ea35ca1f74c0bf59e6480def009bad7b5" - integrity sha1-n/BQTqNcofdMC/WeZIDe8Am617U= + version "5.2.14" + resolved "https://registry.yarnpkg.com/ul/-/ul-5.2.14.tgz#560abd28d0f9762010b0e7a84a56e7208166f61a" + integrity sha512-VaIRQZ5nkEd8VtI3OYo5qNbhHQuBtPtu5k5GrYaKCmcP1H+FkuWtS+XFTSU1oz5GiuAg2FJL5ka8ufr9zdm8eg== dependencies: deffy "^2.2.2" typpy "^2.3.4" @@ -14355,11 +14356,12 @@ umd@^3.0.0: integrity sha512-4IcGSufhFshvLNcMCV80UnQVlZ5pMOC8mvNPForqwA4+lzYQuetTESLDQkeLmihq8bRcnpbQa48Wb8Lh16/xow== undeclared-identifiers@^1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/undeclared-identifiers/-/undeclared-identifiers-1.1.2.tgz#7d850a98887cff4bd0bf64999c014d08ed6d1acc" - integrity sha512-13EaeocO4edF/3JKime9rD7oB6QI8llAGhgn5fKOPyfkJbRb6NFv9pYV6dFEmpa4uRjKeBqLZP8GpuzqHlKDMQ== + version "1.1.3" + resolved "https://registry.yarnpkg.com/undeclared-identifiers/-/undeclared-identifiers-1.1.3.tgz#9254c1d37bdac0ac2b52de4b6722792d2a91e30f" + integrity sha512-pJOW4nxjlmfwKApE4zvxLScM/njmwj/DiUBv7EabwE4O8kRUy+HIwxQtZLBPll/jx1LJyBcqNfB3/cpv9EZwOw== dependencies: acorn-node "^1.3.0" + dash-ast "^1.0.0" get-assigned-identifiers "^1.2.0" simple-concat "^1.0.0" xtend "^4.0.1" @@ -14384,15 +14386,15 @@ unicode-match-property-ecmascript@^1.0.4: unicode-canonical-property-names-ecmascript "^1.0.4" unicode-property-aliases-ecmascript "^1.0.4" -unicode-match-property-value-ecmascript@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-1.0.2.tgz#9f1dc76926d6ccf452310564fd834ace059663d4" - integrity sha512-Rx7yODZC1L/T8XKo/2kNzVAQaRE88AaMvI1EF/Xnj3GW2wzN6fop9DDWuFAKUVFH7vozkz26DzP0qyWLKLIVPQ== +unicode-match-property-value-ecmascript@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-1.1.0.tgz#5b4b426e08d13a80365e0d657ac7a6c1ec46a277" + integrity sha512-hDTHvaBk3RmFzvSl0UVrUmC3PuW9wKVnpoUDYH0JDkSIovzw+J5viQmeYHxVSBptubnr7PbH2e0fnpDRQnQl5g== unicode-property-aliases-ecmascript@^1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-1.0.4.tgz#5a533f31b4317ea76f17d807fa0d116546111dd0" - integrity sha512-2WSLa6OdYd2ng8oqiGIWnJqyFArvhn+5vgx5GTxMbUYjCYKUcuKS62YLFF0R/BDGlB1yzXjQOLtPAfHsgirEpg== + version "1.0.5" + resolved "https://registry.yarnpkg.com/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-1.0.5.tgz#a9cc6cc7ce63a0a3023fc99e341b94431d405a57" + integrity sha512-L5RAqCfXqAwR3RriF8pM0lU0w4Ryf/GgzONwi6KnL1taJQa7x1TCxdJnILX59WIGOwR57IVxn7Nej0fz1Ny6fw== union-class-names@^1.0.0: version "1.0.0" @@ -14434,9 +14436,9 @@ unique-string@^1.0.0: crypto-random-string "^1.0.0" universal-user-agent@^2.0.0: - version "2.0.2" - resolved "https://registry.yarnpkg.com/universal-user-agent/-/universal-user-agent-2.0.2.tgz#b0322da546100c658adcf4965110a56ed238aee6" - integrity sha512-nOwvHWLH3dBazyuzbECPA5uVFNd7AlgviXRHgR4yf48QqitIvpdncRrxMbZNMpPPEfgz30I9ubd1XmiJiqsTrg== + version "2.0.3" + resolved "https://registry.yarnpkg.com/universal-user-agent/-/universal-user-agent-2.0.3.tgz#9f6f09f9cc33de867bb720d84c08069b14937c6c" + integrity sha512-eRHEHhChCBHrZsA4WEhdgiOKgdvgrMIHwnwnqD0r5C6AO8kwKcG7qSku3iXdhvHL3YvsS9ZkSGN8h/hIpoFC8g== dependencies: os-name "^3.0.0" @@ -14478,10 +14480,10 @@ unzipper@^0.8.11: readable-stream "~2.1.5" setimmediate "~1.0.4" -upath@^1.0.5: - version "1.1.0" - resolved "https://registry.yarnpkg.com/upath/-/upath-1.1.0.tgz#35256597e46a581db4793d0ce47fa9aebfc9fabd" - integrity sha512-bzpH/oBhoS/QI/YtbkqCg6VEiPYjSZtrHQM6/QnJS6OL9pKUFLqb3aFh4Scvwm45+7iAgiMkLhSbaZxUqmrprw== +upath@^1.1.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/upath/-/upath-1.1.2.tgz#3db658600edaeeccbe6db5e684d67ee8c2acd068" + integrity sha512-kXpym8nmDmlCBr7nKdIx8P2jNBa+pBpIUFRnKJ4dr8htyYGJFokkr2ZvERRtUN+9SY+JqXouNgUPtv6JQva/2Q== update-notifier@^2.3.0, update-notifier@^2.5.0: version "2.5.0" @@ -14616,7 +14618,14 @@ util@0.10.3: dependencies: inherits "2.0.1" -util@^0.10.3, util@~0.10.1: +util@^0.11.0: + version "0.11.1" + resolved "https://registry.yarnpkg.com/util/-/util-0.11.1.tgz#3236733720ec64bb27f6e26f421aaa2e1b588d61" + integrity sha512-HShAsny+zS2TZfaXxD9tYj4HQGlBezXZMZuM/S5PKLLoZkShZiGk9o5CzukI1LVHZvjdvZ2Sj1aW/Ndn2NB/HQ== + dependencies: + inherits "2.0.3" + +util@~0.10.1: version "0.10.4" resolved "https://registry.yarnpkg.com/util/-/util-0.10.4.tgz#3aa0125bfe668a4672de58857d3ace27ecb76901" integrity sha512-0Pm9hTQ3se5ll1XihRic3FDIku70C+iHUdT/W926rSgHV5QgXsYbKZN8MSC3tJtSkhuROzvsQjAaFENRXr+19A== @@ -14751,9 +14760,9 @@ warning@^3.0.0: loose-envify "^1.0.0" warning@^4.0.1, warning@^4.0.2: - version "4.0.2" - resolved "https://registry.yarnpkg.com/warning/-/warning-4.0.2.tgz#aa6876480872116fa3e11d434b0d0d8d91e44607" - integrity sha512-wbTp09q/9C+jJn4KKJfJfoS6VleK/Dti0yqWSm6KMvJ4MRCXFQNapHuJXutJIrWV0Cf4AhTdeIe4qdKHR1+Hug== + version "4.0.3" + resolved "https://registry.yarnpkg.com/warning/-/warning-4.0.3.tgz#16e9e077eb8a86d6af7d64aa1e05fd85b4678ca3" + integrity sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w== dependencies: loose-envify "^1.0.0" @@ -14890,9 +14899,9 @@ webpack-manifest-plugin@1.3.2: lodash ">=3.5 <5" webpack-merge@^4.0.0: - version "4.1.5" - resolved "https://registry.yarnpkg.com/webpack-merge/-/webpack-merge-4.1.5.tgz#2be31e846c20767d1bef56bdca64c328a681190a" - integrity sha512-sVcM+MMJv6DO0C0GLLltx8mUlGMKXE0zBsuMqZ9jz2X9gsekALw6Rs0cAfTWc97VuWS6NpVUa78959zANnMMLQ== + version "4.2.1" + resolved "https://registry.yarnpkg.com/webpack-merge/-/webpack-merge-4.2.1.tgz#5e923cf802ea2ace4fd5af1d3247368a633489b4" + integrity sha512-4p8WQyS98bUJcCvFMbdGZyZmsKuWjWVnVHnAS3FFg0HDaRVrPbkivx2RYCre8UiemD67RsiFFLfn4JhLAin8Vw== dependencies: lodash "^4.17.5" @@ -15132,9 +15141,9 @@ wrappy@1: integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= write-file-atomic@^2.0.0, write-file-atomic@^2.1.0, write-file-atomic@^2.3.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-2.3.0.tgz#1ff61575c2e2a4e8e510d6fa4e243cce183999ab" - integrity sha512-xuPeK4OdjWqtfi59ylvVL0Yn35SF3zgcAcv7rBPFHVaEapaDr4GdGgm3j7ckTwH9wHL7fGmgfAnb0+THrHb8tA== + version "2.4.2" + resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-2.4.2.tgz#a7181706dfba17855d221140a9c06e15fcdd87b9" + integrity sha512-s0b6vB3xIVRLWywa6X9TOMA7k9zio0TMOsl9ZnDkliA/cfJlpHXAscj0gbHVJiTdIuAYpIyqS5GW91fqm6gG5g== dependencies: graceful-fs "^4.1.11" imurmurhash "^0.1.4" @@ -15176,9 +15185,9 @@ ws@^5.2.0: async-limiter "~1.0.0" ws@^6.0.0: - version "6.1.2" - resolved "https://registry.yarnpkg.com/ws/-/ws-6.1.2.tgz#3cc7462e98792f0ac679424148903ded3b9c3ad8" - integrity sha512-rfUqzvz0WxmSXtJpPMX2EeASXabOrSMk1ruMOV3JBTBjo4ac2lDjGGsbQSyxj8Odhw5fBib8ZKEjDNvgouNKYw== + version "6.2.0" + resolved "https://registry.yarnpkg.com/ws/-/ws-6.2.0.tgz#13806d9913b2a5f3cbb9ba47b563c002cbc7c526" + integrity sha512-deZYUNlt2O4buFCa3t5bKLf8A7FPP/TVjwOeVNpw818Ma5nk4MLXls2eoEGS39o8119QIYxTrTDoPQ5B/gTD6w== dependencies: async-limiter "~1.0.0" @@ -15398,6 +15407,14 @@ yarn-install@^0.2.1: chalk "^1.1.3" cross-spawn "^4.0.2" +yauzl@2.10.0: + version "2.10.0" + resolved "https://registry.yarnpkg.com/yauzl/-/yauzl-2.10.0.tgz#c7eb17c93e112cb1086fa6d8e51fb0667b79a5f9" + integrity sha1-x+sXyT4RLLEIb6bY5R+wZnt5pfk= + dependencies: + buffer-crc32 "~0.2.3" + fd-slicer "~1.1.0" + yauzl@2.4.1: version "2.4.1" resolved "https://registry.yarnpkg.com/yauzl/-/yauzl-2.4.1.tgz#9528f442dab1b2284e58b4379bb194e22e0c4005" @@ -15405,22 +15422,15 @@ yauzl@2.4.1: dependencies: fd-slicer "~1.0.1" -yauzl@2.8.0: - version "2.8.0" - resolved "https://registry.yarnpkg.com/yauzl/-/yauzl-2.8.0.tgz#79450aff22b2a9c5a41ef54e02db907ccfbf9ee2" - integrity sha1-eUUK/yKyqcWkHvVOAtuQfM+/nuI= - dependencies: - buffer-crc32 "~0.2.3" - fd-slicer "~1.0.1" - -zen-observable-ts@^0.8.13: - version "0.8.13" - resolved "https://registry.yarnpkg.com/zen-observable-ts/-/zen-observable-ts-0.8.13.tgz#ae1fd77c84ef95510188b1f8bca579d7a5448fc2" - integrity sha512-WDb8SM0tHCb6c0l1k60qXWlm1ok3zN9U4VkLdnBKQwIYwUoB9psH7LIFgR+JVCCMmBxUgOjskIid8/N02k/2Bg== +zen-observable-ts@^0.8.16: + version "0.8.16" + resolved "https://registry.yarnpkg.com/zen-observable-ts/-/zen-observable-ts-0.8.16.tgz#969367299074fe17422fe2f46ee417e9a30cf3fa" + integrity sha512-pQl75N7qwgybKVsh6WFO+WwPRijeQ52Gn1vSf4uvPFXald9CbVQXLa5QrOPEJhdZiC+CD4quqOVqSG+Ptz5XLA== dependencies: + tslib "^1.9.3" zen-observable "^0.8.0" zen-observable@^0.8.0: - version "0.8.11" - resolved "https://registry.yarnpkg.com/zen-observable/-/zen-observable-0.8.11.tgz#d3415885eeeb42ee5abb9821c95bb518fcd6d199" - integrity sha512-N3xXQVr4L61rZvGMpWe8XoCGX8vhU35dPyQ4fm5CY/KDlG0F75un14hjbckPXTDuKUY6V0dqR2giT6xN8Y4GEQ== + version "0.8.13" + resolved "https://registry.yarnpkg.com/zen-observable/-/zen-observable-0.8.13.tgz#a9f1b9dbdfd2d60a08761ceac6a861427d44ae2e" + integrity sha512-fa+6aDUVvavYsefZw0zaZ/v3ckEtMgCFi30sn91SEZea4y/6jQp05E3omjkX91zV6RVdn15fqnFZ6RKjRGbp2g==