├── .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.test.js │ ├── community │ │ ├── mutations │ │ │ ├── __snapshots__ │ │ │ │ └── editCommunity.test.js.snap │ │ │ └── editCommunity.test.js │ │ └── queries │ │ │ ├── __snapshots__ │ │ │ └── communitySettings.test.js.snap │ │ │ └── communitySettings.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.js │ └── utils │ │ ├── __mocks__ │ │ └── debug.js │ │ ├── __snapshots__ │ │ └── create-graphql-error-formatter.test.js.snap │ │ └── create-graphql-error-formatter.test.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 └── yarn.lock ├── backpack.config.js ├── config-overrides.js ├── cypress.json ├── 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 ├── 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 ├── img │ ├── apple-icon-114x114-precomposed.png │ ├── apple-icon-144x144-precomposed.png │ ├── apple-icon-192x192-precomposed.png │ ├── apple-icon-512x512-precomposed.png │ ├── apple-icon-57x57-precomposed.png │ ├── apple-icon-72x72-precomposed.png │ ├── badge.png │ ├── cluster-1.svg │ ├── cluster-2.svg │ ├── cluster-3.svg │ ├── cluster-4.svg │ ├── cluster-5.svg │ ├── connect.svg │ ├── constellations.svg │ ├── conversation.svg │ ├── create.svg │ ├── default_avatar.svg │ ├── default_community.svg │ ├── diagram.svg │ ├── discover.png │ ├── discover.svg │ ├── empty.svg │ ├── favicon.ico │ ├── favicon_unread.ico │ ├── fills │ │ ├── channel.svg │ │ ├── chat.svg │ │ ├── community.svg │ │ ├── error.svg │ │ ├── locked.svg │ │ ├── notification.svg │ │ ├── onboarding.svg │ │ ├── post.svg │ │ ├── pro.svg │ │ └── user.svg │ ├── head_placeholder.png │ ├── homescreen-icon-114x114.png │ ├── homescreen-icon-144x144.png │ ├── homescreen-icon-192x192.png │ ├── homescreen-icon-512x512.png │ ├── homescreen-icon-57x57.png │ ├── homescreen-icon-72x72.png │ ├── login.svg │ ├── logo-mark.png │ ├── logo.png │ ├── logos │ │ ├── abstract.svg │ │ ├── bootstrap.svg │ │ ├── expo.svg │ │ ├── figma.svg │ │ ├── invision.svg │ │ ├── nodejs.svg │ │ ├── realm.svg │ │ └── sketch.svg │ ├── mark-white.png │ ├── mark.svg │ ├── media.png │ ├── pinned-tab.svg │ ├── planet-1.svg │ ├── planet-2.svg │ ├── share.svg │ ├── slack_colored.png │ └── waterfall.png ├── 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-cache-keys.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 ├── 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 ├── upload.svg └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "env", 5 | { 6 | "targets": { 7 | "node": "current" 8 | }, 9 | "useBuiltIns": true, 10 | "exclude": [ 11 | "babel-plugin-transform-regenerator", 12 | "transform-async-to-generator" 13 | ] 14 | } 15 | ] 16 | ], 17 | "plugins": [ 18 | "babel-plugin-transform-class-properties", 19 | ["styled-components", { "ssr": true }], 20 | "transform-flow-strip-types", 21 | "transform-object-rest-spread", 22 | "babel-plugin-transform-react-jsx", 23 | "syntax-dynamic-import", 24 | "syntax-async-generators", 25 | "transform-async-generator-functions", 26 | "react-loadable/babel", 27 | "babel-plugin-inline-import-graphql-ast" 28 | ] 29 | } 30 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | .git 2 | *docker-compose* 3 | *Dockerfile* 4 | node_modules 5 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | # Note: This is a copy of the .gitignore, 2 | # with flow-typed added 3 | flow-typed 4 | node_modules 5 | .sass-cache 6 | npm-debug.log 7 | build 8 | .DS_Store 9 | src/config/FirebaseConfig.js 10 | rethinkdb_data 11 | debug 12 | now-secrets.json 13 | build-iris 14 | build-api 15 | build-hyperion 16 | package-lock.json 17 | .vscode 18 | dump.rdb 19 | *.swp 20 | queries-by-response-size.js 21 | queries-by-time.js 22 | test-extend.js 23 | stats.json 24 | iris/.env 25 | api/.env 26 | test-results.json 27 | -------------------------------------------------------------------------------- /.flowconfig: -------------------------------------------------------------------------------- 1 | [ignore] 2 | .*/build.* 3 | .*/*.test.js 4 | .*/node_modules/cypress 5 | .*/node_modules/draft-js 6 | .*/node_modules/graphql 7 | .*/node_modules/protobufjs-no-cli 8 | .*/node_modules/reqwest 9 | .*/node_modules/react-apollo 10 | .*/node_modules/dataloader 11 | /node_modules/* 12 | /email-template-scripts/* 13 | 14 | [options] 15 | suppress_comment=.*\\$FlowFixMe 16 | suppress_comment=.*\\$FlowIssue 17 | esproposal.class_instance_fields=enable 18 | module.system.node.resolve_dirname=node_modules 19 | module.system.node.resolve_dirname=. 20 | module.file_ext=.js 21 | module.file_ext=.jsx 22 | module.file_ext=.json 23 | 24 | [lints] 25 | untyped-type-import=error 26 | untyped-import=warn 27 | unclear-type=warn 28 | unsafe-getters-setters=error 29 | 30 | [version] 31 | 0.66.0 -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 12 | 13 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | **Status** 4 | 5 | - [ ] WIP 6 | - [ ] Ready for review 7 | - [ ] Needs testing 8 | 9 | **Deploy after merge (delete what needn't be deployed)** 10 | 11 | - api 12 | - hyperion (frontend) 13 | 14 | **Run database migrations (delete if no migration was added)** 15 | YES 16 | 17 | ## **Release notes for users (delete if codebase-only change)** 18 | 19 | **Related issues (delete if you don't know of any)** 20 | Closes # 21 | 22 | 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .sass-cache 3 | npm-debug.log 4 | build 5 | .DS_Store 6 | src/config/FirebaseConfig.js 7 | npm-debug.log 8 | yarn-error.log 9 | rethinkdb_data 10 | debug 11 | now-secrets.json 12 | build-iris 13 | build-api 14 | build-hyperion 15 | build-electron 16 | package-lock.json 17 | .vscode 18 | dump.rdb 19 | *.swp 20 | queries-by-response-size.js 21 | queries-by-time.js 22 | test-extend.js 23 | stats.json 24 | iris/.env 25 | api/.env 26 | test-results.json 27 | public/uploads 28 | cypress/screenshots/ 29 | cypress/videos/ 30 | cacert 31 | .env 32 | .env.* 33 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # This is used by now when deploying hyperion, replacing .gitignore 2 | # NOTE(@mxstbr): The important change is that `cacert` is NOT ignored! 3 | node_modules 4 | .sass-cache 5 | npm-debug.log 6 | build 7 | .DS_Store 8 | src/config/FirebaseConfig.js 9 | npm-debug.log 10 | yarn-error.log 11 | rethinkdb_data 12 | debug 13 | now-secrets.json 14 | build-iris 15 | build-api 16 | build-hyperion 17 | build-electron 18 | package-lock.json 19 | .vscode 20 | dump.rdb 21 | *.swp 22 | queries-by-response-size.js 23 | queries-by-time.js 24 | test-extend.js 25 | stats.json 26 | iris/.env 27 | api/.env 28 | test-results.json 29 | public/uploads 30 | cypress/screenshots/ 31 | cypress/videos/ 32 | 33 | # This is hyperion-now-specific, do not copy to .gitignore 34 | docs 35 | cypress 36 | admin 37 | .circleci 38 | .github 39 | 40 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | flow-typed 2 | package.json -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "es5" 4 | } 5 | -------------------------------------------------------------------------------- /api/loaders/channel.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { getChannels, getChannelsThreadCounts } from '../models/channel'; 3 | import { getChannelsSettings } from '../models/channelSettings'; 4 | import createLoader from './create-loader'; 5 | 6 | export const __createChannelLoader = createLoader(channels => 7 | getChannels(channels) 8 | ); 9 | 10 | export const __createChannelThreadCountLoader = createLoader( 11 | channels => getChannelsThreadCounts(channels), 12 | 'group' 13 | ); 14 | 15 | export const __createChannelSettingsLoader = createLoader( 16 | channelIds => getChannelsSettings(channelIds), 17 | key => key.channelId 18 | ); 19 | 20 | export default () => { 21 | throw new Error( 22 | '⚠️ Do not import loaders directly, get them from the GraphQL context instead! ⚠️' 23 | ); 24 | }; 25 | -------------------------------------------------------------------------------- /api/loaders/message.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { getManyMessages } from '../models/message'; 3 | import createLoader from './create-loader'; 4 | import type { Loader } from './types'; 5 | 6 | export const __createMessageLoader = createLoader((messages: string[]) => 7 | getManyMessages(messages) 8 | ); 9 | 10 | export default () => { 11 | throw new Error( 12 | '⚠️ Do not import loaders directly, get them from the GraphQL context instead! ⚠️' 13 | ); 14 | }; 15 | -------------------------------------------------------------------------------- /api/loaders/reaction.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { getReactions, getReactionsByIds } from '../models/reaction'; 3 | import createLoader from './create-loader'; 4 | 5 | export const __createReactionLoader = createLoader( 6 | messageIds => getReactions(messageIds), 7 | 'group' 8 | ); 9 | 10 | export const __createSingleReactionLoader = createLoader(reactionIds => 11 | getReactionsByIds(reactionIds) 12 | ); 13 | 14 | export default () => { 15 | throw new Error( 16 | '⚠️ Do not import loaders directly, get them from the GraphQL context instead! ⚠️' 17 | ); 18 | }; 19 | -------------------------------------------------------------------------------- /api/loaders/thread.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { getThreads } from '../models/thread'; 3 | import { getParticipantsInThreads } from '../models/usersThreads'; 4 | import createLoader from './create-loader'; 5 | 6 | export const __createThreadLoader = createLoader(threads => 7 | getThreads(threads) 8 | ); 9 | 10 | export const __createThreadParticipantsLoader = createLoader( 11 | threadIds => getParticipantsInThreads(threadIds), 12 | 'group' 13 | ); 14 | 15 | export default () => { 16 | throw new Error( 17 | '⚠️ Do not import loaders directly, get them from the GraphQL context instead! ⚠️' 18 | ); 19 | }; 20 | -------------------------------------------------------------------------------- /api/loaders/threadReaction.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { getThreadReactions } from '../models/threadReaction'; 3 | import createLoader from './create-loader'; 4 | 5 | export const __createThreadReactionLoader = createLoader( 6 | threadIds => getThreadReactions(threadIds), 7 | 'group' 8 | ); 9 | 10 | export default () => { 11 | throw new Error( 12 | '⚠️ Do not import loaders directly, get them from the GraphQL context instead! ⚠️' 13 | ); 14 | }; 15 | -------------------------------------------------------------------------------- /api/loaders/types.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | export type Loader = { 4 | load: (key: string | Array) => Promise, 5 | loadMany: (keys: Array<*>) => Promise, 6 | clear: (key: string | Array) => void, 7 | }; 8 | 9 | export type DataLoaderOptions = { 10 | cache?: boolean, 11 | }; 12 | -------------------------------------------------------------------------------- /api/migrations/20170706114239-providerfield-indexes.js: -------------------------------------------------------------------------------- 1 | exports.up = function(r, conn) { 2 | return Promise.all([ 3 | r 4 | .table('users') 5 | .indexCreate('providerId') 6 | .run(conn), 7 | r 8 | .table('users') 9 | .indexCreate('fbProviderId') 10 | .run(conn), 11 | r 12 | .table('users') 13 | .indexCreate('googleProviderId') 14 | .run(conn), 15 | ]).catch(err => { 16 | console.log(err); 17 | throw err; 18 | }); 19 | }; 20 | 21 | exports.down = function(r, conn) { 22 | return Promise.resolve(); 23 | }; 24 | -------------------------------------------------------------------------------- /api/migrations/20170706205658-slack-import.js: -------------------------------------------------------------------------------- 1 | exports.up = function(r, conn) { 2 | return Promise.all([ 3 | r 4 | .tableCreate('slackImports') 5 | .run(conn) 6 | .catch(err => { 7 | console.log(err); 8 | throw err; 9 | }), 10 | ]) 11 | .then(() => 12 | Promise.all([ 13 | r 14 | .table('slackImports') 15 | .indexCreate('communityId', r.row('communityId')) 16 | .run(conn) 17 | .catch(err => { 18 | console.log(err); 19 | throw err; 20 | }), 21 | r 22 | .table('users') 23 | .indexCreate('email', r.row('email')) 24 | .run(conn), 25 | ]) 26 | ) 27 | .catch(err => { 28 | console.log(err); 29 | throw err; 30 | }); 31 | }; 32 | 33 | exports.down = function(r, conn) { 34 | return Promise.all([r.tableDrop('slackImports').run(conn)]); 35 | }; 36 | -------------------------------------------------------------------------------- /api/migrations/20170714171920-web-push-subscription.js: -------------------------------------------------------------------------------- 1 | exports.up = function(r, conn) { 2 | return r 3 | .tableCreate('webPushSubscriptions') 4 | .run(conn) 5 | .catch(err => { 6 | throw new Error(err); 7 | }) 8 | .then(() => 9 | r 10 | .table('webPushSubscriptions') 11 | .indexCreate('userId') 12 | .run(conn) 13 | ) 14 | .then(() => 15 | r 16 | .table('webPushSubscriptions') 17 | .indexCreate('endpoint') 18 | .run(conn) 19 | ); 20 | }; 21 | 22 | exports.down = function(r, conn) { 23 | return r 24 | .tableDrop('webPushSubscriptions') 25 | .run(conn) 26 | .catch(err => { 27 | throw new Error(err); 28 | }); 29 | }; 30 | -------------------------------------------------------------------------------- /api/migrations/20170724184557-notifications-entity-added-index.js: -------------------------------------------------------------------------------- 1 | exports.up = function(r, conn) { 2 | return Promise.all([ 3 | r 4 | .table('usersNotifications') 5 | .indexCreate('userIdAndEntityAddedAt', [ 6 | r.row('userId'), 7 | r.row('entityAddedAt'), 8 | ]) 9 | .run(conn), 10 | ]).catch(err => { 11 | console.log(err); 12 | throw err; 13 | }); 14 | }; 15 | 16 | exports.down = function(r, conn) { 17 | return Promise.resolve(); 18 | }; 19 | -------------------------------------------------------------------------------- /api/migrations/20170829233734-userid-index-on-invoices.js: -------------------------------------------------------------------------------- 1 | exports.up = function(r, conn) { 2 | return Promise.all([ 3 | r 4 | .table('invoices') 5 | .indexCreate('userId') 6 | .run(conn), 7 | ]).catch(err => { 8 | console.log(err); 9 | throw err; 10 | }); 11 | }; 12 | 13 | exports.down = function(r, conn) { 14 | return Promise.resolve(); 15 | }; 16 | -------------------------------------------------------------------------------- /api/migrations/20170915201609-clean-up-bad-dm-data.js: -------------------------------------------------------------------------------- 1 | exports.up = function(r, conn) { 2 | return Promise.all([ 3 | r 4 | .table('messages') 5 | .filter({ threadType: 'DIRECT_MESSAGE_GROUP' }) 6 | .update({ 7 | threadType: 'directMessageThread', 8 | }) 9 | .run(conn), 10 | r 11 | .table('messages') 12 | .filter({ threadType: 'STORY' }) 13 | .update({ 14 | threadType: 'story', 15 | }) 16 | .run(conn), 17 | ]); 18 | }; 19 | 20 | exports.down = function(r, conn) { 21 | return Promise.resolve(); 22 | }; 23 | -------------------------------------------------------------------------------- /api/migrations/20170926003025-activate-daily-weekly-digest-settings.js: -------------------------------------------------------------------------------- 1 | exports.up = function(r, conn) { 2 | return Promise.all([ 3 | r 4 | .table('usersSettings') 5 | .update({ 6 | notifications: { 7 | types: { 8 | dailyDigest: { 9 | email: true, 10 | }, 11 | weeklyDigest: { 12 | email: true, 13 | }, 14 | }, 15 | }, 16 | }) 17 | .run(conn), 18 | ]); 19 | }; 20 | 21 | exports.down = function(r, conn) { 22 | return Promise.resolve(); 23 | }; 24 | -------------------------------------------------------------------------------- /api/migrations/20170926102527-speedy-gonzales.js: -------------------------------------------------------------------------------- 1 | exports.up = function(r, conn) { 2 | return Promise.all([ 3 | r 4 | .table('usersCommunities') 5 | .indexCreate('userIdAndCommunityId', [ 6 | r.row('userId'), 7 | r.row('communityId'), 8 | ]) 9 | .run(conn), 10 | ]); 11 | }; 12 | 13 | exports.down = function(r, conn) { 14 | return Promise.resolve(); 15 | }; 16 | -------------------------------------------------------------------------------- /api/migrations/20170927002438-communityid-index-on-reputation-events.js: -------------------------------------------------------------------------------- 1 | exports.up = function(r, conn) { 2 | return Promise.all([ 3 | r 4 | .table('reputationEvents') 5 | .indexCreate('communityId') 6 | .run(conn), 7 | ]); 8 | }; 9 | 10 | exports.down = function(r, conn) { 11 | return Promise.resolve(); 12 | }; 13 | -------------------------------------------------------------------------------- /api/migrations/20171018235659-add-direst-message-user-settings.js: -------------------------------------------------------------------------------- 1 | exports.up = function(r, conn) { 2 | return Promise.all([ 3 | r 4 | .table('usersSettings') 5 | .update({ 6 | notifications: { 7 | types: { 8 | newDirectMessage: { 9 | email: true, 10 | }, 11 | }, 12 | }, 13 | }) 14 | .run(conn), 15 | ]).catch(err => { 16 | console.log(err); 17 | throw err; 18 | }); 19 | }; 20 | 21 | exports.down = function(r, conn) { 22 | return Promise.resolve(); 23 | }; 24 | -------------------------------------------------------------------------------- /api/migrations/20171029090619-users-channels-index.js: -------------------------------------------------------------------------------- 1 | exports.up = function(r, conn) { 2 | return r 3 | .table('usersChannels') 4 | .indexCreate('userIdAndChannelId', [r.row('userId'), r.row('channelId')]) 5 | .run(conn); 6 | }; 7 | 8 | exports.down = function(r, conn) { 9 | return r 10 | .table('usersChannels') 11 | .indexDrop('userIdAndChannelId') 12 | .run(conn); 13 | }; 14 | -------------------------------------------------------------------------------- /api/migrations/20171029094352-users-threads-index.js: -------------------------------------------------------------------------------- 1 | exports.up = function(r, conn) { 2 | return r 3 | .table('usersThreads') 4 | .indexCreate('userIdAndThreadId', [r.row('userId'), r.row('threadId')]) 5 | .run(conn); 6 | }; 7 | 8 | exports.down = function(r, conn) { 9 | return r 10 | .table('usersThreads') 11 | .indexDrop('userIdAndThreadId') 12 | .run(conn); 13 | }; 14 | -------------------------------------------------------------------------------- /api/migrations/20171103014955-add-mention-notification-settings.js: -------------------------------------------------------------------------------- 1 | exports.up = function(r, conn) { 2 | return Promise.all([ 3 | r 4 | .table('usersSettings') 5 | .update({ 6 | notifications: { 7 | types: { 8 | newMention: { 9 | email: true, 10 | }, 11 | }, 12 | }, 13 | }) 14 | .run(conn), 15 | ]); 16 | }; 17 | 18 | exports.down = function(r, conn) { 19 | return Promise.resolve(); 20 | }; 21 | -------------------------------------------------------------------------------- /api/migrations/20171129215512-index-communities-by-slug.js: -------------------------------------------------------------------------------- 1 | exports.up = function(r, conn) { 2 | return Promise.all([ 3 | r 4 | .table('communities') 5 | .indexCreate('slug') 6 | .run(conn), 7 | ]); 8 | }; 9 | 10 | exports.down = function(r, conn) { 11 | return Promise.resolve(); 12 | }; 13 | -------------------------------------------------------------------------------- /api/migrations/20180209015734-github-provider-id-index.js: -------------------------------------------------------------------------------- 1 | exports.up = function(r, conn) { 2 | return Promise.all([ 3 | r 4 | .table('users') 5 | .indexCreate('githubProviderId') 6 | .run(conn), 7 | ]).catch(err => { 8 | console.log(err); 9 | throw err; 10 | }); 11 | }; 12 | 13 | exports.down = function(r, conn) { 14 | return Promise.resolve(); 15 | }; 16 | -------------------------------------------------------------------------------- /api/migrations/20180214111357-expo-push-subscriptions.js: -------------------------------------------------------------------------------- 1 | exports.up = function(r, conn) { 2 | return r 3 | .tableCreate('expoPushSubscriptions') 4 | .run(conn) 5 | .then(() => 6 | r 7 | .table('expoPushSubscriptions') 8 | .indexCreate('userId') 9 | .run(conn) 10 | ) 11 | .then(() => 12 | r 13 | .table('expoPushSubscriptions') 14 | .indexCreate('token') 15 | .run(conn) 16 | ) 17 | .catch(err => { 18 | throw new Error(err); 19 | }); 20 | }; 21 | 22 | exports.down = function(r, conn) { 23 | return r 24 | .tableDrop('expoPushSubscriptions') 25 | .run(conn) 26 | .catch(err => { 27 | throw new Error(err); 28 | }); 29 | }; 30 | -------------------------------------------------------------------------------- /api/migrations/20180309144845-create-community-settings-table.js: -------------------------------------------------------------------------------- 1 | exports.up = function(r, conn) { 2 | return r 3 | .tableCreate('communitySettings') 4 | .run(conn) 5 | .then(() => 6 | r 7 | .table('communitySettings') 8 | .indexCreate('communityId', r.row('communityId')) 9 | .run(conn) 10 | ) 11 | .catch(err => { 12 | console.log(err); 13 | throw err; 14 | }); 15 | }; 16 | 17 | exports.down = function(r, conn) { 18 | return Promise.all([r.tableDrop('communitySettings').run(conn)]); 19 | }; 20 | -------------------------------------------------------------------------------- /api/migrations/20180316195507-create-channel-settings-table.js: -------------------------------------------------------------------------------- 1 | exports.up = function(r, conn) { 2 | return r 3 | .tableCreate('channelSettings') 4 | .run(conn) 5 | .then(() => 6 | r 7 | .table('channelSettings') 8 | .indexCreate('channelId', r.row('channelId')) 9 | .run(conn) 10 | ) 11 | .catch(err => { 12 | console.error(err); 13 | throw err; 14 | }); 15 | }; 16 | 17 | exports.down = function(r, conn) { 18 | return Promise.all([r.tableDrop('channelSettings').run(conn)]); 19 | }; 20 | -------------------------------------------------------------------------------- /api/migrations/20180320122000-create-stripe-tables.js: -------------------------------------------------------------------------------- 1 | exports.up = function(r, conn) { 2 | const createCustomersTable = () => 3 | r.tableCreate('stripeCustomers', { primaryKey: 'customerId' }).run(conn); 4 | const createInvoicesTable = () => 5 | r.tableCreate('stripeInvoices', { primaryKey: 'invoiceId' }).run(conn); 6 | 7 | return Promise.all([createCustomersTable(), createInvoicesTable()]) 8 | .then(() => 9 | Promise.all([ 10 | r 11 | .table('stripeInvoices') 12 | .indexCreate('customerId') 13 | .run(conn), 14 | ]) 15 | ) 16 | .catch(err => console.log(err)); 17 | }; 18 | 19 | exports.down = function(r, conn) { 20 | return Promise.all([ 21 | r.tableDrop('stripeCustomers').run(conn), 22 | r.tableDrop('stripeInvoices').run(conn), 23 | ]); 24 | }; 25 | -------------------------------------------------------------------------------- /api/migrations/20180411183454-lowercase-all-the-slugs.js: -------------------------------------------------------------------------------- 1 | exports.up = async (r, conn) => { 2 | return Promise.all([ 3 | r 4 | .table('users') 5 | .update({ 6 | username: r.row('username').downcase(), 7 | email: r.row('email').downcase(), 8 | }) 9 | .run(conn), 10 | r 11 | .table('communities') 12 | .update({ 13 | slug: r.row('slug').downcase(), 14 | }) 15 | .run(conn), 16 | r 17 | .table('channels') 18 | .update({ 19 | slug: r.row('slug').downcase(), 20 | }) 21 | .run(conn), 22 | ]); 23 | }; 24 | 25 | exports.down = function(r, conn) { 26 | return Promise.resolve(); 27 | }; 28 | -------------------------------------------------------------------------------- /api/migrations/20180428001543-reset-slack-import-records.js: -------------------------------------------------------------------------------- 1 | exports.up = function(r, conn) { 2 | return Promise.all([ 3 | r 4 | .table('slackImports') 5 | .update({ 6 | members: r.literal(), 7 | }) 8 | .run(conn), 9 | ]).catch(err => console.error(err)); 10 | }; 11 | 12 | exports.down = function(r, conn) { 13 | return Promise.resolve(); 14 | }; 15 | -------------------------------------------------------------------------------- /api/migrations/20180517180716-enable-private-communities.js: -------------------------------------------------------------------------------- 1 | exports.up = async (r, conn) => { 2 | return r 3 | .table('communities') 4 | .update({ 5 | isPrivate: false, 6 | }) 7 | .run(conn); 8 | }; 9 | 10 | exports.down = function(r, conn) { 11 | return r 12 | .table('communities') 13 | .update({ 14 | isPrivate: r.literal(), 15 | }) 16 | .run(conn); 17 | }; 18 | -------------------------------------------------------------------------------- /api/migrations/20180517215503-add-ispending-to-userscommunities.js: -------------------------------------------------------------------------------- 1 | exports.up = async (r, conn) => { 2 | return r 3 | .table('usersCommunities') 4 | .update({ 5 | isPending: false, 6 | }) 7 | .run(conn); 8 | }; 9 | 10 | exports.down = function(r, conn) { 11 | return r 12 | .table('usersCommunities') 13 | .update({ 14 | isPending: r.literal(), 15 | }) 16 | .run(conn); 17 | }; 18 | -------------------------------------------------------------------------------- /api/migrations/20180518135040-add-join-settings-to-community-settings.js: -------------------------------------------------------------------------------- 1 | exports.up = async (r, conn) => { 2 | return r 3 | .table('communitySettings') 4 | .update({ 5 | joinSettings: { 6 | tokenJoinEnabled: false, 7 | token: null, 8 | }, 9 | }) 10 | .run(conn); 11 | }; 12 | 13 | exports.down = function(r, conn) { 14 | return r 15 | .table('communitySettings') 16 | .update({ 17 | joinSettings: r.literal(), 18 | }) 19 | .run(conn); 20 | }; 21 | -------------------------------------------------------------------------------- /api/migrations/20180621001409-thread-likes-table.js: -------------------------------------------------------------------------------- 1 | exports.up = function(r, conn) { 2 | return r 3 | .tableCreate('threadReactions') 4 | .run(conn) 5 | .then(() => { 6 | return Promise.all([ 7 | r 8 | .table('threadReactions') 9 | .indexCreate('threadId') 10 | .run(conn), 11 | r 12 | .table('threadReactions') 13 | .indexCreate('userId') 14 | .run(conn), 15 | ]); 16 | }) 17 | .catch(err => console.error(err)); 18 | }; 19 | 20 | exports.down = function(r, conn) { 21 | return Promise.all([r.tableDrop('threadReactions').run(conn)]); 22 | }; 23 | -------------------------------------------------------------------------------- /api/migrations/20181001064151-fix-thread-metadata-message-counts.js: -------------------------------------------------------------------------------- 1 | exports.up = function(r, conn) { 2 | return r 3 | .table('threads') 4 | .update( 5 | { 6 | messageCount: r 7 | .table('messages') 8 | .getAll(r.row('id'), { index: 'threadId' }) 9 | .filter(row => r.not(row.hasFields('deletedAt'))) 10 | .count() 11 | .default(0), 12 | reactionCount: r 13 | .table('threadReactions') 14 | .getAll(r.row('id'), { index: 'threadId' }) 15 | .filter(row => r.not(row.hasFields('deletedAt'))) 16 | .count() 17 | .default(0), 18 | }, 19 | { 20 | nonAtomic: true, 21 | } 22 | ) 23 | .run(conn) 24 | .catch(err => console.error(err)); 25 | }; 26 | 27 | exports.down = function(r, conn) { 28 | return Promise.resolve(); 29 | }; 30 | -------------------------------------------------------------------------------- /api/migrations/20181003233411-thread-reactions-useridandthreadid-index.js: -------------------------------------------------------------------------------- 1 | exports.up = function(r, conn) { 2 | return r 3 | .table('threadReactions') 4 | .indexCreate('userIdAndThreadId', [r.row('userId'), r.row('threadId')]) 5 | .run(conn); 6 | }; 7 | 8 | exports.down = function(r, conn) { 9 | return r 10 | .table('threadReactions') 11 | .indexDrop('userIdAndThreadId') 12 | .run(conn); 13 | }; 14 | -------------------------------------------------------------------------------- /api/migrations/20181005143053-users-notifications-useridandnotificationid-index.js: -------------------------------------------------------------------------------- 1 | exports.up = function(r, conn) { 2 | return r 3 | .table('usersNotifications') 4 | .indexCreate('userIdAndNotificationId', [ 5 | r.row('userId'), 6 | r.row('notificationId'), 7 | ]) 8 | .run(conn); 9 | }; 10 | 11 | exports.down = function(r, conn) { 12 | return r 13 | .table('usersNotifications') 14 | .indexDrop('userIdAndNotificationId') 15 | .run(conn); 16 | }; 17 | -------------------------------------------------------------------------------- /api/migrations/20181005144259-users-notifications-userIdAndIsSeen-index.js: -------------------------------------------------------------------------------- 1 | exports.up = function(r, conn) { 2 | return r 3 | .table('usersNotifications') 4 | .indexCreate('userIdAndIsSeen', [r.row('userId'), r.row('isSeen')]) 5 | .run(conn); 6 | }; 7 | 8 | exports.down = function(r, conn) { 9 | return r 10 | .table('usersNotifications') 11 | .indexDrop('userIdAndIsSeen') 12 | .run(conn); 13 | }; 14 | -------------------------------------------------------------------------------- /api/migrations/20181024173616-indexes-for-digests.js: -------------------------------------------------------------------------------- 1 | exports.up = function(r, conn) { 2 | return Promise.all([ 3 | r 4 | .table('usersSettings') 5 | .indexCreate( 6 | 'weeklyDigestEmail', 7 | r.row('notifications')('types')('weeklyDigest')('email') 8 | ) 9 | .run(conn), 10 | r 11 | .table('usersSettings') 12 | .indexCreate( 13 | 'dailyDigestEmail', 14 | r.row('notifications')('types')('dailyDigest')('email') 15 | ) 16 | .run(conn), 17 | ]); 18 | }; 19 | 20 | exports.down = function(r, conn) { 21 | return Promise.all([ 22 | r 23 | .table('usersSettings') 24 | .indexDrop('weeklyDigestEmail') 25 | .run(conn), 26 | r 27 | .table('usersSettings') 28 | .indexDrop('dailyDigestEmail') 29 | .run(conn), 30 | ]); 31 | }; 32 | -------------------------------------------------------------------------------- /api/migrations/20181027050052-remove-attachments-from-thread-model.js: -------------------------------------------------------------------------------- 1 | exports.up = function(r, conn) { 2 | return Promise.all([ 3 | r 4 | .table('threads') 5 | .update({ 6 | attachments: r.literal(), 7 | }) 8 | .run(conn), 9 | ]).catch(err => console.error(err)); 10 | }; 11 | exports.down = function(r, conn) { 12 | return Promise.resolve(); 13 | }; 14 | -------------------------------------------------------------------------------- /api/migrations/20181116173949-add-terms-last-accepted-field-to-users.js: -------------------------------------------------------------------------------- 1 | exports.up = async (r, conn) => { 2 | return r 3 | .table('users') 4 | .update({ 5 | termsLastAcceptedAt: r.row('createdAt'), 6 | }) 7 | .run(conn); 8 | }; 9 | 10 | exports.down = function(r, conn) { 11 | return r 12 | .table('users') 13 | .update({ 14 | termsLastAcceptedAt: r.literal(), 15 | }) 16 | .run(conn); 17 | }; 18 | -------------------------------------------------------------------------------- /api/migrations/20181122162921-users-communities-useridandmember-index.js: -------------------------------------------------------------------------------- 1 | exports.up = function(r, conn) { 2 | return r 3 | .table('usersCommunities') 4 | .indexCreate('userIdAndIsMember', [r.row('userId'), r.row('isMember')]) 5 | .run(conn); 6 | }; 7 | 8 | exports.down = function(r, conn) { 9 | return r 10 | .table('usersCommunities') 11 | .indexDrop('userIdAndIsMember') 12 | .run(conn); 13 | }; 14 | -------------------------------------------------------------------------------- /api/migrations/20181127090014-communities-member-count-index.js: -------------------------------------------------------------------------------- 1 | exports.up = function(r, conn) { 2 | return r 3 | .table('communities') 4 | .indexCreate('memberCount') 5 | .run(conn); 6 | }; 7 | 8 | exports.down = function(r, conn) { 9 | return r 10 | .table('communities') 11 | .indexDrop('memberCount') 12 | .run(conn); 13 | }; 14 | -------------------------------------------------------------------------------- /api/migrations/20181211181146-add-usersthreads-user-id-and-participant-index.js: -------------------------------------------------------------------------------- 1 | exports.up = function(r, conn) { 2 | return Promise.all([ 3 | r 4 | .table('usersThreads') 5 | .indexCreate('userIdAndIsParticipant', [ 6 | r.row('userId'), 7 | r.row('isParticipant'), 8 | ]) 9 | .run(conn), 10 | ]); 11 | }; 12 | exports.down = function(r, conn) { 13 | return Promise.all([ 14 | r 15 | .table('usersThreads') 16 | .indexDrop('userIdAndIsParticipant') 17 | .run(conn), 18 | ]); 19 | }; 20 | -------------------------------------------------------------------------------- /api/migrations/20190226085909-bot-user-sam.js: -------------------------------------------------------------------------------- 1 | exports.up = function(r, conn) { 2 | return r 3 | .table('users') 4 | .insert({ 5 | id: 'sam', 6 | description: "Spectrum's automated bot.", 7 | createdAt: new Date(), 8 | email: null, 9 | providerId: null, 10 | fbProviderId: null, 11 | githubProviderId: null, 12 | githubUsername: 'withspectrum', 13 | googleProviderId: null, 14 | isOnline: true, 15 | lastSeen: new Date(), 16 | modifiedAt: new Date(), 17 | name: 'Spectrum Bot', 18 | termsLastAcceptedAt: new Date(), 19 | username: 'spectrumbot', 20 | website: 'https://spectrum.chat', 21 | profilePhoto: '/default_images/sam.png', 22 | }) 23 | .run(conn); 24 | }; 25 | exports.down = function(r, conn) { 26 | return r 27 | .table('users') 28 | .get('sam') 29 | .delete() 30 | .run(conn); 31 | }; 32 | -------------------------------------------------------------------------------- /api/migrations/20190306125252-threads-watercooler-index.js: -------------------------------------------------------------------------------- 1 | exports.up = function(r, conn) { 2 | return r 3 | .table('threads') 4 | .indexCreate('communityIdAndWatercooler', [ 5 | r.row('communityId'), 6 | r.row('watercooler'), 7 | ]) 8 | .run(conn); 9 | }; 10 | 11 | exports.down = function(r, conn) { 12 | return r 13 | .table('threads') 14 | .indexDrop('communityIdAndWatercooler') 15 | .run(conn); 16 | }; 17 | -------------------------------------------------------------------------------- /api/migrations/20190327134509-delete-bot-messages.js: -------------------------------------------------------------------------------- 1 | exports.up = function(r, conn) { 2 | return r 3 | .table('messages') 4 | .filter({ bot: true }) 5 | .delete() 6 | .run(conn); 7 | }; 8 | 9 | exports.down = function(r, conn) { 10 | return Promise.resolve(); 11 | }; 12 | -------------------------------------------------------------------------------- /api/migrations/seed/default/channelSettings.js: -------------------------------------------------------------------------------- 1 | const constants = require('./constants'); 2 | const { PAYMENTS_PRIVATE_CHANNEL_ID } = constants; 3 | 4 | module.exports = [ 5 | { 6 | id: 1, 7 | channelId: PAYMENTS_PRIVATE_CHANNEL_ID, 8 | joinSettings: { 9 | tokenJoinEnabled: true, 10 | token: 'abc', 11 | }, 12 | slackSettings: { 13 | botLinks: { 14 | threadCreated: null, 15 | }, 16 | }, 17 | }, 18 | ]; 19 | -------------------------------------------------------------------------------- /api/migrations/seed/default/directMessageThreads.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | const constants = require('./constants'); 3 | const { DATE } = constants; 4 | 5 | module.exports = [ 6 | { 7 | id: 'dm-1', 8 | createdAt: new Date(DATE), 9 | name: null, 10 | threadLastActive: new Date(DATE), 11 | }, 12 | { 13 | id: 'dm-2', 14 | createdAt: new Date(DATE - 1), 15 | name: null, 16 | threadLastActive: new Date(DATE - 1), 17 | }, 18 | ]; 19 | -------------------------------------------------------------------------------- /api/migrations/seed/default/notifications.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | const constants = require('./constants'); 3 | const users = require('./users'); 4 | 5 | const { DATE, BRIAN_ID, PREVIOUS_MEMBER_USER_ID } = constants; 6 | 7 | module.exports = [ 8 | { 9 | actors: [ 10 | { 11 | id: PREVIOUS_MEMBER_USER_ID, 12 | payload: JSON.stringify( 13 | users.find(u => u.id === PREVIOUS_MEMBER_USER_ID) 14 | ), 15 | type: 'USER', 16 | }, 17 | ], 18 | context: { 19 | id: 'dm-2', 20 | payload: '', 21 | type: 'DIRECT_MESSAGE_THREAD', 22 | }, 23 | createdAt: new Date(DATE + 1), 24 | entities: [ 25 | { 26 | id: '1', 27 | payload: '', 28 | type: 'MESSAGE', 29 | }, 30 | ], 31 | event: 'MESSAGE_CREATED', 32 | id: '1', 33 | modifiedAt: new Date(DATE + 1), 34 | }, 35 | ]; 36 | -------------------------------------------------------------------------------- /api/migrations/seed/default/reactions.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | const constants = require('./constants'); 3 | const { DATE, MAX_ID } = constants; 4 | 5 | module.exports = [ 6 | { 7 | id: '1', 8 | messageId: '4', 9 | type: 'like', 10 | senderId: MAX_ID, 11 | timestamp: new Date(DATE + 4), 12 | }, 13 | ]; 14 | -------------------------------------------------------------------------------- /api/migrations/seed/default/usersNotifications.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | const constants = require('./constants'); 3 | const { DATE, BRIAN_ID } = constants; 4 | 5 | module.exports = [ 6 | { 7 | id: '1', 8 | notificationId: '1', 9 | createdAt: new Date(DATE), 10 | userId: BRIAN_ID, 11 | entityAddedAt: new Date(DATE + 1), 12 | isRead: false, 13 | isSeen: false, 14 | }, 15 | ]; 16 | -------------------------------------------------------------------------------- /api/migrations/seed/default/usersSettings.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | module.exports = () => { 4 | let settings = []; 5 | for (let step = 0; step < 11; step++) { 6 | settings.push({ 7 | userId: step.toString(), 8 | notifications: { 9 | types: { 10 | newMessageInThreads: { 11 | email: true, 12 | }, 13 | newMention: { 14 | email: true, 15 | }, 16 | newDirectMessage: { 17 | email: true, 18 | }, 19 | newThreadCreated: { 20 | email: true, 21 | }, 22 | dailyDigest: { 23 | email: true, 24 | }, 25 | weeklyDigest: { 26 | email: true, 27 | }, 28 | }, 29 | }, 30 | }); 31 | } 32 | return settings; 33 | }; 34 | -------------------------------------------------------------------------------- /api/models/curatedContent.js: -------------------------------------------------------------------------------- 1 | //@flow 2 | const { db } = require('shared/db'); 3 | import type { DBCommunity } from 'shared/types'; 4 | import { getCommunitiesBySlug } from './community'; 5 | 6 | // prettier-ignore 7 | export const getCuratedCommunities = (type: string): Promise> => { 8 | return db 9 | .table('curatedContent') 10 | .filter({ type }) 11 | .run() 12 | .then(results => (results && results.length > 0 ? results[0] : null)) 13 | .then(result => result && getCommunitiesBySlug(result.data)); 14 | }; 15 | -------------------------------------------------------------------------------- /api/models/session.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { db } from 'shared/db'; 3 | 4 | export const destroySession = (id: string) => { 5 | return db 6 | .table('sessions') 7 | .get(id) 8 | .delete() 9 | .run(); 10 | }; 11 | -------------------------------------------------------------------------------- /api/models/threadReaction.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { db } from 'shared/db'; 3 | import type { DBThreadReaction } from 'shared/types'; 4 | 5 | // prettier-ignore 6 | export const getThreadReactions = (threadIds: Array): Promise> => { 7 | const distinctMessageIds = threadIds.filter((x, i, a) => a.indexOf(x) == i); 8 | return db 9 | .table('threadReactions') 10 | .getAll(...distinctMessageIds, { index: 'threadId' }) 11 | .filter(row => row.hasFields('deletedAt').not()) 12 | .group('threadId') 13 | .run(); 14 | }; 15 | 16 | export const hasReactedToThread = ( 17 | userId: string, 18 | threadId: string 19 | ): Promise => { 20 | return db 21 | .table('threadReactions') 22 | .getAll([userId, threadId], { index: 'userIdAndThreadId' }) 23 | .filter(row => row.hasFields('deletedAt').not()) 24 | .count() 25 | .eq(1) 26 | .run(); 27 | }; 28 | -------------------------------------------------------------------------------- /api/mutations/channel/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import deleteChannel from './deleteChannel'; 3 | import editChannel from './editChannel'; 4 | 5 | module.exports = { 6 | Mutation: { 7 | deleteChannel, 8 | editChannel, 9 | }, 10 | }; 11 | -------------------------------------------------------------------------------- /api/mutations/community/editCommunity.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type { GraphQLContext } from '../../'; 3 | import type { EditCommunityInput } from '../../models/community'; 4 | import UserError from '../../utils/UserError'; 5 | import { editCommunity } from '../../models/community'; 6 | import { 7 | isAuthedResolver as requireAuth, 8 | canModerateCommunity, 9 | } from '../../utils/permissions'; 10 | 11 | export default requireAuth( 12 | // prettier-ignore 13 | async (_: any, args: EditCommunityInput, ctx: GraphQLContext) => { 14 | const { user, loaders} = ctx 15 | const { communityId } = args.input 16 | 17 | // user must own the community to edit the community 18 | if (!await canModerateCommunity(user.id, communityId, loaders)) { 19 | return new UserError("You don't have permission to edit this community."); 20 | } 21 | 22 | return editCommunity(args, user.id); 23 | } 24 | ); 25 | -------------------------------------------------------------------------------- /api/mutations/community/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import deleteCommunity from './deleteCommunity'; 3 | import editCommunity from './editCommunity'; 4 | import toggleCommunityRedirect from './toggleCommunityRedirect'; 5 | import toggleCommunityNoindex from './toggleCommunityNoindex.js'; 6 | 7 | module.exports = { 8 | Mutation: { 9 | deleteCommunity, 10 | editCommunity, 11 | toggleCommunityRedirect, 12 | toggleCommunityNoindex, 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /api/mutations/files/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import uploadImage from './uploadImage'; 3 | 4 | module.exports = { 5 | Mutation: { 6 | uploadImage, 7 | }, 8 | }; 9 | -------------------------------------------------------------------------------- /api/mutations/files/uploadImage.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { isAuthedResolver } from '../../utils/permissions'; 3 | import { uploadImage } from '../../utils/file-storage'; 4 | import type { EntityTypes } from 'shared/types'; 5 | import type { FileUpload } from 'shared/types'; 6 | import { signImageUrl } from 'shared/imgix'; 7 | 8 | type Args = { 9 | input: { 10 | image: FileUpload, 11 | type: EntityTypes, 12 | id?: string, 13 | }, 14 | }; 15 | 16 | export default isAuthedResolver(async (_: void, { input }: Args) => { 17 | const { image, type, id } = input; 18 | const url = await uploadImage(image, type, id || 'draft'); 19 | return await signImageUrl(url); 20 | }); 21 | -------------------------------------------------------------------------------- /api/mutations/message/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import deleteMessage from './deleteMessage'; 3 | 4 | module.exports = { 5 | Mutation: { 6 | deleteMessage, 7 | }, 8 | }; 9 | -------------------------------------------------------------------------------- /api/mutations/thread/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import deleteThread from './deleteThread'; 3 | 4 | module.exports = { 5 | Mutation: { 6 | deleteThread, 7 | }, 8 | }; 9 | -------------------------------------------------------------------------------- /api/mutations/user/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import editUser from './editUser'; 3 | import deleteCurrentUser from './deleteCurrentUser'; 4 | import banUser from './banUser'; 5 | 6 | module.exports = { 7 | Mutation: { 8 | editUser, 9 | deleteCurrentUser, 10 | banUser, 11 | }, 12 | }; 13 | -------------------------------------------------------------------------------- /api/queries/channel/channelPermissions.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type { GraphQLContext } from '../../'; 3 | import type { DBChannel } from 'shared/types'; 4 | 5 | export default async ( 6 | root: DBChannel, 7 | _: any, 8 | { user, loaders }: GraphQLContext 9 | ) => { 10 | const channelId = root.id; 11 | const defaultPermissions = { 12 | isOwner: false, 13 | isMember: false, 14 | isModerator: false, 15 | isBlocked: false, 16 | isPending: false, 17 | receiveNotifications: false, 18 | }; 19 | 20 | if (!channelId || !user) { 21 | return defaultPermissions; 22 | } 23 | 24 | const permissions = await loaders.userPermissionsInChannel.load([ 25 | user.id, 26 | channelId, 27 | ]); 28 | 29 | return permissions || defaultPermissions; 30 | }; 31 | -------------------------------------------------------------------------------- /api/queries/channel/community.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type { GraphQLContext } from '../../'; 3 | import type { DBChannel } from 'shared/types'; 4 | import { canViewCommunity } from '../../utils/permissions'; 5 | 6 | export default async (channel: DBChannel, _: any, ctx: GraphQLContext) => { 7 | const { communityId } = channel; 8 | const { loaders, user: currentUser } = ctx; 9 | 10 | const community = await loaders.community.load(communityId); 11 | if (community.isPrivate) { 12 | if (await canViewCommunity(currentUser, community.id, loaders)) { 13 | return community; 14 | } else { 15 | return null; 16 | } 17 | } 18 | 19 | return community; 20 | }; 21 | -------------------------------------------------------------------------------- /api/queries/channel/communityPermissions.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type { GraphQLContext } from '../../'; 3 | import type { DBChannel } from 'shared/types'; 4 | 5 | export default async ( 6 | root: DBChannel, 7 | _: any, 8 | { user, loaders }: GraphQLContext 9 | ) => { 10 | const communityId = root.id || root.communityId; 11 | const defaultPermissions = { 12 | isOwner: false, 13 | isMember: false, 14 | isModerator: false, 15 | isBlocked: false, 16 | isPending: false, 17 | receiveNotifications: false, 18 | }; 19 | 20 | if (!communityId || !user) { 21 | return defaultPermissions; 22 | } 23 | 24 | const permissions = await loaders.userPermissionsInCommunity.load([ 25 | user.id, 26 | communityId, 27 | ]); 28 | return permissions || defaultPermissions; 29 | }; 30 | -------------------------------------------------------------------------------- /api/queries/channel/isArchived.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type { DBChannel } from 'shared/types'; 3 | 4 | export default ({ archivedAt, ...rest }: DBChannel) => { 5 | if (archivedAt) return true; 6 | return false; 7 | }; 8 | -------------------------------------------------------------------------------- /api/queries/channel/joinSettings.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type { DBChannel } from 'shared/types'; 3 | import type { GraphQLContext } from '../../'; 4 | import { canModerateChannel } from '../../utils/permissions'; 5 | 6 | export default async ({ id }: DBChannel, _: any, ctx: GraphQLContext) => { 7 | const { user: currentUser, loaders } = ctx; 8 | 9 | if (!currentUser) return null; 10 | 11 | if (!(await canModerateChannel(currentUser.id, id, loaders))) { 12 | return null; 13 | } 14 | 15 | return loaders.channelSettings.load(id).then(settings => { 16 | return settings.joinSettings; 17 | }); 18 | }; 19 | -------------------------------------------------------------------------------- /api/queries/channel/memberCount.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type { DBChannel } from 'shared/types'; 3 | import type { GraphQLContext } from '../../'; 4 | import { canViewChannel } from '../../utils/permissions'; 5 | 6 | export default async (channel: DBChannel, _: any, ctx: GraphQLContext) => { 7 | const { id, memberCount, isPrivate } = channel; 8 | const { loaders, user: currentUser } = ctx; 9 | 10 | if (isPrivate) { 11 | if (!(await canViewChannel(currentUser, id, loaders))) return 0; 12 | } 13 | 14 | return memberCount || 1; 15 | }; 16 | -------------------------------------------------------------------------------- /api/queries/channel/moderators.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type { GraphQLContext } from '../../'; 3 | import type { DBChannel } from 'shared/types'; 4 | import { getModeratorsInChannel } from '../../models/usersChannels'; 5 | import { canViewChannel } from '../../utils/permissions'; 6 | 7 | export default async (channel: DBChannel, _: any, ctx: GraphQLContext) => { 8 | const { loaders, user: currentUser } = ctx; 9 | const { id, isPrivate } = channel; 10 | 11 | if (isPrivate) { 12 | if (!(await canViewChannel(currentUser, id, loaders))) return null; 13 | } 14 | 15 | return getModeratorsInChannel(id).then(users => loaders.user.loadMany(users)); 16 | }; 17 | -------------------------------------------------------------------------------- /api/queries/channel/owners.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type { GraphQLContext } from '../../'; 3 | import type { DBChannel } from 'shared/types'; 4 | import { getOwnersInChannel } from '../../models/usersChannels'; 5 | import { canViewChannel } from '../../utils/permissions'; 6 | 7 | export default async (channel: DBChannel, _: any, ctx: GraphQLContext) => { 8 | const { id, isPrivate } = channel; 9 | const { loaders, user: currentUser } = ctx; 10 | 11 | if (isPrivate) { 12 | if (!(await canViewChannel(currentUser, id, loaders))) return null; 13 | } 14 | 15 | return getOwnersInChannel(id).then(users => loaders.user.loadMany(users)); 16 | }; 17 | -------------------------------------------------------------------------------- /api/queries/community/brandedLogin.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type { DBCommunity } from 'shared/types'; 3 | import type { GraphQLContext } from '../../'; 4 | 5 | export default async ( 6 | { id }: DBCommunity, 7 | _: any, 8 | { loaders }: GraphQLContext 9 | ) => { 10 | return await loaders.communitySettings.load(id).then(() => { 11 | return { isEnabled: null, message: null }; 12 | }); 13 | }; 14 | -------------------------------------------------------------------------------- /api/queries/community/channelConnection.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type { GraphQLContext } from '../../'; 3 | import type { DBCommunity } from 'shared/types'; 4 | import { getChannelsByCommunity } from '../../models/channel'; 5 | import { canViewCommunity } from '../../utils/permissions'; 6 | 7 | export default async ({ id }: DBCommunity, _: any, ctx: GraphQLContext) => { 8 | const { user, loaders } = ctx; 9 | 10 | if (!(await canViewCommunity(user, id, loaders))) { 11 | return { 12 | pageInfo: { 13 | hasNextPage: false, 14 | }, 15 | edges: [], 16 | }; 17 | } 18 | 19 | return { 20 | pageInfo: { 21 | hasNextPage: false, 22 | }, 23 | edges: getChannelsByCommunity(id).then(channels => 24 | channels.map(channel => ({ 25 | node: channel, 26 | })) 27 | ), 28 | }; 29 | }; 30 | -------------------------------------------------------------------------------- /api/queries/community/communityPermissions.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type { DBCommunity } from 'shared/types'; 3 | import type { GraphQLContext } from '../../'; 4 | import { DEFAULT_USER_COMMUNITY_PERMISSIONS } from '../../models/usersCommunities'; 5 | 6 | export default async ( 7 | { id }: DBCommunity, 8 | _: any, 9 | { user, loaders }: GraphQLContext 10 | ) => { 11 | const defaultPermissions = { 12 | ...DEFAULT_USER_COMMUNITY_PERMISSIONS, 13 | userId: null, 14 | communityId: null, 15 | }; 16 | 17 | if (!id || !user) return defaultPermissions; 18 | 19 | const permissions = await loaders.userPermissionsInCommunity.load([ 20 | user.id, 21 | id, 22 | ]); 23 | 24 | const fallbackPermissions = { 25 | ...defaultPermissions, 26 | userId: user.id, 27 | communityId: id, 28 | }; 29 | 30 | return permissions || fallbackPermissions; 31 | }; 32 | -------------------------------------------------------------------------------- /api/queries/community/coverPhoto.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type { GraphQLContext } from '../../'; 3 | import type { DBCommunity } from 'shared/types'; 4 | import { signCommunity } from 'shared/imgix'; 5 | 6 | export default (community: DBCommunity, _: any, ctx: GraphQLContext) => { 7 | const { coverPhoto } = signCommunity(community); 8 | return coverPhoto; 9 | }; 10 | -------------------------------------------------------------------------------- /api/queries/community/joinSettings.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type { DBCommunity } from 'shared/types'; 3 | import type { GraphQLContext } from '../../'; 4 | import { canModerateCommunity } from '../../utils/permissions'; 5 | 6 | export default async ({ id }: DBCommunity, _: any, ctx: GraphQLContext) => { 7 | const { user: currentUser, loaders } = ctx; 8 | 9 | if (!currentUser) return null; 10 | 11 | if (!(await canModerateCommunity(currentUser.id, id, loaders))) { 12 | return null; 13 | } 14 | 15 | return loaders.communitySettings.load(id).then(settings => { 16 | return settings.joinSettings; 17 | }); 18 | }; 19 | -------------------------------------------------------------------------------- /api/queries/community/metaData.js: -------------------------------------------------------------------------------- 1 | // TODO: Flow type again 2 | 3 | export default async () => { 4 | return { 5 | channels: 0, 6 | members: 0, 7 | }; 8 | }; 9 | -------------------------------------------------------------------------------- /api/queries/community/pinnedThread.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type { GraphQLContext } from '../../'; 3 | import type { DBCommunity } from 'shared/types'; 4 | import { getThreadById } from '../../models/thread'; 5 | import { canViewCommunity } from '../../utils/permissions'; 6 | 7 | export default async (root: DBCommunity, _: any, ctx: GraphQLContext) => { 8 | const { user, loaders } = ctx; 9 | const { pinnedThreadId, id } = root; 10 | 11 | if (!pinnedThreadId) return null; 12 | 13 | if (!await canViewCommunity(user, id, loaders)) { 14 | return null; 15 | } 16 | 17 | return await getThreadById(pinnedThreadId); 18 | }; 19 | -------------------------------------------------------------------------------- /api/queries/community/profilePhoto.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type { GraphQLContext } from '../../'; 3 | import type { DBCommunity } from 'shared/types'; 4 | import { signCommunity } from 'shared/imgix'; 5 | 6 | export default (community: DBCommunity, _: any, ctx: GraphQLContext) => { 7 | const { profilePhoto } = signCommunity(community); 8 | return profilePhoto; 9 | }; 10 | -------------------------------------------------------------------------------- /api/queries/community/rootCommunity.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type { GraphQLContext } from '../../'; 3 | 4 | type GetCommunityById = { 5 | id: string, 6 | slug: void, 7 | }; 8 | 9 | type GetCommunityBySlug = { 10 | id: void, 11 | slug: string, 12 | }; 13 | 14 | type GetCommunityArgs = GetCommunityById | GetCommunityBySlug; 15 | 16 | export default ( 17 | _: any, 18 | args: GetCommunityArgs, 19 | { loaders }: GraphQLContext 20 | ) => { 21 | if (args.id) return loaders.community.load(args.id); 22 | if (args.slug) return loaders.communityBySlug.load(args.slug); 23 | 24 | return null; 25 | }; 26 | -------------------------------------------------------------------------------- /api/queries/community/rootRecentCommunities.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { getRecentCommunities } from '../../models/community'; 3 | export default () => getRecentCommunities(); 4 | -------------------------------------------------------------------------------- /api/queries/community/rootTopCommunities.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { getCuratedCommunities } from '../../models/curatedContent'; 3 | export default () => getCuratedCommunities('top-communities-by-members'); 4 | -------------------------------------------------------------------------------- /api/queries/community/slackSettings.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type { DBCommunity } from 'shared/types'; 3 | import type { GraphQLContext } from '../../'; 4 | import UserError from '../../utils/UserError'; 5 | import { 6 | canModerateCommunity, 7 | isAuthedResolver as requireAuth, 8 | } from '../../utils/permissions'; 9 | 10 | export default requireAuth( 11 | async ({ id }: DBCommunity, _: any, { user, loaders }: GraphQLContext) => { 12 | if (!await canModerateCommunity(user.id, id, loaders)) { 13 | return new UserError('You don’t have permission to manage this channel'); 14 | } 15 | 16 | return await loaders.communitySettings.load(id); 17 | } 18 | ); 19 | -------------------------------------------------------------------------------- /api/queries/community/watercooler.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type { GraphQLContext } from '../../'; 3 | import type { DBCommunity } from 'shared/types'; 4 | import { getThreads } from '../../models/thread'; 5 | import { canViewCommunity } from '../../utils/permissions'; 6 | 7 | export default async (root: DBCommunity, _: any, ctx: GraphQLContext) => { 8 | const { watercoolerId, id } = root; 9 | const { user, loaders } = ctx; 10 | if (!watercoolerId) return null; 11 | 12 | if (!await canViewCommunity(user, id, loaders)) { 13 | return null; 14 | } 15 | 16 | return await getThreads([watercoolerId]).then( 17 | res => (res && res.length > 0 ? res[0] : null) 18 | ); 19 | }; 20 | -------------------------------------------------------------------------------- /api/queries/communityMember/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import communityMember from './rootCommunityMember'; 3 | import user from './user'; 4 | import roles from './roles'; 5 | 6 | module.exports = { 7 | Query: { 8 | communityMember, 9 | }, 10 | CommunityMember: { 11 | user, 12 | roles, 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /api/queries/communityMember/roles.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type { DBUsersCommunities } from 'shared/types'; 3 | 4 | export default ({ 5 | isModerator, 6 | isOwner, 7 | isBlocked, 8 | isPending, 9 | }: DBUsersCommunities) => { 10 | const roles = []; 11 | if (isModerator) roles.push('moderator'); 12 | if (isOwner) roles.push('admin'); 13 | if (isBlocked) roles.push('blocked'); 14 | if (isPending) roles.push('pending'); 15 | return roles; 16 | }; 17 | -------------------------------------------------------------------------------- /api/queries/communityMember/rootCommunityMember.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type { GraphQLContext } from '../../'; 3 | 4 | type GetCommunityMemberArgs = { 5 | userId: string, 6 | communityId: string, 7 | }; 8 | 9 | export default ( 10 | _: any, 11 | args: GetCommunityMemberArgs, 12 | { loaders }: GraphQLContext 13 | ) => loaders.userPermissionsInCommunity.load([args.userId, args.communityId]); 14 | -------------------------------------------------------------------------------- /api/queries/communityMember/user.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type { DBUsersCommunities } from 'shared/types'; 3 | import type { GraphQLContext } from '../../'; 4 | 5 | export default async ( 6 | { userId }: DBUsersCommunities, 7 | _: any, 8 | { loaders }: GraphQLContext 9 | ) => { 10 | if (userId) return await loaders.user.load(userId); 11 | return null; 12 | }; 13 | -------------------------------------------------------------------------------- /api/queries/directMessageThread/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import directMessageThread from './rootDirectMessageThread'; 3 | import directMessageThreadByUserIds from './rootDirectMessageThreadByUserIds'; 4 | import messageConnection from './messageConnection'; 5 | import participants from './participants'; 6 | import snippet from './snippet'; 7 | 8 | module.exports = { 9 | Query: { 10 | directMessageThread, 11 | directMessageThreadByUserIds, 12 | }, 13 | DirectMessageThread: { 14 | messageConnection, 15 | participants, 16 | snippet, 17 | }, 18 | }; 19 | -------------------------------------------------------------------------------- /api/queries/directMessageThread/participants.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type { GraphQLContext } from '../../'; 3 | import { canViewDMThread } from '../../utils/permissions'; 4 | import { signUser } from 'shared/imgix'; 5 | 6 | export default async ({ id }: { id: string }, _: any, ctx: GraphQLContext) => { 7 | const { loaders, user } = ctx; 8 | if (!user || !user.id) return []; 9 | 10 | const canViewThread = await canViewDMThread(user.id, id, loaders); 11 | 12 | if (!canViewThread) return []; 13 | 14 | return loaders.directMessageParticipants.load(id).then(results => { 15 | if (!results || results.length === 0) return []; 16 | return results.reduction.map(user => { 17 | return signUser(user); 18 | }); 19 | }); 20 | }; 21 | -------------------------------------------------------------------------------- /api/queries/directMessageThread/rootDirectMessageThread.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type { GraphQLContext } from '../../'; 3 | import { canViewDMThread } from '../../utils/permissions'; 4 | 5 | export default async ( 6 | _: any, 7 | { id }: { id: string }, 8 | { user, loaders }: GraphQLContext 9 | ) => { 10 | // signed out users should never be able to request a dm thread 11 | if (!user || !user.id) return null; 12 | 13 | const canViewThread = await canViewDMThread(user.id, id, loaders); 14 | 15 | if (!canViewThread) return null; 16 | 17 | return loaders.directMessageThread.load(id); 18 | }; 19 | -------------------------------------------------------------------------------- /api/queries/directMessageThread/rootDirectMessageThreadByUserIds.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type { GraphQLContext } from '../../'; 3 | import { checkForExistingDMThread } from '../../models/directMessageThread'; 4 | import { isAuthedResolver as requireAuth } from '../../utils/permissions'; 5 | 6 | type Args = { 7 | userIds: Array, 8 | }; 9 | 10 | export default requireAuth(async (_: any, args: Args, ctx: GraphQLContext) => { 11 | // signed out users will never be able to view a dm thread 12 | const { user: currentUser, loaders } = ctx; 13 | const { userIds } = args; 14 | 15 | const allMemberIds = [...userIds, currentUser.id]; 16 | const existingThread = await checkForExistingDMThread(allMemberIds); 17 | 18 | if (!existingThread) return null; 19 | 20 | return loaders.directMessageThread.load(existingThread); 21 | }); 22 | -------------------------------------------------------------------------------- /api/queries/message/content.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type { GraphQLContext } from '../../'; 3 | import type { DBMessage } from 'shared/types'; 4 | import { signMessage } from 'shared/imgix'; 5 | 6 | export default (message: DBMessage, _: any, ctx: GraphQLContext) => { 7 | const signedMessage = signMessage(message); 8 | return signedMessage.content; 9 | }; 10 | -------------------------------------------------------------------------------- /api/queries/message/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import message from './rootMessage'; 3 | import getMediaMessagesForThread from './rootGetMediaMessagesForThread'; 4 | import sender from './sender'; 5 | import author from './author'; 6 | import thread from './thread'; 7 | import reactions from './reactions'; 8 | import parent from './parent'; 9 | import content from './content'; 10 | 11 | module.exports = { 12 | Query: { 13 | message, 14 | getMediaMessagesForThread, 15 | }, 16 | Message: { 17 | author, 18 | sender, // deprecated 19 | thread, 20 | reactions, 21 | parent, 22 | content, 23 | }, 24 | }; 25 | -------------------------------------------------------------------------------- /api/queries/message/parent.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type { DBMessage } from 'shared/types'; 3 | import type { GraphQLContext } from '../../'; 4 | 5 | export default ( 6 | { parentId }: DBMessage, 7 | _: void, 8 | { loaders }: GraphQLContext 9 | ) => { 10 | if (!parentId) return null; 11 | return loaders.message.load(parentId); 12 | }; 13 | -------------------------------------------------------------------------------- /api/queries/message/reactions.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type { DBMessage } from 'shared/types'; 3 | import type { GraphQLContext } from '../../'; 4 | 5 | export default ({ id }: DBMessage, _: any, { user, loaders }: GraphQLContext) => 6 | loaders.messageReaction.load(id).then(result => { 7 | if (!result) 8 | return { 9 | count: 0, 10 | hasReacted: false, 11 | }; 12 | const reactions = result.reduction; 13 | return { 14 | count: reactions.length, 15 | hasReacted: user 16 | ? reactions.some(reaction => reaction.userId === user.id) 17 | : false, 18 | }; 19 | }); 20 | -------------------------------------------------------------------------------- /api/queries/message/rootGetMediaMessagesForThread.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { getMediaMessagesForThread } from '../../models/message'; 3 | 4 | export default (_: any, { threadId }: { threadId: string }) => 5 | getMediaMessagesForThread(threadId); 6 | -------------------------------------------------------------------------------- /api/queries/message/rootMessage.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { getMessage } from '../../models/message'; 3 | import type { GraphQLContext } from '../../'; 4 | 5 | export default (_: any, { id }: { id: string }, { loaders }: GraphQLContext) => 6 | loaders.message.load(id); 7 | -------------------------------------------------------------------------------- /api/queries/message/thread.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type { DBMessage } from 'shared/types'; 3 | import { getThread } from '../../models/thread'; 4 | 5 | export default ({ threadId }: DBMessage) => getThread(threadId); 6 | -------------------------------------------------------------------------------- /api/queries/reaction/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import reaction from './reaction'; 3 | import user from './user'; 4 | import message from './message'; 5 | 6 | module.exports = { 7 | Query: { 8 | reaction, 9 | }, 10 | Reaction: { 11 | user, 12 | message, 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /api/queries/reaction/message.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type { DBReaction } from 'shared/types'; 3 | import { getMessage } from '../../models/message'; 4 | 5 | export default ({ messageId }: DBReaction) => getMessage(messageId); 6 | -------------------------------------------------------------------------------- /api/queries/reaction/reaction.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { getReaction } from '../../models/reaction'; 3 | export default (_: any, { id }: { id: string }) => getReaction(id); 4 | -------------------------------------------------------------------------------- /api/queries/reaction/user.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type { DBReaction } from 'shared/types'; 3 | import type { GraphQLContext } from '../../'; 4 | 5 | export default ({ userId }: DBReaction, _: any, { loaders }: GraphQLContext) => 6 | loaders.user.load(userId); 7 | -------------------------------------------------------------------------------- /api/queries/thread/attachments.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | /* 3 | 4 | Deprecated Oct 26 2018 by @brian 5 | 6 | */ 7 | import type { DBThread } from 'shared/types'; 8 | 9 | export default () => []; 10 | -------------------------------------------------------------------------------- /api/queries/thread/channel.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type { GraphQLContext } from '../../'; 3 | import type { DBThread } from 'shared/types'; 4 | 5 | export default async (root: DBThread, _: any, ctx: GraphQLContext) => { 6 | const { channelId, id } = root; 7 | const { loaders } = ctx; 8 | const channel = await loaders.channel.load(channelId); 9 | if (!channel) { 10 | console.error( 11 | 'User queried thread of non-existent/deleted channel: ', 12 | channelId 13 | ); 14 | console.error('Thread queried: ', id); 15 | } 16 | return channel; 17 | }; 18 | -------------------------------------------------------------------------------- /api/queries/thread/community.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type { GraphQLContext } from '../../'; 3 | import type { DBThread } from 'shared/types'; 4 | 5 | export default async (root: DBThread, _: any, ctx: GraphQLContext) => { 6 | const { communityId, id } = root; 7 | const { loaders } = ctx; 8 | const community = await loaders.community.load(communityId); 9 | if (!community) { 10 | console.error( 11 | 'User queried thread of non-existent/deleted community: ', 12 | communityId 13 | ); 14 | console.error('Thread queried: ', id); 15 | } 16 | return community; 17 | }; 18 | -------------------------------------------------------------------------------- /api/queries/thread/content.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type { GraphQLContext } from '../../'; 3 | import type { DBThread } from 'shared/types'; 4 | import { signThread } from 'shared/imgix'; 5 | 6 | export default (thread: DBThread, _: any, ctx: GraphQLContext) => { 7 | const signedThread = signThread(thread); 8 | return { 9 | ...signedThread.content, 10 | body: signedThread.content.body, 11 | }; 12 | }; 13 | -------------------------------------------------------------------------------- /api/queries/thread/creator.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | /* 3 | 4 | DEPRECATED 2/3/2018 by @brian 5 | 6 | */ 7 | import type { GraphQLContext } from '../../'; 8 | import type { DBThread } from 'shared/types'; 9 | 10 | export default async ( 11 | { creatorId, communityId }: DBThread, 12 | _: any, 13 | { loaders }: GraphQLContext 14 | ) => { 15 | const creator = await loaders.user.load(creatorId); 16 | 17 | const permissions = await loaders.userPermissionsInCommunity.load([ 18 | creatorId, 19 | communityId, 20 | ]); 21 | 22 | return { 23 | ...creator, 24 | contextPermissions: { 25 | communityId, 26 | isModerator: permissions ? permissions.isModerator : false, 27 | isOwner: permissions ? permissions.isOwner : false, 28 | isBlocked: permissions ? permissions.isBlocked : false, 29 | }, 30 | }; 31 | }; 32 | -------------------------------------------------------------------------------- /api/queries/thread/isAuthor.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type { GraphQLContext } from '../../'; 3 | import type { DBThread } from 'shared/types'; 4 | 5 | export default ({ creatorId }: DBThread, _: any, { user }: GraphQLContext) => { 6 | if (!creatorId || !user) return false; 7 | return user.id === creatorId; 8 | }; 9 | -------------------------------------------------------------------------------- /api/queries/thread/isCreator.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | /* 3 | 4 | DEPRECATED 2/8/2018 by @brian 5 | 6 | */ 7 | import type { GraphQLContext } from '../../'; 8 | import type { DBThread } from 'shared/types'; 9 | 10 | export default ({ creatorId }: DBThread, _: any, { user }: GraphQLContext) => { 11 | if (!creatorId || !user) return false; 12 | return user.id === creatorId; 13 | }; 14 | -------------------------------------------------------------------------------- /api/queries/thread/metaImage.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type { GraphQLContext } from '../../'; 3 | import type { DBThread } from 'shared/types'; 4 | import generateImageFromText from '../../utils/generate-thread-meta-image-from-text'; 5 | import { signImageUrl } from 'shared/imgix'; 6 | 7 | export default async (thread: DBThread, _: any, ctx: GraphQLContext) => { 8 | const { loaders } = ctx; 9 | const { watercooler, communityId, content } = thread; 10 | 11 | const community = await loaders.community.load(communityId); 12 | if (!community) return null; 13 | 14 | const imageUrl = generateImageFromText({ 15 | title: watercooler 16 | ? `Chat with the ${community.name} community` 17 | : content.title, 18 | footer: `spectrum.chat/${community.slug}`, 19 | }); 20 | 21 | if (!imageUrl) return null; 22 | 23 | return signImageUrl(imageUrl); 24 | }; 25 | -------------------------------------------------------------------------------- /api/queries/thread/participants.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type { GraphQLContext } from '../../'; 3 | import type { DBThread } from 'shared/types'; 4 | 5 | export default ({ id }: DBThread, _: any, { loaders }: GraphQLContext) => { 6 | return loaders.threadParticipants 7 | .load(id) 8 | .then(result => (result ? result.reduction : [])); 9 | }; 10 | -------------------------------------------------------------------------------- /api/queries/thread/rootThread.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type { GraphQLContext } from '../../'; 3 | import { canViewThread } from '../../utils/permissions'; 4 | 5 | export default async ( 6 | _: any, 7 | { id }: { id: string }, 8 | { loaders, user }: GraphQLContext 9 | ) => { 10 | if (!(await canViewThread(user ? user.id : 'undefined', id, loaders))) { 11 | return null; 12 | } 13 | 14 | const thread = await loaders.thread.load(id); 15 | 16 | if (!thread) return null; 17 | 18 | return thread; 19 | }; 20 | -------------------------------------------------------------------------------- /api/queries/user/channelConnection.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type { DBUser } from 'shared/types'; 3 | import { getChannelsByUser } from '../../models/channel'; 4 | 5 | export default (user: DBUser) => ({ 6 | pageInfo: { 7 | hasNextPage: false, 8 | }, 9 | edges: getChannelsByUser(user.id).then(channels => 10 | channels.map(channel => ({ 11 | node: channel, 12 | })) 13 | ), 14 | }); 15 | -------------------------------------------------------------------------------- /api/queries/user/coverPhoto.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type { GraphQLContext } from '../../'; 3 | import type { DBUser } from 'shared/types'; 4 | import { signUser } from 'shared/imgix'; 5 | 6 | export default (user: DBUser, _: any, ctx: GraphQLContext) => { 7 | const { coverPhoto } = signUser(user); 8 | return coverPhoto; 9 | }; 10 | -------------------------------------------------------------------------------- /api/queries/user/email.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type { GraphQLContext } from '../../'; 3 | import type { DBUser } from 'shared/types'; 4 | import { isAdmin } from '../../utils/permissions'; 5 | 6 | export default async ( 7 | { id }: DBUser, 8 | _: any, 9 | { user, loaders }: GraphQLContext 10 | ) => { 11 | // Only admins and the user themselves can view the email 12 | if (!user || (id !== user.id && !isAdmin(user.id))) return null; 13 | const { email } = await loaders.user.load(id); 14 | return email; 15 | }; 16 | -------------------------------------------------------------------------------- /api/queries/user/githubProfile.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type { DBUser } from 'shared/types'; 3 | 4 | export default ({ githubProviderId, githubUsername }: DBUser) => { 5 | if (!githubProviderId || !githubUsername) return null; 6 | return { 7 | id: githubProviderId, 8 | username: githubUsername, 9 | }; 10 | }; 11 | -------------------------------------------------------------------------------- /api/queries/user/isAdmin.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type { DBUser } from 'shared/types'; 3 | import { isAdmin } from '../../utils/permissions'; 4 | 5 | export default ({ id }: DBUser) => isAdmin(id); 6 | -------------------------------------------------------------------------------- /api/queries/user/profilePhoto.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type { GraphQLContext } from '../../'; 3 | import type { DBUser } from 'shared/types'; 4 | import { signUser } from 'shared/imgix'; 5 | 6 | export default (user: DBUser, _: any, ctx: GraphQLContext) => { 7 | const { profilePhoto } = signUser(user); 8 | return profilePhoto; 9 | }; 10 | -------------------------------------------------------------------------------- /api/queries/user/rootCurrentUser.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type { GraphQLContext } from '../../'; 3 | import { getUserById } from 'shared/db/queries/user'; 4 | 5 | export default async (_: any, __: any, { user }: GraphQLContext) => { 6 | if (!user || !user.id) return null; 7 | const dbUser = await getUserById(user.id); 8 | if (!dbUser || dbUser.bannedAt) return null; 9 | return dbUser; 10 | }; 11 | -------------------------------------------------------------------------------- /api/queries/user/rootUser.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type { GraphQLContext } from '../../'; 3 | 4 | export default ( 5 | _: any, 6 | args: { id?: string, username?: string } = {}, 7 | { loaders }: GraphQLContext 8 | ) => { 9 | if (args.id) return loaders.user.load(args.id); 10 | if (args.username) return loaders.userByUsername.load(args.username); 11 | return null; 12 | }; 13 | -------------------------------------------------------------------------------- /api/queries/user/settings.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type { GraphQLContext } from '../../'; 3 | import type { DBUser } from 'shared/types'; 4 | import { 5 | getUsersSettings, 6 | createNewUsersSettings, 7 | } from '../../models/usersSettings'; 8 | import UserError from '../../utils/UserError'; 9 | 10 | export default async (_: DBUser, __: any, { user }: GraphQLContext) => { 11 | if (!user) return new UserError('You must be signed in to continue.'); 12 | const settings = await getUsersSettings(user.id); 13 | if (settings) return settings; 14 | return await createNewUsersSettings(user.id); 15 | }; 16 | -------------------------------------------------------------------------------- /api/queries/user/threadCount.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type { GraphQLContext } from '../../'; 3 | import type { DBUser } from 'shared/types'; 4 | 5 | export default ({ id }: DBUser, _: any, { loaders }: GraphQLContext) => { 6 | return loaders.userThreadCount.load(id).then(data => (data ? data.count : 0)); 7 | }; 8 | -------------------------------------------------------------------------------- /api/routes/api/export-user-data.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { Router } from 'express'; 3 | const userDataRouter = Router(); 4 | import { getUserById } from 'shared/db/queries/user'; 5 | 6 | userDataRouter.get('/', async (req: express$Request, res: express$Response) => { 7 | if (!req.user) return res.send('No logged-in user'); 8 | 9 | // $FlowIssue 10 | const user = await getUserById(req.user.id); 11 | if (!user) return res.send('User not found.'); 12 | 13 | // This forces the browser to download a .json file instead of rendering it 14 | res.setHeader('Content-Type', 'application/octet-stream'); 15 | return res.send(user); 16 | }); 17 | 18 | export default userDataRouter; 19 | -------------------------------------------------------------------------------- /api/routes/api/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { Router } from 'express'; 3 | 4 | const apiRouter = Router(); 5 | 6 | // import graphiql from './graphiql'; 7 | // // Only allow GraphiQL in development 8 | // if (process.env.NODE_ENV === 'development') { 9 | // apiRouter.use('/graphiql', graphiql); 10 | // } 11 | 12 | import userExportRouter from './export-user-data'; 13 | apiRouter.use('/user.json', userExportRouter); 14 | 15 | // import graphql from './graphql'; 16 | // apiRouter.use('/', graphql); 17 | 18 | export default apiRouter; 19 | -------------------------------------------------------------------------------- /api/routes/auth/facebook.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { Router } from 'express'; 3 | import { createSigninRoutes } from './create-signin-routes'; 4 | 5 | const facebookAuthRouter = Router(); 6 | const { main, callbacks } = createSigninRoutes('facebook', { 7 | scope: ['email'], 8 | }); 9 | 10 | facebookAuthRouter.get('/', main); 11 | 12 | facebookAuthRouter.get('/callback', ...callbacks); 13 | 14 | export default facebookAuthRouter; 15 | -------------------------------------------------------------------------------- /api/routes/auth/github.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { Router } from 'express'; 3 | import { createSigninRoutes } from './create-signin-routes'; 4 | 5 | const githubAuthRouter = Router(); 6 | const { main, callbacks } = createSigninRoutes('github', { 7 | scope: ['read:user,user:email'], 8 | state: true, 9 | }); 10 | 11 | githubAuthRouter.get('/', main); 12 | 13 | githubAuthRouter.get('/callback', ...callbacks); 14 | 15 | export default githubAuthRouter; 16 | -------------------------------------------------------------------------------- /api/routes/auth/google.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { Router } from 'express'; 3 | import { createSigninRoutes } from './create-signin-routes'; 4 | 5 | const googleAuthRouter = Router(); 6 | const { main, callbacks } = createSigninRoutes('google', { 7 | scope: 'profile email', 8 | }); 9 | 10 | googleAuthRouter.get('/', main); 11 | 12 | googleAuthRouter.get('/callback', ...callbacks); 13 | 14 | export default googleAuthRouter; 15 | -------------------------------------------------------------------------------- /api/routes/auth/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { Router } from 'express'; 3 | import twitterAuthRoutes from './twitter'; 4 | import facebookAuthRoutes from './facebook'; 5 | import googleAuthRoutes from './google'; 6 | import githubAuthRoutes from './github'; 7 | import logoutRoutes from './logout'; 8 | 9 | const authRouter = Router(); 10 | 11 | authRouter.use('/twitter', twitterAuthRoutes); 12 | authRouter.use('/facebook', facebookAuthRoutes); 13 | authRouter.use('/google', googleAuthRoutes); 14 | authRouter.use('/github', githubAuthRoutes); 15 | authRouter.use('/logout', logoutRoutes); 16 | 17 | export default authRouter; 18 | -------------------------------------------------------------------------------- /api/routes/auth/logout.js: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | const debug = require('debug')('api:routes:auth:logout'); 3 | import { destroySession } from '../../models/session'; 4 | 5 | const IS_PROD = process.env.NODE_ENV === 'production'; 6 | const HOME = IS_PROD ? '/explore' : 'http://localhost:3000/explore'; 7 | const logoutRouter = Router(); 8 | 9 | logoutRouter.get('/', (req, res) => { 10 | req.logout(); 11 | return res.redirect(HOME); 12 | }); 13 | 14 | export default logoutRouter; 15 | -------------------------------------------------------------------------------- /api/routes/auth/twitter.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { Router } from 'express'; 3 | import { createSigninRoutes } from './create-signin-routes'; 4 | 5 | const twitterAuthRouter = Router(); 6 | const { main, callbacks } = createSigninRoutes('twitter'); 7 | 8 | twitterAuthRouter.get('/', main); 9 | 10 | twitterAuthRouter.get('/callback', ...callbacks); 11 | 12 | export default twitterAuthRouter; 13 | -------------------------------------------------------------------------------- /api/routes/create-subscription-server.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { SubscriptionServer } from 'subscriptions-transport-ws'; 3 | import { execute, subscribe } from 'graphql'; 4 | 5 | import schema from '../schema'; 6 | 7 | /** 8 | * Create a subscription server based on an exisiting express.js server 9 | */ 10 | const createSubscriptionsServer = (server: any, path: string) => { 11 | // Start subscriptions server 12 | return SubscriptionServer.create( 13 | { 14 | execute, 15 | subscribe, 16 | schema, 17 | keepAlive: 10000, 18 | }, 19 | { 20 | server, 21 | path, 22 | } 23 | ); 24 | }; 25 | 26 | export default createSubscriptionsServer; 27 | -------------------------------------------------------------------------------- /api/subscriptions/community.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | module.exports = { 3 | Subscription: {}, 4 | }; 5 | -------------------------------------------------------------------------------- /api/subscriptions/directMessageThread.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | module.exports = { 3 | Subscription: {}, 4 | }; 5 | -------------------------------------------------------------------------------- /api/subscriptions/message.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | module.exports = { 3 | Subscription: {}, 4 | }; 5 | -------------------------------------------------------------------------------- /api/subscriptions/notification.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | module.exports = { 3 | Subscription: {}, 4 | }; 5 | -------------------------------------------------------------------------------- /api/subscriptions/thread.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | module.exports = { 4 | Subscription: {}, 5 | }; 6 | -------------------------------------------------------------------------------- /api/test/channel/queries/__snapshots__/channelSettings.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`should fetch a private channels token join settings 1`] = ` 4 | Object { 5 | "data": Object { 6 | "channel": Object { 7 | "id": "1", 8 | "joinSettings": Object { 9 | "token": null, 10 | "tokenJoinEnabled": false, 11 | }, 12 | }, 13 | }, 14 | } 15 | `; 16 | -------------------------------------------------------------------------------- /api/test/channel/queries/__snapshots__/root.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`should fetch a channel by id 1`] = ` 4 | Object { 5 | "data": Object { 6 | "channel": Object { 7 | "createdAt": "2016-12-31T23:00:00.000Z", 8 | "description": "General chatter", 9 | "id": "1", 10 | "isPrivate": false, 11 | "name": "General", 12 | "slug": "general", 13 | }, 14 | }, 15 | } 16 | `; 17 | 18 | exports[`should fetch a channel by slug and community slug 1`] = ` 19 | Object { 20 | "data": Object { 21 | "channel": Object { 22 | "createdAt": "2016-12-31T23:00:00.000Z", 23 | "description": "General chatter", 24 | "id": "1", 25 | "isPrivate": false, 26 | "name": "General", 27 | "slug": "general", 28 | }, 29 | }, 30 | } 31 | `; 32 | -------------------------------------------------------------------------------- /api/test/community/queries/__snapshots__/communitySettings.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`should fetch a communitys settings 1`] = ` 4 | Object { 5 | "data": Object { 6 | "community": Object { 7 | "id": "1", 8 | }, 9 | }, 10 | } 11 | `; 12 | -------------------------------------------------------------------------------- /api/test/community/queries/communitySettings.test.js: -------------------------------------------------------------------------------- 1 | //@flow 2 | import { request } from '../../utils'; 3 | import { SPECTRUM_COMMUNITY_ID } from '../../../migrations/seed/default/constants'; 4 | 5 | it('should fetch a communitys settings', async () => { 6 | const query = /* GraphQL */ ` 7 | { 8 | community(id: "${SPECTRUM_COMMUNITY_ID}") { 9 | id 10 | } 11 | } 12 | `; 13 | 14 | expect.assertions(1); 15 | const result = await request(query); 16 | expect(result).toMatchSnapshot(); 17 | }); 18 | -------------------------------------------------------------------------------- /api/test/message/__snapshots__/queries.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`sender should fetch a user 1`] = ` 4 | Object { 5 | "data": Object { 6 | "message": Object { 7 | "author": Object { 8 | "id": "1", 9 | "user": Object { 10 | "username": "mxstbr", 11 | }, 12 | }, 13 | }, 14 | }, 15 | } 16 | `; 17 | 18 | exports[`should fetch a message 1`] = ` 19 | Object { 20 | "data": Object { 21 | "message": Object { 22 | "content": Object { 23 | "body": "{\\"blocks\\":[{\\"key\\":\\"9u8bg\\",\\"text\\":\\"This is the first message!\\",\\"type\\":\\"unstyled\\",\\"depth\\":0,\\"inlineStyleRanges\\":[],\\"entityRanges\\":[],\\"data\\":{}}],\\"entityMap\\":{}}", 24 | }, 25 | "id": "1", 26 | "messageType": "draftjs", 27 | "timestamp": "2016-12-31T23:00:00.000Z", 28 | }, 29 | }, 30 | } 31 | `; 32 | -------------------------------------------------------------------------------- /api/test/thread/queries/__snapshots__/messageConnection.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`messageConnection should fetch a threads messages 1`] = ` 4 | Object { 5 | "data": Object { 6 | "thread": Object { 7 | "messageConnection": Object { 8 | "edges": Array [ 9 | Object { 10 | "node": Object { 11 | "id": "1", 12 | }, 13 | }, 14 | Object { 15 | "node": Object { 16 | "id": "2", 17 | }, 18 | }, 19 | Object { 20 | "node": Object { 21 | "id": "3", 22 | }, 23 | }, 24 | Object { 25 | "node": Object { 26 | "id": "4", 27 | }, 28 | }, 29 | ], 30 | }, 31 | }, 32 | }, 33 | } 34 | `; 35 | -------------------------------------------------------------------------------- /api/test/thread/queries/__snapshots__/root.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`should fetch a thread 1`] = ` 4 | Object { 5 | "data": Object { 6 | "thread": Object { 7 | "content": Object { 8 | "title": "The first thread! 🎉", 9 | }, 10 | "createdAt": "2016-12-31T23:00:00.000Z", 11 | "id": "thread-1", 12 | "isLocked": false, 13 | "isPublished": true, 14 | "lastActive": "2016-12-31T23:00:00.000Z", 15 | "modifiedAt": "2016-12-31T23:00:00.000Z", 16 | "type": "DRAFTJS", 17 | }, 18 | }, 19 | } 20 | `; 21 | -------------------------------------------------------------------------------- /api/test/thread/queries/root.test.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { request } from '../../utils'; 3 | 4 | it('should fetch a thread', async () => { 5 | const query = /* GraphQL */ ` 6 | { 7 | thread(id: "thread-1") { 8 | id 9 | createdAt 10 | modifiedAt 11 | lastActive 12 | isPublished 13 | isLocked 14 | type 15 | content { 16 | title 17 | } 18 | } 19 | } 20 | `; 21 | 22 | expect.hasAssertions(); 23 | const result = await request(query); 24 | 25 | expect(result).toMatchSnapshot(); 26 | }); 27 | -------------------------------------------------------------------------------- /api/test/utils.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { graphql } from 'graphql'; 3 | import createLoaders from '../loaders'; 4 | import schema from '../schema'; 5 | 6 | type Options = { 7 | context?: { 8 | user?: ?Object, 9 | }, 10 | variables?: ?Object, 11 | }; 12 | 13 | // Nice little helper function for tests 14 | export const request = (query: mixed, { context, variables }: Options = {}) => { 15 | return graphql( 16 | schema, 17 | query, 18 | undefined, 19 | { 20 | loaders: createLoaders(), 21 | ...context, 22 | }, 23 | variables 24 | ); 25 | }; 26 | -------------------------------------------------------------------------------- /api/test/utils/__mocks__/debug.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | const loggers = {}; 4 | 5 | function makeLogger() { 6 | function logger(...args: any[]) { 7 | logger.log.push(args.join(' ')); 8 | } 9 | logger.log = []; 10 | return logger; 11 | } 12 | 13 | module.exports = (namespace: string) => { 14 | return (loggers[namespace] = loggers[namespace] || makeLogger()); 15 | }; 16 | -------------------------------------------------------------------------------- /api/types/CommunityMember.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | const CommunityMember = /* GraphQL */ ` 3 | type CommunityMember @cacheControl(maxAge: 600) { 4 | id: ID! 5 | user: User! 6 | roles: [String] 7 | isMember: Boolean 8 | isModerator: Boolean 9 | isOwner: Boolean 10 | isBlocked: Boolean 11 | isPending: Boolean 12 | } 13 | 14 | extend type Query { 15 | communityMember(userId: ID!, communityId: ID!): CommunityMember 16 | } 17 | `; 18 | 19 | module.exports = CommunityMember; 20 | -------------------------------------------------------------------------------- /api/types/Invoice.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | // deprecated 9/27/2018 while removing payments 3 | const Invoice = /* GraphQL */ ` 4 | type Invoice { 5 | id: ID! 6 | paidAt: Int 7 | amount: Int 8 | sourceBrand: String 9 | sourceLast4: String 10 | planName: String 11 | } 12 | 13 | extend type Query { 14 | invoice(id: ID): Invoice 15 | } 16 | `; 17 | 18 | module.exports = Invoice; 19 | -------------------------------------------------------------------------------- /api/types/Reaction.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | const Reaction = /* GraphQL */ ` 3 | enum ReactionTypes { 4 | like 5 | } 6 | 7 | type Reaction @cacheControl(maxAge: 84700) { 8 | id: ID! 9 | timestamp: Date! 10 | message: Message! 11 | user: User! 12 | type: ReactionTypes! 13 | } 14 | 15 | input ReactionInput { 16 | messageId: ID! 17 | type: ReactionTypes! 18 | } 19 | 20 | extend type Query { 21 | reaction(id: String!): Reaction 22 | } 23 | `; 24 | 25 | module.exports = Reaction; 26 | -------------------------------------------------------------------------------- /api/types/ThreadParticipant.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | const ThreadParticipant = /* GraphQL */ ` 3 | type ThreadParticipant { 4 | id: ID! 5 | user: User! 6 | roles: [String] 7 | isMember: Boolean 8 | isModerator: Boolean 9 | isOwner: Boolean 10 | isBlocked: Boolean 11 | } 12 | `; 13 | 14 | module.exports = ThreadParticipant; 15 | -------------------------------------------------------------------------------- /api/types/custom-scalars/LowercaseString.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { GraphQLScalarType } from 'graphql'; 3 | // $FlowIssue 4 | import { Kind } from 'graphql/language'; 5 | 6 | const LowercaseString = new GraphQLScalarType({ 7 | name: 'LowercaseString', 8 | description: 'Returns all strings in lower case', 9 | parseValue(value) { 10 | return value.toLowerCase(); 11 | }, 12 | serialize(value) { 13 | return value.toLowerCase(); 14 | }, 15 | parseLiteral(ast) { 16 | if (ast.kind === Kind.STRING) { 17 | return ast.value.toLowerCase(); 18 | } 19 | return null; 20 | }, 21 | }); 22 | 23 | export default LowercaseString; 24 | -------------------------------------------------------------------------------- /api/types/scalars.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | /** 3 | * Custom scalars (data types, like Int, String,...) live in this file, 4 | * both their type definitions and their resolvers 5 | */ 6 | const GraphQLDate = require('graphql-date'); 7 | // NOTE(@mxstbr): We can remove this once we stop using makeExecutableSchema 8 | import { GraphQLUpload } from 'apollo-server-express'; 9 | import LowercaseString from './custom-scalars/LowercaseString'; 10 | 11 | const typeDefs = /* GraphQL */ ` 12 | scalar Date 13 | scalar Upload 14 | scalar LowercaseString 15 | `; 16 | 17 | const resolvers = { 18 | Date: GraphQLDate, 19 | Upload: GraphQLUpload, 20 | LowercaseString: LowercaseString, 21 | }; 22 | 23 | module.exports = { 24 | typeDefs, 25 | resolvers, 26 | }; 27 | -------------------------------------------------------------------------------- /api/utils/UserError.js: -------------------------------------------------------------------------------- 1 | // Taken from https://github.com/kadirahq/graphql-errors 2 | 3 | export const IsUserError = Symbol('IsUserError'); 4 | 5 | class UserError extends Error { 6 | constructor(...args) { 7 | super(...args); 8 | this.name = 'Error'; 9 | this.message = args[0]; 10 | this[IsUserError] = true; 11 | Error.captureStackTrace(this, 'Error'); 12 | } 13 | } 14 | 15 | export default UserError; 16 | -------------------------------------------------------------------------------- /api/utils/base64.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | /** 4 | * Encode a string to base64 (using the Node built-in Buffer) 5 | * 6 | * Stolen from http://stackoverflow.com/a/38237610/2115623 7 | */ 8 | export const encode = (string: string) => 9 | Buffer.from(string).toString('base64'); 10 | 11 | type Base64String = string; 12 | /** 13 | * Decode a base64 string (using the Node built-in Buffer) 14 | * 15 | * Stolen from http://stackoverflow.com/a/38237610/2115623 16 | */ 17 | export const decode = (string?: Base64String) => 18 | (string ? Buffer.from(string, 'base64').toString('ascii') : ''); 19 | -------------------------------------------------------------------------------- /api/utils/file-storage.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | require('now-env'); 3 | 4 | import type { FileUpload, EntityTypes } from 'shared/types'; 5 | 6 | const { FILE_STORAGE } = process.env; 7 | 8 | const getUploadImageFn = () => { 9 | switch (FILE_STORAGE) { 10 | case 'local': 11 | return require('./file-system').uploadImage; 12 | case 's3': 13 | default: 14 | return require('./s3').uploadImage; 15 | } 16 | }; 17 | 18 | const uploadImageFn = getUploadImageFn(); 19 | 20 | export const uploadImage = ( 21 | file: FileUpload, 22 | entity: EntityTypes, 23 | id: string 24 | ): Promise => 25 | uploadImageFn(file, entity, id).catch(err => { 26 | throw new Error(err); 27 | }); 28 | -------------------------------------------------------------------------------- /api/utils/get-random-default-photo.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | const faker = require('faker'); 3 | 4 | // Helper function to get a random profile and cover photo 5 | const PALETTE = [ 6 | 'blue', 7 | 'blush', 8 | 'cool', 9 | 'green', 10 | 'orange', 11 | 'peach', 12 | 'pink', 13 | 'red', 14 | 'teal', 15 | 'violet', 16 | ]; 17 | export default () => { 18 | const color = faker.random.arrayElement(PALETTE); 19 | return { 20 | profilePhoto: `/default_images/profile-${color}.png`, 21 | coverPhoto: `/default_images/cover-${color}.svg`, 22 | }; 23 | }; 24 | -------------------------------------------------------------------------------- /api/utils/paginate-arrays.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | export type PaginationOptions = { 3 | first: number, 4 | after?: string, 5 | }; 6 | 7 | type Input = any; 8 | 9 | type Output = { 10 | list: Array, 11 | hasMoreItems: boolean, 12 | }; 13 | 14 | /** 15 | * Paginate an array 16 | * 17 | * For more complex value pass a getAfter callback to get the index of the cursor 18 | */ 19 | export default ( 20 | arr: Array, 21 | { first, after }: PaginationOptions, 22 | getAfter?: any => mixed 23 | ): Output => { 24 | const cursor = getAfter ? arr.findIndex(getAfter) : arr.indexOf(after); 25 | const begin = cursor > -1 ? cursor + 1 : 0; 26 | const end = begin + first; 27 | return { 28 | list: arr.slice(begin, end), 29 | hasMoreItems: arr.length > end ? true : false, 30 | }; 31 | }; 32 | -------------------------------------------------------------------------------- /api/utils/validate-draft-js-input.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | export const validateRawContentState = (input: any) => { 3 | if ( 4 | !input || 5 | !input.blocks || 6 | !Array.isArray(input.blocks) || 7 | !input.entityMap 8 | ) { 9 | return false; 10 | } 11 | 12 | return true; 13 | }; 14 | -------------------------------------------------------------------------------- /cypress.json: -------------------------------------------------------------------------------- 1 | { 2 | "baseUrl": "http://localhost:3000", 3 | "viewportWidth": 1300, 4 | "defaultCommandTimeout": 20000, 5 | "env": { 6 | "DEBUG": "src*,testing*,build*" 7 | }, 8 | "projectId": "6a92uk" 9 | } -------------------------------------------------------------------------------- /cypress/fixtures/example.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Using fixtures to represent data", 3 | "email": "hello@cypress.io", 4 | "body": "Fixtures are a great way to mock data for responses to routes" 5 | } -------------------------------------------------------------------------------- /cypress/integration/explore_spec.js: -------------------------------------------------------------------------------- 1 | describe('Login View', () => { 2 | beforeEach(() => { 3 | cy.visit('/explore'); 4 | }); 5 | 6 | it('should render', () => { 7 | cy.get('[data-cy="explore-page"]').should('be.visible'); 8 | }); 9 | }); 10 | -------------------------------------------------------------------------------- /cypress/integration/login_spec.js: -------------------------------------------------------------------------------- 1 | describe('Log in', () => { 2 | beforeEach(() => { 3 | cy.visit('/login'); 4 | }); 5 | 6 | it('should render login methods', () => { 7 | cy.get('[data-cy="login-page"]').should('be.visible'); 8 | cy.get('[href*="/auth/twitter"]').should('be.visible'); 9 | cy.get('[href*="/auth/facebook"]').should('be.visible'); 10 | cy.get('[href*="/auth/google"]').should('be.visible'); 11 | cy.get('[href*="/auth/github"]').should('be.visible'); 12 | 13 | cy.get('[href*="github.com/withspectrum/code-of-conduct"]').should( 14 | 'be.visible' 15 | ); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /cypress/plugins/index.js: -------------------------------------------------------------------------------- 1 | const browserify = require('@cypress/browserify-preprocessor'); 2 | 3 | module.exports = (on, config) => { 4 | const options = browserify.defaultOptions; 5 | options.browserifyOptions.transform[1][1].presets.push('@babel/preset-flow'); 6 | on('file:preprocessor', browserify(options)); 7 | }; 8 | -------------------------------------------------------------------------------- /docker/Dockerfile.api: -------------------------------------------------------------------------------- 1 | # Base builder 2 | # 3 | # 4 | FROM node:12 AS builder 5 | WORKDIR /usr/src/spectrum 6 | COPY package.json yarn.lock ./ 7 | RUN yarn 8 | COPY . . 9 | 10 | # API builder 11 | # 12 | # 13 | FROM builder AS builder-api 14 | WORKDIR /usr/src/spectrum 15 | RUN yarn --cwd ./api 16 | RUN yarn run build:api 17 | RUN cp -r ./api/node_modules ./build-api 18 | 19 | # API image 20 | # 21 | # 22 | FROM node:12 AS api 23 | COPY --from=builder-api /usr/src/spectrum/build-api /usr/src/api 24 | WORKDIR /usr/src/api 25 | CMD ["yarn", "run", "start"] 26 | -------------------------------------------------------------------------------- /docker/Dockerfile.hyperion: -------------------------------------------------------------------------------- 1 | FROM node:12 AS builder 2 | WORKDIR /usr/src/spectrum 3 | COPY package.json yarn.lock ./ 4 | RUN yarn 5 | COPY . . 6 | 7 | FROM builder AS builder-hyperion 8 | RUN yarn --cwd ./hyperion 9 | RUN yarn run build:hyperion 10 | RUN yarn --cwd ./build-hyperion 11 | 12 | FROM node:12 AS hyperion 13 | COPY --from=builder-hyperion /usr/src/spectrum/build-hyperion /usr/src/spectrum-hyperion 14 | WORKDIR /usr/src/spectrum-hyperion 15 | CMD ["yarn", "run", "start"] 16 | -------------------------------------------------------------------------------- /docs/admin/intro.md: -------------------------------------------------------------------------------- 1 | [Table of contents](../readme.md) 2 | 3 | # Admin 4 | 5 | This directory runs an internal application for viewing core metrics on the platform. While it can be built and run locally, we don't encourage contributors to spend much time here as the dashboard itself will only ever be used internally by the Spectrum team. The code can of course be reviewed for those interested to see what kind of numbers and tools are accessible to our team. -------------------------------------------------------------------------------- /docs/api/intro.md: -------------------------------------------------------------------------------- 1 | [Table of contents](../readme.md) 2 | 3 | # API 4 | 5 | The Spectrum API is a Node.js web server based on Express.js and GraphQL. It's also houses a websocket server for all of our subscription needs. 6 | 7 | ## Structure 8 | 9 | This server follows a GraphQL-first philosophy. That means we design the GraphQL schema first and then start implementing business logic. This is great because it gives us a clear separation of concerns (business logic vs. schema), and it's how Facebook recommends to use GraphQL. 10 | 11 | ## GraphQL 12 | - [Intro](graphql/intro.md) 13 | - [Fragments](graphql/fragments.md) 14 | - [Pagination](graphql/pagination.md) 15 | - [Testing](graphql/testing.md) 16 | - [Tips & Tricks](graphql/tips-and-tricks.md) -------------------------------------------------------------------------------- /docs/hyperion (server side rendering)/intro.md: -------------------------------------------------------------------------------- 1 | [Table of contents](../readme.md) 2 | 3 | # Hyperion 4 | 5 | *Hyperion: (/haɪˈpɪəriən/) is one of the twelve Titan children of Gaia and Uranus.* 6 | 7 | Hyperion is the server responsible for server-side rendering the front end. In production, Hyperion does an initial render of a requested view on the server and responds with static HTML. The static HTML is then rehydrated with our JS bundle. 8 | 9 | Learn more about [development with Hyperion](development.md) 10 | 11 | -------------------------------------------------------------------------------- /docs/operations/hourly-backups.md: -------------------------------------------------------------------------------- 1 | # Hourly Off-site Backups 2 | 3 | In order to avoid more data loss we implemented hourly off-site backups in [#5150](https://github.com/withspectrum/spectrum/pull/5150). While the implementation is simple, it should cover us well enough. 4 | 5 | It works by running two cron jobs: 6 | 7 | 1. Runs at 30 minutes past every hour and triggers an "on-demand backup" with our database host (Compose) 8 | 2. Runs at 0 minutes past every hour, fetches the latest "on-demand backup" from our database host and uploads it to our S3 bucket 9 | 10 | To access the latest hourly backup you can either go to the Compose dashboard (app.compose.com), navigate to the RethinkDB deployment and download the newest on-demand backup, or open our S3 bucket and download the latest one from there. 11 | -------------------------------------------------------------------------------- /docs/operations/importing-rethinkdb-backups.md: -------------------------------------------------------------------------------- 1 | [Table of contents](../readme.md) / [Operations](./index.md) 2 | 3 | # Importing production data locally 4 | 5 | Sometimes it's useful to have production data running locally in rethinkdb for debugging and testing. To get production data running locally, follow these steps: 6 | 7 | 1. Go to http://localhost:8080/#tables and delete the local 'spectrum' table 8 | 2. Log in to the Space Program AWS console 9 | 3. Go to S3 > Spectrum Backups > Hourly 10 | 4. Download the latest backup .tar.gz 11 | 5. Unzip that backup onto your desktop 12 | 6. Rename the unzipped directory to 'prod-backup' 13 | 6. In your terminal, run: `cd ~/Desktop && rethinkdb import -d prod-backup` 14 | 7. The import will take a couple hours, at which point you can clear localstorage at localhost:3000 to re-authenticate 15 | -------------------------------------------------------------------------------- /docs/operations/intro.md: -------------------------------------------------------------------------------- 1 | # Operations 2 | 3 | This directory is for docs related to operating the Spectrum platform. Common questions about how to perform non-code-related tasks should go here. 4 | 5 | Learn more about: 6 | - [Banning users](banning-users.md) 7 | - [Deleting users](deleting-users.md) 8 | - [Importing a RethinkDB backup locally](importing-rethinkdb-backups.md) -------------------------------------------------------------------------------- /docs/readme.md: -------------------------------------------------------------------------------- 1 | ## Table of Contents 2 | 3 | - [Deployments](deployments.md) 4 | - [Admin](admin/intro.md) 5 | - [API](api/intro.md) 6 | - [GraphQL](api/graphql/intro.md) 7 | - [Fragments](api/graphql/fragments.md) 8 | - [Pagination](api/graphql/pagination.md) 9 | - [Testing](api/graphql/testing.md) 10 | - [Tips & Tricks](api/graphql/tips-and-tricks.md) 11 | - [Hyperion (server side rendering)]() 12 | - [Development]() 13 | - [Operations](operations/intro.md) 14 | - [Deleting users](operations/deleting-users.md) 15 | - [Importing RethinkDB backups](operations/importing-rethinkdb-backups.md) 16 | - [Testing](testing/intro.md) 17 | - [Integration](testing/integration.md) 18 | - [Unit](testing/unit.md) 19 | - [Workers](workers/intro.md) 20 | - [Background jobs with Redis](workers/background-jobs.md) 21 | -------------------------------------------------------------------------------- /docs/testing/intro.md: -------------------------------------------------------------------------------- 1 | [Table of contents](../readme.md) 2 | 3 | # Testing 4 | 5 | We have a test suite consisting of a bunch of unit tests (mostly for the API) and integration tests to verify Spectrum keeps working as expected. The entire test suite is run in CI for every commit and PR, so if you introduce a breaking change the CI will fail and the PR will not be merge-able. 6 | 7 | Learn more about: 8 | - [Unit testing](./unit.md) 9 | - [Integration testing](./integration.md) 10 | 11 | -------------------------------------------------------------------------------- /flow-typed/npm/electron-context-menu_vx.x.x.js: -------------------------------------------------------------------------------- 1 | // flow-typed signature: fa2daa44307abd285e881b992fa9d76c 2 | // flow-typed version: <>/electron-context-menu_v0.9.1/flow_v0.66.0 3 | 4 | /** 5 | * This is an autogenerated libdef stub for: 6 | * 7 | * 'electron-context-menu' 8 | * 9 | * Fill this stub out by replacing all the `any` types. 10 | * 11 | * Once filled out, we encourage you to share your work with the 12 | * community by sending a pull request to: 13 | * https://github.com/flowtype/flow-typed 14 | */ 15 | 16 | declare module 'electron-context-menu' { 17 | declare module.exports: any; 18 | } 19 | -------------------------------------------------------------------------------- /flow-typed/npm/electron-window-state_vx.x.x.js: -------------------------------------------------------------------------------- 1 | // flow-typed signature: 77130022186123cfc81d76787330354e 2 | // flow-typed version: <>/electron-window-state_vx.x.x/flow_v0.66.0 3 | 4 | /** 5 | * This is an autogenerated libdef stub for: 6 | * 7 | * 'electron-window-state' 8 | * 9 | * Fill this stub out by replacing all the `any` types. 10 | * 11 | * Once filled out, we encourage you to share your work with the 12 | * community by sending a pull request to: 13 | * https://github.com/flowtype/flow-typed 14 | */ 15 | 16 | declare module 'electron-window-state' { 17 | declare module.exports: any; 18 | } 19 | -------------------------------------------------------------------------------- /flow-typed/npm/expo-server-sdk_vx.x.x.js: -------------------------------------------------------------------------------- 1 | // flow-typed signature: 3f270976cb128c991faff0ddfc6f6aa3 2 | // flow-typed version: <>/expo-server-sdk_v2.3.3/flow_v0.63.1 3 | 4 | /** 5 | * This is an autogenerated libdef stub for: 6 | * 7 | * 'expo-server-sdk' 8 | * 9 | * Fill this stub out by replacing all the `any` types. 10 | * 11 | * Once filled out, we encourage you to share your work with the 12 | * community by sending a pull request to: 13 | * https://github.com/flowtype/flow-typed 14 | */ 15 | 16 | declare module 'expo-server-sdk' { 17 | declare module.exports: any; 18 | } 19 | -------------------------------------------------------------------------------- /flow-typed/npm/expo_vx.x.x.js: -------------------------------------------------------------------------------- 1 | // flow-typed signature: 0ab3677c8ee0ecd2c38c9d3b9a511bf2 2 | // flow-typed version: <>/expo_v25.0.0/flow_v0.63.1 3 | 4 | /** 5 | * This is an autogenerated libdef stub for: 6 | * 7 | * 'expo' 8 | * 9 | * Fill this stub out by replacing all the `any` types. 10 | * 11 | * Once filled out, we encourage you to share your work with the 12 | * community by sending a pull request to: 13 | * https://github.com/flowtype/flow-typed 14 | */ 15 | 16 | declare module 'expo' { 17 | declare module.exports: any; 18 | } 19 | -------------------------------------------------------------------------------- /flow-typed/npm/flow-bin_v0.x.x.js: -------------------------------------------------------------------------------- 1 | // flow-typed signature: 6a5610678d4b01e13bbfbbc62bdaf583 2 | // flow-typed version: 3817bc6980/flow-bin_v0.x.x/flow_>=v0.25.x 3 | 4 | declare module 'flow-bin' { 5 | declare module.exports: string; 6 | } 7 | -------------------------------------------------------------------------------- /flow-typed/npm/idx_v2.x.x.js: -------------------------------------------------------------------------------- 1 | // flow-typed signature: aeb1419f64937fdd64367e23af4f80d0 2 | // flow-typed version: 75f67f047c/idx_v2.x.x/flow_>=v0.33.x 3 | 4 | // From: https://github.com/facebookincubator/idx/blob/master/packages/idx/src/idx.js.flow 5 | 6 | declare module idx { 7 | declare module.exports: $Facebookism$Idx; 8 | } 9 | -------------------------------------------------------------------------------- /flow-typed/npm/isomorphic-fetch_v2.x.x.js: -------------------------------------------------------------------------------- 1 | // flow-typed signature: 47370d221401bec823c43c3598266e26 2 | // flow-typed version: ec28077c25/isomorphic-fetch_v2.x.x/flow_>=v0.25.x 3 | 4 | 5 | declare module 'isomorphic-fetch' { 6 | declare module.exports: (input: string | Request | URL, init?: RequestOptions) => Promise; 7 | } 8 | -------------------------------------------------------------------------------- /flow-typed/npm/rimraf_v2.x.x.js: -------------------------------------------------------------------------------- 1 | // flow-typed signature: 1dff23447d5e18f5ac2b05aaec7cfb74 2 | // flow-typed version: a453e98ea2/rimraf_v2.x.x/flow_>=v0.25.0 3 | 4 | declare module 'rimraf' { 5 | declare type Options = { 6 | maxBusyTries?: number, 7 | emfileWait?: number, 8 | glob?: boolean, 9 | disableGlob?: boolean 10 | }; 11 | 12 | declare type Callback = (err: ?Error, path: ?string) => void; 13 | 14 | declare module.exports: { 15 | (f: string, opts?: Options | Callback, callback?: Callback): void; 16 | sync(path: string, opts?: Options): void; 17 | }; 18 | } 19 | -------------------------------------------------------------------------------- /flow-typed/npm/sentry-expo_vx.x.x.js: -------------------------------------------------------------------------------- 1 | // flow-typed signature: 25940cfb9401c1fbcc661adf692edf15 2 | // flow-typed version: <>/sentry-expo_v1.7.0/flow_v0.63.1 3 | 4 | /** 5 | * This is an autogenerated libdef stub for: 6 | * 7 | * 'sentry-expo' 8 | * 9 | * Fill this stub out by replacing all the `any` types. 10 | * 11 | * Once filled out, we encourage you to share your work with the 12 | * community by sending a pull request to: 13 | * https://github.com/flowtype/flow-typed 14 | */ 15 | 16 | declare module 'sentry-expo' { 17 | declare module.exports: any; 18 | } 19 | -------------------------------------------------------------------------------- /flow-typed/react-native.js: -------------------------------------------------------------------------------- 1 | declare module 'react-native' { 2 | declare module.exports: any; 3 | } 4 | -------------------------------------------------------------------------------- /hyperion/renderer/browser-shim.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | // Shim some browser stuff we use in the client for server-side rendering 3 | // NOTE(@mxstbr): We should be getting rid of this over time 4 | global.window = { 5 | location: { 6 | protocol: 'https:', 7 | host: 'spectrum.chat', 8 | hash: '', 9 | }, 10 | addEventListener: () => {}, 11 | }; 12 | global.localStorage = { 13 | getItem: () => null, 14 | setItem: () => {}, 15 | removeItem: () => {}, 16 | }; 17 | global.navigator = { 18 | userAgent: '', 19 | }; 20 | global.CSS = { 21 | escape: require('css.escape'), 22 | }; 23 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | // The Jest configuration 3 | const path = require('path'); 4 | 5 | module.exports = { 6 | setupTestFrameworkScriptFile: path.resolve( 7 | __dirname, 8 | './shared/testing/setup-test-framework' 9 | ), 10 | globalSetup: path.resolve(__dirname, './shared/testing/setup'), 11 | globalTeardown: path.resolve(__dirname, './shared/testing/teardown'), 12 | testPathIgnorePatterns: ['/node_modules/', '/mutations/'], 13 | testURL: 'http://localhost/', 14 | }; 15 | -------------------------------------------------------------------------------- /now-secrets.example.json: -------------------------------------------------------------------------------- 1 | { 2 | "@twitter-oauth-client-secret-development": "uwjAMUwcX8ZhhYq0Vlnq2fLNwV3CfONB8Tjzo2LW42JlVc2uKJ", 3 | "@facebook-oauth-client-id-development": "231715924020859", 4 | "@facebook-oauth-client-secret-development": "0c3a5b3521fe79636b568f3f4db67f2b", 5 | "@google-oauth-client-secret-development": "i7H7ZLfntkIEp7kyyNsvyH3O", 6 | "@github-oauth-client-secret-development": "789f3a4b5772e978acd135fe7c86886e62f688c7", 7 | "@session-cookie-secret": "this-is-an-example-secret", 8 | "@api-token-secret": "this-is-another-example-secret-string" 9 | } 10 | -------------------------------------------------------------------------------- /public/img/apple-icon-114x114-precomposed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/withspectrum/spectrum/e8ebdcbba829ce067dd4e4b21c631b093299ef07/public/img/apple-icon-114x114-precomposed.png -------------------------------------------------------------------------------- /public/img/apple-icon-144x144-precomposed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/withspectrum/spectrum/e8ebdcbba829ce067dd4e4b21c631b093299ef07/public/img/apple-icon-144x144-precomposed.png -------------------------------------------------------------------------------- /public/img/apple-icon-192x192-precomposed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/withspectrum/spectrum/e8ebdcbba829ce067dd4e4b21c631b093299ef07/public/img/apple-icon-192x192-precomposed.png -------------------------------------------------------------------------------- /public/img/apple-icon-512x512-precomposed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/withspectrum/spectrum/e8ebdcbba829ce067dd4e4b21c631b093299ef07/public/img/apple-icon-512x512-precomposed.png -------------------------------------------------------------------------------- /public/img/apple-icon-57x57-precomposed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/withspectrum/spectrum/e8ebdcbba829ce067dd4e4b21c631b093299ef07/public/img/apple-icon-57x57-precomposed.png -------------------------------------------------------------------------------- /public/img/apple-icon-72x72-precomposed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/withspectrum/spectrum/e8ebdcbba829ce067dd4e4b21c631b093299ef07/public/img/apple-icon-72x72-precomposed.png -------------------------------------------------------------------------------- /public/img/badge.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/withspectrum/spectrum/e8ebdcbba829ce067dd4e4b21c631b093299ef07/public/img/badge.png -------------------------------------------------------------------------------- /public/img/discover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/withspectrum/spectrum/e8ebdcbba829ce067dd4e4b21c631b093299ef07/public/img/discover.png -------------------------------------------------------------------------------- /public/img/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/withspectrum/spectrum/e8ebdcbba829ce067dd4e4b21c631b093299ef07/public/img/favicon.ico -------------------------------------------------------------------------------- /public/img/favicon_unread.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/withspectrum/spectrum/e8ebdcbba829ce067dd4e4b21c631b093299ef07/public/img/favicon_unread.ico -------------------------------------------------------------------------------- /public/img/head_placeholder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/withspectrum/spectrum/e8ebdcbba829ce067dd4e4b21c631b093299ef07/public/img/head_placeholder.png -------------------------------------------------------------------------------- /public/img/homescreen-icon-114x114.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/withspectrum/spectrum/e8ebdcbba829ce067dd4e4b21c631b093299ef07/public/img/homescreen-icon-114x114.png -------------------------------------------------------------------------------- /public/img/homescreen-icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/withspectrum/spectrum/e8ebdcbba829ce067dd4e4b21c631b093299ef07/public/img/homescreen-icon-144x144.png -------------------------------------------------------------------------------- /public/img/homescreen-icon-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/withspectrum/spectrum/e8ebdcbba829ce067dd4e4b21c631b093299ef07/public/img/homescreen-icon-192x192.png -------------------------------------------------------------------------------- /public/img/homescreen-icon-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/withspectrum/spectrum/e8ebdcbba829ce067dd4e4b21c631b093299ef07/public/img/homescreen-icon-512x512.png -------------------------------------------------------------------------------- /public/img/homescreen-icon-57x57.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/withspectrum/spectrum/e8ebdcbba829ce067dd4e4b21c631b093299ef07/public/img/homescreen-icon-57x57.png -------------------------------------------------------------------------------- /public/img/homescreen-icon-72x72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/withspectrum/spectrum/e8ebdcbba829ce067dd4e4b21c631b093299ef07/public/img/homescreen-icon-72x72.png -------------------------------------------------------------------------------- /public/img/logo-mark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/withspectrum/spectrum/e8ebdcbba829ce067dd4e4b21c631b093299ef07/public/img/logo-mark.png -------------------------------------------------------------------------------- /public/img/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/withspectrum/spectrum/e8ebdcbba829ce067dd4e4b21c631b093299ef07/public/img/logo.png -------------------------------------------------------------------------------- /public/img/mark-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/withspectrum/spectrum/e8ebdcbba829ce067dd4e4b21c631b093299ef07/public/img/mark-white.png -------------------------------------------------------------------------------- /public/img/mark.svg: -------------------------------------------------------------------------------- 1 | 2 | mark 3 | Created using Figma 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /public/img/media.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/withspectrum/spectrum/e8ebdcbba829ce067dd4e4b21c631b093299ef07/public/img/media.png -------------------------------------------------------------------------------- /public/img/pinned-tab.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /public/img/slack_colored.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/withspectrum/spectrum/e8ebdcbba829ce067dd4e4b21c631b093299ef07/public/img/slack_colored.png -------------------------------------------------------------------------------- /public/img/waterfall.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/withspectrum/spectrum/e8ebdcbba829ce067dd4e4b21c631b093299ef07/public/img/waterfall.png -------------------------------------------------------------------------------- /public/install-raven.js: -------------------------------------------------------------------------------- 1 | Raven.config('https://3bd8523edd5d43d7998f9b85562d6924@sentry.io/154812', { 2 | whitelistUrls: [/spectrum.chat/, /www.spectrum.chat/], 3 | }).install(); 4 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: -------------------------------------------------------------------------------- /robots.txt: -------------------------------------------------------------------------------- 1 | # This robots.txt file is used to disallow robots to crawl our staging environment alpha.spectrum.chat, which has the same content as the main site 2 | User-agent: * 3 | Disallow: / 4 | -------------------------------------------------------------------------------- /rules-alpha.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": [ 3 | { "pathname": "/robots.txt", "dest": "robots.alpha.spectrum.chat" }, 4 | { "pathname": "/api", "dest": "api.alpha.spectrum.chat" }, 5 | { "pathname": "/api/**", "dest": "api.alpha.spectrum.chat" }, 6 | { "pathname": "/auth", "dest": "api.alpha.spectrum.chat" }, 7 | { "pathname": "/auth/**", "dest": "api.alpha.spectrum.chat" }, 8 | { "pathname": "/websocket", "dest": "api.alpha.spectrum.chat" }, 9 | { "pathname": "/websocket/**", "dest": "api.alpha.spectrum.chat" }, 10 | { "dest": "hyperion.alpha.spectrum.chat" } 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /rules.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": [ 3 | { "pathname": "/api", "dest": "api.spectrum.chat" }, 4 | { "pathname": "/api/**", "dest": "api.spectrum.chat" }, 5 | { "pathname": "/auth", "dest": "api.spectrum.chat" }, 6 | { "pathname": "/auth/**", "dest": "api.spectrum.chat" }, 7 | { "pathname": "/websocket", "dest": "api.spectrum.chat" }, 8 | { "pathname": "/websocket/**", "dest": "api.spectrum.chat" }, 9 | { "dest": "hyperion.workers.spectrum.chat" } 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /scripts/introspection-query.js: -------------------------------------------------------------------------------- 1 | const fetch = require('node-fetch'); 2 | const fs = require('fs'); 3 | 4 | fetch(`http://localhost:3001/api`, { 5 | method: 'POST', 6 | headers: { 'Content-Type': 'application/json' }, 7 | body: JSON.stringify({ 8 | query: ` 9 | { 10 | __schema { 11 | types { 12 | kind 13 | name 14 | possibleTypes { 15 | name 16 | } 17 | } 18 | } 19 | } 20 | `, 21 | }), 22 | }) 23 | .then(result => result.json()) 24 | .then(result => { 25 | fs.writeFile( 26 | './fragmentTypes.json', 27 | JSON.stringify(result.data, null, 2), 28 | err => { 29 | if (err) console.error('Error writing fragmentTypes file', err); 30 | console.log('Fragment types successfully extracted!'); 31 | } 32 | ); 33 | }); 34 | -------------------------------------------------------------------------------- /scripts/utils/error.js: -------------------------------------------------------------------------------- 1 | module.exports = (...args) => { 2 | console.error('\n🚨 Error:', args[0], '🚨\n\n', ...args.slice(1), '\n'); 3 | process.exit(1); 4 | return; 5 | }; 6 | -------------------------------------------------------------------------------- /scripts/utils/parse-argv.js: -------------------------------------------------------------------------------- 1 | // Cheap process.argv parser 2 | module.exports = argv => { 3 | const processArgs = argv.slice(2); 4 | const args = processArgs.filter(arg => arg.indexOf('--') !== 0); 5 | const flags = processArgs 6 | .filter(arg => arg.indexOf('--') === 0) 7 | .reduce((flags, flag) => { 8 | flags[flag.replace(/^--/, '')] = true; 9 | return flags; 10 | }, {}); 11 | 12 | return { args, flags }; 13 | }; 14 | -------------------------------------------------------------------------------- /set-heroku-config: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | 5 | ENV_FILE=.env 6 | heroku config:set -a spectrum-chat-hyperion $(cat $ENV_FILE | tr '\n' ' ') 7 | -------------------------------------------------------------------------------- /shared/clients/draft-js/links-decorator/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React from 'react'; 3 | import { Link } from 'react-router-dom'; 4 | import createLinksDecorator, { 5 | type LinksDecoratorComponentProps, 6 | } from './core'; 7 | import { SPECTRUM_URLS } from 'shared/regexps'; 8 | 9 | export default createLinksDecorator((props: LinksDecoratorComponentProps) => { 10 | const regexp = new RegExp(SPECTRUM_URLS, 'ig'); 11 | const match = regexp.exec(props.href); 12 | 13 | if (match && match[0] && match[1]) 14 | return {props.children}; 15 | 16 | return ( 17 | 18 | {props.children} 19 | 20 | ); 21 | }); 22 | -------------------------------------------------------------------------------- /shared/clients/draft-js/mentions-decorator/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import createMentionsDecorator from './core'; 3 | import { Mention } from 'src/components/rich-text-editor/style.js'; 4 | 5 | export default createMentionsDecorator(Mention); 6 | -------------------------------------------------------------------------------- /shared/clients/draft-js/message/renderer.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { createRenderer } from '../renderer'; 3 | 4 | export const messageRenderer = createRenderer({ 5 | headings: false, 6 | }); 7 | -------------------------------------------------------------------------------- /shared/clients/draft-js/message/test/__snapshots__/renderer.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`messageRenderer should render certain blocks 1`] = ` 4 | Array [ 5 | "unstyled", 6 | "code-block", 7 | "blockquote", 8 | "unordered-list-item", 9 | "ordered-list-item", 10 | ] 11 | `; 12 | -------------------------------------------------------------------------------- /shared/clients/draft-js/message/test/renderer.test.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { messageRenderer } from '../renderer'; 3 | import mentionsDecorator from '../../mentions-decorator/index'; 4 | import linksDecorator from '../../links-decorator/index'; 5 | 6 | describe('messageRenderer', () => { 7 | it('should render certain blocks', () => { 8 | expect(Object.keys(messageRenderer.blocks)).toMatchSnapshot(); 9 | }); 10 | 11 | it('should have decorators', () => { 12 | expect(messageRenderer.decorators).toContain(mentionsDecorator); 13 | expect(messageRenderer.decorators).toContain(linksDecorator); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /shared/clients/draft-js/message/types.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | export type KeyObj = { 4 | key: string, 5 | }; 6 | 7 | export type KeysObj = { 8 | keys: string[], 9 | data?: Object, 10 | }; 11 | 12 | export type DataObj = { 13 | url?: string, 14 | href?: string, 15 | }; 16 | -------------------------------------------------------------------------------- /shared/clients/draft-js/thread/renderer.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { createRenderer } from '../renderer'; 3 | 4 | export default createRenderer({ 5 | headings: true, 6 | }); 7 | -------------------------------------------------------------------------------- /shared/clients/draft-js/utils/getStringElements.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | export const getStringElements = (arr: Array): Array => { 3 | return arr 4 | .map(elem => { 5 | if (!elem) return null; 6 | if (Array.isArray(elem)) return getStringElements(elem); 7 | if (typeof elem === 'string') return elem; 8 | // Handle React elements being passed as array elements 9 | // $FlowIssue 10 | if (elem.props && elem.props.children) 11 | return getStringElements(elem.props.children); 12 | return null; 13 | }) 14 | .filter(Boolean) 15 | .reduce((final, elem) => { 16 | if (Array.isArray(elem)) return [...final, ...elem]; 17 | return [...final, elem]; 18 | }, []); 19 | }; 20 | -------------------------------------------------------------------------------- /shared/clients/draft-js/utils/hasStringElements.js: -------------------------------------------------------------------------------- 1 | //@flow 2 | export const hasStringElements = (arr: Array | mixed) => { 3 | if (Array.isArray(arr)) return arr.some(elem => hasStringElements(elem)); 4 | 5 | return typeof arr === 'string'; 6 | }; 7 | -------------------------------------------------------------------------------- /shared/clients/draft-js/utils/isShort.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { toPlainText } from './plaintext'; 3 | import type { MessageInfoType } from '../../../graphql/fragments/message/messageInfo'; 4 | 5 | export const isShort = (message: MessageInfoType): boolean => { 6 | if (message.messageType === 'media') return false; 7 | const jsonBody = JSON.parse(message.content.body); 8 | return jsonBody.blocks.length <= 1 && toPlainText(jsonBody).length <= 170; 9 | }; 10 | -------------------------------------------------------------------------------- /shared/clients/draft-js/utils/plaintext.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type { RawContentState } from 'draft-js'; 3 | 4 | export const toPlainText = (raw: RawContentState) => { 5 | return raw.blocks 6 | .filter(block => block.type === 'unstyled') 7 | .map(block => block.text) 8 | .join('\n'); 9 | }; 10 | -------------------------------------------------------------------------------- /shared/clients/test/__snapshots__/messages.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`should add a timestamp above the first message 1`] = ` 4 | Array [ 5 | Array [ 6 | Object { 7 | "author": Object { 8 | "user": Object { 9 | "id": "robo", 10 | }, 11 | }, 12 | "content": Object { 13 | "body": 2016-12-31T23:00:00.000Z, 14 | }, 15 | "id": 2016-12-31T23:00:00.000Z, 16 | "timestamp": 2016-12-31T23:00:00.000Z, 17 | "type": "timestamp", 18 | }, 19 | ], 20 | Array [ 21 | Object { 22 | "author": Object { 23 | "user": Object { 24 | "id": "asdf123", 25 | }, 26 | }, 27 | "content": Object { 28 | "body": "Hey", 29 | }, 30 | "id": "whatever", 31 | "messageType": "text", 32 | "timestamp": 2016-12-31T23:00:00.000Z, 33 | }, 34 | ], 35 | ] 36 | `; 37 | -------------------------------------------------------------------------------- /shared/cookie-utils.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import Keygrip from 'keygrip'; 3 | import jwt from 'jsonwebtoken'; 4 | 5 | export const cookieKeygrip = new Keygrip([process.env.SESSION_COOKIE_SECRET]); 6 | 7 | export const getCookies = ({ userId }: { userId: string }) => { 8 | // The value of our "session" cookie 9 | const session = new Buffer( 10 | JSON.stringify({ passport: { user: userId } }) 11 | ).toString('base64'); 12 | // The value of our "session.sig" cookie 13 | const sessionSig = cookieKeygrip.sign(`session=${session}`); 14 | 15 | return { session, 'session.sig': sessionSig }; 16 | }; 17 | 18 | export const signCookie = (cookie: string) => { 19 | return jwt.sign({ cookie }, process.env.API_TOKEN_SECRET, { 20 | expiresIn: '25y', 21 | }); 22 | }; 23 | -------------------------------------------------------------------------------- /shared/db/constants.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | export const READ_RUN_ERROR = `Do not call .run() on the query passed to createReadQuery! 4 | 5 | Bad: db.table('users').get(userId).run() 6 | Good: db.table('users').get(userId) 7 | 8 | If you need to post-process the query data (.run().then()), use the \`process\` hook. 9 | `; 10 | 11 | export const WRITE_RUN_ERROR = `Don't forget to call .run() on the query passed to createWriteQuery! 12 | 13 | Bad: db.table('users').get(userId) 14 | Good: db.table('users').get(userId).run() 15 | 16 | If you need to post-process the result, simply use .then()! \`.run().then(result => /* ... */)!\` 17 | `; 18 | -------------------------------------------------------------------------------- /shared/db/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { db } from 'shared/db/db'; 3 | import { createReadQuery, createWriteQuery } from './create-query'; 4 | 5 | export { db, createReadQuery, createWriteQuery }; 6 | -------------------------------------------------------------------------------- /shared/db/queries/channel.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { createReadQuery, db } from 'shared/db'; 3 | import type { DBChannel } from 'shared/types'; 4 | 5 | export const getChannelById = createReadQuery((id: string) => ({ 6 | query: db.table('channels').get(id), 7 | tags: (channel: ?DBChannel) => (channel ? [channel.id] : []), 8 | })); 9 | 10 | export const getChannelsById = createReadQuery((ids: Array) => ({ 11 | query: db.table('channels').getAll(...ids), 12 | tags: (channels: ?Array) => 13 | channels ? channels.map(({ id }) => id) : [], 14 | })); 15 | -------------------------------------------------------------------------------- /shared/db/queries/message.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { createReadQuery, db } from 'shared/db'; 3 | import type { DBMessage } from 'shared/types'; 4 | 5 | export const getMessageById = createReadQuery((id: string) => ({ 6 | query: db.table('messages').get(id), 7 | tags: (message: ?DBMessage) => (message ? [message.id] : []), 8 | })); 9 | -------------------------------------------------------------------------------- /shared/db/queries/thread.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { createReadQuery, db } from 'shared/db'; 3 | import type { DBThread } from 'shared/types'; 4 | 5 | export const getThreadById = createReadQuery((id: string) => ({ 6 | query: db.table('threads').get(id), 7 | tags: (thread: ?DBThread) => (thread ? [thread.id] : []), 8 | })); 9 | -------------------------------------------------------------------------------- /shared/db/query-cache.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import TagCache from 'redis-tag-cache'; 3 | 4 | const DEFAULT_REDIS_OPTIONS = { 5 | keyPrefix: 'query-cache', 6 | }; 7 | 8 | const queryCache = new TagCache({ 9 | defaultTimeout: 86400, 10 | redis: { 11 | ...DEFAULT_REDIS_OPTIONS, 12 | }, 13 | }); 14 | 15 | export default queryCache; 16 | -------------------------------------------------------------------------------- /shared/draft-utils/message-types.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | export const messageTypeObj = { 4 | text: 'text', 5 | media: 'media', 6 | draftjs: 'draftjs', 7 | }; 8 | export type MessageType = $Keys; 9 | -------------------------------------------------------------------------------- /shared/graphql-cache-keys.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | export const communityChannelCount = (id: string) => 3 | `community:${id}:channelCount`; 4 | -------------------------------------------------------------------------------- /shared/graphql/constants.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | // The constants for the GraphQL client on the web 3 | 4 | export const IS_PROD = 5 | process.env.NODE_ENV === 'production' && !process.env.FORCE_DEV; 6 | // In production the API is at the same URL, in development it's at a different port 7 | export const API_URI = IS_PROD ? '/api' : 'http://localhost:3001/api'; 8 | export const WS_URI = IS_PROD 9 | ? `wss://${window.location.host}/websocket` 10 | : 'ws://localhost:3001/websocket'; 11 | -------------------------------------------------------------------------------- /shared/graphql/fragments/channel/channelMetaData.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import gql from 'graphql-tag'; 3 | 4 | export type ChannelMetaDataType = { 5 | metaData: { 6 | members: number, 7 | }, 8 | }; 9 | 10 | export default gql` 11 | fragment channelMetaData on Channel { 12 | metaData { 13 | members 14 | } 15 | } 16 | `; 17 | -------------------------------------------------------------------------------- /shared/graphql/fragments/community/communityChannelConnection.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import gql from 'graphql-tag'; 3 | import channelInfoFragment from '../../fragments/channel/channelInfo'; 4 | import type { ChannelInfoType } from '../../fragments/channel/channelInfo'; 5 | 6 | type Edge = { 7 | node: { 8 | ...$Exact, 9 | }, 10 | }; 11 | 12 | export type CommunityChannelConnectionType = { 13 | channelConnection: { 14 | edges: Array, 15 | }, 16 | }; 17 | 18 | export default gql` 19 | fragment communityChannelConnection on Community { 20 | channelConnection { 21 | edges { 22 | node { 23 | ...channelInfo 24 | } 25 | } 26 | } 27 | } 28 | ${channelInfoFragment} 29 | `; 30 | -------------------------------------------------------------------------------- /shared/graphql/fragments/community/communityMetaData.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import gql from 'graphql-tag'; 3 | 4 | export type CommunityMetaDataType = { 5 | metaData: { 6 | members: number, 7 | }, 8 | }; 9 | 10 | export default gql` 11 | fragment communityMetaData on Community { 12 | metaData { 13 | members 14 | } 15 | } 16 | `; 17 | -------------------------------------------------------------------------------- /shared/graphql/fragments/community/communitySettings.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import gql from 'graphql-tag'; 3 | 4 | export type CommunitySettingsType = { 5 | joinSettings: { 6 | tokenJoinEnabled: boolean, 7 | token: string, 8 | }, 9 | }; 10 | 11 | export default gql` 12 | fragment communitySettings on Community { 13 | joinSettings { 14 | tokenJoinEnabled 15 | token 16 | } 17 | } 18 | `; 19 | -------------------------------------------------------------------------------- /shared/graphql/fragments/communityMember/communityMemberInfo.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import gql from 'graphql-tag'; 3 | import userInfoFragment, { type UserInfoType } from '../user/userInfo'; 4 | 5 | export type CommunityMemberInfoType = { 6 | user: { 7 | ...$Exact, 8 | }, 9 | isMember: boolean, 10 | isModerator: boolean, 11 | isBlocked: boolean, 12 | isOwner: boolean, 13 | isPending: boolean, 14 | roles: Array, 15 | }; 16 | 17 | export default gql` 18 | fragment communityMemberInfo on CommunityMember { 19 | id 20 | user { 21 | ...userInfo 22 | } 23 | isMember 24 | isModerator 25 | isBlocked 26 | isOwner 27 | isPending 28 | roles 29 | } 30 | ${userInfoFragment} 31 | `; 32 | -------------------------------------------------------------------------------- /shared/graphql/fragments/directMessageThread/directMessageThreadInfo.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import gql from 'graphql-tag'; 3 | import type { UserInfoType } from '../user/userInfo'; 4 | 5 | export type ParticipantType = { 6 | ...$Exact, 7 | userId: string, 8 | }; 9 | 10 | export type DirectMessageThreadInfoType = { 11 | id: string, 12 | snippet: string, 13 | threadLastActive: Date, 14 | participants: Array, 15 | }; 16 | 17 | export default gql` 18 | fragment directMessageThreadInfo on DirectMessageThread { 19 | id 20 | snippet 21 | threadLastActive 22 | participants { 23 | id 24 | name 25 | profilePhoto 26 | username 27 | userId 28 | } 29 | } 30 | `; 31 | -------------------------------------------------------------------------------- /shared/graphql/fragments/thread/threadParticipant.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import gql from 'graphql-tag'; 3 | import userInfoFragment from '../user/userInfo'; 4 | import type { UserInfoType } from '../user/userInfo'; 5 | 6 | export type ThreadParticipantType = { 7 | id: string, 8 | user: { 9 | ...$Exact, 10 | }, 11 | isMember: boolean, 12 | isModerator: boolean, 13 | isBlocked: boolean, 14 | isOwner: boolean, 15 | roles: Array, 16 | }; 17 | 18 | export default gql` 19 | fragment threadParticipant on ThreadParticipant { 20 | user { 21 | ...userInfo 22 | } 23 | isMember 24 | isModerator 25 | isBlocked 26 | isOwner 27 | roles 28 | } 29 | ${userInfoFragment} 30 | `; 31 | -------------------------------------------------------------------------------- /shared/graphql/fragments/user/userChannelConnection.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import gql from 'graphql-tag'; 3 | import channelInfoFragment from '../channel/channelInfo'; 4 | import type { ChannelInfoType } from '../channel/channelInfo'; 5 | 6 | type Edge = { 7 | node: { 8 | ...$Exact, 9 | }, 10 | }; 11 | 12 | export type UserChannelConnectionType = { 13 | channelConnection: { 14 | pageInfo: { 15 | hasNextPage: boolean, 16 | hasPreviousPage: boolean, 17 | }, 18 | edges: Array, 19 | }, 20 | }; 21 | 22 | export default gql` 23 | fragment userChannels on User { 24 | channelConnection { 25 | pageInfo { 26 | hasNextPage 27 | hasPreviousPage 28 | } 29 | edges { 30 | node { 31 | ...channelInfo 32 | } 33 | } 34 | } 35 | } 36 | ${channelInfoFragment} 37 | `; 38 | -------------------------------------------------------------------------------- /shared/graphql/fragments/user/userInfo.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import gql from 'graphql-tag'; 3 | 4 | export type UserInfoType = { 5 | id: string, 6 | profilePhoto: string, 7 | coverPhoto: string, 8 | name: string, 9 | firstName: ?string, 10 | description: ?string, 11 | website: ?string, 12 | username: string, 13 | timezone: number, 14 | betaSupporter?: boolean, 15 | }; 16 | 17 | export default gql` 18 | fragment userInfo on User { 19 | id 20 | profilePhoto 21 | coverPhoto 22 | name 23 | firstName 24 | description 25 | website 26 | username 27 | timezone 28 | betaSupporter 29 | } 30 | `; 31 | -------------------------------------------------------------------------------- /shared/graphql/mutations/channel/deleteChannel.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import gql from 'graphql-tag'; 3 | import { graphql } from 'react-apollo'; 4 | 5 | export type DeleteChannelType = { 6 | data: { 7 | deleteChannel: boolean, 8 | }, 9 | }; 10 | 11 | export const deleteChannelMutation = gql` 12 | mutation deleteChannel($channelId: ID!) { 13 | deleteChannel(channelId: $channelId) 14 | } 15 | `; 16 | 17 | const deleteChannelOptions = { 18 | props: ({ mutate }) => ({ 19 | deleteChannel: (channelId: string) => 20 | mutate({ 21 | variables: { 22 | channelId, 23 | }, 24 | }), 25 | }), 26 | }; 27 | 28 | export default graphql(deleteChannelMutation, deleteChannelOptions); 29 | -------------------------------------------------------------------------------- /shared/graphql/mutations/community/deleteCommunity.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import gql from 'graphql-tag'; 3 | import { graphql } from 'react-apollo'; 4 | 5 | export type DeleteCommunityType = { 6 | data: { 7 | deleteCommunity: boolean, 8 | }, 9 | }; 10 | 11 | export const deleteCommunityMutation = gql` 12 | mutation deleteCommunity($communityId: ID!) { 13 | deleteCommunity(communityId: $communityId) 14 | } 15 | `; 16 | 17 | const deleteCommunityOptions = { 18 | props: ({ mutate }) => ({ 19 | deleteCommunity: (communityId: string) => 20 | mutate({ 21 | variables: { 22 | communityId, 23 | }, 24 | }), 25 | }), 26 | }; 27 | 28 | export default graphql(deleteCommunityMutation, deleteCommunityOptions); 29 | -------------------------------------------------------------------------------- /shared/graphql/mutations/community/toggleCommunityNoindex.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import gql from 'graphql-tag'; 3 | import { graphql } from 'react-apollo'; 4 | 5 | export const toggleCommunityNoindexMutation = gql` 6 | mutation toggleCommunityNoindex($communityId: ID!) { 7 | toggleCommunityNoindex(communityId: $communityId) { 8 | id 9 | slug 10 | noindex 11 | } 12 | } 13 | `; 14 | 15 | const toggleCommunityNoindexOptions = { 16 | props: ({ mutate }) => ({ 17 | toggleCommunityNoindex: communityId => 18 | mutate({ 19 | variables: { 20 | communityId, 21 | }, 22 | }), 23 | }), 24 | }; 25 | 26 | export default graphql( 27 | toggleCommunityNoindexMutation, 28 | toggleCommunityNoindexOptions 29 | ); 30 | -------------------------------------------------------------------------------- /shared/graphql/mutations/community/toggleCommunityRedirect.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import gql from 'graphql-tag'; 3 | import { graphql } from 'react-apollo'; 4 | 5 | export const toggleCommunityRedirectMutation = gql` 6 | mutation toggleCommunityRedirect($communityId: ID!) { 7 | toggleCommunityRedirect(communityId: $communityId) { 8 | id 9 | slug 10 | redirect 11 | } 12 | } 13 | `; 14 | 15 | const toggleCommunityRedirectOptions = { 16 | props: ({ mutate }) => ({ 17 | toggleCommunityRedirect: communityId => 18 | mutate({ 19 | variables: { 20 | communityId, 21 | }, 22 | }), 23 | }), 24 | }; 25 | 26 | export default graphql( 27 | toggleCommunityRedirectMutation, 28 | toggleCommunityRedirectOptions 29 | ); 30 | -------------------------------------------------------------------------------- /shared/graphql/mutations/thread/deleteThread.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import gql from 'graphql-tag'; 3 | import { graphql } from 'react-apollo'; 4 | 5 | export type DeleteThreadType = { 6 | data: { 7 | deleteThread: boolean, 8 | }, 9 | }; 10 | 11 | export const deleteThreadMutation = gql` 12 | mutation deleteThread($threadId: ID!) { 13 | deleteThread(threadId: $threadId) 14 | } 15 | `; 16 | 17 | const deleteThreadOptions = { 18 | props: ({ mutate }) => ({ 19 | deleteThread: (threadId: string) => 20 | mutate({ 21 | variables: { 22 | threadId, 23 | }, 24 | }), 25 | }), 26 | }; 27 | 28 | export default graphql(deleteThreadMutation, deleteThreadOptions); 29 | -------------------------------------------------------------------------------- /shared/graphql/mutations/uploadImage.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import gql from 'graphql-tag'; 3 | import { graphql } from 'react-apollo'; 4 | import type { EntityTypes } from 'shared/types'; 5 | 6 | export type UploadImageType = { 7 | data: { 8 | uploadImage: string, 9 | }, 10 | }; 11 | 12 | export type UploadImageInput = { 13 | image: Object, 14 | type: EntityTypes, 15 | id?: string, 16 | }; 17 | 18 | export const uploadImageMutation = gql` 19 | mutation uploadImage($input: UploadImageInput!) { 20 | uploadImage(input: $input) 21 | } 22 | `; 23 | 24 | const uploadImageOptions = { 25 | props: ({ mutate }) => ({ 26 | uploadImage: (input: UploadImageInput) => 27 | mutate({ 28 | variables: { 29 | input, 30 | }, 31 | }), 32 | }), 33 | }; 34 | 35 | export default graphql(uploadImageMutation, uploadImageOptions); 36 | -------------------------------------------------------------------------------- /shared/graphql/mutations/user/banUser.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { graphql } from 'react-apollo'; 3 | import gql from 'graphql-tag'; 4 | 5 | type BanUserInput = { 6 | userId: string, 7 | reason: string, 8 | }; 9 | 10 | export const banUserMutation = gql` 11 | mutation banUser($input: BanUserInput!) { 12 | banUser(input: $input) 13 | } 14 | `; 15 | 16 | const banUserOptions = { 17 | props: ({ mutate }) => ({ 18 | banUser: input => 19 | mutate({ 20 | variables: { 21 | input, 22 | }, 23 | }), 24 | }), 25 | }; 26 | 27 | export default graphql(banUserMutation, banUserOptions); 28 | -------------------------------------------------------------------------------- /shared/graphql/mutations/user/deleteCurrentUser.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { graphql } from 'react-apollo'; 3 | import gql from 'graphql-tag'; 4 | 5 | export const deleteCurrentUserMutation = gql` 6 | mutation deleteCurrentUser { 7 | deleteCurrentUser 8 | } 9 | `; 10 | 11 | const deleteCurrentUserOptions = { 12 | props: ({ mutate }) => ({ 13 | deleteCurrentUser: () => mutate(), 14 | }), 15 | }; 16 | 17 | export default graphql(deleteCurrentUserMutation, deleteCurrentUserOptions); 18 | -------------------------------------------------------------------------------- /shared/graphql/queries/communityMember/getCommunityMember.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { graphql } from 'react-apollo'; 3 | import gql from 'graphql-tag'; 4 | import { type CommunityMemberInfoType } from '../../fragments/communityMember/communityMemberInfo'; 5 | 6 | export type GetCommunityMemberType = { 7 | ...$Exact, 8 | }; 9 | 10 | export const getCommunityMemberQuery = gql` 11 | query getCommunityMember($userId: ID!, $communityId: ID!) { 12 | communityMember(userId: $userId, communityId: $communityId) { 13 | id 14 | isPending 15 | } 16 | } 17 | `; 18 | 19 | const getCommunityMemberOptions = { 20 | options: ({ userId, communityId }) => ({ 21 | variables: { 22 | userId, 23 | communityId, 24 | }, 25 | }), 26 | }; 27 | 28 | export default graphql(getCommunityMemberQuery, getCommunityMemberOptions); 29 | -------------------------------------------------------------------------------- /shared/graphql/queries/message/getMessage.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { graphql } from 'react-apollo'; 3 | import gql from 'graphql-tag'; 4 | import messageInfoFragment from '../../fragments/message/messageInfo'; 5 | import type { MessageInfoType } from '../../fragments/message/messageInfo'; 6 | 7 | export type GetMessageType = { 8 | ...$Exact, 9 | }; 10 | 11 | export const getMessageByIdQuery = gql` 12 | query getMessageById($id: ID!) { 13 | message(id: $id) { 14 | ...messageInfo 15 | } 16 | } 17 | ${messageInfoFragment} 18 | `; 19 | 20 | const getMessageByIdOptions = { 21 | options: ({ id }) => ({ 22 | variables: { 23 | id, 24 | }, 25 | }), 26 | }; 27 | 28 | export const getMessageById = graphql( 29 | getMessageByIdQuery, 30 | getMessageByIdOptions 31 | ); 32 | -------------------------------------------------------------------------------- /shared/graphql/queries/user/getUserGithubProfile.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { graphql } from 'react-apollo'; 3 | import gql from 'graphql-tag'; 4 | 5 | type Profile = { 6 | id: string, 7 | username: string, 8 | }; 9 | 10 | export type GetUserGithbProfileType = { 11 | id: string, 12 | username: string, 13 | githubProfile: ?Profile, 14 | }; 15 | 16 | export const getUserGithubProfileQuery = gql` 17 | query getUserGithubProfile($id: ID) { 18 | user(id: $id) { 19 | id 20 | username 21 | githubProfile { 22 | id 23 | username 24 | } 25 | } 26 | } 27 | `; 28 | 29 | const getUserGithubProfileOptions = { 30 | options: ({ id }) => ({ 31 | variables: { 32 | id, 33 | }, 34 | }), 35 | }; 36 | export default graphql(getUserGithubProfileQuery, getUserGithubProfileOptions); 37 | -------------------------------------------------------------------------------- /shared/imgix/getDefaultExpires.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | /* 4 | Expire images sent to the client at midnight each day (UTC). 5 | Expiration needs to be consistent across all images in order 6 | to preserve client-side caching abilities and to prevent checksum 7 | mismatches during SSR 8 | */ 9 | 10 | export const getDefaultExpires = () => { 11 | const date = new Date(); 12 | date.setHours(24); 13 | date.setMinutes(0); 14 | date.setSeconds(0); 15 | date.setMilliseconds(0); 16 | return date.getTime(); 17 | }; 18 | -------------------------------------------------------------------------------- /shared/imgix/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { 3 | signImageUrl, 4 | stripLegacyPrefix, 5 | hasLegacyPrefix, 6 | LEGACY_PREFIX, 7 | } from './sign'; 8 | import { getDefaultExpires } from './getDefaultExpires'; 9 | import { signCommunity } from './signCommunity'; 10 | import { signThread } from './signThread'; 11 | import { signUser } from './signUser'; 12 | import { signMessage } from './signMessage'; 13 | 14 | export { 15 | getDefaultExpires, 16 | LEGACY_PREFIX, 17 | stripLegacyPrefix, 18 | hasLegacyPrefix, 19 | signImageUrl, 20 | signCommunity, 21 | signThread, 22 | signUser, 23 | signMessage, 24 | }; 25 | -------------------------------------------------------------------------------- /shared/imgix/signCommunity.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type { DBCommunity } from 'shared/types'; 3 | import { signImageUrl } from 'shared/imgix'; 4 | 5 | // prettier-ignore 6 | export const signCommunity = (community: DBCommunity, expires?: number): DBCommunity => { 7 | const { profilePhoto, coverPhoto, ...rest } = community; 8 | 9 | return { 10 | ...rest, 11 | profilePhoto: signImageUrl(profilePhoto, { 12 | w: 256, 13 | h: 256, 14 | dpr: 2, 15 | auto: 'compress', 16 | expires 17 | }), 18 | coverPhoto: signImageUrl(coverPhoto, { 19 | w: 1280, 20 | h: 384, 21 | dpr: 2, 22 | q: 100, 23 | expires 24 | }), 25 | }; 26 | }; 27 | -------------------------------------------------------------------------------- /shared/imgix/signMessage.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type { DBMessage } from 'shared/types'; 3 | import { signImageUrl } from 'shared/imgix'; 4 | 5 | export const signMessage = ( 6 | message: DBMessage, 7 | expires?: number 8 | ): DBMessage => { 9 | const { content, messageType } = message; 10 | if (messageType !== 'media') return message; 11 | return { 12 | ...message, 13 | content: { 14 | ...content, 15 | body: signImageUrl(message.content.body, { expires }), 16 | }, 17 | }; 18 | }; 19 | -------------------------------------------------------------------------------- /shared/imgix/signUser.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type { DBUser } from 'shared/types'; 3 | import { signImageUrl } from 'shared/imgix'; 4 | 5 | export const signUser = (user: DBUser, expires?: number): DBUser => { 6 | const { profilePhoto, coverPhoto, ...rest } = user; 7 | 8 | return { 9 | ...rest, 10 | profilePhoto: signImageUrl(profilePhoto, { 11 | w: 256, 12 | h: 256, 13 | dpr: 2, 14 | auto: 'compress', 15 | expires, 16 | }), 17 | coverPhoto: signImageUrl(coverPhoto, { 18 | w: 1280, 19 | h: 384, 20 | dpr: 2, 21 | q: 100, 22 | expires, 23 | }), 24 | }; 25 | }; 26 | -------------------------------------------------------------------------------- /shared/middlewares/cors.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import cors from 'cors'; 3 | 4 | export const corsOptions = { 5 | origin: 6 | process.env.NODE_ENV === 'production' && !process.env.FORCE_DEV 7 | ? [ 8 | 'https://spectrum.chat', 9 | 'https://alpha.spectrum.chat', 10 | 'https://admin.spectrum.chat', 11 | 'https://hyperion.workers.spectrum.chat', 12 | 'https://hyperion.alpha.spectrum.chat', 13 | process.env.NOW_URL, 14 | ].filter(Boolean) 15 | : [/localhost/], 16 | credentials: true, 17 | }; 18 | 19 | export default cors(corsOptions); 20 | -------------------------------------------------------------------------------- /shared/middlewares/error-handler.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import Raven from 'shared/raven'; 3 | 4 | export default ( 5 | err: Error, 6 | req: express$Request, 7 | res: express$Response, 8 | next: express$NextFunction 9 | ) => { 10 | if (err) { 11 | console.error(err); 12 | res 13 | .status(500) 14 | .send( 15 | 'Oops, something went wrong! Our engineers have been alerted and will fix this asap.' 16 | ); 17 | Raven.captureException(err); 18 | } else { 19 | return next(); 20 | } 21 | }; 22 | -------------------------------------------------------------------------------- /shared/middlewares/logging.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | // Log requests with debug 3 | const debug = require('debug')('shared:middlewares:logging'); 4 | 5 | module.exports = ( 6 | req: express$Request, 7 | res: express$Response, 8 | next: express$NextFunction 9 | ) => { 10 | if (req.body && req.body.operationName) { 11 | debug(`requesting ${req.url}: ${req.body.operationName}`); 12 | } else { 13 | debug(`requesting ${req.url}`); 14 | } 15 | next(); 16 | }; 17 | -------------------------------------------------------------------------------- /shared/middlewares/raven.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import Raven from 'shared/raven'; 3 | 4 | export default Raven.requestHandler(); 5 | -------------------------------------------------------------------------------- /shared/middlewares/session.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import session from 'cookie-session'; 3 | import { cookieKeygrip } from '../cookie-utils'; 4 | 5 | const ONE_WEEK = 604800000; 6 | 7 | if (!process.env.SESSION_COOKIE_SECRET && !process.env.TEST_DB) { 8 | throw new Error( 9 | '[shared/middlewares/session] You have to provide the SESSION_COOKIE_SECRET environment variable.' 10 | ); 11 | } 12 | 13 | // Create session middleware 14 | export default session({ 15 | keys: cookieKeygrip, 16 | name: 'session', 17 | secure: process.env.NODE_ENV === 'production', 18 | // This is refresh everytime a user does a request 19 | // @see api/routes/middleware/index.js 20 | maxAge: ONE_WEEK, 21 | signed: process.env.TEST_DB ? false : true, 22 | sameSite: 'lax', 23 | }); 24 | -------------------------------------------------------------------------------- /shared/middlewares/statsd.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import statsdMiddleware from 'express-hot-shots'; 3 | import { statsd } from '../statsd'; 4 | 5 | const middleware = statsdMiddleware({ 6 | client: statsd, 7 | }); 8 | 9 | export default ( 10 | req: express$Request, 11 | res: express$Response, 12 | next: express$NextFunction 13 | ) => { 14 | // Set a sensible default req.statsdKey, which is what will be shown in the DataDog UI. Example key: 15 | // hyperion.http.get 16 | // $FlowFixMe 17 | req.statsdKey = `http.${req.method.toLowerCase() || 'unknown_method'}`; 18 | return middleware(req, res, next); 19 | }; 20 | -------------------------------------------------------------------------------- /shared/middlewares/thread-param.js: -------------------------------------------------------------------------------- 1 | // Redirect any route ?thread= or ?t= to /thread/ 2 | 3 | const threadParamRedirect = (req, res, next) => { 4 | const threadId = req.query.thread || req.query.t; 5 | 6 | if (threadId) { 7 | if (req.query.m) { 8 | res.redirect(`/thread/${threadId}?m=${req.query.m}`); 9 | } else { 10 | res.redirect(`/thread/${threadId}`); 11 | } 12 | } else { 13 | next(); 14 | } 15 | }; 16 | 17 | export default threadParamRedirect; 18 | -------------------------------------------------------------------------------- /shared/middlewares/toobusy.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import toobusy from 'toobusy-js'; 3 | 4 | // Middleware which blocks requests when the Node server is too busy 5 | // now automatically retries the request at another instance of the server if it's too busy 6 | export default ( 7 | req: express$Request | http$IncomingMessage, 8 | res: express$Response | http$ServerResponse, 9 | next: express$NextFunction | (() => void) 10 | ) => { 11 | // // Don't send 503s in testing, that's dumb, just wait it out 12 | if (process.env.NODE_ENV !== 'testing' && !process.env.TEST_DB && toobusy()) { 13 | res.statusCode = 503; 14 | res.end( 15 | 'It looks like Spectrum is very busy right now, please try again in a minute.' 16 | ); 17 | } else { 18 | next(); 19 | } 20 | }; 21 | -------------------------------------------------------------------------------- /shared/only-contains-emoji.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import createEmojiRegex from 'emoji-regex'; 3 | import type { RawDraftContentState } from 'draft-js/lib/RawDraftContentState'; 4 | 5 | // This regex matches every string with any emoji in it, not just strings that only have emojis 6 | const originalEmojiRegex = createEmojiRegex(); 7 | 8 | // Make sure we match strings that _only_ contain emojis (and whitespace) 9 | const regex = new RegExp( 10 | '^(' + originalEmojiRegex.toString().replace(/\/g$/, '') + '|\\s)+$' 11 | ); 12 | 13 | const onlyContainsEmoji = (text: string) => regex.test(text); 14 | 15 | // DraftJS-specific check 16 | export const draftOnlyContainsEmoji = (raw: RawDraftContentState) => 17 | raw.blocks.length === 1 && 18 | raw.blocks[0].type === 'unstyled' && 19 | onlyContainsEmoji(raw.blocks[0].text); 20 | 21 | export default onlyContainsEmoji; 22 | -------------------------------------------------------------------------------- /shared/raven/index.js: -------------------------------------------------------------------------------- 1 | require('now-env'); 2 | const debug = require('debug')('shared:raven'); 3 | 4 | let Raven; 5 | if ( 6 | process.env.NODE_ENV === 'production' && 7 | !process.env.FORCE_DEV && 8 | process.env.SENTRY_DSN_SERVER 9 | ) { 10 | Raven = require('raven'); 11 | Raven.config(process.env.SENTRY_DSN_SERVER, { 12 | environment: process.env.NODE_ENV, 13 | name: process.env.SENTRY_NAME, 14 | }).install(); 15 | } else { 16 | const noop = () => {}; 17 | debug('mocking Raven in development'); 18 | // Mock the Raven API in development 19 | Raven = { 20 | captureException: noop, 21 | setUserContext: noop, 22 | config: () => ({ install: noop }), 23 | requestHandler: () => (req, res, next) => next(), 24 | parsers: { 25 | parseRequest: noop, 26 | }, 27 | }; 28 | } 29 | 30 | export default Raven; 31 | -------------------------------------------------------------------------------- /shared/regexps.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | module.exports.MENTIONS = /\/?\B@[a-z0-9._-]+[a-z0-9_-]/gi; 4 | module.exports.URL = /https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,6}\b([-a-zA-Z0-9@:%_\+.~#?&\/=]*)/gi; 5 | module.exports.RELATIVE_URL = /^\/([^\/].*|$)/g; 6 | module.exports.SPECTRUM_URLS = /(?:(?:https?:\/\/)?|\B)(?:spectrum\.chat|localhost:3000)(\/[A-Za-z0-9\-\._~:\/\?#\[\]@!$&'\(\)\*\+,;\=]*)?/gi; 7 | -------------------------------------------------------------------------------- /shared/testing/db.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | module.exports = require('rethinkhaberdashery')({ 3 | db: 'testing', 4 | }); 5 | -------------------------------------------------------------------------------- /shared/testing/empty-db.js: -------------------------------------------------------------------------------- 1 | import data from './data'; 2 | import db from './db'; 3 | const tables = Object.keys(data); 4 | 5 | /** 6 | * This is run after all tests 7 | */ 8 | export const empty = db => { 9 | // Create the tables 10 | return Promise.all( 11 | tables.map(table => 12 | db 13 | .table(table) 14 | .delete() 15 | .run() 16 | ) 17 | ).catch(err => { 18 | console.log(err); 19 | throw err; 20 | }); 21 | }; 22 | 23 | empty(db).then(() => process.exit()); 24 | -------------------------------------------------------------------------------- /shared/testing/setup-test-framework.js: -------------------------------------------------------------------------------- 1 | const mockDb = require('./db'); 2 | 3 | // Wait for 15s before timing out, this is useful for e2e tests which have a tendency to time out 4 | jest.setTimeout(30000); 5 | 6 | // Mock the database 7 | jest.mock('shared/db/db', () => ({ 8 | db: mockDb, 9 | })); 10 | -------------------------------------------------------------------------------- /shared/testing/teardown.js: -------------------------------------------------------------------------------- 1 | // This script gets run once after all tests 2 | // It's responsible for clearing the test database 3 | // NOTE(@mxstbr): While this teardown script could also do mockDb.dbDrop('testing'), that would mean that our API server would crash everytime due to changefeeds dropping, which would be very annoying. Instead we just clear the data from all the tables. 4 | const debug = require('debug')('testing:teardown'); 5 | const mockDb = require('./db'); 6 | const data = require('./data'); 7 | 8 | const tables = Object.keys(data); 9 | 10 | module.exports = () => { 11 | debug(`clearing data in database "testing"`); 12 | return Promise.all( 13 | tables.map(table => 14 | mockDb 15 | .table(table) 16 | .delete() 17 | .run() 18 | ) 19 | ); 20 | }; 21 | -------------------------------------------------------------------------------- /shared/truthy-values.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | export const getTruthyValuesFromObject = (object: Object): Array => { 3 | if (!object) return []; 4 | try { 5 | return Object.keys(object).filter(key => object[key] === true); 6 | } catch (err) { 7 | return []; 8 | } 9 | }; 10 | -------------------------------------------------------------------------------- /spectrum-tmuxp.yaml: -------------------------------------------------------------------------------- 1 | session_name: spectrum 2 | start_directory: ./ 3 | windows: 4 | - window_name: dev 5 | layout: main-vertical 6 | focus: true 7 | panes: 8 | - vim 9 | - echo "Development here" 10 | - window_name: servers 11 | layout: tiled 12 | panes: 13 | - yarn run dev:api 14 | - yarn run dev:web 15 | -------------------------------------------------------------------------------- /src/actions/directMessageThreads.js: -------------------------------------------------------------------------------- 1 | export const initNewThreadWithUser = (user: Object) => { 2 | return { 3 | type: 'ADD_USERS_DIRECT_MESSAGES_COMPOSER', 4 | payload: user, 5 | }; 6 | }; 7 | 8 | export const clearDirectMessagesComposer = () => { 9 | return { 10 | type: 'CLEAR_DIRECT_MESSAGES_COMPOSER', 11 | }; 12 | }; 13 | -------------------------------------------------------------------------------- /src/actions/gallery.js: -------------------------------------------------------------------------------- 1 | //@flow 2 | 3 | /** 4 | * Open the gallery at a certain image 5 | */ 6 | export const openGallery = (threadId: string, messageId: string) => { 7 | return { 8 | type: 'SHOW_GALLERY', 9 | isOpen: true, 10 | threadId, 11 | messageId, 12 | }; 13 | }; 14 | 15 | export const closeGallery = () => { 16 | return { 17 | type: 'HIDE_GALLERY', 18 | }; 19 | }; 20 | -------------------------------------------------------------------------------- /src/actions/modals.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type { ModalTypes } from 'src/components/modals/modalRoot'; 3 | 4 | export const openModal = (name: ModalTypes, props?: Object) => { 5 | return { 6 | type: 'SHOW_MODAL', 7 | modalType: name, 8 | modalProps: props || {}, 9 | }; 10 | }; 11 | 12 | export const closeModal = () => ({ 13 | type: 'HIDE_MODAL', 14 | }); 15 | -------------------------------------------------------------------------------- /src/actions/threadSlider.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | export const openThreadSlider = (threadId: string) => { 4 | return { 5 | type: 'OPEN_SLIDER', 6 | threadId, 7 | }; 8 | }; 9 | 10 | export const closeThreadSlider = () => ({ 11 | type: 'CLOSE_SLIDER', 12 | }); 13 | -------------------------------------------------------------------------------- /src/actions/titlebar.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type { TitlebarPayloadProps } from 'src/views/globalTitlebar'; 3 | 4 | export const setTitlebarProps = (payload: TitlebarPayloadProps) => { 5 | return { 6 | type: 'SET_TITLEBAR_PROPS', 7 | payload, 8 | }; 9 | }; 10 | -------------------------------------------------------------------------------- /src/api/constants.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | export const IS_PROD = process.env.NODE_ENV === 'production'; 3 | 4 | export const SERVER_URL = IS_PROD 5 | ? // In production we want to redirect to /whatever 6 | `` 7 | : // In development we gotta redirect to localhost:3001/whatever tho 8 | 'http://localhost:3001'; 9 | 10 | export const CLIENT_URL = IS_PROD 11 | ? `${window.location.protocol}//${window.location.host}` 12 | : 'http://localhost:3000'; 13 | -------------------------------------------------------------------------------- /src/components/announcementBanner/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import * as React from 'react'; 3 | import Icon from 'src/components/icon'; 4 | import { Bar, Content } from './style'; 5 | class Banner extends React.Component<{}> { 6 | render() { 7 | return ( 8 | 9 | 10 | 11 |

12 | Spectrum is now read-only. Learn more about the decision in our{' '} 13 | 18 | official announcement 19 | 20 | . 21 |

22 |
23 |
24 | ); 25 | } 26 | } 27 | 28 | export default Banner; 29 | -------------------------------------------------------------------------------- /src/components/avatar/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import CommunityAvatar from './communityAvatar'; 3 | import UserAvatar from './userAvatar'; 4 | 5 | export { CommunityAvatar, UserAvatar }; 6 | -------------------------------------------------------------------------------- /src/components/card/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import theme from 'shared/theme'; 3 | import React from 'react'; 4 | import compose from 'recompose/compose'; 5 | import styled from 'styled-components'; 6 | import { FlexCol } from '../globals'; 7 | import { MEDIA_BREAK } from 'src/components/layout'; 8 | 9 | const StyledCard = styled(FlexCol)` 10 | background: ${theme.bg.default}; 11 | position: relative; 12 | width: 100%; 13 | max-width: 100%; 14 | background-clip: padding-box; 15 | overflow: visible; 16 | flex: none; 17 | 18 | @media (max-width: ${MEDIA_BREAK}px) { 19 | border-radius: 0; 20 | box-shadow: none; 21 | } 22 | `; 23 | 24 | const CardPure = (props: Object): React$Element => ( 25 | {props.children} 26 | ); 27 | 28 | export const Card = compose()(CardPure); 29 | export default Card; 30 | -------------------------------------------------------------------------------- /src/components/conditionalWrap/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | // Conditionally wrap a Component in some more JSX 3 | // 4 | // Usage: 5 | // {children}} 8 | // > 9 | // 10 | // 11 | 12 | import type { Node } from 'react'; 13 | 14 | type Props = { 15 | condition: boolean, 16 | wrap: (children: Node) => *, 17 | children: Node, 18 | }; 19 | 20 | function ConditionalWrap({ condition, wrap, children }: Props) { 21 | return condition ? wrap(children) : children; 22 | } 23 | 24 | export default ConditionalWrap; 25 | -------------------------------------------------------------------------------- /src/components/entities/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { UserListItem, CommunityListItem, ChannelListItem } from './listItems'; 3 | import { 4 | ChannelProfileCard, 5 | CommunityProfileCard, 6 | UserProfileCard, 7 | } from './profileCards'; 8 | 9 | export { 10 | UserListItem, 11 | CommunityListItem, 12 | ChannelListItem, 13 | ChannelProfileCard, 14 | CommunityProfileCard, 15 | UserProfileCard, 16 | }; 17 | -------------------------------------------------------------------------------- /src/components/entities/listItems/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { ChannelListItem } from './channel'; 3 | import { UserListItem } from './user'; 4 | import { CommunityListItem } from './community'; 5 | 6 | export { ChannelListItem, UserListItem, CommunityListItem }; 7 | -------------------------------------------------------------------------------- /src/components/entities/profileCards/components/channelCommunityMeta.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React from 'react'; 3 | import { Link } from 'react-router-dom'; 4 | import type { ChannelInfoType } from 'shared/graphql/fragments/channel/channelInfo'; 5 | import { CommunityAvatar } from 'src/components/avatar'; 6 | import { ChannelCommunityMetaRow, ChannelCommunityName } from '../style'; 7 | 8 | type Props = { 9 | channel: ChannelInfoType, 10 | }; 11 | 12 | export const ChannelCommunityMeta = (props: Props) => { 13 | const { channel } = props; 14 | const { community } = channel; 15 | 16 | return ( 17 | 18 | 19 | 20 | {community.name} 21 | 22 | 23 | ); 24 | }; 25 | -------------------------------------------------------------------------------- /src/components/entities/profileCards/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { ChannelProfileCard } from './channel'; 3 | import { UserProfileCard } from './user'; 4 | import { CommunityProfileCard } from './community'; 5 | 6 | export { ChannelProfileCard, CommunityProfileCard, UserProfileCard }; 7 | -------------------------------------------------------------------------------- /src/components/error/BlueScreen.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | // This component is shown as a full replacement for the entire app in production whenever an error happens that would otherwise crash the app 3 | import React from 'react'; 4 | import ViewError from '../viewError'; 5 | 6 | const BlueScreen = () => { 7 | return ( 8 | 15 | ); 16 | }; 17 | 18 | export default BlueScreen; 19 | -------------------------------------------------------------------------------- /src/components/error/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import ErrorBoundary from './ErrorBoundary'; 3 | import SettingsFallback from './SettingsFallback'; 4 | import BlueScreen from './BlueScreen'; 5 | 6 | export { ErrorBoundary, SettingsFallback }; 7 | export default BlueScreen; 8 | -------------------------------------------------------------------------------- /src/components/hoverProfile/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import UserHoverProfile from './userContainer'; 3 | 4 | export { UserHoverProfile }; 5 | -------------------------------------------------------------------------------- /src/components/hoverProfile/loadingHoverProfile.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import * as React from 'react'; 3 | import { Loading } from 'src/components/loading'; 4 | import { HoverWrapper, ProfileCard } from './style'; 5 | 6 | type Props = { 7 | ref?: (?HTMLElement) => void, 8 | style: CSSStyleDeclaration, 9 | }; 10 | 11 | export default class LoadingHoverProfile extends React.Component { 12 | render() { 13 | const { ref, style } = this.props; 14 | 15 | return ( 16 | 17 | 18 | 19 | 20 | 21 | ); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/components/inboxThread/activity.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import * as React from 'react'; 3 | import type { GetThreadType } from 'shared/graphql/queries/thread/getThread'; 4 | import MessageCount from './messageCount'; 5 | import { ThreadActivityWrapper } from './style'; 6 | 7 | type Props = { 8 | currentUser: ?Object, 9 | thread: GetThreadType, 10 | active: boolean, 11 | }; 12 | 13 | class ThreadActivity extends React.Component { 14 | render() { 15 | const { thread, active, currentUser } = this.props; 16 | 17 | if (!thread) return null; 18 | 19 | return ( 20 | 21 | 26 | 27 | ); 28 | } 29 | } 30 | 31 | export default ThreadActivity; 32 | -------------------------------------------------------------------------------- /src/components/inboxThread/header/timestamp.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import * as React from 'react'; 3 | import { timeDifferenceShort } from 'shared/time-difference'; 4 | import { Timestamp } from './style'; 5 | import type { HeaderProps } from './index'; 6 | 7 | class ThreadTimestamp extends React.Component { 8 | render() { 9 | const { thread, active } = this.props; 10 | 11 | const now = new Date().getTime(); 12 | const then = thread.lastActive || thread.createdAt; 13 | let timestamp = timeDifferenceShort(now, new Date(then).getTime()); 14 | // show 'just now' instead of '0s' for new threads 15 | if (timestamp.slice(-1) === 's') { 16 | timestamp = 'Just now'; 17 | } 18 | 19 | return ( 20 | 21 | {timestamp} 22 | 23 | ); 24 | } 25 | } 26 | 27 | export default ThreadTimestamp; 28 | -------------------------------------------------------------------------------- /src/components/inboxThread/messageCount.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import * as React from 'react'; 3 | import type { GetThreadType } from 'shared/graphql/queries/thread/getThread'; 4 | import Icon from 'src/components/icon'; 5 | import { CountWrapper } from './style'; 6 | 7 | type Props = { 8 | currentUser: ?Object, 9 | thread: GetThreadType, 10 | active: boolean, 11 | }; 12 | 13 | class MessageCount extends React.Component { 14 | render() { 15 | const { 16 | thread: { messageCount }, 17 | active, 18 | } = this.props; 19 | 20 | return ( 21 | 22 | 27 | {messageCount} 28 | 29 | ); 30 | } 31 | } 32 | 33 | export default MessageCount; 34 | -------------------------------------------------------------------------------- /src/components/infiniteScroll/deduplicateChildren.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | export const deduplicateChildren = ( 3 | array: Array, 4 | prop: string 5 | ): Array => { 6 | if (!array || !Array.isArray(array) || array.length === 0) return array; 7 | 8 | return array.filter((obj, pos, arr) => { 9 | return arr.map(mapObj => mapObj[prop]).indexOf(obj[prop]) === pos; 10 | }); 11 | }; 12 | -------------------------------------------------------------------------------- /src/components/listItems/channel/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import * as React from 'react'; 3 | import { 4 | ChannelContainer, 5 | ChannelNameLink, 6 | ChannelName, 7 | ChannelActions, 8 | } from './style'; 9 | import type { ChannelInfoType } from 'shared/graphql/fragments/channel/channelInfo'; 10 | 11 | type Props = { 12 | children: React$Node, 13 | channel: ChannelInfoType, 14 | }; 15 | 16 | class ChannelListItem extends React.Component { 17 | render() { 18 | const { channel, children } = this.props; 19 | 20 | return ( 21 | 22 | 23 | {channel.name} 24 | 25 | 26 | {children && {children}} 27 | 28 | ); 29 | } 30 | } 31 | 32 | export default ChannelListItem; 33 | -------------------------------------------------------------------------------- /src/components/loginButtonSet/facebook.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import * as React from 'react'; 3 | import type { ButtonProps } from './'; 4 | import { FacebookButton, Label, A } from './style'; 5 | import Icon from 'src/components/icon'; 6 | 7 | export const FacebookSigninButton = (props: ButtonProps) => { 8 | const { href, preferred, showAfter, onClickHandler } = props; 9 | 10 | return ( 11 | onClickHandler && onClickHandler('facebook')} href={href}> 12 | 13 | 14 | 15 | 16 | 17 | ); 18 | }; 19 | -------------------------------------------------------------------------------- /src/components/loginButtonSet/github.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import * as React from 'react'; 3 | import type { ButtonProps } from './'; 4 | import { GithubButton, Label, A } from './style'; 5 | import Icon from 'src/components/icon'; 6 | 7 | export const GithubSigninButton = (props: ButtonProps) => { 8 | const { href, preferred, showAfter, onClickHandler, githubOnly } = props; 9 | 10 | return ( 11 | onClickHandler && onClickHandler('github')} 14 | href={href} 15 | > 16 | 17 | 18 | 19 | 20 | 21 | ); 22 | }; 23 | -------------------------------------------------------------------------------- /src/components/loginButtonSet/google.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import * as React from 'react'; 3 | import type { ButtonProps } from './'; 4 | import { GoogleButton, Label, A } from './style'; 5 | import Icon from 'src/components/icon'; 6 | 7 | export const GoogleSigninButton = (props: ButtonProps) => { 8 | const { href, preferred, showAfter, onClickHandler } = props; 9 | 10 | return ( 11 | onClickHandler && onClickHandler('google')} href={href}> 12 | 13 | 14 | 15 | 16 | 17 | ); 18 | }; 19 | -------------------------------------------------------------------------------- /src/components/loginButtonSet/twitter.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import * as React from 'react'; 3 | import type { ButtonProps } from './'; 4 | import { TwitterButton, Label, A } from './style'; 5 | import Icon from 'src/components/icon'; 6 | 7 | export const TwitterSigninButton = (props: ButtonProps) => { 8 | const { href, preferred, showAfter, onClickHandler } = props; 9 | 10 | return ( 11 | onClickHandler && onClickHandler('twitter')} href={href}> 12 | 13 | 14 | 15 | 16 | 17 | ); 18 | }; 19 | -------------------------------------------------------------------------------- /src/components/message/messageErrorFallback.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import * as React from 'react'; 3 | import { 4 | Text, 5 | OuterMessageContainer, 6 | GutterContainer, 7 | InnerMessageContainer, 8 | } from './style'; 9 | 10 | class MessageErrorFallback extends React.Component<{}> { 11 | render() { 12 | return ( 13 | 14 | 15 | 16 | 17 | 18 | Something went wrong loading this message. The Spectrum team has 19 | been alerted and will investigate soon. 20 | 21 | 22 | 23 | ); 24 | } 25 | } 26 | 27 | export default MessageErrorFallback; 28 | -------------------------------------------------------------------------------- /src/components/modals/BanUserModal/style.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import styled from 'styled-components'; 3 | import theme from 'shared/theme'; 4 | import { FlexRow } from '../../globals'; 5 | 6 | export const Form = styled.form` 7 | display: flex; 8 | flex-direction: column; 9 | align-self: stretch; 10 | flex: 1 0 auto; 11 | padding: 0 24px 24px; 12 | `; 13 | 14 | export const Actions = styled(FlexRow)` 15 | margin-top: 24px; 16 | justify-content: flex-end; 17 | 18 | button + button { 19 | margin-left: 8px; 20 | } 21 | `; 22 | 23 | export const Subtitle = styled.h3` 24 | font-size: 16px; 25 | font-weight: 400; 26 | color: ${theme.text.alt}; 27 | margin-left: 24px; 28 | margin-top: 16px; 29 | margin-right: 24px; 30 | `; 31 | -------------------------------------------------------------------------------- /src/components/modals/DeleteDoubleCheckModal/style.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | // $FlowFixMe 3 | import styled from 'styled-components'; 4 | import { FlexRow } from '../../globals'; 5 | 6 | export const Actions = styled(FlexRow)` 7 | margin-top: 24px; 8 | padding: 0 24px 24px; 9 | justify-content: flex-end; 10 | 11 | button + button { 12 | margin-left: 8px; 13 | } 14 | `; 15 | 16 | export const Message = styled.div` 17 | line-height: 1.4; 18 | margin: 8px 24px; 19 | 20 | p { 21 | margin-top: 8px; 22 | } 23 | 24 | b { 25 | font-weight: 700; 26 | } 27 | `; 28 | -------------------------------------------------------------------------------- /src/components/reaction/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import * as React from 'react'; 3 | import type { GetMessageType } from 'shared/graphql/queries/message/getMessage'; 4 | import type { Dispatch } from 'redux'; 5 | 6 | type Props = { 7 | dispatch: Dispatch, 8 | currentUser?: Object, 9 | me: boolean, 10 | message: GetMessageType, 11 | render: Function, 12 | }; 13 | 14 | class Reaction extends React.Component { 15 | render() { 16 | const { 17 | me, 18 | message: { 19 | reactions: { hasReacted, count }, 20 | }, 21 | } = this.props; 22 | return this.props.render({ me, hasReacted, count }); 23 | } 24 | } 25 | 26 | export default Reaction; 27 | -------------------------------------------------------------------------------- /src/components/scrollRow/style.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | import { FlexRow } from '../globals'; 3 | 4 | export const ScrollableFlexRow = styled(FlexRow)` 5 | overflow-x: scroll; 6 | flex-wrap: nowrap; 7 | background: transparent; 8 | cursor: pointer; 9 | cursor: hand; 10 | cursor: grab; 11 | 12 | &:active { 13 | cursor: grabbing; 14 | } 15 | `; 16 | -------------------------------------------------------------------------------- /src/components/select/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React from 'react'; 3 | import Icon from 'src/components/icon'; 4 | import { Select, Container, IconContainer } from './style'; 5 | 6 | type Props = { 7 | children: React$Node, 8 | onChange: (evt: SyntheticInputEvent) => void, 9 | defaultValue?: ?string, 10 | }; 11 | 12 | export default (props: Props) => ( 13 | 14 |