diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 0000000..8477585 --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,7 @@ +/* eslint-env node */ +module.exports = { + extends: ['@dcl/eslint-config/dapps'], + parserOptions: { + project: ['tsconfig.json'] + } +} diff --git a/.prettierrc.json b/.prettierrc.json new file mode 100644 index 0000000..56f7d09 --- /dev/null +++ b/.prettierrc.json @@ -0,0 +1,8 @@ +{ + "semi": false, + "singleQuote": true, + "printWidth": 140, + "tabWidth": 2, + "trailingComma": "none", + "arrowParens": "avoid" +} diff --git a/src/assets/images/background.png b/src/assets/images/background.png index 92fbca3..05231b1 100644 Binary files a/src/assets/images/background.png and b/src/assets/images/background.png differ diff --git a/src/assets/images/meet-on-decentraland.png b/src/assets/images/meet-on-decentraland.png new file mode 100644 index 0000000..effc3d1 Binary files /dev/null and b/src/assets/images/meet-on-decentraland.png differ diff --git a/src/components/Modals/index.ts b/src/components/Modals/index.ts index 9a65e38..69add33 100644 --- a/src/components/Modals/index.ts +++ b/src/components/Modals/index.ts @@ -1 +1 @@ -export { default as LoginModal } from "decentraland-dapps/dist/containers/LoginModal" +export { default as LoginModal } from 'decentraland-dapps/dist/containers/LoginModal' diff --git a/src/components/PageLayout/PageLayout.tsx b/src/components/PageLayout/PageLayout.tsx index f3627d3..1a3a0de 100644 --- a/src/components/PageLayout/PageLayout.tsx +++ b/src/components/PageLayout/PageLayout.tsx @@ -1,7 +1,7 @@ -import React from "react" -import classNames from "classnames" -import { Props } from "./PageLayout.types" -import styles from "./PageLayout.module.css" +import React from 'react' +import classNames from 'classnames' +import { Props } from './PageLayout.types' +import styles from './PageLayout.module.css' const PageLayout = ({ children, className }: Props) => { return ( diff --git a/src/components/Pages/Conference/Conference.container.ts b/src/components/Pages/Conference/Conference.container.ts index 0aeb35b..0be1522 100644 --- a/src/components/Pages/Conference/Conference.container.ts +++ b/src/components/Pages/Conference/Conference.container.ts @@ -1,30 +1,24 @@ -import { connect } from "react-redux"; -import { - getAddress, - isConnecting, -} from "decentraland-dapps/dist/modules/wallet/selectors"; -import { isLoggingIn } from "../../../modules/identity/selector"; -import { getServer, getToken } from "../../../modules/conference/selector"; -import { RootState } from "../../../modules/reducer"; -import withRouter from "../../../utils/WithRouter"; -import Conference from "./Conference"; -import { MapDispatch, MapStateProps, OwnProps } from "./Conference.types"; +import { connect } from 'react-redux' +import { getAddress, isConnecting } from 'decentraland-dapps/dist/modules/wallet/selectors' +import { getServer, getToken } from '../../../modules/conference/selector' +import { isLoggingIn } from '../../../modules/identity/selector' +import { RootState } from '../../../modules/reducer' +import withRouter from '../../../utils/WithRouter' +import Conference from './Conference' +import { MapDispatch, MapStateProps, OwnProps } from './Conference.types' -const mapStateToProps = ( - state: RootState, - ownProps: OwnProps -): MapStateProps => { - const addressFromPath = ownProps.router.params.profileAddress; +const mapStateToProps = (state: RootState, ownProps: OwnProps): MapStateProps => { + const addressFromPath = ownProps.router.params.profileAddress return { profileAddress: addressFromPath?.toLowerCase(), isLoading: isLoggingIn(state) || isConnecting(state), loggedInAddress: getAddress(state)?.toLowerCase(), server: getServer(state), - token: getToken(state), - }; -}; + token: getToken(state) + } +} -const mapDispatch = (_dispatch: MapDispatch): any => ({}); +const mapDispatch = (_dispatch: MapDispatch): any => ({}) -export default withRouter(connect(mapStateToProps, mapDispatch)(Conference)); +export default withRouter(connect(mapStateToProps, mapDispatch)(Conference)) diff --git a/src/components/Pages/Conference/Conference.tsx b/src/components/Pages/Conference/Conference.tsx index 12cb3ed..774ca10 100644 --- a/src/components/Pages/Conference/Conference.tsx +++ b/src/components/Pages/Conference/Conference.tsx @@ -1,9 +1,9 @@ -import React from "react" -import { Props } from "./Conference.types" -import { LiveKitRoom } from "@livekit/components-react" -import { VideoConference } from "../../VideoConference/Videoconference" -import "@livekit/components-styles" -import "./Conference.css" +import React from 'react' +import { LiveKitRoom } from '@livekit/components-react' +import '@livekit/components-styles' +import { VideoConference } from '../../VideoConference/Videoconference' +import { Props } from './Conference.types' +import './Conference.css' export default function Conference(props: Props) { const { token, server } = props diff --git a/src/components/Pages/Conference/Conference.types.ts b/src/components/Pages/Conference/Conference.types.ts index 5038d25..6482362 100644 --- a/src/components/Pages/Conference/Conference.types.ts +++ b/src/components/Pages/Conference/Conference.types.ts @@ -1,5 +1,5 @@ -import { Dispatch } from "redux" -import { RouterProps } from "../../../utils/WithRouter" +import { Dispatch } from 'redux' +import { RouterProps } from '../../../utils/WithRouter' export type Props = { loggedInAddress?: string @@ -9,7 +9,7 @@ export type Props = { token?: string } -export type MapStateProps = Pick +export type MapStateProps = Pick export type MapDispatch = Dispatch type Params = { profileAddress?: string diff --git a/src/components/Pages/Conference/index.ts b/src/components/Pages/Conference/index.ts index 8316fe6..4055dbc 100644 --- a/src/components/Pages/Conference/index.ts +++ b/src/components/Pages/Conference/index.ts @@ -1,2 +1,2 @@ -import Conference from "./Conference.container" +import Conference from './Conference.container' export default Conference diff --git a/src/components/Pages/ConnectToWorld/ConnectToWorld.container.ts b/src/components/Pages/ConnectToWorld/ConnectToWorld.container.ts new file mode 100644 index 0000000..2f66c16 --- /dev/null +++ b/src/components/Pages/ConnectToWorld/ConnectToWorld.container.ts @@ -0,0 +1,31 @@ +import { connect } from 'react-redux' +import { getAddress, isConnecting } from 'decentraland-dapps/dist/modules/wallet/selectors' +import { setServer, setToken } from '../../../modules/conference/action' +import { config } from '../../../modules/config' +import { getCurrentIdentity, isLoggingIn } from '../../../modules/identity/selector' +import { RootState } from '../../../modules/reducer' +import withRouter from '../../../utils/WithRouter' +import { getPreviouslyLoadedServers } from '../../../utils/worldServers' +import MainPage from './ConnectToWorld' +import { MapDispatch, MapDispatchProps, MapStateProps, OwnProps } from './ConnectToWorld.types' + +const mapStateToProps = (state: RootState, ownProps: OwnProps): MapStateProps => { + const identity = getCurrentIdentity(state) + return { + isLoading: isLoggingIn(state) || isConnecting(state), + loggedInAddress: getAddress(state)?.toLowerCase(), + previouslyLoadedServers: getPreviouslyLoadedServers(), + worldsContentServerUrl: + new URLSearchParams(ownProps.router.location.search).get('worlds-content-server-url') || config.get('WORLDS_CONTENT_SERVER_URL'), + identity + } +} + +const mapDispatch = (dispatch: MapDispatch): MapDispatchProps => ({ + onSubmitConnectForm: (server: string, token: string) => { + dispatch(setServer({ server })) + dispatch(setToken({ token })) + } +}) + +export default withRouter(connect(mapStateToProps, mapDispatch)(MainPage)) diff --git a/src/components/Pages/ConnectToWorld/ConnectToWorld.module.css b/src/components/Pages/ConnectToWorld/ConnectToWorld.module.css new file mode 100644 index 0000000..55cd286 --- /dev/null +++ b/src/components/Pages/ConnectToWorld/ConnectToWorld.module.css @@ -0,0 +1,81 @@ +:global(#root) { + height: 100%; +} + +.ConnectToWorld { + display: flex; + justify-content: center; + align-items: center; + height: 100%; +} + +.ConnectToWorld .content { + background: var(--background); + padding: 24px 141px 50px; + border-radius: 10px; + text-align: center; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + row-gap: 20px; +} + +.ConnectToWorld .content .title { + font-size: 20px; + font-weight: 700; + line-height: 24px; + letter-spacing: 0px; +} + +.ConnectToWorld .content .form { + width: 100%; +} + +.ConnectToWorld .content .description { + font-size: 15px; + font-weight: 400; + line-height: 24px; + letter-spacing: 0px; +} + +.ConnectToWorld .content .img { + max-width: 260px; +} + +.ConnectToWorld .content .inputContainer { + width: 100%; + display: flex; + flex-direction: column; + row-gap: 5px; + text-align: left; +} + +.ConnectToWorld .content .inputContainer .label { + font-size: 12px; + font-weight: 400; + line-height: 15px; + letter-spacing: 0px; +} + +.ConnectToWorld .content :global(.select-field.error.warning p.message) { + margin-bottom: 15px; +} + +@media (min-width: 769px) { + .content :global(.SignIn.center) { + min-width: 700px; + } +} + +@media (max-width: 768px) { + .ConnectToWorld .content :global(.dcl.field), .ConnectToWorld .content :global(.dcl.select-field) { + min-width: auto; + } + + .ConnectToWorld .content { + height: 100%; + border-radius: unset; + padding: 24px; + } +} \ No newline at end of file diff --git a/src/components/Pages/ConnectToWorld/ConnectToWorld.tsx b/src/components/Pages/ConnectToWorld/ConnectToWorld.tsx new file mode 100644 index 0000000..d7a1328 --- /dev/null +++ b/src/components/Pages/ConnectToWorld/ConnectToWorld.tsx @@ -0,0 +1,179 @@ +import React, { ChangeEvent, useCallback, useEffect, useState } from 'react' +import { useNavigate } from 'react-router-dom' +import { AuthIdentity } from '@dcl/crypto' +import { t } from 'decentraland-dapps/dist/modules/translation/utils' +import { Button, Loader, SelectField, Field, DropdownProps, Form } from 'decentraland-ui' +import meetOnDecentralandImg from '../../../assets/images/meet-on-decentraland.png' +import { locations } from '../../../modules/routing/locations' +import { signedFetch } from '../../../utils/auth' +import { isErrorMessage } from '../../../utils/errors' +import { flatFetch } from '../../../utils/flat-fetch' +import { addServerToPreviouslyLoaded } from '../../../utils/worldServers' +import { PageLayout } from '../../PageLayout' +import { Props } from './ConnectToWorld.types' +import styles from './ConnectToWorld.module.css' + +function ConnectToWorld(props: Props) { + const [selectedServer, setSelectedServer] = useState('') + const [error, setError] = useState('') + const [availableServers, setAvailableServers] = useState([]) + const [isConnectingToServer, setIsConnectingToServer] = useState(false) + + const { isLoading, loggedInAddress, identity, previouslyLoadedServers, worldsContentServerUrl, onSubmitConnectForm } = props + + const navigate = useNavigate() + + useEffect(() => { + if (!loggedInAddress && !isLoading) { + navigate(locations.signIn(locations.root(worldsContentServerUrl))) + } + }, [isLoading, loggedInAddress]) + + useEffect(() => { + if (previouslyLoadedServers) { + setAvailableServers(previouslyLoadedServers) + setSelectedServer(previouslyLoadedServers[0]) + } + }, [previouslyLoadedServers, setAvailableServers]) + + const handleChange = useCallback( + (e: ChangeEvent) => { + setError('') + setSelectedServer(e.target.value) + }, + [setSelectedServer] + ) + + const handleSelectChange = useCallback( + (_event: React.SyntheticEvent, { value }: DropdownProps) => { + setError('') + const newOption = value as string + if (!availableServers.includes(newOption)) setAvailableServers([...availableServers, newOption]) + setSelectedServer(newOption) + }, + [availableServers, setAvailableServers, setSelectedServer] + ) + + async function livekitConnect(identity: AuthIdentity, worldServer: string, worldName: string) { + const aboutResponse = await flatFetch(`${worldServer}/world/${worldName}/about`) + console.log(aboutResponse.text) + if (aboutResponse.status === 200) { + const url = JSON.parse(aboutResponse.text!) + ['comms']['fixedAdapter'].replace('signed-login:', '') + .replace('get-comms-adapter', 'cast-adapter') + const response = await signedFetch( + url, + identity, + { + method: 'POST' + }, + { + signer: 'dcl:explorer', + intent: 'dcl:explorer:comms-handshake' + } + ) + + if (response.status === 200) { + console.log(response.text) + return JSON.parse(response.text!) + } else { + let message = '' + try { + message = JSON.parse(response.text || '')?.message + } catch (e) { + message = response.text || '' + } + throw Error(message) + } + // throw Error(`Failed to connect to LiveKit: ${JSON.stringify(response.text || response.json?.message)}`) + } else if (aboutResponse.status === 404) { + throw Error(`World ${worldName} not found`) + } + throw Error('An error has occurred') + } + + const handleClick = useCallback( + async (event: React.MouseEvent) => { + event.preventDefault() + setError('') + setIsConnectingToServer(true) + + try { + if (!identity) return + + const response: { url: string; token: string } = await livekitConnect(identity, worldsContentServerUrl, selectedServer) + onSubmitConnectForm(response.url, response.token) + addServerToPreviouslyLoaded(selectedServer) + navigate(`/meet/${encodeURIComponent(response.url)}?token=${encodeURIComponent(response.token)}`) + } catch (error) { + console.error('ERROR livekit connect', error) + if (isErrorMessage(error)) setError(error.message) + } + }, + [identity, selectedServer, onSubmitConnectForm] + ) + + return ( + + {isLoading ? ( + + ) : ( +
+
+

{t('connect_to_world.title')}

+

{t('connect_to_world.description')}

+ {t('connect_to_world.image_alt')} +
+
+ + {availableServers.length > 0 ? ( + ({ + value: server, + text: server + }))} + onAddItem={handleSelectChange} + onChange={handleSelectChange} + allowAdditions + error={!!error} + message={error} + /> + ) : ( + + )} +
+ +
+
+
+ )} +
+ ) +} + +export default ConnectToWorld diff --git a/src/components/Pages/ConnectToWorld/ConnectToWorld.types.ts b/src/components/Pages/ConnectToWorld/ConnectToWorld.types.ts new file mode 100644 index 0000000..b8e5a7e --- /dev/null +++ b/src/components/Pages/ConnectToWorld/ConnectToWorld.types.ts @@ -0,0 +1,21 @@ +import { Dispatch } from 'redux' +import { AuthIdentity } from '@dcl/crypto' +import { RouterProps } from '../../../utils/WithRouter' + +export type Props = { + loggedInAddress?: string + isLoading: boolean + previouslyLoadedServers: string[] | null + identity: AuthIdentity | null + worldsContentServerUrl: string + onSubmitConnectForm: (server: string, token: string) => void +} + +export type MapStateProps = Pick +export type MapDispatchProps = Pick +export type MapDispatch = Dispatch + +type Params = Record +export type OwnProps = { + router: RouterProps +} diff --git a/src/components/Pages/ConnectToWorld/index.ts b/src/components/Pages/ConnectToWorld/index.ts new file mode 100644 index 0000000..c62b244 --- /dev/null +++ b/src/components/Pages/ConnectToWorld/index.ts @@ -0,0 +1,2 @@ +import ConnectToWorld from './ConnectToWorld.container' +export default ConnectToWorld diff --git a/src/components/Pages/MainPage/MainPage.container.ts b/src/components/Pages/MainPage/MainPage.container.ts deleted file mode 100644 index 244051b..0000000 --- a/src/components/Pages/MainPage/MainPage.container.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { connect } from "react-redux" -import { getAddress, isConnecting } from "decentraland-dapps/dist/modules/wallet/selectors" -import { getCurrentIdentity, isLoggingIn } from "../../../modules/identity/selector" -import { RootState } from "../../../modules/reducer" -import withRouter from "../../../utils/WithRouter" -import MainPage from "./MainPage" -import { MapDispatch, MapDispatchProps, MapStateProps, OwnProps } from "./MainPage.types" -import { setServer, setToken } from "../../../modules/conference/action" - -const mapStateToProps = (state: RootState, ownProps: OwnProps): MapStateProps => { - const addressFromPath = ownProps.router.params.profileAddress - const identity = getCurrentIdentity(state) - - return { - profileAddress: addressFromPath?.toLowerCase(), - isLoading: isLoggingIn(state) || isConnecting(state), - loggedInAddress: getAddress(state)?.toLowerCase(), - identity, - } -} - -const mapDispatch = (dispatch: MapDispatch): MapDispatchProps => ({ - onSubmitConnectForm: (server: string, token:string) => { - dispatch(setServer({ server })) - dispatch(setToken({ token })) - }, -}) - -export default withRouter(connect(mapStateToProps, mapDispatch)(MainPage)) diff --git a/src/components/Pages/MainPage/MainPage.module.css b/src/components/Pages/MainPage/MainPage.module.css deleted file mode 100644 index 9a21695..0000000 --- a/src/components/Pages/MainPage/MainPage.module.css +++ /dev/null @@ -1,29 +0,0 @@ -:global(#root) { - height: 100%; - width: 100%; -} - -.MainPage { - display: flex; - flex-direction: row; - gap: 5%; - justify-content: center; - height: 100%; - padding: 30px; -} - -.MainPage .infoContainer { - backdrop-filter: blur(20px); - background: linear-gradient(151deg, rgba(255, 255, 255, 0.24) 0%, rgba(255, 255, 255, 0.06) 100%); - box-shadow: 0px 4px 24px -1px rgba(0, 0, 0, 0.2); - border-radius: 10px; - display: flex; - flex: 1; - flex-direction: column; - padding: 32px; -} - -.MainPage .tab { - color: var(--text); - font-weight: 700; -} diff --git a/src/components/Pages/MainPage/MainPage.tsx b/src/components/Pages/MainPage/MainPage.tsx deleted file mode 100644 index 4058c78..0000000 --- a/src/components/Pages/MainPage/MainPage.tsx +++ /dev/null @@ -1,132 +0,0 @@ -import React, { ChangeEvent, useEffect, useState } from "react"; -import { useNavigate } from "react-router-dom"; -import Divider from "semantic-ui-react/dist/commonjs/elements/Divider/Divider"; -import { Loader } from "decentraland-ui"; -import { locations } from "../../../modules/routing/locations"; -import { PageLayout } from "../../PageLayout"; -import { Props } from "./MainPage.types"; -import styles from "./MainPage.module.css"; -import { flatFetch } from "../../../utils/flat-fetch"; -import { signedFetch } from "../../../utils/auth"; -import { AuthIdentity } from "@dcl/crypto"; - -function MainPage(props: Props) { - const [selectedServer, setSelectedServer] = useState(""); - const [isConnectingToServer, _setIsConnectingToServer] = useState(false); - - const { - isLoading, - onFetchProfile, - profileAddress, - loggedInAddress, - identity, - onSubmitConnectForm, - } = props; - - const navigate = useNavigate(); - - useEffect(() => { - if (profileAddress) { - onFetchProfile(profileAddress); - } - }, [profileAddress]); - - useEffect(() => { - if (!profileAddress && !loggedInAddress && !isLoading) { - navigate(locations.signIn(locations.root())); - } - }, [isLoading, loggedInAddress, profileAddress]); - - const handleChange = (e: ChangeEvent) => { - setSelectedServer(e.target.value); - }; - - async function livekitConnect( - identity: AuthIdentity, - worldServer: string, - worldName: string - ) { - const aboutResponse = await flatFetch( - `${worldServer}/world/${worldName}/about` - ); - console.log(aboutResponse.text); - if (aboutResponse.status === 200) { - const url = JSON.parse(aboutResponse.text!) - ["comms"]["fixedAdapter"].replace("signed-login:", "") - .replace("get-comms-adapter", "cast-adapter"); - const response = await signedFetch( - url, - identity, - { - method: "POST", - }, - { - signer: "dcl:explorer", - intent: "dcl:explorer:comms-handshake", - } - ); - - if (response.status === 200) { - console.log(response.text); - return JSON.parse(response.text!); - } else { - let message = ""; - try { - message = JSON.parse(response.text || "")?.message; - } catch (e) { - message = response.text || ""; - } - throw Error(message); - } - // throw Error(`Failed to connect to LiveKit: ${JSON.stringify(response.text || response.json?.message)}`) - } else if (aboutResponse.status === 404) { - throw Error(`World ${worldName} not found`); - } - throw Error("An error has occurred"); - } - - const handleClick = () => { - livekitConnect( - identity!, - "https://worlds-content-server.decentraland.zone", - selectedServer - ) - .then((response: { url: string; token: string }) => { - onSubmitConnectForm(response.url, response.token); - navigate( - `/meet/${encodeURIComponent(response.url)}?token=${encodeURIComponent( - response.token - )}` - ); - }) - .catch((err) => { - console.error("ERROR livekit connect", err); - }); - }; - - return ( - - {isLoading || isConnectingToServer ? ( - - ) : ( -
-
- - -
- - - -
-
-
- )} -
- ); -} - -export default MainPage; diff --git a/src/components/Pages/MainPage/MainPage.types.ts b/src/components/Pages/MainPage/MainPage.types.ts deleted file mode 100644 index 7c478f8..0000000 --- a/src/components/Pages/MainPage/MainPage.types.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { Dispatch } from "redux" -import { loadProfileRequest } from "decentraland-dapps/dist/modules/profile/actions" -import { RouterProps } from "../../../utils/WithRouter" -import { AuthIdentity } from "@dcl/crypto" - -export type Props = { - onFetchProfile: typeof loadProfileRequest - loggedInAddress?: string - profileAddress?: string - isLoading: boolean - onSubmitConnectForm: (server: string, token: string) => void - identity: AuthIdentity | null -} - -export type MapStateProps = Pick -export type MapDispatchProps = Pick -export type MapDispatch = Dispatch -type Params = { - profileAddress?: string -} -export type OwnProps = { - router: RouterProps -} diff --git a/src/components/Pages/MainPage/constants.ts b/src/components/Pages/MainPage/constants.ts deleted file mode 100644 index cecff0a..0000000 --- a/src/components/Pages/MainPage/constants.ts +++ /dev/null @@ -1 +0,0 @@ -export const nullAddress = '0x0000000000000000000000000000000000000000' diff --git a/src/components/Pages/MainPage/index.ts b/src/components/Pages/MainPage/index.ts deleted file mode 100644 index 8fe7f09..0000000 --- a/src/components/Pages/MainPage/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -import MainPage from './MainPage.container' -export default MainPage diff --git a/src/components/Pages/SignInPage/SignInPage.module.css b/src/components/Pages/SignInPage/SignInPage.module.css new file mode 100644 index 0000000..a269a75 --- /dev/null +++ b/src/components/Pages/SignInPage/SignInPage.module.css @@ -0,0 +1,27 @@ +.content { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + height: 100%; +} + +.content :global(.SignIn.center) { + background: var(--background); + padding: 24px 11px 14px 11px; + border-radius: 10px; + position: unset; +} + +@media (min-width: 769px) { + .content :global(.SignIn.center) { + min-width: 700px; + } +} + +@media (max-width: 768px) { + .content :global(.SignIn.center) { + height: 100%; + border-radius: unset; + } +} \ No newline at end of file diff --git a/src/components/Pages/SignInPage/SignInPage.tsx b/src/components/Pages/SignInPage/SignInPage.tsx index ad3b6e6..95573b0 100644 --- a/src/components/Pages/SignInPage/SignInPage.tsx +++ b/src/components/Pages/SignInPage/SignInPage.tsx @@ -3,6 +3,7 @@ import { useSearchParams, useNavigate } from 'react-router-dom' import { default as SignIn } from 'decentraland-dapps/dist/containers/SignInPage' import { PageLayout } from '../../PageLayout' import { Props } from './SignInPage.types' +import styles from './SignInPage.module.css' const SignInPage = (props: Props) => { const { isConnected, onConnect } = props @@ -19,7 +20,9 @@ const SignInPage = (props: Props) => { return ( - +
+ +
) } diff --git a/src/components/VideoConference/ParticipantTile.tsx b/src/components/VideoConference/ParticipantTile.tsx index 299121f..7cd6b7c 100644 --- a/src/components/VideoConference/ParticipantTile.tsx +++ b/src/components/VideoConference/ParticipantTile.tsx @@ -1,8 +1,5 @@ -import * as React from "react" -import type { Participant, TrackPublication } from "livekit-client" -import { Track } from "livekit-client" -import type { ParticipantClickEvent, TrackReferenceOrPlaceholder } from "@livekit/components-core" -import { isParticipantSourcePinned } from "@livekit/components-core" +import * as React from 'react' +import { isParticipantSourcePinned } from '@livekit/components-core' import { AudioTrack, ConnectionQualityIndicator, @@ -15,9 +12,12 @@ import { useMaybeLayoutContext, useMaybeParticipantContext, useMaybeTrackContext, - useParticipantTile, -} from "@livekit/components-react" -import Profile from "decentraland-dapps/dist/containers/Profile" + useParticipantTile +} from '@livekit/components-react' +import { Track } from 'livekit-client' +import Profile from 'decentraland-dapps/dist/containers/Profile' +import type { ParticipantClickEvent, TrackReferenceOrPlaceholder } from '@livekit/components-core' +import type { Participant, TrackPublication } from 'livekit-client' /** @public */ export function ParticipantContextIfNeeded( @@ -40,7 +40,7 @@ export interface ParticipantTileProps extends React.HTMLAttributes void - imageSize?: "normal" | "large" | "huge" | "massive" + imageSize?: 'normal' | 'large' | 'huge' | 'massive' } /** @@ -69,7 +69,7 @@ export function ParticipantTile({ const trackRef: TrackReferenceOrPlaceholder = useMaybeTrackContext() ?? { participant: p, source, - publication, + publication } const { elementProps } = useParticipantTile({ @@ -78,7 +78,7 @@ export function ParticipantTile({ source: trackRef.source, publication: trackRef.publication, disableSpeakingIndicator, - onParticipantClick, + onParticipantClick }) const layoutContext = useMaybeLayoutContext() @@ -92,7 +92,7 @@ export function ParticipantTile({ layoutContext.pin.dispatch && isParticipantSourcePinned(trackRef.participant, trackRef.source, layoutContext.pin.state) ) { - layoutContext.pin.dispatch({ msg: "clear_pin" }) + layoutContext.pin.dispatch({ msg: 'clear_pin' }) } }, [trackRef.participant, layoutContext, trackRef.source] @@ -101,17 +101,17 @@ export function ParticipantTile({ const participantWithProfile: Participant = React.useMemo( () => ({ ...trackRef.participant, - name: "Edita me", + name: 'Edita me' }), [trackRef.participant] ) as Participant return ( -
+
{children ?? ( <> - {trackRef.publication?.kind === "video" || + {trackRef.publication?.kind === 'video' || trackRef.source === Track.Source.Camera || trackRef.source === Track.Source.ScreenShare ? ( {trackRef.source === Track.Source.Camera ? ( <> - + ) : ( diff --git a/src/components/VideoConference/Videoconference.tsx b/src/components/VideoConference/Videoconference.tsx index 6663cd1..a1830a4 100644 --- a/src/components/VideoConference/Videoconference.tsx +++ b/src/components/VideoConference/Videoconference.tsx @@ -1,8 +1,5 @@ -import * as React from "react" -import type { WidgetState } from "@livekit/components-core" -import { isEqualTrackRef, isTrackReference, log, isWeb } from "@livekit/components-core" -import { RoomEvent, Track } from "livekit-client" -import type { TrackReferenceOrPlaceholder } from "@livekit/components-core" +import * as React from 'react' +import { isEqualTrackRef, isTrackReference, log, isWeb } from '@livekit/components-core' import { CarouselView, Chat, @@ -15,11 +12,13 @@ import { MessageFormatter, RoomAudioRenderer, useCreateLayoutContext, - useParticipants, + // useParticipants, usePinnedTracks, - useTracks, -} from "@livekit/components-react" -import { ParticipantTile } from "./ParticipantTile" + useTracks +} from '@livekit/components-react' +import { RoomEvent, Track } from 'livekit-client' +import { ParticipantTile } from './ParticipantTile' +import type { TrackReferenceOrPlaceholder, WidgetState } from '@livekit/components-core' /** * @public @@ -51,46 +50,43 @@ export function VideoConference({ chatMessageFormatter, ...props }: VideoConfere const tracks = useTracks( [ { source: Track.Source.Camera, withPlaceholder: true }, - { source: Track.Source.ScreenShare, withPlaceholder: false }, + { source: Track.Source.ScreenShare, withPlaceholder: false } ], { updateOnlyOn: [RoomEvent.ActiveSpeakersChanged] } ) - const participants = useParticipants({ - updateOnlyOn: [RoomEvent.ParticipantConnected, RoomEvent.ParticipantDisconnected], - }) + // TODO: remove this unused declaration if it's not needed + /* const participants = useParticipants({ + updateOnlyOn: [RoomEvent.ParticipantConnected, RoomEvent.ParticipantDisconnected] + }) */ const widgetUpdate = (state: WidgetState) => { - log.debug("updating widget state", state) + log.debug('updating widget state', state) setWidgetState(state) } const layoutContext = useCreateLayoutContext() - const screenShareTracks = tracks - .filter(isTrackReference) - .filter((track) => track.publication.source === Track.Source.ScreenShare) + const screenShareTracks = tracks.filter(isTrackReference).filter(track => track.publication.source === Track.Source.ScreenShare) const focusTrack = usePinnedTracks(layoutContext)?.[0] - const carouselTracks = tracks.filter((track) => !isEqualTrackRef(track, focusTrack)) + const carouselTracks = tracks.filter(track => !isEqualTrackRef(track, focusTrack)) React.useEffect(() => { // If screen share tracks are published, and no pin is set explicitly, auto set the screen share. if (screenShareTracks.length > 0 && lastAutoFocusedScreenShareTrack.current === null) { - log.debug("Auto set screen share focus:", { newScreenShareTrack: screenShareTracks[0] }) - layoutContext.pin.dispatch?.({ msg: "set_pin", trackReference: screenShareTracks[0] }) + log.debug('Auto set screen share focus:', { newScreenShareTrack: screenShareTracks[0] }) + layoutContext.pin.dispatch?.({ msg: 'set_pin', trackReference: screenShareTracks[0] }) lastAutoFocusedScreenShareTrack.current = screenShareTracks[0] } else if ( lastAutoFocusedScreenShareTrack.current && - !screenShareTracks.some( - (track) => track.publication.trackSid === lastAutoFocusedScreenShareTrack.current?.publication?.trackSid - ) + !screenShareTracks.some(track => track.publication.trackSid === lastAutoFocusedScreenShareTrack.current?.publication?.trackSid) ) { - log.debug("Auto clearing screen share focus.") - layoutContext.pin.dispatch?.({ msg: "clear_pin" }) + log.debug('Auto clearing screen share focus.') + layoutContext.pin.dispatch?.({ msg: 'clear_pin' }) lastAutoFocusedScreenShareTrack.current = null } - }, [screenShareTracks.map((ref) => ref.publication.trackSid).join(), focusTrack?.publication?.trackSid]) + }, [screenShareTracks.map(ref => ref.publication.trackSid).join(), focusTrack?.publication?.trackSid]) return (
@@ -119,7 +115,7 @@ export function VideoConference({ chatMessageFormatter, ...props }: VideoConfere )}
- + )} diff --git a/src/lib/MarketplaceGraphClient.ts b/src/lib/MarketplaceGraphClient.ts deleted file mode 100644 index be8d8de..0000000 --- a/src/lib/MarketplaceGraphClient.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { graphql } from 'decentraland-dapps/dist/lib/graph' - -const BATCH_SIZE = 1000 - -const getSubdomainQuery = (owner: string, offset: number) => `{ - nfts(first: ${BATCH_SIZE}, skip: ${offset}, where: { owner: "${owner}", category: ens }) { - ens { - subdomain - } - } -}` - -type SubdomainTuple = { - ens: { - subdomain: string[] - } -} - -type SubdomainQueryResult = { - nfts: SubdomainTuple[] -} - -export class MarketplaceGraphClient { - constructor(private readonly _graphUrl: string) {} - - fetchENSList = async (address: string | undefined): Promise => { - if (!address) { - return [] - } - - const owner: string = address.toLowerCase() - let results: string[] = [] - let page: string[] = [] - let offset = 0 - let nextPage = true - - while (nextPage) { - const { nfts } = await graphql(this._graphUrl, getSubdomainQuery(owner, offset)) - - page = nfts.map(ntf => ntf.ens.subdomain.toString()) - results = [...results, ...page] - if (page.length === BATCH_SIZE) { - offset += BATCH_SIZE - } else { - nextPage = false - } - } - return results - } -} diff --git a/src/main.tsx b/src/main.tsx index a7db6c0..88c438c 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -1,39 +1,35 @@ /* eslint-disable import/order */ -import "semantic-ui-css/semantic.min.css" -import React from "react" -import ReactDOM from "react-dom" -import { Provider } from "react-redux" -import { RouterProvider, createBrowserRouter } from "react-router-dom" -import ModalProvider from "decentraland-dapps/dist/providers/ModalProvider" -import TranslationProvider from "decentraland-dapps/dist/providers/TranslationProvider" -import WalletProvider from "decentraland-dapps/dist/providers/WalletProvider" -import * as modals from "./components/Modals" -import MainPage from "./components/Pages/MainPage" -import SignInPage from "./components/Pages/SignInPage" -import { initStore } from "./modules/store" -import * as locales from "./modules/translation/locales" +import 'semantic-ui-css/semantic.min.css' +import React from 'react' +import ReactDOM from 'react-dom' +import { Provider } from 'react-redux' +import { RouterProvider, createBrowserRouter } from 'react-router-dom' +import ModalProvider from 'decentraland-dapps/dist/providers/ModalProvider' +import TranslationProvider from 'decentraland-dapps/dist/providers/TranslationProvider' +import WalletProvider from 'decentraland-dapps/dist/providers/WalletProvider' +import * as modals from './components/Modals' +import ConnectToWorld from './components/Pages/ConnectToWorld' +import SignInPage from './components/Pages/SignInPage' +import { initStore } from './modules/store' +import * as locales from './modules/translation/locales' +import Conference from './components/Pages/Conference' // These CSS styles must be defined last to avoid overriding other styles -import "decentraland-ui/dist/themes/alternative/dark-theme.css" -import "./index.css" -import Conference from "./components/Pages/Conference" +import 'decentraland-ui/dist/themes/alternative/light-theme.css' +import './index.css' const router = createBrowserRouter([ { - path: "/", - element: , + path: '/', + element: }, { - path: "/accounts/:profileAddress?", - element: , + path: 'sign-in', + element: }, { - path: "sign-in", - element: , - }, - { - path: "/meet/:server", - element: , - }, + path: '/meet/:server', + element: + } ]) const component = ( @@ -50,4 +46,4 @@ const component = ( ) -ReactDOM.render(component, document.getElementById("root") as HTMLElement) +ReactDOM.render(component, document.getElementById('root') as HTMLElement) diff --git a/src/modules/conference/action.ts b/src/modules/conference/action.ts index 399624e..034e6ff 100644 --- a/src/modules/conference/action.ts +++ b/src/modules/conference/action.ts @@ -1,9 +1,9 @@ -import { createAction } from "@reduxjs/toolkit" +import { createAction } from '@reduxjs/toolkit' -export const setServer = createAction<{ server: string }>("Set Server") +export const setServer = createAction<{ server: string }>('Set Server') export type SetServerAction = ReturnType -export const setToken = createAction<{ token: string }>("Set Token") +export const setToken = createAction<{ token: string }>('Set Token') export type SetTokenAction = ReturnType diff --git a/src/modules/conference/reducer.ts b/src/modules/conference/reducer.ts index 57197e5..c644e52 100644 --- a/src/modules/conference/reducer.ts +++ b/src/modules/conference/reducer.ts @@ -1,5 +1,5 @@ -import { createReducer } from "@reduxjs/toolkit" -import { setServer, setToken } from "./action" +import { createReducer } from '@reduxjs/toolkit' +import { setServer, setToken } from './action' export type ConferenceState = { token: string @@ -7,11 +7,11 @@ export type ConferenceState = { } export const INITIAL_STATE: ConferenceState = { - token: "", - server: "", + token: '', + server: '' } -export const conferenceReducer = createReducer(INITIAL_STATE, (builder) => +export const conferenceReducer = createReducer(INITIAL_STATE, builder => builder .addCase(setServer, (state, action) => { state.server = action.payload.server diff --git a/src/modules/conference/selector.ts b/src/modules/conference/selector.ts index 1fb1729..aa447d6 100644 --- a/src/modules/conference/selector.ts +++ b/src/modules/conference/selector.ts @@ -1,9 +1,9 @@ -import { createSelector } from "@reduxjs/toolkit" -import { RootState } from "../reducer" +import { createSelector } from '@reduxjs/toolkit' +import { RootState } from '../reducer' const getState = (state: RootState) => state.conference export const getToken = (state: RootState) => getState(state).token export const getServer = (state: RootState) => getState(state).server -export const isLoading = createSelector([getToken], (token) => !!token) +export const isLoading = createSelector([getToken], token => !!token) diff --git a/src/modules/reducer.ts b/src/modules/reducer.ts index eb6214a..b489d63 100644 --- a/src/modules/reducer.ts +++ b/src/modules/reducer.ts @@ -1,19 +1,12 @@ -import { configureStore, Reducer, Middleware, AnyAction, combineReducers, Store } from "@reduxjs/toolkit" -import { FeaturesState, featuresReducer as features } from "decentraland-dapps/dist/modules/features/reducer" -import { ModalState, modalReducer as modal } from "decentraland-dapps/dist/modules/modal/reducer" -import { ProfileState, profileReducer as profile } from "decentraland-dapps/dist/modules/profile/reducer" -import { - StorageState, - storageReducer as storage, - storageReducerWrapper, -} from "decentraland-dapps/dist/modules/storage/reducer" -import { - TranslationState, - translationReducer as translation, -} from "decentraland-dapps/dist/modules/translation/reducer" -import { WalletState, walletReducer as wallet } from "decentraland-dapps/dist/modules/wallet/reducer" -import { IdentityState, identityReducer as identity } from "./identity/reducer" -import { ConferenceState, conferenceReducer as conference } from "./conference/reducer" +import { configureStore, Reducer, Middleware, AnyAction, combineReducers, Store } from '@reduxjs/toolkit' +import { FeaturesState, featuresReducer as features } from 'decentraland-dapps/dist/modules/features/reducer' +import { ModalState, modalReducer as modal } from 'decentraland-dapps/dist/modules/modal/reducer' +import { ProfileState, profileReducer as profile } from 'decentraland-dapps/dist/modules/profile/reducer' +import { StorageState, storageReducer as storage, storageReducerWrapper } from 'decentraland-dapps/dist/modules/storage/reducer' +import { TranslationState, translationReducer as translation } from 'decentraland-dapps/dist/modules/translation/reducer' +import { WalletState, walletReducer as wallet } from 'decentraland-dapps/dist/modules/wallet/reducer' +import { ConferenceState, conferenceReducer as conference } from './conference/reducer' +import { IdentityState, identityReducer as identity } from './identity/reducer' export const createRootReducer = (middlewares: Middleware[], preloadedState = {}) => configureStore({ @@ -26,25 +19,19 @@ export const createRootReducer = (middlewares: Middleware[], preloadedState = {} translation: translation as Reducer, profile, identity, - conference, + conference }) ), preloadedState, - middleware: (getDefaultMiddleware) => + middleware: getDefaultMiddleware => getDefaultMiddleware({ thunk: false, serializableCheck: { // Ignore these action types - ignoredActions: [ - "[Request] Login", - "[Success] Login", - "Open modal", - "REDUX_PERSISTENCE_SAVE", - "REDUX_PERSISTENCE_LOAD", - ], - ignoredPaths: ["modal", "identity"], - }, - }).concat(middlewares), + ignoredActions: ['[Request] Login', '[Success] Login', 'Open modal', 'REDUX_PERSISTENCE_SAVE', 'REDUX_PERSISTENCE_LOAD'], + ignoredPaths: ['modal', 'identity'] + } + }).concat(middlewares) }) // We need to build the Store type manually due to the storageReducerWrapper function not propagating the type correctly diff --git a/src/modules/routing/locations.ts b/src/modules/routing/locations.ts index 27b8cb7..32e419c 100644 --- a/src/modules/routing/locations.ts +++ b/src/modules/routing/locations.ts @@ -1,8 +1,7 @@ export const locations = { - root: () => "/", - account: (address: string) => `/accounts/${address}`, + root: (worldsContentServerUrl?: string) => '/' + (worldsContentServerUrl ? `?worlds-content-server-url=${worldsContentServerUrl}` : ''), signIn: (redirectTo?: string) => { - return `/sign-in${redirectTo ? `?redirectTo=${encodeURIComponent(redirectTo)}` : ""}` + return `/sign-in${redirectTo ? `?redirectTo=${encodeURIComponent(redirectTo)}` : ''}` }, - meet: (server: string, token: string) => `/meet/${server}?token${token}`, + meet: (server: string, token: string) => `/meet/${server}?token${token}` } diff --git a/src/modules/saga.ts b/src/modules/saga.ts index eb6a22b..2f065e9 100644 --- a/src/modules/saga.ts +++ b/src/modules/saga.ts @@ -1,12 +1,12 @@ -import { all } from "redux-saga/effects" -import { createAnalyticsSaga } from "decentraland-dapps/dist/modules/analytics/sagas" -import { featuresSaga } from "decentraland-dapps/dist/modules/features/sagas" -import { createProfileSaga } from "decentraland-dapps/dist/modules/profile/sagas" -import { createWalletSaga } from "decentraland-dapps/dist/modules/wallet/sagas" -import { config } from "./config" -import { identitySaga } from "./identity/sagas" -import { modalSagas } from "./modal/sagas" -import { translationSaga } from "./translation/sagas" +import { all } from 'redux-saga/effects' +import { createAnalyticsSaga } from 'decentraland-dapps/dist/modules/analytics/sagas' +import { featuresSaga } from 'decentraland-dapps/dist/modules/features/sagas' +import { createProfileSaga } from 'decentraland-dapps/dist/modules/profile/sagas' +import { createWalletSaga } from 'decentraland-dapps/dist/modules/wallet/sagas' +import { config } from './config' +import { identitySaga } from './identity/sagas' +import { modalSagas } from './modal/sagas' +import { translationSaga } from './translation/sagas' const analyticsSaga = createAnalyticsSaga() @@ -15,23 +15,23 @@ export function* rootSaga() { analyticsSaga(), createWalletSaga({ // eslint-disable-next-line @typescript-eslint/naming-convention - CHAIN_ID: Number(config.get("CHAIN_ID")), + CHAIN_ID: Number(config.get('CHAIN_ID')), // eslint-disable-next-line @typescript-eslint/naming-convention POLL_INTERVAL: 0, // eslint-disable-next-line @typescript-eslint/naming-convention - TRANSACTIONS_API_URL: "https://transactions-api.decentraland.org/v1", + TRANSACTIONS_API_URL: 'https://transactions-api.decentraland.org/v1' })(), translationSaga(), identitySaga(), modalSagas(), - createProfileSaga({ peerUrl: config.get("PEER_URL") })(), + createProfileSaga({ peerUrl: config.get('PEER_URL') })(), featuresSaga({ polling: { apps: [ /* Application name here */ ], - delay: 60000 /** 60 seconds */, - }, - }), + delay: 60000 /** 60 seconds */ + } + }) ]) } diff --git a/src/modules/store.ts b/src/modules/store.ts index 61f91b9..bc7547a 100644 --- a/src/modules/store.ts +++ b/src/modules/store.ts @@ -1,25 +1,25 @@ -import { createLogger } from "redux-logger" -import createSagasMiddleware from "redux-saga" -import { Env } from "@dcl/ui-env" -import { createAnalyticsMiddleware } from "decentraland-dapps/dist/modules/analytics/middleware" -import { createStorageMiddleware } from "decentraland-dapps/dist/modules/storage/middleware" -import { config } from "./config" -import { createRootReducer } from "./reducer" -import { rootSaga } from "./saga" +import { createLogger } from 'redux-logger' +import createSagasMiddleware from 'redux-saga' +import { Env } from '@dcl/ui-env' +import { createAnalyticsMiddleware } from 'decentraland-dapps/dist/modules/analytics/middleware' +import { createStorageMiddleware } from 'decentraland-dapps/dist/modules/storage/middleware' +import { config } from './config' +import { createRootReducer } from './reducer' +import { rootSaga } from './saga' export function initStore() { const sagasMiddleware = createSagasMiddleware() const isDev = config.is(Env.DEVELOPMENT) const loggerMiddleware = createLogger({ collapsed: () => true, - predicate: (_: any, action) => isDev || action.type.includes("Failure"), + predicate: (_: any, action) => isDev || action.type.includes('Failure') }) - const analyticsMiddleware = createAnalyticsMiddleware(config.get("SEGMENT_API_KEY")) + const analyticsMiddleware = createAnalyticsMiddleware(config.get('SEGMENT_API_KEY')) const { storageMiddleware, loadStorageMiddleware } = createStorageMiddleware({ - storageKey: "profile", // this is the key used to save the state in localStorage (required) - paths: [["identity", "data"]], // array of paths from state to be persisted (optional) - actions: ["[Success] Login", "Logout"], // array of actions types that will trigger a SAVE (optional) - migrations: {}, // migration object that will migrate your localstorage (optional) + storageKey: 'profile', // this is the key used to save the state in localStorage (required) + paths: [['identity', 'data']], // array of paths from state to be persisted (optional) + actions: ['[Success] Login', 'Logout'], // array of actions types that will trigger a SAVE (optional) + migrations: {} // migration object that will migrate your localstorage (optional) }) const store = createRootReducer([sagasMiddleware, loggerMiddleware, analyticsMiddleware, storageMiddleware]) if (isDev) { diff --git a/src/modules/translation/locales/en.json b/src/modules/translation/locales/en.json index b26b716..f8e3c4f 100644 --- a/src/modules/translation/locales/en.json +++ b/src/modules/translation/locales/en.json @@ -1,46 +1,10 @@ { - "avatar": { - "edit": "Edit Avatar", - "message": "Edit your avatar in Decentraland" - }, - "profile_information": { - "default_name": "Unnamed", - "edit": "Edit Profile", - "copied": "Copied", - "copy_link": "Copy Link", - "share_on_tw": "Share on Twitter", - "tw_message": "Hey, checkout my profile in decentraland\n" - }, - "worlds_button": { - "get_a_name": "Get an unique name", - "activate_world": "Activate my world", - "jump": "Jump to \"{domain}\" World" - }, - "tabs": { - "assets": "Assets", - "creations": "Creations", - "dao_activity": "DAO Activity", - "lists": "Lists", - "overview": "Overview", - "places": "Places" - }, - "friendship_button": { - "friends": "Friends", - "add_friend": "Add Friend", - "pending": "Pending Request", - "cancel_request": "Cancel Request", - "accept_request": "Accept Request", - "blocked": "Blocked", - "unfriend": "Unfriend" - }, - "friends_modal": { - "friends_title": "Friends", - "mutuals_title": "Mutual friends" - }, - "friends_counter": { - "friends": "{count} {count, plural, one {friend} other {friends}}" - }, - "mutual_friends_counter": { - "mutual": "mutual" + "connect_to_world": { + "title": "Meet on Decentraland", + "description": "Choose the world you want to connect to and start streaming.", + "image_alt": "Meet on Decentraland", + "cta": "Connect", + "input_label": "World name", + "input_placeholder": "example:.dcl.eth" } } diff --git a/src/modules/translation/locales/es.json b/src/modules/translation/locales/es.json index 6778a1a..1e745bd 100644 --- a/src/modules/translation/locales/es.json +++ b/src/modules/translation/locales/es.json @@ -1,46 +1,10 @@ { - "avatar": { - "edit": "Editar Avatar", - "message": "Edita tu Avatar en Decentraland" - }, - "profile_information": { - "default_name": "Sin nombrar", - "edit": "Editar Perfil", - "copied": "Copiado", - "copy_link": "Copiar enlace", - "share_on_tw": "Compartir en Twitter", - "tw_message": "Oye, revisa mi perfil en decentraland\n" - }, - "worlds_button": { - "get_a_name": "Obtenga un nombre único", - "activate_world": "Activar mi mundo", - "jump": "Saltar al mundo \"{domain}\"" - }, - "tabs": { - "overview": "Resumen", - "assets": "Objetos", - "creations": "Creaciones", - "lists": "Listas", - "dao_activity": "Actividad DAO", - "places": "Places" - }, - "friendship_button": { - "friends": "Amigos", - "add_friend": "Agregar amigo", - "pending": "Petición pendiente", - "cancel_request": "Cancelar petición", - "accept_request": "Acceptar petición", - "blocked": "Bloqueado", - "unfriend": "Dejar de ser amigo" - }, - "friends_modal": { - "friends_title": "Amigos", - "mutuals_title": "Amigos en común" - }, - "friends_counter": { - "friends": "{count} {count, plural, one {amigo} other {amigos}}" - }, - "mutual_friends_counter": { - "mutual": "amigos en común" + "connect_to_world": { + "title": "Reunirse en Decentraland", + "description": "Elija el mundo al que desea conectarse y comience a transmitir.", + "image_alt": "Reunirse en Decentraland", + "cta": "Conectar", + "input_label": "Nombre del mundo", + "input_placeholder": "ejemplo:.dcl.eth" } } diff --git a/src/modules/translation/locales/zh.json b/src/modules/translation/locales/zh.json index af8325c..cbb0329 100644 --- a/src/modules/translation/locales/zh.json +++ b/src/modules/translation/locales/zh.json @@ -1,46 +1,10 @@ { - "avatar": { - "edit": "編輯頭像", - "message": "在 Decentraland 中编辑您的头像" - }, - "profile_information": { - "default_name": "未命名的", - "edit": "編輯個人資料", - "copied": "已復制", - "copy_link": "複製鏈接", - "share_on_tw": "在 Twitter 上分享", - "tw_message": "嘿,查看我在 decentraland 中的個人資料\n" - }, - "worlds_button": { - "get_a_name": "取一个独特的名字", - "activate_world": "创造我的世界", - "jump": "跳转到“{domain}”世界" - }, - "tabs": { - "overview": "概述", - "assets": "資產", - "creations": "創作", - "lists": "列表", - "dao_activity": "DAO 活動", - "places": "地點" - }, - "friendship_button": { - "friends": "朋友们", - "add_friend": "添加好友", - "pending": "待处理请求", - "cancel_request": "取消请求", - "accept_request": "接受请求", - "blocked": "阻止", - "unfriend": "不朋友" - }, - "friends_modal": { - "friends_title": "朋友们", - "mutuals_title": "共同的朋友" - }, - "friends_counter": { - "friends": "{count} 朋友們" - }, - "mutual_friends_counter": { - "mutual": "共同的朋友" + "connect_to_world": { + "title": "在 Decentraland 上见面", + "description": "选择您想要连接的世界并开始流式传输。", + "image_alt": "在 Decentraland 上见面", + "cta": "连接", + "input_label": "世界名称", + "input_placeholder": "example:.dcl.eth" } } diff --git a/src/tests/store.ts b/src/tests/store.ts index 16e1216..4a92ccb 100644 --- a/src/tests/store.ts +++ b/src/tests/store.ts @@ -1,22 +1,19 @@ -import createSagasMiddleware from "redux-saga"; -import { createStorageMiddleware } from "decentraland-dapps/dist/modules/storage/middleware"; -import { createRootReducer } from "../modules/reducer"; -import { rootSaga } from "../modules/saga"; +import createSagasMiddleware from 'redux-saga' +import { createStorageMiddleware } from 'decentraland-dapps/dist/modules/storage/middleware' +import { createRootReducer } from '../modules/reducer' +import { rootSaga } from '../modules/saga' export function initTestStore(preloadedState = {}) { - const sagasMiddleware = createSagasMiddleware(); + const sagasMiddleware = createSagasMiddleware() const { storageMiddleware, loadStorageMiddleware } = createStorageMiddleware({ - storageKey: "profile", // this is the key used to save the state in localStorage (required) - paths: [["identity", "data"]], // array of paths from state to be persisted (optional) + storageKey: 'profile', // this is the key used to save the state in localStorage (required) + paths: [['identity', 'data']], // array of paths from state to be persisted (optional) actions: [], // array of actions types that will trigger a SAVE (optional) - migrations: {}, // migration object that will migrate your localstorage (optional) - }); - const store = createRootReducer( - [sagasMiddleware, storageMiddleware], - preloadedState - ); - sagasMiddleware.run(rootSaga); - loadStorageMiddleware(store); + migrations: {} // migration object that will migrate your localstorage (optional) + }) + const store = createRootReducer([sagasMiddleware, storageMiddleware], preloadedState) + sagasMiddleware.run(rootSaga) + loadStorageMiddleware(store) - return store; + return store } diff --git a/src/utils/auth.ts b/src/utils/auth.ts index 6abc146..a59bae0 100644 --- a/src/utils/auth.ts +++ b/src/utils/auth.ts @@ -1,7 +1,7 @@ -import { RequestManager } from "eth-connect" -import { createUnsafeIdentity } from "@dcl/crypto/dist/crypto" -import { FlatFetchInit, flatFetch } from "./flat-fetch" -import { AuthChain, AuthIdentity, Authenticator } from "@dcl/crypto" +import { RequestManager } from 'eth-connect' +import { AuthChain, AuthIdentity, Authenticator } from '@dcl/crypto' +import { createUnsafeIdentity } from '@dcl/crypto/dist/crypto' +import { FlatFetchInit, flatFetch } from './flat-fetch' const ephemeralLifespanMinutes = 10_000 @@ -16,10 +16,7 @@ export type ExplorerIdentity = { signer: (message: string) => Promise } -export async function getUserAccount( - requestManager: RequestManager, - returnChecksum: boolean -): Promise { +export async function getUserAccount(requestManager: RequestManager, returnChecksum: boolean): Promise { try { const accounts = await requestManager.eth_accounts() @@ -33,25 +30,20 @@ export async function getUserAccount( } } -const AUTH_CHAIN_HEADER_PREFIX = "x-identity-auth-chain-" -const AUTH_TIMESTAMP_HEADER = "x-identity-timestamp" -const AUTH_METADATA_HEADER = "x-identity-metadata" +const AUTH_CHAIN_HEADER_PREFIX = 'x-identity-auth-chain-' +const AUTH_TIMESTAMP_HEADER = 'x-identity-timestamp' +const AUTH_METADATA_HEADER = 'x-identity-metadata' -export function getAuthChainSignature( - method: string, - path: string, - metadata: string, - chainProvider: (payload: string) => AuthChain -) { +export function getAuthChainSignature(method: string, path: string, metadata: string, chainProvider: (payload: string) => AuthChain) { const timestamp = Date.now() const payloadParts = [method.toLowerCase(), path.toLowerCase(), timestamp.toString(), metadata] - const payloadToSign = payloadParts.join(":").toLowerCase() + const payloadToSign = payloadParts.join(':').toLowerCase() const authChain = chainProvider(payloadToSign) return { authChain, metadata, - timestamp, + timestamp } } @@ -72,28 +64,23 @@ export function getSignedHeaders( return headers } -export function signedFetch( - url: string, - identity: AuthIdentity, - init?: FlatFetchInit, - additionalMetadata: Record = {} -) { +export function signedFetch(url: string, identity: AuthIdentity, init?: FlatFetchInit, additionalMetadata: Record = {}) { const path = new URL(url).pathname const actualInit = { ...init, headers: { ...getSignedHeaders( - init?.method ?? "get", + init?.method ?? 'get', path, { origin: location.origin, - ...additionalMetadata, + ...additionalMetadata }, - (payload) => Authenticator.signPayload(identity, payload) + payload => Authenticator.signPayload(identity, payload) ), - ...init?.headers, - }, + ...init?.headers + } } as FlatFetchInit return flatFetch(url, actualInit) @@ -104,10 +91,7 @@ export function signedFetch( // the ephemeral private key is used to sign the rest of the authChain and subsequent // messages. this is a good way to not over-expose the real user accounts to excessive // signing requests. -export async function identityFromSigner( - address: string, - signer: (message: string) => Promise -): Promise { +export async function identityFromSigner(address: string, signer: (message: string) => Promise): Promise { const ephemeral = createUnsafeIdentity() const authChain = await Authenticator.initializeAuthChain(address, ephemeral, ephemeralLifespanMinutes, signer) @@ -116,6 +100,6 @@ export async function identityFromSigner( address, signer, authChain, - isGuest: true, + isGuest: true } } diff --git a/src/utils/client-utils.ts b/src/utils/client-utils.ts index cdee23f..7d17513 100644 --- a/src/utils/client-utils.ts +++ b/src/utils/client-utils.ts @@ -1,19 +1,19 @@ -import { useEffect, useState } from "react" +import { useEffect, useState } from 'react' export function useServerUrl(region?: string) { const [serverUrl, setServerUrl] = useState() useEffect(() => { - let endpoint = `/api/url` + let endpoint = '/api/url' if (region) { endpoint += `?region=${region}` } - fetch(endpoint).then(async (res) => { + fetch(endpoint).then(async res => { if (res.ok) { const body = await res.json() console.log(body) setServerUrl(body.url) } else { - throw Error("Error fetching server url, check server logs") + throw Error('Error fetching server url, check server logs') } }) }) diff --git a/src/utils/errors.ts b/src/utils/errors.ts new file mode 100644 index 0000000..8fea4eb --- /dev/null +++ b/src/utils/errors.ts @@ -0,0 +1,3 @@ +export function isErrorMessage(error: unknown): error is Error { + return error instanceof Error && 'message' in error +} diff --git a/src/utils/flat-fetch.ts b/src/utils/flat-fetch.ts index 32c9cf1..22022ca 100644 --- a/src/utils/flat-fetch.ts +++ b/src/utils/flat-fetch.ts @@ -7,14 +7,14 @@ type FlatFetchResponse = { text?: string } -type BodyType = "json" | "text" +type BodyType = 'json' | 'text' export type FlatFetchInit = RequestInit & { responseBodyType?: BodyType } export async function flatFetch(url: string, init?: FlatFetchInit): Promise { const response = await fetch(url, init) - const responseBodyType = init?.responseBodyType || "text" + const responseBodyType = init?.responseBodyType || 'text' const headers: Record = {} @@ -24,14 +24,14 @@ export async function flatFetch(url: string, init?: FlatFetchInit): Promise { + describe('and the key is not yet set', () => { + beforeEach(() => { + jest.spyOn(Storage.prototype, 'getItem').mockReturnValueOnce(null) + }) + + it('should return an empty array', () => { + expect(getPreviouslyLoadedServers()).toStrictEqual([]) + }) + }) + + describe('and the value is empty', () => { + beforeEach(() => { + jest.spyOn(Storage.prototype, 'getItem').mockReturnValueOnce('') + }) + + it('should return an empty array', () => { + expect(getPreviouslyLoadedServers()).toStrictEqual([]) + }) + }) + + describe('and the value has one previously loaded server', () => { + let previouslyLoadedServer: string + + beforeEach(() => { + previouslyLoadedServer = 'previous-server.dcl.eth' + jest.spyOn(Storage.prototype, 'getItem').mockReturnValueOnce(previouslyLoadedServer) + }) + + it('should return an array with the previously loaded server', () => { + expect(getPreviouslyLoadedServers()).toStrictEqual([previouslyLoadedServer]) + }) + }) + + describe('and the value has multiple previously loaded server', () => { + let previouslyLoadedServers: string[] + + describe('and all the servers are different', () => { + beforeEach(() => { + previouslyLoadedServers = ['previous-server.dcl.eth', 'another-server.dcl.eth', 'and-another-server.dcl.eth'] + jest.spyOn(Storage.prototype, 'getItem').mockReturnValueOnce(previouslyLoadedServers.join(',')) + }) + + it('should return an array with all those different servers', () => { + expect(getPreviouslyLoadedServers()).toStrictEqual(previouslyLoadedServers) + }) + }) + + describe('and there are some duplicates in the local storage value', () => { + beforeEach(() => { + previouslyLoadedServers = [ + 'previous-server.dcl.eth', + 'another-server.dcl.eth', + 'another-server.dcl.eth', + 'and-another-server.dcl.eth', + 'and-another-server.dcl.eth', + 'and-another-server.dcl.eth' + ] + jest.spyOn(Storage.prototype, 'getItem').mockReturnValueOnce(previouslyLoadedServers.join(',')) + }) + + it('should return an array with only one occurrence of each server', () => { + expect(getPreviouslyLoadedServers()).toStrictEqual([ + 'previous-server.dcl.eth', + 'another-server.dcl.eth', + 'and-another-server.dcl.eth' + ]) + }) + }) + }) +}) + +describe('when adding a new server to the same key in the local storage', () => { + let newServer: string + + beforeEach(() => { + newServer = 'new-server.dcl.eth' + jest.spyOn(Storage.prototype, 'setItem').mockImplementationOnce(jest.fn()) + }) + + describe('and the new server is already in the array of previously loaded servers', () => { + beforeEach(() => { + jest.spyOn(Storage.prototype, 'getItem').mockReturnValueOnce([newServer].join(',')) + addServerToPreviouslyLoaded(newServer) + }) + + it('should not call the local storage set method to add the new server', () => { + // eslint-disable-next-line @typescript-eslint/unbound-method + expect(localStorage.setItem).not.toBeCalled() + }) + }) + + describe('and there was not previous loaded servers in the local storage', () => { + beforeEach(() => { + jest.spyOn(Storage.prototype, 'getItem').mockReturnValueOnce(null) + addServerToPreviouslyLoaded(newServer) + }) + + it('should set in the local storage only the new server', () => { + // eslint-disable-next-line @typescript-eslint/unbound-method + expect(localStorage.setItem).toBeCalledWith(PREVIOUSLY_LOADED_SERVERS_KEY, newServer) + }) + }) + + describe('and there were some previous loaded servers in the local storage', () => { + const previouslyLoadedServers = ['previous-server.dcl.eth', 'another-server.dcl.eth', 'and-another-server.dcl.eth'] + + beforeEach(() => { + jest.spyOn(Storage.prototype, 'getItem').mockReturnValueOnce(previouslyLoadedServers.join(',')) + addServerToPreviouslyLoaded(newServer) + }) + + it('should set in the local storage only the new server', () => { + // eslint-disable-next-line @typescript-eslint/unbound-method + expect(localStorage.setItem).toBeCalledWith(PREVIOUSLY_LOADED_SERVERS_KEY, [...previouslyLoadedServers, newServer].join(',')) + }) + }) +}) diff --git a/src/utils/worldServers/worldServers.ts b/src/utils/worldServers/worldServers.ts new file mode 100644 index 0000000..22d07b1 --- /dev/null +++ b/src/utils/worldServers/worldServers.ts @@ -0,0 +1,13 @@ +export const PREVIOUSLY_LOADED_SERVERS_KEY = 'previously-loaded-servers' + +export const getPreviouslyLoadedServers = () => + Array.from(new Set(localStorage.getItem(PREVIOUSLY_LOADED_SERVERS_KEY)?.split(',').filter(Boolean))) + +export const addServerToPreviouslyLoaded = (server: string) => { + const previouslyLoadedServers = getPreviouslyLoadedServers() || [] + + if (previouslyLoadedServers.includes(server)) return + + previouslyLoadedServers.push(server) + localStorage.setItem(PREVIOUSLY_LOADED_SERVERS_KEY, previouslyLoadedServers.join(',')) +} diff --git a/tsconfig.json b/tsconfig.json index 0adb603..289c719 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -21,6 +21,12 @@ "noUnusedParameters": true, "noFallthroughCasesInSwitch": true }, - "include": ["src", ".eslintrc.js", "vite.config.ts", "jest.config.ts", "scripts"], + "include": [ + "src", + ".eslintrc.js", + "vite.config.ts", + "jest.config.ts", + "scripts" + ], "references": [{ "path": "./tsconfig.node.json" }] }