Showing preview only (3,723K chars total). Download the full file or copy to clipboard to get everything.
Repository: withspectrum/spectrum
Branch: alpha
Commit: e8ebdcbba829
Files: 1114
Total size: 3.4 MB
Directory structure:
gitextract_m8f95147/
├── .babelrc
├── .circleci/
│ └── config.yml
├── .dockerignore
├── .eslintignore
├── .eslintrc.js
├── .flowconfig
├── .github/
│ ├── ISSUE_TEMPLATE.md
│ └── PULL_REQUEST_TEMPLATE.md
├── .gitignore
├── .npmignore
├── .prettierignore
├── .prettierrc
├── LICENSE
├── README.md
├── api/
│ ├── apollo-server.js
│ ├── authentication.js
│ ├── index.js
│ ├── loaders/
│ │ ├── channel.js
│ │ ├── community.js
│ │ ├── create-loader.js
│ │ ├── directMessageThread.js
│ │ ├── index.js
│ │ ├── message.js
│ │ ├── reaction.js
│ │ ├── thread.js
│ │ ├── threadReaction.js
│ │ ├── types.js
│ │ └── user.js
│ ├── migrations/
│ │ ├── 20170410074258-initial-data.js
│ │ ├── 20170613200350-notifications.js
│ │ ├── 20170616113103-compound-indexes-for-ordering.js
│ │ ├── 20170627104435-user-email-settings.js
│ │ ├── 20170701173337-linkify-messages.js
│ │ ├── 20170702194221-fix-images.js
│ │ ├── 20170706114239-providerfield-indexes.js
│ │ ├── 20170706205658-slack-import.js
│ │ ├── 20170714171920-web-push-subscription.js
│ │ ├── 20170724184557-notifications-entity-added-index.js
│ │ ├── 20170803104302-dedupe-users-settings.js
│ │ ├── 20170825220615-clean-recurring-payments.js
│ │ ├── 20170829233734-userid-index-on-invoices.js
│ │ ├── 20170831163211-invoice-data-model-update.js
│ │ ├── 20170907222544-digest-email-notification-settings.js
│ │ ├── 20170908230623-add-reputation-field-to-communities.js
│ │ ├── 20170912000619-backfill-rep.js
│ │ ├── 20170915201609-clean-up-bad-dm-data.js
│ │ ├── 20170926003025-activate-daily-weekly-digest-settings.js
│ │ ├── 20170926102527-speedy-gonzales.js
│ │ ├── 20170927002438-communityid-index-on-reputation-events.js
│ │ ├── 20170928143435-slate-to-draftjs.js.js
│ │ ├── 20171005075445-remove-markdown-links-from-messages.js
│ │ ├── 20171008101118-last-slate-to-draft.js
│ │ ├── 20171013195530-core-metrics-table.js
│ │ ├── 20171018235659-add-direst-message-user-settings.js
│ │ ├── 20171029090619-users-channels-index.js
│ │ ├── 20171029094352-users-threads-index.js
│ │ ├── 20171103014955-add-mention-notification-settings.js
│ │ ├── 20171129215512-index-communities-by-slug.js
│ │ ├── 20171129221050-curated-content-table-creation.js
│ │ ├── 20171208175038-index-users-for-search.js
│ │ ├── 20171208180800-index-communities-for-search.js
│ │ ├── 20171213002813-add-modified-at-field-to-users-and-communities.js
│ │ ├── 20180209015734-github-provider-id-index.js
│ │ ├── 20180214111357-expo-push-subscriptions.js
│ │ ├── 20180309144845-create-community-settings-table.js
│ │ ├── 20180316195507-create-channel-settings-table.js
│ │ ├── 20180320122000-create-stripe-tables.js
│ │ ├── 20180320173414-set-administrator-info-on-community.js
│ │ ├── 20180411183454-lowercase-all-the-slugs.js
│ │ ├── 20180428001543-reset-slack-import-records.js
│ │ ├── 20180504003702-encrypt-existing-slack-data.js
│ │ ├── 20180517180716-enable-private-communities.js
│ │ ├── 20180517215503-add-ispending-to-userscommunities.js
│ │ ├── 20180518135040-add-join-settings-to-community-settings.js
│ │ ├── 20180621001409-thread-likes-table.js
│ │ ├── 20180823115847-add-users-communities-indexes.js
│ │ ├── 20181001061156-thread-metadata-denormalization.js
│ │ ├── 20181001064151-fix-thread-metadata-message-counts.js
│ │ ├── 20181002060237-remove-payments.js
│ │ ├── 20181003233411-thread-reactions-useridandthreadid-index.js
│ │ ├── 20181004222636-denormalize-channel-community-member-counts.js
│ │ ├── 20181005143053-users-notifications-useridandnotificationid-index.js
│ │ ├── 20181005144259-users-notifications-userIdAndIsSeen-index.js
│ │ ├── 20181023160027-update-denormalized-member-counts.js
│ │ ├── 20181024173616-indexes-for-digests.js
│ │ ├── 20181027050052-remove-attachments-from-thread-model.js
│ │ ├── 20181102025454-fix-old-image-urls-in-messages.js
│ │ ├── 20181102040518-fix-old-image-urls-in-threads.js
│ │ ├── 20181102044407-fix-old-image-urls-in-communities.js
│ │ ├── 20181102045821-fix-old-image-urls-in-users.js
│ │ ├── 20181102054523-fix-aws-static-url-community-photos.js
│ │ ├── 20181116173949-add-terms-last-accepted-field-to-users.js
│ │ ├── 20181121054300-resync-community-member-counts.js
│ │ ├── 20181122162921-users-communities-useridandmember-index.js
│ │ ├── 20181126094455-users-channels-roles.js
│ │ ├── 20181127090014-communities-member-count-index.js
│ │ ├── 20181205171559-remove-old-users-notifications.js
│ │ ├── 20181211181146-add-usersthreads-user-id-and-participant-index.js
│ │ ├── 20190226085909-bot-user-sam.js
│ │ ├── 20190306125252-threads-watercooler-index.js
│ │ ├── 20190315142923-backfill-userscommunities-last-seen-community-last-active.js
│ │ ├── 20190327134509-delete-bot-messages.js
│ │ ├── config.js
│ │ └── seed/
│ │ ├── default/
│ │ │ ├── channelSettings.js
│ │ │ ├── channels.js
│ │ │ ├── communities.js
│ │ │ ├── communitySettings.js
│ │ │ ├── constants.js
│ │ │ ├── directMessageThreads.js
│ │ │ ├── index.js
│ │ │ ├── messages.js
│ │ │ ├── notifications.js
│ │ │ ├── reactions.js
│ │ │ ├── threads.js
│ │ │ ├── users.js
│ │ │ ├── usersChannels.js
│ │ │ ├── usersCommunities.js
│ │ │ ├── usersDirectMessageThreads.js
│ │ │ ├── usersNotifications.js
│ │ │ ├── usersSettings.js
│ │ │ └── usersThreads.js
│ │ ├── generate.js
│ │ └── index.js
│ ├── models/
│ │ ├── channel.js
│ │ ├── channelSettings.js
│ │ ├── community.js
│ │ ├── communitySettings.js
│ │ ├── curatedContent.js
│ │ ├── directMessageThread.js
│ │ ├── message.js
│ │ ├── reaction.js
│ │ ├── search.js
│ │ ├── session.js
│ │ ├── test/
│ │ │ ├── __snapshots__/
│ │ │ │ └── channel.test.js.snap
│ │ │ └── channel.test.js
│ │ ├── thread.js
│ │ ├── threadReaction.js
│ │ ├── usersChannels.js
│ │ ├── usersCommunities.js
│ │ ├── usersDirectMessageThreads.js
│ │ ├── usersSettings.js
│ │ ├── usersThreads.js
│ │ └── utils.js
│ ├── mutations/
│ │ ├── channel/
│ │ │ ├── deleteChannel.js
│ │ │ ├── editChannel.js
│ │ │ └── index.js
│ │ ├── community/
│ │ │ ├── deleteCommunity.js
│ │ │ ├── editCommunity.js
│ │ │ ├── index.js
│ │ │ ├── toggleCommunityNoindex.js
│ │ │ └── toggleCommunityRedirect.js
│ │ ├── files/
│ │ │ ├── index.js
│ │ │ └── uploadImage.js
│ │ ├── message/
│ │ │ ├── deleteMessage.js
│ │ │ └── index.js
│ │ ├── thread/
│ │ │ ├── deleteThread.js
│ │ │ └── index.js
│ │ └── user/
│ │ ├── banUser.js
│ │ ├── deleteCurrentUser.js
│ │ ├── editUser.js
│ │ └── index.js
│ ├── package.json
│ ├── queries/
│ │ ├── channel/
│ │ │ ├── channelPermissions.js
│ │ │ ├── community.js
│ │ │ ├── communityPermissions.js
│ │ │ ├── index.js
│ │ │ ├── isArchived.js
│ │ │ ├── joinSettings.js
│ │ │ ├── memberConnection.js
│ │ │ ├── memberCount.js
│ │ │ ├── metaData.js
│ │ │ ├── moderators.js
│ │ │ ├── owners.js
│ │ │ ├── rootChannel.js
│ │ │ └── threadConnection.js
│ │ ├── community/
│ │ │ ├── brandedLogin.js
│ │ │ ├── channelConnection.js
│ │ │ ├── communityPermissions.js
│ │ │ ├── contextPermissions.js
│ │ │ ├── coverPhoto.js
│ │ │ ├── index.js
│ │ │ ├── joinSettings.js
│ │ │ ├── memberConnection.js
│ │ │ ├── members.js
│ │ │ ├── metaData.js
│ │ │ ├── pinnedThread.js
│ │ │ ├── profilePhoto.js
│ │ │ ├── rootCommunities.js
│ │ │ ├── rootCommunity.js
│ │ │ ├── rootRecentCommunities.js
│ │ │ ├── rootTopCommunities.js
│ │ │ ├── slackSettings.js
│ │ │ ├── threadConnection.js
│ │ │ └── watercooler.js
│ │ ├── communityMember/
│ │ │ ├── index.js
│ │ │ ├── roles.js
│ │ │ ├── rootCommunityMember.js
│ │ │ └── user.js
│ │ ├── directMessageThread/
│ │ │ ├── index.js
│ │ │ ├── messageConnection.js
│ │ │ ├── participants.js
│ │ │ ├── rootDirectMessageThread.js
│ │ │ ├── rootDirectMessageThreadByUserIds.js
│ │ │ └── snippet.js
│ │ ├── message/
│ │ │ ├── author.js
│ │ │ ├── content.js
│ │ │ ├── index.js
│ │ │ ├── parent.js
│ │ │ ├── reactions.js
│ │ │ ├── rootGetMediaMessagesForThread.js
│ │ │ ├── rootMessage.js
│ │ │ ├── sender.js
│ │ │ └── thread.js
│ │ ├── reaction/
│ │ │ ├── index.js
│ │ │ ├── message.js
│ │ │ ├── reaction.js
│ │ │ └── user.js
│ │ ├── thread/
│ │ │ ├── attachments.js
│ │ │ ├── author.js
│ │ │ ├── channel.js
│ │ │ ├── community.js
│ │ │ ├── content.js
│ │ │ ├── creator.js
│ │ │ ├── editedBy.js
│ │ │ ├── index.js
│ │ │ ├── isAuthor.js
│ │ │ ├── isCreator.js
│ │ │ ├── messageConnection.js
│ │ │ ├── metaImage.js
│ │ │ ├── participants.js
│ │ │ ├── reactions.js
│ │ │ └── rootThread.js
│ │ └── user/
│ │ ├── channelConnection.js
│ │ ├── communityConnection.js
│ │ ├── contextPermissions.js
│ │ ├── coverPhoto.js
│ │ ├── directMessageThreadsConnection.js
│ │ ├── email.js
│ │ ├── everything.js
│ │ ├── githubProfile.js
│ │ ├── index.js
│ │ ├── isAdmin.js
│ │ ├── profilePhoto.js
│ │ ├── rootCurrentUser.js
│ │ ├── rootUser.js
│ │ ├── settings.js
│ │ ├── threadConnection.js
│ │ └── threadCount.js
│ ├── routes/
│ │ ├── api/
│ │ │ ├── export-user-data.js
│ │ │ └── index.js
│ │ ├── auth/
│ │ │ ├── create-signin-routes.js
│ │ │ ├── facebook.js
│ │ │ ├── github.js
│ │ │ ├── google.js
│ │ │ ├── index.js
│ │ │ ├── logout.js
│ │ │ └── twitter.js
│ │ ├── create-subscription-server.js
│ │ └── middlewares/
│ │ └── index.js
│ ├── schema.js
│ ├── subscriptions/
│ │ ├── community.js
│ │ ├── directMessageThread.js
│ │ ├── message.js
│ │ ├── notification.js
│ │ └── thread.js
│ ├── test/
│ │ ├── __snapshots__/
│ │ │ ├── community.test.js.snap
│ │ │ ├── directMessageThread.test.js.snap
│ │ │ └── user.test.js.snap
│ │ ├── channel/
│ │ │ ├── mutations/
│ │ │ │ ├── __snapshots__/
│ │ │ │ │ ├── createChannel.test.js.snap
│ │ │ │ │ ├── deleteChannel.test.js.snap
│ │ │ │ │ └── editChannel.test.js.snap
│ │ │ │ ├── createChannel.test.js
│ │ │ │ ├── deleteChannel.test.js
│ │ │ │ └── editChannel.test.js
│ │ │ └── queries/
│ │ │ ├── __snapshots__/
│ │ │ │ ├── channelSettings.test.js.snap
│ │ │ │ ├── memberConnection.test.js.snap
│ │ │ │ └── root.test.js.snap
│ │ │ ├── channelSettings.test.js
│ │ │ ├── memberConnection.test.js
│ │ │ └── root.test.js
│ │ ├── community/
│ │ │ ├── mutations/
│ │ │ │ ├── __snapshots__/
│ │ │ │ │ └── editCommunity.test.js.snap
│ │ │ │ └── editCommunity.test.js
│ │ │ └── queries/
│ │ │ ├── __snapshots__/
│ │ │ │ └── communitySettings.test.js.snap
│ │ │ └── communitySettings.test.js
│ │ ├── community.test.js
│ │ ├── directMessageThread.test.js
│ │ ├── message/
│ │ │ ├── __snapshots__/
│ │ │ │ └── queries.test.js.snap
│ │ │ ├── mutations/
│ │ │ │ └── addMessage.test.js
│ │ │ └── queries.test.js
│ │ ├── thread/
│ │ │ ├── mutations/
│ │ │ │ ├── __snapshots__/
│ │ │ │ │ ├── deleteThread.test.js.snap
│ │ │ │ │ └── publishThread.test.js.snap
│ │ │ │ ├── deleteThread.test.js
│ │ │ │ └── publishThread.test.js
│ │ │ └── queries/
│ │ │ ├── __snapshots__/
│ │ │ │ ├── messageConnection.test.js.snap
│ │ │ │ └── root.test.js.snap
│ │ │ ├── messageConnection.test.js
│ │ │ ├── reversePagination.test.js
│ │ │ └── root.test.js
│ │ ├── user.test.js
│ │ ├── utils/
│ │ │ ├── __mocks__/
│ │ │ │ └── debug.js
│ │ │ ├── __snapshots__/
│ │ │ │ └── create-graphql-error-formatter.test.js.snap
│ │ │ └── create-graphql-error-formatter.test.js
│ │ └── utils.js
│ ├── types/
│ │ ├── Channel.js
│ │ ├── Community.js
│ │ ├── CommunityMember.js
│ │ ├── DirectMessageThread.js
│ │ ├── Invoice.js
│ │ ├── Message.js
│ │ ├── Reaction.js
│ │ ├── Thread.js
│ │ ├── ThreadParticipant.js
│ │ ├── User.js
│ │ ├── custom-scalars/
│ │ │ └── LowercaseString.js
│ │ ├── general.js
│ │ └── scalars.js
│ └── utils/
│ ├── UserError.js
│ ├── base64.js
│ ├── create-graphql-error-formatter.js
│ ├── file-storage.js
│ ├── file-system.js
│ ├── generate-thread-meta-image-from-text.js
│ ├── get-page-meta.js
│ ├── get-random-default-photo.js
│ ├── is-spectrum-url.js
│ ├── markdown-linkify.js
│ ├── paginate-arrays.js
│ ├── permissions.js
│ ├── s3.js
│ ├── session-store.js
│ └── validate-draft-js-input.js
├── backpack.config.js
├── config-overrides.js
├── cypress/
│ ├── fixtures/
│ │ └── example.json
│ ├── integration/
│ │ ├── channel/
│ │ │ ├── settings/
│ │ │ │ ├── delete_spec.js
│ │ │ │ └── edit_spec.js
│ │ │ └── view/
│ │ │ ├── membership_spec.js
│ │ │ ├── profile_spec.js
│ │ │ └── threads_spec.js
│ │ ├── community/
│ │ │ └── view/
│ │ │ └── profile_spec.js
│ │ ├── community_settings_members_spec.js
│ │ ├── community_settings_overview_spec.js
│ │ ├── explore_spec.js
│ │ ├── login_spec.js
│ │ ├── messages_spec.js
│ │ ├── modal_routes_spec.js
│ │ ├── thread/
│ │ │ ├── action_bar_spec.js
│ │ │ └── view_spec.js
│ │ ├── thread_spec.js
│ │ ├── toasts_spec.js
│ │ ├── user/
│ │ │ ├── delete_user_spec.js
│ │ │ ├── edit_user_spec.js
│ │ │ └── me_redirect_spec.js
│ │ └── user_spec.js
│ ├── plugins/
│ │ └── index.js
│ └── support/
│ ├── commands.js
│ └── index.js
├── cypress.json
├── docker/
│ ├── Dockerfile.api
│ └── Dockerfile.hyperion
├── docs/
│ ├── admin/
│ │ └── intro.md
│ ├── api/
│ │ ├── graphql/
│ │ │ ├── fragments.md
│ │ │ ├── intro.md
│ │ │ ├── pagination.md
│ │ │ ├── testing.md
│ │ │ └── tips-and-tricks.md
│ │ └── intro.md
│ ├── backend/
│ │ └── api/
│ │ ├── README.md
│ │ ├── fragments.md
│ │ ├── pagination.md
│ │ ├── testing.md
│ │ └── tips-and-tricks.md
│ ├── deployments.md
│ ├── hyperion (server side rendering)/
│ │ ├── development.md
│ │ └── intro.md
│ ├── operations/
│ │ ├── hourly-backups.md
│ │ ├── importing-rethinkdb-backups.md
│ │ └── intro.md
│ ├── readme.md
│ └── testing/
│ ├── integration.md
│ ├── intro.md
│ └── unit.md
├── flow-typed/
│ ├── npm/
│ │ ├── @sendgrid/
│ │ │ └── mail_vx.x.x.js
│ │ ├── @tippy.js/
│ │ │ └── react_vx.x.x.js
│ │ ├── @vx/
│ │ │ ├── curve_vx.x.x.js
│ │ │ ├── event_vx.x.x.js
│ │ │ ├── gradient_vx.x.x.js
│ │ │ ├── grid_vx.x.x.js
│ │ │ ├── scale_vx.x.x.js
│ │ │ ├── shape_vx.x.x.js
│ │ │ └── tooltip_vx.x.x.js
│ │ ├── amplitude_vx.x.x.js
│ │ ├── apollo-cache-inmemory_vx.x.x.js
│ │ ├── apollo-client_vx.x.x.js
│ │ ├── apollo-engine_vx.x.x.js
│ │ ├── apollo-link-http_vx.x.x.js
│ │ ├── apollo-link-retry_vx.x.x.js
│ │ ├── apollo-link-schema_vx.x.x.js
│ │ ├── apollo-link-ws_vx.x.x.js
│ │ ├── apollo-link_vx.x.x.js
│ │ ├── apollo-local-query_vx.x.x.js
│ │ ├── apollo-server-cache-redis_vx.x.x.js
│ │ ├── apollo-server-express_vx.x.x.js
│ │ ├── apollo-server-plugin-response-cache_vx.x.x.js
│ │ ├── apollo-upload-client_vx.x.x.js
│ │ ├── apollo-upload-server_vx.x.x.js
│ │ ├── apollo-utilities_vx.x.x.js
│ │ ├── aws-sdk_vx.x.x.js
│ │ ├── axios_v0.17.x.js
│ │ ├── axios_vx.x.x.js
│ │ ├── b2a_vx.x.x.js
│ │ ├── babel-cli_vx.x.x.js
│ │ ├── babel-eslint_vx.x.x.js
│ │ ├── babel-plugin-import-inspector_vx.x.x.js
│ │ ├── babel-plugin-styled-components_vx.x.x.js
│ │ ├── babel-plugin-syntax-async-generators_vx.x.x.js
│ │ ├── babel-plugin-syntax-dynamic-import_vx.x.x.js
│ │ ├── babel-plugin-transform-async-generator-functions_vx.x.x.js
│ │ ├── babel-plugin-transform-class-properties_vx.x.x.js
│ │ ├── babel-plugin-transform-flow-strip-types_vx.x.x.js
│ │ ├── babel-plugin-transform-object-rest-spread_vx.x.x.js
│ │ ├── babel-preset-env_vx.x.x.js
│ │ ├── backpack-core_vx.x.x.js
│ │ ├── bad-words_vx.x.x.js
│ │ ├── bluebird_vx.x.x.js
│ │ ├── body-parser_v1.x.x.js
│ │ ├── casual_vx.x.x.js
│ │ ├── cheerio_vx.x.x.js
│ │ ├── common-tags_v1.4.x.js
│ │ ├── compression_vx.x.x.js
│ │ ├── cookie-parser_vx.x.x.js
│ │ ├── cookie-session_vx.x.x.js
│ │ ├── cors_vx.x.x.js
│ │ ├── cross-env_vx.x.x.js
│ │ ├── cryptr_vx.x.x.js
│ │ ├── css.escape_vx.x.x.js
│ │ ├── d3-array_vx.x.x.js
│ │ ├── danger-plugin-flow_vx.x.x.js
│ │ ├── danger-plugin-jest_vx.x.x.js
│ │ ├── danger-plugin-labels_vx.x.x.js
│ │ ├── danger-plugin-no-console_vx.x.x.js
│ │ ├── danger-plugin-no-test-shortcuts_vx.x.x.js
│ │ ├── danger-plugin-yarn_vx.x.x.js
│ │ ├── danger_vx.x.x.js
│ │ ├── datadog-metrics_vx.x.x.js
│ │ ├── dataloader_vx.x.x.js
│ │ ├── debounce_vx.x.x.js
│ │ ├── debug_v2.x.x.js
│ │ ├── decode-uri-component_vx.x.x.js
│ │ ├── draft-js-code-editor-plugin_vx.x.x.js
│ │ ├── draft-js-drag-n-drop-plugin_vx.x.x.js
│ │ ├── draft-js-embed-plugin_vx.x.x.js
│ │ ├── draft-js-export-markdown_vx.x.x.js
│ │ ├── draft-js-focus-plugin_vx.x.x.js
│ │ ├── draft-js-image-plugin_vx.x.x.js
│ │ ├── draft-js-import-markdown_vx.x.x.js
│ │ ├── draft-js-linkify-plugin_vx.x.x.js
│ │ ├── draft-js-markdown-plugin_vx.x.x.js
│ │ ├── draft-js-plugins-editor_vx.x.x.js
│ │ ├── draft-js-prism-plugin_vx.x.x.js
│ │ ├── draft-js_vx.x.x.js
│ │ ├── draftjs-to-markdown_vx.x.x.js
│ │ ├── electron-context-menu_vx.x.x.js
│ │ ├── electron-is-dev_vx.x.x.js
│ │ ├── electron-updater_vx.x.x.js
│ │ ├── electron-window-state_vx.x.x.js
│ │ ├── electron_vx.x.x.js
│ │ ├── emoji-regex_vx.x.x.js
│ │ ├── eslint-plugin-flowtype_vx.x.x.js
│ │ ├── eslint-plugin-jest_vx.x.x.js
│ │ ├── eslint-plugin-promise_vx.x.x.js
│ │ ├── eslint-plugin-react_vx.x.x.js
│ │ ├── eslint_vx.x.x.js
│ │ ├── expo-server-sdk_vx.x.x.js
│ │ ├── expo_vx.x.x.js
│ │ ├── express-enforces-ssl_vx.x.x.js
│ │ ├── express-hot-shots_vx.x.x.js
│ │ ├── express-session_vx.x.x.js
│ │ ├── express_v4.x.x.js
│ │ ├── faker_vx.x.x.js
│ │ ├── find-with-regex_vx.x.x.js
│ │ ├── flow-bin_v0.x.x.js
│ │ ├── flow-typed_vx.x.x.js
│ │ ├── graphql-cost-analysis_vx.x.x.js
│ │ ├── graphql-date_vx.x.x.js
│ │ ├── graphql-depth-limit_vx.x.x.js
│ │ ├── graphql-log_vx.x.x.js
│ │ ├── graphql-redis-subscriptions_vx.x.x.js
│ │ ├── graphql-server-express_vx.x.x.js
│ │ ├── graphql-subscriptions_vx.x.x.js
│ │ ├── graphql-tag_vx.x.x.js
│ │ ├── graphql-tools_vx.x.x.js
│ │ ├── graphql_vx.x.x.js
│ │ ├── helmet_vx.x.x.js
│ │ ├── highlight.js_vx.x.x.js
│ │ ├── history_vx.x.x.js
│ │ ├── hoist-non-react-statics_vx.x.x.js
│ │ ├── host-validation_vx.x.x.js
│ │ ├── hot-shots_vx.x.x.js
│ │ ├── hpp_vx.x.x.js
│ │ ├── hsts_vx.x.x.js
│ │ ├── http-proxy-middleware_vx.x.x.js
│ │ ├── idx_v2.x.x.js
│ │ ├── imgix-core-js_vx.x.x.js
│ │ ├── immutability-helper_vx.x.x.js
│ │ ├── ioredis_vx.x.x.js
│ │ ├── is-html_vx.x.x.js
│ │ ├── isomorphic-fetch_v2.x.x.js
│ │ ├── iterall_vx.x.x.js
│ │ ├── jest_v22.x.x.js
│ │ ├── json-stringify-pretty-compact_vx.x.x.js
│ │ ├── jsonwebtoken_vx.x.x.js
│ │ ├── keygrip_vx.x.x.js
│ │ ├── linkify-it_vx.x.x.js
│ │ ├── lint-staged_vx.x.x.js
│ │ ├── localstorage-memory_vx.x.x.js
│ │ ├── lodash.intersection_vx.x.x.js
│ │ ├── lodash_v4.x.x.js
│ │ ├── longjohn_vx.x.x.js
│ │ ├── markdown-draft-js_vx.x.x.js
│ │ ├── micromatch_vx.x.x.js
│ │ ├── moment_v2.3.x.js
│ │ ├── ms_vx.x.x.js
│ │ ├── newrelic_vx.x.x.js
│ │ ├── node-env-file_vx.x.x.js
│ │ ├── node-fetch_vx.x.x.js
│ │ ├── node-localstorage_vx.x.x.js
│ │ ├── nodemon_vx.x.x.js
│ │ ├── now-env_vx.x.x.js
│ │ ├── offline-plugin_vx.x.x.js
│ │ ├── optics-agent_vx.x.x.js
│ │ ├── passport-facebook_vx.x.x.js
│ │ ├── passport-github2_vx.x.x.js
│ │ ├── passport-google-oauth2_vx.x.x.js
│ │ ├── passport-twitter_vx.x.x.js
│ │ ├── passport_vx.x.x.js
│ │ ├── postmark_vx.x.x.js
│ │ ├── pre-commit_vx.x.x.js
│ │ ├── prettier_vx.x.x.js
│ │ ├── prism-react-renderer_vx.x.x.js
│ │ ├── prismjs_vx.x.x.js
│ │ ├── puppeteer_vx.x.x.js
│ │ ├── query-string_vx.x.x.js
│ │ ├── raf_vx.x.x.js
│ │ ├── raven-js_v3.17.x.js
│ │ ├── raven-js_vx.x.x.js
│ │ ├── raven_vx.x.x.js
│ │ ├── raw-loader_vx.x.x.js
│ │ ├── react-apollo_vx.x.x.js
│ │ ├── react-app-rewire-styled-components_vx.x.x.js
│ │ ├── react-app-rewired_vx.x.x.js
│ │ ├── react-async-hook_vx.x.x.js
│ │ ├── react-clipboard.js_vx.x.x.js
│ │ ├── react-dropzone_vx.x.x.js
│ │ ├── react-error-boundary_vx.x.x.js
│ │ ├── react-flip-move_v2.9.x.js
│ │ ├── react-helmet-async_vx.x.x.js
│ │ ├── react-helmet_vx.x.x.js
│ │ ├── react-hot-loader_vx.x.x.js
│ │ ├── react-image_vx.x.x.js
│ │ ├── react-infinite-scroller-fork-mxstbr_vx.x.x.js
│ │ ├── react-infinite-scroller-with-scroll-element_vx.x.x.js
│ │ ├── react-loadable_vx.x.x.js
│ │ ├── react-mentions_vx.x.x.js
│ │ ├── react-modal_vx.x.x.js
│ │ ├── react-popper_vx.x.x.js
│ │ ├── react-redux_v5.x.x.js
│ │ ├── react-remarkable_vx.x.x.js
│ │ ├── react-router-dom_vx.x.x.js
│ │ ├── react-router_v4.x.x.js
│ │ ├── react-router_vx.x.x.js
│ │ ├── react-scripts_vx.x.x.js
│ │ ├── react-stripe-checkout_vx.x.x.js
│ │ ├── react-stripe-elements_vx.x.x.js
│ │ ├── react-textarea-autosize_vx.x.x.js
│ │ ├── react-transition-group_vx.x.x.js
│ │ ├── react-trend_vx.x.x.js
│ │ ├── react-visibility-sensor_vx.x.x.js
│ │ ├── react_v16.8.0.js
│ │ ├── recharts_vx.x.x.js
│ │ ├── recompose_v0.x.x.js
│ │ ├── recompose_vx.x.x.js
│ │ ├── redis-tag-cache_vx.x.x.js
│ │ ├── redraft_vx.x.x.js
│ │ ├── redux-thunk_vx.x.x.js
│ │ ├── redux_v3.x.x.js
│ │ ├── request-ip_vx.x.x.js
│ │ ├── rethinkdb-changefeed-reconnect_vx.x.x.js
│ │ ├── rethinkdb-inspector_vx.x.x.js
│ │ ├── rethinkdb-migrate_vx.x.x.js
│ │ ├── rethinkdbdash_vx.x.x.js
│ │ ├── rethinkhaberdashery_vx.x.x.js
│ │ ├── rimraf_v2.x.x.js
│ │ ├── rimraf_vx.x.x.js
│ │ ├── sanitize-filename_vx.x.x.js
│ │ ├── sentry-expo_vx.x.x.js
│ │ ├── serialize-javascript_vx.x.x.js
│ │ ├── session-rethinkdb_vx.x.x.js
│ │ ├── sha1_vx.x.x.js
│ │ ├── shortid_vx.x.x.js
│ │ ├── slate-markdown_vx.x.x.js
│ │ ├── slate_vx.x.x.js
│ │ ├── slugg_vx.x.x.js
│ │ ├── snarkdown_vx.x.x.js
│ │ ├── stopword_vx.x.x.js
│ │ ├── string-replace-to-array_vx.x.x.js
│ │ ├── string-similarity_vx.x.x.js
│ │ ├── stripe_vx.x.x.js
│ │ ├── striptags_vx.x.x.js
│ │ ├── styled-components_v2.x.x.js
│ │ ├── styled-components_vx.x.x.js
│ │ ├── subscriptions-transport-ws_vx.x.x.js
│ │ ├── sw-precache-webpack-plugin_vx.x.x.js
│ │ ├── then-queue_vx.x.x.js
│ │ ├── toobusy-js_vx.x.x.js
│ │ ├── uuid_v3.x.x.js
│ │ ├── validator_vx.x.x.js
│ │ ├── web-push_vx.x.x.js
│ │ ├── webpack-bundle-analyzer_vx.x.x.js
│ │ ├── webpack-module-manifest-plugin_vx.x.x.js
│ │ └── write-file-webpack-plugin_vx.x.x.js
│ └── react-native.js
├── hyperion/
│ ├── index.js
│ └── renderer/
│ ├── browser-shim.js
│ ├── html-template.js
│ └── index.js
├── jest.config.js
├── now-secrets.example.json
├── now.json
├── package.json
├── public/
│ ├── index.html
│ ├── install-raven.js
│ ├── manifest.json
│ ├── push-sw.js
│ ├── robots.txt
│ └── service-worker.js
├── robots.txt
├── rules-alpha.json
├── rules.json
├── scripts/
│ ├── deploy.js
│ ├── generate-table-diagram.js
│ ├── heroku-deploy.js
│ ├── introspection-query.js
│ └── utils/
│ ├── error.js
│ └── parse-argv.js
├── set-heroku-config
├── shared/
│ ├── clients/
│ │ ├── draft-js/
│ │ │ ├── links-decorator/
│ │ │ │ ├── core.js
│ │ │ │ └── index.js
│ │ │ ├── mentions-decorator/
│ │ │ │ ├── core.js
│ │ │ │ ├── index.js
│ │ │ │ └── test/
│ │ │ │ ├── core.test.js
│ │ │ │ └── mentions-decorator.test.js
│ │ │ ├── message/
│ │ │ │ ├── renderer.js
│ │ │ │ ├── test/
│ │ │ │ │ ├── __snapshots__/
│ │ │ │ │ │ └── renderer.test.js.snap
│ │ │ │ │ └── renderer.test.js
│ │ │ │ └── types.js
│ │ │ ├── renderer/
│ │ │ │ └── index.js
│ │ │ ├── thread/
│ │ │ │ └── renderer.js
│ │ │ └── utils/
│ │ │ ├── getSnippet.js
│ │ │ ├── getStringElements.js
│ │ │ ├── hasStringElements.js
│ │ │ ├── isShort.js
│ │ │ └── plaintext.js
│ │ ├── group-messages.js
│ │ └── test/
│ │ ├── __snapshots__/
│ │ │ └── messages.test.js.snap
│ │ └── messages.test.js
│ ├── cookie-utils.js
│ ├── db/
│ │ ├── constants.js
│ │ ├── create-query.js
│ │ ├── db.js
│ │ ├── index.js
│ │ ├── queries/
│ │ │ ├── channel.js
│ │ │ ├── community.js
│ │ │ ├── message.js
│ │ │ ├── thread.js
│ │ │ └── user.js
│ │ └── query-cache.js
│ ├── draft-utils/
│ │ ├── add-embeds-to-draft-js.js
│ │ ├── index.js
│ │ ├── message-types.js
│ │ ├── process-message-content.js
│ │ ├── process-thread-content.js
│ │ └── test/
│ │ ├── __snapshots__/
│ │ │ └── add-embeds-to-draft-js.test.js.snap
│ │ └── add-embeds-to-draft-js.test.js
│ ├── encryption/
│ │ └── index.js
│ ├── generate-meta-info.js
│ ├── get-mentions.js
│ ├── graphql/
│ │ ├── apollo-client-options.js
│ │ ├── constants.js
│ │ ├── fragments/
│ │ │ ├── channel/
│ │ │ │ ├── channelInfo.js
│ │ │ │ ├── channelMemberConnection.js
│ │ │ │ ├── channelMetaData.js
│ │ │ │ └── channelThreadConnection.js
│ │ │ ├── community/
│ │ │ │ ├── communityChannelConnection.js
│ │ │ │ ├── communityInfo.js
│ │ │ │ ├── communityMembers.js
│ │ │ │ ├── communityMetaData.js
│ │ │ │ ├── communitySettings.js
│ │ │ │ └── communityThreadConnection.js
│ │ │ ├── communityMember/
│ │ │ │ └── communityMemberInfo.js
│ │ │ ├── directMessageThread/
│ │ │ │ ├── directMessageThreadInfo.js
│ │ │ │ └── directMessageThreadMessageConnection.js
│ │ │ ├── message/
│ │ │ │ ├── directMessageInfo.js
│ │ │ │ └── messageInfo.js
│ │ │ ├── notification/
│ │ │ │ └── notificationInfo.js
│ │ │ ├── thread/
│ │ │ │ ├── threadInfo.js
│ │ │ │ ├── threadMessageConnection.js
│ │ │ │ └── threadParticipant.js
│ │ │ └── user/
│ │ │ ├── userChannelConnection.js
│ │ │ ├── userCommunityConnection.js
│ │ │ ├── userDirectMessageThreadConnection.js
│ │ │ ├── userEverythingConnection.js
│ │ │ ├── userInfo.js
│ │ │ ├── userSettings.js
│ │ │ └── userThreadConnection.js
│ │ ├── index.js
│ │ ├── mutations/
│ │ │ ├── channel/
│ │ │ │ ├── deleteChannel.js
│ │ │ │ └── editChannel.js
│ │ │ ├── community/
│ │ │ │ ├── deleteCommunity.js
│ │ │ │ ├── editCommunity.js
│ │ │ │ ├── toggleCommunityNoindex.js
│ │ │ │ └── toggleCommunityRedirect.js
│ │ │ ├── message/
│ │ │ │ └── deleteMessage.js
│ │ │ ├── thread/
│ │ │ │ └── deleteThread.js
│ │ │ ├── uploadImage.js
│ │ │ └── user/
│ │ │ ├── banUser.js
│ │ │ ├── deleteCurrentUser.js
│ │ │ └── editUser.js
│ │ ├── queries/
│ │ │ ├── channel/
│ │ │ │ ├── getChannel.js
│ │ │ │ ├── getChannelMemberConnection.js
│ │ │ │ ├── getChannelSettings.js
│ │ │ │ └── getChannelThreadConnection.js
│ │ │ ├── community/
│ │ │ │ ├── getCommunities.js
│ │ │ │ ├── getCommunity.js
│ │ │ │ ├── getCommunityChannelConnection.js
│ │ │ │ ├── getCommunityMembers.js
│ │ │ │ ├── getCommunitySettings.js
│ │ │ │ └── getCommunityThreadConnection.js
│ │ │ ├── communityMember/
│ │ │ │ └── getCommunityMember.js
│ │ │ ├── composer/
│ │ │ │ └── getComposerCommunitiesAndChannels.js
│ │ │ ├── directMessageThread/
│ │ │ │ ├── getCurrentUserDMThreadConnection.js
│ │ │ │ ├── getDirectMessageThread.js
│ │ │ │ ├── getDirectMessageThreadByUserIds.js
│ │ │ │ └── getDirectMessageThreadMessageConnection.js
│ │ │ ├── message/
│ │ │ │ ├── getMediaMessagesForThread.js
│ │ │ │ └── getMessage.js
│ │ │ ├── thread/
│ │ │ │ ├── getThread.js
│ │ │ │ └── getThreadMessageConnection.js
│ │ │ └── user/
│ │ │ ├── getCurrentUserEverythingFeed.js
│ │ │ ├── getCurrentUserSettings.js
│ │ │ ├── getUser.js
│ │ │ ├── getUserCommunityConnection.js
│ │ │ ├── getUserGithubProfile.js
│ │ │ └── getUserThreadConnection.js
│ │ ├── schema.json
│ │ └── subscriptions/
│ │ ├── index.js
│ │ └── utils.js
│ ├── graphql-cache-keys.js
│ ├── imgix/
│ │ ├── getDefaultExpires.js
│ │ ├── index.js
│ │ ├── sign.js
│ │ ├── signCommunity.js
│ │ ├── signMessage.js
│ │ ├── signThread.js
│ │ └── signUser.js
│ ├── install-dependencies.js
│ ├── middlewares/
│ │ ├── cors.js
│ │ ├── csrf.js
│ │ ├── error-handler.js
│ │ ├── logging.js
│ │ ├── raven.js
│ │ ├── security.js
│ │ ├── session.js
│ │ ├── statsd.js
│ │ ├── thread-param.js
│ │ └── toobusy.js
│ ├── normalize-url.js
│ ├── only-contains-emoji.js
│ ├── raven/
│ │ └── index.js
│ ├── regexps.js
│ ├── sentencify.js
│ ├── slate-utils.js
│ ├── slug-deny-lists.js
│ ├── sort-by-date.js
│ ├── statsd.js
│ ├── test/
│ │ ├── encryption.test.js
│ │ ├── fixtures/
│ │ │ ├── CHANNEL_CREATED.json
│ │ │ ├── COMMUNITY_INVITE.json
│ │ │ ├── DIRECT_MESSAGE_CREATED.json
│ │ │ ├── MEDIA_MESSAGE_CREATED.json
│ │ │ ├── MENTION_MESSAGE.json
│ │ │ ├── MENTION_THREAD.json
│ │ │ ├── MESSAGE_CREATED.json
│ │ │ ├── REACTION_CREATED.json
│ │ │ ├── THREAD_CREATED.json
│ │ │ ├── THREAD_REACTION_CREATED.json
│ │ │ └── USER_JOINED_COMMUNITY.json
│ │ ├── get-mentions.test.js
│ │ └── normalize-url.test.js
│ ├── testing/
│ │ ├── data.js
│ │ ├── db.js
│ │ ├── empty-db.js
│ │ ├── setup-test-framework.js
│ │ ├── setup.js
│ │ └── teardown.js
│ ├── theme/
│ │ └── index.js
│ ├── time-difference.js
│ ├── time-formatting.js
│ ├── truncate.js
│ ├── truthy-values.js
│ ├── types.js
│ └── unique-elements.js
├── spectrum-tmuxp.yaml
└── src/
├── actions/
│ ├── authentication.js
│ ├── directMessageThreads.js
│ ├── gallery.js
│ ├── modals.js
│ ├── threadSlider.js
│ ├── titlebar.js
│ └── toasts.js
├── api/
│ └── constants.js
├── components/
│ ├── announcementBanner/
│ │ ├── index.js
│ │ └── style.js
│ ├── appViewWrapper/
│ │ ├── index.js
│ │ └── style.js
│ ├── avatar/
│ │ ├── communityAvatar.js
│ │ ├── image.js
│ │ ├── index.js
│ │ ├── style.js
│ │ └── userAvatar.js
│ ├── badges/
│ │ ├── index.js
│ │ └── style.js
│ ├── button/
│ │ ├── index.js
│ │ └── style.js
│ ├── card/
│ │ └── index.js
│ ├── column/
│ │ └── index.js
│ ├── communitySidebar/
│ │ └── index.js
│ ├── conditionalWrap/
│ │ └── index.js
│ ├── editForm/
│ │ └── style.js
│ ├── entities/
│ │ ├── index.js
│ │ ├── listItems/
│ │ │ ├── channel.js
│ │ │ ├── community.js
│ │ │ ├── index.js
│ │ │ ├── style.js
│ │ │ └── user.js
│ │ └── profileCards/
│ │ ├── channel.js
│ │ ├── community.js
│ │ ├── components/
│ │ │ ├── channelActions.js
│ │ │ ├── channelCommunityMeta.js
│ │ │ ├── channelMeta.js
│ │ │ ├── communityActions.js
│ │ │ ├── communityMeta.js
│ │ │ ├── userActions.js
│ │ │ └── userMeta.js
│ │ ├── index.js
│ │ ├── style.js
│ │ └── user.js
│ ├── error/
│ │ ├── BlueScreen.js
│ │ ├── ErrorBoundary.js
│ │ ├── SettingsFallback.js
│ │ └── index.js
│ ├── flyout/
│ │ └── index.js
│ ├── formElements/
│ │ ├── index.js
│ │ └── style.js
│ ├── fullscreenView/
│ │ ├── index.js
│ │ └── style.js
│ ├── gallery/
│ │ ├── browser.js
│ │ ├── index.js
│ │ └── style.js
│ ├── githubProfile/
│ │ └── index.js
│ ├── globals/
│ │ └── index.js
│ ├── goop/
│ │ └── index.js
│ ├── head/
│ │ └── index.js
│ ├── hoverProfile/
│ │ ├── channelProfile.js
│ │ ├── communityProfile.js
│ │ ├── index.js
│ │ ├── loadingHoverProfile.js
│ │ ├── style.js
│ │ ├── userContainer.js
│ │ └── userProfile.js
│ ├── icon/
│ │ └── index.js
│ ├── illustrations/
│ │ └── index.js
│ ├── inboxThread/
│ │ ├── activity.js
│ │ ├── header/
│ │ │ ├── index.js
│ │ │ ├── style.js
│ │ │ ├── threadHeader.js
│ │ │ ├── timestamp.js
│ │ │ └── userProfileThreadHeader.js
│ │ ├── index.js
│ │ ├── messageCount.js
│ │ └── style.js
│ ├── infiniteScroll/
│ │ ├── deduplicateChildren.js
│ │ ├── index.js
│ │ └── tallViewports.js
│ ├── layout/
│ │ └── index.js
│ ├── listItems/
│ │ ├── channel/
│ │ │ ├── index.js
│ │ │ └── style.js
│ │ ├── index.js
│ │ └── style.js
│ ├── loading/
│ │ ├── index.js
│ │ └── style.js
│ ├── loginButtonSet/
│ │ ├── facebook.js
│ │ ├── github.js
│ │ ├── google.js
│ │ ├── index.js
│ │ ├── style.js
│ │ └── twitter.js
│ ├── logo/
│ │ └── index.js
│ ├── maintenance/
│ │ └── index.js
│ ├── menu/
│ │ ├── index.js
│ │ └── style.js
│ ├── message/
│ │ ├── authorByline.js
│ │ ├── index.js
│ │ ├── messageErrorFallback.js
│ │ ├── style.js
│ │ ├── threadAttachment/
│ │ │ ├── attachment.js
│ │ │ ├── index.js
│ │ │ └── style.js
│ │ └── view.js
│ ├── messageGroup/
│ │ ├── directMessage.js
│ │ ├── index.js
│ │ ├── style.js
│ │ └── thread.js
│ ├── modals/
│ │ ├── BanUserModal/
│ │ │ ├── index.js
│ │ │ └── style.js
│ │ ├── DeleteDoubleCheckModal/
│ │ │ ├── index.js
│ │ │ └── style.js
│ │ ├── modalContainer.js
│ │ ├── modalRoot.js
│ │ └── styles.js
│ ├── nextPageButton/
│ │ ├── index.js
│ │ └── style.js
│ ├── outsideClickHandler/
│ │ └── index.js
│ ├── profile/
│ │ ├── coverPhoto.js
│ │ ├── index.js
│ │ ├── metaData.js
│ │ ├── style.js
│ │ └── thread.js
│ ├── reaction/
│ │ ├── index.js
│ │ └── style.js
│ ├── redirectHandler/
│ │ └── index.js
│ ├── rich-text-editor/
│ │ ├── prism-theme.css
│ │ └── style.js
│ ├── scrollManager/
│ │ └── index.js
│ ├── scrollRow/
│ │ ├── index.js
│ │ └── style.js
│ ├── segmentedControl/
│ │ ├── index.js
│ │ └── style.js
│ ├── select/
│ │ ├── index.js
│ │ └── style.js
│ ├── settingsViews/
│ │ ├── header.js
│ │ ├── style.js
│ │ └── subnav.js
│ ├── themedSection/
│ │ └── index.js
│ ├── threadFeed/
│ │ ├── index.js
│ │ ├── nullState.js
│ │ └── style.js
│ ├── threadFeedCard/
│ │ └── style.js
│ ├── threadRenderer/
│ │ └── index.js
│ ├── titlebar/
│ │ ├── actions.js
│ │ ├── base.js
│ │ ├── index.js
│ │ └── style.js
│ ├── toasts/
│ │ ├── index.js
│ │ └── style.js
│ ├── tooltip/
│ │ └── index.js
│ ├── upsell/
│ │ ├── index.js
│ │ └── style.js
│ ├── usernameSearch/
│ │ ├── index.js
│ │ └── style.js
│ ├── viewError/
│ │ ├── index.js
│ │ └── style.js
│ ├── viewNetworkHandler/
│ │ └── index.js
│ ├── visuallyHidden/
│ │ └── index.js
│ └── withCurrentUser/
│ └── index.js
├── helpers/
│ ├── directMessageThreads.js
│ ├── get-thread-link.js
│ ├── history.js
│ ├── images.js
│ ├── is-admin.js
│ ├── is-viewing-marketing-page.js
│ ├── keycodes.js
│ ├── localStorage.js
│ ├── navigation-context.js
│ ├── notifications.js
│ ├── realtimeThreads.js
│ ├── regexps.js
│ ├── render-text-with-markdown-links.js
│ ├── sentry-redux-middleware.js
│ ├── signed-out-fallback.js
│ ├── utils.js
│ └── web-push-manager.js
├── hooks/
│ ├── useAppScroller.js
│ ├── useConnectionRestored.js
│ ├── useDebounce.js
│ └── usePrevious.js
├── hot-routes.js
├── index.js
├── reducers/
│ ├── connectionStatus.js
│ ├── gallery.js
│ ├── index.js
│ ├── modals.js
│ ├── threadSlider.js
│ ├── titlebar.js
│ └── toasts.js
├── registerServiceWorker.js
├── reset.css.js
├── routes.js
├── store/
│ └── index.js
└── views/
├── authViewHandler/
│ └── index.js
├── channel/
│ ├── components/
│ │ ├── MembersList.js
│ │ └── PostsFeed.js
│ ├── index.js
│ └── style.js
├── channelSettings/
│ ├── components/
│ │ ├── channelMembers.js
│ │ ├── editForm.js
│ │ └── overview.js
│ ├── index.js
│ └── style.js
├── community/
│ ├── components/
│ │ ├── channelsList.js
│ │ ├── communityFeeds.js
│ │ ├── membersList.js
│ │ ├── mobileCommunityInfoActions.js
│ │ ├── postsFeeds.js
│ │ └── teamMembersList.js
│ ├── containers/
│ │ ├── privateCommunity.js
│ │ └── signedIn.js
│ ├── index.js
│ └── style.js
├── communityLogin/
│ ├── index.js
│ └── style.js
├── communityMembers/
│ ├── components/
│ │ ├── communityMembers.js
│ │ ├── getMembers.js
│ │ └── mutationWrapper.js
│ ├── index.js
│ └── style.js
├── communitySettings/
│ ├── components/
│ │ ├── channelList.js
│ │ ├── editForm.js
│ │ ├── overview.js
│ │ └── redirect.js
│ ├── index.js
│ └── style.js
├── directMessages/
│ ├── components/
│ │ ├── avatars.js
│ │ ├── header.js
│ │ ├── loading.js
│ │ ├── messageThreadListItem.js
│ │ ├── messages.js
│ │ ├── style.js
│ │ └── threadsList.js
│ ├── containers/
│ │ ├── existingThread.js
│ │ └── index.js
│ ├── index.js
│ └── style.js
├── explore/
│ ├── collections.js
│ ├── index.js
│ ├── style.js
│ └── view.js
├── globalTitlebar/
│ └── index.js
├── homeViewRedirect/
│ └── index.js
├── login/
│ ├── index.js
│ └── style.js
├── navigation/
│ ├── accessibility.js
│ ├── communityList.js
│ ├── directMessagesTab.js
│ ├── index.js
│ ├── navHead.js
│ └── style.js
├── newUserOnboarding/
│ ├── components/
│ │ └── setUsername/
│ │ ├── index.js
│ │ └── style.js
│ ├── index.js
│ └── style.js
├── pages/
│ ├── components/
│ │ ├── communities.js
│ │ ├── footer.js
│ │ ├── logos.js
│ │ └── nav.js
│ ├── index.js
│ ├── privacy/
│ │ └── index.js
│ ├── style.js
│ └── terms/
│ ├── index.js
│ └── style.js
├── queryParamToastDispatcher/
│ └── index.js
├── status/
│ ├── index.js
│ └── style.js
├── thread/
│ ├── components/
│ │ ├── actionBar.js
│ │ ├── actionsDropdown.js
│ │ ├── lockedMessages.js
│ │ ├── messagesSubscriber.js
│ │ ├── nullMessages.js
│ │ ├── stickyHeader.js
│ │ ├── threadByline.js
│ │ ├── threadDetail.js
│ │ └── threadHead.js
│ ├── container/
│ │ └── index.js
│ ├── index.js
│ ├── redirect-old-route.js
│ └── style.js
├── threadSlider/
│ ├── index.js
│ └── style.js
├── user/
│ ├── components/
│ │ └── communityList.js
│ ├── index.js
│ └── style.js
├── userSettings/
│ ├── components/
│ │ ├── deleteAccountForm.js
│ │ ├── downloadDataForm.js
│ │ ├── editForm.js
│ │ ├── logout.js
│ │ └── overview.js
│ ├── index.js
│ └── style.js
└── viewHelpers/
├── errorView.js
├── fullScreenRedirect.js
├── index.js
├── loadingView.js
├── style.js
└── textValidationHelper.js
================================================
FILE CONTENTS
================================================
================================================
FILE: .babelrc
================================================
{
"presets": [
[
"env",
{
"targets": {
"node": "current"
},
"useBuiltIns": true,
"exclude": [
"babel-plugin-transform-regenerator",
"transform-async-to-generator"
]
}
]
],
"plugins": [
"babel-plugin-transform-class-properties",
["styled-components", { "ssr": true }],
"transform-flow-strip-types",
"transform-object-rest-spread",
"babel-plugin-transform-react-jsx",
"syntax-dynamic-import",
"syntax-async-generators",
"transform-async-generator-functions",
"react-loadable/babel",
"babel-plugin-inline-import-graphql-ast"
]
}
================================================
FILE: .circleci/config.yml
================================================
# CircleCI configuration for Spectrum
version: 2.1
# Aliases
aliases:
# Cache Management
- &restore-yarn-cache
keys:
- v1-yarn-{{ arch }}-{{ checksum "package.json" }}
- v1-yarn-{{ arch }}-
- &save-yarn-cache
paths:
- node_modules
- ~/.cache/yarn
key: v1-yarn-{{ arch }}-{{ checksum "package.json" }}
- &yarn |
yarn
- &install-rethinkdb
name: Install RethinkDB 2.3.5
command: |
echo "deb http://download.rethinkdb.com/apt jessie main" | sudo tee /etc/apt/sources.list.d/rethinkdb.list
wget -qO- http://download.rethinkdb.com/apt/pubkey.gpg | sudo apt-key add -
sudo apt-get update
sudo apt-get install rethinkdb=2.3.5~0jessie
- &start-rethinkdb
name: Start RethinkDB
command: rethinkdb --bind all
background: true
- &setup-and-build-web
name: Setup and build web
command: |
cp now-secrets.example.json now-secrets.json
yarn run build:web
- &build-api
name: Build API
command: yarn run build:api
- &start-api
name: Start the API in the background
command: yarn run start:api:test
background: true
- &start-web
name: Start web client in the background
command: yarn run dev:web
background: true
defaults: &defaults
working_directory: ~/spectrum
js_defaults: &js_defaults
<<: *defaults
docker:
- image: circleci/node:8
jobs:
# Set up environment and install required dependencies
checkout_environment:
<<: *js_defaults
steps:
- checkout
- restore_cache: *restore-yarn-cache
- run: *yarn
- save_cache: *save-yarn-cache
- persist_to_workspace:
root: .
paths: .
build_web:
<<: *js_defaults
steps:
- attach_workspace:
at: ~/spectrum
- run: *setup-and-build-web
- run: *build-api
- persist_to_workspace:
root: .
paths:
- build-api
- build
test_unit:
<<: *defaults
docker:
- image: circleci/node:8
- image: redis:3.2.7
- image: rethinkdb:2.3.5
environment:
TERM: xterm
steps:
- attach_workspace:
at: ~/spectrum
- run: yarn run db:migrate
- run: yarn run db:seed
- run:
name: Run Unit Tests
command: yarn run test:ci
# Start db and servers, then run e2e and unit tests
test_integration:
<<: *defaults
docker:
- image: circleci/node:8-browsers
- image: redis:3.2.7
- image: cypress/base:6
- image: rethinkdb:2.3.5
parameters:
parallelism:
type: integer
default: 1
description: Number of boxes to use to run this job
parallelism: <<parameters.parallelism>>
environment:
TERM: xterm
steps:
- attach_workspace:
at: ~/spectrum
- run: yarn run db:migrate
- run: yarn run db:seed
- run: *start-api
- run: *start-web
# Wait for the API and webserver to start
- run: ./node_modules/.bin/wait-on http://localhost:3000 http://localhost:3001
- run:
name: Install Cypress
command: yarn run cypress:install
- run:
name: Run E2E Tests
command: |
if [ $CYPRESS_RECORD_KEY ]; then
yarn run test:e2e -- --record --parallel
else
yarn run test:e2e
fi
# This runs after the above and is only here to hack around missing support for varying jobs based on external vs internal PRs
# See https://github.com/withspectrum/spectrum/pull/4820
test_e2e:
docker:
- image: cypress/base:10
steps:
- run: echo "pass"
# Run eslint, flow etc.
test_static_js:
<<: *js_defaults
steps:
- attach_workspace:
at: ~/spectrum
- run:
name: Run Flow
command: yarn run flow
- run:
name: Run ESLint
command: yarn run lint
workflows:
test:
jobs:
- checkout_environment
- test_unit:
requires:
- checkout_environment
- test_static_js:
requires:
- checkout_environment
- build_web:
requires:
- checkout_environment
# Run pull requests from internal contributors in parallel
- test_integration:
name: test_e2e_internal
requires:
- build_web
parallelism: 8
filters:
branches:
ignore: /pull.*/
# Run pull requests from external contributors on one machine
- test_integration:
name: test_e2e_external
requires:
- build_web
parallelism: 1
filters:
branches:
only: /pull.*/
# If either of the test_e2e_* jobs pass, this one passes so we can mark it as required on GitHub
- test_e2e:
requires:
- test_e2e_internal
- test_e2e_external
================================================
FILE: .dockerignore
================================================
.git
*docker-compose*
*Dockerfile*
node_modules
================================================
FILE: .eslintignore
================================================
# Note: This is a copy of the .gitignore,
# with flow-typed added
flow-typed
node_modules
.sass-cache
npm-debug.log
build
.DS_Store
src/config/FirebaseConfig.js
rethinkdb_data
debug
now-secrets.json
build-iris
build-api
build-hyperion
package-lock.json
.vscode
dump.rdb
*.swp
queries-by-response-size.js
queries-by-time.js
test-extend.js
stats.json
iris/.env
api/.env
test-results.json
================================================
FILE: .eslintrc.js
================================================
module.exports = {
extends: ['eslint:recommended', 'plugin:react/recommended'],
parser: 'babel-eslint',
env: {
es6: true,
node: true,
},
parserOptions: {
ecmaVersion: 6,
sourceType: 'module',
ecmaFeatures: {
jsx: true,
},
},
plugins: ['eslint-plugin-flowtype'],
rules: {
'no-undef': 0,
'no-console': ['warn', { allow: ['warn', 'error'] }],
'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,
},
};
================================================
FILE: .flowconfig
================================================
[ignore]
.*/build.*
.*/*.test.js
.*/node_modules/cypress
.*/node_modules/draft-js
.*/node_modules/graphql
.*/node_modules/protobufjs-no-cli
.*/node_modules/reqwest
.*/node_modules/react-apollo
.*/node_modules/dataloader
<PROJECT_ROOT>/node_modules/*
<PROJECT_ROOT>/email-template-scripts/*
[options]
suppress_comment=.*\\$FlowFixMe
suppress_comment=.*\\$FlowIssue
esproposal.class_instance_fields=enable
module.system.node.resolve_dirname=node_modules
module.system.node.resolve_dirname=.
module.file_ext=.js
module.file_ext=.jsx
module.file_ext=.json
[lints]
untyped-type-import=error
untyped-import=warn
unclear-type=warn
unsafe-getters-setters=error
[version]
0.66.0
================================================
FILE: .github/ISSUE_TEMPLATE.md
================================================
<!--
FILL OUT THE FORM BELOW OR THE ISSUE WILL BE AUTO-CLOSED
**Issue Type (check one)**
- [ ] Bug Report
- [ ] Feature Idea
- [ ] Technical Discussion
- [ ] Question (these will be auto-closed, please ask them on Spectrum instead https://spectrum.chat/spectrum/open)
**Description (type any text below)** -->
================================================
FILE: .github/PULL_REQUEST_TEMPLATE.md
================================================
<!-- FILL OUT THE BELOW FORM OR YOUR PR WILL BE AUTOMATICALLY CLOSED -->
**Status**
- [ ] WIP
- [ ] Ready for review
- [ ] Needs testing
**Deploy after merge (delete what needn't be deployed)**
- api
- hyperion (frontend)
**Run database migrations (delete if no migration was added)**
YES
## **Release notes for users (delete if codebase-only change)**
**Related issues (delete if you don't know of any)**
Closes #
<!-- If there are UI changes please share mobile-responsive and desktop screenshots or recordings. -->
================================================
FILE: .gitignore
================================================
node_modules
.sass-cache
npm-debug.log
build
.DS_Store
src/config/FirebaseConfig.js
npm-debug.log
yarn-error.log
rethinkdb_data
debug
now-secrets.json
build-iris
build-api
build-hyperion
build-electron
package-lock.json
.vscode
dump.rdb
*.swp
queries-by-response-size.js
queries-by-time.js
test-extend.js
stats.json
iris/.env
api/.env
test-results.json
public/uploads
cypress/screenshots/
cypress/videos/
cacert
.env
.env.*
================================================
FILE: .npmignore
================================================
# This is used by now when deploying hyperion, replacing .gitignore
# NOTE(@mxstbr): The important change is that `cacert` is NOT ignored!
node_modules
.sass-cache
npm-debug.log
build
.DS_Store
src/config/FirebaseConfig.js
npm-debug.log
yarn-error.log
rethinkdb_data
debug
now-secrets.json
build-iris
build-api
build-hyperion
build-electron
package-lock.json
.vscode
dump.rdb
*.swp
queries-by-response-size.js
queries-by-time.js
test-extend.js
stats.json
iris/.env
api/.env
test-results.json
public/uploads
cypress/screenshots/
cypress/videos/
# This is hyperion-now-specific, do not copy to .gitignore
docs
cypress
admin
.circleci
.github
================================================
FILE: .prettierignore
================================================
flow-typed
package.json
================================================
FILE: .prettierrc
================================================
{
"singleQuote": true,
"trailingComma": "es5"
}
================================================
FILE: LICENSE
================================================
Copyright 2018 Space Program Inc.
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
================================================
FILE: README.md
================================================
<div align="center">
[](https://spectrum.chat)
### Simple, powerful online communities.
</div>
This is the main monorepo codebase of [Spectrum](https://spectrum.chat). Every single line of code that's not packaged into a reusable library is in this repository.
## What is Spectrum?
### Vision
It is difficult to grow, manage and measure the impact of online communities. Community owners need modern, chat-based communities but are running into scaling issues when their community grows beyond a few hundred members. It becomes hard to keep track of who's who, know what conversations are happening, and ensure that the community is staying healthy and productive.
**Spectrum aims to be the best platform to build any kind of community online by combining the best of forums and real-time chat apps.** With best-in-class moderation tooling, a single platform for all your communities, threaded conversations by default, community health monitoring, and much more to come we think that we will be able to help more people start and grow the best online communities.
> "[Spectrum] will take the place that Reddit used to have a long time ago for communities (especially tech) to freely share ideas and interact. Except realtime and trolling-free."
>
> \- [Guillermo Rauch (@rauchg)](https://twitter.com/rauchg/status/930946768841228288)
### Status
Spectrum has been in full-time development since March 2017 and is [part of GitHub since November 2018](https://spectrum.chat/spectrum/general/spectrum-is-joining-github~1d3eb8ee-4c99-46c0-8daf-ca35a96be6ce). See our current priorities and what we are working on in the [main project board](https://github.com/withspectrum/spectrum/projects/23).
<div align="center">
<img height="50px" src="public/img/cluster-1.svg" />
</div>
## Docs
- [Contributing](#contributing)
- [Ground Rules](#ground-rules)
- [Codebase](#codebase)
- [Technologies](#technologies)
- [Folder Structure](#folder-structure)
- [Code Style](#code-style)
- [First time setup](#first-time-setup)
- [Running the app locally](#running-the-app-locally)
- [Roadmap](https://github.com/withspectrum/spectrum/projects/19)
- [Technical](docs/)
- [Testing](docs/testing/intro.md)
- [Background Jobs](docs/workers/background-jobs.md)
- [Deployment](docs/deployments.md)
- [API](docs/backend/api/)
- [Fragments](docs/backend/api/fragments.md)
- [Pagination](docs/backend/api/pagination.md)
- [Testing](docs/backend/api/testing.md)
- [Tips and Tricks](docs/backend/api/tips-and-tricks.md)
## Contributing
**We heartily welcome any and all contributions that match our engineering standards!**
That being said, this codebase isn't your typical open source project because it's not a library or package with a limited scope—it's our entire product.
### Ground Rules
#### Contributions and discussion guidelines
All conversations and communities on Spectrum agree to GitHub's [Community Guidelines](https://help.github.com/en/github/site-policy/github-community-guidelines) and [Acceptable Use Policies](https://help.github.com/en/github/site-policy/github-acceptable-use-policies). This code of conduct also applies to all conversations that happen within our contributor community here on GitHub. We expect discussions in issues and pull requests to stay positive, productive, and respectful. Remember: there are real people on the other side of that screen!
#### Reporting a bug or discussing a feature idea
If you found a technical bug on Spectrum or have ideas for features we should implement, the issue tracker is the best place to share your ideas. Make sure to follow the issue template and you should be golden! ([click here to open a new issue](https://github.com/withspectrum/spectrum/issues/new))
#### Fixing a bug or implementing a new feature
If you find a bug on Spectrum and open a PR that fixes it we'll review it as soon as possible to ensure it matches our engineering standards.
If you want to implement a new feature, open an issue first to discuss what it'd look like and to ensure it fits in our roadmap and plans for the app (see [the main project board](https://github.com/withspectrum/spectrum/projects/23) for planned and currently ongoing work).
If you want to contribute but are unsure to start, we have [a "good first issue" label](https://github.com/withspectrum/spectrum/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22) which is applied to newcomer-friendly issues. Take a look at [the full list of good first issues](https://github.com/withspectrum/spectrum/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22) and pick something you like! There is also [an "open" channel in the Spectrum community on Spectrum](https://spectrum.chat/spectrum/open) (how meta), if you run into troubles while trying to contribute that is the best place to talk to us.
Want to fix a bug or implement an agreed-upon feature? Great, jump to the [local setup instructions](#first-time-setup)!
<div align="center">
<img height="70px" src="public/img/cluster-2.svg" />
</div>
### Codebase
#### Technologies
With the ground rules out of the way, let's talk about the coarse architecture of this mono repo:
- **Full-stack JavaScript**: We use Node.js to power our servers, and React to power our frontend apps. Almost all of the code you'll touch in this codebase will be JavaScript.
Here is a list of all the big technologies we use:
- **RethinkDB**: Data storage
- **Redis**: Background jobs and caching
- **GraphQL**: API, powered by the entire Apollo toolchain
- **Flowtype**: Type-safe JavaScript
- **PassportJS**: Authentication
- **React**: Frontend React app
#### Folder structure
```sh
spectrum/
├── api # API server
├── docs
├── hyperion # Rendering server
├── public # Public files used on the frontend
├── shared # Shared JavaScript code
├── src # Frontend SPA
```
<details>
<summary>Click to learn about the worker naming scheme</summary>
#### Naming Scheme
As you can see we follow a loose naming scheme based on ancient Greek, Roman, and philosophical figures that are somewhat related to what our servers do:
- Hyperion: (/haɪˈpɪəriən/) is one of the twelve Titan children of Gaia and Uranus.
</details>
#### Code Style
We run Prettier on-commit, which means you can write code in whatever style you want and it will be automatically formatted according to the common style when you run `git commit`. We also have ESLint set up, although we've disabled all stylistic rules since Prettier takes care of those.
##### Rules
- **All new `.js` files must be flow typed**: Since we only introduced Flowtype after we finished building the first version of Spectrum, we enforce in CI that all new files added to the codebase are typed. (if you've never used Flowtype before that's totally fine, just write your code in plain JS and let us know in the PR body, we can take care of it for you)
- **No `console.log`s in any file**: We use the `debug` module across the codebase to log debugging information in development only. Never commit a file that contains a `console.log` as CI will fail your build. The only exceptions are errors, which you can log, but you have to use `console.error` to be explicit about it
<div align="center">
<img height="70px" src="public/img/cluster-3.svg" />
</div>
### First time setup
The first step to running Spectrum locally is downloading the code by cloning the repository:
```sh
git clone git@github.com:withspectrum/spectrum.git
```
If you get `Permission denied` error using `ssh` refer [here](https://help.github.com/articles/error-permission-denied-publickey/)
or use `https` link as a fallback.
```sh
git clone https://github.com/withspectrum/spectrum.git
```
#### Installation
Spectrum has four big installation steps:
1. **Install RethinkDB**: See [the RethinkDB documentation](https://rethinkdb.com/docs/install/) for instructions on installing it with your OS.
2. **Install Redis**: See [the Redis documentation](https://redis.io/download) for instructions on installing it with your OS.
3. **Install yarn**: We use [yarn](https://yarnpkg.com) to handle our JavaScript dependencies. (plain `npm` doesn't work due to our monorepo setup) See [the yarn documentation](https://yarnpkg.com/en/docs/install) for instructions on installing it.
4. **Install the dependencies**: Because it's pretty tedious to install the dependencies for each worker individually we've created a script that goes through and runs `yarn install` for every worker for you: (this takes a couple minutes, so dive into the [technical docs](./docs) in the meantime)
```sh
node shared/install-dependencies.js
```
You've now finished installing everything! Let's migrate the database and you'll be ready to go :100:
#### Migrating the database
When you first download the code and want to run it locally you have to migrate the database and seed it with test data. First, start rethinkdb in its own terminal tab:
```sh
rethinkdb
```
Then, in a new tab, run these commands:
```sh
yarn run db:migrate
yarn run db:seed
# ⚠️ To empty the database (e.g. if there's faulty data) run yarn run db:drop
```
There's a shortcut for dropping, migrating and seeding the database too:
```sh
yarn run db:reset
```
The `testing` database used in end to end tests is managed separately. It is built, migrated, and seeded when you run:
```sh
yarn run start:api:test
```
To drop the `testing` database, go to http://localhost:8080/#tables while `rethinkdb` is running, and click Delete Database on the appropriate database.
#### Getting the secrets
While the app will run without any secrets set up, you won't be able to sign in locally. To get that set up, copy the provided example secrets file to the real location:
```
cp now-secrets.example.json now-secrets.json
```
> Note: If you're an employee at Spectrum we've got a more complete list of secrets that also lets you upload images etc. in 1Password, search for "now-secrets.json" to find it.
Now you're ready to run the app locally and sign into your local instance!
### Running the app locally
#### Background services
Whenever you want to run Spectrum locally you have to have RethinkDB and Redis running in the background. First start rethinkdb like we did to migrate the database:
```sh
rethinkdb
```
Then (without closing the rethinkdb tab!) open another tab and start Redis:
```sh
redis-server
```
#### Start the servers
Depending on what you're trying to work on you'll need to start different servers. Generally, all servers run in development mode by doing `yarn run dev:<workername>`, e.g. `yarn run dev:hermes` to start the email worker.
No matter what you're trying to do though, you'll want to have the API running, so start that in a background tab:
```
yarn run dev:api
```
#### Develop the web UI
To develop the frontend and web UI run
```
yarn run dev:web
```
<br />
<div align="center">
<img height="200px" src="public/img/connect.svg" />
</div>
## GitHub
Spectrum is now part of GitHub. For code of conduct, please see [GitHub's Community Guidelines](https://help.github.com/en/github/site-policy/github-community-guidelines) and [Acceptable Use Policies](https://help.github.com/en/github/site-policy/github-acceptable-use-policies).
## License
BSD 3-Clause, see the [LICENSE](./LICENSE) file.
================================================
FILE: api/apollo-server.js
================================================
// @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 createLoaders from './loaders';
import createErrorFormatter from './utils/create-graphql-error-formatter';
import schema from './schema';
import { statsd } from 'shared/statsd';
import { getUserIdFromReq } from './utils/session-store';
import UserError from './utils/UserError';
import type { DBUser } from 'shared/types';
import { getUserById } from '../shared/db/queries/user';
// NOTE(@mxstbr): Evil hack to make graphql-cost-analysis work with Apollo Server v2
// @see pa-bru/graphql-cost-analysis#12
// @author @arianon
class ProtectedApolloServer extends ApolloServer {
async createGraphQLServerOptions(
req: express$Request,
res: express$Response
): Promise<*> {
const options = await super.createGraphQLServerOptions(req, res);
return {
...options,
validationRules: [
...options.validationRules,
costAnalysis({
maximumCost: 750,
defaultCost: 1,
variables: req.body.variables,
createError: (max, actual) => {
const err = new UserError(
`GraphQL query exceeds maximum complexity, please remove some nesting or fields and try again. (max: ${max}, actual: ${actual})`
);
return err;
},
}),
],
};
}
}
let connections = 0;
setInterval(() => {
statsd.gauge('websocket.connections.count', connections);
}, 5000);
const server = new ProtectedApolloServer({
schema,
formatError: createErrorFormatter(),
// For subscriptions, this gets passed "connection", for everything else "req" and "res"
context: ({ req, res, connection, ...rest }, ...other) => {
if (connection) {
return {
...(connection.context || {}),
};
}
// Add GraphQL operation information to the statsd tags
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;
return {
loaders,
updateCookieUserData: (data: DBUser) =>
new Promise((res, rej) =>
req.login(data, err => (err ? rej(err) : res()))
),
req,
user: currentUser,
};
},
subscriptions: {
path: '/websocket',
onConnect: (connectionParams, rawSocket) => {
connections++;
return getUserIdFromReq(rawSocket.upgradeReq)
.then(id => (id ? getUserById(id) : null))
.then(user => {
return {
user: user || null,
loaders: createLoaders({ cache: false }),
};
})
.catch(err => {
console.error(err);
return {
loaders: createLoaders({ cache: false }),
};
});
},
},
playground: process.env.NODE_ENV !== 'production' && {
settings: {
'editor.theme': 'light',
},
tabs: [
{
endpoint: 'http://localhost:3001/api',
query: `{
user(username: "mxstbr") {
id
username
}
}`,
},
],
},
introspection: process.env.NODE_ENV !== 'production',
maxFileSize: 25 * 1024 * 1024, // 25MB
engine: false,
tracing: false,
validationRules: [depthLimit(10)],
cacheControl: {
calculateHttpHeaders: false,
// Cache everything for at least a minute since we only cache public responses
defaultMaxAge: 60,
},
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;
================================================
FILE: api/authentication.js
================================================
// @flow
require('now-env');
const passport = require('passport');
const { Strategy: TwitterStrategy } = require('passport-twitter');
const { Strategy: FacebookStrategy } = require('passport-facebook');
const { Strategy: GoogleStrategy } = require('passport-google-oauth2');
const { Strategy: GitHubStrategy } = require('passport-github2');
const {
getUserById,
createOrFindUser,
saveUserProvider,
getUserByIndex,
} = require('shared/db/queries/user');
const IS_PROD = !process.env.FORCE_DEV && process.env.NODE_ENV === 'production';
const TWITTER_OAUTH_CLIENT_SECRET = IS_PROD
? process.env.TWITTER_OAUTH_CLIENT_SECRET
: process.env.TWITTER_OAUTH_CLIENT_SECRET_DEVELOPMENT;
const FACEBOOK_OAUTH_CLIENT_ID = IS_PROD
? process.env.FACEBOOK_OAUTH_CLIENT_ID
: process.env.FACEBOOK_OAUTH_CLIENT_SECRET_DEVELOPMENT;
const FACEBOOK_OAUTH_CLIENT_SECRET = IS_PROD
? process.env.FACEBOOK_OAUTH_CLIENT_SECRET
: process.env.FACEBOOK_OAUTH_CLIENT_SECRET_DEVELOPMENT;
const GOOGLE_OAUTH_CLIENT_SECRET = IS_PROD
? process.env.GOOGLE_OAUTH_CLIENT_SECRET
: process.env.GOOGLE_OAUTH_CLIENT_SECRET_DEVELOPMENT;
const GITHUB_OAUTH_CLIENT_SECRET = IS_PROD
? process.env.GITHUB_OAUTH_CLIENT_SECRET
: process.env.GITHUB_OAUTH_CLIENT_SECRET_DEVELOPMENT;
const TWITTER_OAUTH_CLIENT_ID = IS_PROD
? 'vxmsICGyIIoT5NEYi1I8baPrf'
: 'Qk7BWFe44JKswEw2sNaDAA4x7';
const GOOGLE_OAUTH_CLIENT_ID = IS_PROD
? '923611718470-chv7p9ep65m3fqqjr154r1p3a5j6oidc.apps.googleusercontent.com'
: '923611718470-hjribk5128dr3s26cbp5cbdecigrsjsp.apps.googleusercontent.com';
const GITHUB_OAUTH_CLIENT_ID = IS_PROD
? '208a2e8684d88883eded'
: 'ed3e924f4a599313c83b';
const CALLBACK_BASE = IS_PROD
? 'https://spectrum.chat'
: 'http://localhost:3001';
const isSerializedJSON = (str: string) =>
str[0] === '{' && str[str.length - 1] === '}';
const init = () => {
// Setup use serialization
passport.serializeUser((user, done) => {
done(null, typeof user === 'string' ? user : JSON.stringify(user));
});
// NOTE(@mxstbr): `data` used to be just the userID, but is now the full user data
// to avoid having to go to the db on every single request. We have to handle both
// cases here, as more and more users use Spectrum again we go to the db less and less
passport.deserializeUser((data, done) => {
// Fast path: we got the full user data in the cookie
if (isSerializedJSON(data)) {
let user;
// Ignore errors if our isSerializedJSON heuristic is wrong and `data` isn't serialized JSON
try {
user = JSON.parse(data);
} catch (err) {}
if (user && user.id && user.createdAt) {
return done(null, user);
}
}
// Slow path: data is just the userID (legacy), so we have to go to the db to get the full data
return getUserById(data)
.then(user => {
done(null, user);
})
.catch(err => {
done(err);
});
});
// Set up Twitter login
passport.use(
new TwitterStrategy(
{
consumerKey: TWITTER_OAUTH_CLIENT_ID,
consumerSecret: TWITTER_OAUTH_CLIENT_SECRET,
callbackURL: `${CALLBACK_BASE}/auth/twitter/callback`,
includeEmail: true,
},
(token, tokenSecret, profile, done) => {
const name =
profile.displayName ||
profile._json.name ||
profile._json.screen_name ||
profile.username ||
'';
const user = {
providerId: profile.id,
fbProviderId: null,
googleProviderId: null,
githubProviderId: null,
username: null,
name: name,
email:
(profile.emails &&
profile.emails.length > 0 &&
profile.emails[0].value) ||
null,
profilePhoto:
(profile.photos &&
profile.photos.length > 0 &&
profile.photos[0].value) ||
null,
coverPhoto: profile._json.profile_background_image_url_https
? profile._json.profile_background_image_url_https
: null,
description:
profile._json.description && profile._json.description.length > 0
? profile._json.description
: '',
website:
profile._json.entities.url &&
profile._json.entities.url.urls &&
profile._json.entities.url.urls.length > 0
? profile._json.entities.url.urls[0].expanded_url
: '',
};
return createOrFindUser(user, 'providerId')
.then(user => {
done(null, user);
return user;
})
.catch(err => {
return done(null, err, {
message: 'Please sign in with GitHub to create a new account.',
});
});
}
)
);
// Set up Facebook login
passport.use(
new FacebookStrategy(
{
clientID: FACEBOOK_OAUTH_CLIENT_ID,
clientSecret: FACEBOOK_OAUTH_CLIENT_SECRET,
callbackURL: `${CALLBACK_BASE}/auth/facebook/callback`,
profileFields: [
'id',
'displayName',
'email',
'photos',
'about',
'cover',
'first_name',
'last_name',
'website',
],
},
(token, tokenSecret, profile, done) => {
const user = {
providerId: null,
fbProviderId: profile.id,
googleProviderId: null,
githubProviderId: null,
username: null,
name: profile.displayName,
firstName:
profile.name && profile.name.givenName
? profile.name.givenName
: '',
lastName:
profile.name && profile.name.familyName
? profile.name.familyName
: '',
description: profile.about ? profile.about : '',
website: profile.website ? profile.website : '',
email:
profile.emails &&
profile.emails.length > 0 &&
profile.emails[0].value !== undefined
? profile.emails[0].value
: null,
profilePhoto:
profile.photos &&
profile.photos.length > 0 &&
profile.photos[0].value !== undefined
? profile.photos[0].value
: null,
coverPhoto: profile._json.cover ? profile._json.cover.source : '',
};
return createOrFindUser(user, 'fbProviderId')
.then(user => {
done(null, user);
return user;
})
.catch(err => {
return done(null, err, {
message: 'Please sign in with GitHub to create a new account.',
});
});
}
)
);
// Set up Google login
passport.use(
new GoogleStrategy(
{
clientID: GOOGLE_OAUTH_CLIENT_ID,
clientSecret: GOOGLE_OAUTH_CLIENT_SECRET,
callbackURL: `${CALLBACK_BASE}/auth/google/callback`,
},
(token, tokenSecret, profile, done) => {
const name =
profile.displayName || profile.name
? `${profile.name.givenName} ${profile.name.familyName}`
: '';
const user = {
providerId: null,
fbProviderId: null,
googleProviderId: profile.id,
githubProviderId: null,
username: null,
name: name,
firstName:
profile.name && profile.name.givenName
? profile.name.givenName
: '',
lastName:
profile.name && profile.name.familyName
? profile.name.familyName
: '',
description: profile.tagline ? profile.tagline : '',
email:
(profile.emails &&
profile.emails.length > 0 &&
profile.emails[0].value) ||
null,
profilePhoto:
(profile.photos &&
profile.photos.length > 0 &&
profile.photos[0].value) ||
null,
coverPhoto:
profile._json.cover &&
profile._json.cover.coverPhoto &&
profile._json.cover.coverPhoto.url
? profile._json.cover.coverPhoto.url
: '',
website:
profile._json.urls && profile._json.urls.length > 0
? profile._json.urls[0].value
: '',
};
return createOrFindUser(user, 'googleProviderId')
.then(user => {
done(null, user);
return user;
})
.catch(err => {
return done(null, err, {
message: 'Please sign in with GitHub to create a new account.',
});
});
}
)
);
// Set up GitHub login
passport.use(
new GitHubStrategy(
{
clientID: GITHUB_OAUTH_CLIENT_ID,
clientSecret: GITHUB_OAUTH_CLIENT_SECRET,
callbackURL: `${CALLBACK_BASE}/auth/github/callback`,
scope: ['user'],
passReqToCallback: true,
},
async (req, token, tokenSecret, profile, done) => {
const name =
profile.displayName || profile.username || profile._json.name || '';
const splitProfileUrl = profile.profileUrl.split('/');
const fallbackUsername = splitProfileUrl[splitProfileUrl.length - 1];
const githubUsername =
profile.username || profile._json.login || fallbackUsername;
const existingUserWithProviderId = await getUserByIndex(
'githubProviderId',
profile.id
);
if (req.user) {
// if a user exists in the request body, it means the user is already
// authed and is trying to connect a github account. Before we do so
// we need to make sure that:
// 1. The user doesn't have an existing githubProviderId on their user
// 2. The providerId returned from GitHub isnt' being used by another user
// 1
if (req.user.githubProviderId) {
/*
Update the cached content of the github profile that we store
in redis for the graphql resolver. This allows us to put a button
on the client for a user to re-connect a github profile from
the web app which will update the cache with any changed usernames
*/
if (
!req.user.githubUsername ||
req.user.githubUsername !== githubUsername
) {
return saveUserProvider(
req.user.id,
'githubProviderId',
profile.id,
{ githubUsername: githubUsername }
)
.then(user => {
done(null, user);
return user;
})
.catch(err => {
done(err);
return null;
});
}
return done(null, req.user);
}
// 2
// if no user exists with this provider id, it's safe to save on the req.user's object
if (!existingUserWithProviderId) {
return saveUserProvider(
req.user.id,
'githubProviderId',
profile.id,
{ githubUsername: githubUsername }
)
.then(user => {
done(null, user);
return user;
})
.catch(err => {
done(err);
return null;
});
}
// if a user exists with this provider id, don't do anything and return
if (existingUserWithProviderId) {
return done(null, req.user, {
message:
'Your GitHub account is already linked to another Spectrum profile.',
});
}
}
const user = {
providerId: null,
fbProviderId: null,
googleProviderId: null,
githubProviderId: profile.id,
githubUsername: githubUsername,
username: null,
name: name,
description: profile._json.bio,
website: profile._json.blog,
email:
(profile.emails &&
profile.emails.length > 0 &&
profile.emails[0].value) ||
null,
profilePhoto:
(profile._json.avatar_url && profile._json.avatar_url) || null,
};
return createOrFindUser(user, 'githubProviderId')
.then(user => {
done(null, user);
return user;
})
.catch(err => {
done(err);
return null;
});
}
)
);
};
module.exports = {
init,
};
================================================
FILE: api/index.js
================================================
// @flow
/**
* The entry point for the server, this is where everything starts
*/
const compression = require('compression');
const debug = require('debug')('api');
debug('Server starting...');
debug('logging with debug enabled!');
import { createServer } from 'http';
import express from 'express';
import Raven from 'shared/raven';
import toobusy from 'shared/middlewares/toobusy';
import addSecurityMiddleware from 'shared/middlewares/security';
import csrf from 'shared/middlewares/csrf';
import statsd from 'shared/middlewares/statsd';
import { init as initPassport } from './authentication.js';
import apolloServer from './apollo-server';
import { corsOptions } from 'shared/middlewares/cors';
import errorHandler from 'shared/middlewares/error-handler';
import middlewares from './routes/middlewares';
import authRoutes from './routes/auth';
import apiRoutes from './routes/api';
import type { DBUser } from 'shared/types';
import type { Loader } from './loaders/types';
export type GraphQLContext = {
user: DBUser,
updateCookieUserData: (data: DBUser) => Promise<void>,
loaders: {
[key: string]: Loader,
},
};
const PORT = process.env.PORT ? parseInt(process.env.PORT, 10) : 3001;
initPassport();
const app = express();
// Instantiate the statsd middleware as soon as possible to get accurate time tracking
app.use(statsd);
// Trust the now proxy
app.set('trust proxy', true);
app.use(toobusy);
// Security middleware.
addSecurityMiddleware(app, { enableNonce: false, enableCSP: false });
if (process.env.NODE_ENV === 'production' && !process.env.FORCE_DEV) {
app.use(csrf);
}
// All other middlewares
app.use(compression());
app.use(middlewares);
// Routes
app.use('/auth', authRoutes);
app.use('/api', apiRoutes);
// GraphQL middleware
apolloServer.applyMiddleware({ app, path: '/api', cors: corsOptions });
// Redirect a request to the root path to the main app
app.use('/', (req: express$Request, res: express$Response) => {
res.redirect(
process.env.NODE_ENV === 'production' && !process.env.FORCE_DEV
? 'https://spectrum.chat'
: 'http://localhost:3000'
);
});
// $FlowIssue
app.use(errorHandler);
// We need to create a separate HTTP server to handle GraphQL subscriptions via websockets
const httpServer = createServer(app);
apolloServer.installSubscriptionHandlers(httpServer);
httpServer.listen(PORT);
debug(`GraphQL API running at http://localhost:${PORT}/api`);
process.on('unhandledRejection', async err => {
console.error('Unhandled rejection', err);
try {
await new Promise(resolve => Raven.captureException(err, resolve));
} catch (err) {
console.error('Raven error', err);
} finally {
process.exit(1);
}
});
process.on('uncaughtException', async err => {
console.error('Uncaught exception', err);
try {
await new Promise(resolve => Raven.captureException(err, resolve));
} catch (err) {
console.error('Raven error', err);
} finally {
process.exit(1);
}
});
================================================
FILE: api/loaders/channel.js
================================================
// @flow
import { getChannels, getChannelsThreadCounts } from '../models/channel';
import { getChannelsSettings } from '../models/channelSettings';
import createLoader from './create-loader';
export const __createChannelLoader = createLoader(channels =>
getChannels(channels)
);
export const __createChannelThreadCountLoader = createLoader(
channels => getChannelsThreadCounts(channels),
'group'
);
export const __createChannelSettingsLoader = createLoader(
channelIds => getChannelsSettings(channelIds),
key => key.channelId
);
export default () => {
throw new Error(
'⚠️ Do not import loaders directly, get them from the GraphQL context instead! ⚠️'
);
};
================================================
FILE: api/loaders/community.js
================================================
// @flow
import {
getCommunities,
getCommunitiesBySlug,
getCommunitiesChannelCounts,
getCommunitiesMemberCounts,
} from '../models/community';
import { getCommunitiesSettings } from '../models/communitySettings';
import createLoader from './create-loader';
export const __createCommunityLoader = createLoader(communities =>
getCommunities(communities)
);
export const __createCommunityBySlugLoader = createLoader(
communities => getCommunitiesBySlug(communities),
'slug'
);
export const __createCommunityMemberCountLoader = createLoader(
communityIds => getCommunitiesMemberCounts(communityIds),
'group'
);
export const __createCommunityChannelCountLoader = createLoader(
communityIds => getCommunitiesChannelCounts(communityIds),
'group'
);
export const __createCommunitySettingsLoader = createLoader(
communityIds => getCommunitiesSettings(communityIds),
key => key.communityId
);
export default () => {
throw new Error(
'⚠️ Do not import loaders directly, get them from the GraphQL context instead! ⚠️'
);
};
================================================
FILE: api/loaders/create-loader.js
================================================
// @flow
import DataLoader from 'dataloader';
import unique from 'shared/unique-elements';
import type { Loader, DataLoaderOptions } from './types';
/**
* Create a dataloader instance for a request and type
*
* Usage:
* createUserLoader = () => createLoader(users => getUsers(users), 'id');
*/
const createLoader = (
batchFn: Function,
indexField: string | Function = 'id',
cacheKeyFn: Function = key => key
) => (options?: DataLoaderOptions): Loader => {
return new DataLoader(keys => {
return batchFn(unique(keys)).then(
normalizeRethinkDbResults(keys, indexField, cacheKeyFn)
);
}, options);
};
// These helper functions were taken from the DataLoader docs
// https://github.com/facebook/dataloader/blob/master/examples/RethinkDB.md
function indexResults(results, indexField, cacheKeyFn) {
var indexedResults = new Map();
results.filter(Boolean).forEach(res => {
const key =
typeof indexField === 'function' ? indexField(res) : res[indexField];
indexedResults.set(cacheKeyFn(key), res);
});
return indexedResults;
}
function normalizeRethinkDbResults(keys, indexField, cacheKeyFn) {
return results => {
var indexedResults = indexResults(results, indexField, cacheKeyFn);
return keys.map(val => indexedResults.get(cacheKeyFn(val)) || null);
};
}
export default createLoader;
================================================
FILE: api/loaders/directMessageThread.js
================================================
// @flow
import { getDirectMessageThreads } from '../models/directMessageThread';
import { getMembersInDirectMessageThreads } from '../models/usersDirectMessageThreads';
import { getLastMessageOfThreads } from '../models/message';
import createLoader from './create-loader';
import type { Loader } from './types';
export const __createDirectMessageThreadLoader = createLoader(threads =>
getDirectMessageThreads(threads)
);
export const __createDirectMessageParticipantsLoader = createLoader(
threads => getMembersInDirectMessageThreads(threads),
'group'
);
export const __createDirectMessageSnippetLoader = createLoader(
threads => getLastMessageOfThreads(threads),
'threadId'
);
export default () => {
throw new Error(
'⚠️ Do not import loaders directly, get them from the GraphQL context instead! ⚠️'
);
};
================================================
FILE: api/loaders/index.js
================================================
// @flow
import {
__createUserLoader,
__createUserByUsernameLoader,
__createUserThreadCountLoader,
__createUserPermissionsInCommunityLoader,
__createUserPermissionsInChannelLoader,
} from './user';
import {
__createThreadLoader,
__createThreadParticipantsLoader,
} from './thread';
import {
__createChannelLoader,
__createChannelThreadCountLoader,
__createChannelSettingsLoader,
} from './channel';
import {
__createCommunityLoader,
__createCommunityBySlugLoader,
__createCommunityChannelCountLoader,
__createCommunitySettingsLoader,
__createCommunityMemberCountLoader,
} from './community';
import {
__createDirectMessageThreadLoader,
__createDirectMessageParticipantsLoader,
__createDirectMessageSnippetLoader,
} from './directMessageThread';
import {
__createReactionLoader,
__createSingleReactionLoader,
} from './reaction';
import { __createThreadReactionLoader } from './threadReaction';
import { __createMessageLoader } from './message';
import type { DataLoaderOptions } from './types';
// Create all the necessary loaders to be attached to the GraphQL context for each request
const createLoaders = (options?: DataLoaderOptions) => ({
user: __createUserLoader(options),
userByUsername: __createUserByUsernameLoader(options),
userThreadCount: __createUserThreadCountLoader(options),
userPermissionsInCommunity: __createUserPermissionsInCommunityLoader(options),
userPermissionsInChannel: __createUserPermissionsInChannelLoader(options),
thread: __createThreadLoader(options),
threadParticipants: __createThreadParticipantsLoader(options),
channel: __createChannelLoader(options),
channelThreadCount: __createChannelThreadCountLoader(options),
channelSettings: __createChannelSettingsLoader(options),
community: __createCommunityLoader(options),
communityBySlug: __createCommunityBySlugLoader(options),
communityChannelCount: __createCommunityChannelCountLoader(options),
communityMemberCount: __createCommunityMemberCountLoader(options),
communitySettings: __createCommunitySettingsLoader(options),
directMessageThread: __createDirectMessageThreadLoader(options),
directMessageParticipants: __createDirectMessageParticipantsLoader(options),
directMessageSnippet: __createDirectMessageSnippetLoader(options),
message: __createMessageLoader(options),
messageReaction: __createReactionLoader(options),
threadReaction: __createThreadReactionLoader(options),
reaction: __createSingleReactionLoader(options),
});
export default createLoaders;
================================================
FILE: api/loaders/message.js
================================================
// @flow
import { getManyMessages } from '../models/message';
import createLoader from './create-loader';
import type { Loader } from './types';
export const __createMessageLoader = createLoader((messages: string[]) =>
getManyMessages(messages)
);
export default () => {
throw new Error(
'⚠️ Do not import loaders directly, get them from the GraphQL context instead! ⚠️'
);
};
================================================
FILE: api/loaders/reaction.js
================================================
// @flow
import { getReactions, getReactionsByIds } from '../models/reaction';
import createLoader from './create-loader';
export const __createReactionLoader = createLoader(
messageIds => getReactions(messageIds),
'group'
);
export const __createSingleReactionLoader = createLoader(reactionIds =>
getReactionsByIds(reactionIds)
);
export default () => {
throw new Error(
'⚠️ Do not import loaders directly, get them from the GraphQL context instead! ⚠️'
);
};
================================================
FILE: api/loaders/thread.js
================================================
// @flow
import { getThreads } from '../models/thread';
import { getParticipantsInThreads } from '../models/usersThreads';
import createLoader from './create-loader';
export const __createThreadLoader = createLoader(threads =>
getThreads(threads)
);
export const __createThreadParticipantsLoader = createLoader(
threadIds => getParticipantsInThreads(threadIds),
'group'
);
export default () => {
throw new Error(
'⚠️ Do not import loaders directly, get them from the GraphQL context instead! ⚠️'
);
};
================================================
FILE: api/loaders/threadReaction.js
================================================
// @flow
import { getThreadReactions } from '../models/threadReaction';
import createLoader from './create-loader';
export const __createThreadReactionLoader = createLoader(
threadIds => getThreadReactions(threadIds),
'group'
);
export default () => {
throw new Error(
'⚠️ Do not import loaders directly, get them from the GraphQL context instead! ⚠️'
);
};
================================================
FILE: api/loaders/types.js
================================================
// @flow
export type Loader = {
load: (key: string | Array<string>) => Promise<any>,
loadMany: (keys: Array<*>) => Promise<any>,
clear: (key: string | Array<string>) => void,
};
export type DataLoaderOptions = {
cache?: boolean,
};
================================================
FILE: api/loaders/user.js
================================================
// @flow
import {
getUsers,
getUsersThreadCount,
getUsersByUsername,
} from 'shared/db/queries/user';
import { getUsersPermissionsInCommunities } from '../models/usersCommunities';
import { getUsersPermissionsInChannels } from '../models/usersChannels';
import createLoader from './create-loader';
export const __createUserLoader = createLoader(users => getUsers(users), 'id');
export const __createUserByUsernameLoader = createLoader(
users => getUsersByUsername(users),
'username'
);
export const __createUserThreadCountLoader = createLoader(
users => getUsersThreadCount(users),
'id'
);
export const __createUserPermissionsInCommunityLoader = createLoader(
usersCommunities => getUsersPermissionsInCommunities(usersCommunities),
input => `${input.userId}|${input.communityId}`,
key => (Array.isArray(key) ? `${key[0]}|${key[1]}` : key)
);
export const __createUserPermissionsInChannelLoader = createLoader(
usersChannels => getUsersPermissionsInChannels(usersChannels),
input => `${input.userId}|${input.channelId}`,
key => (Array.isArray(key) ? `${key[0]}|${key[1]}` : key)
);
export default () => {
throw new Error(
'⚠️ Do not import loaders directly, get them from the GraphQL context instead! ⚠️'
);
};
================================================
FILE: api/migrations/20170410074258-initial-data.js
================================================
exports.up = function(r, conn) {
return (
Promise.all([
r
.tableCreate('threads')
.run(conn)
.catch(err => {
console.log(err);
throw err;
}),
r
.tableCreate('channels')
.run(conn)
.catch(err => {
console.log(err);
throw err;
}),
r
.tableCreate('communities')
.run(conn)
.catch(err => {
console.log(err);
throw err;
}),
r
.tableCreate('messages')
.run(conn)
.catch(err => {
console.log(err);
throw err;
}),
r
.tableCreate('sessions')
.run(conn)
.catch(err => {
console.log(err);
throw err;
}),
r
.tableCreate('reactions')
.run(conn)
.catch(err => {
console.log(err);
throw err;
}),
r
.tableCreate('directMessageThreads')
.run(conn)
.catch(err => {
console.log(err);
throw err;
}),
r
.tableCreate('users')
.run(conn)
.catch(err => {
console.log(err);
throw err;
}),
r
.tableCreate('recurringPayments')
.run(conn)
.catch(err => {
console.log(err);
throw err;
}),
r
.tableCreate('invoices')
.run(conn)
.catch(err => {
console.log(err);
throw err;
}),
r
.tableCreate('usersCommunities')
.run(conn)
.catch(err => {
console.log(err);
throw err;
}),
r
.tableCreate('usersChannels')
.run(conn)
.catch(err => {
console.log(err);
throw err;
}),
r
.tableCreate('usersDirectMessageThreads')
.run(conn)
.catch(err => {
console.log(err);
throw err;
}),
])
// Create secondary indexes
.then(() =>
Promise.all([
// index user by username
r
.table('users')
.indexCreate('username', r.row('username'))
.run(conn)
.catch(err => {
console.log(err);
throw err;
}),
// index recurringPayments by userId
r
.table('recurringPayments')
.indexCreate('userId', r.row('userId'))
.run(conn)
.catch(err => {
console.log(err);
throw err;
}),
// index invoices by communityId
r
.table('invoices')
.indexCreate('communityId', r.row('communityId'))
.run(conn)
.catch(err => {
console.log(err);
throw err;
}),
// indexes on usersCommunities join table
r
.table('usersCommunities')
.indexCreate('userId', r.row('userId'))
.run(conn)
.catch(err => {
console.log(err);
throw err;
}),
r
.table('usersCommunities')
.indexCreate('communityId', r.row('communityId'))
.run(conn)
.catch(err => {
console.log(err);
throw err;
}),
// indexes on usersChannels join table
r
.table('usersChannels')
.indexCreate('userId', r.row('userId'))
.run(conn)
.catch(err => {
console.log(err);
throw err;
}),
r
.table('usersChannels')
.indexCreate('channelId', r.row('channelId'))
.run(conn)
.catch(err => {
console.log(err);
throw err;
}),
// indexes on usersDirectMessageThreads join table
r
.table('usersDirectMessageThreads')
.indexCreate('userId', r.row('userId'))
.run(conn)
.catch(err => {
console.log(err);
throw err;
}),
r
.table('usersDirectMessageThreads')
.indexCreate('threadId', r.row('threadId'))
.run(conn)
.catch(err => {
console.log(err);
throw err;
}),
// index direct message threads by the users
r
.table('directMessageThreads')
.indexCreate('participants', { multi: true })
.run(conn)
.catch(err => {
console.log(err);
throw err;
}),
// index threads by creator
r
.table('threads')
.indexCreate('creatorId', r.row('creatorId'))
.run(conn)
.catch(err => {
console.log(err);
throw err;
}),
// index threads by channelId
r
.table('threads')
.indexCreate('channelId', r.row('channelId'))
.run(conn)
.catch(err => {
console.log(err);
throw err;
}),
// index threads by communityId
r
.table('threads')
.indexCreate('communityId', r.row('communityId'))
.run(conn)
.catch(err => {
console.log(err);
throw err;
}),
// index threads by lastActive
r
.table('threads')
.indexCreate('lastActive', r.row('lastActive'))
.run(conn)
.catch(err => {
console.log(err);
throw err;
}),
// index reactions by message
r
.table('reactions')
.indexCreate('messageId', r.row('messageId'))
.run(conn)
.catch(err => {
console.log(err);
throw err;
}),
// index channels by communityId
r
.table('channels')
.indexCreate('communityId', r.row('communityId'))
.run(conn)
.catch(err => {
console.log(err);
throw err;
}),
// index messages by thread
r
.table('messages')
.indexCreate('threadId', r.row('threadId'))
.run(conn)
.catch(err => {
console.log(err);
throw err;
}),
])
)
.catch(err => {
console.log(err);
throw err;
})
);
};
exports.down = function(r, conn) {
return Promise.all([
r
.tableDrop('threads')
.run(conn)
.catch(err => {
console.log(err);
throw err;
}),
r
.tableDrop('channels')
.run(conn)
.catch(err => {
console.log(err);
throw err;
}),
r
.tableDrop('communities')
.run(conn)
.catch(err => {
console.log(err);
throw err;
}),
r
.tableDrop('messages')
.run(conn)
.catch(err => {
console.log(err);
throw err;
}),
r
.tableDrop('sessions')
.run(conn)
.catch(err => {
console.log(err);
throw err;
}),
r
.tableDrop('users')
.run(conn)
.catch(err => {
console.log(err);
throw err;
}),
r
.tableDrop('directMessageThreads')
.run(conn)
.catch(err => {
console.log(err);
throw err;
}),
r
.tableDrop('reactions')
.run(conn)
.catch(err => {
console.log(err);
throw err;
}),
r
.tableDrop('recurringPayments')
.run(conn)
.catch(err => {
console.log(err);
throw err;
}),
r
.tableDrop('invoices')
.run(conn)
.catch(err => {
console.log(err);
throw err;
}),
r
.tableDrop('usersCommunities')
.run(conn)
.catch(err => {
console.log(err);
throw err;
}),
r
.tableDrop('usersChannels')
.run(conn)
.catch(err => {
console.log(err);
throw err;
}),
r
.tableDrop('usersDirectMessageThreads')
.run(conn)
.catch(err => {
console.log(err);
throw err;
}),
]).catch(err => {
console.log(err);
throw err;
});
};
================================================
FILE: api/migrations/20170613200350-notifications.js
================================================
exports.up = function(r, conn) {
return (
// Create new tables, update old ones with receiveNotifications
Promise.all([
r
.table('usersChannels')
.filter({ isBlocked: false })
.update({ receiveNotifications: true })
.run(conn)
.catch(err => {
console.log(err);
throw err;
}),
r
.table('usersCommunities')
.filter({ isBlocked: false })
.update({ receiveNotifications: true })
.run(conn)
.catch(err => {
console.log(err);
throw err;
}),
r
.tableCreate('notifications')
.run(conn)
.catch(err => {
console.log(err);
throw err;
}),
r
.tableCreate('usersThreads')
.run(conn)
.catch(err => {
console.log(err);
throw err;
}),
r
.tableCreate('usersNotifications')
.run(conn)
.catch(err => {
console.log(err);
throw err;
}),
])
// Add secondary indexes to new tables
.then(() =>
Promise.all([
r
.table('notifications')
.indexCreate('modifiedAt', r.row('modifiedAt'))
.run(conn)
.catch(err => {
console.log(err);
throw err;
}),
r
.table('usersThreads')
.indexCreate('userId')
.run(conn)
.catch(err => {
console.log(err);
throw err;
}),
r
.table('usersThreads')
.indexCreate('threadId')
.run(conn)
.catch(err => {
console.log(err);
throw err;
}),
r
.table('usersNotifications')
.indexCreate('userId')
.run(conn)
.catch(err => {
console.log(err);
throw err;
}),
r
.table('notifications')
.indexCreate('contextId', r.row('context')('id'))
.run(conn)
.catch(err => {
console.log(err);
throw err;
}),
r
.table('usersNotifications')
.indexCreate('notificationId', r.row('notificationId'))
.run(conn)
.catch(err => {
console.log(err);
throw err;
}),
])
)
// Insert data into new tables that should be there
.then(() =>
Promise.all([
// Thread creators -> usersThreads
r
.table('threads')
.filter(r.row.hasFields('deletedAt').not())
.run(conn)
.then(cursor => cursor.toArray())
.then(threads =>
Promise.all(
threads.map(thread =>
r
.table('usersThreads')
.insert({
createdAt: r.now(),
isParticipant: true,
receiveNotifications: true,
threadId: thread.id,
userId: thread.creatorId,
})
.run(conn)
.catch(err => {
console.log(err);
throw err;
})
)
)
)
.catch(err => {
console.log(err);
throw err;
}),
// Thread participants -> usersThreads
r
.table('threads')
.filter(r.row.hasFields('deletedAt').not())
.run(conn)
.catch(err => {
console.log(err);
throw err;
})
.then(cursor => cursor.toArray())
.then(threads =>
Promise.all(
threads.map(thread => {
return Promise.all([
r
.table('messages')
.filter({ threadId: thread.id })
.map(message => message('senderId'))
.run(conn)
.then(cursor => cursor.toArray())
.catch(err => {
console.log(err);
throw err;
}),
thread,
]);
})
)
)
.then(threadsMessages =>
Promise.all(
threadsMessages.map(([threadSenders, thread]) => {
if (!threadSenders) return Promise.resolve();
let uniqueThreadSenders = threadSenders.filter(
(item, i, ar) => ar.indexOf(item) === i
);
return Promise.all(
uniqueThreadSenders.map(sender => {
return r
.table('usersThreads')
.insert({
createdAt: r.now(),
isParticipant: true,
receiveNotifications: true,
threadId: thread.id,
userId: sender,
})
.run(conn)
.catch(err => {
console.log(err);
throw err;
});
})
);
})
)
),
])
)
.catch(err => {
console.log(err);
throw err;
})
);
};
exports.down = function(r, conn) {
return Promise.all([
r.tableDrop('usersThreads').run(conn),
r.tableDrop('usersNotifications').run(conn),
r.tableDrop('notifications').run(conn),
r
.table('usersCommunities')
.update({ receiveNotifications: r.literal() })
.run(conn),
r
.table('usersCommunities')
.update({ receiveNotifications: r.literal() })
.run(conn),
]);
};
================================================
FILE: api/migrations/20170616113103-compound-indexes-for-ordering.js
================================================
exports.up = function(r, conn) {
return Promise.all([
// messages#threadIdAndTimestamp
r
.table('messages')
.indexCreate('threadIdAndTimestamp', [
r.row('threadId'),
r.row('timestamp'),
])
.run(conn),
// threads#channelIdAndLastActive
r
.table('threads')
.indexCreate('channelIdAndLastActive', [
r.row('channelId'),
r.row('lastActive'),
])
.run(conn),
// threads#communityIdAndLastActive
r
.table('threads')
.indexCreate('communityIdAndLastActive', [
r.row('communityId'),
r.row('lastActive'),
])
.run(conn),
// community#createdAt
r
.table('communities')
.indexCreate('createdAt')
.run(conn),
]).catch(err => {
console.log(err);
throw err;
});
};
exports.down = function(r, conn) {
return Promise.resolve();
};
================================================
FILE: api/migrations/20170627104435-user-email-settings.js
================================================
exports.up = function(r, conn) {
return Promise.all([
r
.tableCreate('usersSettings')
.run(conn)
// Create secondary indexes
.then(() =>
// index user by username
r
.table('usersSettings')
.indexCreate('userId', r.row('userId'))
.run(conn)
)
.then(() =>
Promise.all([
// Get all user ids while we wait for the index to finish creating
r.table('users').withFields('id').map(user => user('id')).run(conn),
r.table('usersSettings').indexWait('userId').run(conn),
])
)
.then(([userIds]) => userIds.toArray())
.then(userIds =>
Promise.all(
userIds.map(id =>
r
.table('usersSettings')
.insert({
userId: id,
notifications: {
types: {
newMessageInThreads: {
email: true,
},
},
},
})
.run(conn)
)
)
)
.catch(err => {
console.log(err);
throw err;
}),
]);
};
exports.down = function(r, conn) {
return r.tableDrop('usersSettings').run(conn);
};
================================================
FILE: api/migrations/20170701173337-linkify-messages.js
================================================
const markdownLinkify = require('../utils/markdown-linkify');
exports.up = function(r, conn) {
return (
// Markdown linkify each message
r
.table('messages')
.withFields('id', 'content')
.run(conn)
.then(cursor => cursor.toArray())
.then(messages =>
Promise.all(
messages.map(message =>
r
.table('messages')
.get(message.id)
.update({
content: {
body: markdownLinkify(message.content.body),
},
})
.run(conn)
)
)
)
);
};
exports.down = function(r, conn) {
// Can't really undo just this change so we don't bother
return Promise.resolve();
};
================================================
FILE: api/migrations/20170702194221-fix-images.js
================================================
const MARKDOWN_LINK = /(?:\[(.*?)\]\((.*?)\))/g;
exports.up = function(r, conn) {
return (
// The last migration fucked images up in production
// so this one fixes them again 😅
r
.table('messages')
.filter({ messageType: 'media' })
.withFields('id', 'content')
.run(conn)
.then(cursor => cursor.toArray())
.then(messages =>
Promise.all(
messages.map(message =>
r
.table('messages')
.get(message.id)
.update({
content: {
body: MARKDOWN_LINK.test(message.content.body)
? message.content.body.replace(MARKDOWN_LINK, '$2')
: message.content.body,
},
})
.run(conn)
)
)
)
);
};
exports.down = function(r, conn) {
// Can't really undo just this change so we don't bother
return Promise.resolve();
};
================================================
FILE: api/migrations/20170706114239-providerfield-indexes.js
================================================
exports.up = function(r, conn) {
return Promise.all([
r
.table('users')
.indexCreate('providerId')
.run(conn),
r
.table('users')
.indexCreate('fbProviderId')
.run(conn),
r
.table('users')
.indexCreate('googleProviderId')
.run(conn),
]).catch(err => {
console.log(err);
throw err;
});
};
exports.down = function(r, conn) {
return Promise.resolve();
};
================================================
FILE: api/migrations/20170706205658-slack-import.js
================================================
exports.up = function(r, conn) {
return Promise.all([
r
.tableCreate('slackImports')
.run(conn)
.catch(err => {
console.log(err);
throw err;
}),
])
.then(() =>
Promise.all([
r
.table('slackImports')
.indexCreate('communityId', r.row('communityId'))
.run(conn)
.catch(err => {
console.log(err);
throw err;
}),
r
.table('users')
.indexCreate('email', r.row('email'))
.run(conn),
])
)
.catch(err => {
console.log(err);
throw err;
});
};
exports.down = function(r, conn) {
return Promise.all([r.tableDrop('slackImports').run(conn)]);
};
================================================
FILE: api/migrations/20170714171920-web-push-subscription.js
================================================
exports.up = function(r, conn) {
return r
.tableCreate('webPushSubscriptions')
.run(conn)
.catch(err => {
throw new Error(err);
})
.then(() =>
r
.table('webPushSubscriptions')
.indexCreate('userId')
.run(conn)
)
.then(() =>
r
.table('webPushSubscriptions')
.indexCreate('endpoint')
.run(conn)
);
};
exports.down = function(r, conn) {
return r
.tableDrop('webPushSubscriptions')
.run(conn)
.catch(err => {
throw new Error(err);
});
};
================================================
FILE: api/migrations/20170724184557-notifications-entity-added-index.js
================================================
exports.up = function(r, conn) {
return Promise.all([
r
.table('usersNotifications')
.indexCreate('userIdAndEntityAddedAt', [
r.row('userId'),
r.row('entityAddedAt'),
])
.run(conn),
]).catch(err => {
console.log(err);
throw err;
});
};
exports.down = function(r, conn) {
return Promise.resolve();
};
================================================
FILE: api/migrations/20170803104302-dedupe-users-settings.js
================================================
const without = require('lodash/without');
// This is taken from the Babel REPL and is what it transpiles ...arr to.
// (can't use rest/spread in Node yet)
function _toConsumableArray(arr) {
if (Array.isArray(arr)) {
for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) {
arr2[i] = arr[i];
}
return arr2;
} else {
return Array.from(arr);
}
}
exports.up = function(r, conn) {
// Get every UserID that has opted out of new message in threads emails
return (
r
.table('usersSettings')
.filter({
notifications: {
types: {
newMessageInThreads: {
email: false,
},
},
},
})
.map(rec => rec('userId'))
.distinct()
.run(conn)
.then(cursor => cursor.toArray())
.then(optedOut => {
return Promise.all([
optedOut,
r
.table('usersSettings')
.filter({
notifications: {
types: {
newMessageInThreads: {
email: true,
},
},
},
})
.map(rec => rec('userId'))
.distinct()
.run(conn)
.then(cursor => cursor.toArray()),
]);
})
.then(([optedOut, optedIn]) => {
return [
optedOut,
// Remove userIds that appear in optedOut from optedIn
without.apply(
undefined,
[optedIn].concat(_toConsumableArray(optedOut))
),
];
})
// Drop the usersSettings table
.then(([optedOut, optedIn]) => {
return Promise.all([
optedOut,
optedIn,
r.table('usersSettings').delete().run(conn),
]);
})
.then(([optedOut, optedIn]) => {
// Insert records of users that have opted out
const optedOutInsertions = optedOut.map(userId =>
r
.table('usersSettings')
.insert({
userId,
notifications: {
types: {
newMessageInThreads: {
email: false,
},
newThreadCreated: {
email: true,
},
},
},
})
.run(conn)
);
// Insert records of users that have opted in
const optedInInsertions = optedIn.map(userId =>
r
.table('usersSettings')
.insert({
userId,
notifications: {
types: {
newMessageInThreads: {
email: true,
},
newThreadCreated: {
email: true,
},
},
},
})
.run(conn)
);
return Promise.all(
[].concat(
_toConsumableArray(optedOutInsertions),
_toConsumableArray(optedInInsertions)
)
);
})
);
};
exports.down = function(r, conn) {
return Promise.resolve();
};
================================================
FILE: api/migrations/20170825220615-clean-recurring-payments.js
================================================
exports.up = function(r, conn) {
// get all recurring payments
return r
.table('recurringPayments')
.run(conn)
.then(cursor => cursor.toArray())
.then(subscriptions => {
// for each subscription record, map it and create a new object with a cleaner data model
return subscriptions.map(subscription => {
if (!subscription.stripeData) return subscription;
return Object.assign(
{},
{
id: subscription.id,
// userId is used to keep track of who is making payments for any subscription it is also queries against to determine if a user isPro
userId: subscription.userId,
// communityId is queried against to determine if a community isPro
communityId: subscription.communityId,
customerId: subscription.stripeData.customer,
subscriptionId: subscription.stripeData.id,
// planId and planName will determine if the user is paying for a Pro user account or for an upgraded community
planId: subscription.stripeData.plan.id,
planName: subscription.stripeData.plan.name,
amount: subscription.stripeData.plan.amount,
// rather than having multiple plans on Stripe for every possible tier of upgraded community, we can instead just stack quantities - so an upgraded community with 9k members would have a single 'pro community' record with 9 quantity that determines how much they will be billed every month
// when rendering subscriptions in the client, the quantity should be multiplied by the amount to determine the total cost of the subscription
quantity: subscription.stripeData.quantity,
// will be used to determine the current state of an isPro query against a user or community. When a community or user gets downgraded this field will switch from 'active' to 'canceled'
status: subscription.stripeData.status,
// the following field dates will be used for client side rendering - we will need to implement webhooks to keep the currentPeriodStart and currentPeriodEnd timestamps up to date
currentPeriodStart: subscription.stripeData.current_period_start,
currentPeriodEnd: subscription.stripeData.current_period_end,
createdAt: subscription.stripeData.plan.created,
}
);
});
})
.then(cleanSubscriptions => {
return Promise.all([
cleanSubscriptions,
// delete all the old records in recurringPayments table
r
.table('recurringPayments')
.delete()
.run(conn),
// also create a new index against communityId for faster isPro lookups on communities
r
.table('recurringPayments')
.indexCreate('communityId')
.run(conn),
]);
})
.then(([cleanSubscriptions]) => {
// insert each new clean record into the table
return cleanSubscriptions.map(subscription => {
return r
.table('recurringPayments')
.insert({ ...subscription })
.run(conn);
});
});
};
exports.down = function(r, conn) {
return Promise.resolve();
};
================================================
FILE: api/migrations/20170829233734-userid-index-on-invoices.js
================================================
exports.up = function(r, conn) {
return Promise.all([
r
.table('invoices')
.indexCreate('userId')
.run(conn),
]).catch(err => {
console.log(err);
throw err;
});
};
exports.down = function(r, conn) {
return Promise.resolve();
};
================================================
FILE: api/migrations/20170831163211-invoice-data-model-update.js
================================================
exports.up = function(r, conn) {
// get all recurring payments
return r
.table('invoices')
.run(conn)
.then(cursor => cursor.toArray())
.then(invoices => {
// for each subscription record, map it and create a new object with a cleaner data model
return invoices.map(invoice => {
if (!invoice.stripeData) return invoice;
return Object.assign(
{},
{
id: invoice.id,
amount: invoice.amount,
chargeId: invoice.stripeData.id,
communityId: invoice.communityId,
customerId: null,
paidAt: invoice.stripeData.created,
planId: 'community-standard',
planName: 'Spectrum Standard Community',
quantity: 1,
sourceBrand: invoice.stripeData.source.brand,
sourceLast4: invoice.stripeData.source.last4,
status: 'paid',
subscriptionId: null,
userId: null,
}
);
});
})
.then(cleanInvoices => {
return Promise.all([
cleanInvoices,
// delete all the old records in recurringPayments table
r
.table('invoices')
.delete()
.run(conn),
]);
})
.then(([cleanInvoices]) => {
// insert each new clean record into the table
return cleanInvoices.map(invoice => {
return r
.table('invoices')
.insert(invoice)
.run(conn);
});
});
};
exports.down = function(r, conn) {
return Promise.resolve();
};
================================================
FILE: api/migrations/20170907222544-digest-email-notification-settings.js
================================================
exports.up = function(r, conn) {
return Promise.all([
r
.table('usersSettings')
.run(conn)
.then(cursor => cursor.toArray())
.then(settings => {
return settings.map(setting => {
return Object.assign(
{},
{
...setting,
notifications: {
types: {
newMessageInThreads: {
email: setting.notifications.types.newMessageInThreads
? setting.notifications.types.newMessageInThreads.email
: true,
},
newThreadCreated: {
email: setting.notifications.types.newThreadCreated
? setting.notifications.types.newThreadCreated.email
: true,
},
newDirectMessage: {
email: setting.notifications.types.newDirectMessage
? setting.notifications.types.newDirectMessage.email
: true,
},
weeklyDigest: {
email: true,
},
dailyDigest: {
email: true,
},
},
},
}
);
});
})
.then(newSettings => {
return Promise.all([
newSettings,
// delete all the old records
r
.table('usersSettings')
.delete()
.run(conn),
]);
})
.then(([newSettings]) => {
// insert each new clean record into the table
return newSettings.map(setting => {
return r
.table('usersSettings')
.insert({ ...setting })
.run(conn);
});
})
.catch(err => {
console.log(err);
throw err;
}),
]);
};
exports.down = function(r, conn) {
return Promise.resolve();
};
================================================
FILE: api/migrations/20170908230623-add-reputation-field-to-communities.js
================================================
exports.up = function(r, conn) {
return Promise.all([
r
.table('usersCommunities')
.update({
reputation: 1,
})
.run(conn)
.catch(err => {
console.log(err);
throw err;
}),
r
.tableCreate('reputationEvents')
.run(conn)
.catch(err => {
console.log(err);
throw err;
}),
]).then(() => {
// create a compound index to easily fetch all reputation events in chronological order
return Promise.all([
r
.table('reputationEvents')
.indexCreate('userIdAndTimestamp', [
r.row('userId'),
r.row('timestamp'),
])
.run(conn)
.catch(err => {
console.log(err);
throw err;
}),
]);
});
};
exports.down = function(r, conn) {
return r
.tableDrop('reputationEvents')
.run(conn)
.catch(err => {
console.log(err);
throw err;
});
};
================================================
FILE: api/migrations/20170912000619-backfill-rep.js
================================================
exports.up = function(r, conn) {
const updateReputation = (userId, communityId, score, type) => {
return r
.table('usersCommunities')
.getAll(userId, { index: 'userId' })
.filter({ communityId })
.update({
reputation: r.row('reputation').add(score),
})
.run(conn)
.then(() => {
return r
.table('reputationEvents')
.insert({
timestamp: new Date(),
userId,
type,
communityId,
score,
})
.run(conn);
});
};
return r
.table('threads')
.filter(row => row.hasFields('deletedAt').not())
.run(conn)
.then(cursor => cursor.toArray())
.then(threads => {
const threadPromises = threads.map(thread => {
return updateReputation(
thread.creatorId,
thread.communityId,
100,
'thread created'
);
});
return Promise.all(threadPromises);
});
};
exports.down = function(r, conn) {
return Promise.resolve();
};
================================================
FILE: api/migrations/20170915201609-clean-up-bad-dm-data.js
================================================
exports.up = function(r, conn) {
return Promise.all([
r
.table('messages')
.filter({ threadType: 'DIRECT_MESSAGE_GROUP' })
.update({
threadType: 'directMessageThread',
})
.run(conn),
r
.table('messages')
.filter({ threadType: 'STORY' })
.update({
threadType: 'story',
})
.run(conn),
]);
};
exports.down = function(r, conn) {
return Promise.resolve();
};
================================================
FILE: api/migrations/20170926003025-activate-daily-weekly-digest-settings.js
================================================
exports.up = function(r, conn) {
return Promise.all([
r
.table('usersSettings')
.update({
notifications: {
types: {
dailyDigest: {
email: true,
},
weeklyDigest: {
email: true,
},
},
},
})
.run(conn),
]);
};
exports.down = function(r, conn) {
return Promise.resolve();
};
================================================
FILE: api/migrations/20170926102527-speedy-gonzales.js
================================================
exports.up = function(r, conn) {
return Promise.all([
r
.table('usersCommunities')
.indexCreate('userIdAndCommunityId', [
r.row('userId'),
r.row('communityId'),
])
.run(conn),
]);
};
exports.down = function(r, conn) {
return Promise.resolve();
};
================================================
FILE: api/migrations/20170927002438-communityid-index-on-reputation-events.js
================================================
exports.up = function(r, conn) {
return Promise.all([
r
.table('reputationEvents')
.indexCreate('communityId')
.run(conn),
]);
};
exports.down = function(r, conn) {
return Promise.resolve();
};
================================================
FILE: api/migrations/20170928143435-slate-to-draftjs.js.js
================================================
const compose = require('redux/lib/compose').default;
const { convertToRaw, genKey } = require('draft-js');
const { stateFromMarkdown } = require('draft-js-import-markdown');
const isHtml = require('is-html');
const cheerio = require('cheerio');
const { toPlainText, toState } = require('../../shared/slate-utils');
const convertEmbeds = state => {
const entityMap = state.entityMap || {};
const blocks = state.blocks.map(block => {
if (block.type !== 'unstyled') return block;
if (!isHtml(block.text)) return block;
const $ = cheerio.load(block.text);
const iframe = $('iframe')[0];
if (!iframe) return block;
const src = $(iframe).attr('src');
const height = $(iframe).attr('height');
const width = $(iframe).attr('width');
if (!src) return block;
const keys = Object.keys(entityMap).map(key => parseInt(key, 10));
const lastKey = keys.sort()[keys.length - 1];
const newKey = lastKey === undefined ? 0 : lastKey + 1;
entityMap[newKey] = {
data: { src, height, width },
mutability: 'IMMUTABLE',
type: 'embed',
};
return {
data: {},
depth: 0,
entityRanges: [
{
key: newKey,
offset: 0,
length: 1,
},
],
inlineStyleRanges: [],
key: genKey(),
text: ' ',
type: 'atomic',
};
});
return {
...state,
entityMap,
blocks,
};
};
const plainToDraft = compose(
JSON.stringify,
convertEmbeds,
convertToRaw,
stateFromMarkdown
);
const slateToDraft = compose(plainToDraft, toPlainText, toState, JSON.parse);
exports.up = function(r, conn) {
return (
r
.table('threads')
.filter(thread =>
r.not(thread.hasFields('type')).or(thread('type').ne('DRAFTJS'))
)
.run(conn)
.then(cursor => cursor.toArray())
// Transform slate state to draftjs state
.then(threads =>
threads.map(thread =>
Object.assign({}, thread, {
type: 'DRAFTJS',
content: Object.assign({}, thread.content, {
body:
thread.type === 'SLATE'
? slateToDraft(thread.content.body)
: plainToDraft(thread.content.body),
}),
})
)
)
// Store the transformed threads
.then(threads =>
Promise.all(
threads.map(thread =>
r
.table('threads')
.get(thread.id)
.update(thread)
.run(conn)
)
)
)
);
};
exports.down = function(r, conn) {
// Not spending any time undoing this
return Promise.resolve();
};
================================================
FILE: api/migrations/20171005075445-remove-markdown-links-from-messages.js
================================================
const replace = require('string-replace-to-array');
const MARKDOWN_LINK = /(?:\[(.*?)\]\((.*?)\))/g;
const removeMarkdownLinks = text => {
if (!text || typeof text !== 'string') return text;
return replace(text, MARKDOWN_LINK, (fullLink, text, url) => url).join('');
};
exports.up = function(r, conn) {
return r
.table('messages')
.withFields('id', 'content')
.run(conn)
.then(cursor => cursor.toArray())
.then(messages =>
Promise.all(
messages.map(message =>
r
.table('messages')
.get(message.id)
.update({
content: {
body: removeMarkdownLinks(message.content.body),
},
})
.run(conn)
)
)
);
};
exports.down = function(r, conn) {
return Promise.resolve();
};
================================================
FILE: api/migrations/20171008101118-last-slate-to-draft.js
================================================
const compose = require('redux/lib/compose').default;
const { convertToRaw, genKey } = require('draft-js');
const { stateFromMarkdown } = require('draft-js-import-markdown');
const isHtml = require('is-html');
const cheerio = require('cheerio');
const { toPlainText, toState } = require('../../shared/slate-utils');
const convertEmbeds = state => {
const entityMap = state.entityMap || {};
const blocks = state.blocks.map(block => {
if (block.type !== 'unstyled') return block;
if (!isHtml(block.text)) return block;
const $ = cheerio.load(block.text);
const iframe = $('iframe')[0];
if (!iframe) return block;
const src = $(iframe).attr('src');
const height = $(iframe).attr('height');
const width = $(iframe).attr('width');
if (!src) return block;
const keys = Object.keys(entityMap).map(key => parseInt(key, 10));
const lastKey = keys.sort()[keys.length - 1];
const newKey = lastKey === undefined ? 0 : lastKey + 1;
entityMap[newKey] = {
data: { src, height, width },
mutability: 'IMMUTABLE',
type: 'embed',
};
return {
data: {},
depth: 0,
entityRanges: [
{
key: newKey,
offset: 0,
length: 1,
},
],
inlineStyleRanges: [],
key: genKey(),
text: ' ',
type: 'atomic',
};
});
return {
...state,
entityMap,
blocks,
};
};
const plainToDraft = compose(
JSON.stringify,
convertEmbeds,
convertToRaw,
stateFromMarkdown
);
const slateToDraft = compose(plainToDraft, toPlainText, toState, JSON.parse);
exports.up = function(r, conn) {
return (
r
.table('threads')
.filter({
type: 'SLATE',
})
.run(conn)
.then(cursor => cursor.toArray())
// Transform slate state to draftjs state
.then(threads =>
threads.map(thread =>
Object.assign({}, thread, {
type: 'DRAFTJS',
content: Object.assign({}, thread.content, {
body: slateToDraft(thread.content.body),
}),
})
)
)
// Store the transformed threads
.then(threads =>
Promise.all(
threads.map(thread =>
r
.table('threads')
.get(thread.id)
.update(thread)
.run(conn)
)
)
)
);
};
exports.down = function(r, conn) {
// Not spending any time undoing this
return Promise.resolve();
};
================================================
FILE: api/migrations/20171013195530-core-metrics-table.js
================================================
exports.up = function(r, conn) {
return Promise.all([
r
.tableCreate('coreMetrics')
.run(conn)
.catch(err => {
console.log(err);
throw err;
}),
])
.then(() => {
return r
.table('coreMetrics')
.indexCreate('date', r.row('date'))
.run(conn)
.catch(err => {
console.log(err);
throw err;
});
})
.then(() => {
const saveCoreMetrics = (data, index) => {
const d = new Date();
d.setDate(d.getDate() - index);
return r
.table('coreMetrics')
.insert({
date: d,
...data,
})
.run(conn);
};
const getCount = (table, filter, index, field) => {
if (filter) {
return r
.table(table)
.filter(filter)
.filter(
r.row(field).during(
// go back to all time, for some reason minval isn't working
r.now().sub(60 * 60 * 24 * 10000),
r.now().sub(60 * 60 * 24 * index)
)
)
.filter(row => r.not(row.hasFields('deletedAt')))
.count()
.run(conn);
}
return r
.table(table)
.filter(
r.row(field).during(
// go back to all time, for some reason minval isn't working
r.now().sub(60 * 60 * 24 * 10000),
r.now().sub(60 * 60 * 24 * index)
)
)
.filter(row => r.not(row.hasFields('deletedAt')))
.count()
.run(conn);
};
// cpu, tpu, mpu
const getPu = (table, field, index) => {
// get the count of the users within a given range
return getCount('users', null, index, 'createdAt')
.then(userCount => {
// get the count of whatever entity within a given range
const tableCount = r
.table(table)
.filter(
r.row(field).during(
// go back to all time, for some reason minval isn't working
r.now().sub(60 * 60 * 24 * 10000),
r.now().sub(60 * 60 * 24 * index)
)
)
.filter(row => r.not(row.hasFields('deletedAt')))
.count()
.run(conn);
return Promise.all([userCount, tableCount]);
})
.then(([userCount, tableCount]) => {
return parseFloat((tableCount / userCount).toFixed(3));
});
};
// array of 30 things
const arr = Array.apply(null, { length: 60 }).map(() => 0);
const backfill = arr.map((o, i) => {
const cpu = getPu('usersCommunities', 'createdAt', i);
const mpu = getPu('messages', 'timestamp', i);
const tpu = getPu('threads', 'createdAt', i);
const users = getCount('users', null, i, 'createdAt');
const communities = getCount('communities', null, i, 'createdAt');
const threads = getCount('threads', null, i, 'createdAt');
const dmThreads = getCount(
'directMessageThreads',
null,
i,
'createdAt'
);
const threadMessages = getCount(
'messages',
{ threadType: 'story' },
i,
'timestamp'
);
const dmMessages = getCount(
'messages',
{
threadType: 'directMessageThread',
},
i,
'timestamp'
);
return Promise.all([
cpu,
mpu,
tpu,
users,
communities,
threads,
dmThreads,
threadMessages,
dmMessages,
]).then(
([
cpu,
mpu,
tpu,
users,
communities,
threads,
dmThreads,
threadMessages,
dmMessages,
]) => {
const coreMetrics = {
cpu: cpu || 0,
mpu: mpu || 0,
tpu: tpu || 0,
users: users || 0,
communities: communities || 0,
threads: threads || 0,
dmThreads: dmThreads || 0,
threadMessages: threadMessages || 0,
dmMessages: dmMessages || 0,
};
return saveCoreMetrics(coreMetrics, i);
}
);
});
return Promise.all(backfill);
})
.catch(err => {
console.log(err);
throw err;
});
};
exports.down = function(r, conn) {
return Promise.all([r.tableDrop('coreMetrics').run(conn)]);
};
================================================
FILE: api/migrations/20171018235659-add-direst-message-user-settings.js
================================================
exports.up = function(r, conn) {
return Promise.all([
r
.table('usersSettings')
.update({
notifications: {
types: {
newDirectMessage: {
email: true,
},
},
},
})
.run(conn),
]).catch(err => {
console.log(err);
throw err;
});
};
exports.down = function(r, conn) {
return Promise.resolve();
};
================================================
FILE: api/migrations/20171029090619-users-channels-index.js
================================================
exports.up = function(r, conn) {
return r
.table('usersChannels')
.indexCreate('userIdAndChannelId', [r.row('userId'), r.row('channelId')])
.run(conn);
};
exports.down = function(r, conn) {
return r
.table('usersChannels')
.indexDrop('userIdAndChannelId')
.run(conn);
};
================================================
FILE: api/migrations/20171029094352-users-threads-index.js
================================================
exports.up = function(r, conn) {
return r
.table('usersThreads')
.indexCreate('userIdAndThreadId', [r.row('userId'), r.row('threadId')])
.run(conn);
};
exports.down = function(r, conn) {
return r
.table('usersThreads')
.indexDrop('userIdAndThreadId')
.run(conn);
};
================================================
FILE: api/migrations/20171103014955-add-mention-notification-settings.js
================================================
exports.up = function(r, conn) {
return Promise.all([
r
.table('usersSettings')
.update({
notifications: {
types: {
newMention: {
email: true,
},
},
},
})
.run(conn),
]);
};
exports.down = function(r, conn) {
return Promise.resolve();
};
================================================
FILE: api/migrations/20171129215512-index-communities-by-slug.js
================================================
exports.up = function(r, conn) {
return Promise.all([
r
.table('communities')
.indexCreate('slug')
.run(conn),
]);
};
exports.down = function(r, conn) {
return Promise.resolve();
};
================================================
FILE: api/migrations/20171129221050-curated-content-table-creation.js
================================================
exports.up = function(r, conn) {
return Promise.all([
r
.tableCreate('curatedContent')
.run(conn)
.then(() => {
const types = [
{
type: 'design-communities',
data: [
'product-design',
'icon-design',
'typography',
'illustrators',
'design-management',
'specfm',
'up-coming',
'sketchcasts',
'google-design',
'design-code',
'vectors',
'designhunt',
'figma',
'sketch',
'framer',
'abstract',
'invision',
'principle',
'compositor',
'origami-studio',
'webflow',
'fuse',
],
},
{
type: 'development-communities',
data: [
'realm',
'expo',
'compositor',
'codepen',
'bootstrap',
'tachyons',
'frontend',
'specfm',
'android',
'swiftdev',
'react-native',
'react',
'node',
'vue-js',
'angular',
'ember-js',
'laravel',
'elixir',
'styled-components',
'graphql',
'css-in-js',
'electron',
],
},
{
type: 'tech-communities',
data: [
'tech-tea',
'balancemymoney',
'crypto',
'btc',
'ethereum',
'augmented-reality',
'voice-interfaces',
],
},
{
type: 'life-communities',
data: [
'for-good',
'mental-health',
'dev-fit',
'music',
'tabletop-rpg',
'gaming',
'careers',
'job-opportunities',
'need-some-work',
],
},
{
type: 'top-communities-by-members',
data: [
'spectrum',
'sketch',
'react',
'specfm',
'product-design',
'figma',
'styled-components',
'framer',
'frontend',
'google-design',
'wip',
'abstract',
'vectors',
'crypto',
'sketchcasts',
'bootstrap',
'css-in-js',
'ooohours',
'tech-tea',
'design-code',
],
},
];
const insertPromises = types.map(type => {
return r
.table('curatedContent')
.insert(type)
.run(conn);
});
return Promise.all([insertPromises]);
})
.catch(err => {
console.log(err);
throw err;
}),
]);
};
exports.down = function(r, conn) {
return Promise.all([r.tableDrop('curatedContent').run(conn)]);
};
================================================
FILE: api/migrations/20171208175038-index-users-for-search.js
================================================
require('now-env');
// const initIndex = require('../../shared/algolia');
// const searchIndex = initIndex('users');
exports.up = function(r, conn) {
return Promise.resolve();
// return r
// .table('users')
// .filter(user => user.hasFields('username'))
// .run(conn)
// .then(cursor => cursor.toArray())
// .then(users =>
// users.map(user => ({
// name: user.name,
// username: user.username,
// description: user.description,
// website: user.website,
// objectID: user.id,
// }))
// )
// .then(searchableUsers => {
// return;
// // return searchIndex.addObjects(searchableUsers)
// })
// .catch(err => console.log(err));
};
exports.down = function(r, conn) {
// Not spending any time undoing this
return Promise.resolve();
};
================================================
FILE: api/migrations/20171208180800-index-communities-for-search.js
================================================
require('now-env');
// const initIndex = require('../../shared/algolia');
// const searchIndex = initIndex('communities');
exports.up = function(r, conn) {
return Promise.resolve();
// return r
// .table('communities')
// .filter(community => community.hasFields('deletedAt').not())
// .run(conn)
// .then(cursor => cursor.toArray())
// .then(communities =>
// communities.map(community => ({
// description: community.description,
// name: community.name,
// slug: community.slug,
// website: community.website ? community.website : null,
// objectID: community.id,
// }))
// )
// .then(searchableCommunities => {
// return;
// // searchIndex.addObjects(searchableCommunities)
// })
// .catch(err => console.log(err));
};
exports.down = function(r, conn) {
// Not spending any time undoing this
return Promise.resolve();
};
================================================
FILE: api/migrations/20171213002813-add-modified-at-field-to-users-and-communities.js
================================================
exports.up = function(r, conn) {
/*
We are adding a modifiedAt field on users and communities for 2 key reasons:
1. By having a single field that changes as edits happen, it makes changefeeds
much easier to follow because we only ever have to filter by the change
on a single field (versus for watching for changes on many potentially-nested fields)
like content.title or content.body. Because users and communities can have many
different fields modified, in any random combination, it will make our lives
easier to add a single `modifiedAt` field that we update whenever an edit occurs.
Then, in the changefeed, we don't have to care about *what* changed, only
that something changed, and we can handle downstream events (like re-indexing in
search results)
2. It's probably good practice anyways to keep track of when things are updated. Since
we don't have a universal event stream, this is an incremental step towards having
more complete knowledge about when things are being changed or deleted in the database,
which thus makes it easier for us to trace down replayable events should a server
crash. E.g. we can know that if a worker crashed, there were 10 users who updated
their profiles during the downtime based on the `modifiedAt` field
*/
return Promise.all([
r
.table('users')
.update({
modifiedAt: new Date(),
})
.run(conn),
r
.table('communities')
.update({
modifiedAt: new Date(),
})
.run(conn),
]);
};
exports.down = function(r, conn) {
return Promise.all([
r
.table('users')
.update({
modifiedAt: r.literal(),
})
.run(conn),
r
.table('communities')
.update({
modifiedAt: r.literal(),
})
.run(conn),
]);
};
================================================
FILE: api/migrations/20180209015734-github-provider-id-index.js
================================================
exports.up = function(r, conn) {
return Promise.all([
r
.table('users')
.indexCreate('githubProviderId')
.run(conn),
]).catch(err => {
console.log(err);
throw err;
});
};
exports.down = function(r, conn) {
return Promise.resolve();
};
================================================
FILE: api/migrations/20180214111357-expo-push-subscriptions.js
================================================
exports.up = function(r, conn) {
return r
.tableCreate('expoPushSubscriptions')
.run(conn)
.then(() =>
r
.table('expoPushSubscriptions')
.indexCreate('userId')
.run(conn)
)
.then(() =>
r
.table('expoPushSubscriptions')
.indexCreate('token')
.run(conn)
)
.catch(err => {
throw new Error(err);
});
};
exports.down = function(r, conn) {
return r
.tableDrop('expoPushSubscriptions')
.run(conn)
.catch(err => {
throw new Error(err);
});
};
================================================
FILE: api/migrations/20180309144845-create-community-settings-table.js
================================================
exports.up = function(r, conn) {
return r
.tableCreate('communitySettings')
.run(conn)
.then(() =>
r
.table('communitySettings')
.indexCreate('communityId', r.row('communityId'))
.run(conn)
)
.catch(err => {
console.log(err);
throw err;
});
};
exports.down = function(r, conn) {
return Promise.all([r.tableDrop('communitySettings').run(conn)]);
};
================================================
FILE: api/migrations/20180316195507-create-channel-settings-table.js
================================================
exports.up = function(r, conn) {
return r
.tableCreate('channelSettings')
.run(conn)
.then(() =>
r
.table('channelSettings')
.indexCreate('channelId', r.row('channelId'))
.run(conn)
)
.catch(err => {
console.error(err);
throw err;
});
};
exports.down = function(r, conn) {
return Promise.all([r.tableDrop('channelSettings').run(conn)]);
};
================================================
FILE: api/migrations/20180320122000-create-stripe-tables.js
================================================
exports.up = function(r, conn) {
const createCustomersTable = () =>
r.tableCreate('stripeCustomers', { primaryKey: 'customerId' }).run(conn);
const createInvoicesTable = () =>
r.tableCreate('stripeInvoices', { primaryKey: 'invoiceId' }).run(conn);
return Promise.all([createCustomersTable(), createInvoicesTable()])
.then(() =>
Promise.all([
r
.table('stripeInvoices')
.indexCreate('customerId')
.run(conn),
])
)
.catch(err => console.log(err));
};
exports.down = function(r, conn) {
return Promise.all([
r.tableDrop('stripeCustomers').run(conn),
r.tableDrop('stripeInvoices').run(conn),
]);
};
================================================
FILE: api/migrations/20180320173414-set-administrator-info-on-community.js
================================================
exports.up = async (r, conn) => {
const addFields = r
.table('communities')
.update({
// a distinct email for each community where receipts and admin
// messages will be sent, can be different from the owner's personal
// email
administratorEmail: null,
// references the actual creator of the community, discrete from the 'owner'
// role
creatorId: null,
// will be used to fetch records from the stripeCustomers and stripeInvoices
// tables in order to populate the billing area and resolve query fields
// for community features
stripeCustomerId: null,
// these are two feature flags that pluto will listen to changes for in
// order to trigger billing events
analyticsEnabled: false,
prioritySupportEnabled: false,
})
.run(conn);
// gets all the owner records in a community, figures out which one is the oldest,
// then uses that record to populate an administratorEmail and creatorId field
// if they exist
const oldestOwners = await r
.table('usersCommunities')
.filter({ isOwner: true })
.group('communityId')
.ungroup()
.map(row => row('reduction'))
.map(row => row.min('createdAt'))
.eqJoin('userId', r.table('users'))
.zip()
.run(conn)
.then(cursor => cursor.toArray());
const setAdminFields = oldestOwners.map(async owner => {
if (!owner || !owner.userId || !owner.communityId) return;
return await r
.table('communities')
.get(owner.communityId)
.update({
administratorEmail: owner.email,
creatorId: owner.userId,
})
.run(conn);
});
return Promise.all([addFields]).then(
async () => await Promise.all(setAdminFields)
);
};
exports.down = function(r, conn) {
return r
.table('communities')
.update({
administratorEmail: r.literal(),
creatorId: r.literal(),
stripeCustomerId: r.literal(),
analyticsEnabled: r.literal(),
prioritySupportEnabled: r.literal(),
})
.run(conn);
};
================================================
FILE: api/migrations/20180411183454-lowercase-all-the-slugs.js
================================================
exports.up = async (r, conn) => {
return Promise.all([
r
.table('users')
.update({
username: r.row('username').downcase(),
email: r.row('email').downcase(),
})
.run(conn),
r
.table('communities')
.update({
slug: r.row('slug').downcase(),
})
.run(conn),
r
.table('channels')
.update({
slug: r.row('slug').downcase(),
})
.run(conn),
]);
};
exports.down = function(r, conn) {
return Promise.resolve();
};
================================================
FILE: api/migrations/20180428001543-reset-slack-import-records.js
================================================
exports.up = function(r, conn) {
return Promise.all([
r
.table('slackImports')
.update({
members: r.literal(),
})
.run(conn),
]).catch(err => console.error(err));
};
exports.down = function(r, conn) {
return Promise.resolve();
};
================================================
FILE: api/migrations/20180504003702-encrypt-existing-slack-data.js
================================================
const { encryptString } = require('../../shared/encryption');
exports.up = async (r, conn) => {
const encryptOldSlackImportData = async () => {
const records = await r
.table('slackImports')
.run(conn)
.then(cursor => cursor.toArray());
const recordPromises = records.map(async record => {
const teamId = encryptString(record.teamId);
const teamName = encryptString(record.teamName);
const token = encryptString(record.token);
return await r
.table('slackImports')
.get(record.id)
.update({
teamId,
teamName,
token,
})
.run(conn);
});
return await Promise.all([...recordPromises]);
};
const encryptNewSlackImportData = async () => {
const records = await r
.table('communitySettings')
.filter(row => row.hasFields('slackSettings'))
.run(conn)
.then(cursor => cursor.toArray());
const recordPromises = records.map(async record => {
const teamId = encryptString(record.slackSettings.teamId);
const teamName = encryptString(record.slackSettings.teamName);
const token = encryptString(record.slackSettings.token);
const scope = encryptString(record.slackSettings.scope);
return await r
.table('communitySettings')
.get(record.id)
.update({
slackSettings: {
teamId,
teamName,
token,
scope,
},
})
.run(conn);
});
return await Promise.all([...recordPromises]);
};
return await Promise.all([
encryptOldSlackImportData(),
encryptNewSlackImportData(),
]).catch(err => console.error(err));
};
exports.down = function(r, conn) {
return Promise.resolve();
};
================================================
FILE: api/migrations/20180517180716-enable-private-communities.js
================================================
exports.up = async (r, conn) => {
return r
.table('communities')
.update({
isPrivate: false,
})
.run(conn);
};
exports.down = function(r, conn) {
return r
.table('communities')
.update({
isPrivate: r.literal(),
})
.run(conn);
};
================================================
FILE: api/migrations/20180517215503-add-ispending-to-userscommunities.js
================================================
exports.up = async (r, conn) => {
return r
.table('usersCommunities')
.update({
isPending: false,
})
.run(conn);
};
exports.down = function(r, conn) {
return r
.table('usersCommunities')
.update({
isPending: r.literal(),
})
.run(conn);
};
================================================
FILE: api/migrations/20180518135040-add-join-settings-to-community-settings.js
================================================
exports.up = async (r, conn) => {
return r
.table('communitySettings')
.update({
joinSettings: {
tokenJoinEnabled: false,
token: null,
},
})
.run(conn);
};
exports.down = function(r, conn) {
return r
.table('communitySettings')
.update({
joinSettings: r.literal(),
})
.run(conn);
};
================================================
FILE: api/migrations/20180621001409-thread-likes-table.js
================================================
exports.up = function(r, conn) {
return r
.tableCreate('threadReactions')
.run(conn)
.then(() => {
return Promise.all([
r
.table('threadReactions')
.indexCreate('threadId')
.run(conn),
r
.table('threadReactions')
.indexCreate('userId')
.run(conn),
]);
})
.catch(err => console.error(err));
};
exports.down = function(r, conn) {
return Promise.all([r.tableDrop('threadReactions').run(conn)]);
};
================================================
FILE: api/migrations/20180823115847-add-users-communities-indexes.js
================================================
exports.up = function(r, conn) {
return Promise.all([
r
.table('usersCommunities')
.indexCreate('communityIdAndIsMember', [
r.row('communityId'),
r.row('isMember'),
])
.run(conn),
r
.table('usersCommunities')
.indexCreate('communityIdAndIsMemberAndReputation', [
r.row('communityId'),
r.row('isMember'),
r.row('reputation'),
])
.run(conn),
r
.table('usersCommunities')
.indexCreate('communityIdAndIsModerator', [
r.row('communityId'),
r.row('isModerator'),
])
.run(conn),
r
.table('usersCommunities')
.indexCreate('communityIdAndIsOwner', [
r.row('communityId'),
r.row('isOwner'),
])
.run(conn),
r
.table('usersCommunities')
.indexCreate('communityIdAndIsTeamMember', [
r.row('communityId'),
r.row('isOwner').or(r.row('isModerator')),
])
.run(conn),
]);
};
exports.down = function(r, conn) {
return Promise.all([
r
.table('usersCommunities')
.indexDrop('communityIdAndIsMember')
.run(conn),
r
.table('usersCommunities')
.indexDrop('communityIdAndIsModerator')
.run(conn),
r
.table('usersCommunities')
.indexDrop('communityIdAndIsOwner')
.run(conn),
r
.table('usersCommunities')
.indexDrop('communityIdAndIsTeamMember')
.run(conn),
]);
};
================================================
FILE: api/migrations/20181001061156-thread-metadata-denormalization.js
================================================
exports.up = function(r, conn) {
return r
.table('threads')
.update(
{
messageCount: r
.table('messages')
.getAll(r.row('id'), { index: 'threadId' })
.count()
.default(0),
reactionCount: r
.table('threadReactions')
.getAll(r.row('id'), { index: 'threadId' })
.count()
.default(0),
},
{
nonAtomic: true,
}
)
.run(conn)
.then(() => {
return Promise.all([
r
.table('threads')
.indexCreate('messageCount')
.run(conn),
r
.table('threads')
.indexCreate('reactionCount')
.run(conn),
]);
})
.catch(err => console.error(err));
};
exports.down = function(r, conn) {
return r
.table('threads')
.update({
messageCount: r.literal(),
reactionCount: r.literal(),
})
.run(conn)
.then(() => {
return Promise.all([
r
.table('threads')
.indexDrop('messageCount')
.run(conn),
r
.table('threads')
.indexDrop('reactionCount')
.run(conn),
]);
});
};
================================================
FILE: api/migrations/20181001064151-fix-thread-metadata-message-counts.js
================================================
exports.up = function(r, conn) {
return r
.table('threads')
.update(
{
messageCount: r
.table('messages')
.getAll(r.row('id'), { index: 'threadId' })
.filter(row => r.not(row.hasFields('deletedAt')))
.count()
.default(0),
reactionCount: r
.table('threadReactions')
.getAll(r.row('id'), { index: 'threadId' })
.filter(row => r.not(row.hasFields('deletedAt')))
.count()
.default(0),
},
{
nonAtomic: true,
}
)
.run(conn)
.catch(err => console.error(err));
};
exports.down = function(r, conn) {
return Promise.resolve();
};
================================================
FILE: api/migrations/20181002060237-remove-payments.js
================================================
exports.up = async (r, conn) => {
const betaSupporterIds = await r
.db('spectrum')
.table('recurringPayments')
.filter({ planId: 'beta-pro' })
.map(row => row('userId'))
.run(conn)
.then(cursor => cursor.toArray());
const addSupporterFieldToUsersPromises = async () =>
await r
.db('spectrum')
.table('users')
.getAll(...betaSupporterIds)
.update({
betaSupporter: true,
})
.run(conn);
const cleanCommunitiesModel = () =>
r
.table('communities')
.update({
stripeCustomerId: r.literal(),
analyticsEnabled: r.literal(),
prioritySupportEnabled: r.literal(),
ossVerified: r.literal(),
})
.run(conn);
return await Promise.all([
cleanCommunitiesModel(),
addSupporterFieldToUsersPromises(),
]);
};
exports.down = async (r, conn) => {
return await Promise.all([
r
.db('spectrum')
.table('communities')
.update({
stripeCustomerId: null,
analyticsEnabled: false,
prioritySupportEnabled: false,
ossVerified: false,
})
.run(conn),
r
.db('spectrum')
.table('users')
.update({
betaSupporter: r.literal(),
})
.run(conn),
]);
};
================================================
FILE: api/migrations/20181003233411-thread-reactions-useridandthreadid-index.js
================================================
exports.up = function(r, conn) {
return r
.table('threadReactions')
.indexCreate('userIdAndThreadId', [r.row('userId'), r.row('threadId')])
.run(conn);
};
exports.down = function(r, conn) {
return r
.table('threadReactions')
.indexDrop('userIdAndThreadId')
.run(conn);
};
================================================
FILE: api/migrations/20181004222636-denormalize-channel-community-member-counts.js
================================================
exports.up = function(r, conn) {
return Promise.all([
r
.table('communities')
.update(
{
memberCount: r
.table('usersCommunities')
.getAll(r.row('id'), { index: 'communityId' })
.filter(row => row('isMember').eq(true))
.count()
.default(1),
},
{
nonAtomic: true,
}
)
.run(conn),
r
.table('channels')
.update(
{
memberCount: r
.table('usersChannels')
.getAll(r.row('id'), { index: 'channelId' })
.filter(row => row('isMember').eq(true))
.count()
.default(1),
},
{
nonAtomic: true,
}
)
.run(conn),
]).catch(err => console.error(err));
};
exports.down = function(r, conn) {
return Promise.all([
r
.table('communities')
.update({
memberCount: r.literal(),
})
.run(conn),
r
.table('channels')
.update({
memberCount: r.literal(),
})
.run(conn),
]);
};
================================================
FILE: api/migrations/20181005143053-users-notifications-useridandnotificationid-index.js
================================================
exports.up = function(r, conn) {
return r
.table('usersNotifications')
.indexCreate('userIdAndNotificationId', [
r.row('userId'),
r.row('notificationId'),
])
.run(conn);
};
exports.down = function(r, conn) {
return r
.table('usersNotifications')
.indexDrop('userIdAndNotificationId')
.run(conn);
};
================================================
FILE: api/migrations/20181005144259-users-notifications-userIdAndIsSeen-index.js
================================================
exports.up = function(r, conn) {
return r
.table('usersNotifications')
.indexCreate('userIdAndIsSeen', [r.row('userId'), r.row('isSeen')])
.run(conn);
};
exports.down = function(r, conn) {
return r
.table('usersNotifications')
.indexDrop('userIdAndIsSeen')
.run(conn);
};
================================================
FILE: api/migrations/20181023160027-update-denormalized-member-counts.js
================================================
exports.up = function(r, conn) {
return Promise.all([
r
.table('communities')
.update(
{
memberCount: r
.table('usersCommunities')
.getAll(r.row('id'), { index: 'communityId' })
.filter(row => row('isMember').eq(true))
.count()
.default(1),
},
{
nonAtomic: true,
}
)
.run(conn),
r
.table('channels')
.update(
{
memberCount: r
.table('usersChannels')
.getAll(r.row('id'), { index: 'channelId' })
.filter(row => row('isMember').eq(true))
.count()
.default(1),
},
{
nonAtomic: true,
}
)
.run(conn),
]).catch(err => console.error(err));
};
exports.down = function(r, conn) {
return Promise.resolve();
};
================================================
FILE: api/migrations/20181024173616-indexes-for-digests.js
================================================
exports.up = function(r, conn) {
return Promise.all([
r
.table('usersSettings')
.indexCreate(
'weeklyDigestEmail',
r.row('notifications')('types')('weeklyDigest')('email')
)
.run(conn),
r
.table('usersSettings')
.indexCreate(
'dailyDigestEmail',
r.row('notifications')('types')('dailyDigest')('email')
)
.run(conn),
]);
};
exports.down = function(r, conn) {
return Promise.all([
r
.table('usersSettings')
.indexDrop('weeklyDigestEmail')
.run(conn),
r
.table('usersSettings')
.indexDrop('dailyDigestEmail')
.run(conn),
]);
};
================================================
FILE: api/migrations/20181027050052-remove-attachments-from-thread-model.js
================================================
exports.up = function(r, conn) {
return Promise.all([
r
.table('threads')
.update({
attachments: r.literal(),
})
.run(conn),
]).catch(err => console.error(err));
};
exports.down = function(r, conn) {
return Promise.resolve();
};
================================================
FILE: api/migrations/20181102025454-fix-old-image-urls-in-messages.js
================================================
exports.up = async function(r, conn) {
const messages = await r
.db('spectrum')
.table('messages')
.filter({ messageType: 'media' })
.filter(row => row('timestamp').lt(r.epochTime(1540929600)))
.filter(row => row('content')('body').match('spectrum.imgix.net'))
.filter(row => row('content')('body').match('%20'))
.filter(row => row.hasFields('deletedAt').not())
.map(row => ({ id: row('id'), url: row('content')('body') }))
.run(conn)
.then(cursor => cursor.toArray());
const messagePromises = messages.map(async obj => {
return await r
.db('spectrum')
.table('messages')
.get(obj.id)
.update({
content: {
body: decodeURIComponent(obj.url),
},
imageReplaced: new Date(),
})
.run(conn);
});
return await Promise.all(messagePromises);
};
exports.down = function(r, conn) {
return Promise.resolve();
};
================================================
FILE: api/migrations/20181102040518-fix-old-image-urls-in-threads.js
================================================
exports.up = async function(r, conn) {
const threads = await r
.db('spectrum')
.table('threads')
.filter(row => row('modifiedAt').lt(r.epochTime(1540929600)))
.filter(row => row('content')('body').match('spectrum.imgix.net'))
.filter(row => row('content')('body').match('%20'))
.filter(row => row.hasFields('deletedAt').not())
.map(row => ({ id: row('id'), body: row('content')('body') }))
.run(conn)
.then(cursor => cursor.toArray());
const threadPromises = threads.map(async obj => {
const newBody = JSON.parse(obj.body);
const imageKeys = Object.keys(newBody.entityMap).filter(
key => newBody.entityMap[key].type.toLowerCase() === 'image'
);
const LEGACY_PREFIX = 'https://spectrum.imgix.net/';
const hasLegacyPrefix = url => url.startsWith(LEGACY_PREFIX, 0);
const stripLegacyPrefix = url => url.replace(LEGACY_PREFIX, '');
const processImageUrl = str => {
if (str.indexOf(LEGACY_PREFIX) < 0) {
return str;
}
if (str.indexOf('%20') < 0) {
return str;
}
const split = str.split('?');
const imagePath = split[0];
const decoded = decodeURIComponent(imagePath);
const processed = hasLegacyPrefix(decoded)
? stripLegacyPrefix(decoded)
: decoded;
return processed;
};
imageKeys.forEach((key, index) => {
if (!newBody.entityMap[key]) {
return;
}
const { src } = newBody.entityMap[key].data;
newBody.entityMap[key].data.src = processImageUrl(src);
});
return await r
.db('spectrum')
.table('threads')
.get(obj.id)
.update({
content: {
body: JSON.stringify(newBody),
},
imageReplaced: new Date(),
})
.run(conn);
});
return await Promise.all(threadPromises);
};
exports.down = function(r, conn) {
return Promise.resolve();
};
================================================
FILE: api/migrations/20181102044407-fix-old-image-urls-in-communities.js
================================================
exports.up = async function(r, conn) {
const communities = await r
.db('spectrum')
.table('communities')
.filter(row => row('modifiedAt').lt(r.epochTime(1540929600)))
.filter(row =>
row('profilePhoto')
.match('spectrum.imgix.net')
.or(row('coverPhoto').match('spectrum.imgix.net'))
)
.filter(row =>
row('profilePhoto')
.match('%20')
.or(row('coverPhoto').match('%20'))
)
.filter(row => row.hasFields('deletedAt').not())
.map(row => ({
id: row('id'),
profilePhoto: row('profilePhoto'),
coverPhoto: row('coverPhoto'),
}))
.run(conn)
.then(cursor => cursor.toArray());
const communityPromises = communities.map(async obj => {
const { profilePhoto, coverPhoto } = obj;
const LEGACY_PREFIX = 'https://spectrum.imgix.net/';
const hasLegacyPrefix = url => url.startsWith(LEGACY_PREFIX, 0);
const stripLegacyPrefix = url => url.replace(LEGACY_PREFIX, '');
const processImageUrl = str => {
if (str.indexOf(LEGACY_PREFIX) < 0) {
return str;
}
if (str.indexOf('%20') < 0) {
return str;
}
const split = str.split('?');
const imagePath = split[0];
const decoded = decodeURIComponent(imagePath);
const processed = hasLegacyPrefix(decoded)
? stripLegacyPrefix(decoded)
: decoded;
return processed;
};
const newProfilePhoto = processImageUrl(profilePhoto);
const newCoverPhoto = processImageUrl(coverPhoto);
return await r
.db('spectrum')
.table('communities')
.get(obj.id)
.update({
coverPhoto: newCoverPhoto,
profilePhoto: newProfilePhoto,
imageReplaced: new Date(),
})
.run(conn);
});
return await Promise.all(communityPromises);
};
exports.down = function(r, conn) {
return Promise.resolve();
};
================================================
FILE: api/migrations/20181102045821-fix-old-image-urls-in-users.js
================================================
exports.up = async function(r, conn) {
const users = await r
.db('spectrum')
.table('users')
.filter(row => row('modifiedAt').lt(r.epochTime(1540929600)))
.filter(row =>
row.hasFields('coverPhoto').and(row.hasFields('profilePhoto'))
)
.filter(row =>
row('profilePhoto')
.match('spectrum.imgix.net')
.or(row('coverPhoto').match('spectrum.imgix.net'))
)
.filter(row =>
row('profilePhoto')
.match('%20')
.or(row('coverPhoto').match('%20'))
)
.filter(row => row.hasFields('deletedAt').not())
.map(row => ({
id: row('id'),
profilePhoto: row('profilePhoto'),
coverPhoto: row('coverPhoto'),
}))
.run(conn)
.then(cursor => cursor.toArray());
const userPromises = users.map(async obj => {
const { profilePhoto, coverPhoto } = obj;
const LEGACY_PREFIX = 'https://spectrum.imgix.net/';
const hasLegacyPrefix = url => url.startsWith(LEGACY_PREFIX, 0);
const stripLegacyPrefix = url => url.replace(LEGACY_PREFIX, '');
const processImageUrl = str => {
if (str.indexOf(LEGACY_PREFIX) < 0) {
return str;
}
if (str.indexOf('%20') < 0) {
return str;
}
const split = str.split('?');
const imagePath = split[0];
const decoded = decodeURIComponent(imagePath);
const processed = hasLegacyPrefix(decoded)
? stripLegacyPrefix(decoded)
: decoded;
return processed;
};
const newProfilePhoto = processImageUrl(profilePhoto);
const newCoverPhoto = processImageUrl(coverPhoto);
return await r
.db('spectrum')
.table('users')
.get(obj.id)
.update({
coverPhoto: newCoverPhoto,
profilePhoto: newProfilePhoto,
imageReplaced: new Date(),
})
.run(conn);
});
return await Promise.all(userPromises);
};
exports.down = function(r, conn) {
return Promise.resolve();
};
================================================
FILE: api/migrations/20181102054523-fix-aws-static-url-community-photos.js
================================================
exports.up = async function(r, conn) {
const LEGACY_PREFIX = 'https://s3.amazonaws.com/spectrum-chat/';
const communities = await r
.db('spectrum')
.table('communities')
.filter(row =>
row('profilePhoto')
.match(LEGACY_PREFIX)
.or(row('coverPhoto').match(LEGACY_PREFIX))
)
.filter(row => row.hasFields('deletedAt').not())
.map(row => ({
id: row('id'),
profilePhoto: row('profilePhoto'),
coverPhoto: row('coverPhoto'),
}))
.run(conn)
.then(cursor => cursor.toArray());
const communityPromises = communities.map(async obj => {
const { profilePhoto, coverPhoto } = obj;
const hasLegacyPrefix = url => url.startsWith(LEGACY_PREFIX, 0);
const stripLegacyPrefix = url => url.replace(LEGACY_PREFIX, '');
const processImageUrl = str => {
if (str.indexOf(LEGACY_PREFIX) < 0) {
return str;
}
return stripLegacyPrefix(str);
};
const newProfilePhoto = processImageUrl(profilePhoto);
const newCoverPhoto = processImageUrl(coverPhoto);
return await r
.db('spectrum')
.table('communities')
.get(obj.id)
.update({
coverPhoto: newCoverPhoto,
profilePhoto: newProfilePhoto,
awsStaticReplaced: new Date(),
})
.run(conn);
});
return await Promise.all(communityPromises);
};
exports.down = function(r, conn) {
return Promise.resolve();
};
================================================
FILE: api/migrations/20181116173949-add-terms-last-accepted-field-to-users.js
================================================
exports.up = async (r, conn) => {
return r
.table('users')
.update({
termsLastAcceptedAt: r.row('createdAt'),
})
.run(conn);
};
exports.down = function(r, conn) {
return r
.table('users')
.update({
termsLastAcceptedAt: r.literal(),
})
.run(conn);
};
================================================
FILE: api/migrations/20181121054300-resync-community-member-counts.js
================================================
exports.up = function(r, conn) {
return Promise.all([
r
.table('communities')
.update(
{
memberCount: r
.table('usersCommunities')
.getAll(r.row('id'), { index: 'communityId' })
.filter(row => row('isMember').eq(true))
.count()
.default(1),
},
{
nonAtomic: true,
}
)
.run(conn),
r
.table('channels')
.update(
{
memberCount: r
.table('usersChannels')
.getAll(r.row('id'), { index: 'channelId' })
.filter(row => row('isMember').eq(true))
.count()
.default(1),
},
{
nonAtomic: true,
}
)
.run(conn),
]).catch(err => console.error(err));
};
exports.down = function(r, conn) {
return Promise.resolve();
};
================================================
FILE: api/migrations/20181122162921-users-communities-useridandmember-index.js
================================================
exports.up = function(r, conn) {
return r
.table('usersCommunities')
.indexCreate('userIdAndIsMember', [r.row('userId'), r.row('isMember')])
.run(conn);
};
exports.down = function(r, conn) {
return r
.table('usersCommunities')
.indexDrop('userIdAndIsMember')
.run(conn);
};
================================================
FILE: api/migrations/20181126094455-users-channels-roles.js
================================================
const branch = (r, field, fallback) => {
return r.branch(r.row(`is${field}`).eq(true), field.toLowerCase(), fallback);
};
exports.up = function(r, conn) {
return Promise.all([
r
.table('usersChannels')
.indexCreate('channelIdAndRole', [
r.row('channelId'),
branch(
r,
'Pending',
branch(
r,
'Blocked',
branch(
r,
'Owner',
branch(r, 'Moderator', branch(r, 'Member', r.literal()))
)
)
),
])
.run(conn),
r
.table('usersChannels')
.indexCreate('userIdAndRole', [
r.row('userId'),
branch(
r,
'Pending',
branch(
r,
'Blocked',
branch(
r,
'Owner',
branch(r, 'Moderator', branch(r, 'Member', r.literal()))
)
)
),
])
.run(conn),
]);
};
exports.down = function(r, conn) {
return Promise.all([
r
.table('usersChannels')
.indexDrop('channelIdAndRole')
.run(conn),
r
.table('usersChannels')
.indexDrop('userIdAndRole')
.run(conn),
]);
};
================================================
FILE: api/migrations/20181127090014-communities-member-count-index.js
================================================
exports.up = function(r, conn) {
return r
.table('communities')
.indexCreate('memberCount')
.run(conn);
};
exports.down = function(r, conn) {
return r
.table('communities')
.indexDrop('memberCount')
.run(conn);
};
================================================
FILE: api/migrations/20181205171559-remove-old-users-notifications.js
================================================
exports.up = async (r, conn) => {
let after = 0;
let limit = 10000;
let done = false;
const getRecords = async (after, limit) => {
return await r
.table('usersNotifications')
.skip(after)
.limit(limit)
.run(conn)
.then(cursor => cursor.toArray());
};
const deleteRecords = async arr => {
if (!arr || arr.length === 0) return;
const filtered = arr
.filter(rec => rec.isSeen)
.filter(rec => {
const THIRTY_DAYS = 1000 * 60 * 60 * 24 * 30; //ms
const added = new Date(rec.entityAddedAt).getTime(); //ms
const now = new Date().getTime(); //ms
return now - added > THIRTY_DAYS;
})
.map(rec => rec.id)
.filter(Boolean);
return await r
.table('usersNotifications')
.getAll(...filtered)
.delete()
.run(conn);
};
const processUsersNotificiations = async arr => {
if (done) {
return await deleteRecords(arr);
}
if (arr.length < limit) {
done = true;
return await deleteRecords(arr);
}
return deleteRecords(arr).then(async () => {
after = after + limit;
const nextRecords = await getRecords(after, limit);
return processUsersNotificiations(nextRecords);
});
};
const initialRecordIds = await getRecords(after, limit);
return processUsersNotificiations(initialRecordIds);
};
exports.down = function(r, conn) {
return Promise.resolve();
};
================================================
FILE: api/migrations/20181211181146-add-usersthreads-user-id-and-participant-index.js
================================================
exports.up = function(r, conn) {
return Promise.all([
r
.table('usersThreads')
.indexCreate('userIdAndIsParticipant', [
r.row('userId'),
r.row('isParticipant'),
])
.run(conn),
]);
};
exports.down = function(r, conn) {
return Promise.all([
r
.table('usersThreads')
.indexDrop('userIdAndIsParticipant')
.run(conn),
]);
};
================================================
FILE: api/migrations/20190226085909-bot-user-sam.js
================================================
exports.up = function(r, conn) {
return r
.table('users')
.insert({
id: 'sam',
description: "Spectrum's automated bot.",
createdAt: new Date(),
email: null,
providerId: null,
fbProviderId: null,
githubProviderId: null,
githubUsername: 'withspectrum',
googleProviderId: null,
isOnline: true,
lastSeen: new Date(),
modifiedAt: new Date(),
name: 'Spectrum Bot',
termsLastAcceptedAt: new Date(),
username: 'spectrumbot',
website: 'https://spectrum.chat',
profilePhoto: '/default_images/sam.png',
})
.run(conn);
};
exports.down = function(r, conn) {
return r
.table('users')
.get('sam')
.delete()
.run(conn);
};
================================================
FILE: api/migrations/20190306125252-threads-watercooler-index.js
================================================
exports.up = function(r, conn) {
return r
.table('threads')
.indexCreate('communityIdAndWatercooler', [
r.row('communityId'),
r.row('watercooler'),
])
.run(conn);
};
exports.down = function(r, conn) {
return r
.table('threads')
.indexDrop('communityIdAndWatercooler')
.run(conn);
};
================================================
FILE: api/migrations/20190315142923-backfill-userscommunities-last-seen-community-last-active.js
================================================
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),
]);
};
================================================
FILE: api/migrations/20190327134509-delete-bot-messages.js
================================================
exports.up = function(r, conn) {
return r
.table('messages')
.filter({ bot: true })
.delete()
.run(conn);
};
exports.down = function(r, conn) {
return Promise.resolve();
};
================================================
FILE: api/migrations/config.js
================================================
const path = require('path');
const fs = require('fs');
const debug = require('debug')('migrations');
const DEFAULT_CONFIG = {
driver: 'rethinkdbdash',
db: process.env.NODE_ENV === 'test' ? 'testing' : 'spectrum',
host: 'localhost',
port: 28015,
migrationsDirectory: 'api/migrations',
};
let ca;
try {
ca = fs.readFileSync(path.join(process.cwd(), 'cacert'));
} catch (err) {}
const RUN_IN_PROD = !!process.env.AWS_RETHINKDB_PASSWORD;
if (!ca && RUN_IN_PROD)
throw new Error(
'Please provide the SSL certificate to connect to the production database in a file called `cacert` in the root directory.'
);
if (RUN_IN_PROD && process.argv[4] === 'down') {
throw new Error('Do not drop the production database!!!!!');
}
if (RUN_IN_PROD) debug('Running migration in production...');
module.exports = !RUN_IN_PROD
? DEFAULT_CONFIG
: Object.assign({}, DEFAULT_CONFIG, {
password: process.env.AWS_RETHINKDB_PASSWORD,
host: process.env.AWS_RETHINKDB_URL,
port: process.env.AWS_RETHINKDB_PORT,
...(ca
? {
ssl: {
ca,
},
}
: {}),
});
================================================
FILE: api/migrations/seed/default/channelSettings.js
================================================
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,
},
},
},
];
================================================
FILE: api/migrations/seed/default/channels.js
================================================
// @flow
const constants = require('./constants');
const {
DATE,
SPECTRUM_COMMUNITY_ID,
PAYMENTS_COMMUNITY_ID,
DELETED_COMMUNITY_ID,
PRIVATE_COMMUNITY_ID,
SINGLE_CHANNEL_COMMUNITY_ID,
SPECTRUM_GENERAL_CHANNEL_ID,
SPECTRUM_PRIVATE_CHANNEL_ID,
PAYMENTS_GENERAL_CHANNEL_ID,
PAYMENTS_PRIVATE_CHANNEL_ID,
PAYMENTS_FEATURES_CHANNEL_ID,
SPECTRUM_ARCHIVED_CHANNEL_ID,
SPECTRUM_DELETED_CHANNEL_ID,
DELETED_COMMUNITY_DELETED_CHANNEL_ID,
MODERATOR_CREATED_CHANNEL_ID,
PRIVATE_GENERAL_CHANNEL_ID,
SINGLE_CHANNEL_COMMUNITY_GENERAL_CHANNEL_ID,
} = constants;
module.exports = [
{
id: SPECTRUM_GENERAL_CHANNEL_ID,
communityId: SPECTRUM_COMMUNITY_ID,
createdAt: new Date(DATE),
name: 'General',
description: 'General chatter',
slug: 'general',
isPrivate: false,
isDefault: true,
memberCount: 5,
},
{
id: SPECTRUM_PRIVATE_CHANNEL_ID,
communityId: SPECTRUM_COMMUNITY_ID,
createdAt: new Date(DATE),
name: 'Private',
description: 'Private chatter',
slug: 'private',
isPrivate: true,
isDefault: false,
memberCount: 5,
},
{
id: PAYMENTS_GENERAL_CHANNEL_ID,
communityId: PAYMENTS_COMMUNITY_ID,
createdAt: new Date(DATE),
name: 'General',
description: 'General chatter',
slug: 'general',
isPrivate: false,
isDefault: true,
memberCount: 5,
},
{
id: PAYMENTS_PRIVATE_CHANNEL_ID,
communityId: PAYMENTS_COMMUNITY_ID,
createdAt: new Date(DATE),
name: 'Private',
description: 'Private chatter',
slug: 'private',
isPrivate: true,
isDefault: false,
memberCount: 5,
},
{
id: PAYMENTS_FEATURES_CHANNEL_ID,
communityId: PAYMENTS_COMMUNITY_ID,
createdAt: new Date(DATE),
name: 'Payments Features',
description: 'Payments Features',
slug: 'features',
isPrivate: false,
isDefault: false,
memberCount: 5,
},
{
id: SPECTRUM_ARCHIVED_CHANNEL_ID,
communityId: SPECTRUM_COMMUNITY_ID,
createdAt: new Date(DATE),
name: 'Archived',
description: 'Testing archiving',
slug: 'archived',
isPrivate: false,
isDefault: true,
archivedAt: new Date(DATE),
memberCount: 3,
},
{
id: SPECTRUM_DELETED_CHANNEL_ID,
communityId: SPECTRUM_COMMUNITY_ID,
createdAt: new Date(DATE),
name: 'Deleted',
description: 'Testing deleted channel',
slug: 'deleted',
isPrivate: false,
isDefault: false,
deletedAt: new Date(DATE),
memberCount: 0,
},
{
id: DELETED_COMMUNITY_DELETED_CHANNEL_ID,
communityId: DELETED_COMMUNITY_ID,
createdAt: new Date(DATE),
name: 'Deleted',
description: 'Testing deleted channel',
slug: 'deleted',
isPrivate: false,
isDefault: false,
deletedAt: new Date(DATE),
memberCount: 1,
},
{
id: MODERATOR_CREATED_CHANNEL_ID,
communityId: SPECTRUM_COMMUNITY_ID,
createdAt: new Date(DATE),
name: 'Moderator created',
description: 'Moderator created channel',
slug: 'moderator-created',
isPrivate: false,
isDefault: false,
memberCount: 1,
},
{
id: PRIVATE_GENERAL_CHANNEL_ID,
communityId: PRIVATE_COMMUNITY_ID,
createdAt: new Date(DATE),
name: 'General',
description: 'General',
slug: 'private-general',
isPrivate: false,
isDefault: false,
memberCount: 1,
},
{
id: SINGLE_CHANNEL_COMMUNITY_GENERAL_CHANNEL_ID,
communityId: SINGLE_CHANNEL_COMMUNITY_ID,
createdAt: new Date(DATE),
name: 'General',
description: 'General',
slug: 'general',
isPrivate: false,
isDefault: false,
memberCount: 1,
},
];
================================================
FILE: api/migrations/seed/default/communities.js
================================================
// @flow
const constants = require('./constants');
const {
DATE,
SPECTRUM_COMMUNITY_ID,
PAYMENTS_COMMUNITY_ID,
DELETED_COMMUNITY_ID,
PRIVATE_COMMUNITY_ID,
SINGLE_CHANNEL_COMMUNITY_ID,
PRIVATE_COMMUNITY_WITH_JOIN_TOKEN_ID,
} = constants;
module.exports = [
{
id: SPECTRUM_COMMUNITY_ID,
createdAt: new Date(DATE),
isPrivate: false,
name: 'Spectrum',
description: 'The future of communities',
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: 'spectrum',
memberCount: 4,
},
{
id: PAYMENTS_COMMUNITY_ID,
createdAt: new Date(DATE),
isPrivate: false,
name: 'Payments',
description: 'Where payments are tested',
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: 'payments',
memberCount: 5,
},
{
id: DELETED_COMMUNITY_ID,
createdAt: new Date(DATE),
deletedAt: new Date(DATE),
isPrivate: false,
name: 'Deleted',
description: 'Things didnt work out',
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: 'deleted',
memberCount: 0,
},
{
id: PRIVATE_COMMUNITY_ID,
createdAt: new Date(DATE),
isPrivate: true,
name: 'Private community',
description: 'Private community',
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',
memberCount: 1,
},
{
id: SINGLE_CHANNEL_COMMUNITY_ID,
createdAt: new Date(DATE),
isPrivate: false,
name: 'Single channel community',
description: 'Single channel community',
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: '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,
},
];
================================================
FILE: api/migrations/seed/default/communitySettings.js
================================================
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',
},
},
];
================================================
FILE: api/migrations/seed/default/constants.js
================================================
// @flow
const DATE = 1483225200000;
// users
const MAX_ID = '1';
const BRIAN_ID = '2';
const BRYN_ID = '3';
// this user is blocked in spectrum community
const BLOCKED_USER_ID = '4';
// this user is has never joined communities or channels
const QUIET_USER_ID = '5';
// this user was a previous member of spectrum community
const PREVIOUS_MEMBER_USER_ID = '6';
// this user is pending in all private channels
const PENDING_USER_ID = '7';
// this user is moderator in all channels, member in all communities
const CHANNEL_MODERATOR_USER_ID = '8';
// this user is moderator in all communities
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';
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';
const SPECTRUM_PRIVATE_CHANNEL_ID = '2';
const PAYMENTS_GENERAL_CHANNEL_ID = '3';
const PAYMENTS_PRIVATE_CHANNEL_ID = '4';
const PAYMENTS_FEATURES_CHANNEL_ID = '10';
const SPECTRUM_ARCHIVED_CHANNEL_ID = '5';
const SPECTRUM_DELETED_CHANNEL_ID = '6';
const DELETED_COMMUNITY_DELETED_CHANNEL_ID = '7';
const MODERATOR_CREATED_CHANNEL_ID = '8';
const PRIVATE_GENERAL_CHANNEL_ID = '9';
const SINGLE_CHANNEL_COMMUNITY_GENERAL_CHANNEL_ID = '11';
module.exports = {
DATE,
MAX_ID,
BRIAN_ID,
BRYN_ID,
BLOCKED_USER_ID,
QUIET_USER_ID,
PREVIOUS_MEMBER_USER_ID,
PENDING_USER_ID,
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,
PRIVATE_COMMUNITY_ID,
SINGLE_CHANNEL_COMMUNITY_ID,
SPECTRUM_GENERAL_CHANNEL_ID,
SPECTRUM_PRIVATE_CHANNEL_ID,
PAYMENTS_GENERAL_CHANNEL_ID,
PAYMENTS_PRIVATE_CHANNEL_ID,
PAYMENTS_FEATURES_CHANNEL_ID,
SPECTRUM_ARCHIVED_CHANNEL_ID,
SPECTRUM_DELETED_CHANNEL_ID,
DELETED_COMMUNITY_DELETED_CHANNEL_ID,
MODERATOR_CREATED_CHANNEL_ID,
PRIVATE_GENERAL_CHANNEL_ID,
SINGLE_CHANNEL_COMMUNITY_GENERAL_CHANNEL_ID,
};
================================================
FILE: api/migrations/seed/default/directMessageThreads.js
================================================
// @flow
const constants = require('./constants');
const { DATE } = constants;
module.exports = [
{
id: 'dm-1',
createdAt: new Date(DATE),
name: null,
threadLastActive: new Date(DATE),
},
{
id: 'dm-2',
createdAt: new Date(DATE - 1),
name: null,
threadLastActive: new Date(DATE - 1),
},
];
================================================
FILE: api/migrations/seed/default/index.js
================================================
// @flow
const constants = require('./constants');
const defaultUsers = require('./users');
const defaultCommunities = require('./communities');
const defaultChannels = require('./channels');
const defaultThreads = require('./threads');
const defaultUsersThreads = require('./usersThreads');
const defaultDirectMessageThreads = require('./directMessageThreads');
const defaultUsersDirectMessageThreads = require('./usersDirectMessageThreads');
const defaultUsersCommunities = require('./usersCommunities');
const defaultUsersChannels = require('./usersChannels');
const defaultUsersSettings = require('./usersSettings')();
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,
defaultUsers,
defaultCommunities,
defaultChannels,
defaultThreads,
defaultUsersThreads,
defaultDirectMessageThreads,
defaultUsersDirectMessageThreads,
defaultUsersCommunities,
defaultUsersChannels,
defaultMessages,
defaultUsersSettings,
defaultNotifications,
defaultUsersNotifications,
defaultCommunitySettings,
defaultChannelSettings,
defaultReactions,
};
================================================
FILE: api/migrations/seed/default/messages.js
================================================
// @flow
const constants = require('./constants');
const { fromPlainText, toJSON } = require('../../../../shared/draft-utils');
const { DATE, MAX_ID, BRYN_ID, BRIAN_ID } = constants;
module.exports = [
{
id: '1',
threadId: 'thread-1',
content: {
body: JSON.stringify({
blocks: [
{
key: '9u8bg',
text: 'This is the first message!',
type: 'unstyled',
depth: 0,
inlineStyleRanges: [],
entityRanges: [],
data: {},
},
],
entityMap: {},
}),
},
messageType: 'draftjs',
threadType: 'story',
senderId: MAX_ID,
timestamp: new Date(DATE),
},
{
id: '2',
threadId: 'thread-1',
content: {
body: JSON.stringify(
toJSON(fromPlainText('This is the second message!'))
),
},
messageType: 'draftjs',
threadType: 'story',
senderId: BRYN_ID,
timestamp: new Date(DATE + 1),
},
{
id: '3',
threadId: 'thread-1',
content: {
body: JSON.stringify(
toJSON(fromPlainText('The next one is an emoji-only one :scream:'))
),
},
messageType: 'draftjs',
threadType: 'story',
senderId: MAX_ID,
timestamp: new Date(DATE + 2),
},
{
id: '4',
threadId: 'thread-1',
content: {
body: JSON.stringify(toJSON(fromPlainText('🎉'))),
},
messageType: 'draftjs',
threadType: 'story',
senderId: BRIAN_ID,
timestamp: new Date(DATE + 3),
},
{
id: '5',
threadId: 'thread-2',
content: {
body: JSON.stringify({
blocks: [
{
key: '9u8bg',
text: 'This is the first message!',
type: 'unstyled',
depth: 0,
inlineStyleRanges: [],
entityRanges: [],
data: {},
},
],
entityMap: {},
}),
},
messageType: 'draftjs',
threadType: 'story',
senderId: MAX_ID,
timestamp: new Date(DATE),
},
{
id: '6',
threadId: 'thread-2',
content: {
body: JSON.stringify(
toJSON(fromPlainText('This is the second message!'))
),
},
messageType: 'draftjs',
threadType: 'story',
senderId: BRYN_ID,
timestamp: new Date(DATE + 1),
},
{
id: '7',
threadId: 'thread-2',
content: {
body: JSON.stringify(
toJSON(fromPlainText('The next one is an emoji-only one :scream:'))
),
},
messageType: 'draftjs',
threadType: 'story',
senderId: MAX_ID,
timestamp: new Date(DATE + 2),
},
{
id: '8',
threadId: 'thread-2',
content: {
body: JSON.stringify(toJSON(fromPlainText('🎉'))),
},
messageType: 'draftjs',
threadType: 'story',
senderId: BRIAN_ID,
timestamp: new Date(DATE + 3),
},
// DM Thread
{
id: '9',
threadId: 'dm-1',
threadType: 'directMessageThread',
content: {
body: JSON.stringify(
toJSON(fromPlainText('Direct message thread message!'))
),
},
messageType: 'draftjs',
senderId: MAX_ID,
timestamp: new Date(DATE),
},
{
id: '10',
threadId: 'dm-1',
threadType: 'directMessageThread',
content: {
body: JSON.stringify(toJSON(fromPlainText('A second one'))),
},
messageType: 'draftjs',
senderId: BRYN_ID,
timestamp: new Date(DATE + 50000),
},
{
id: '11',
threadId: 'dm-1',
threadType: 'directMessageThread',
content: {
body: JSON.stringify(toJSON(fromPlainText('A third one'))),
},
messageType: 'draftjs',
senderId: BRIAN_ID,
timestamp: new Date(DATE + 100000),
},
{
id: '12',
threadId: 'dm-1',
threadType: 'directMessageThread',
content: {
body: JSON.stringify(toJSON(fromPlainText('A fourth one'))),
},
messageType: 'draftjs',
senderId: MAX_ID,
timestamp: new Date(DATE + 200000),
},
{
id: '13',
threadId: 'dm-1',
threadType: 'directMessageThread',
content: {
body: JSON.stringify(toJSON(fromPlainText('A fifth one'))),
},
messageType: 'draftjs',
senderId: BRYN_ID,
timestamp: new Date(DATE + 300000),
},
{
id: '14',
threadId: 'thread-6',
threadType: 'story',
content: {
body: `{"blocks":[{"key":"7d3uf","text":"http://localhost:3000/spectrum/general/yet-another-thread~thread-9","type":"unstyled","depth":0,"inlineStyleRanges":[],"entityRanges":[],"data":{}},{"type":"atomic","data":{},"text":" ","depth":0,"entityRanges":[{"offset":0,"length":1,"key":0}],"inlineStyleRanges":[],"key":"7jad1"},{"type":"unstyled","data":{},"text":" ","depth":0,"entityRanges":[],"inlineStyleRanges":[],"key":"7a0pk"}],"entityMap":{"0":{"data":{"type":"internal","id":"thread-9","entity":"thread"},"mutability":"MUTABLE","type":"embed"}}}`,
},
messageType: 'draftjs',
senderId: BRIAN_ID,
timestamp: new Date(DATE + 300000),
},
];
================================================
FILE: api/migrations/seed/default/notifications.js
================================================
// @flow
const constants = require('./constants');
const users = require('./users');
const { DATE, BRIAN_ID, PREVIOUS_MEMBER_USER_ID } = constants;
module.exports = [
{
actors: [
{
id: PREVIOUS_MEMBER_USER_ID,
payload: JSON.stringify(
users.find(u => u.id === PREVIOUS_MEMBER_USER_ID)
),
type: 'USER',
},
],
context: {
id: 'dm-2',
payload: '',
type: 'DIRECT_MESSAGE_THREAD',
},
createdAt: new Date(DATE + 1),
entities: [
{
id: '1',
payload: '',
type: 'MESSAGE',
},
],
event: 'MESSAGE_CREATED',
id: '1',
modifiedAt: new Date(DATE + 1),
},
];
================================================
FILE: api/migrations/seed/default/reactions.js
================================================
// @flow
const constants = require('./constants');
const { DATE, MAX_ID } = constants;
module.exports = [
{
id: '1',
messageId: '4',
type: 'like',
senderId: MAX_ID,
timestamp: new Date(DATE + 4),
},
];
================================================
FILE: api/migrations/seed/default/threads.js
================================================
// @flow
const { fromPlainText, toJSON } = require('../../../../shared/draft-utils');
const constants = require('./constants');
const {
DATE,
BRIAN_ID,
MAX_ID,
BRYN_ID,
SPECTRUM_GENERAL_CHANNEL_ID,
PRIVATE_GENERAL_CHANNEL_ID,
SPECTRUM_PRIVATE_CHANNEL_ID,
DELETED_COMMUNITY_DELETED_CHANNEL_ID,
MODERATOR_CREATED_CHANNEL_ID,
DELETED_COMMUNITY_ID,
SPECTRUM_COMMUNITY_ID,
PRIVATE_COMMUNITY_ID,
SPECTRUM_ARCHIVED_CHANNEL_ID,
} = constants;
module.exports = [
{
id: 'thread-1',
createdAt: new Date(DATE),
creatorId: BRIAN_ID,
channelId: SPECTRUM_GENERAL_CHANNEL_ID,
communityId: SPECTRUM_COMMUNITY_ID,
isPublished: true,
isLocked: false,
type: 'DRAFTJS',
content: {
title: 'The first thread! 🎉',
body: JSON.stringify(
toJSON(fromPlainText('This is it, we got a thread here'))
),
},
edits: [
{
timestamp: new Date(DATE),
content: {
title: 'The first thread! 🎉',
body: JSON.stringify(
toJSON(fromPlainText('This is it, we got a thread here'))
),
},
},
],
modifiedAt: new Date(DATE),
lastActive: new Date(DATE),
messageCount: 4,
reactionCount: 0,
},
{
id: 'thread-2',
createdAt: new Date(DATE + 1),
creatorId: MAX_ID,
channelId: SPECTRUM_GENERAL_CHANNEL_ID,
communityId: SPECTRUM_COMMUNITY_ID,
isPublished: true,
isLocked: false,
type: 'DRAFTJS',
content: {
title: 'Another thread',
body: JSON.stringify(
toJSON(fromPlainText('This is just another thread'))
),
},
edits: [
{
timestamp: new Date(DATE + 1),
content: {
title: 'Another thread',
body: JSON.stringify(
toJSON(fromPlainText('This is just another thread'))
),
},
},
],
modifiedAt: new Date(DATE + 1),
lastActive: new Date(DATE + 1),
messageCount: 4,
reactionCount: 0,
},
{
id: 'thread-3',
createdAt: new Date(DATE + 2),
creatorId: BRYN_ID,
channelId: SPECTRUM_GENERAL_CHANNEL_ID,
communityId: SPECTRUM_COMMUNITY_ID,
isPublished: true,
isLocked: false,
type: 'DRAFTJS',
content: {
title: 'Yet another thread',
body: JSON.stringify(
toJSON(fromPlainText('This is just another thread'))
),
},
edits: [
{
timestamp: new Date(DATE + 2),
content: {
title: 'Yet another thread',
body: JSON.stringify(
toJSON(fromPlainText('This is just another thread'))
),
},
},
],
modifiedAt: new Date(DATE + 2),
lastActive: new Date(DATE + 2),
messageCount: 0,
reactionCount: 0,
},
{
id: 'thread-4',
createdAt: new Date(DATE),
creatorId: BRIAN_ID,
channelId: SPECTRUM_PRIVATE_CHANNEL_ID,
communityId: SPECTRUM_COMMUNITY_ID,
isPublished: true,
isLocked: false,
type: 'DRAFTJS',
content: {
title: 'The first thread! 🎉',
body: JSON.stringify(
toJSON(fromPlainText('This is it, we got a thread here'))
),
},
edits: [
{
timestamp: new Date(DATE),
content: {
title: 'The first thread! 🎉',
body: JSON.stringify(
toJSON(fromPlainText('This is it, we got a thread here'))
),
},
},
],
modifiedAt: new Date(DATE),
lastActive: new Date(DATE),
messageCount: 0,
reactionCount: 0,
},
{
id: 'thread-5',
createdAt: new Date(DATE + 1),
creatorId: MAX_ID,
channelId: SPECTRUM_PRIVATE_CHANNEL_ID,
communityId: SPECTRUM_COMMUNITY_ID,
isPublished: true,
isLocked: false,
type: 'DRAFTJS',
content: {
title: 'Another thread',
body: JSON.stringify(
toJSON(fromPlainText('This is just another thread'))
),
},
edits: [
{
timestamp: new Date(DATE + 1),
content: {
title: 'Another thread',
body: JSON.stringify(
toJSON(fromPlainText('This is just another thread'))
),
},
},
],
modifiedAt: new Date(DATE + 1),
lastActive: new Date(DATE + 1),
messageCount: 0,
reactionCount: 0,
},
{
id: 'thread-6',
createdAt: new Date(DATE + 2),
creatorId: BRYN_ID,
channelId: SPECTRUM_PRIVATE_CHANNEL_ID,
communityId: SPECTRUM_COMMUNITY_ID,
isPublished: true,
isLocked: false,
type: 'DRAFTJS',
content: {
title: 'Yet another thread',
body: JSON.stringify(
toJSON(fromPlainText('This is just another thread'))
),
},
edits: [
{
timestamp: new Date(DATE + 2),
content: {
title: 'Yet another thread',
body: JSON.stringify(
toJSON(fromPlainText('This is just another thread'))
),
},
},
],
modifiedAt: new Date(DATE + 2),
lastActive: new Date(DATE + 2),
messageCount: 0,
reactionCount: 0,
},
{
id: 'thread-7',
createdAt: new Date(DATE + 2),
creatorId: BRYN_ID,
channelId: DELETED_COMMUNITY_DELETED_CHANNEL_ID,
communityId: DELETED_COMMUNITY_ID,
isPublished: true,
isLocked: false,
type: 'DRAFTJS',
content: {
title: 'Yet another thread',
body: JSON.stringify(
toJSON(fromPlainText('This is just another thread'))
),
},
edits: [
{
timestamp: new Date(DATE + 2),
content: {
title: 'Yet another thread',
body: JSON.stringify(
toJSON(fromPlainText('This is just another thread'))
),
},
},
],
modifiedAt: new Date(DATE + 2),
lastActive: new Date(DATE + 2),
deletedAt: new Date(DATE + 3),
messageCount: 0,
reactionCount: 0,
},
{
id: 'thread-8',
createdAt: new Date(DATE + 2),
creatorId: BRYN_ID,
channelId: SPECTRUM_ARCHIVED_CHANNEL_ID,
communityId: SPECTRUM_COMMUNITY_ID,
isPublished: true,
isLocked: false,
type: 'DRAFTJS',
content: {
title: 'Yet another thread',
body: JSON.stringify(
toJSON(fromPlainText('This is just another thread'))
),
},
edits: [
{
timestamp: new Date(DATE + 2),
content: {
title: 'Yet another thread',
body: JSON.stringify(
toJSON(fromPlainText('This is just another thread'))
),
},
},
],
modifiedAt: new Date(DATE + 2),
lastActive: new Date(DATE + 2),
messageCount: 0,
reactionCount: 0,
},
{
id: 'thread-9',
createdAt: new Date(DATE + 2),
creatorId: BRYN_ID,
channelId: SPECTRUM_GENERAL_CHANNEL_ID,
communityId: SPECTRUM_COMMUNITY_ID,
isPublished: true,
isLocked: true,
type: 'DRAFTJS',
content: {
title: 'Yet another thread',
body: JSON.stringify(
toJSON(fromPlainText('This is just another thread'))
),
},
edits: [
{
timestamp: new Date(DATE + 2),
content: {
title: 'Yet another thread',
body: JSON.stringify(
toJSON(fromPlainText('This is just another thread'))
),
},
},
],
modifiedAt: new Date(DATE + 2),
lastActive: new Date(DATE + 2),
},
{
id: 'thread-10',
createdAt: new Date(DATE + 2),
creatorId: BRYN_ID,
channelId: MODERATOR_CREATED_CHANNEL_ID,
communityId: SPECTRUM_COMMUNITY_ID,
isPublished: true,
isLocked: false,
type: 'DRAFTJS',
content: {
title: 'Yet another thread',
body: JSON.stringify(
toJSON(fromPlainText('This is just another thread'))
),
},
edits: [
{
timestamp: new Date(DATE + 2),
content: {
title: 'Yet another thread',
body: JSON.stringify(
toJSON(fromPlainText('This is just another thread'))
),
},
},
],
modifiedAt: new Date(DATE + 2),
lastActive: new Date(DATE + 2),
messageCount: 0,
reactionCount: 0,
},
{
id: 'thread-11',
createdAt: new Date(DATE + 2),
creatorId: BRYN_ID,
channelId: SPECTRUM_GENERAL_CHANNEL_ID,
communityId: SPECTRUM_COMMUNITY_ID,
isPublished: true,
isLocked: true,
type: 'DRAFTJS',
content: {
title: 'Deleted thread',
body: JSON.stringify(toJSON(fromPlainText('This is a deleted thread'))),
},
edits: [
{
timestamp: new Date(DATE + 2),
content: {
title: 'Deleted thread',
body: JSON.stringify(
toJSON(fromPlainText('This is a deleted thread'))
),
},
},
],
modifiedAt: new Date(DATE + 2),
lastActive: new Date(DATE + 2),
deletedAt: new Date(DATE + 3),
messageCount: 0,
reactionCount: 0,
},
{
id: 'thread-12',
createdAt: new Date(DATE + 2),
creatorId: BRYN_ID,
channelId: DELETED_COMMUNITY_DELETED_CHANNEL_ID,
communityId: DELETED_COMMUNITY_ID,
isPublished: true,
isLocked: false,
type: 'DRAFTJS',
content: {
title: 'Yet another thread',
body: JSON.stringify(
toJSON(fromPlainText('This is just another thread'))
),
},
edits: [
{
timestamp: new Date(DATE + 2),
content: {
title: 'Yet another thread',
body: JSON.stringify(
toJSON(fromPlainText('This is just another thread'))
),
},
},
],
modifiedAt: new Date(DATE + 2),
lastActive: new Date(DATE + 2),
deletedAt: new Date(DATE),
messageCount: 0,
reactionCount: 0,
},
{
id: 'thread-13',
createdAt: new Date(DATE + 2),
creatorId: MAX_ID,
channelId: PRIVATE_GENERAL_CHANNEL_ID,
communityId: PRIVATE_COMMUNITY_ID,
isPublished: true,
isLocked: false,
type: 'DRAFTJS',
content: {
title: 'Yet another thread',
body: JSON.stringify(
toJSON(fromPlainText('This is just another thread'))
),
},
edits: [
{
timestamp: new Date(DATE + 2),
content: {
title: 'Yet another thread',
body: JSON.stringify(
toJSON(fromPlainText('This is just another thread'))
),
},
},
],
modifiedAt: new Date(DATE + 2),
lastActive: new Date(DATE + 2),
messageCount: 0,
reactionCount: 0,
},
];
================================================
FILE: api/migrations/seed/default/users.js
================================================
// @flow
const constants = require('./constants');
const {
MAX_ID,
BRIAN_ID,
BRYN_ID,
QUIET_USER_ID,
BLOCKED_USER_ID,
PREVIOUS_MEMBER_USER_ID,
CHANNEL_MODERATOR_USER_ID,
COMMUNITY_MODERATOR_USER_ID,
SINGLE_CHANNEL_COMMUNITY_USER_ID,
NEW_USER_ID,
DATE,
} = constants;
module.exports = [
{
id: MAX_ID,
name: 'Max Stoiber',
description:
'Makes styled-components, react-boilerplate and micro-analytics 💅 Speciality coffee geek, skier, traveller ☕',
website: 'https://mxstbr.com',
username: 'mxstbr',
profilePhoto: 'https://img.gs/jztmrqvgzv/500/mxstbr.com/headshot.jpeg',
coverPhoto:
'https://pbs.twimg.com/profile_banners/2451223458/1479507323/1500x500',
email: 'contact@mxstbr.com',
providerId: '2451223458',
createdAt: new Date(DATE),
lastSeen: new Date(DATE),
},
{
id: BRIAN_ID,
name: 'Brian Lovin',
description: 'Chief Nice Boy™',
website: 'https://brianlovin.com',
username: 'brian',
profilePhoto:
'https://pbs.twimg.com/profile_images/570313913648955392/cf4tgX7M_bigger.jpeg',
coverPhoto:
'https://pbs.twimg.com/profile_banners/465068802/1490051733/1500x500',
email: 'briandlovin@gmail.com',
providerId: '465068802',
createdAt: new Date(DATE),
lastSeen: new Date(DATE),
},
{
id: BRYN_ID,
name: 'Bryn Jackson',
description: 'full-stack flapjack',
website: 'https://bryn.io',
username: 'bryn',
profilePhoto:
'https://pbs.twimg.com/profile_images/848823167699230721/-9CbPtto_bigger.jpg',
coverPhoto:
'https://pbs.twimg.com/profile_banners/17106008/1491444958/1500x500',
email: 'hi@bryn.io',
providerId: '17106008',
createdAt: new Date(DATE),
lastSeen: new Date(DATE),
},
{
id: QUIET_USER_ID,
name: 'Quiet user',
description: "I've never joined anything on Spectrum",
website: '',
username: 'quiet-user',
profilePhoto:
'https://pbs.twimg.com/profile_images/848823167699230721/-9CbPtto_bigger.jpg',
coverPhoto:
'https://pbs.twimg.com/profile_banners/17106008/1491444958/1500x500',
email: 'hi@quietuser.com',
createdAt: new Date(DATE),
lastSeen: new Date(DATE),
},
{
id: BLOCKED_USER_ID,
name: 'Blocked user',
description: 'I am blocked in the Spectrum community',
website: '',
username: 'blocked-user',
profilePhoto:
'https://pbs.twimg.com/profile_images/848823167699230721/-9CbPtto_bigger.jpg',
coverPhoto:
'https://pbs.twimg.com/profile_banners/17106008/1491444958/1500x500',
email: 'hi@blockeduser.com',
createdAt: new Date(DATE),
lastSeen: new Date(DATE),
},
{
id: PREVIOUS_MEMBER_USER_ID,
name: 'Previous member',
description: 'I used to be in the Spectrum community, but then left',
website: '',
username: 'previous-user',
profilePhoto:
'https://pbs.twimg.com/profile_images/848823167699230721/-9CbPtto_bigger.jpg',
coverPhoto:
'https://pbs.twimg.com/profile_banners/17106008/1491444958/1500x500',
email: 'hi@previousboy.io',
createdAt: new Date(DATE),
lastSeen: new Date(DATE),
},
{
id: CHANNEL_MODERATOR_USER_ID,
name: 'Channel moderator',
description: 'I moderate all channels',
website: '',
username: 'channel-moderator-user',
profilePhoto:
'https://pbs.twimg.com/profile_images/848823167699230721/-9CbPtto_bigger.jpg',
coverPhoto:
'https://pbs.twimg.com/profile_banners/17106008/1491444958/1500x500',
email: 'hi@channelmoderatorboy.io',
createdAt: new Date(DATE),
lastSeen: new Date(DATE),
},
{
id: COMMUNITY_MODERATOR_USER_ID,
name: 'Community moderator',
description: 'I moderate all communities',
website: '',
username: 'community-moderator-user',
profilePhoto:
'https://pbs.twimg.com/profile_images/848823167699230721/-9CbPtto_bigger.jpg',
coverPhoto:
'https://pbs.twimg.com/profile_banners/17106008/1491444958/1500x500',
email: 'hi@communitymoderatorboy.io',
createdAt: new Date(DATE),
lastSeen: new Date(DATE),
},
{
id: SINGLE_CHANNEL_COMMUNITY_USER_ID,
name: 'Single community person',
description: 'Im a member of one community',
website: '',
username: 'single-community-user',
profilePhoto:
'https://pbs.twimg.com/profile_images/848823167699230721/-9CbPtto_bigger.jpg',
coverPhoto:
'https://pbs.twimg.com/profile_banners/17106008/1491444958/1500x500',
email: 'hi@singlecommunity.io',
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),
},
];
================================================
FILE: api/migrations/seed/default/usersChannels.js
================================================
// @flow
const constants = require('./constants');
const {
DATE,
BRIAN_ID,
MAX_ID,
BRYN_ID,
BLOCKED_USER_ID,
PREVIOUS_MEMBER_USER_ID,
CHANNEL_MODERATOR_USER_ID,
COMMUNITY_MODERATOR_USER_ID,
SINGLE_CHANNEL_COMMUNITY_USER_ID,
SPECTRUM_GENERAL_CHANNEL_ID,
PRIVATE_GENERAL_CHANNEL_ID,
SPECTRUM_ARCHIVED_CHANNEL_ID,
SPECTRUM_PRIVATE_CHANNEL_ID,
DELETED_COMMUNITY_DELETED_CHANNEL_ID,
PAYMENTS_GENERAL_CHANNEL_ID,
PAYMENTS_PRIVATE_CHANNEL_ID,
PAYMENTS_FEATURES_CHANNEL_ID,
MODERATOR_CREATED_CHANNEL_ID,
SINGLE_CHANNEL_COMMUNITY_GENERAL_CHANNEL_ID,
} = constants;
module.exports = [
{
id: '1',
createdAt: new Date(DATE),
userId: MAX_ID,
channelId: SPECTRUM_GENERAL_CHANNEL_ID,
isOwner: true,
isModerator: false,
isMember: true,
isBlocked: false,
isPending: false,
receiveNotifications: true,
},
{
id: '2',
createdAt: new Date(DATE),
userId: BRIAN_ID,
channelId: SPECTRUM_GENERAL_CHANNEL_ID,
isOwner: false,
isModerator: false,
isMember: true,
isBlocked: false,
isPending: false,
receiveNotifications: true,
},
{
id: '3',
createdAt: new Date(DATE),
userId: BRYN_ID,
channelId: SPECTRUM_GENERAL_CHANNEL_ID,
isOwner: false,
isModerator: false,
isMember: true,
isBlocked: false,
isPending: false,
receiveNotifications: true,
},
{
id: '4',
createdAt: new Date(DATE),
userId: BLOCKED_USER_ID,
channelId: SPECTRUM_GENERAL_CHANNEL_ID,
isOwner: false,
isModerator: false,
isMember: false,
isBlocked: true,
isPending: false,
receiveNotifications: true,
},
{
id: '5',
createdAt: new Date(DATE),
userId: MAX_ID,
channelId: SPECTRUM_PRIVATE_CHANNEL_ID,
isOwner: true,
isModerator: false,
isMember: true,
isBlocked: false,
isPending: false,
receiveNotifications: true,
},
{
id: '6',
createdAt: new Date(DATE),
userId: BRIAN_ID,
channelId: SPECTRUM_PRIVATE_CHANNEL_ID,
isOwner: false,
isModerator: false,
isMember: true,
isBlocked: false,
isPending: false,
receiveNotifications: true,
},
{
id: '7',
createdAt: new Date(DATE),
userId: BRYN_ID,
channelId: SPECTRUM_PRIVATE_CHANNEL_ID,
isOwner: false,
isModerator: false,
isMember: true,
isBlocked: false,
isPending: false,
receiveNotifications: true,
},
{
id: '8',
createdAt: new Date(DATE),
userId: BLOCKED_USER_ID,
channelId: SPECTRUM_PRIVATE_CHANNEL_ID,
isOwner: false,
isModerator: false,
isMember: false,
isBlocked: true,
isPending: false,
receiveNotifications: true,
},
{
id: '9',
createdAt: new Date(DATE),
userId: MAX_ID,
channelId: PAYMENTS_GENERAL_CHANNEL_ID,
isOwner: true,
isModerator: false,
isMember: true,
isBlocked: false,
isPending: false,
receiveNotifications: true,
},
{
id: '10',
createdAt: new Date(DATE),
userId: BRIAN_ID,
channelId: PAYMENTS_GENERAL_CHANNEL_ID,
isOwner: false,
isModerator: false,
isMember: true,
isBlocked: false,
isPending: false,
receiveNotifications: true,
},
{
id: '32',
createdAt: new Date(DATE),
userId: BRIAN_ID,
channelId: PAYMENTS_FEATURES_CHANNEL_ID,
isOwner: true,
isModerator: false,
isMember: true,
isBlocked: false,
isPending: false,
receiveNotifications: true,
},
{
id: '11',
createdAt: new Date(DATE),
userId: BRYN_ID,
channelId: PAYMENTS_GENERAL_CHANNEL_ID,
isOwner: false,
isModerator: false,
isMember: true,
isBlocked: false,
isPending: false,
receiveNotifications: true,
},
{
id: '12',
createdAt: new Date(DATE),
userId: BLOCKED_USER_ID,
channelId: PAYMENTS_GENERAL_CHANNEL_ID,
isOwner: false,
isModerator: false,
isMember: false,
isBlocked: true,
isPending: false,
receiveNotifications: true,
},
{
id: '13',
createdAt: new Date(DATE),
userId: MAX_ID,
channelId: PAYMENTS_PRIVATE_CHANNEL_ID,
isOwner: true,
isModerator: false,
isMember: true,
isBlocked: false,
isPending: false,
receiveNotifications: true,
},
{
id: '14',
createdAt: new Date(DATE),
userId: BRIAN_ID,
channelId: PAYMENTS_PRIVATE_CHANNEL_ID,
isOwner: false,
isModerator: false,
isMember: true,
isBlocked: false,
isPending: false,
receiveNotifications: true,
},
{
id: '15',
createdAt: new Date(DATE),
userId: BRYN_ID,
channelId: PAYMENTS_PRIVATE_CHANNEL_ID,
isOwner: false,
isModerator: false,
isMember: true,
isBlocked: false,
isPending: false,
receiveNotifications: true,
},
{
id: '16',
createdAt: new Date(DATE),
userId: BLOCKED_USER_ID,
channelId: PAYMENTS_PRIVATE_CHANNEL_ID,
isOwner: false,
isModerator: false,
isMember: false,
isBlocked: true,
isPending: false,
receiveNotifications: true,
},
{
id: '17',
createdAt: new Date(DATE),
userId: MAX_ID,
channelId: SPECTRUM_ARCHIVED_CHANNEL_ID,
isOwner: true,
isModerator: false,
isMember: true,
isBlocked: false,
isPending: false,
receiveNotifications: true,
},
{
id: '18',
createdAt: new Date(DATE),
userId: CHANNEL_MODERATOR_USER_ID,
channelId: SPECTRUM_GENERAL_CHANNEL_ID,
isOwner: false,
isModerator: true,
isMember: true,
isBlocked: false,
isPending: false,
receiveNotifications: true,
},
{
id: '19',
createdAt: new Date(DATE),
userId: CHANNEL_MODERATOR_USER_ID,
channelId: SPECTRUM_ARCHIVED_CHANNEL_ID,
isOwner: false,
isModerator: true,
isMember: true,
isBlocked: false,
isPending: false,
receiveNotifications: true,
},
{
id: '20',
createdAt: new Date(DATE),
userId: CHANNEL_MODERATOR_USER_ID,
channelId: SPECTRUM_PRIVATE_CHANNEL_ID,
isOwner: false,
isModerator: true,
isMember: true,
isBlocked: false,
isPending: false,
receiveNotifications: true,
},
{
id: '21',
createdAt: new Date(DATE),
userId: CHANNEL_MODERATOR_USER_ID,
channelId: PAYMENTS_GENERAL_CHANNEL_ID,
isOwner: false,
isModerator: true,
isMember: true,
isBlocked: false,
isPending: false,
receiveNotifications: true,
},
{
id: '22',
createdAt: new Date(DATE),
userId: CHANNEL_MODERATOR_USER_ID,
channelId: PAYMENTS_PRIVATE_CHANNEL_ID,
isOwner: false,
isModerator: true,
isMember: true,
isBlocked: false,
isPending: false,
receiveNotifications: true,
},
{
id: '23',
createdAt: new Date(DATE),
userId: CHANNEL_MODERATOR_USER_ID,
channelId: MODERATOR_CREATED_CHANNEL_ID,
isOwner: true,
isModerator: false,
isMember: true,
isBlocked: false,
isPending: false,
receiveNotifications: true,
},
{
id: '24',
createdAt: new Date(DATE),
userId: COMMUNITY_MODERATOR_USER_ID,
channelId: SPECTRUM_GENERAL_CHANNEL_ID,
isOwner: false,
isModerator: false,
isMember: true,
isBlocked: false,
isPending: false,
receiveNotifications: true,
},
{
id: '25',
createdAt: new Date(DATE),
userId: COMMUNITY_MODERATOR_USER_ID,
channelId: SPECTRUM_ARCHIVED_CHANNEL_ID,
isOwner: false,
isModerator: false,
isMember: true,
isBlocked: false,
isPending: false,
receiveNotifications: true,
},
{
id: '26',
createdAt: new Date(DATE),
userId: COMMUNITY_MODERATOR_USER_ID,
channelId: SPECTRUM_PRIVATE_CHANNEL_ID,
isOwner: false,
isModerator: false,
isMember: true,
isBlocked: false,
isPending: false,
receiveNotifications: true,
},
{
id: '27',
createdAt: new Date(DATE),
userId: COMMUNITY_MODERATOR_USER_ID,
channelId: PAYMENTS_GENERAL_CHANNEL_ID,
isOwner: false,
isModerator: false,
isMember: true,
isBlocked: false,
isPending: false,
receiveNotifications: true,
},
{
id: '28',
createdAt: new Date(DATE),
userId: COMMUNITY_MODERATOR_USER_ID,
channelId: PAYMENTS_PRIVATE_CHANNEL_ID,
isOwner: false,
isModerator: false,
isMember: true,
isBlocked: false,
isPending: false,
receiveNotifications: true,
},
{
id: '29',
createdAt: new Date(DATE),
userId: BRIAN_ID,
channelId: DELETED_COMMUNITY_DELETED_CHANNEL_ID,
isOwner: false,
isModerator: false,
isMember: true,
isBlocked: false,
isPending: false,
receiveNotifications: true,
},
{
id: '30',
createdAt: new Date(DATE),
userId: PREVIOUS_MEMBER_USER_ID,
channelId: SPECTRUM_GENERAL_CHANNEL_ID,
isOwner: false,
isModerator: false,
isMember: false,
isBlocked: false,
isPending: false,
receiveNotifications: false,
},
{
id: '31',
createdAt: new Date(DATE),
userId: MAX_ID,
channelId: PRIVATE_GENERAL_CHANNEL_ID,
isOwner: true,
isModerator: false,
isMember: true,
isBlocked: false,
isPending: false,
receiveNotifications: false,
},
{
id: '33',
createdAt: new Date(DATE),
userId: BRIAN_ID,
gitextract_m8f95147/
├── .babelrc
├── .circleci/
│ └── config.yml
├── .dockerignore
├── .eslintignore
├── .eslintrc.js
├── .flowconfig
├── .github/
│ ├── ISSUE_TEMPLATE.md
│ └── PULL_REQUEST_TEMPLATE.md
├── .gitignore
├── .npmignore
├── .prettierignore
├── .prettierrc
├── LICENSE
├── README.md
├── api/
│ ├── apollo-server.js
│ ├── authentication.js
│ ├── index.js
│ ├── loaders/
│ │ ├── channel.js
│ │ ├── community.js
│ │ ├── create-loader.js
│ │ ├── directMessageThread.js
│ │ ├── index.js
│ │ ├── message.js
│ │ ├── reaction.js
│ │ ├── thread.js
│ │ ├── threadReaction.js
│ │ ├── types.js
│ │ └── user.js
│ ├── migrations/
│ │ ├── 20170410074258-initial-data.js
│ │ ├── 20170613200350-notifications.js
│ │ ├── 20170616113103-compound-indexes-for-ordering.js
│ │ ├── 20170627104435-user-email-settings.js
│ │ ├── 20170701173337-linkify-messages.js
│ │ ├── 20170702194221-fix-images.js
│ │ ├── 20170706114239-providerfield-indexes.js
│ │ ├── 20170706205658-slack-import.js
│ │ ├── 20170714171920-web-push-subscription.js
│ │ ├── 20170724184557-notifications-entity-added-index.js
│ │ ├── 20170803104302-dedupe-users-settings.js
│ │ ├── 20170825220615-clean-recurring-payments.js
│ │ ├── 20170829233734-userid-index-on-invoices.js
│ │ ├── 20170831163211-invoice-data-model-update.js
│ │ ├── 20170907222544-digest-email-notification-settings.js
│ │ ├── 20170908230623-add-reputation-field-to-communities.js
│ │ ├── 20170912000619-backfill-rep.js
│ │ ├── 20170915201609-clean-up-bad-dm-data.js
│ │ ├── 20170926003025-activate-daily-weekly-digest-settings.js
│ │ ├── 20170926102527-speedy-gonzales.js
│ │ ├── 20170927002438-communityid-index-on-reputation-events.js
│ │ ├── 20170928143435-slate-to-draftjs.js.js
│ │ ├── 20171005075445-remove-markdown-links-from-messages.js
│ │ ├── 20171008101118-last-slate-to-draft.js
│ │ ├── 20171013195530-core-metrics-table.js
│ │ ├── 20171018235659-add-direst-message-user-settings.js
│ │ ├── 20171029090619-users-channels-index.js
│ │ ├── 20171029094352-users-threads-index.js
│ │ ├── 20171103014955-add-mention-notification-settings.js
│ │ ├── 20171129215512-index-communities-by-slug.js
│ │ ├── 20171129221050-curated-content-table-creation.js
│ │ ├── 20171208175038-index-users-for-search.js
│ │ ├── 20171208180800-index-communities-for-search.js
│ │ ├── 20171213002813-add-modified-at-field-to-users-and-communities.js
│ │ ├── 20180209015734-github-provider-id-index.js
│ │ ├── 20180214111357-expo-push-subscriptions.js
│ │ ├── 20180309144845-create-community-settings-table.js
│ │ ├── 20180316195507-create-channel-settings-table.js
│ │ ├── 20180320122000-create-stripe-tables.js
│ │ ├── 20180320173414-set-administrator-info-on-community.js
│ │ ├── 20180411183454-lowercase-all-the-slugs.js
│ │ ├── 20180428001543-reset-slack-import-records.js
│ │ ├── 20180504003702-encrypt-existing-slack-data.js
│ │ ├── 20180517180716-enable-private-communities.js
│ │ ├── 20180517215503-add-ispending-to-userscommunities.js
│ │ ├── 20180518135040-add-join-settings-to-community-settings.js
│ │ ├── 20180621001409-thread-likes-table.js
│ │ ├── 20180823115847-add-users-communities-indexes.js
│ │ ├── 20181001061156-thread-metadata-denormalization.js
│ │ ├── 20181001064151-fix-thread-metadata-message-counts.js
│ │ ├── 20181002060237-remove-payments.js
│ │ ├── 20181003233411-thread-reactions-useridandthreadid-index.js
│ │ ├── 20181004222636-denormalize-channel-community-member-counts.js
│ │ ├── 20181005143053-users-notifications-useridandnotificationid-index.js
│ │ ├── 20181005144259-users-notifications-userIdAndIsSeen-index.js
│ │ ├── 20181023160027-update-denormalized-member-counts.js
│ │ ├── 20181024173616-indexes-for-digests.js
│ │ ├── 20181027050052-remove-attachments-from-thread-model.js
│ │ ├── 20181102025454-fix-old-image-urls-in-messages.js
│ │ ├── 20181102040518-fix-old-image-urls-in-threads.js
│ │ ├── 20181102044407-fix-old-image-urls-in-communities.js
│ │ ├── 20181102045821-fix-old-image-urls-in-users.js
│ │ ├── 20181102054523-fix-aws-static-url-community-photos.js
│ │ ├── 20181116173949-add-terms-last-accepted-field-to-users.js
│ │ ├── 20181121054300-resync-community-member-counts.js
│ │ ├── 20181122162921-users-communities-useridandmember-index.js
│ │ ├── 20181126094455-users-channels-roles.js
│ │ ├── 20181127090014-communities-member-count-index.js
│ │ ├── 20181205171559-remove-old-users-notifications.js
│ │ ├── 20181211181146-add-usersthreads-user-id-and-participant-index.js
│ │ ├── 20190226085909-bot-user-sam.js
│ │ ├── 20190306125252-threads-watercooler-index.js
│ │ ├── 20190315142923-backfill-userscommunities-last-seen-community-last-active.js
│ │ ├── 20190327134509-delete-bot-messages.js
│ │ ├── config.js
│ │ └── seed/
│ │ ├── default/
│ │ │ ├── channelSettings.js
│ │ │ ├── channels.js
│ │ │ ├── communities.js
│ │ │ ├── communitySettings.js
│ │ │ ├── constants.js
│ │ │ ├── directMessageThreads.js
│ │ │ ├── index.js
│ │ │ ├── messages.js
│ │ │ ├── notifications.js
│ │ │ ├── reactions.js
│ │ │ ├── threads.js
│ │ │ ├── users.js
│ │ │ ├── usersChannels.js
│ │ │ ├── usersCommunities.js
│ │ │ ├── usersDirectMessageThreads.js
│ │ │ ├── usersNotifications.js
│ │ │ ├── usersSettings.js
│ │ │ └── usersThreads.js
│ │ ├── generate.js
│ │ └── index.js
│ ├── models/
│ │ ├── channel.js
│ │ ├── channelSettings.js
│ │ ├── community.js
│ │ ├── communitySettings.js
│ │ ├── curatedContent.js
│ │ ├── directMessageThread.js
│ │ ├── message.js
│ │ ├── reaction.js
│ │ ├── search.js
│ │ ├── session.js
│ │ ├── test/
│ │ │ ├── __snapshots__/
│ │ │ │ └── channel.test.js.snap
│ │ │ └── channel.test.js
│ │ ├── thread.js
│ │ ├── threadReaction.js
│ │ ├── usersChannels.js
│ │ ├── usersCommunities.js
│ │ ├── usersDirectMessageThreads.js
│ │ ├── usersSettings.js
│ │ ├── usersThreads.js
│ │ └── utils.js
│ ├── mutations/
│ │ ├── channel/
│ │ │ ├── deleteChannel.js
│ │ │ ├── editChannel.js
│ │ │ └── index.js
│ │ ├── community/
│ │ │ ├── deleteCommunity.js
│ │ │ ├── editCommunity.js
│ │ │ ├── index.js
│ │ │ ├── toggleCommunityNoindex.js
│ │ │ └── toggleCommunityRedirect.js
│ │ ├── files/
│ │ │ ├── index.js
│ │ │ └── uploadImage.js
│ │ ├── message/
│ │ │ ├── deleteMessage.js
│ │ │ └── index.js
│ │ ├── thread/
│ │ │ ├── deleteThread.js
│ │ │ └── index.js
│ │ └── user/
│ │ ├── banUser.js
│ │ ├── deleteCurrentUser.js
│ │ ├── editUser.js
│ │ └── index.js
│ ├── package.json
│ ├── queries/
│ │ ├── channel/
│ │ │ ├── channelPermissions.js
│ │ │ ├── community.js
│ │ │ ├── communityPermissions.js
│ │ │ ├── index.js
│ │ │ ├── isArchived.js
│ │ │ ├── joinSettings.js
│ │ │ ├── memberConnection.js
│ │ │ ├── memberCount.js
│ │ │ ├── metaData.js
│ │ │ ├── moderators.js
│ │ │ ├── owners.js
│ │ │ ├── rootChannel.js
│ │ │ └── threadConnection.js
│ │ ├── community/
│ │ │ ├── brandedLogin.js
│ │ │ ├── channelConnection.js
│ │ │ ├── communityPermissions.js
│ │ │ ├── contextPermissions.js
│ │ │ ├── coverPhoto.js
│ │ │ ├── index.js
│ │ │ ├── joinSettings.js
│ │ │ ├── memberConnection.js
│ │ │ ├── members.js
│ │ │ ├── metaData.js
│ │ │ ├── pinnedThread.js
│ │ │ ├── profilePhoto.js
│ │ │ ├── rootCommunities.js
│ │ │ ├── rootCommunity.js
│ │ │ ├── rootRecentCommunities.js
│ │ │ ├── rootTopCommunities.js
│ │ │ ├── slackSettings.js
│ │ │ ├── threadConnection.js
│ │ │ └── watercooler.js
│ │ ├── communityMember/
│ │ │ ├── index.js
│ │ │ ├── roles.js
│ │ │ ├── rootCommunityMember.js
│ │ │ └── user.js
│ │ ├── directMessageThread/
│ │ │ ├── index.js
│ │ │ ├── messageConnection.js
│ │ │ ├── participants.js
│ │ │ ├── rootDirectMessageThread.js
│ │ │ ├── rootDirectMessageThreadByUserIds.js
│ │ │ └── snippet.js
│ │ ├── message/
│ │ │ ├── author.js
│ │ │ ├── content.js
│ │ │ ├── index.js
│ │ │ ├── parent.js
│ │ │ ├── reactions.js
│ │ │ ├── rootGetMediaMessagesForThread.js
│ │ │ ├── rootMessage.js
│ │ │ ├── sender.js
│ │ │ └── thread.js
│ │ ├── reaction/
│ │ │ ├── index.js
│ │ │ ├── message.js
│ │ │ ├── reaction.js
│ │ │ └── user.js
│ │ ├── thread/
│ │ │ ├── attachments.js
│ │ │ ├── author.js
│ │ │ ├── channel.js
│ │ │ ├── community.js
│ │ │ ├── content.js
│ │ │ ├── creator.js
│ │ │ ├── editedBy.js
│ │ │ ├── index.js
│ │ │ ├── isAuthor.js
│ │ │ ├── isCreator.js
│ │ │ ├── messageConnection.js
│ │ │ ├── metaImage.js
│ │ │ ├── participants.js
│ │ │ ├── reactions.js
│ │ │ └── rootThread.js
│ │ └── user/
│ │ ├── channelConnection.js
│ │ ├── communityConnection.js
│ │ ├── contextPermissions.js
│ │ ├── coverPhoto.js
│ │ ├── directMessageThreadsConnection.js
│ │ ├── email.js
│ │ ├── everything.js
│ │ ├── githubProfile.js
│ │ ├── index.js
│ │ ├── isAdmin.js
│ │ ├── profilePhoto.js
│ │ ├── rootCurrentUser.js
│ │ ├── rootUser.js
│ │ ├── settings.js
│ │ ├── threadConnection.js
│ │ └── threadCount.js
│ ├── routes/
│ │ ├── api/
│ │ │ ├── export-user-data.js
│ │ │ └── index.js
│ │ ├── auth/
│ │ │ ├── create-signin-routes.js
│ │ │ ├── facebook.js
│ │ │ ├── github.js
│ │ │ ├── google.js
│ │ │ ├── index.js
│ │ │ ├── logout.js
│ │ │ └── twitter.js
│ │ ├── create-subscription-server.js
│ │ └── middlewares/
│ │ └── index.js
│ ├── schema.js
│ ├── subscriptions/
│ │ ├── community.js
│ │ ├── directMessageThread.js
│ │ ├── message.js
│ │ ├── notification.js
│ │ └── thread.js
│ ├── test/
│ │ ├── __snapshots__/
│ │ │ ├── community.test.js.snap
│ │ │ ├── directMessageThread.test.js.snap
│ │ │ └── user.test.js.snap
│ │ ├── channel/
│ │ │ ├── mutations/
│ │ │ │ ├── __snapshots__/
│ │ │ │ │ ├── createChannel.test.js.snap
│ │ │ │ │ ├── deleteChannel.test.js.snap
│ │ │ │ │ └── editChannel.test.js.snap
│ │ │ │ ├── createChannel.test.js
│ │ │ │ ├── deleteChannel.test.js
│ │ │ │ └── editChannel.test.js
│ │ │ └── queries/
│ │ │ ├── __snapshots__/
│ │ │ │ ├── channelSettings.test.js.snap
│ │ │ │ ├── memberConnection.test.js.snap
│ │ │ │ └── root.test.js.snap
│ │ │ ├── channelSettings.test.js
│ │ │ ├── memberConnection.test.js
│ │ │ └── root.test.js
│ │ ├── community/
│ │ │ ├── mutations/
│ │ │ │ ├── __snapshots__/
│ │ │ │ │ └── editCommunity.test.js.snap
│ │ │ │ └── editCommunity.test.js
│ │ │ └── queries/
│ │ │ ├── __snapshots__/
│ │ │ │ └── communitySettings.test.js.snap
│ │ │ └── communitySettings.test.js
│ │ ├── community.test.js
│ │ ├── directMessageThread.test.js
│ │ ├── message/
│ │ │ ├── __snapshots__/
│ │ │ │ └── queries.test.js.snap
│ │ │ ├── mutations/
│ │ │ │ └── addMessage.test.js
│ │ │ └── queries.test.js
│ │ ├── thread/
│ │ │ ├── mutations/
│ │ │ │ ├── __snapshots__/
│ │ │ │ │ ├── deleteThread.test.js.snap
│ │ │ │ │ └── publishThread.test.js.snap
│ │ │ │ ├── deleteThread.test.js
│ │ │ │ └── publishThread.test.js
│ │ │ └── queries/
│ │ │ ├── __snapshots__/
│ │ │ │ ├── messageConnection.test.js.snap
│ │ │ │ └── root.test.js.snap
│ │ │ ├── messageConnection.test.js
│ │ │ ├── reversePagination.test.js
│ │ │ └── root.test.js
│ │ ├── user.test.js
│ │ ├── utils/
│ │ │ ├── __mocks__/
│ │ │ │ └── debug.js
│ │ │ ├── __snapshots__/
│ │ │ │ └── create-graphql-error-formatter.test.js.snap
│ │ │ └── create-graphql-error-formatter.test.js
│ │ └── utils.js
│ ├── types/
│ │ ├── Channel.js
│ │ ├── Community.js
│ │ ├── CommunityMember.js
│ │ ├── DirectMessageThread.js
│ │ ├── Invoice.js
│ │ ├── Message.js
│ │ ├── Reaction.js
│ │ ├── Thread.js
│ │ ├── ThreadParticipant.js
│ │ ├── User.js
│ │ ├── custom-scalars/
│ │ │ └── LowercaseString.js
│ │ ├── general.js
│ │ └── scalars.js
│ └── utils/
│ ├── UserError.js
│ ├── base64.js
│ ├── create-graphql-error-formatter.js
│ ├── file-storage.js
│ ├── file-system.js
│ ├── generate-thread-meta-image-from-text.js
│ ├── get-page-meta.js
│ ├── get-random-default-photo.js
│ ├── is-spectrum-url.js
│ ├── markdown-linkify.js
│ ├── paginate-arrays.js
│ ├── permissions.js
│ ├── s3.js
│ ├── session-store.js
│ └── validate-draft-js-input.js
├── backpack.config.js
├── config-overrides.js
├── cypress/
│ ├── fixtures/
│ │ └── example.json
│ ├── integration/
│ │ ├── channel/
│ │ │ ├── settings/
│ │ │ │ ├── delete_spec.js
│ │ │ │ └── edit_spec.js
│ │ │ └── view/
│ │ │ ├── membership_spec.js
│ │ │ ├── profile_spec.js
│ │ │ └── threads_spec.js
│ │ ├── community/
│ │ │ └── view/
│ │ │ └── profile_spec.js
│ │ ├── community_settings_members_spec.js
│ │ ├── community_settings_overview_spec.js
│ │ ├── explore_spec.js
│ │ ├── login_spec.js
│ │ ├── messages_spec.js
│ │ ├── modal_routes_spec.js
│ │ ├── thread/
│ │ │ ├── action_bar_spec.js
│ │ │ └── view_spec.js
│ │ ├── thread_spec.js
│ │ ├── toasts_spec.js
│ │ ├── user/
│ │ │ ├── delete_user_spec.js
│ │ │ ├── edit_user_spec.js
│ │ │ └── me_redirect_spec.js
│ │ └── user_spec.js
│ ├── plugins/
│ │ └── index.js
│ └── support/
│ ├── commands.js
│ └── index.js
├── cypress.json
├── docker/
│ ├── Dockerfile.api
│ └── Dockerfile.hyperion
├── docs/
│ ├── admin/
│ │ └── intro.md
│ ├── api/
│ │ ├── graphql/
│ │ │ ├── fragments.md
│ │ │ ├── intro.md
│ │ │ ├── pagination.md
│ │ │ ├── testing.md
│ │ │ └── tips-and-tricks.md
│ │ └── intro.md
│ ├── backend/
│ │ └── api/
│ │ ├── README.md
│ │ ├── fragments.md
│ │ ├── pagination.md
│ │ ├── testing.md
│ │ └── tips-and-tricks.md
│ ├── deployments.md
│ ├── hyperion (server side rendering)/
│ │ ├── development.md
│ │ └── intro.md
│ ├── operations/
│ │ ├── hourly-backups.md
│ │ ├── importing-rethinkdb-backups.md
│ │ └── intro.md
│ ├── readme.md
│ └── testing/
│ ├── integration.md
│ ├── intro.md
│ └── unit.md
├── flow-typed/
│ ├── npm/
│ │ ├── @sendgrid/
│ │ │ └── mail_vx.x.x.js
│ │ ├── @tippy.js/
│ │ │ └── react_vx.x.x.js
│ │ ├── @vx/
│ │ │ ├── curve_vx.x.x.js
│ │ │ ├── event_vx.x.x.js
│ │ │ ├── gradient_vx.x.x.js
│ │ │ ├── grid_vx.x.x.js
│ │ │ ├── scale_vx.x.x.js
│ │ │ ├── shape_vx.x.x.js
│ │ │ └── tooltip_vx.x.x.js
│ │ ├── amplitude_vx.x.x.js
│ │ ├── apollo-cache-inmemory_vx.x.x.js
│ │ ├── apollo-client_vx.x.x.js
│ │ ├── apollo-engine_vx.x.x.js
│ │ ├── apollo-link-http_vx.x.x.js
│ │ ├── apollo-link-retry_vx.x.x.js
│ │ ├── apollo-link-schema_vx.x.x.js
│ │ ├── apollo-link-ws_vx.x.x.js
│ │ ├── apollo-link_vx.x.x.js
│ │ ├── apollo-local-query_vx.x.x.js
│ │ ├── apollo-server-cache-redis_vx.x.x.js
│ │ ├── apollo-server-express_vx.x.x.js
│ │ ├── apollo-server-plugin-response-cache_vx.x.x.js
│ │ ├── apollo-upload-client_vx.x.x.js
│ │ ├── apollo-upload-server_vx.x.x.js
│ │ ├── apollo-utilities_vx.x.x.js
│ │ ├── aws-sdk_vx.x.x.js
│ │ ├── axios_v0.17.x.js
│ │ ├── axios_vx.x.x.js
│ │ ├── b2a_vx.x.x.js
│ │ ├── babel-cli_vx.x.x.js
│ │ ├── babel-eslint_vx.x.x.js
│ │ ├── babel-plugin-import-inspector_vx.x.x.js
│ │ ├── babel-plugin-styled-components_vx.x.x.js
│ │ ├── babel-plugin-syntax-async-generators_vx.x.x.js
│ │ ├── babel-plugin-syntax-dynamic-import_vx.x.x.js
│ │ ├── babel-plugin-transform-async-generator-functions_vx.x.x.js
│ │ ├── babel-plugin-transform-class-properties_vx.x.x.js
│ │ ├── babel-plugin-transform-flow-strip-types_vx.x.x.js
│ │ ├── babel-plugin-transform-object-rest-spread_vx.x.x.js
│ │ ├── babel-preset-env_vx.x.x.js
│ │ ├── backpack-core_vx.x.x.js
│ │ ├── bad-words_vx.x.x.js
│ │ ├── bluebird_vx.x.x.js
│ │ ├── body-parser_v1.x.x.js
│ │ ├── casual_vx.x.x.js
│ │ ├── cheerio_vx.x.x.js
│ │ ├── common-tags_v1.4.x.js
│ │ ├── compression_vx.x.x.js
│ │ ├── cookie-parser_vx.x.x.js
│ │ ├── cookie-session_vx.x.x.js
│ │ ├── cors_vx.x.x.js
│ │ ├── cross-env_vx.x.x.js
│ │ ├── cryptr_vx.x.x.js
│ │ ├── css.escape_vx.x.x.js
│ │ ├── d3-array_vx.x.x.js
│ │ ├── danger-plugin-flow_vx.x.x.js
│ │ ├── danger-plugin-jest_vx.x.x.js
│ │ ├── danger-plugin-labels_vx.x.x.js
│ │ ├── danger-plugin-no-console_vx.x.x.js
│ │ ├── danger-plugin-no-test-shortcuts_vx.x.x.js
│ │ ├── danger-plugin-yarn_vx.x.x.js
│ │ ├── danger_vx.x.x.js
│ │ ├── datadog-metrics_vx.x.x.js
│ │ ├── dataloader_vx.x.x.js
│ │ ├── debounce_vx.x.x.js
│ │ ├── debug_v2.x.x.js
│ │ ├── decode-uri-component_vx.x.x.js
│ │ ├── draft-js-code-editor-plugin_vx.x.x.js
│ │ ├── draft-js-drag-n-drop-plugin_vx.x.x.js
│ │ ├── draft-js-embed-plugin_vx.x.x.js
│ │ ├── draft-js-export-markdown_vx.x.x.js
│ │ ├── draft-js-focus-plugin_vx.x.x.js
│ │ ├── draft-js-image-plugin_vx.x.x.js
│ │ ├── draft-js-import-markdown_vx.x.x.js
│ │ ├── draft-js-linkify-plugin_vx.x.x.js
│ │ ├── draft-js-markdown-plugin_vx.x.x.js
│ │ ├── draft-js-plugins-editor_vx.x.x.js
│ │ ├── draft-js-prism-plugin_vx.x.x.js
│ │ ├── draft-js_vx.x.x.js
│ │ ├── draftjs-to-markdown_vx.x.x.js
│ │ ├── electron-context-menu_vx.x.x.js
│ │ ├── electron-is-dev_vx.x.x.js
│ │ ├── electron-updater_vx.x.x.js
│ │ ├── electron-window-state_vx.x.x.js
│ │ ├── electron_vx.x.x.js
│ │ ├── emoji-regex_vx.x.x.js
│ │ ├── eslint-plugin-flowtype_vx.x.x.js
│ │ ├── eslint-plugin-jest_vx.x.x.js
│ │ ├── eslint-plugin-promise_vx.x.x.js
│ │ ├── eslint-plugin-react_vx.x.x.js
│ │ ├── eslint_vx.x.x.js
│ │ ├── expo-server-sdk_vx.x.x.js
│ │ ├── expo_vx.x.x.js
│ │ ├── express-enforces-ssl_vx.x.x.js
│ │ ├── express-hot-shots_vx.x.x.js
│ │ ├── express-session_vx.x.x.js
│ │ ├── express_v4.x.x.js
│ │ ├── faker_vx.x.x.js
│ │ ├── find-with-regex_vx.x.x.js
│ │ ├── flow-bin_v0.x.x.js
│ │ ├── flow-typed_vx.x.x.js
│ │ ├── graphql-cost-analysis_vx.x.x.js
│ │ ├── graphql-date_vx.x.x.js
│ │ ├── graphql-depth-limit_vx.x.x.js
│ │ ├── graphql-log_vx.x.x.js
│ │ ├── graphql-redis-subscriptions_vx.x.x.js
│ │ ├── graphql-server-express_vx.x.x.js
│ │ ├── graphql-subscriptions_vx.x.x.js
│ │ ├── graphql-tag_vx.x.x.js
│ │ ├── graphql-tools_vx.x.x.js
│ │ ├── graphql_vx.x.x.js
│ │ ├── helmet_vx.x.x.js
│ │ ├── highlight.js_vx.x.x.js
│ │ ├── history_vx.x.x.js
│ │ ├── hoist-non-react-statics_vx.x.x.js
│ │ ├── host-validation_vx.x.x.js
│ │ ├── hot-shots_vx.x.x.js
│ │ ├── hpp_vx.x.x.js
│ │ ├── hsts_vx.x.x.js
│ │ ├── http-proxy-middleware_vx.x.x.js
│ │ ├── idx_v2.x.x.js
│ │ ├── imgix-core-js_vx.x.x.js
│ │ ├── immutability-helper_vx.x.x.js
│ │ ├── ioredis_vx.x.x.js
│ │ ├── is-html_vx.x.x.js
│ │ ├── isomorphic-fetch_v2.x.x.js
│ │ ├── iterall_vx.x.x.js
│ │ ├── jest_v22.x.x.js
│ │ ├── json-stringify-pretty-compact_vx.x.x.js
│ │ ├── jsonwebtoken_vx.x.x.js
│ │ ├── keygrip_vx.x.x.js
│ │ ├── linkify-it_vx.x.x.js
│ │ ├── lint-staged_vx.x.x.js
│ │ ├── localstorage-memory_vx.x.x.js
│ │ ├── lodash.intersection_vx.x.x.js
│ │ ├── lodash_v4.x.x.js
│ │ ├── longjohn_vx.x.x.js
│ │ ├── markdown-draft-js_vx.x.x.js
│ │ ├── micromatch_vx.x.x.js
│ │ ├── moment_v2.3.x.js
│ │ ├── ms_vx.x.x.js
│ │ ├── newrelic_vx.x.x.js
│ │ ├── node-env-file_vx.x.x.js
│ │ ├── node-fetch_vx.x.x.js
│ │ ├── node-localstorage_vx.x.x.js
│ │ ├── nodemon_vx.x.x.js
│ │ ├── now-env_vx.x.x.js
│ │ ├── offline-plugin_vx.x.x.js
│ │ ├── optics-agent_vx.x.x.js
│ │ ├── passport-facebook_vx.x.x.js
│ │ ├── passport-github2_vx.x.x.js
│ │ ├── passport-google-oauth2_vx.x.x.js
│ │ ├── passport-twitter_vx.x.x.js
│ │ ├── passport_vx.x.x.js
│ │ ├── postmark_vx.x.x.js
│ │ ├── pre-commit_vx.x.x.js
│ │ ├── prettier_vx.x.x.js
│ │ ├── prism-react-renderer_vx.x.x.js
│ │ ├── prismjs_vx.x.x.js
│ │ ├── puppeteer_vx.x.x.js
│ │ ├── query-string_vx.x.x.js
│ │ ├── raf_vx.x.x.js
│ │ ├── raven-js_v3.17.x.js
│ │ ├── raven-js_vx.x.x.js
│ │ ├── raven_vx.x.x.js
│ │ ├── raw-loader_vx.x.x.js
│ │ ├── react-apollo_vx.x.x.js
│ │ ├── react-app-rewire-styled-components_vx.x.x.js
│ │ ├── react-app-rewired_vx.x.x.js
│ │ ├── react-async-hook_vx.x.x.js
│ │ ├── react-clipboard.js_vx.x.x.js
│ │ ├── react-dropzone_vx.x.x.js
│ │ ├── react-error-boundary_vx.x.x.js
│ │ ├── react-flip-move_v2.9.x.js
│ │ ├── react-helmet-async_vx.x.x.js
│ │ ├── react-helmet_vx.x.x.js
│ │ ├── react-hot-loader_vx.x.x.js
│ │ ├── react-image_vx.x.x.js
│ │ ├── react-infinite-scroller-fork-mxstbr_vx.x.x.js
│ │ ├── react-infinite-scroller-with-scroll-element_vx.x.x.js
│ │ ├── react-loadable_vx.x.x.js
│ │ ├── react-mentions_vx.x.x.js
│ │ ├── react-modal_vx.x.x.js
│ │ ├── react-popper_vx.x.x.js
│ │ ├── react-redux_v5.x.x.js
│ │ ├── react-remarkable_vx.x.x.js
│ │ ├── react-router-dom_vx.x.x.js
│ │ ├── react-router_v4.x.x.js
│ │ ├── react-router_vx.x.x.js
│ │ ├── react-scripts_vx.x.x.js
│ │ ├── react-stripe-checkout_vx.x.x.js
│ │ ├── react-stripe-elements_vx.x.x.js
│ │ ├── react-textarea-autosize_vx.x.x.js
│ │ ├── react-transition-group_vx.x.x.js
│ │ ├── react-trend_vx.x.x.js
│ │ ├── react-visibility-sensor_vx.x.x.js
│ │ ├── react_v16.8.0.js
│ │ ├── recharts_vx.x.x.js
│ │ ├── recompose_v0.x.x.js
│ │ ├── recompose_vx.x.x.js
│ │ ├── redis-tag-cache_vx.x.x.js
│ │ ├── redraft_vx.x.x.js
│ │ ├── redux-thunk_vx.x.x.js
│ │ ├── redux_v3.x.x.js
│ │ ├── request-ip_vx.x.x.js
│ │ ├── rethinkdb-changefeed-reconnect_vx.x.x.js
│ │ ├── rethinkdb-inspector_vx.x.x.js
│ │ ├── rethinkdb-migrate_vx.x.x.js
│ │ ├── rethinkdbdash_vx.x.x.js
│ │ ├── rethinkhaberdashery_vx.x.x.js
│ │ ├── rimraf_v2.x.x.js
│ │ ├── rimraf_vx.x.x.js
│ │ ├── sanitize-filename_vx.x.x.js
│ │ ├── sentry-expo_vx.x.x.js
│ │ ├── serialize-javascript_vx.x.x.js
│ │ ├── session-rethinkdb_vx.x.x.js
│ │ ├── sha1_vx.x.x.js
│ │ ├── shortid_vx.x.x.js
│ │ ├── slate-markdown_vx.x.x.js
│ │ ├── slate_vx.x.x.js
│ │ ├── slugg_vx.x.x.js
│ │ ├── snarkdown_vx.x.x.js
│ │ ├── stopword_vx.x.x.js
│ │ ├── string-replace-to-array_vx.x.x.js
│ │ ├── string-similarity_vx.x.x.js
│ │ ├── stripe_vx.x.x.js
│ │ ├── striptags_vx.x.x.js
│ │ ├── styled-components_v2.x.x.js
│ │ ├── styled-components_vx.x.x.js
│ │ ├── subscriptions-transport-ws_vx.x.x.js
│ │ ├── sw-precache-webpack-plugin_vx.x.x.js
│ │ ├── then-queue_vx.x.x.js
│ │ ├── toobusy-js_vx.x.x.js
│ │ ├── uuid_v3.x.x.js
│ │ ├── validator_vx.x.x.js
│ │ ├── web-push_vx.x.x.js
│ │ ├── webpack-bundle-analyzer_vx.x.x.js
│ │ ├── webpack-module-manifest-plugin_vx.x.x.js
│ │ └── write-file-webpack-plugin_vx.x.x.js
│ └── react-native.js
├── hyperion/
│ ├── index.js
│ └── renderer/
│ ├── browser-shim.js
│ ├── html-template.js
│ └── index.js
├── jest.config.js
├── now-secrets.example.json
├── now.json
├── package.json
├── public/
│ ├── index.html
│ ├── install-raven.js
│ ├── manifest.json
│ ├── push-sw.js
│ ├── robots.txt
│ └── service-worker.js
├── robots.txt
├── rules-alpha.json
├── rules.json
├── scripts/
│ ├── deploy.js
│ ├── generate-table-diagram.js
│ ├── heroku-deploy.js
│ ├── introspection-query.js
│ └── utils/
│ ├── error.js
│ └── parse-argv.js
├── set-heroku-config
├── shared/
│ ├── clients/
│ │ ├── draft-js/
│ │ │ ├── links-decorator/
│ │ │ │ ├── core.js
│ │ │ │ └── index.js
│ │ │ ├── mentions-decorator/
│ │ │ │ ├── core.js
│ │ │ │ ├── index.js
│ │ │ │ └── test/
│ │ │ │ ├── core.test.js
│ │ │ │ └── mentions-decorator.test.js
│ │ │ ├── message/
│ │ │ │ ├── renderer.js
│ │ │ │ ├── test/
│ │ │ │ │ ├── __snapshots__/
│ │ │ │ │ │ └── renderer.test.js.snap
│ │ │ │ │ └── renderer.test.js
│ │ │ │ └── types.js
│ │ │ ├── renderer/
│ │ │ │ └── index.js
│ │ │ ├── thread/
│ │ │ │ └── renderer.js
│ │ │ └── utils/
│ │ │ ├── getSnippet.js
│ │ │ ├── getStringElements.js
│ │ │ ├── hasStringElements.js
│ │ │ ├── isShort.js
│ │ │ └── plaintext.js
│ │ ├── group-messages.js
│ │ └── test/
│ │ ├── __snapshots__/
│ │ │ └── messages.test.js.snap
│ │ └── messages.test.js
│ ├── cookie-utils.js
│ ├── db/
│ │ ├── constants.js
│ │ ├── create-query.js
│ │ ├── db.js
│ │ ├── index.js
│ │ ├── queries/
│ │ │ ├── channel.js
│ │ │ ├── community.js
│ │ │ ├── message.js
│ │ │ ├── thread.js
│ │ │ └── user.js
│ │ └── query-cache.js
│ ├── draft-utils/
│ │ ├── add-embeds-to-draft-js.js
│ │ ├── index.js
│ │ ├── message-types.js
│ │ ├── process-message-content.js
│ │ ├── process-thread-content.js
│ │ └── test/
│ │ ├── __snapshots__/
│ │ │ └── add-embeds-to-draft-js.test.js.snap
│ │ └── add-embeds-to-draft-js.test.js
│ ├── encryption/
│ │ └── index.js
│ ├── generate-meta-info.js
│ ├── get-mentions.js
│ ├── graphql/
│ │ ├── apollo-client-options.js
│ │ ├── constants.js
│ │ ├── fragments/
│ │ │ ├── channel/
│ │ │ │ ├── channelInfo.js
│ │ │ │ ├── channelMemberConnection.js
│ │ │ │ ├── channelMetaData.js
│ │ │ │ └── channelThreadConnection.js
│ │ │ ├── community/
│ │ │ │ ├── communityChannelConnection.js
│ │ │ │ ├── communityInfo.js
│ │ │ │ ├── communityMembers.js
│ │ │ │ ├── communityMetaData.js
│ │ │ │ ├── communitySettings.js
│ │ │ │ └── communityThreadConnection.js
│ │ │ ├── communityMember/
│ │ │ │ └── communityMemberInfo.js
│ │ │ ├── directMessageThread/
│ │ │ │ ├── directMessageThreadInfo.js
│ │ │ │ └── directMessageThreadMessageConnection.js
│ │ │ ├── message/
│ │ │ │ ├── directMessageInfo.js
│ │ │ │ └── messageInfo.js
│ │ │ ├── notification/
│ │ │ │ └── notificationInfo.js
│ │ │ ├── thread/
│ │ │ │ ├── threadInfo.js
│ │ │ │ ├── threadMessageConnection.js
│ │ │ │ └── threadParticipant.js
│ │ │ └── user/
│ │ │ ├── userChannelConnection.js
│ │ │ ├── userCommunityConnection.js
│ │ │ ├── userDirectMessageThreadConnection.js
│ │ │ ├── userEverythingConnection.js
│ │ │ ├── userInfo.js
│ │ │ ├── userSettings.js
│ │ │ └── userThreadConnection.js
│ │ ├── index.js
│ │ ├── mutations/
│ │ │ ├── channel/
│ │ │ │ ├── deleteChannel.js
│ │ │ │ └── editChannel.js
│ │ │ ├── community/
│ │ │ │ ├── deleteCommunity.js
│ │ │ │ ├── editCommunity.js
│ │ │ │ ├── toggleCommunityNoindex.js
│ │ │ │ └── toggleCommunityRedirect.js
│ │ │ ├── message/
│ │ │ │ └── deleteMessage.js
│ │ │ ├── thread/
│ │ │ │ └── deleteThread.js
│ │ │ ├── uploadImage.js
│ │ │ └── user/
│ │ │ ├── banUser.js
│ │ │ ├── deleteCurrentUser.js
│ │ │ └── editUser.js
│ │ ├── queries/
│ │ │ ├── channel/
│ │ │ │ ├── getChannel.js
│ │ │ │ ├── getChannelMemberConnection.js
│ │ │ │ ├── getChannelSettings.js
│ │ │ │ └── getChannelThreadConnection.js
│ │ │ ├── community/
│ │ │ │ ├── getCommunities.js
│ │ │ │ ├── getCommunity.js
│ │ │ │ ├── getCommunityChannelConnection.js
│ │ │ │ ├── getCommunityMembers.js
│ │ │ │ ├── getCommunitySettings.js
│ │ │ │ └── getCommunityThreadConnection.js
│ │ │ ├── communityMember/
│ │ │ │ └── getCommunityMember.js
│ │ │ ├── composer/
│ │ │ │ └── getComposerCommunitiesAndChannels.js
│ │ │ ├── directMessageThread/
│ │ │ │ ├── getCurrentUserDMThreadConnection.js
│ │ │ │ ├── getDirectMessageThread.js
│ │ │ │ ├── getDirectMessageThreadByUserIds.js
│ │ │ │ └── getDirectMessageThreadMessageConnection.js
│ │ │ ├── message/
│ │ │ │ ├── getMediaMessagesForThread.js
│ │ │ │ └── getMessage.js
│ │ │ ├── thread/
│ │ │ │ ├── getThread.js
│ │ │ │ └── getThreadMessageConnection.js
│ │ │ └── user/
│ │ │ ├── getCurrentUserEverythingFeed.js
│ │ │ ├── getCurrentUserSettings.js
│ │ │ ├── getUser.js
│ │ │ ├── getUserCommunityConnection.js
│ │ │ ├── getUserGithubProfile.js
│ │ │ └── getUserThreadConnection.js
│ │ ├── schema.json
│ │ └── subscriptions/
│ │ ├── index.js
│ │ └── utils.js
│ ├── graphql-cache-keys.js
│ ├── imgix/
│ │ ├── getDefaultExpires.js
│ │ ├── index.js
│ │ ├── sign.js
│ │ ├── signCommunity.js
│ │ ├── signMessage.js
│ │ ├── signThread.js
│ │ └── signUser.js
│ ├── install-dependencies.js
│ ├── middlewares/
│ │ ├── cors.js
│ │ ├── csrf.js
│ │ ├── error-handler.js
│ │ ├── logging.js
│ │ ├── raven.js
│ │ ├── security.js
│ │ ├── session.js
│ │ ├── statsd.js
│ │ ├── thread-param.js
│ │ └── toobusy.js
│ ├── normalize-url.js
│ ├── only-contains-emoji.js
│ ├── raven/
│ │ └── index.js
│ ├── regexps.js
│ ├── sentencify.js
│ ├── slate-utils.js
│ ├── slug-deny-lists.js
│ ├── sort-by-date.js
│ ├── statsd.js
│ ├── test/
│ │ ├── encryption.test.js
│ │ ├── fixtures/
│ │ │ ├── CHANNEL_CREATED.json
│ │ │ ├── COMMUNITY_INVITE.json
│ │ │ ├── DIRECT_MESSAGE_CREATED.json
│ │ │ ├── MEDIA_MESSAGE_CREATED.json
│ │ │ ├── MENTION_MESSAGE.json
│ │ │ ├── MENTION_THREAD.json
│ │ │ ├── MESSAGE_CREATED.json
│ │ │ ├── REACTION_CREATED.json
│ │ │ ├── THREAD_CREATED.json
│ │ │ ├── THREAD_REACTION_CREATED.json
│ │ │ └── USER_JOINED_COMMUNITY.json
│ │ ├── get-mentions.test.js
│ │ └── normalize-url.test.js
│ ├── testing/
│ │ ├── data.js
│ │ ├── db.js
│ │ ├── empty-db.js
│ │ ├── setup-test-framework.js
│ │ ├── setup.js
│ │ └── teardown.js
│ ├── theme/
│ │ └── index.js
│ ├── time-difference.js
│ ├── time-formatting.js
│ ├── truncate.js
│ ├── truthy-values.js
│ ├── types.js
│ └── unique-elements.js
├── spectrum-tmuxp.yaml
└── src/
├── actions/
│ ├── authentication.js
│ ├── directMessageThreads.js
│ ├── gallery.js
│ ├── modals.js
│ ├── threadSlider.js
│ ├── titlebar.js
│ └── toasts.js
├── api/
│ └── constants.js
├── components/
│ ├── announcementBanner/
│ │ ├── index.js
│ │ └── style.js
│ ├── appViewWrapper/
│ │ ├── index.js
│ │ └── style.js
│ ├── avatar/
│ │ ├── communityAvatar.js
│ │ ├── image.js
│ │ ├── index.js
│ │ ├── style.js
│ │ └── userAvatar.js
│ ├── badges/
│ │ ├── index.js
│ │ └── style.js
│ ├── button/
│ │ ├── index.js
│ │ └── style.js
│ ├── card/
│ │ └── index.js
│ ├── column/
│ │ └── index.js
│ ├── communitySidebar/
│ │ └── index.js
│ ├── conditionalWrap/
│ │ └── index.js
│ ├── editForm/
│ │ └── style.js
│ ├── entities/
│ │ ├── index.js
│ │ ├── listItems/
│ │ │ ├── channel.js
│ │ │ ├── community.js
│ │ │ ├── index.js
│ │ │ ├── style.js
│ │ │ └── user.js
│ │ └── profileCards/
│ │ ├── channel.js
│ │ ├── community.js
│ │ ├── components/
│ │ │ ├── channelActions.js
│ │ │ ├── channelCommunityMeta.js
│ │ │ ├── channelMeta.js
│ │ │ ├── communityActions.js
│ │ │ ├── communityMeta.js
│ │ │ ├── userActions.js
│ │ │ └── userMeta.js
│ │ ├── index.js
│ │ ├── style.js
│ │ └── user.js
│ ├── error/
│ │ ├── BlueScreen.js
│ │ ├── ErrorBoundary.js
│ │ ├── SettingsFallback.js
│ │ └── index.js
│ ├── flyout/
│ │ └── index.js
│ ├── formElements/
│ │ ├── index.js
│ │ └── style.js
│ ├── fullscreenView/
│ │ ├── index.js
│ │ └── style.js
│ ├── gallery/
│ │ ├── browser.js
│ │ ├── index.js
│ │ └── style.js
│ ├── githubProfile/
│ │ └── index.js
│ ├── globals/
│ │ └── index.js
│ ├── goop/
│ │ └── index.js
│ ├── head/
│ │ └── index.js
│ ├── hoverProfile/
│ │ ├── channelProfile.js
│ │ ├── communityProfile.js
│ │ ├── index.js
│ │ ├── loadingHoverProfile.js
│ │ ├── style.js
│ │ ├── userContainer.js
│ │ └── userProfile.js
│ ├── icon/
│ │ └── index.js
│ ├── illustrations/
│ │ └── index.js
│ ├── inboxThread/
│ │ ├── activity.js
│ │ ├── header/
│ │ │ ├── index.js
│ │ │ ├── style.js
│ │ │ ├── threadHeader.js
│ │ │ ├── timestamp.js
│ │ │ └── userProfileThreadHeader.js
│ │ ├── index.js
│ │ ├── messageCount.js
│ │ └── style.js
│ ├── infiniteScroll/
│ │ ├── deduplicateChildren.js
│ │ ├── index.js
│ │ └── tallViewports.js
│ ├── layout/
│ │ └── index.js
│ ├── listItems/
│ │ ├── channel/
│ │ │ ├── index.js
│ │ │ └── style.js
│ │ ├── index.js
│ │ └── style.js
│ ├── loading/
│ │ ├── index.js
│ │ └── style.js
│ ├── loginButtonSet/
│ │ ├── facebook.js
│ │ ├── github.js
│ │ ├── google.js
│ │ ├── index.js
│ │ ├── style.js
│ │ └── twitter.js
│ ├── logo/
│ │ └── index.js
│ ├── maintenance/
│ │ └── index.js
│ ├── menu/
│ │ ├── index.js
│ │ └── style.js
│ ├── message/
│ │ ├── authorByline.js
│ │ ├── index.js
│ │ ├── messageErrorFallback.js
│ │ ├── style.js
│ │ ├── threadAttachment/
│ │ │ ├── attachment.js
│ │ │ ├── index.js
│ │ │ └── style.js
│ │ └── view.js
│ ├── messageGroup/
│ │ ├── directMessage.js
│ │ ├── index.js
│ │ ├── style.js
│ │ └── thread.js
│ ├── modals/
│ │ ├── BanUserModal/
│ │ │ ├── index.js
│ │ │ └── style.js
│ │ ├── DeleteDoubleCheckModal/
│ │ │ ├── index.js
│ │ │ └── style.js
│ │ ├── modalContainer.js
│ │ ├── modalRoot.js
│ │ └── styles.js
│ ├── nextPageButton/
│ │ ├── index.js
│ │ └── style.js
│ ├── outsideClickHandler/
│ │ └── index.js
│ ├── profile/
│ │ ├── coverPhoto.js
│ │ ├── index.js
│ │ ├── metaData.js
│ │ ├── style.js
│ │ └── thread.js
│ ├── reaction/
│ │ ├── index.js
│ │ └── style.js
│ ├── redirectHandler/
│ │ └── index.js
│ ├── rich-text-editor/
│ │ ├── prism-theme.css
│ │ └── style.js
│ ├── scrollManager/
│ │ └── index.js
│ ├── scrollRow/
│ │ ├── index.js
│ │ └── style.js
│ ├── segmentedControl/
│ │ ├── index.js
│ │ └── style.js
│ ├── select/
│ │ ├── index.js
│ │ └── style.js
│ ├── settingsViews/
│ │ ├── header.js
│ │ ├── style.js
│ │ └── subnav.js
│ ├── themedSection/
│ │ └── index.js
│ ├── threadFeed/
│ │ ├── index.js
│ │ ├── nullState.js
│ │ └── style.js
│ ├── threadFeedCard/
│ │ └── style.js
│ ├── threadRenderer/
│ │ └── index.js
│ ├── titlebar/
│ │ ├── actions.js
│ │ ├── base.js
│ │ ├── index.js
│ │ └── style.js
│ ├── toasts/
│ │ ├── index.js
│ │ └── style.js
│ ├── tooltip/
│ │ └── index.js
│ ├── upsell/
│ │ ├── index.js
│ │ └── style.js
│ ├── usernameSearch/
│ │ ├── index.js
│ │ └── style.js
│ ├── viewError/
│ │ ├── index.js
│ │ └── style.js
│ ├── viewNetworkHandler/
│ │ └── index.js
│ ├── visuallyHidden/
│ │ └── index.js
│ └── withCurrentUser/
│ └── index.js
├── helpers/
│ ├── directMessageThreads.js
│ ├── get-thread-link.js
│ ├── history.js
│ ├── images.js
│ ├── is-admin.js
│ ├── is-viewing-marketing-page.js
│ ├── keycodes.js
│ ├── localStorage.js
│ ├── navigation-context.js
│ ├── notifications.js
│ ├── realtimeThreads.js
│ ├── regexps.js
│ ├── render-text-with-markdown-links.js
│ ├── sentry-redux-middleware.js
│ ├── signed-out-fallback.js
│ ├── utils.js
│ └── web-push-manager.js
├── hooks/
│ ├── useAppScroller.js
│ ├── useConnectionRestored.js
│ ├── useDebounce.js
│ └── usePrevious.js
├── hot-routes.js
├── index.js
├── reducers/
│ ├── connectionStatus.js
│ ├── gallery.js
│ ├── index.js
│ ├── modals.js
│ ├── threadSlider.js
│ ├── titlebar.js
│ └── toasts.js
├── registerServiceWorker.js
├── reset.css.js
├── routes.js
├── store/
│ └── index.js
└── views/
├── authViewHandler/
│ └── index.js
├── channel/
│ ├── components/
│ │ ├── MembersList.js
│ │ └── PostsFeed.js
│ ├── index.js
│ └── style.js
├── channelSettings/
│ ├── components/
│ │ ├── channelMembers.js
│ │ ├── editForm.js
│ │ └── overview.js
│ ├── index.js
│ └── style.js
├── community/
│ ├── components/
│ │ ├── channelsList.js
│ │ ├── communityFeeds.js
│ │ ├── membersList.js
│ │ ├── mobileCommunityInfoActions.js
│ │ ├── postsFeeds.js
│ │ └── teamMembersList.js
│ ├── containers/
│ │ ├── privateCommunity.js
│ │ └── signedIn.js
│ ├── index.js
│ └── style.js
├── communityLogin/
│ ├── index.js
│ └── style.js
├── communityMembers/
│ ├── components/
│ │ ├── communityMembers.js
│ │ ├── getMembers.js
│ │ └── mutationWrapper.js
│ ├── index.js
│ └── style.js
├── communitySettings/
│ ├── components/
│ │ ├── channelList.js
│ │ ├── editForm.js
│ │ ├── overview.js
│ │ └── redirect.js
│ ├── index.js
│ └── style.js
├── directMessages/
│ ├── components/
│ │ ├── avatars.js
│ │ ├── header.js
│ │ ├── loading.js
│ │ ├── messageThreadListItem.js
│ │ ├── messages.js
│ │ ├── style.js
│ │ └── threadsList.js
│ ├── containers/
│ │ ├── existingThread.js
│ │ └── index.js
│ ├── index.js
│ └── style.js
├── explore/
│ ├── collections.js
│ ├── index.js
│ ├── style.js
│ └── view.js
├── globalTitlebar/
│ └── index.js
├── homeViewRedirect/
│ └── index.js
├── login/
│ ├── index.js
│ └── style.js
├── navigation/
│ ├── accessibility.js
│ ├── communityList.js
│ ├── directMessagesTab.js
│ ├── index.js
│ ├── navHead.js
│ └── style.js
├── newUserOnboarding/
│ ├── components/
│ │ └── setUsername/
│ │ ├── index.js
│ │ └── style.js
│ ├── index.js
│ └── style.js
├── pages/
│ ├── components/
│ │ ├── communities.js
│ │ ├── footer.js
│ │ ├── logos.js
│ │ └── nav.js
│ ├── index.js
│ ├── privacy/
│ │ └── index.js
│ ├── style.js
│ └── terms/
│ ├── index.js
│ └── style.js
├── queryParamToastDispatcher/
│ └── index.js
├── status/
│ ├── index.js
│ └── style.js
├── thread/
│ ├── components/
│ │ ├── actionBar.js
│ │ ├── actionsDropdown.js
│ │ ├── lockedMessages.js
│ │ ├── messagesSubscriber.js
│ │ ├── nullMessages.js
│ │ ├── stickyHeader.js
│ │ ├── threadByline.js
│ │ ├── threadDetail.js
│ │ └── threadHead.js
│ ├── container/
│ │ └── index.js
│ ├── index.js
│ ├── redirect-old-route.js
│ └── style.js
├── threadSlider/
│ ├── index.js
│ └── style.js
├── user/
│ ├── components/
│ │ └── communityList.js
│ ├── index.js
│ └── style.js
├── userSettings/
│ ├── components/
│ │ ├── deleteAccountForm.js
│ │ ├── downloadDataForm.js
│ │ ├── editForm.js
│ │ ├── logout.js
│ │ └── overview.js
│ ├── index.js
│ └── style.js
└── viewHelpers/
├── errorView.js
├── fullScreenRedirect.js
├── index.js
├── loadingView.js
├── style.js
└── textValidationHelper.js
SYMBOL INDEX (345 symbols across 165 files)
FILE: api/apollo-server.js
class ProtectedApolloServer (line 20) | class ProtectedApolloServer extends ApolloServer {
method createGraphQLServerOptions (line 21) | async createGraphQLServerOptions(
FILE: api/authentication.js
constant IS_PROD (line 15) | const IS_PROD = !process.env.FORCE_DEV && process.env.NODE_ENV === 'prod...
constant TWITTER_OAUTH_CLIENT_SECRET (line 17) | const TWITTER_OAUTH_CLIENT_SECRET = IS_PROD
constant FACEBOOK_OAUTH_CLIENT_ID (line 21) | const FACEBOOK_OAUTH_CLIENT_ID = IS_PROD
constant FACEBOOK_OAUTH_CLIENT_SECRET (line 25) | const FACEBOOK_OAUTH_CLIENT_SECRET = IS_PROD
constant GOOGLE_OAUTH_CLIENT_SECRET (line 29) | const GOOGLE_OAUTH_CLIENT_SECRET = IS_PROD
constant GITHUB_OAUTH_CLIENT_SECRET (line 33) | const GITHUB_OAUTH_CLIENT_SECRET = IS_PROD
constant TWITTER_OAUTH_CLIENT_ID (line 37) | const TWITTER_OAUTH_CLIENT_ID = IS_PROD
constant GOOGLE_OAUTH_CLIENT_ID (line 41) | const GOOGLE_OAUTH_CLIENT_ID = IS_PROD
constant GITHUB_OAUTH_CLIENT_ID (line 45) | const GITHUB_OAUTH_CLIENT_ID = IS_PROD
constant CALLBACK_BASE (line 49) | const CALLBACK_BASE = IS_PROD
FILE: api/index.js
constant PORT (line 34) | const PORT = process.env.PORT ? parseInt(process.env.PORT, 10) : 3001;
FILE: api/loaders/create-loader.js
function indexResults (line 26) | function indexResults(results, indexField, cacheKeyFn) {
function normalizeRethinkDbResults (line 36) | function normalizeRethinkDbResults(keys, indexField, cacheKeyFn) {
FILE: api/migrations/20170702194221-fix-images.js
constant MARKDOWN_LINK (line 1) | const MARKDOWN_LINK = /(?:\[(.*?)\]\((.*?)\))/g;
FILE: api/migrations/20170803104302-dedupe-users-settings.js
function _toConsumableArray (line 4) | function _toConsumableArray(arr) {
FILE: api/migrations/20171005075445-remove-markdown-links-from-messages.js
constant MARKDOWN_LINK (line 2) | const MARKDOWN_LINK = /(?:\[(.*?)\]\((.*?)\))/g;
FILE: api/migrations/config.js
constant DEFAULT_CONFIG (line 5) | const DEFAULT_CONFIG = {
constant RUN_IN_PROD (line 19) | const RUN_IN_PROD = !!process.env.AWS_RETHINKDB_PASSWORD;
FILE: api/migrations/seed/default/constants.js
constant DATE (line 2) | const DATE = 1483225200000;
constant MAX_ID (line 5) | const MAX_ID = '1';
constant BRIAN_ID (line 6) | const BRIAN_ID = '2';
constant BRYN_ID (line 7) | const BRYN_ID = '3';
constant BLOCKED_USER_ID (line 9) | const BLOCKED_USER_ID = '4';
constant QUIET_USER_ID (line 11) | const QUIET_USER_ID = '5';
constant PREVIOUS_MEMBER_USER_ID (line 13) | const PREVIOUS_MEMBER_USER_ID = '6';
constant PENDING_USER_ID (line 15) | const PENDING_USER_ID = '7';
constant CHANNEL_MODERATOR_USER_ID (line 17) | const CHANNEL_MODERATOR_USER_ID = '8';
constant COMMUNITY_MODERATOR_USER_ID (line 19) | const COMMUNITY_MODERATOR_USER_ID = '9';
constant SINGLE_CHANNEL_COMMUNITY_USER_ID (line 22) | const SINGLE_CHANNEL_COMMUNITY_USER_ID = '10';
constant NEW_USER_ID (line 23) | const NEW_USER_ID = '11';
constant SPECTRUM_COMMUNITY_ID (line 26) | const SPECTRUM_COMMUNITY_ID = '1';
constant PAYMENTS_COMMUNITY_ID (line 27) | const PAYMENTS_COMMUNITY_ID = '2';
constant DELETED_COMMUNITY_ID (line 28) | const DELETED_COMMUNITY_ID = '3';
constant PRIVATE_COMMUNITY_ID (line 29) | const PRIVATE_COMMUNITY_ID = '4';
constant SINGLE_CHANNEL_COMMUNITY_ID (line 30) | const SINGLE_CHANNEL_COMMUNITY_ID = '5';
constant PRIVATE_COMMUNITY_WITH_JOIN_TOKEN_ID (line 31) | const PRIVATE_COMMUNITY_WITH_JOIN_TOKEN_ID = '6';
constant SPECTRUM_GENERAL_CHANNEL_ID (line 34) | const SPECTRUM_GENERAL_CHANNEL_ID = '1';
constant SPECTRUM_PRIVATE_CHANNEL_ID (line 35) | const SPECTRUM_PRIVATE_CHANNEL_ID = '2';
constant PAYMENTS_GENERAL_CHANNEL_ID (line 36) | const PAYMENTS_GENERAL_CHANNEL_ID = '3';
constant PAYMENTS_PRIVATE_CHANNEL_ID (line 37) | const PAYMENTS_PRIVATE_CHANNEL_ID = '4';
constant PAYMENTS_FEATURES_CHANNEL_ID (line 38) | const PAYMENTS_FEATURES_CHANNEL_ID = '10';
constant SPECTRUM_ARCHIVED_CHANNEL_ID (line 39) | const SPECTRUM_ARCHIVED_CHANNEL_ID = '5';
constant SPECTRUM_DELETED_CHANNEL_ID (line 40) | const SPECTRUM_DELETED_CHANNEL_ID = '6';
constant DELETED_COMMUNITY_DELETED_CHANNEL_ID (line 41) | const DELETED_COMMUNITY_DELETED_CHANNEL_ID = '7';
constant MODERATOR_CREATED_CHANNEL_ID (line 42) | const MODERATOR_CREATED_CHANNEL_ID = '8';
constant PRIVATE_GENERAL_CHANNEL_ID (line 43) | const PRIVATE_GENERAL_CHANNEL_ID = '9';
constant SINGLE_CHANNEL_COMMUNITY_GENERAL_CHANNEL_ID (line 44) | const SINGLE_CHANNEL_COMMUNITY_GENERAL_CHANNEL_ID = '11';
FILE: api/models/usersCommunities.js
constant DEFAULT_USER_COMMUNITY_PERMISSIONS (line 135) | const DEFAULT_USER_COMMUNITY_PERMISSIONS = {
FILE: api/routes/auth/create-signin-routes.js
constant IS_PROD (line 14) | const IS_PROD = process.env.NODE_ENV === 'production';
constant FALLBACK_URL (line 15) | const FALLBACK_URL = IS_PROD
FILE: api/routes/auth/logout.js
constant IS_PROD (line 5) | const IS_PROD = process.env.NODE_ENV === 'production';
constant HOME (line 6) | const HOME = IS_PROD ? '/explore' : 'http://localhost:3000/explore';
FILE: api/test/channel/mutations/deleteChannel.test.js
constant DEFAULT_CHANNELS (line 11) | const DEFAULT_CHANNELS = [
constant DEFAULT_USERS_CHANNELS (line 23) | const DEFAULT_USERS_CHANNELS = [
constant DEFAULT_THREADS (line 46) | const DEFAULT_THREADS = [
FILE: api/test/utils/__mocks__/debug.js
function makeLogger (line 5) | function makeLogger() {
FILE: api/types/custom-scalars/LowercaseString.js
method parseValue (line 9) | parseValue(value) {
method serialize (line 12) | serialize(value) {
method parseLiteral (line 15) | parseLiteral(ast) {
FILE: api/utils/UserError.js
class UserError (line 5) | class UserError extends Error {
method constructor (line 6) | constructor(...args) {
FILE: api/utils/file-system.js
constant STORAGE_DIR (line 7) | const STORAGE_DIR = 'public/uploads';
constant READ_WRITE_MODE (line 8) | const READ_WRITE_MODE = 0o777;
FILE: api/utils/generate-thread-meta-image-from-text.js
constant WIDTH (line 11) | const WIDTH = 1500;
constant IMGIX_TEXT_ENDPOINT (line 12) | const IMGIX_TEXT_ENDPOINT = 'https://assets.imgix.net/~text';
constant TITLE_PARAMS (line 14) | const TITLE_PARAMS = {
constant FOOTER_PARAMS (line 26) | const FOOTER_PARAMS = {
constant BACKGROUND_URL (line 34) | const BACKGROUND_URL = `https://spectrum.imgix.net/default_images/twitte...
FILE: api/utils/get-page-meta.js
constant PATH_DENY_LIST (line 6) | const PATH_DENY_LIST = ['robots.txt', 'home', 'messages', 'notifications'];
FILE: api/utils/get-random-default-photo.js
constant PALETTE (line 5) | const PALETTE = [
FILE: api/utils/is-spectrum-url.js
constant IS_PROD (line 4) | const IS_PROD = process.env.NODE_ENV === 'production';
constant EXPO_URL (line 6) | const EXPO_URL = /^https:\/\/auth\.expo\.io\//;
FILE: api/utils/s3.js
constant IS_PROD (line 10) | const IS_PROD = process.env.NODE_ENV === 'production';
constant S3_TOKEN (line 14) | let S3_TOKEN = process.env.S3_TOKEN;
constant S3_SECRET (line 15) | let S3_SECRET = process.env.S3_SECRET;
FILE: api/utils/session-store.js
constant ONE_YEAR (line 4) | const ONE_YEAR = 31556952000;
constant ONE_DAY (line 5) | const ONE_DAY = 86400000;
FILE: config-overrides.js
function walkFolder (line 24) | function walkFolder(currentDirPath, callback) {
FILE: cypress/integration/channel/settings/edit_spec.js
constant NEW_NAME (line 13) | const NEW_NAME = 'General Update';
constant NEW_DESCRIPTION (line 14) | const NEW_DESCRIPTION = 'New description';
FILE: cypress/integration/channel/view/membership_spec.js
constant QUIET_USER_ID (line 20) | const QUIET_USER_ID = constants.QUIET_USER_ID;
FILE: cypress/integration/user/edit_user_spec.js
constant NEW_NAME (line 5) | const NEW_NAME = 'Brian Edited';
constant NEW_DESCRIPTION (line 6) | const NEW_DESCRIPTION = 'Description Edited';
constant NEW_WEBSITE (line 7) | const NEW_WEBSITE = 'Website Edited';
FILE: flow-typed/npm/axios_v0.17.x.js
class CancelToken (line 20) | class CancelToken {
class AxiosInterceptorIdent (line 75) | class AxiosInterceptorIdent extends String {}
FILE: flow-typed/npm/common-tags_v1.4.x.js
class TemplateTag (line 38) | class TemplateTag {
FILE: flow-typed/npm/express_v4.x.x.js
class express$RequestResponseBase (line 13) | class express$RequestResponseBase {
method constructor (line 197) | constructor(): void,
FILE: hyperion/index.js
constant PORT (line 20) | const PORT = process.env.PORT || 3006;
constant ONE_HOUR (line 21) | const ONE_HOUR = 3600;
FILE: hyperion/renderer/index.js
constant IN_MAINTENANCE_MODE (line 36) | const IN_MAINTENANCE_MODE =
constant IS_PROD (line 38) | const IS_PROD = process.env.NODE_ENV === 'production';
constant FORCE_DEV (line 39) | const FORCE_DEV = process.env.FORCE_DEV;
constant FIVE_MINUTES (line 40) | const FIVE_MINUTES = 300;
constant ONE_HOUR (line 41) | const ONE_HOUR = 3600;
FILE: scripts/deploy.js
constant VALID_SERVERS (line 16) | const VALID_SERVERS = ['all', 'api', 'hyperion'];
constant VALID_ALPHA_SERVERS (line 17) | const VALID_ALPHA_SERVERS = ['api', 'hyperion'];
FILE: scripts/generate-table-diagram.js
constant REF_TABLE_MAP (line 4) | const REF_TABLE_MAP = {
FILE: scripts/heroku-deploy.js
constant VALID_SERVERS (line 11) | const VALID_SERVERS = ['all', 'api', 'hyperion'];
FILE: shared/clients/test/messages.test.js
constant FIRST_JAN (line 8) | const FIRST_JAN = 1483225200000;
constant SEVEN_HOURS (line 81) | const SEVEN_HOURS = 25200000;
FILE: shared/db/constants.js
constant READ_RUN_ERROR (line 3) | const READ_RUN_ERROR = `Do not call .run() on the query passed to create...
constant WRITE_RUN_ERROR (line 11) | const WRITE_RUN_ERROR = `Don't forget to call .run() on the query passed...
FILE: shared/db/db.js
constant IS_PROD (line 10) | const IS_PROD = !process.env.FORCE_DEV && process.env.NODE_ENV === 'prod...
constant CONNECTIONS (line 12) | const CONNECTIONS = 20;
constant DEFAULT_CONFIG (line 13) | const DEFAULT_CONFIG = {
constant PRODUCTION_CONFIG (line 33) | const PRODUCTION_CONFIG = {
FILE: shared/db/queries/user.js
method let (line 150) | let promise;
FILE: shared/db/query-cache.js
constant DEFAULT_REDIS_OPTIONS (line 4) | const DEFAULT_REDIS_OPTIONS = {
FILE: shared/draft-utils/add-embeds-to-draft-js.js
constant FIGMA_URLS (line 5) | const FIGMA_URLS = /\b((?:https?:\/\/)?(?:www\.)?figma.com\/(file|proto)...
constant YOUTUBE_URLS (line 6) | const YOUTUBE_URLS = /\b(?:https?:\/\/)?(?:www\.)?youtu(?:be\.com\/watch...
constant VIMEO_URLS (line 7) | const VIMEO_URLS = /\b(?:https?:\/\/)?(?:www\.)?vimeo.com\/(?:channels\/...
constant IFRAME_TAG (line 8) | const IFRAME_TAG = /<iframe.+?src=['"](.+?)['"]/gi;
constant FRAMER_URLS (line 9) | const FRAMER_URLS = /\b(?:https?:\/\/)?(?:www\.)?(?:framer\.cloud|share\...
constant CODEPEN_URLS (line 10) | const CODEPEN_URLS = /\b(?:https?:\/\/)?(?:www\.)?codepen\.io(\/[A-Za-z0...
constant CODESANDBOX_URLS (line 11) | const CODESANDBOX_URLS = /\b(?:https?:\/\/)?(?:www\.)?codesandbox\.io(\/...
constant SIMPLECAST_URLS (line 12) | const SIMPLECAST_URLS = /\b(?:https?:\/\/)?(?:www\.)?simplecast\.com(\/[...
constant THREAD_URLS (line 13) | const THREAD_URLS = /(?:(?:https?:\/\/)?|\B)(?:spectrum\.chat|localhost:...
constant REGEXPS (line 15) | const REGEXPS = {
FILE: shared/encryption/index.js
function encryptString (line 18) | function encryptString(text /*: string */) /*: string */ {
function decryptString (line 22) | function decryptString(text /*: string */) /*: string */ {
function encryptObject (line 26) | function encryptObject(object /*: Object */) /*: Object */ {
function decryptObject (line 30) | function decryptObject(text /*: string */) /*: Object */ {
FILE: shared/generate-meta-info.js
function setDefault (line 66) | function setDefault(input /*: MaybeMeta */) /*: Meta */ {
function cleanDescription (line 82) | function cleanDescription(input /*: string */) /*: string */ {
function generateMetaInfo (line 86) | function generateMetaInfo(input /*: Input */) /*: Meta */ {
FILE: shared/graphql/apollo-client-options.js
function dataIdFromObject (line 4) | function dataIdFromObject(result) {
FILE: shared/graphql/constants.js
constant IS_PROD (line 4) | const IS_PROD =
constant API_URI (line 7) | const API_URI = IS_PROD ? '/api' : 'http://localhost:3001/api';
constant WS_URI (line 8) | const WS_URI = IS_PROD
FILE: shared/graphql/subscriptions/index.js
constant SUBSCRIBE_TO_WEB_PUSH_MUTATION (line 5) | const SUBSCRIBE_TO_WEB_PUSH_MUTATION = gql`
constant SUBSCRIBE_TO_WEB_PUSH_OPTIONS (line 11) | const SUBSCRIBE_TO_WEB_PUSH_OPTIONS = {
FILE: shared/imgix/sign.js
constant IS_PROD (line 7) | const IS_PROD = process.env.NODE_ENV === 'production';
constant LEGACY_PREFIX (line 8) | const LEGACY_PREFIX = 'https://spectrum.imgix.net/';
FILE: shared/middlewares/security.js
constant IS_PROD (line 7) | const IS_PROD = process.env.NODE_ENV === 'production' && !process.env.FO...
function securityMiddleware (line 9) | function securityMiddleware(
FILE: shared/middlewares/session.js
constant ONE_WEEK (line 5) | const ONE_WEEK = 604800000;
FILE: shared/normalize-url.js
constant STARTS_WITH_PROTOCOL (line 8) | const STARTS_WITH_PROTOCOL = /^https?:\/\//i;
function isRelativeUrl (line 10) | function isRelativeUrl(url) {
function sanitizeUrl (line 14) | function sanitizeUrl(url) {
FILE: shared/sentencify.js
function sentencify (line 16) | function sentencify(strings /*: Array<strings> */) /*:strings */ {
FILE: shared/slate-utils.js
function toJSON (line 14) | function toJSON(state /*: Object */) {
function toState (line 17) | function toState(json /*: Object */) {
function toPlainText (line 21) | function toPlainText(state /*: Object*/) {
function fromPlainText (line 24) | function fromPlainText(string /*: string*/) {
FILE: shared/sort-by-date.js
function sortByDate (line 11) | function sortByDate(
FILE: shared/time-difference.js
constant MS_PER_SECOND (line 2) | const MS_PER_SECOND = 1000;
constant MS_PER_MINUTE (line 3) | const MS_PER_MINUTE = 60000;
constant MS_PER_HOUR (line 4) | const MS_PER_HOUR = 3600000;
constant MS_PER_DAY (line 5) | const MS_PER_DAY = 86400000;
constant MS_PER_YEAR (line 6) | const MS_PER_YEAR = 31536000000;
function timeDifferenceShort (line 8) | function timeDifferenceShort(current: number, previous: number) {
FILE: shared/truncate.js
function truncate (line 13) | function truncate(str /*: string */, length /*: number */) {
FILE: src/api/constants.js
constant IS_PROD (line 2) | const IS_PROD = process.env.NODE_ENV === 'production';
constant SERVER_URL (line 4) | const SERVER_URL = IS_PROD
constant CLIENT_URL (line 10) | const CLIENT_URL = IS_PROD
FILE: src/components/announcementBanner/index.js
method render (line 6) | render() {
FILE: src/components/appViewWrapper/index.js
class AppViewWrapper (line 17) | class AppViewWrapper extends React.Component<Props> {
FILE: src/components/avatar/communityAvatar.js
method render (line 18) | render() {
method render (line 59) | render() {
FILE: src/components/avatar/image.js
method render (line 16) | render() {
FILE: src/components/avatar/userAvatar.js
method render (line 54) | render() {
method render (line 105) | render() {
FILE: src/components/badges/index.js
method render (line 20) | render() {
FILE: src/components/column/index.js
method if (line 62) | if (props.type === 'primary') {
FILE: src/components/conditionalWrap/index.js
function ConditionalWrap (line 20) | function ConditionalWrap({ condition, wrap, children }: Props) {
FILE: src/components/error/ErrorBoundary.js
method if (line 31) | if (this.props.fallbackComponent) {
FILE: src/components/error/SettingsFallback.js
method render (line 12) | render() {
FILE: src/components/formElements/index.js
method render (line 185) | render() {
FILE: src/components/fullscreenView/index.js
method componentDidMount (line 19) | componentDidMount() {
method componentWillUnmount (line 23) | componentWillUnmount() {
FILE: src/components/gallery/browser.js
method constructor (line 33) | constructor(props) {
method componentDidMount (line 61) | componentDidMount() {
method componentWillUnmount (line 66) | componentWillUnmount() {
FILE: src/components/gallery/index.js
method render (line 21) | render() {
FILE: src/components/githubProfile/index.js
method render (line 21) | render() {
FILE: src/components/goop/index.js
method returnGoop (line 56) | returnGoop() {
FILE: src/components/hoverProfile/channelProfile.js
method render (line 33) | render() {
FILE: src/components/hoverProfile/communityProfile.js
method render (line 34) | render() {
FILE: src/components/hoverProfile/loadingHoverProfile.js
method render (line 12) | render() {
FILE: src/components/hoverProfile/userContainer.js
class UserHoverProfileWrapper (line 43) | class UserHoverProfileWrapper extends React.Component<Props, State> {
FILE: src/components/hoverProfile/userProfile.js
method render (line 37) | render() {
FILE: src/components/icon/index.js
method render (line 937) | render() {
FILE: src/components/inboxThread/activity.js
method render (line 14) | render() {
FILE: src/components/inboxThread/header/index.js
method render (line 22) | render() {
FILE: src/components/inboxThread/header/threadHeader.js
method render (line 20) | render() {
FILE: src/components/inboxThread/header/timestamp.js
method render (line 8) | render() {
FILE: src/components/inboxThread/header/userProfileThreadHeader.js
method render (line 20) | render() {
FILE: src/components/inboxThread/index.js
method render (line 47) | render() {
FILE: src/components/inboxThread/messageCount.js
method render (line 14) | render() {
FILE: src/components/layout/index.js
constant NAVBAR_WIDTH (line 5) | const NAVBAR_WIDTH = 72;
constant NAVBAR_EXPANDED_WIDTH (line 6) | const NAVBAR_EXPANDED_WIDTH = 256;
constant MIN_PRIMARY_COLUMN_WIDTH (line 7) | const MIN_PRIMARY_COLUMN_WIDTH = 600;
constant MIN_SECONDARY_COLUMN_WIDTH (line 8) | const MIN_SECONDARY_COLUMN_WIDTH = 320;
constant MAX_PRIMARY_COLUMN_WIDTH (line 9) | const MAX_PRIMARY_COLUMN_WIDTH = 968;
constant MAX_SECONDARY_COLUMN_WIDTH (line 10) | const MAX_SECONDARY_COLUMN_WIDTH = 400;
constant COL_GAP (line 11) | const COL_GAP = 24;
constant TITLEBAR_HEIGHT (line 12) | const TITLEBAR_HEIGHT = 62;
constant MIN_MAX_WIDTH (line 13) | const MIN_MAX_WIDTH =
constant MAX_WIDTH (line 15) | const MAX_WIDTH =
constant MIN_WIDTH_TO_EXPAND_NAVIGATION (line 17) | const MIN_WIDTH_TO_EXPAND_NAVIGATION = MAX_WIDTH + 256;
constant SINGLE_COLUMN_WIDTH (line 18) | const SINGLE_COLUMN_WIDTH = MAX_WIDTH;
constant MEDIA_BREAK (line 20) | const MEDIA_BREAK =
FILE: src/components/listItems/channel/index.js
method render (line 17) | render() {
FILE: src/components/listItems/index.js
method render (line 28) | render() {
FILE: src/components/menu/index.js
class Menu (line 16) | class Menu extends React.Component<Props, State> {
FILE: src/components/message/index.js
class Message (line 60) | class Message extends React.Component<Props> {
FILE: src/components/message/messageErrorFallback.js
method render (line 11) | render() {
FILE: src/components/message/threadAttachment/attachment.js
method render (line 15) | render() {
FILE: src/components/modals/BanUserModal/index.js
class BanUserModal (line 32) | class BanUserModal extends React.Component<Props, State> {
FILE: src/components/modals/modalRoot.js
constant MODAL_COMPONENTS (line 7) | const MODAL_COMPONENTS = {
FILE: src/components/outsideClickHandler/index.js
class OutsideAlerter (line 10) | class OutsideAlerter extends React.Component<Props> {
FILE: src/components/profile/thread.js
method componentDidMount (line 24) | componentDidMount() {
method render (line 44) | render() {
FILE: src/components/reaction/index.js
method render (line 15) | render() {
FILE: src/components/redirectHandler/index.js
method componentDidUpdate (line 16) | componentDidUpdate(prev: Props) {
method render (line 31) | render() {
FILE: src/components/rich-text-editor/style.js
method render (line 42) | render() {
FILE: src/components/scrollManager/index.js
method switch (line 113) | switch (nextProps.history.action) {
FILE: src/components/scrollRow/index.js
class ScrollRow (line 4) | class ScrollRow extends Component {
method render (line 44) | render() {
FILE: src/components/settingsViews/header.js
method render (line 21) | render() {
FILE: src/components/settingsViews/subnav.js
method render (line 17) | render() {
FILE: src/components/threadFeed/index.js
class ThreadFeedPure (line 47) | class ThreadFeedPure extends React.Component<Props> {
method shouldComponentUpdate (line 48) | shouldComponentUpdate(nextProps: Props) {
method if (line 71) | if (curr.hasThreads) {
method if (line 75) | if (curr.hasNoThreads) {
FILE: src/components/toasts/index.js
method if (line 13) | if (!toasts || toasts.length === 0) {
FILE: src/components/usernameSearch/index.js
class UsernameSearch (line 32) | class UsernameSearch extends React.Component<Props, State> {
method constructor (line 33) | constructor(props) {
method componentDidMount (line 45) | componentDidMount() {
FILE: src/components/viewError/index.js
method render (line 28) | render() {
FILE: src/components/withCurrentUser/index.js
class CurrentUserComponent (line 25) | class CurrentUserComponent extends Component<Props> {
method render (line 26) | render() {
FILE: src/helpers/images.js
constant PRO_USER_MAX_IMAGE_SIZE_BYTES (line 2) | const PRO_USER_MAX_IMAGE_SIZE_BYTES = 25000000;
constant PRO_USER_MAX_IMAGE_SIZE_STRING (line 3) | const PRO_USER_MAX_IMAGE_SIZE_STRING = `${Math.floor(
FILE: src/helpers/keycodes.js
constant ENTER (line 2) | const ENTER = 13;
constant ESC (line 3) | const ESC = 27;
constant BACKSPACE (line 4) | const BACKSPACE = 8;
constant ARROW_RIGHT (line 5) | const ARROW_RIGHT = 39;
constant ARROW_DOWN (line 6) | const ARROW_DOWN = 40;
constant ARROW_UP (line 7) | const ARROW_UP = 38;
constant ARROW_LEFT (line 8) | const ARROW_LEFT = 37;
constant DELETE (line 9) | const DELETE = 46;
FILE: src/helpers/regexps.js
constant URLS (line 2) | const URLS = /https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,...
constant FIGMA_URLS (line 4) | const FIGMA_URLS = /https:\/\/([w.-]+.)?figma.com\/(file|proto)\/([0-9a-...
constant YOUTUBE_URLS (line 6) | const YOUTUBE_URLS = /http(?:s?):\/\/(?:www\.)?youtu(?:be\.com\/watch\?v...
constant VIMEO_URLS (line 8) | const VIMEO_URLS = /\/\/(?:www\.)?vimeo.com\/([0-9a-z\-_]+)/gi;
constant IFRAME_TAG (line 10) | const IFRAME_TAG = /(<iframe.*?src=['"](.*?)['"])/gi;
constant FRAMER_URLS (line 12) | const FRAMER_URLS = /(https?:\/\/(.+?\.)?framer\.cloud(\/[A-Za-z0-9\-\._...
constant CODEPEN_URLS (line 14) | const CODEPEN_URLS = /(https?:\/\/)?(.+?\.)?codepen\.io(\/[A-Za-z0-9\-\....
constant CODESANDBOX_URLS (line 16) | const CODESANDBOX_URLS = /(https?:\/\/)?(.+?\.)?codesandbox\.io(\/[A-Za-...
constant ENDS_IN_WHITESPACE (line 18) | const ENDS_IN_WHITESPACE = /(\s|\n)$/;
FILE: src/helpers/render-text-with-markdown-links.js
constant MARKDOWN_LINK (line 6) | const MARKDOWN_LINK = /(?:\[(.*?)\]\((.*?)\))/g;
FILE: src/helpers/utils.js
function isMobile (line 10) | function isMobile() {
FILE: src/helpers/web-push-manager.js
function urlB64ToUint8Array (line 1) | function urlB64ToUint8Array(base64String) {
class WebPushManager (line 14) | class WebPushManager {
method constructor (line 18) | constructor() {
FILE: src/hooks/useDebounce.js
function useDebounce (line 5) | function useDebounce(value: string, delay: number) {
FILE: src/index.js
function render (line 56) | function render() {
FILE: src/reducers/connectionStatus.js
function status (line 23) | function status(
FILE: src/reducers/gallery.js
function gallery (line 7) | function gallery(state = initialState, action) {
FILE: src/reducers/modals.js
function modal (line 7) | function modal(state = initialState, action) {
FILE: src/reducers/threadSlider.js
function threadSlider (line 6) | function threadSlider(state = initialState, action) {
FILE: src/reducers/titlebar.js
function titlebar (line 9) | function titlebar(
FILE: src/reducers/toasts.js
function toasts (line 13) | function toasts(state = initialState, action) {
FILE: src/registerServiceWorker.js
constant IS_PROD (line 18) | const IS_PROD = process.env.NODE_ENV === 'production';
method if (line 25) | if ('serviceWorker' in navigator) {
function unregister (line 60) | function unregister() {
FILE: src/views/authViewHandler/index.js
method componentDidUpdate (line 23) | componentDidUpdate(prev: Props) {
FILE: src/views/channel/components/MembersList.js
method shouldComponentUpdate (line 33) | shouldComponentUpdate(nextProps) {
method render (line 41) | render() {
FILE: src/views/channel/index.js
method constructor (line 53) | constructor(props) {
method componentDidMount (line 65) | componentDidMount() {
method componentDidUpdate (line 84) | componentDidUpdate(prevProps) {
FILE: src/views/channelSettings/components/channelMembers.js
method shouldComponentUpdate (line 30) | shouldComponentUpdate(nextProps: Props) {
method render (line 45) | render() {
FILE: src/views/channelSettings/components/editForm.js
class ChannelWithData (line 56) | class ChannelWithData extends React.Component<Props, State> {
method constructor (line 57) | constructor(props) {
FILE: src/views/channelSettings/components/overview.js
method render (line 14) | render() {
FILE: src/views/channelSettings/index.js
method componentDidMount (line 33) | componentDidMount() {
method render (line 43) | render() {
FILE: src/views/community/components/membersList.js
method shouldComponentUpdate (line 34) | shouldComponentUpdate(nextProps) {
method render (line 42) | render() {
FILE: src/views/community/components/teamMembersList.js
method render (line 33) | render() {
FILE: src/views/communityLogin/index.js
class Login (line 41) | class Login extends React.Component<Props, State> {
method constructor (line 42) | constructor(props: Props) {
FILE: src/views/communityMembers/components/communityMembers.js
class CommunityMembers (line 42) | class CommunityMembers extends React.Component<Props, State> {
FILE: src/views/communityMembers/components/getMembers.js
method render (line 21) | render() {
FILE: src/views/communityMembers/index.js
method render (line 22) | render() {
FILE: src/views/communitySettings/components/channelList.js
method render (line 28) | render() {
FILE: src/views/communitySettings/components/editForm.js
class EditForm (line 58) | class EditForm extends React.Component<Props, State> {
method constructor (line 59) | constructor(props) {
FILE: src/views/communitySettings/components/overview.js
method render (line 15) | render() {
FILE: src/views/communitySettings/index.js
method componentDidMount (line 30) | componentDidMount() {
method render (line 39) | render() {
FILE: src/views/directMessages/components/messageThreadListItem.js
method render (line 24) | render() {
FILE: src/views/directMessages/components/messages.js
method componentDidMount (line 29) | componentDidMount() {
method getSnapshotBeforeUpdate (line 39) | getSnapshotBeforeUpdate(prev) {
method snapshot (line 95) | snapshot) {
FILE: src/views/directMessages/components/threadsList.js
method componentDidUpdate (line 39) | componentDidUpdate(prev: Props) {
method shouldComponentUpdate (line 48) | shouldComponentUpdate(nextProps) {
FILE: src/views/directMessages/containers/existingThread.js
method componentDidMount (line 42) | componentDidMount() {
method componentDidUpdate (line 49) | componentDidUpdate(prev) {
method render (line 94) | render() {
FILE: src/views/directMessages/containers/index.js
method componentDidMount (line 31) | componentDidMount() {
method componentDidUpdate (line 40) | componentDidUpdate() {
method render (line 52) | render() {
FILE: src/views/explore/index.js
method componentDidMount (line 19) | componentDidMount() {
method render (line 24) | render() {
FILE: src/views/explore/view.js
class CollectionSwitcher (line 39) | class CollectionSwitcher extends React.Component<{}, State> {
method render (line 114) | render() {
FILE: src/views/login/index.js
method componentDidMount (line 22) | componentDidMount() {
method render (line 27) | render() {
FILE: src/views/newUserOnboarding/components/setUsername/index.js
class SetUsername (line 30) | class SetUsername extends React.Component<Props, State> {
method super (line 34) | super(props);
FILE: src/views/newUserOnboarding/index.js
class NewUserOnboarding (line 24) | class NewUserOnboarding extends React.Component<Props> {
method componentDidMount (line 25) | componentDidMount() {
FILE: src/views/pages/privacy/index.js
method componentDidMount (line 5) | componentDidMount() {
method render (line 9) | render() {
FILE: src/views/pages/terms/index.js
method componentDidMount (line 5) | componentDidMount() {
method render (line 9) | render() {
FILE: src/views/queryParamToastDispatcher/index.js
class QueryParamToastDispatcher (line 16) | class QueryParamToastDispatcher extends React.Component<Props> {
FILE: src/views/status/index.js
class Status (line 26) | class Status extends React.Component<Props, State> {
method if (line 106) | if (prevProps.websocketConnection !== curr.websocketConnection) {
FILE: src/views/thread/components/actionBar.js
method render (line 18) | render() {
FILE: src/views/thread/components/messagesSubscriber.js
class Messages (line 35) | class Messages extends React.Component<Props> {
method componentDidMount (line 36) | componentDidMount() {
method getSnapshotBeforeUpdate (line 46) | getSnapshotBeforeUpdate(prev) {
method if (line 118) | if (currMessageConnection.edges.length > 0) {
FILE: src/views/thread/components/threadByline.js
method render (line 21) | render() {
FILE: src/views/thread/components/threadDetail.js
class ThreadDetailPure (line 40) | class ThreadDetailPure extends React.Component<Props, State> {
method if (line 71) | if (
FILE: src/views/user/components/communityList.js
method render (line 22) | render() {
FILE: src/views/user/index.js
class UserView (line 76) | class UserView extends React.Component<Props, State> {
FILE: src/views/userSettings/components/deleteAccountForm.js
class DeleteAccountForm (line 43) | class DeleteAccountForm extends React.Component<Props, State> {
FILE: src/views/userSettings/components/downloadDataForm.js
method render (line 29) | render() {
FILE: src/views/userSettings/components/editForm.js
class UserWithData (line 69) | class UserWithData extends React.Component<Props, State> {
method constructor (line 70) | constructor(props) {
FILE: src/views/userSettings/components/overview.js
method render (line 16) | render() {
FILE: src/views/userSettings/index.js
method componentDidMount (line 30) | componentDidMount() {
method render (line 39) | render() {
Condensed preview — 1114 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (3,728K chars).
[
{
"path": ".babelrc",
"chars": 670,
"preview": "{\n \"presets\": [\n [\n \"env\",\n {\n \"targets\": {\n \"node\": \"current\"\n },\n \"useBuil"
},
{
"path": ".circleci/config.yml",
"chars": 4983,
"preview": "# CircleCI configuration for Spectrum\nversion: 2.1\n\n# Aliases\naliases:\n # Cache Management\n - &restore-yarn-cache\n "
},
{
"path": ".dockerignore",
"chars": 48,
"preview": ".git\n*docker-compose*\n*Dockerfile*\nnode_modules\n"
},
{
"path": ".eslintignore",
"chars": 386,
"preview": "# Note: This is a copy of the .gitignore,\n# with flow-typed added\nflow-typed\nnode_modules\n.sass-cache\nnpm-debug.log\nbuil"
},
{
"path": ".eslintrc.js",
"chars": 803,
"preview": "module.exports = {\n extends: ['eslint:recommended', 'plugin:react/recommended'],\n parser: 'babel-eslint',\n env: {\n "
},
{
"path": ".flowconfig",
"chars": 672,
"preview": "[ignore]\n.*/build.*\n.*/*.test.js\n.*/node_modules/cypress\n.*/node_modules/draft-js\n.*/node_modules/graphql\n.*/node_module"
},
{
"path": ".github/ISSUE_TEMPLATE.md",
"chars": 314,
"preview": "<!--\nFILL OUT THE FORM BELOW OR THE ISSUE WILL BE AUTO-CLOSED\n\n**Issue Type (check one)**\n\n- [ ] Bug Report\n- [ ] Featur"
},
{
"path": ".github/PULL_REQUEST_TEMPLATE.md",
"chars": 526,
"preview": "<!-- FILL OUT THE BELOW FORM OR YOUR PR WILL BE AUTOMATICALLY CLOSED -->\n\n**Status**\n\n- [ ] WIP\n- [ ] Ready for review\n-"
},
{
"path": ".gitignore",
"chars": 424,
"preview": "node_modules\n.sass-cache\nnpm-debug.log\nbuild\n.DS_Store\nsrc/config/FirebaseConfig.js\nnpm-debug.log\nyarn-error.log\nrethink"
},
{
"path": ".npmignore",
"chars": 642,
"preview": "# This is used by now when deploying hyperion, replacing .gitignore\n# NOTE(@mxstbr): The important change is that `cacer"
},
{
"path": ".prettierignore",
"chars": 23,
"preview": "flow-typed\npackage.json"
},
{
"path": ".prettierrc",
"chars": 52,
"preview": "{\n \"singleQuote\": true,\n \"trailingComma\": \"es5\"\n}\n"
},
{
"path": "LICENSE",
"chars": 1463,
"preview": "Copyright 2018 Space Program Inc.\n\nRedistribution and use in source and binary forms, with or without modification, are "
},
{
"path": "README.md",
"chars": 11412,
"preview": "<div align=\"center\">\n\n[](https://spectrum.chat)\n\n### Simple, powerful online communit"
},
{
"path": "api/apollo-server.js",
"chars": 4072,
"preview": "// @flow\nconst debug = require('debug')('api:graphql');\nimport { ApolloServer } from 'apollo-server-express';\nimport res"
},
{
"path": "api/authentication.js",
"chars": 12814,
"preview": "// @flow\nrequire('now-env');\nconst passport = require('passport');\nconst { Strategy: TwitterStrategy } = require('passpo"
},
{
"path": "api/index.js",
"chars": 2978,
"preview": "// @flow\n/**\n * The entry point for the server, this is where everything starts\n */\nconst compression = require('compres"
},
{
"path": "api/loaders/channel.js",
"chars": 680,
"preview": "// @flow\nimport { getChannels, getChannelsThreadCounts } from '../models/channel';\nimport { getChannelsSettings } from '"
},
{
"path": "api/loaders/community.js",
"chars": 1052,
"preview": "// @flow\nimport {\n getCommunities,\n getCommunitiesBySlug,\n getCommunitiesChannelCounts,\n getCommunitiesMemberCounts,"
},
{
"path": "api/loaders/create-loader.js",
"chars": 1344,
"preview": "// @flow\nimport DataLoader from 'dataloader';\nimport unique from 'shared/unique-elements';\nimport type { Loader, DataLoa"
},
{
"path": "api/loaders/directMessageThread.js",
"chars": 831,
"preview": "// @flow\nimport { getDirectMessageThreads } from '../models/directMessageThread';\nimport { getMembersInDirectMessageThre"
},
{
"path": "api/loaders/index.js",
"chars": 2531,
"preview": "// @flow\nimport {\n __createUserLoader,\n __createUserByUsernameLoader,\n __createUserThreadCountLoader,\n __createUserP"
},
{
"path": "api/loaders/message.js",
"chars": 389,
"preview": "// @flow\nimport { getManyMessages } from '../models/message';\nimport createLoader from './create-loader';\nimport type { "
},
{
"path": "api/loaders/reaction.js",
"chars": 478,
"preview": "// @flow\nimport { getReactions, getReactionsByIds } from '../models/reaction';\nimport createLoader from './create-loader"
},
{
"path": "api/loaders/thread.js",
"chars": 519,
"preview": "// @flow\nimport { getThreads } from '../models/thread';\nimport { getParticipantsInThreads } from '../models/usersThreads"
},
{
"path": "api/loaders/threadReaction.js",
"chars": 372,
"preview": "// @flow\nimport { getThreadReactions } from '../models/threadReaction';\nimport createLoader from './create-loader';\n\nexp"
},
{
"path": "api/loaders/types.js",
"chars": 242,
"preview": "// @flow\n\nexport type Loader = {\n load: (key: string | Array<string>) => Promise<any>,\n loadMany: (keys: Array<*>) => "
},
{
"path": "api/loaders/user.js",
"chars": 1250,
"preview": "// @flow\nimport {\n getUsers,\n getUsersThreadCount,\n getUsersByUsername,\n} from 'shared/db/queries/user';\nimport { get"
},
{
"path": "api/migrations/20170410074258-initial-data.js",
"chars": 8548,
"preview": "exports.up = function(r, conn) {\n return (\n Promise.all([\n r\n .tableCreate('threads')\n .run(conn)"
},
{
"path": "api/migrations/20170613200350-notifications.js",
"chars": 6126,
"preview": "exports.up = function(r, conn) {\n return (\n // Create new tables, update old ones with receiveNotifications\n Prom"
},
{
"path": "api/migrations/20170616113103-compound-indexes-for-ordering.js",
"chars": 899,
"preview": "exports.up = function(r, conn) {\n return Promise.all([\n // messages#threadIdAndTimestamp\n r\n .table('message"
},
{
"path": "api/migrations/20170627104435-user-email-settings.js",
"chars": 1274,
"preview": "exports.up = function(r, conn) {\n return Promise.all([\n r\n .tableCreate('usersSettings')\n .run(conn)\n "
},
{
"path": "api/migrations/20170701173337-linkify-messages.js",
"chars": 763,
"preview": "const markdownLinkify = require('../utils/markdown-linkify');\n\nexports.up = function(r, conn) {\n return (\n // Markdo"
},
{
"path": "api/migrations/20170702194221-fix-images.js",
"chars": 966,
"preview": "const MARKDOWN_LINK = /(?:\\[(.*?)\\]\\((.*?)\\))/g;\n\nexports.up = function(r, conn) {\n return (\n // The last migration "
},
{
"path": "api/migrations/20170706114239-providerfield-indexes.js",
"chars": 434,
"preview": "exports.up = function(r, conn) {\n return Promise.all([\n r\n .table('users')\n .indexCreate('providerId')\n "
},
{
"path": "api/migrations/20170706205658-slack-import.js",
"chars": 744,
"preview": "exports.up = function(r, conn) {\n return Promise.all([\n r\n .tableCreate('slackImports')\n .run(conn)\n "
},
{
"path": "api/migrations/20170714171920-web-push-subscription.js",
"chars": 559,
"preview": "exports.up = function(r, conn) {\n return r\n .tableCreate('webPushSubscriptions')\n .run(conn)\n .catch(err => {\n"
},
{
"path": "api/migrations/20170724184557-notifications-entity-added-index.js",
"chars": 361,
"preview": "exports.up = function(r, conn) {\n return Promise.all([\n r\n .table('usersNotifications')\n .indexCreate('use"
},
{
"path": "api/migrations/20170803104302-dedupe-users-settings.js",
"chars": 3178,
"preview": "const without = require('lodash/without');\n// This is taken from the Babel REPL and is what it transpiles ...arr to.\n// "
},
{
"path": "api/migrations/20170825220615-clean-recurring-payments.js",
"chars": 3228,
"preview": "exports.up = function(r, conn) {\n // get all recurring payments\n return r\n .table('recurringPayments')\n .run(con"
},
{
"path": "api/migrations/20170829233734-userid-index-on-invoices.js",
"chars": 267,
"preview": "exports.up = function(r, conn) {\n return Promise.all([\n r\n .table('invoices')\n .indexCreate('userId')\n "
},
{
"path": "api/migrations/20170831163211-invoice-data-model-update.js",
"chars": 1569,
"preview": "exports.up = function(r, conn) {\n // get all recurring payments\n return r\n .table('invoices')\n .run(conn)\n .t"
},
{
"path": "api/migrations/20170907222544-digest-email-notification-settings.js",
"chars": 1993,
"preview": "exports.up = function(r, conn) {\n return Promise.all([\n r\n .table('usersSettings')\n .run(conn)\n .then"
},
{
"path": "api/migrations/20170908230623-add-reputation-field-to-communities.js",
"chars": 957,
"preview": "exports.up = function(r, conn) {\n return Promise.all([\n r\n .table('usersCommunities')\n .update({\n r"
},
{
"path": "api/migrations/20170912000619-backfill-rep.js",
"chars": 1066,
"preview": "exports.up = function(r, conn) {\n const updateReputation = (userId, communityId, score, type) => {\n return r\n ."
},
{
"path": "api/migrations/20170915201609-clean-up-bad-dm-data.js",
"chars": 445,
"preview": "exports.up = function(r, conn) {\n return Promise.all([\n r\n .table('messages')\n .filter({ threadType: 'DIRE"
},
{
"path": "api/migrations/20170926003025-activate-daily-weekly-digest-settings.js",
"chars": 418,
"preview": "exports.up = function(r, conn) {\n return Promise.all([\n r\n .table('usersSettings')\n .update({\n noti"
},
{
"path": "api/migrations/20170926102527-speedy-gonzales.js",
"chars": 298,
"preview": "exports.up = function(r, conn) {\n return Promise.all([\n r\n .table('usersCommunities')\n .indexCreate('userI"
},
{
"path": "api/migrations/20170927002438-communityid-index-on-reputation-events.js",
"chars": 223,
"preview": "exports.up = function(r, conn) {\n return Promise.all([\n r\n .table('reputationEvents')\n .indexCreate('commu"
},
{
"path": "api/migrations/20170928143435-slate-to-draftjs.js.js",
"chars": 2678,
"preview": "const compose = require('redux/lib/compose').default;\nconst { convertToRaw, genKey } = require('draft-js');\nconst { stat"
},
{
"path": "api/migrations/20171005075445-remove-markdown-links-from-messages.js",
"chars": 836,
"preview": "const replace = require('string-replace-to-array');\nconst MARKDOWN_LINK = /(?:\\[(.*?)\\]\\((.*?)\\))/g;\n\nconst removeMarkdo"
},
{
"path": "api/migrations/20171008101118-last-slate-to-draft.js",
"chars": 2507,
"preview": "const compose = require('redux/lib/compose').default;\nconst { convertToRaw, genKey } = require('draft-js');\nconst { stat"
},
{
"path": "api/migrations/20171013195530-core-metrics-table.js",
"chars": 4690,
"preview": "exports.up = function(r, conn) {\n return Promise.all([\n r\n .tableCreate('coreMetrics')\n .run(conn)\n ."
},
{
"path": "api/migrations/20171018235659-add-direst-message-user-settings.js",
"chars": 410,
"preview": "exports.up = function(r, conn) {\n return Promise.all([\n r\n .table('usersSettings')\n .update({\n noti"
},
{
"path": "api/migrations/20171029090619-users-channels-index.js",
"chars": 300,
"preview": "exports.up = function(r, conn) {\n return r\n .table('usersChannels')\n .indexCreate('userIdAndChannelId', [r.row('u"
},
{
"path": "api/migrations/20171029094352-users-threads-index.js",
"chars": 295,
"preview": "exports.up = function(r, conn) {\n return r\n .table('usersThreads')\n .indexCreate('userIdAndThreadId', [r.row('use"
},
{
"path": "api/migrations/20171103014955-add-mention-notification-settings.js",
"chars": 347,
"preview": "exports.up = function(r, conn) {\n return Promise.all([\n r\n .table('usersSettings')\n .update({\n noti"
},
{
"path": "api/migrations/20171129215512-index-communities-by-slug.js",
"chars": 211,
"preview": "exports.up = function(r, conn) {\n return Promise.all([\n r\n .table('communities')\n .indexCreate('slug')\n "
},
{
"path": "api/migrations/20171129221050-curated-content-table-creation.js",
"chars": 3241,
"preview": "exports.up = function(r, conn) {\n return Promise.all([\n r\n .tableCreate('curatedContent')\n .run(conn)\n "
},
{
"path": "api/migrations/20171208175038-index-users-for-search.js",
"chars": 840,
"preview": "require('now-env');\n// const initIndex = require('../../shared/algolia');\n// const searchIndex = initIndex('users');\n\nex"
},
{
"path": "api/migrations/20171208180800-index-communities-for-search.js",
"chars": 935,
"preview": "require('now-env');\n// const initIndex = require('../../shared/algolia');\n// const searchIndex = initIndex('communities'"
},
{
"path": "api/migrations/20171213002813-add-modified-at-field-to-users-and-communities.js",
"chars": 1887,
"preview": "exports.up = function(r, conn) {\n /*\n\n We are adding a modifiedAt field on users and communities for 2 key reasons:\n"
},
{
"path": "api/migrations/20180209015734-github-provider-id-index.js",
"chars": 274,
"preview": "exports.up = function(r, conn) {\n return Promise.all([\n r\n .table('users')\n .indexCreate('githubProviderId"
},
{
"path": "api/migrations/20180214111357-expo-push-subscriptions.js",
"chars": 560,
"preview": "exports.up = function(r, conn) {\n return r\n .tableCreate('expoPushSubscriptions')\n .run(conn)\n .then(() =>\n "
},
{
"path": "api/migrations/20180309144845-create-community-settings-table.js",
"chars": 419,
"preview": "exports.up = function(r, conn) {\n return r\n .tableCreate('communitySettings')\n .run(conn)\n .then(() =>\n r"
},
{
"path": "api/migrations/20180316195507-create-channel-settings-table.js",
"chars": 411,
"preview": "exports.up = function(r, conn) {\n return r\n .tableCreate('channelSettings')\n .run(conn)\n .then(() =>\n r\n "
},
{
"path": "api/migrations/20180320122000-create-stripe-tables.js",
"chars": 685,
"preview": "exports.up = function(r, conn) {\n const createCustomersTable = () =>\n r.tableCreate('stripeCustomers', { primaryKey:"
},
{
"path": "api/migrations/20180320173414-set-administrator-info-on-community.js",
"chars": 2060,
"preview": "exports.up = async (r, conn) => {\n const addFields = r\n .table('communities')\n .update({\n // a distinct emai"
},
{
"path": "api/migrations/20180411183454-lowercase-all-the-slugs.js",
"chars": 525,
"preview": "exports.up = async (r, conn) => {\n return Promise.all([\n r\n .table('users')\n .update({\n username: r"
},
{
"path": "api/migrations/20180428001543-reset-slack-import-records.js",
"chars": 273,
"preview": "exports.up = function(r, conn) {\n return Promise.all([\n r\n .table('slackImports')\n .update({\n membe"
},
{
"path": "api/migrations/20180504003702-encrypt-existing-slack-data.js",
"chars": 1784,
"preview": "const { encryptString } = require('../../shared/encryption');\n\nexports.up = async (r, conn) => {\n const encryptOldSlack"
},
{
"path": "api/migrations/20180517180716-enable-private-communities.js",
"chars": 278,
"preview": "exports.up = async (r, conn) => {\n return r\n .table('communities')\n .update({\n isPrivate: false,\n })\n "
},
{
"path": "api/migrations/20180517215503-add-ispending-to-userscommunities.js",
"chars": 288,
"preview": "exports.up = async (r, conn) => {\n return r\n .table('usersCommunities')\n .update({\n isPending: false,\n })"
},
{
"path": "api/migrations/20180518135040-add-join-settings-to-community-settings.js",
"chars": 354,
"preview": "exports.up = async (r, conn) => {\n return r\n .table('communitySettings')\n .update({\n joinSettings: {\n "
},
{
"path": "api/migrations/20180621001409-thread-likes-table.js",
"chars": 508,
"preview": "exports.up = function(r, conn) {\n return r\n .tableCreate('threadReactions')\n .run(conn)\n .then(() => {\n r"
},
{
"path": "api/migrations/20180823115847-add-users-communities-indexes.js",
"chars": 1463,
"preview": "exports.up = function(r, conn) {\n return Promise.all([\n r\n .table('usersCommunities')\n .indexCreate('commu"
},
{
"path": "api/migrations/20181001061156-thread-metadata-denormalization.js",
"chars": 1200,
"preview": "exports.up = function(r, conn) {\n return r\n .table('threads')\n .update(\n {\n messageCount: r\n "
},
{
"path": "api/migrations/20181001064151-fix-thread-metadata-message-counts.js",
"chars": 693,
"preview": "exports.up = function(r, conn) {\n return r\n .table('threads')\n .update(\n {\n messageCount: r\n "
},
{
"path": "api/migrations/20181002060237-remove-payments.js",
"chars": 1277,
"preview": "exports.up = async (r, conn) => {\n const betaSupporterIds = await r\n .db('spectrum')\n .table('recurringPayments')"
},
{
"path": "api/migrations/20181003233411-thread-reactions-useridandthreadid-index.js",
"chars": 301,
"preview": "exports.up = function(r, conn) {\n return r\n .table('threadReactions')\n .indexCreate('userIdAndThreadId', [r.row('"
},
{
"path": "api/migrations/20181004222636-denormalize-channel-community-member-counts.js",
"chars": 1106,
"preview": "exports.up = function(r, conn) {\n return Promise.all([\n r\n .table('communities')\n .update(\n {\n "
},
{
"path": "api/migrations/20181005143053-users-notifications-useridandnotificationid-index.js",
"chars": 344,
"preview": "exports.up = function(r, conn) {\n return r\n .table('usersNotifications')\n .indexCreate('userIdAndNotificationId',"
},
{
"path": "api/migrations/20181005144259-users-notifications-userIdAndIsSeen-index.js",
"chars": 301,
"preview": "exports.up = function(r, conn) {\n return r\n .table('usersNotifications')\n .indexCreate('userIdAndIsSeen', [r.row("
},
{
"path": "api/migrations/20181023160027-update-denormalized-member-counts.js",
"chars": 886,
"preview": "exports.up = function(r, conn) {\n return Promise.all([\n r\n .table('communities')\n .update(\n {\n "
},
{
"path": "api/migrations/20181024173616-indexes-for-digests.js",
"chars": 666,
"preview": "exports.up = function(r, conn) {\n return Promise.all([\n r\n .table('usersSettings')\n .indexCreate(\n "
},
{
"path": "api/migrations/20181027050052-remove-attachments-from-thread-model.js",
"chars": 271,
"preview": "exports.up = function(r, conn) {\n return Promise.all([\n r\n .table('threads')\n .update({\n attachment"
},
{
"path": "api/migrations/20181102025454-fix-old-image-urls-in-messages.js",
"chars": 927,
"preview": "exports.up = async function(r, conn) {\n const messages = await r\n .db('spectrum')\n .table('messages')\n .filter"
},
{
"path": "api/migrations/20181102040518-fix-old-image-urls-in-threads.js",
"chars": 1917,
"preview": "exports.up = async function(r, conn) {\n const threads = await r\n .db('spectrum')\n .table('threads')\n .filter(r"
},
{
"path": "api/migrations/20181102044407-fix-old-image-urls-in-communities.js",
"chars": 1898,
"preview": "exports.up = async function(r, conn) {\n const communities = await r\n .db('spectrum')\n .table('communities')\n ."
},
{
"path": "api/migrations/20181102045821-fix-old-image-urls-in-users.js",
"chars": 1958,
"preview": "exports.up = async function(r, conn) {\n const users = await r\n .db('spectrum')\n .table('users')\n .filter(row ="
},
{
"path": "api/migrations/20181102054523-fix-aws-static-url-community-photos.js",
"chars": 1432,
"preview": "exports.up = async function(r, conn) {\n const LEGACY_PREFIX = 'https://s3.amazonaws.com/spectrum-chat/';\n\n const commu"
},
{
"path": "api/migrations/20181116173949-add-terms-last-accepted-field-to-users.js",
"chars": 299,
"preview": "exports.up = async (r, conn) => {\n return r\n .table('users')\n .update({\n termsLastAcceptedAt: r.row('created"
},
{
"path": "api/migrations/20181121054300-resync-community-member-counts.js",
"chars": 886,
"preview": "exports.up = function(r, conn) {\n return Promise.all([\n r\n .table('communities')\n .update(\n {\n "
},
{
"path": "api/migrations/20181122162921-users-communities-useridandmember-index.js",
"chars": 303,
"preview": "exports.up = function(r, conn) {\n return r\n .table('usersCommunities')\n .indexCreate('userIdAndIsMember', [r.row("
},
{
"path": "api/migrations/20181126094455-users-channels-roles.js",
"chars": 1243,
"preview": "const branch = (r, field, fallback) => {\n return r.branch(r.row(`is${field}`).eq(true), field.toLowerCase(), fallback);"
},
{
"path": "api/migrations/20181127090014-communities-member-count-index.js",
"chars": 243,
"preview": "exports.up = function(r, conn) {\n return r\n .table('communities')\n .indexCreate('memberCount')\n .run(conn);\n};"
},
{
"path": "api/migrations/20181205171559-remove-old-users-notifications.js",
"chars": 1454,
"preview": "exports.up = async (r, conn) => {\n let after = 0;\n let limit = 10000;\n let done = false;\n\n const getRecords = async "
},
{
"path": "api/migrations/20181211181146-add-usersthreads-user-id-and-participant-index.js",
"chars": 394,
"preview": "exports.up = function(r, conn) {\n return Promise.all([\n r\n .table('usersThreads')\n .indexCreate('userIdAnd"
},
{
"path": "api/migrations/20190226085909-bot-user-sam.js",
"chars": 745,
"preview": "exports.up = function(r, conn) {\n return r\n .table('users')\n .insert({\n id: 'sam',\n description: \"Spect"
},
{
"path": "api/migrations/20190306125252-threads-watercooler-index.js",
"chars": 328,
"preview": "exports.up = function(r, conn) {\n return r\n .table('threads')\n .indexCreate('communityIdAndWatercooler', [\n "
},
{
"path": "api/migrations/20190315142923-backfill-userscommunities-last-seen-community-last-active.js",
"chars": 1270,
"preview": "exports.up = function(r, conn) {\n return Promise.all([\n r\n .table('usersCommunities')\n .update(\n {\n"
},
{
"path": "api/migrations/20190327134509-delete-bot-messages.js",
"chars": 194,
"preview": "exports.up = function(r, conn) {\n return r\n .table('messages')\n .filter({ bot: true })\n .delete()\n .run(con"
},
{
"path": "api/migrations/config.js",
"chars": 1147,
"preview": "const path = require('path');\nconst fs = require('fs');\nconst debug = require('debug')('migrations');\n\nconst DEFAULT_CON"
},
{
"path": "api/migrations/seed/default/channelSettings.js",
"chars": 341,
"preview": "const constants = require('./constants');\nconst { PAYMENTS_PRIVATE_CHANNEL_ID } = constants;\n\nmodule.exports = [\n {\n "
},
{
"path": "api/migrations/seed/default/channels.js",
"chars": 3636,
"preview": "// @flow\nconst constants = require('./constants');\nconst {\n DATE,\n SPECTRUM_COMMUNITY_ID,\n PAYMENTS_COMMUNITY_ID,\n D"
},
{
"path": "api/migrations/seed/default/communities.js",
"chars": 3306,
"preview": "// @flow\nconst constants = require('./constants');\nconst {\n DATE,\n SPECTRUM_COMMUNITY_ID,\n PAYMENTS_COMMUNITY_ID,\n D"
},
{
"path": "api/migrations/seed/default/communitySettings.js",
"chars": 1088,
"preview": "const constants = require('./constants');\nconst {\n PRIVATE_COMMUNITY_WITH_JOIN_TOKEN_ID,\n PAYMENTS_COMMUNITY_ID,\n} = c"
},
{
"path": "api/migrations/seed/default/constants.js",
"chars": 2389,
"preview": "// @flow\nconst DATE = 1483225200000;\n\n// users\nconst MAX_ID = '1';\nconst BRIAN_ID = '2';\nconst BRYN_ID = '3';\n// this us"
},
{
"path": "api/migrations/seed/default/directMessageThreads.js",
"chars": 330,
"preview": "// @flow\nconst constants = require('./constants');\nconst { DATE } = constants;\n\nmodule.exports = [\n {\n id: 'dm-1',\n "
},
{
"path": "api/migrations/seed/default/index.js",
"chars": 1391,
"preview": "// @flow\nconst constants = require('./constants');\nconst defaultUsers = require('./users');\nconst defaultCommunities = r"
},
{
"path": "api/migrations/seed/default/messages.js",
"chars": 4940,
"preview": "// @flow\nconst constants = require('./constants');\nconst { fromPlainText, toJSON } = require('../../../../shared/draft-u"
},
{
"path": "api/migrations/seed/default/notifications.js",
"chars": 695,
"preview": "// @flow\nconst constants = require('./constants');\nconst users = require('./users');\n\nconst { DATE, BRIAN_ID, PREVIOUS_M"
},
{
"path": "api/migrations/seed/default/reactions.js",
"chars": 227,
"preview": "// @flow\nconst constants = require('./constants');\nconst { DATE, MAX_ID } = constants;\n\nmodule.exports = [\n {\n id: '"
},
{
"path": "api/migrations/seed/default/threads.js",
"chars": 10415,
"preview": "// @flow\nconst { fromPlainText, toJSON } = require('../../../../shared/draft-utils');\nconst constants = require('./const"
},
{
"path": "api/migrations/seed/default/users.js",
"chars": 5014,
"preview": "// @flow\nconst constants = require('./constants');\nconst {\n MAX_ID,\n BRIAN_ID,\n BRYN_ID,\n QUIET_USER_ID,\n BLOCKED_U"
},
{
"path": "api/migrations/seed/default/usersChannels.js",
"chars": 9737,
"preview": "// @flow\nconst constants = require('./constants');\nconst {\n DATE,\n BRIAN_ID,\n MAX_ID,\n BRYN_ID,\n BLOCKED_USER_ID,\n "
},
{
"path": "api/migrations/seed/default/usersCommunities.js",
"chars": 4928,
"preview": "// @flow\nconst constants = require('./constants');\nconst {\n DATE,\n MAX_ID,\n BRYN_ID,\n BRIAN_ID,\n PREVIOUS_MEMBER_US"
},
{
"path": "api/migrations/seed/default/usersDirectMessageThreads.js",
"chars": 1123,
"preview": "// @flow\nconst constants = require('./constants');\nconst { DATE, BRIAN_ID, MAX_ID, BRYN_ID, PREVIOUS_MEMBER_USER_ID } = "
},
{
"path": "api/migrations/seed/default/usersNotifications.js",
"chars": 289,
"preview": "// @flow\nconst constants = require('./constants');\nconst { DATE, BRIAN_ID } = constants;\n\nmodule.exports = [\n {\n id:"
},
{
"path": "api/migrations/seed/default/usersSettings.js",
"chars": 639,
"preview": "// @flow\n\nmodule.exports = () => {\n let settings = [];\n for (let step = 0; step < 11; step++) {\n settings.push({\n "
},
{
"path": "api/migrations/seed/default/usersThreads.js",
"chars": 1568,
"preview": "// @flow\nconst constants = require('./constants');\nconst { DATE, MAX_ID, BRYN_ID, BRIAN_ID } = constants;\n\nmodule.export"
},
{
"path": "api/migrations/seed/generate.js",
"chars": 6900,
"preview": "// $FlowFixMe\nconst { v4: uuid } = require('uuid');\n// $FlowFixMe\nconst faker = require('faker');\n// $FlowFixMe\nconst sl"
},
{
"path": "api/migrations/seed/index.js",
"chars": 6111,
"preview": "// @flow\nconst faker = require('faker');\nconst debug = require('debug')('api:migrations:seed');\nconst { v4: uuid } = req"
},
{
"path": "api/models/channel.js",
"chars": 6871,
"preview": "// @flow\nconst { db } = require('shared/db');\nimport type { DBChannel } from 'shared/types';\n\n// reusable query parts --"
},
{
"path": "api/models/channelSettings.js",
"chars": 924,
"preview": "// @flow\nconst { db } = require('shared/db');\nimport type { DBChannelSettings } from 'shared/types';\n\nconst defaultSetti"
},
{
"path": "api/models/community.js",
"chars": 10389,
"preview": "// @flow\nconst { db } = require('shared/db');\nimport intersection from 'lodash.intersection';\nimport { uploadImage } fro"
},
{
"path": "api/models/communitySettings.js",
"chars": 2217,
"preview": "// @flow\nconst { db } = require('shared/db');\nimport type { DBCommunitySettings } from 'shared/types';\n\nconst defaultSet"
},
{
"path": "api/models/curatedContent.js",
"chars": 469,
"preview": "//@flow\nconst { db } = require('shared/db');\nimport type { DBCommunity } from 'shared/types';\nimport { getCommunitiesByS"
},
{
"path": "api/models/directMessageThread.js",
"chars": 2544,
"preview": "//@flow\nconst { db } = require('shared/db');\n\nexport type DBDirectMessageThread = {\n createdAt: Date,\n id: string,\n n"
},
{
"path": "api/models/message.js",
"chars": 5081,
"preview": "//@flow\nconst { db } = require('shared/db');\nimport { incrementMessageCount, decrementMessageCount } from './thread';\nim"
},
{
"path": "api/models/reaction.js",
"chars": 912,
"preview": "// @flow\nimport { db } from 'shared/db';\nimport type { DBReaction } from 'shared/types';\n\ntype ReactionType = 'like';\n\ne"
},
{
"path": "api/models/search.js",
"chars": 4154,
"preview": "//@flow\nconst { db } = require('shared/db');\n\n// prettier-ignore\nexport const getPublicChannelIdsInCommunity = (communit"
},
{
"path": "api/models/session.js",
"chars": 167,
"preview": "// @flow\nimport { db } from 'shared/db';\n\nexport const destroySession = (id: string) => {\n return db\n .table('sessio"
},
{
"path": "api/models/test/__snapshots__/channel.test.js.snap",
"chars": 4681,
"preview": "// Jest Snapshot v1, https://goo.gl/fbAQLP\n\nexports[`models/channel getChannels excludes deleted channels 1`] = `\nArray "
},
{
"path": "api/models/test/channel.test.js",
"chars": 4249,
"preview": "// @flow\n\nimport * as channel from '../channel';\n\nconst queries = channel.__forQueryTests;\n\nimport {\n BRIAN_ID,\n MAX_I"
},
{
"path": "api/models/thread.js",
"chars": 13431,
"preview": "// @flow\nconst { db } = require('shared/db');\nimport intersection from 'lodash.intersection';\nconst { parseRange } = req"
},
{
"path": "api/models/threadReaction.js",
"chars": 782,
"preview": "// @flow\nimport { db } from 'shared/db';\nimport type { DBThreadReaction } from 'shared/types';\n\n// prettier-ignore\nexpor"
},
{
"path": "api/models/usersChannels.js",
"chars": 6351,
"preview": "// @flow\nconst { db } = require('shared/db');\nimport { decrementMemberCount, setMemberCount } from './channel';\nimport t"
},
{
"path": "api/models/usersCommunities.js",
"chars": 6585,
"preview": "// @flow\nconst { db } = require('shared/db');\nimport type { DBUsersCommunities } from 'shared/types';\nimport { decrement"
},
{
"path": "api/models/usersDirectMessageThreads.js",
"chars": 2613,
"preview": "// @flow\nconst { db } = require('shared/db');\n\n/*\n===========================================================\n\n MODIFYI"
},
{
"path": "api/models/usersSettings.js",
"chars": 1922,
"preview": "const { db } = require('shared/db');\nimport type { DBUserSettings } from 'shared/types';\n\nexport const createNewUsersSet"
},
{
"path": "api/models/usersThreads.js",
"chars": 1093,
"preview": "// @flow\nimport type { DBUsersThreads } from 'shared/types';\nconst { db } = require('shared/db');\n\n// prettier-ignore\nex"
},
{
"path": "api/models/utils.js",
"chars": 918,
"preview": "// @flow\nimport { db } from 'shared/db';\n\nexport type Timeframe = 'daily' | 'weekly' | 'monthly' | 'quarterly';\n\nexport "
},
{
"path": "api/mutations/channel/deleteChannel.js",
"chars": 1253,
"preview": "// @flow\nimport type { GraphQLContext } from '../../';\nimport UserError from '../../utils/UserError';\nimport { removeMem"
},
{
"path": "api/mutations/channel/editChannel.js",
"chars": 916,
"preview": "// @flow\nimport type { GraphQLContext } from '../../';\nimport type { EditChannelInput } from '../../models/channel';\nimp"
},
{
"path": "api/mutations/channel/index.js",
"chars": 173,
"preview": "// @flow\nimport deleteChannel from './deleteChannel';\nimport editChannel from './editChannel';\n\nmodule.exports = {\n Mut"
},
{
"path": "api/mutations/community/deleteCommunity.js",
"chars": 2149,
"preview": "// @flow\nimport type { GraphQLContext } from '../../';\nimport UserError from '../../utils/UserError';\nimport { removeMem"
},
{
"path": "api/mutations/community/editCommunity.js",
"chars": 781,
"preview": "// @flow\nimport type { GraphQLContext } from '../../';\nimport type { EditCommunityInput } from '../../models/community';"
},
{
"path": "api/mutations/community/index.js",
"chars": 373,
"preview": "// @flow\nimport deleteCommunity from './deleteCommunity';\nimport editCommunity from './editCommunity';\nimport toggleComm"
},
{
"path": "api/mutations/community/toggleCommunityNoindex.js",
"chars": 820,
"preview": "// @flow\nimport type { GraphQLContext } from '../..';\nimport UserError from '../../utils/UserError';\nimport {\n toggleCo"
},
{
"path": "api/mutations/community/toggleCommunityRedirect.js",
"chars": 823,
"preview": "// @flow\nimport type { GraphQLContext } from '../../';\nimport UserError from '../../utils/UserError';\nimport {\n toggleC"
},
{
"path": "api/mutations/files/index.js",
"chars": 109,
"preview": "// @flow\nimport uploadImage from './uploadImage';\n\nmodule.exports = {\n Mutation: {\n uploadImage,\n },\n};\n"
},
{
"path": "api/mutations/files/uploadImage.js",
"chars": 571,
"preview": "// @flow\nimport { isAuthedResolver } from '../../utils/permissions';\nimport { uploadImage } from '../../utils/file-stora"
},
{
"path": "api/mutations/message/deleteMessage.js",
"chars": 2194,
"preview": "// @flow\nimport type { GraphQLContext } from '../../';\nimport UserError from '../../utils/UserError';\nimport {\n getMess"
},
{
"path": "api/mutations/message/index.js",
"chars": 115,
"preview": "// @flow\nimport deleteMessage from './deleteMessage';\n\nmodule.exports = {\n Mutation: {\n deleteMessage,\n },\n};\n"
},
{
"path": "api/mutations/thread/deleteThread.js",
"chars": 1754,
"preview": "// @flow\nimport type { GraphQLContext } from '../../';\nimport UserError from '../../utils/UserError';\nimport { getUserPe"
},
{
"path": "api/mutations/thread/index.js",
"chars": 112,
"preview": "// @flow\nimport deleteThread from './deleteThread';\n\nmodule.exports = {\n Mutation: {\n deleteThread,\n },\n};\n"
},
{
"path": "api/mutations/user/banUser.js",
"chars": 1185,
"preview": "// @flow\nimport { isAuthedResolver, isAdmin } from '../../utils/permissions';\nimport UserError from '../../utils/UserErr"
},
{
"path": "api/mutations/user/deleteCurrentUser.js",
"chars": 824,
"preview": "// @flow\nimport type { GraphQLContext } from '../../';\nimport UserError from '../../utils/UserError';\nimport { deleteUse"
},
{
"path": "api/mutations/user/editUser.js",
"chars": 2204,
"preview": "// @flow\nimport Raven from 'shared/raven';\nimport type { GraphQLContext } from '../../';\nimport type { EditUserInput } f"
},
{
"path": "api/mutations/user/index.js",
"chars": 222,
"preview": "// @flow\nimport editUser from './editUser';\nimport deleteCurrentUser from './deleteCurrentUser';\nimport banUser from './"
},
{
"path": "api/package.json",
"chars": 4439,
"preview": "{\n \"dependencies\": {\n \"apollo-local-query\": \"^0.3.1\",\n \"apollo-server-cache-redis\": \"^0.3.1\",\n \"apollo-server-"
},
{
"path": "api/queries/channel/channelPermissions.js",
"chars": 621,
"preview": "// @flow\nimport type { GraphQLContext } from '../../';\nimport type { DBChannel } from 'shared/types';\n\nexport default as"
},
{
"path": "api/queries/channel/community.js",
"chars": 573,
"preview": "// @flow\nimport type { GraphQLContext } from '../../';\nimport type { DBChannel } from 'shared/types';\nimport { canViewCo"
},
{
"path": "api/queries/channel/communityPermissions.js",
"chars": 648,
"preview": "// @flow\nimport type { GraphQLContext } from '../../';\nimport type { DBChannel } from 'shared/types';\n\nexport default as"
},
{
"path": "api/queries/channel/index.js",
"chars": 800,
"preview": "// @flow\nimport channel from './rootChannel';\n\nimport memberCount from './memberCount';\nimport threadConnection from './"
},
{
"path": "api/queries/channel/isArchived.js",
"chars": 164,
"preview": "// @flow\nimport type { DBChannel } from 'shared/types';\n\nexport default ({ archivedAt, ...rest }: DBChannel) => {\n if ("
},
{
"path": "api/queries/channel/joinSettings.js",
"chars": 513,
"preview": "// @flow\nimport type { DBChannel } from 'shared/types';\nimport type { GraphQLContext } from '../../';\nimport { canModera"
},
{
"path": "api/queries/channel/memberConnection.js",
"chars": 1297,
"preview": "// @flow\nimport type { PaginationOptions } from '../../utils/paginate-arrays';\nimport type { GraphQLContext } from '../."
},
{
"path": "api/queries/channel/memberCount.js",
"chars": 457,
"preview": "// @flow\nimport type { DBChannel } from 'shared/types';\nimport type { GraphQLContext } from '../../';\nimport { canViewCh"
},
{
"path": "api/queries/channel/metaData.js",
"chars": 886,
"preview": "// TODO: Flow type again\nimport Raven from 'shared/raven';\nimport type { DBChannel } from 'shared/types';\nimport type { "
},
{
"path": "api/queries/channel/moderators.js",
"chars": 570,
"preview": "// @flow\nimport type { GraphQLContext } from '../../';\nimport type { DBChannel } from 'shared/types';\nimport { getModera"
},
{
"path": "api/queries/channel/owners.js",
"chars": 562,
"preview": "// @flow\nimport type { GraphQLContext } from '../../';\nimport type { DBChannel } from 'shared/types';\nimport { getOwners"
},
{
"path": "api/queries/channel/rootChannel.js",
"chars": 893,
"preview": "// @flow\nimport type { GraphQLContext } from '../../';\nimport type { GetChannelArgs } from '../../models/channel';\nimpor"
},
{
"path": "api/queries/channel/threadConnection.js",
"chars": 972,
"preview": "// @flow\nimport type { GraphQLContext } from '../../';\nimport type { PaginationOptions } from '../../utils/paginate-arra"
},
{
"path": "api/queries/community/brandedLogin.js",
"chars": 317,
"preview": "// @flow\nimport type { DBCommunity } from 'shared/types';\nimport type { GraphQLContext } from '../../';\n\nexport default "
},
{
"path": "api/queries/community/channelConnection.js",
"chars": 686,
"preview": "// @flow\nimport type { GraphQLContext } from '../../';\nimport type { DBCommunity } from 'shared/types';\nimport { getChan"
},
{
"path": "api/queries/community/communityPermissions.js",
"chars": 710,
"preview": "// @flow\nimport type { DBCommunity } from 'shared/types';\nimport type { GraphQLContext } from '../../';\nimport { DEFAULT"
},
{
"path": "api/queries/community/contextPermissions.js",
"chars": 1047,
"preview": "// @flow\nimport type { DBCommunity } from 'shared/types';\nimport type { GraphQLContext } from '../../';\n\n(community: DBC"
},
{
"path": "api/queries/community/coverPhoto.js",
"chars": 300,
"preview": "// @flow\nimport type { GraphQLContext } from '../../';\nimport type { DBCommunity } from 'shared/types';\nimport { signCom"
},
{
"path": "api/queries/community/index.js",
"chars": 1749,
"preview": "// @flow\n\nimport community from './rootCommunity';\nimport communities from './rootCommunities';\nimport topCommunities fr"
},
{
"path": "api/queries/community/joinSettings.js",
"chars": 523,
"preview": "// @flow\nimport type { DBCommunity } from 'shared/types';\nimport type { GraphQLContext } from '../../';\nimport { canMode"
},
{
"path": "api/queries/community/memberConnection.js",
"chars": 1644,
"preview": "// @flow\n/*\n\n DEPRECATED 2/3/2018 by @brian\n\n*/\nimport type { DBCommunity } from 'shared/types';\nimport type { GraphQ"
},
{
"path": "api/queries/community/members.js",
"chars": 2069,
"preview": "// @flow\nimport type { DBCommunity } from 'shared/types';\nimport type { GraphQLContext } from '../../';\nimport type { Pa"
},
{
"path": "api/queries/community/metaData.js",
"chars": 107,
"preview": "// TODO: Flow type again\n\nexport default async () => {\n return {\n channels: 0,\n members: 0,\n };\n};\n"
},
{
"path": "api/queries/community/pinnedThread.js",
"chars": 526,
"preview": "// @flow\nimport type { GraphQLContext } from '../../';\nimport type { DBCommunity } from 'shared/types';\nimport { getThre"
},
{
"path": "api/queries/community/profilePhoto.js",
"chars": 304,
"preview": "// @flow\nimport type { GraphQLContext } from '../../';\nimport type { DBCommunity } from 'shared/types';\nimport { signCom"
},
{
"path": "api/queries/community/rootCommunities.js",
"chars": 850,
"preview": "// @flow\nimport type { GraphQLContext } from '../../';\nimport { getCuratedCommunities } from '../../models/curatedConten"
},
{
"path": "api/queries/community/rootCommunity.js",
"chars": 467,
"preview": "// @flow\nimport type { GraphQLContext } from '../../';\n\ntype GetCommunityById = {\n id: string,\n slug: void,\n};\n\ntype G"
},
{
"path": "api/queries/community/rootRecentCommunities.js",
"chars": 117,
"preview": "// @flow\nimport { getRecentCommunities } from '../../models/community';\nexport default () => getRecentCommunities();\n"
},
{
"path": "api/queries/community/rootTopCommunities.js",
"chars": 152,
"preview": "// @flow\nimport { getCuratedCommunities } from '../../models/curatedContent';\nexport default () => getCuratedCommunities"
},
{
"path": "api/queries/community/slackSettings.js",
"chars": 568,
"preview": "// @flow\nimport type { DBCommunity } from 'shared/types';\nimport type { GraphQLContext } from '../../';\nimport UserError"
},
{
"path": "api/queries/community/threadConnection.js",
"chars": 2240,
"preview": "// @flow\nimport type { DBCommunity } from 'shared/types';\nimport type { GraphQLContext } from '../../';\nimport { encode,"
},
{
"path": "api/queries/community/watercooler.js",
"chars": 579,
"preview": "// @flow\nimport type { GraphQLContext } from '../../';\nimport type { DBCommunity } from 'shared/types';\nimport { getThre"
},
{
"path": "api/queries/communityMember/index.js",
"chars": 225,
"preview": "// @flow\nimport communityMember from './rootCommunityMember';\nimport user from './user';\nimport roles from './roles';\n\nm"
},
{
"path": "api/queries/communityMember/roles.js",
"chars": 363,
"preview": "// @flow\nimport type { DBUsersCommunities } from 'shared/types';\n\nexport default ({\n isModerator,\n isOwner,\n isBlocke"
},
{
"path": "api/queries/communityMember/rootCommunityMember.js",
"chars": 301,
"preview": "// @flow\nimport type { GraphQLContext } from '../../';\n\ntype GetCommunityMemberArgs = {\n userId: string,\n communityId:"
},
{
"path": "api/queries/communityMember/user.js",
"chars": 288,
"preview": "// @flow\nimport type { DBUsersCommunities } from 'shared/types';\nimport type { GraphQLContext } from '../../';\n\nexport d"
},
{
"path": "api/queries/directMessageThread/index.js",
"chars": 460,
"preview": "// @flow\nimport directMessageThread from './rootDirectMessageThread';\nimport directMessageThreadByUserIds from './rootDi"
},
{
"path": "api/queries/directMessageThread/messageConnection.js",
"chars": 1692,
"preview": "// @flow\nimport type { PaginationOptions } from '../../utils/paginate-arrays';\nimport type { GraphQLContext } from '../."
}
]
// ... and 914 more files (download for full content)
About this extraction
This page contains the full source code of the withspectrum/spectrum GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 1114 files (3.4 MB), approximately 936.8k tokens, and a symbol index with 345 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.