diff --git a/docs/bootstrap-application.md b/docs/bootstrap-application.md index b7a697046b..9dffce7e56 100644 --- a/docs/bootstrap-application.md +++ b/docs/bootstrap-application.md @@ -1,7 +1,8 @@ -### Connect your application to Firebase +# Connect your application to Firebase + ## 'Bootstrap' the platform -Make sure that you're located in ./scripts folder. +Make sure that you're located in `./scripts` folder. In order to complete following steps you'd need to log in your Firebase account via CLI. @@ -22,7 +23,7 @@ npx firebase use TODO-PROJECT-ID Now you're ready to proceed. -First of all you'd need to boostrap an environment using the following command: +First of all, you need to boostrap an environment using the following command: * Run the bootstrap script @@ -48,8 +49,7 @@ User created: email=user1@example.com, password=REDACTED User created: email=user2@example.com, password=REDACTED ``` -Once complete you should be able to make that user an admin. To do so run this command: -* Run this +Once complete you should be able to make that user an admin by running: ``` ./update-admin-role-users.ts example-project-firebase-adminsdk-XXXXX-XXXXXXXXXX.json ADD user1@example.com user2@example.com @@ -66,6 +66,7 @@ User successfully added to ( or already existed in ) 'admin' role ## Update Firestore rules + Please run the following command: ``` @@ -84,7 +85,7 @@ Deploy Completed! ## Configure ENV file -In order for your application to connect to the proper Firebase environment you'd need to set up `.env.local` config. Please follow steps below: +In order for your application to connect to the proper Firebase environment you'll need to set up `.env.local` config. Please follow steps below: * Locate or create `.env.local` file inside project root folder * Paste the following config @@ -97,10 +98,8 @@ REACT_APP_MEASUREMENT_ID= REACT_APP_BUCKET_URL= ``` -You can find all of these values in the Firebase - Project Settings - General tab. - -REACT_APP_MEASUREMENT_ID can be found at https://analytics.google.com/analytics/web. You can search for Measurement ID in the search bar at the top of the page +You can find all of these values in the Firebase - _Project Settings_ - _General_ tab. -Once you've populated ENV file with correct data, please proceed to the next part where you will be able to launch your project. +Once you've populated the ENV file with correct data, please proceed to the next part where you will be able to launch your project. -Proceed to [Getting Started](docs/getting-started.md) to launch your application. +Proceed to [Getting Started](getting-started.md) to launch your application. diff --git a/docs/create-new-environment.md b/docs/create-new-environment.md index a235660646..b48747d09d 100644 --- a/docs/create-new-environment.md +++ b/docs/create-new-environment.md @@ -1,4 +1,5 @@ -### Firebase Project Setup +## Firebase Project Setup + ### Step 1: Create New Firebase Project 1. Go to https://console.firebase.google.com @@ -18,6 +19,7 @@ This part of the setup is complete! ### Step 2: Configure Firebase Project Settings + 1. Go to https://console.firebase.google.com/ and find the `Example Project` you chose in step 1 2. From _Project Overview_, hover over the gear icon and click _Project Settings_ @@ -48,6 +50,7 @@ This part of the setup is complete! This part of the setup is now complete! ### Step 4: Set up Firebase Hosting + 1. From the Firebase console, within the appropriate project, click on _Hosting_ on the left hand menu. 2. Click on _Get started_ @@ -95,7 +98,7 @@ This part of the setup is complete! ### Step 6: Set up Twilio Account -Please see [Twilio Account Setup](/docs/twilio-configuration.md) +Please see [Twilio Account Setup](twilio-configuration.md) ### Step 7: Generate Private Key File @@ -107,7 +110,7 @@ Before you run the following steps, you will need to ensure you have access to t In a new terminal, from the directory you cloned the code to, enter the following commands: -``` +```bash # While not necessary (as we already include it in our devDependencies), you can install the firebase-tools globally if desired # npm install -g firebase-tools@latest @@ -132,20 +135,19 @@ npx firebase use TODO-PROJECT-ID Now you need to set up the function config -``` +```bash npx firebase --project TODO-PROJECT-ID functions:config:set project.id=TODO-PROJECT-ID twilio.account_sid=TODO twilio.api_key=TODO twilio.api_secret=TODO stripe.endpoint_secret=TODO stripe.secret_key=TODO ``` # Go to /scripts -Now we need to upload function config to Firebase. Use the .json file with private key that you've downloaded just recently: +Now we need to upload function config to Firebase. Use the `.json` file with private key that you've downloaded just recently: -``` +```bash ./upload-function-config-service-account.ts TODO-PROJECT-ID example-project-firebase-adminsdk-XXXXX-XXXXXXXXXX.json ``` -``` - +```bash # Copy the runtime config locally npx firebase functions:config:get > ./functions/.runtimeconfig.json @@ -166,4 +168,4 @@ This part of the setup is complete! In order to run Sparkle you'd need to bootstrap and connect your local application with the Firebase environment that you've created. Please follow the link below for detailed information. -See [Bootstrap application](docs/bootstrap-application.md) +See [Bootstrap application](bootstrap-application.md) diff --git a/docs/firebase-emulators.md b/docs/firebase-emulators.md index b1aa5b620c..928d9b4ac8 100644 --- a/docs/firebase-emulators.md +++ b/docs/firebase-emulators.md @@ -1,19 +1,20 @@ ### Firebase Emulators -Instead of running just the functions' emulator, the full suite of emulators can be used. -You can find out more at https://firebase.google.com/docs/emulator-suite. +Instead of running just the functions emulator, the full suite of emulators can be used. +You can find out more at https://firebase.google.com/docs/emulator-suite -**Note**: If your code accidentally invokes non-emulated (production) resources, there is a chance of data change, usage and billing. -To prevent this, you might opt in to use a Firebase project name beginning with `demo-` (e.g. `demo-staging`) in which case no production resources will be used. +**Note**: If your code accidentally invokes non-emulated (production) resources, there is a chance of data change, usage and billing. To prevent this, you might opt in to use a Firebase project name beginning with `demo-` (e.g. `demo-staging`) in which case no production resources will be used. This might entail some code changes as well, to enable the emulation, e.g. +In `src/index.tsx`: ```typescript // Enable the functions emulator when running in development at specific port if (process.env.NODE_ENV === "development" && window.location.port === "5000") { firebaseApp.firestore().useEmulator("localhost", 8080); } ``` + or to account for changed and/or deprecated Firebase client API, e.g. ```typescript @@ -37,7 +38,8 @@ All emulators ready! View status and logs at http://localhost:4000 ``` That's the location you can access and manage the running emulators. -The data between emulator runs should be persisted at + +If run as above, the data between emulator runs should be persisted at: ```bash tmp/firebase-export-metadata.json diff --git a/docs/getting-started.md b/docs/getting-started.md index 27a9f9b7ec..86b4f701af 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -4,31 +4,33 @@ If you don't have a working Firebase environment please follow the guide below: -[Firebase setup](docs/create-new-environment.md) +[Firebase setup](create-new-environment.md) In case you didn't connect your application to the Firebase environment, then this guide should help: -[Bootstrap application](docs/bootstrap-application.md) +[Bootstrap application](bootstrap-application.md) Otherwise, if you've got through these steps, please follow the guide below to successfully tart your application. ### Launch application You're going to need 2 terminal tabs to launch Sparkle application locally. Make sure that you're located in the application root folder and follow steps below: + * Run `npm run firebase:emulate-functions` in the first tab * Run `npm run start` in the second Once the launch is complete you may proceed to http://localhost:3000/v/bootstrap -You can register a new user by pressing Log In in the top right corner: +You can register a new user by pressing _Log In_ in the top right corner: -Once redirected to authorization form select Create account: -* Fill in necessary data and press Continue +Once redirected to authorization form select _Create account_: -* Fill in your Username and upload custom avatar, or select from the default ones. Then press Create my profile: +* Fill in necessary data and press _Continue_ +* Fill in your Username and upload custom avatar, or select from the default ones. Then press _Create my profile_: Navigate to http://localhost:3000/v/bootstrap where you can enter your first venue. Now you may also visit Admin dashboard to edit venues, create new ones or manage users. -* https://example.sparkle.space/admin -* https://example.sparkle.space/admin_v2 + +* http://localhost:3000/admin +* http://localhost:3000/admin-ng diff --git a/docs/twilio-configuration.md b/docs/twilio-configuration.md index 925e1bb4b0..d3f3fdd306 100644 --- a/docs/twilio-configuration.md +++ b/docs/twilio-configuration.md @@ -34,4 +34,4 @@ Login to Twilio This part of the setup is done! -Please go back to the Step 7 at [Firebase Project Setup](docs/create-new-environment.md) +Please go back to the Step 7 at [Firebase Project Setup](create-new-environment.md) diff --git a/functions/.runtimeconfig.json.example b/functions/.runtimeconfig.json.example index c55624394f..df67d46a44 100644 --- a/functions/.runtimeconfig.json.example +++ b/functions/.runtimeconfig.json.example @@ -14,5 +14,8 @@ "agora": { "app_id": "", "app_certificate": "" + }, + "flag": { + "autoadmin": "" } } diff --git a/functions/auth.js b/functions/auth.js index 68ae7726fb..8dbb7e693a 100644 --- a/functions/auth.js +++ b/functions/auth.js @@ -4,6 +4,7 @@ const admin = require("firebase-admin"); const { HttpsError } = require("firebase-functions/lib/providers/https"); const { fetchAuthConfig } = require("./src/api/auth"); +const { addAdmin } = require("./src/api/roles"); const { assertValidUrl, assertValidVenueId } = require("./src/utils/assert"); const { createOAuth2Client } = require("./src/utils/auth"); @@ -225,3 +226,35 @@ exports.connectI4AOAuthHandler = functions.https.onRequest(async (req, res) => { res.redirect(customTokenReturnUrl.toString()); }); + +/** Automatically make user admin upon register. + * + * A function that triggers when a Firebase user is created, not on https request + * + * Firebase accounts will trigger user creation events for Cloud Functions when: + * - A user creates an email account and password. + * - A user signs in for the first time using a federated identity provider. + * - The developer creates an account using the Firebase Admin SDK. + * - A user signs in to a new anonymous auth session for the first time. + * + * NOTE: A Cloud Functions event is not triggered when a user signs in for the first time using a custom token. + * + * @see https://firebase.google.com/docs/functions/auth-events + */ +exports.autoAdminOnRegister = functions.auth.user().onCreate(async (user) => { + const flag = functions.config().flag || {}; + + if (flag.autoadmin) { + functions.logger.log( + "flag.autoadmin is", + flag.autoadmin, + "adding user.uid", + user.uid, + "with email", + user.email, + "to the admin role" + ); + + await addAdmin(user.uid); + } +}); diff --git a/functions/src/api/roles.js b/functions/src/api/roles.js new file mode 100644 index 0000000000..1205333e44 --- /dev/null +++ b/functions/src/api/roles.js @@ -0,0 +1,32 @@ +const admin = require("firebase-admin"); + +/** Remove a user from the list of admins + * + * @param {string} adminId + */ +const removeAdmin = async (adminId) => { + await admin + .firestore() + .collection("roles") + .doc("admin") + .update({ + users: admin.firestore.FieldValue.arrayRemove(adminId), + }); +}; + +/** Add a user to the list of admins + * + * @param {string} newAdminId + */ +const addAdmin = async (newAdminId) => { + await admin + .firestore() + .collection("roles") + .doc("admin") + .update({ + users: admin.firestore.FieldValue.arrayUnion(newAdminId), + }); +}; + +exports.addAdmin = addAdmin; +exports.removeAdmin = removeAdmin; diff --git a/functions/venue.js b/functions/venue.js index a8186b25b4..d6c0f310d2 100644 --- a/functions/venue.js +++ b/functions/venue.js @@ -2,6 +2,8 @@ const admin = require("firebase-admin"); const functions = require("firebase-functions"); const { HttpsError } = require("firebase-functions/lib/providers/https"); +const { addAdmin, removeAdmin } = require("./src/api/roles"); + const { checkAuth } = require("./src/utils/assert"); const { getVenueId, checkIfValidVenueId } = require("./src/utils/venue"); @@ -360,10 +362,6 @@ const createBaseUpdateVenueData = (data, updated) => { updated.showBadges = data.showBadges; } - if (typeof data.showZendesk === "boolean") { - updated.showZendesk = data.showZendesk; - } - if (typeof data.showRangers === "boolean") { updated.showRangers = data.showRangers; } @@ -408,34 +406,6 @@ const dataOrUpdateKey = (data, updated, key) => typeof updated[key] !== "undefined" && updated[key]); -/** Add a user to the list of admins - * - * @param {string} newAdminId - */ -const addAdmin = async (newAdminId) => { - await admin - .firestore() - .collection("roles") - .doc("admin") - .update({ - users: admin.firestore.FieldValue.arrayUnion(newAdminId), - }); -}; - -/** Remove a user from the list of admins - * - * @param {string} adminId - */ -const removeAdmin = async (adminId) => { - await admin - .firestore() - .collection("roles") - .doc("admin") - .update({ - users: admin.firestore.FieldValue.arrayRemove(adminId), - }); -}; - exports.addVenueOwner = functions.https.onCall(async (data, context) => { checkAuth(context); diff --git a/package-lock.json b/package-lock.json index cee4a12e6b..accda5c43d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5867,11 +5867,6 @@ "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.4.tgz", "integrity": "sha512-1z8k4wzFnNjVK/tlxvrWuK5WMt6mydWWP7+zvH5eFep4oj+UkrfiJTRtjCeBXNpwaA/FYqqtb4/QS4ianFpIRA==" }, - "@types/mixpanel-browser": { - "version": "2.35.4", - "resolved": "https://registry.npmjs.org/@types/mixpanel-browser/-/mixpanel-browser-2.35.4.tgz", - "integrity": "sha512-Qfr9XChsDsqAo5FM9IGQffbelX1w0xonSTUcsW2SXQeOORgDDA4bSTNZstFPxTpU5FYj+nNjjqmII6bCPvod5A==" - }, "@types/mousetrap": { "version": "1.6.4", "resolved": "https://registry.npmjs.org/@types/mousetrap/-/mousetrap-1.6.4.tgz", @@ -10166,9 +10161,9 @@ "optional": true }, "date-fns": { - "version": "2.21.1", - "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.21.1.tgz", - "integrity": "sha512-m1WR0xGiC6j6jNFAyW4Nvh4WxAi4JF4w9jRJwSI8nBmNcyZXPcP9VUQG+6gHQXAmqaGEKDKhOqAtENDC941UkA==" + "version": "2.22.1", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.22.1.tgz", + "integrity": "sha512-yUFPQjrxEmIsMqlHhAhmxkuH769baF21Kk+nZwZGyrMoyLA+LugaQtC0+Tqf9CBUUULWwUJt6Q5ySI3LJDDCGg==" }, "dayjs": { "version": "1.8.35", @@ -19414,16 +19409,6 @@ "resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.7.1.tgz", "integrity": "sha512-Hesni4s5UkWkwCGJMQGAh71PaLUmKFM60dHvq0zi/vDhhrzuk+4GgNbTXJ12YYQJn6ZKBDNIjYcuQGKudvqrIw==" }, - "logrocket": { - "version": "1.0.14", - "resolved": "https://registry.npmjs.org/logrocket/-/logrocket-1.0.14.tgz", - "integrity": "sha512-notwwiIiXOmWSKQDsW8UrFJPu81u9rd6YaIFBmx6uF0XtXXwNQ+Mvteh5WHdABWcQ2nN4I7QkQrCAocYDx7OVg==" - }, - "logrocket-react": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/logrocket-react/-/logrocket-react-4.0.1.tgz", - "integrity": "sha512-TbQELEaDQspKHH0XylAeVZpsHoAcz+sIEG/enUQiH+LoYLW7cMK000XIS6W3Zz14zS0V2Db+KyKi8ivBpGYkzg==" - }, "long": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz", @@ -20095,11 +20080,6 @@ } } }, - "mixpanel-browser": { - "version": "2.40.0", - "resolved": "https://registry.npmjs.org/mixpanel-browser/-/mixpanel-browser-2.40.0.tgz", - "integrity": "sha512-lhBsxIP9FX3ZJlOICgCzkFjfOKZ4T1Z5MfW8duYbb1kq7Lc4jSuJF2kTiuDISubAiZhsUXUwrGNATjeGjoXJWw==" - }, "mkdirp": { "version": "0.5.5", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", diff --git a/package.json b/package.json index 840d8f5590..4a547e2565 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,6 @@ "@testing-library/jest-dom": "^4.2.4", "@testing-library/react": "^9.5.0", "@testing-library/user-event": "^7.2.1", - "@types/mixpanel-browser": "^2.35.4", "@types/mousetrap": "^1.6.4", "@types/qs": "^6.9.3", "@types/react-resize-detector": "^4.2.0", @@ -27,7 +26,7 @@ "bootstrap": "^4.4.1", "bootswatch": "^4.5.0", "classnames": "^2.2.6", - "date-fns": "^2.21.1", + "date-fns": "^2.22.1", "dayjs": "^1.8.35", "emoji-mart": "^3.0.1", "esm": "^3.2.25", @@ -40,9 +39,6 @@ "jsonschema": "^1.2.6", "lint-staged": "^10.2.7", "lodash": "^4.17.15", - "logrocket": "^1.0.14", - "logrocket-react": "^4.0.1", - "mixpanel-browser": "^2.40.0", "mousetrap": "^1.6.5", "npm-force-resolutions": "0.0.3", "popper.js": "^1.16.1", diff --git a/src/api/admin.ts b/src/api/admin.ts index e4316e8d66..9876f13dac 100644 --- a/src/api/admin.ts +++ b/src/api/admin.ts @@ -110,7 +110,6 @@ export type VenueInput = AdvancedVenueInput & showRadio?: boolean; radioStations?: string; showNametags?: UsernameVisibility; - showZendesk?: boolean; showUserStatus?: boolean; requestToJoinStage?: boolean; }; @@ -238,11 +237,6 @@ const createFirestoreVenueInput = async (input: VenueInput, user: UserInfo) => { rooms: [], // eventually we will be getting the rooms from the form }; - // Default to showing Zendesk - if (input.showZendesk === undefined) { - input.showZendesk = true; - } - return firestoreVenueInput; }; diff --git a/src/components/atoms/UserAvatar/UserAvatar.tsx b/src/components/atoms/UserAvatar/UserAvatar.tsx index bd1b5b6bd2..b32bcaa080 100644 --- a/src/components/atoms/UserAvatar/UserAvatar.tsx +++ b/src/components/atoms/UserAvatar/UserAvatar.tsx @@ -1,5 +1,6 @@ import React, { useMemo } from "react"; import classNames from "classnames"; +import { isEqual } from "lodash"; import { DEFAULT_PARTY_NAME, DEFAULT_PROFILE_IMAGE } from "settings"; @@ -25,7 +26,7 @@ export interface UserAvatarProps { } // @debt the UserProfilePicture component serves a very similar purpose to this, we should unify them as much as possible -export const UserAvatar: React.FC = ({ +export const _UserAvatar: React.FC = ({ user, containerClassName, imageClassName, @@ -36,7 +37,9 @@ export const UserAvatar: React.FC = ({ medium, }) => { const venueId = useVenueId(); + const { recentWorldUsers } = useRecentWorldUsers(); + const { userStatus, venueUserStatuses, @@ -108,3 +111,5 @@ export const UserAvatar: React.FC = ({ ); }; + +export const UserAvatar = React.memo(_UserAvatar, isEqual); diff --git a/src/components/molecules/ChatMessageBox/ChatMessageBox.tsx b/src/components/molecules/ChatMessageBox/ChatMessageBox.tsx index cf347d7806..28d3b9e13b 100644 --- a/src/components/molecules/ChatMessageBox/ChatMessageBox.tsx +++ b/src/components/molecules/ChatMessageBox/ChatMessageBox.tsx @@ -7,7 +7,7 @@ import { faPaperPlane, faSmile } from "@fortawesome/free-solid-svg-icons"; import { CHAT_MESSAGE_TIMEOUT } from "settings"; -import { MessageToDisplay, SendChatReply, SendMessage } from "types/chat"; +import { MessageToDisplay, SendChatReplyProps, SendMessage } from "types/chat"; import { WithId } from "utils/id"; @@ -22,16 +22,16 @@ import "./ChatMessageBox.scss"; export interface ChatMessageBoxProps { selectedThread?: WithId; sendMessage: SendMessage; - sendThreadReply: SendChatReply; unselectOption: () => void; isQuestion?: boolean; + onReplyToThread: (data: SendChatReplyProps) => void; } export const ChatMessageBox: React.FC = ({ selectedThread, sendMessage, - sendThreadReply, unselectOption, + onReplyToThread, isQuestion = false, }) => { const hasChosenThread = selectedThread !== undefined; @@ -74,9 +74,8 @@ export const ChatMessageBox: React.FC = ({ if (!selectedThread) return; setMessageSending(true); - sendThreadReply({ replyText: message, threadId: selectedThread.id }); + onReplyToThread({ replyText: message, threadId: selectedThread.id }); reset(); - unselectOption(); }); const { diff --git a/src/components/molecules/ChatPoll/ChatPoll.tsx b/src/components/molecules/ChatPoll/ChatPoll.tsx index c0f16874a7..b5693cfdeb 100644 --- a/src/components/molecules/ChatPoll/ChatPoll.tsx +++ b/src/components/molecules/ChatPoll/ChatPoll.tsx @@ -1,4 +1,5 @@ import React, { useCallback, useMemo } from "react"; +import { useAsyncFn } from "react-use"; import classNames from "classnames"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { faPoll } from "@fortawesome/free-solid-svg-icons"; @@ -17,11 +18,13 @@ import { WithId } from "utils/id"; import { useRoles } from "hooks/useRoles"; import { useUser } from "hooks/useUser"; +import { RenderMarkdown } from "components/organisms/RenderMarkdown"; + +import { Loading } from "components/molecules/Loading"; + import { ChatMessageInfo } from "components/atoms/ChatMessageInfo"; import Button from "components/atoms/Button"; -import { RenderMarkdown } from "components/organisms/RenderMarkdown"; - import "./ChatPoll.scss"; export interface ChatPollProps { @@ -65,8 +68,8 @@ export const ChatPoll: React.FC = ({ "ChatPoll--me": isMine, }); - const handleVote = useCallback( - (question) => + const [{ loading: isVoting }, handleVote] = useAsyncFn( + async (question) => voteInPoll({ questionId: question.id, pollId: id, @@ -82,7 +85,12 @@ export const ChatPoll: React.FC = ({ customClass="ChatPoll__question" onClick={() => handleVote(question)} > - + )), [questions, handleVote] @@ -115,11 +123,28 @@ export const ChatPoll: React.FC = ({ style={{ width: `${question.share}%` }} /> {question.share}% - + )); }, [questions, calculateVotePercentage]); + const renderPollContent = () => { + if (isVoting) { + return ; + } + + if (hasVoted || isMine) { + return renderResults; + } + + return renderQuestions; + }; + const deleteThisPollMessage = useCallback(() => deletePollMessage(id), [ id, deletePollMessage, @@ -132,7 +157,8 @@ export const ChatPoll: React.FC = ({
-
{isMine || hasVoted ? renderResults : renderQuestions}
+ {renderPollContent()} +
{`${votes.length} votes`}
diff --git a/src/components/molecules/Chatbox/Chatbox.tsx b/src/components/molecules/Chatbox/Chatbox.tsx index ab351d723f..370413bc6d 100644 --- a/src/components/molecules/Chatbox/Chatbox.tsx +++ b/src/components/molecules/Chatbox/Chatbox.tsx @@ -1,4 +1,5 @@ import React, { useMemo, useState, useCallback } from "react"; +import { isEqual } from "lodash"; import { DeleteMessage, @@ -33,7 +34,7 @@ export interface ChatboxProps { displayPoll?: boolean; } -export const Chatbox: React.FC = ({ +export const _Chatbox: React.FC = ({ messages, venue, sendMessage, @@ -89,6 +90,15 @@ export const Chatbox: React.FC = ({ [messages, deleteMessage, voteInPoll, venue] ); + const onReplyToThread = useCallback( + ({ replyText, threadId }) => { + sendThreadReply({ replyText, threadId }); + unselectOption(); + closeThread(); + }, + [unselectOption, closeThread, sendThreadReply] + ); + return (
{renderedMessages}
@@ -119,12 +129,14 @@ export const Chatbox: React.FC = ({ )}
); }; + +export const Chatbox = React.memo(_Chatbox, isEqual); diff --git a/src/components/molecules/Footer/Footer.tsx b/src/components/molecules/Footer/Footer.tsx index 0ea124546c..4f1c1db3d7 100644 --- a/src/components/molecules/Footer/Footer.tsx +++ b/src/components/molecules/Footer/Footer.tsx @@ -1,8 +1,8 @@ import React from "react"; import { - HOMEPAGE_URL, PRIVACY_POLICY, + SPARKLE_FOOTER_URL, TERMS_AND_CONDITIONS_URL, } from "settings"; import { getExtraLinkProps } from "utils/url"; @@ -24,7 +24,7 @@ export const Footer = () => (
|
- + Made with{" "} ❤️ diff --git a/src/components/molecules/ProfilePictureInput/ProfilePictureInput.tsx b/src/components/molecules/ProfilePictureInput/ProfilePictureInput.tsx index 4a521a6f5c..aa3dbce466 100644 --- a/src/components/molecules/ProfilePictureInput/ProfilePictureInput.tsx +++ b/src/components/molecules/ProfilePictureInput/ProfilePictureInput.tsx @@ -2,7 +2,7 @@ import React, { useEffect, useRef, useState } from "react"; import { useFirebase } from "react-redux-firebase"; import { UserInfo } from "firebase/app"; import { FirebaseStorage } from "@firebase/storage-types"; -import { externalUrlAdditionalProps } from "utils/url"; +import "firebase/storage"; import { ACCEPTED_IMAGE_TYPES, @@ -10,6 +10,7 @@ import { } from "settings"; import { resizeFile } from "utils/image"; +import { externalUrlAdditionalProps } from "utils/url"; import "./ProfilePictureInput.scss"; diff --git a/src/components/molecules/ScheduleEvent/ScheduleEvent.scss b/src/components/molecules/ScheduleEvent/ScheduleEvent.scss index c0162ac797..2b98e6625f 100644 --- a/src/components/molecules/ScheduleEvent/ScheduleEvent.scss +++ b/src/components/molecules/ScheduleEvent/ScheduleEvent.scss @@ -2,23 +2,25 @@ $ScheduleEvent--margin-top: 0.25rem; $ScheduleEvent--height: 3.75rem; -$ScheduleEvent--padding: 0 0.25rem 0 0.75rem; +$ScheduleEvent--padding--right: 0.25rem; +$ScheduleEvent--padding--left: 0.75rem; $ScheduleEvent--border-radius: 18px; $ScheduleEvent--box-shadow: 0 5px 10px rgba(0, 0, 0, 0.65); $bookmark--padding: 0.5rem; +$expand--margin: 0.3rem; $bookmark-hover--padding-top: 0.375rem; +$expand-hover--margin-top: 0.375rem; $description--margin-top: 2px; +$expand--width: 40px; .ScheduleEvent { display: flex; position: absolute; border-radius: $ScheduleEvent--border-radius; - padding: $ScheduleEvent--padding; cursor: pointer; height: $ScheduleEvent--height; - justify-content: space-between; align-items: center; background-color: $secondary--schedule-event; box-shadow: $ScheduleEvent--box-shadow; @@ -44,6 +46,49 @@ $description--margin-top: 2px; } } + &--expandable { + &:hover { + width: var(--event--expanded-width); + } + } + + &--short { + & .ScheduleEvent__info { + display: none; + } + + & .ScheduleEvent__bookmark { + display: none; + } + + &:hover { + & .ScheduleEvent__info { + display: block; + } + + & .ScheduleEvent__bookmark { + display: flex; + } + + & .ScheduleEvent__expand { + margin: 0; + display: flex; + width: $expand--width; + padding: $spacing--lg; + } + + & .ScheduleEvent__expand--arrows { + display: none; + } + + & .ScheduleEvent__expand--arrows--out { + display: inline-block; + margin-top: $spacing--xs; + margin-left: $spacing--xs; + } + } + } + &--live { background-color: $primary--live; color: $white; @@ -60,6 +105,8 @@ $description--margin-top: 2px; &__info { font-size: $font-size--sm; overflow: hidden; + padding-left: $ScheduleEvent--padding--left; + padding-right: $ScheduleEvent--padding--right; } &__title { @@ -83,9 +130,59 @@ $description--margin-top: 2px; justify-content: center; align-items: center; padding: $bookmark--padding; + margin-left: auto; &:hover { padding-top: $bookmark-hover--padding-top; } } + + &__expand { + color: $concrete--dark; + border: none; + background-color: transparent; + + &:focus { + outline: none; + } + + &--hidden { + display: none; + } + + &--square { + position: absolute; + height: $spacing--xl; + } + + &--arrows { + height: $spacing--lg; + margin-top: $spacing--xs; + + &--out { + display: none; + } + } + + &--marged { + margin: auto; + } + + &--padded { + padding-left: $ScheduleEvent--padding--left; + padding-right: $ScheduleEvent--padding--right; + } + + &--live { + color: $white; + } + } + + & .ScheduleEvent__expand--square { + width: $spacing--xl; + } + + & .ScheduleEvent__expand--arrows { + width: $spacing--xl; + } } diff --git a/src/components/molecules/ScheduleEvent/ScheduleEvent.tsx b/src/components/molecules/ScheduleEvent/ScheduleEvent.tsx index c22a45dfd1..01687760cd 100644 --- a/src/components/molecules/ScheduleEvent/ScheduleEvent.tsx +++ b/src/components/molecules/ScheduleEvent/ScheduleEvent.tsx @@ -1,12 +1,25 @@ import React, { MouseEventHandler, useCallback } from "react"; import classNames from "classnames"; import { useCss } from "react-use"; +import { minutesToHours } from "date-fns"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { faBookmark as solidBookmark } from "@fortawesome/free-solid-svg-icons"; -import { faBookmark as regularBookmark } from "@fortawesome/free-regular-svg-icons"; +import { + faBookmark as solidBookmark, + faExpandAlt as solidExpand, + faCompressAlt as solidCompress, +} from "@fortawesome/free-solid-svg-icons"; +import { + faBookmark as regularBookmark, + faSquare as regularSquare, +} from "@fortawesome/free-regular-svg-icons"; -import { SCHEDULE_HOUR_COLUMN_WIDTH_PX } from "settings"; +import { + SCHEDULE_HOUR_COLUMN_WIDTH_PX, + SCHEDULE_LONG_EVENT_LENGTH_MIN, + SCHEDULE_MEDIUM_EVENT_LENGTH_MIN, + SCHEDULE_SHORT_EVENT_LENGTH_MIN, +} from "settings"; import { addEventToPersonalizedSchedule, @@ -16,7 +29,6 @@ import { import { PersonalizedVenueEvent } from "types/venues"; import { isEventLive } from "utils/event"; -import { ONE_HOUR_IN_MINUTES } from "utils/time"; import { useUser } from "hooks/useUser"; import { useShowHide } from "hooks/useShowHide"; @@ -41,10 +53,13 @@ export const ScheduleEvent: React.FC = ({ }) => { const { userId } = useUser(); - // @debt ONE_HOUR_IN_MINUTES is deprectated; refactor to use utils/time or date-fns functions - const eventWidthPx = - (event.duration_minutes * SCHEDULE_HOUR_COLUMN_WIDTH_PX) / - ONE_HOUR_IN_MINUTES; + const eventWidthPx = minutesToHours( + event.duration_minutes * SCHEDULE_HOUR_COLUMN_WIDTH_PX + ); + + const expandedEventPx = + minutesToHours(SCHEDULE_LONG_EVENT_LENGTH_MIN) * + SCHEDULE_HOUR_COLUMN_WIDTH_PX; const eventMarginLeftPx = calcStartPosition( event.start_utc_seconds, @@ -54,17 +69,33 @@ export const ScheduleEvent: React.FC = ({ const containerCssVars = useCss({ "--event--margin-left": `${eventMarginLeftPx}px`, "--event--width": `${eventWidthPx}px`, + "--event--expanded-width": `${expandedEventPx}px`, }); + const isEventFullLength = + event.duration_minutes < SCHEDULE_LONG_EVENT_LENGTH_MIN; + const isEventLong = event.duration_minutes > SCHEDULE_MEDIUM_EVENT_LENGTH_MIN; + const isEventShort = + event.duration_minutes <= SCHEDULE_SHORT_EVENT_LENGTH_MIN; + const containerClasses = classNames( "ScheduleEvent", { "ScheduleEvent--live": isEventLive(event), "ScheduleEvent--users": isPersonalizedEvent, + "ScheduleEvent--short": !isEventLong, + "ScheduleEvent--expandable": isEventFullLength, }, containerCssVars ); + const expandClasses = classNames("ScheduleEvent__expand", { + "ScheduleEvent__expand--hidden": isEventShort, + "ScheduleEvent__expand--marged": !isEventLong, + "ScheduleEvent__expand--padded": isEventLong, + "ScheduleEvent__expand--live": isEventLive(event), + }); + const bookmarkEvent: MouseEventHandler = useCallback(() => { if (!userId || !event.id) return; @@ -91,12 +122,27 @@ export const ScheduleEvent: React.FC = ({ return ( <>
+ +
{event.name}
by {event.host}
-
+
diff --git a/src/components/molecules/TablesControlBar/TablesControlBar.scss b/src/components/molecules/TablesControlBar/TablesControlBar.scss new file mode 100644 index 0000000000..f8ef2ce60b --- /dev/null +++ b/src/components/molecules/TablesControlBar/TablesControlBar.scss @@ -0,0 +1,12 @@ +@import "scss/constants.scss"; + +$control-bar-height: 40px; + +.TablesControlBar { + height: $control-bar-height; + margin-top: $spacing--md; + + &__checkbox { + justify-content: start; + } +} diff --git a/src/components/molecules/TablesControlBar/TablesControlBar.tsx b/src/components/molecules/TablesControlBar/TablesControlBar.tsx new file mode 100644 index 0000000000..9787c31287 --- /dev/null +++ b/src/components/molecules/TablesControlBar/TablesControlBar.tsx @@ -0,0 +1,31 @@ +import React from "react"; +import classNames from "classnames"; + +import { Checkbox } from "components/atoms/Checkbox"; + +import "./TablesControlBar.scss"; + +export interface TablesControlBarProps { + showOnlyAvailableTables: boolean; + onToggleAvailableTables: () => void; + containerClassName?: string; +} + +export const TablesControlBar: React.FC = ({ + showOnlyAvailableTables, + onToggleAvailableTables, + containerClassName, +}) => { + const containerClasses = classNames("TablesControlBar", containerClassName); + + return ( +
+ +
+ ); +}; diff --git a/src/components/molecules/TablesControlBar/index.ts b/src/components/molecules/TablesControlBar/index.ts new file mode 100644 index 0000000000..5569d15af2 --- /dev/null +++ b/src/components/molecules/TablesControlBar/index.ts @@ -0,0 +1 @@ +export { TablesControlBar } from "./TablesControlBar"; diff --git a/src/components/molecules/TablesUserList/TablesUserList.tsx b/src/components/molecules/TablesUserList/TablesUserList.tsx index fcf411ed06..1845ec394c 100644 --- a/src/components/molecules/TablesUserList/TablesUserList.tsx +++ b/src/components/molecules/TablesUserList/TablesUserList.tsx @@ -15,6 +15,7 @@ import { User } from "types/User"; import { experienceSelector } from "utils/selectors"; import { isTruthy } from "utils/types"; import { getUserExperience } from "utils/user"; +import { WithId } from "utils/id"; import { useSelector } from "hooks/useSelector"; import { useShowHide } from "hooks/useShowHide"; @@ -60,7 +61,8 @@ export interface TablesUserListProps { venueName: string; setSeatedAtTable: (value: string) => void; seatedAtTable: string; - customTables?: Table[]; + customTables: Table[]; + showOnlyAvailableTables?: boolean; TableComponent: React.FC; joinMessage: boolean; leaveText?: string; @@ -71,6 +73,7 @@ export const TablesUserList: React.FC = ({ setSeatedAtTable, seatedAtTable, customTables, + showOnlyAvailableTables = false, TableComponent, joinMessage, }) => { @@ -130,19 +133,41 @@ export const TablesUserList: React.FC = ({ [recentVenueUsers, user, venueName, videoRoom] ); - const tableLocked = useCallback( - (table: string) => { - const areUsersAtTable = recentVenueUsers.some( - (user: User) => getUserExperience(venueName)(user)?.table === table - ); + const usersAtTableReducer = useCallback( + (obj: Record[]>, table: Table) => ({ + ...obj, + [table.reference]: recentVenueUsers.filter( + (user: User) => + getUserExperience(venueName)(user)?.table === table.reference + ), + }), + [recentVenueUsers, venueName] + ); + + const usersSeatedAtTables = useMemo( + () => tables.reduce(usersAtTableReducer, {}), + [tables, usersAtTableReducer] + ); + const isFullTable = useCallback( + (table: Table) => { + const numberOfSeatsLeft = + table.capacity && + table.capacity - usersSeatedAtTables[table.reference].length; + return numberOfSeatsLeft === 0; + }, + [usersSeatedAtTables] + ); + + const tableLocked = useCallback( + (tableReference: string) => { // Empty tables are never locked - if (!areUsersAtTable) return false; + if (!usersSeatedAtTables[tableReference].length) return false; // Locked state is in the experience record - return isTruthy(experience?.tables?.[table]?.locked); + return isTruthy(experience?.tables?.[tableReference]?.locked); }, - [experience?.tables, recentVenueUsers, venueName] + [experience?.tables, usersSeatedAtTables] ); const onAcceptJoinMessage = useCallback( @@ -175,14 +200,8 @@ export const TablesUserList: React.FC = ({ const emptyTables = useMemo( () => - tables.filter( - (table) => - !recentVenueUsers.some( - (user: User) => - getUserExperience(venueName)(user)?.table === table.reference - ) - ), - [recentVenueUsers, tables, venueName] + tables.filter((table) => !usersSeatedAtTables[table.reference].length), + [tables, usersSeatedAtTables] ); const canStartTable = @@ -191,9 +210,16 @@ export const TablesUserList: React.FC = ({ const renderedTables = useMemo(() => { if (isSeatedAtTable) return; - return tables.map((table: Table, index: number) => ( + const tablesToShow = showOnlyAvailableTables + ? tables.filter( + (table) => !(isFullTable(table) || tableLocked(table.reference)) + ) + : tables; + + return tablesToShow.map((table: Table, index: number) => ( = ({ recentVenueUsers, tableLocked, tables, + showOnlyAvailableTables, + isFullTable, venueName, ]); diff --git a/src/components/molecules/UserList/UserList.tsx b/src/components/molecules/UserList/UserList.tsx index b7e9569f8e..8062976d4f 100644 --- a/src/components/molecules/UserList/UserList.tsx +++ b/src/components/molecules/UserList/UserList.tsx @@ -23,7 +23,6 @@ interface UserListProps { activity?: string; containerClassName?: string; cellClassName?: string; - isAudioEffectDisabled?: boolean; hasClickableAvatars?: boolean; showEvenWhenNoUsers?: boolean; showMoreUsersToggler?: boolean; @@ -36,7 +35,6 @@ export const UserList: React.FC = ({ activity = "partying", containerClassName, cellClassName, - isAudioEffectDisabled, hasClickableAvatars = false, showEvenWhenNoUsers = false, showMoreUsersToggler = true, diff --git a/src/components/organisms/ChatSidebar/ChatSidebar.tsx b/src/components/organisms/ChatSidebar/ChatSidebar.tsx index 88f6b06b93..de2bf250ae 100644 --- a/src/components/organisms/ChatSidebar/ChatSidebar.tsx +++ b/src/components/organisms/ChatSidebar/ChatSidebar.tsx @@ -1,5 +1,6 @@ import React from "react"; import classNames from "classnames"; +import { isEqual } from "lodash"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { faChevronRight, @@ -22,7 +23,7 @@ export interface ChatSidebarProps { venue: WithId; } -export const ChatSidebar: React.FC = ({ venue }) => { +export const _ChatSidebar: React.FC = ({ venue }) => { const { isExpanded, toggleSidebar, @@ -85,3 +86,5 @@ export const ChatSidebar: React.FC = ({ venue }) => {
); }; + +export const ChatSidebar = React.memo(_ChatSidebar, isEqual); diff --git a/src/components/organisms/ChatSidebar/components/VenueChat/VenueChat.tsx b/src/components/organisms/ChatSidebar/components/VenueChat/VenueChat.tsx index 100472dd9d..ef9a3479ea 100644 --- a/src/components/organisms/ChatSidebar/components/VenueChat/VenueChat.tsx +++ b/src/components/organisms/ChatSidebar/components/VenueChat/VenueChat.tsx @@ -1,4 +1,5 @@ import React from "react"; +import { isEqual } from "lodash"; import { AnyVenue } from "types/venues"; @@ -14,7 +15,7 @@ export interface VenueChatProps { venue: WithId; } -export const VenueChat: React.FC = ({ venue }) => { +export const _VenueChat: React.FC = ({ venue }) => { const { sendMessage, deleteMessage, @@ -36,3 +37,5 @@ export const VenueChat: React.FC = ({ venue }) => {
); }; + +export const VenueChat = React.memo(_VenueChat, isEqual); diff --git a/src/components/organisms/RenderMarkdown/RenderMarkdown.tsx b/src/components/organisms/RenderMarkdown/RenderMarkdown.tsx index abd836087c..0c60afe255 100644 --- a/src/components/organisms/RenderMarkdown/RenderMarkdown.tsx +++ b/src/components/organisms/RenderMarkdown/RenderMarkdown.tsx @@ -12,6 +12,7 @@ const RenderMarkdownInner = lazy(() => export interface RenderMarkdownProps { text?: string; + components?: object; allowBasicFormatting?: boolean; allowPreAndCode?: boolean; allowHeadings?: boolean; @@ -22,6 +23,7 @@ export interface RenderMarkdownProps { const _RenderMarkdown: React.FC = ({ text, + components, allowBasicFormatting = true, allowPreAndCode = true, allowHeadings = true, @@ -31,6 +33,7 @@ const _RenderMarkdown: React.FC = ({ }) => ( {text}
}> = ({ text, + components, allowBasicFormatting = true, allowPreAndCode = true, allowHeadings = true, @@ -56,6 +58,7 @@ const _RenderMarkdownInner: React.FC = ({ rehypePlugins={REHYPE_PLUGINS} linkTarget="_blank" allowedElements={allowedElements} + components={components} > {text} diff --git a/src/components/templates/Auditorium/components/SectionPreview/SectionPreview.tsx b/src/components/templates/Auditorium/components/SectionPreview/SectionPreview.tsx index 5533970a4f..59802b6ad9 100644 --- a/src/components/templates/Auditorium/components/SectionPreview/SectionPreview.tsx +++ b/src/components/templates/Auditorium/components/SectionPreview/SectionPreview.tsx @@ -82,7 +82,6 @@ export const SectionPreview: React.FC = ({ showTitle={false} limit={SECTION_PREVIEW_USER_DISPLAY_COUNT} showMoreUsersToggler={false} - hasClickableAvatars={false} cellClassName="SectionPreview__avatar" />
diff --git a/src/components/templates/ConversationSpace/ConversationSpace.scss b/src/components/templates/ConversationSpace/ConversationSpace.scss index 4b4f1d1e17..2ae5baec34 100644 --- a/src/components/templates/ConversationSpace/ConversationSpace.scss +++ b/src/components/templates/ConversationSpace/ConversationSpace.scss @@ -253,6 +253,12 @@ margin: 10px 70px; } + .ControlBar__container { + margin: $spacing--md 40px 0 auto; + padding-right: 40px; + width: max-content; + } + .seated-area { border: 0px solid white; display: flex; diff --git a/src/components/templates/ConversationSpace/ConversationSpace.tsx b/src/components/templates/ConversationSpace/ConversationSpace.tsx index 10d4686f3d..fcd19a27b7 100644 --- a/src/components/templates/ConversationSpace/ConversationSpace.tsx +++ b/src/components/templates/ConversationSpace/ConversationSpace.tsx @@ -9,6 +9,7 @@ import { WithId } from "utils/id"; import { useRecentVenueUsers } from "hooks/users"; import { useExperiences } from "hooks/useExperiences"; +import { useShowHide } from "hooks/useShowHide"; import { useRelatedVenues } from "hooks/useRelatedVenues"; import { InformationLeftColumn } from "components/organisms/InformationLeftColumn"; @@ -20,6 +21,7 @@ import TableComponent from "components/molecules/TableComponent"; import TableHeader from "components/molecules/TableHeader"; import { UserList } from "components/molecules/UserList"; import { TablesUserList } from "components/molecules/TablesUserList"; +import { TablesControlBar } from "components/molecules/TablesControlBar"; import { BackButton } from "components/atoms/BackButton"; @@ -40,6 +42,11 @@ export const ConversationSpace: React.FC = ({ const { recentVenueUsers } = useRecentVenueUsers({ venueName: venue?.name }); + const { + isShown: showOnlyAvailableTables, + toggle: toggleTablesVisibility, + } = useShowHide(); + const [seatedAtTable, setSeatedAtTable] = useState(""); useExperiences(venue?.name); @@ -113,6 +120,13 @@ export const ConversationSpace: React.FC = ({ /> )} + {!seatedAtTable && ( + + )}
@@ -123,6 +137,7 @@ export const ConversationSpace: React.FC = ({ TableComponent={TableComponent} joinMessage={venue.hideVideo === false} customTables={tables} + showOnlyAvailableTables={showOnlyAvailableTables} />
= ({ setUserList, venue }) => { const { recentVenueUsers } = useRecentVenueUsers({ venueName: venue.name }); + const { + isShown: showOnlyAvailableTables, + toggle: toggleTablesVisibility, + } = useShowHide(); + const { parentVenue } = useRelatedVenues({ currentVenueId: venue.id }); const parentVenueId = parentVenue?.id; @@ -200,6 +206,13 @@ const Jazz: React.FC = ({ setUserList, venue }) => { )} */} )} + {!seatedAtTable && ( + + )} )} @@ -219,6 +232,7 @@ const Jazz: React.FC = ({ setUserList, venue }) => { TableComponent={JazzBarTableComponent} joinMessage={!venue.hideVideo ?? true} customTables={jazzbarTables} + showOnlyAvailableTables={showOnlyAvailableTables} /> diff --git a/src/components/templates/PartyMap/components/RoomModal/RoomModal.tsx b/src/components/templates/PartyMap/components/RoomModal/RoomModal.tsx index a3ea17777b..6108d4fb9e 100644 --- a/src/components/templates/PartyMap/components/RoomModal/RoomModal.tsx +++ b/src/components/templates/PartyMap/components/RoomModal/RoomModal.tsx @@ -159,6 +159,7 @@ export const RoomModalContent: React.FC = ({ users={recentRoomUsers} limit={11} activity="in this room" + hasClickableAvatars /> {room.about && ( diff --git a/src/components/templates/Playa/VenuePreview.tsx b/src/components/templates/Playa/VenuePreview.tsx index d08a980f65..0d4e23b607 100644 --- a/src/components/templates/Playa/VenuePreview.tsx +++ b/src/components/templates/Playa/VenuePreview.tsx @@ -231,11 +231,7 @@ const VenuePreview: React.FC = ({
- +
diff --git a/src/components/templates/ReactionPage/ReactionPage.tsx b/src/components/templates/ReactionPage/ReactionPage.tsx index 3266f86ca6..c6c6c92a60 100644 --- a/src/components/templates/ReactionPage/ReactionPage.tsx +++ b/src/components/templates/ReactionPage/ReactionPage.tsx @@ -57,11 +57,7 @@ export const ReactionPage: React.FC = () => {
- +
diff --git a/src/components/templates/TalkShowStudio/TalkShowStudio.tsx b/src/components/templates/TalkShowStudio/TalkShowStudio.tsx index 3a94c6386b..c59a5956b9 100644 --- a/src/components/templates/TalkShowStudio/TalkShowStudio.tsx +++ b/src/components/templates/TalkShowStudio/TalkShowStudio.tsx @@ -1,5 +1,4 @@ import React, { FC, useCallback, useEffect, useMemo } from "react"; -import AgoraRTC, { IAgoraRTCClient } from "agora-rtc-sdk-ng"; import { FullTalkShowVenue } from "types/venues"; import { AgoraClientConnectionState } from "types/agora"; @@ -24,21 +23,6 @@ import SettingsSidebar from "./components/SettingsSidebar/SettingsSidebar"; import "./TalkShowStudio.scss"; -const remotesClient: IAgoraRTCClient = AgoraRTC.createClient({ - codec: "h264", - mode: "rtc", -}); - -const screenClient: IAgoraRTCClient = AgoraRTC.createClient({ - codec: "h264", - mode: "rtc", -}); - -const cameraClient: IAgoraRTCClient = AgoraRTC.createClient({ - codec: "h264", - mode: "rtc", -}); - export interface TalkShowStudioProps { venue: WithId; } @@ -47,26 +31,29 @@ export const TalkShowStudio: FC = ({ venue }) => { const stage = useStage(); const { userId, profile } = useUser(); const currentVenue = useSelector(currentVenueSelectorData); - const remoteUsers = useAgoraRemotes({ client: remotesClient }); const isRequestToJoinStageEnabled = venue.requestToJoinStage; + const remoteUsers = useAgoraRemotes(); + const { localCameraTrack, toggleCamera, toggleMicrophone, isCameraOn, isMicrophoneOn, + client: cameraClient, joinChannel: cameraClientJoin, leaveChannel: cameraClientLeave, - } = useAgoraCamera({ client: cameraClient }); + } = useAgoraCamera(); const { localScreenTrack, shareScreen, stopShare, + client: screenClient, joinChannel: screenClientJoin, leaveChannel: screenClientLeave, - } = useAgoraScreenShare({ client: screenClient }); + } = useAgoraScreenShare(); const localUser = useMemo( () => stage.peopleOnStage.find(({ id }) => id === userId), @@ -126,8 +113,8 @@ export const TalkShowStudio: FC = ({ venue }) => { ) .map( (user) => - user.uid !== screenClient.uid && - user.uid !== cameraClient.uid && ( + user.uid !== screenClient?.uid && + user.uid !== cameraClient?.uid && (
{user.hasVideo && ( = ({ venue }) => {
) ); - }, [remoteUsers, venue.id, stage.peopleOnStage, userOnStageSharingScreen]); + }, [ + remoteUsers, + venue.id, + stage.peopleOnStage, + userOnStageSharingScreen, + cameraClient?.uid, + screenClient?.uid, + ]); const onStageJoin = useCallback(() => { cameraClientJoin(); @@ -158,14 +152,19 @@ export const TalkShowStudio: FC = ({ venue }) => { }, [cameraClientLeave, stage, screenClientLeave]); useEffect(() => { - cameraClient.connectionState === AgoraClientConnectionState.DISCONNECTED && + cameraClient?.connectionState === AgoraClientConnectionState.DISCONNECTED && stage.isUserOnStage && onStageJoin(); - cameraClient.connectionState === AgoraClientConnectionState.CONNECTED && + cameraClient?.connectionState === AgoraClientConnectionState.CONNECTED && !stage.isUserOnStage && onStageLeaving(); - }, [stage.isUserOnStage, onStageJoin, onStageLeaving]); + }, [ + stage.isUserOnStage, + onStageJoin, + onStageLeaving, + cameraClient?.connectionState, + ]); useEffect(() => { !stage.isUserSharing && localScreenTrack && stopShare(); @@ -254,7 +253,7 @@ export const TalkShowStudio: FC = ({ venue }) => { mixpanel.hasOwnProperty("get_distinct_id"); - -const noopTrack: typeof mixpanel.track = () => {}; - -/** - * Returns an object to allow us to expose more of Mixpanel's API in future. - * - * NOTE: Extra level of function indirection ensures `isLoaded` is evaluated not just the first time. - */ -export const useMixpanel = () => - useMemo( - () => ({ - track: ( - event_name: string, - properties?: Dict, - optionsOrCallback?: RequestOptions | Callback, - callback?: Callback - ) => { - const track = isLoaded() ? mixpanel.track.bind(mixpanel) : noopTrack; - return track(event_name, properties, optionsOrCallback, callback); - }, - }), - [] - ); diff --git a/src/hooks/useRoles.ts b/src/hooks/useRoles.ts index efac9142a7..bb7079a395 100644 --- a/src/hooks/useRoles.ts +++ b/src/hooks/useRoles.ts @@ -1,48 +1,60 @@ -import { useFirestoreConnect } from "./useFirestoreConnect"; +import { isEqual } from "lodash"; + +import { isLoaded, useFirestoreConnect } from "./useFirestoreConnect"; import { useUser } from "./useUser"; import { useSelector } from "./useSelector"; export const useRoles = () => { const { user } = useUser(); - useFirestoreConnect({ - collection: "roles", - where: [["users", "array-contains", user?.uid || ""]], - storeAs: "userRoles", - }); + + useFirestoreConnect( + user + ? { + collection: "roles", + where: [["users", "array-contains", user.uid]], + storeAs: "userRoles", + } + : undefined + ); + useFirestoreConnect({ collection: "roles", where: [["allowAll", "==", true]], storeAs: "allowAllRoles", }); - const { userRoles, allowAllRoles } = useSelector((state) => ({ - userRoles: state.firestore.data.userRoles, - allowAllRoles: state.firestore.data.allowAllRoles, - })); - - // Note: null here means data is loaded, but there was none. - // A value of undefined indicates data is not loaded yet. - // undefined should be returned so callers can show loading indications + + const { + isUserRolesLoaded, + isAllowAllRolesLoaded, + isRolesLoaded, + userRoles, + allowAllRoles, + roles, + } = useSelector((state) => { + const { userRoles, allowAllRoles } = state.firestore.data; + + const rolesForUser = userRoles ? Object.keys(userRoles) : []; + const rolesForAll = allowAllRoles ? Object.keys(allowAllRoles) : []; + const combinedRoles = [...rolesForUser, ...rolesForAll]; + + return { + isUserRolesLoaded: isLoaded(userRoles), + userRoles: rolesForUser, + + isAllowAllRolesLoaded: isLoaded(allowAllRoles), + allowAllRoles: rolesForAll, + + isRolesLoaded: isLoaded(userRoles) && isLoaded(allowAllRoles), + roles: combinedRoles, + }; + }, isEqual); + return { - userRoles: - userRoles === undefined - ? undefined - : userRoles === null - ? [] - : Object.keys(userRoles), - allowAllRoles: - allowAllRoles === undefined - ? undefined - : allowAllRoles === null - ? [] - : Object.keys(allowAllRoles), - roles: - userRoles === undefined || allowAllRoles === undefined - ? undefined - : [ - ...Object.keys(userRoles ?? []), - ...Object.keys(allowAllRoles ?? []), - ], + isUserRolesLoaded, + isAllowAllRolesLoaded, + isRolesLoaded, + userRoles, + allowAllRoles, + roles, }; }; - -export default useRoles; diff --git a/src/hooks/useUser.ts b/src/hooks/useUser.ts index b856570b76..df439f127a 100644 --- a/src/hooks/useUser.ts +++ b/src/hooks/useUser.ts @@ -1,27 +1,32 @@ +import { useMemo } from "react"; import { FirebaseReducer } from "react-redux-firebase"; +import { isEqual } from "lodash"; import { User } from "types/User"; -import { WithId } from "utils/id"; +import { withId, WithId } from "utils/id"; import { authSelector, profileSelector } from "utils/selectors"; import { useSelector } from "hooks/useSelector"; -type UseUserResult = { +export interface UseUserResult { user?: FirebaseReducer.AuthState; profile?: FirebaseReducer.Profile; userWithId?: WithId; userId?: string; -}; +} export const useUser = (): UseUserResult => { - const auth = useSelector(authSelector); - const profile = useSelector(profileSelector); + const auth = useSelector(authSelector, isEqual); + const profile = useSelector(profileSelector, isEqual); - return { - user: !auth.isEmpty ? auth : undefined, - profile: !profile.isEmpty ? profile : undefined, - userWithId: auth && profile ? { ...profile, id: auth.uid } : undefined, - userId: auth && profile ? auth.uid : undefined, - }; + return useMemo( + () => ({ + user: !auth.isEmpty ? auth : undefined, + profile: !profile.isEmpty ? profile : undefined, + userWithId: auth && profile ? withId(profile, auth.uid) : undefined, + userId: auth && profile ? auth.uid : undefined, + }), + [auth, profile] + ); }; diff --git a/src/hooks/useVenueChat.ts b/src/hooks/useVenueChat.ts index 1e4f4b3e28..f3c07ece7e 100644 --- a/src/hooks/useVenueChat.ts +++ b/src/hooks/useVenueChat.ts @@ -1,4 +1,5 @@ import { useMemo, useCallback } from "react"; +import { isEqual } from "lodash"; import { VENUE_CHAT_AGE_DAYS } from "settings"; @@ -29,6 +30,8 @@ import { useUser } from "./useUser"; import { useWorldUsersByIdWorkaround } from "./users"; import { useRoles } from "./useRoles"; +const noMessages: WithId[] = []; + export const useConnectVenueChatMessages = (venueId?: string) => { useFirestoreConnect( venueId @@ -49,19 +52,24 @@ export const useVenueChat = (venueId?: string) => { useConnectVenueChatMessages(venueId); - const chatMessages = useSelector(venueChatMessagesSelector) ?? []; + const chatMessages = + useSelector(venueChatMessagesSelector, isEqual) ?? noMessages; const isAdmin = Boolean(userRoles?.includes("admin")); const venueChatAgeThresholdSec = getDaysAgoInSeconds(VENUE_CHAT_AGE_DAYS); - const filteredMessages = chatMessages - .filter( - (message) => - message.deleted !== true && - message.ts_utc.seconds > venueChatAgeThresholdSec - ) - .sort(chatSort); + const filteredMessages = useMemo( + () => + chatMessages + .filter( + (message) => + message.deleted !== true && + message.ts_utc.seconds > venueChatAgeThresholdSec + ) + .sort(chatSort), + [chatMessages, venueChatAgeThresholdSec] + ); const sendMessage: SendMessage = useCallback( async ({ message, isQuestion }) => { @@ -143,14 +151,11 @@ export const useVenueChat = (venueId?: string) => { [userId, worldUsersById, isAdmin, messages, allMessagesReplies] ); - return useMemo( - () => ({ - messagesToDisplay, + return { + messagesToDisplay, - sendMessage, - deleteMessage, - sendThreadReply, - }), - [messagesToDisplay, sendMessage, sendThreadReply, deleteMessage] - ); + sendMessage, + deleteMessage, + sendThreadReply, + }; }; diff --git a/src/hooks/useVenuePoll.ts b/src/hooks/useVenuePoll.ts index 7eafa456cf..ba3669ad04 100644 --- a/src/hooks/useVenuePoll.ts +++ b/src/hooks/useVenuePoll.ts @@ -22,7 +22,7 @@ export const useVenuePoll = () => { (pollVote: PollVoteBase) => { if (!venueId) return; - voteInVenuePoll({ pollVote, venueId }); + return voteInVenuePoll({ pollVote, venueId }); }, [venueId] ); diff --git a/src/hooks/useVenueUserStatuses.ts b/src/hooks/useVenueUserStatuses.ts index c487ecd31c..0713db8036 100644 --- a/src/hooks/useVenueUserStatuses.ts +++ b/src/hooks/useVenueUserStatuses.ts @@ -1,20 +1,23 @@ -import { useCallback } from "react"; +import { useCallback, useMemo } from "react"; import { updateUserOnlineStatus } from "api/profile"; import { DEFAULT_USER_STATUS, DEFAULT_SHOW_USER_STATUSES } from "settings"; -import { User } from "types/User"; +import { User, UserStatus } from "types/User"; import { WithId } from "utils/id"; import { useSovereignVenue } from "./useSovereignVenue"; import { useUser } from "./useUser"; +const emptyStatuses: UserStatus[] = []; + export const useVenueUserStatuses = (venueId?: string, user?: WithId) => { const { sovereignVenue } = useSovereignVenue({ venueId }); const { userId, profile } = useUser(); + // @debt replace this with useAsync / useAsyncFn / similar const changeUserStatus = useCallback( (newStatus?: string) => { if (!userId) return; @@ -27,20 +30,22 @@ export const useVenueUserStatuses = (venueId?: string, user?: WithId) => { [userId] ); - const venueStatuses = sovereignVenue?.userStatuses ?? []; + const venueUserStatuses = sovereignVenue?.userStatuses ?? emptyStatuses; - const userStatus = venueStatuses.find( - (userStatus) => userStatus.status === user?.status + const userStatus = useMemo( + () => + venueUserStatuses.find(({ status }) => status === user?.status) ?? { + status: profile?.status ?? DEFAULT_USER_STATUS.status, + color: venueUserStatuses[0]?.color ?? DEFAULT_USER_STATUS.color, + }, + [profile?.status, user?.status, venueUserStatuses] ); return { changeUserStatus, - venueUserStatuses: venueStatuses, + venueUserStatuses, isStatusEnabledForVenue: sovereignVenue?.showUserStatus ?? DEFAULT_SHOW_USER_STATUSES, - userStatus: userStatus ?? { - status: profile?.status ?? DEFAULT_USER_STATUS.status, - color: venueStatuses[0]?.color ?? DEFAULT_USER_STATUS.color, - }, + userStatus, }; }; diff --git a/src/hooks/video/agora/useAgoraCamera.ts b/src/hooks/video/agora/useAgoraCamera.ts index 659bc65b03..02f88fdf27 100644 --- a/src/hooks/video/agora/useAgoraCamera.ts +++ b/src/hooks/video/agora/useAgoraCamera.ts @@ -1,10 +1,13 @@ import { useCallback, useEffect, useState } from "react"; -import AgoraRTC, { ILocalAudioTrack, ILocalVideoTrack } from "agora-rtc-sdk-ng"; +import AgoraRTC, { + IAgoraRTCClient, + ILocalAudioTrack, + ILocalVideoTrack, +} from "agora-rtc-sdk-ng"; import { AGORA_APP_ID, AGORA_CHANNEL, AGORA_TOKEN } from "secrets"; -import { UseAgoraCameraProps, UseAgoraCameraReturn } from "types/agora"; -import { ReactHook } from "types/utility"; +import { UseAgoraCameraReturn } from "types/agora"; import { updateTalkShowStudioExperience } from "api/profile"; @@ -12,13 +15,11 @@ import { useShowHide } from "hooks/useShowHide"; import { useUser } from "hooks/useUser"; import { useVenueId } from "hooks/useVenueId"; -export const useAgoraCamera: ReactHook< - UseAgoraCameraProps, - UseAgoraCameraReturn -> = ({ client }) => { +export const useAgoraCamera = (): UseAgoraCameraReturn => { const { userId } = useUser(); const venueId = useVenueId(); + const [client, setClient] = useState(); const [localCameraTrack, setLocalCameraTrack] = useState(); const [ localMicrophoneTrack, @@ -53,7 +54,7 @@ export const useAgoraCamera: ReactHook< cameraClientUid: `${cameraClientUid}`, }; - updateTalkShowStudioExperience({ venueId, userId, experience }); + await updateTalkShowStudioExperience({ venueId, userId, experience }); setIsCameraOn(true); setIsMicrophoneOn(true); @@ -75,12 +76,21 @@ export const useAgoraCamera: ReactHook< setLocalCameraTrack(undefined); setLocalMicrophoneTrack(undefined); + await client?.unpublish(); await client?.leave(); }, [client, localCameraTrack, localMicrophoneTrack]); useEffect(() => { + setClient( + AgoraRTC.createClient({ + codec: "h264", + mode: "rtc", + }) + ); + return () => { leaveChannel(); + setClient(undefined); }; // Otherwise, it will fire when local tracks are updated // @debt We shouldn't be disabling our linting rules like this @@ -88,6 +98,7 @@ export const useAgoraCamera: ReactHook< }, []); return { + client, localCameraTrack, toggleCamera, toggleMicrophone, diff --git a/src/hooks/video/agora/useAgoraRemotes.ts b/src/hooks/video/agora/useAgoraRemotes.ts index fdaca2aa41..8c23830311 100644 --- a/src/hooks/video/agora/useAgoraRemotes.ts +++ b/src/hooks/video/agora/useAgoraRemotes.ts @@ -1,15 +1,15 @@ import { useCallback, useEffect, useState } from "react"; -import { IAgoraRTCRemoteUser } from "agora-rtc-sdk-ng"; +import AgoraRTC, { + IAgoraRTCClient, + IAgoraRTCRemoteUser, +} from "agora-rtc-sdk-ng"; import { AGORA_APP_ID, AGORA_CHANNEL, AGORA_TOKEN } from "secrets"; -import { UseAgoraRemotesProps, UseAgoraRemotesReturn } from "types/agora"; -import { ReactHook } from "types/utility"; +import { UseAgoraRemotesReturn } from "types/agora"; -export const useAgoraRemotes: ReactHook< - UseAgoraRemotesProps, - UseAgoraRemotesReturn -> = ({ client }) => { +export const useAgoraRemotes = (): UseAgoraRemotesReturn => { + const [client, setClient] = useState(); const [remoteUsers, setRemoteUsers] = useState([]); const updateRemoteUsers = useCallback(() => { @@ -29,7 +29,16 @@ export const useAgoraRemotes: ReactHook< ); useEffect(() => { - if (!client) return; + if (!client) { + setClient( + AgoraRTC.createClient({ + codec: "h264", + mode: "rtc", + }) + ); + + return; + } updateRemoteUsers(); @@ -49,6 +58,7 @@ export const useAgoraRemotes: ReactHook< // @debt promise returned from .leave is ignored client.leave(); + setClient(undefined); }; }, [client, handleUserPublished, updateRemoteUsers]); diff --git a/src/hooks/video/agora/useAgoraScreenShare.ts b/src/hooks/video/agora/useAgoraScreenShare.ts index 4b62f00715..70c6cfc1ef 100644 --- a/src/hooks/video/agora/useAgoraScreenShare.ts +++ b/src/hooks/video/agora/useAgoraScreenShare.ts @@ -1,26 +1,24 @@ import { useCallback, useEffect, useState } from "react"; -import AgoraRTC, { ILocalAudioTrack, ILocalVideoTrack } from "agora-rtc-sdk-ng"; +import AgoraRTC, { + IAgoraRTCClient, + ILocalAudioTrack, + ILocalVideoTrack, +} from "agora-rtc-sdk-ng"; import { AGORA_APP_ID, AGORA_CHANNEL, AGORA_TOKEN } from "secrets"; -import { - UseAgoraScreenShareProps, - UseAgoraScreenShareReturn, -} from "types/agora"; -import { ReactHook } from "types/utility"; +import { UseAgoraScreenShareReturn } from "types/agora"; import { updateTalkShowStudioExperience } from "api/profile"; import { useUser } from "hooks/useUser"; import { useVenueId } from "hooks/useVenueId"; -export const useAgoraScreenShare: ReactHook< - UseAgoraScreenShareProps, - UseAgoraScreenShareReturn -> = ({ client }) => { +export const useAgoraScreenShare = (): UseAgoraScreenShareReturn => { const { userId } = useUser(); const venueId = useVenueId(); + const [client, setClient] = useState(); const [localScreenTrack, setLocalScreenTrack] = useState(); const [localAudioTrack, setLocalAudioTrack] = useState(); @@ -68,17 +66,25 @@ export const useAgoraScreenShare: ReactHook< screenClientUid: `${screenClientUid}`, }; - updateTalkShowStudioExperience({ venueId, userId, experience }); + await updateTalkShowStudioExperience({ venueId, userId, experience }); }; const leaveChannel = useCallback(async () => { - stopShare(); + await stopShare(); await client?.leave(); }, [client, stopShare]); useEffect(() => { + setClient( + AgoraRTC.createClient({ + codec: "h264", + mode: "rtc", + }) + ); + return () => { leaveChannel(); + setClient(undefined); }; // Otherwise, it will fire when local tracks are updated // @debt We shouldn't be disabling our linting rules like this @@ -86,6 +92,7 @@ export const useAgoraScreenShare: ReactHook< }, []); return { + client, localScreenTrack, shareScreen, stopShare, diff --git a/src/index.tsx b/src/index.tsx index 889b51ee50..a22acfa3d7 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -1,13 +1,10 @@ import "./wdyr"; -import React, { useEffect } from "react"; +import React from "react"; import { render } from "react-dom"; import Bugsnag from "@bugsnag/js"; import BugsnagPluginReact from "@bugsnag/plugin-react"; -import LogRocket from "logrocket"; -// eslint-disable-next-line no-restricted-imports -import mixpanel from "mixpanel-browser"; import { Provider } from "react-redux"; import { createStore, combineReducers, applyMiddleware, Reducer } from "redux"; @@ -40,8 +37,6 @@ import { BUILD_PULL_REQUESTS, BUILD_SHA1, BUILD_TAG, - LOGROCKET_APP_ID, - MIXPANEL_PROJECT_TOKEN, STRIPE_PUBLISHABLE_KEY, } from "secrets"; import { FIREBASE_CONFIG } from "settings"; @@ -60,7 +55,6 @@ import { traceReactScheduler, } from "utils/performance"; import { authSelector } from "utils/selectors"; -import { initializeZendesk } from "utils/zendesk"; import { CustomSoundsProvider } from "hooks/sounds"; import { useSelector } from "hooks/useSelector"; @@ -74,17 +68,6 @@ import { ThemeProvider } from "styled-components"; import { theme } from "theme/theme"; activatePolyFills(); -initializeZendesk(); - -if (LOGROCKET_APP_ID) { - LogRocket.init(LOGROCKET_APP_ID, { - release: BUILD_SHA1, - }); - - Bugsnag.addOnError((event) => { - event.addMetadata("logrocket", "sessionUrl", LogRocket.sessionURL); - }); -} const firebaseApp = firebase.initializeApp(FIREBASE_CONFIG); firebaseApp.analytics(); @@ -122,12 +105,7 @@ const initialState = {}; const store = createStore( rootReducer, initialState, - composeWithDevTools( - applyMiddleware( - thunkMiddleware, - LogRocket.reduxMiddleware() // logrocket needs to be last - ) - ) + composeWithDevTools(applyMiddleware(thunkMiddleware)) ); export type AppDispatch = typeof store.dispatch; @@ -236,33 +214,11 @@ const BugsnagErrorBoundary = BUGSNAG_API_KEY ? Bugsnag.getPlugin("react")?.createErrorBoundary(React) ?? React.Fragment : React.Fragment; -if (MIXPANEL_PROJECT_TOKEN) { - mixpanel.init(MIXPANEL_PROJECT_TOKEN, { batch_requests: true }); -} - const AuthIsLoaded: React.FunctionComponent> = ({ children, }) => { const auth = useSelector(authSelector); - useEffect(() => { - if (!auth || !auth.uid) return; - - const displayName = auth.displayName || "N/A"; - const email = auth.email || "N/A"; - - if (LOGROCKET_APP_ID) { - LogRocket.identify(auth.uid, { - displayName, - email, - }); - } - - if (MIXPANEL_PROJECT_TOKEN) { - mixpanel.identify(email); - } - }, [auth]); - if (!isLoaded(auth)) return ; return <>{children}; diff --git a/src/pages/Admin/Admin_v2.tsx b/src/pages/Admin/Admin_v2.tsx index 60ce9eefe5..4afa15c3a7 100644 --- a/src/pages/Admin/Admin_v2.tsx +++ b/src/pages/Admin/Admin_v2.tsx @@ -7,7 +7,7 @@ import { orderedVenuesSelector } from "utils/selectors"; import { useSelector } from "hooks/useSelector"; import { useUser } from "hooks/useUser"; -import useRoles from "hooks/useRoles"; +import { useRoles } from "hooks/useRoles"; import { useIsAdminUser } from "hooks/roles"; import { useAdminVenues } from "hooks/useAdminVenues"; diff --git a/src/pages/Admin/AdvancedSettings/AdvancedSettings.tsx b/src/pages/Admin/AdvancedSettings/AdvancedSettings.tsx index 7b830ab419..428184ba1c 100644 --- a/src/pages/Admin/AdvancedSettings/AdvancedSettings.tsx +++ b/src/pages/Admin/AdvancedSettings/AdvancedSettings.tsx @@ -76,7 +76,6 @@ const validationSchema = Yup.object().shape({ .notRequired(), showRadio: Yup.bool().notRequired(), showRangers: Yup.bool().notRequired(), - showZendesk: Yup.bool().notRequired(), // TODO: Figure out how to validate with enum values // roomVisibility: Yup.string().notRequired() @@ -104,7 +103,6 @@ const AdvancedSettings: React.FC = ({ showNametags: venue.showNametags, showGrid: venue.showGrid, showRadio: venue.showRadio, - showZendesk: venue.showZendesk, showRangers: venue.showRangers, bannerMessage: venue.bannerMessage, attendeesTitle: venue.attendeesTitle, @@ -316,13 +314,6 @@ const AdvancedSettings: React.FC = ({ {renderShowNametags()} - - = ({ ); - // @debt pass the header into Toggler's 'label' prop instead of being external like this - const renderShowZendeskToggle = () => ( -
-

Show Zendesk support popup

- -
- ); - const renderSeatingNumberInput = () => ( <>
@@ -1051,7 +1043,6 @@ const DetailsFormLeft: React.FC = ({ renderShowGridToggle()} {renderShowBadgesToggle()} {renderShowNametagsToggle()} - {renderShowZendeskToggle()} {templateID && HAS_REACTIONS_TEMPLATES.includes(templateID) && renderShowReactions()} diff --git a/src/pages/VenueEntrancePage/VenueEntrancePage.tsx b/src/pages/VenueEntrancePage/VenueEntrancePage.tsx index 6705b568bb..632cc4cb38 100644 --- a/src/pages/VenueEntrancePage/VenueEntrancePage.tsx +++ b/src/pages/VenueEntrancePage/VenueEntrancePage.tsx @@ -1,4 +1,4 @@ -import React, { useEffect } from "react"; +import React from "react"; import { Redirect, useHistory, useParams } from "react-router-dom"; import { EntranceStepTemplate } from "types/EntranceStep"; @@ -7,7 +7,6 @@ import { withId } from "utils/id"; import { isCompleteProfile } from "utils/profile"; import { currentVenueSelectorData } from "utils/selectors"; import { venueEntranceUrl, venueInsideUrl } from "utils/url"; -import { showZendeskWidget } from "utils/zendesk"; import useConnectCurrentVenue from "hooks/useConnectCurrentVenue"; import { useSelector } from "hooks/useSelector"; @@ -28,12 +27,6 @@ export const VenueEntrancePage: React.FunctionComponent<{}> = () => { useConnectCurrentVenue(); const venue = useSelector(currentVenueSelectorData); - useEffect(() => { - if (venue?.showZendesk) { - showZendeskWidget(); - } - }, [venue]); - if (!venue || !venueId) { return ; } diff --git a/src/pages/VenueLandingPage/VenueLandingPage.tsx b/src/pages/VenueLandingPage/VenueLandingPage.tsx index b5fa5eddc4..ca195f7204 100644 --- a/src/pages/VenueLandingPage/VenueLandingPage.tsx +++ b/src/pages/VenueLandingPage/VenueLandingPage.tsx @@ -24,7 +24,6 @@ import { venueEventsSelector, } from "utils/selectors"; import { hasEventFinished } from "utils/event"; -import { showZendeskWidget } from "utils/zendesk"; import useConnectCurrentVenue from "hooks/useConnectCurrentVenue"; import { useSelector } from "hooks/useSelector"; @@ -102,12 +101,6 @@ export const VenueLandingPage: React.FC = () => { } }, [shouldOpenPaymentModal, isAuthenticationModalOpen]); - useEffect(() => { - if (venue?.showZendesk) { - showZendeskWidget(); - } - }, [venue]); - if (venueRequestStatus && !venue) { return <>This venue does not exist; } diff --git a/src/pages/VenuePage/TemplateWrapper.tsx b/src/pages/VenuePage/TemplateWrapper.tsx index 44eb2e9530..def3a0352e 100644 --- a/src/pages/VenuePage/TemplateWrapper.tsx +++ b/src/pages/VenuePage/TemplateWrapper.tsx @@ -84,20 +84,24 @@ export const TemplateWrapper: React.FC = ({ venue }) => { case VenueTemplate.artcar: if (venue.zoomUrl) { window.location.replace(venue.zoomUrl); + + // Note that we are explicitly returning here so that none of the rest of this component has a chance to render + return ; + } else { + template = ( +

+ Venue {venue.name} should redirect to a URL, but none was set. +
+ +

+ ); } - template = ( -

- Venue {venue.name} should redirect to a URL, but none was set. -
- -

- ); break; // Note: This is the template that is used for Auditorium (v1) diff --git a/src/pages/VenuePage/VenuePage.tsx b/src/pages/VenuePage/VenuePage.tsx index 514bb6de58..9d72408d95 100644 --- a/src/pages/VenuePage/VenuePage.tsx +++ b/src/pages/VenuePage/VenuePage.tsx @@ -23,7 +23,7 @@ import { useUpdateTimespentPeriodically, } from "utils/userLocation"; import { venueEntranceUrl } from "utils/url"; -import { showZendeskWidget } from "utils/zendesk"; + import { tracePromise } from "utils/performance"; import { isCompleteProfile, updateProfileEnteredVenueIds } from "utils/profile"; import { isTruthy } from "utils/types"; @@ -32,7 +32,6 @@ import { hasEventFinished, isEventStartingSoon } from "utils/event"; import { useConnectCurrentEvent } from "hooks/useConnectCurrentEvent"; import { useConnectUserPurchaseHistory } from "hooks/useConnectUserPurchaseHistory"; import { useInterval } from "hooks/useInterval"; -import { useMixpanel } from "hooks/useMixpanel"; import { useSelector } from "hooks/useSelector"; import { useWorldUserLocation } from "hooks/users"; import { useUser } from "hooks/useUser"; @@ -73,7 +72,6 @@ const hasPaidEvents = (template: VenueTemplate) => { export const VenuePage: React.FC = () => { const venueId = useVenueId(); - const mixpanel = useMixpanel(); // const [isAccessDenied, setIsAccessDenied] = useState(false); @@ -102,7 +100,6 @@ export const VenuePage: React.FC = () => { const userId = user?.uid; const venueName = venue?.name ?? ""; - const venueTemplate = venue?.template; const event = currentEvent?.[0]; @@ -170,21 +167,6 @@ export const VenuePage: React.FC = () => { useUpdateTimespentPeriodically({ locationName: venueName, userId }); - useEffect(() => { - if (user && profile && venueId && venueTemplate) { - mixpanel.track("VenuePage loaded", { - venueId, - template: venueTemplate, - }); - } - }, [user, profile, venueId, venueTemplate, mixpanel]); - - useEffect(() => { - if (venue?.showZendesk) { - showZendeskWidget(); - } - }, [venue]); - // const handleAccessDenied = useCallback(() => setIsAccessDenied(true), []); // useVenueAccess(venue, handleAccessDenied); diff --git a/src/scss/constants.scss b/src/scss/constants.scss index 702d74e8f2..03a14a89a6 100644 --- a/src/scss/constants.scss +++ b/src/scss/constants.scss @@ -29,6 +29,7 @@ $lighter-intermediate-grey: #3d3a3f; $light-grey: #292929; $pale-slate: #bab9ba; $concrete: #a3a6ab; +$concrete--dark: #9f9fa0; $black: #000000; $white: #fff; $dark: #1a1d24; diff --git a/src/secrets.ts b/src/secrets.ts index d6ebc51223..35cda4c948 100644 --- a/src/secrets.ts +++ b/src/secrets.ts @@ -7,7 +7,6 @@ export const BUCKET_URL = process.env.REACT_APP_BUCKET_URL; export const STRIPE_PUBLISHABLE_KEY = process.env.REACT_APP_STRIPE_PUBLISHABLE_KEY; export const BUGSNAG_API_KEY = process.env.REACT_APP_BUGSNAG_API_KEY; -export const LOGROCKET_APP_ID = process.env.REACT_APP_LOGROCKET_APP_ID; export const MIXPANEL_PROJECT_TOKEN = process.env.REACT_APP_MIXPANEL_PROJECT_TOKEN; diff --git a/src/settings.ts b/src/settings.ts index f06f9ee8ab..67cf1e5a6a 100644 --- a/src/settings.ts +++ b/src/settings.ts @@ -18,7 +18,8 @@ import sparkleNavLogo from "assets/icons/sparkle-nav-logo.png"; import defaultMapIcon from "assets/icons/default-map-icon.png"; import sparkleverseLogo from "assets/images/sparkleverse-logo.png"; -export const SPARKLE_HOMEPAGE_URL = "/in/explore/"; +export const SPARKLE_HOMEPAGE_URL = "/in/explore"; +export const SPARKLE_FOOTER_URL = "https://sparklespaces.com/"; export const SPARKLE_TERMS_AND_CONDITIONS_URL = "https://sparklespaces.com/terms-of-use/"; export const SPARKLE_PRIVACY_POLICY = @@ -90,6 +91,9 @@ export const GITHUB_MAIN_STAGE_NAME = "Main Stage"; // How often to refresh events schedule export const REFETCH_SCHEDULE_MS = 10 * 60 * 1000; // 10 mins +export const SCHEDULE_LONG_EVENT_LENGTH_MIN = 60; +export const SCHEDULE_MEDIUM_EVENT_LENGTH_MIN = 45; +export const SCHEDULE_SHORT_EVENT_LENGTH_MIN = 10; // @debt FIVE_MINUTES_MS is deprecated; use utils/time or date-fns functions instead // How often to update location for counting @@ -634,8 +638,6 @@ export const DEFAULT_SHOW_SHOUTOUTS = true; export const DEFAULT_SHOW_USER_STATUSES = true; -export const ZENDESK_URL_PREFIXES = ["/admin"]; - export const REACTIONS_CONTAINER_HEIGHT_IN_SEATS = 2; // Audience diff --git a/src/types/agora.ts b/src/types/agora.ts index f63b5ee7f2..e13f3fd7eb 100644 --- a/src/types/agora.ts +++ b/src/types/agora.ts @@ -12,15 +12,10 @@ export enum AgoraClientConnectionState { DISCONNECTING = "DISCONNECTING", } -export interface UseAgoraRemotesProps { - client?: IAgoraRTCClient; -} export type UseAgoraRemotesReturn = IAgoraRTCRemoteUser[]; -export interface UseAgoraScreenShareProps { - client?: IAgoraRTCClient; -} export interface UseAgoraScreenShareReturn { + client?: IAgoraRTCClient; localScreenTrack?: ILocalVideoTrack; stopShare(): void; shareScreen(): Promise; @@ -28,10 +23,8 @@ export interface UseAgoraScreenShareReturn { leaveChannel(): Promise; } -export interface UseAgoraCameraProps { - client?: IAgoraRTCClient; -} export interface UseAgoraCameraReturn { + client?: IAgoraRTCClient; isCameraOn: boolean; isMicrophoneOn: boolean; localCameraTrack?: ILocalVideoTrack; diff --git a/src/types/venues.ts b/src/types/venues.ts index 1b6b2222e3..3b0d407631 100644 --- a/src/types/venues.ts +++ b/src/types/venues.ts @@ -106,7 +106,6 @@ export interface Venue_v2_AdvancedConfig { showNametags?: UsernameVisibility; showRadio?: boolean; showRangers?: boolean; - showZendesk?: boolean; } export interface Venue_v2_EntranceConfig { @@ -183,7 +182,6 @@ export interface BaseVenue { showRadio?: boolean; showBadges?: boolean; showNametags?: UsernameVisibility; - showZendesk?: boolean; showUserStatus?: boolean; } diff --git a/src/utils/calendar.ts b/src/utils/calendar.ts index 5694ecbfbb..31d7b4b3c0 100644 --- a/src/utils/calendar.ts +++ b/src/utils/calendar.ts @@ -19,7 +19,7 @@ export const createCalendar = ({ calendar.createEvent({ start: eventStartTime(event), end: eventEndTime(event), - organizer: `${event.host} `, // string format: "name ". email cannot be blank + organizer: `${event.host || "Unknown"} `, // string format: "name ". email cannot be blank description: event.description, summary: event.name, url: getFullVenueInsideUrl(event.venueId), diff --git a/src/utils/loadScript.ts b/src/utils/loadScript.ts deleted file mode 100644 index be8236b327..0000000000 --- a/src/utils/loadScript.ts +++ /dev/null @@ -1,13 +0,0 @@ -type ScriptProps = "src" | "id" | "onload"; -// eslint-disable-next-line @typescript-eslint/no-explicit-any -type LoadScriptProps = Record; - -export const loadScript = (options: LoadScriptProps) => { - const script = document.createElement("script"); - - Object.entries(options).forEach(([propertyName, property]) => { - script[propertyName as ScriptProps] = property; - }); - - document.querySelector("body")?.appendChild(script); -}; diff --git a/src/utils/zendesk.ts b/src/utils/zendesk.ts deleted file mode 100644 index 338e4e5824..0000000000 --- a/src/utils/zendesk.ts +++ /dev/null @@ -1,42 +0,0 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ - -import { ZENDESK_URL_PREFIXES } from "settings"; -import { loadScript } from "./loadScript"; - -const ZENDESK_SCRIPT_SRC = - "https://static.zdassets.com/ekr/snippet.js?key=57ad1fbc-d174-4561-8102-b3c8d98436d5"; - -const ZENDESK_SCRIPT_ID = "ze-snippet"; -const ZENDESK_WARMUP_DELAY_MS = 6000; - -export const initializeZendesk = () => { - loadScript({ - src: ZENDESK_SCRIPT_SRC, - id: ZENDESK_SCRIPT_ID, - onload: hideZendeskWidget, - }); -}; - -const hideZendeskWidget = () => { - if ((window as any).zE) { - const showZendeskForThisPath = ZENDESK_URL_PREFIXES.find((prefix) => - window.location.pathname.startsWith(prefix) - ); - - if (!showZendeskForThisPath) { - (window as any).zE("webWidget", "hide"); - } - } -}; - -export const showZendeskWidget = () => { - if ((window as any).zE) { - (window as any).zE("webWidget", "show"); - } else { - setTimeout(() => { - if ((window as any).zE) { - (window as any).zE("webWidget", "show"); - } - }, ZENDESK_WARMUP_DELAY_MS); - } -};