Merge pull request #2 from decentraland/feat/add-connect-to-world-component

feat: Add Connect to World component styles
This commit is contained in:
Kevin Szuchet 2023-08-09 14:26:01 +02:00 committed by GitHub
commit 23b519fff2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
48 changed files with 726 additions and 645 deletions

7
.eslintrc.js Normal file
View File

@ -0,0 +1,7 @@
/* eslint-env node */
module.exports = {
extends: ['@dcl/eslint-config/dapps'],
parserOptions: {
project: ['tsconfig.json']
}
}

8
.prettierrc.json Normal file
View File

@ -0,0 +1,8 @@
{
"semi": false,
"singleQuote": true,
"printWidth": 140,
"tabWidth": 2,
"trailingComma": "none",
"arrowParens": "avoid"
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 MiB

After

Width:  |  Height:  |  Size: 2.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

View File

@ -1 +1 @@
export { default as LoginModal } from "decentraland-dapps/dist/containers/LoginModal"
export { default as LoginModal } from 'decentraland-dapps/dist/containers/LoginModal'

View File

@ -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 (

View File

@ -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))

View File

@ -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

View File

@ -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<Props, "loggedInAddress" | "isLoading" | "profileAddress" | "server" | "token">
export type MapStateProps = Pick<Props, 'loggedInAddress' | 'isLoading' | 'profileAddress' | 'server' | 'token'>
export type MapDispatch = Dispatch
type Params = {
profileAddress?: string

View File

@ -1,2 +1,2 @@
import Conference from "./Conference.container"
import Conference from './Conference.container'
export default Conference

View File

@ -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))

View File

@ -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;
}
}

View File

@ -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<string>('')
const [availableServers, setAvailableServers] = useState<string[]>([])
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<HTMLInputElement>) => {
setError('')
setSelectedServer(e.target.value)
},
[setSelectedServer]
)
const handleSelectChange = useCallback(
(_event: React.SyntheticEvent<HTMLElement>, { 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<HTMLButtonElement>) => {
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 (
<PageLayout>
{isLoading ? (
<Loader active />
) : (
<div className={styles.ConnectToWorld}>
<div className={styles.content}>
<h4 className={styles.title}>{t('connect_to_world.title')}</h4>
<p className={styles.description}>{t('connect_to_world.description')}</p>
<img
className={styles.img}
src={meetOnDecentralandImg}
alt={t('connect_to_world.image_alt')}
aria-label={t('connect_to_world.image_alt')}
/>
<Form className={styles.form}>
<div className={styles.inputContainer}>
<label className={styles.label} htmlFor="server">
{t('connect_to_world.input_label')}
</label>
{availableServers.length > 0 ? (
<SelectField
value={selectedServer}
options={availableServers.map(server => ({
value: server,
text: server
}))}
onAddItem={handleSelectChange}
onChange={handleSelectChange}
allowAdditions
error={!!error}
message={error}
/>
) : (
<Field
name="server"
value={selectedServer}
onChange={handleChange}
placeholder={t('connect_to_world.input_placeholder')}
error={!!error}
message={error}
onEnter={handleClick}
/>
)}
</div>
<Button
primary
onClick={handleClick}
fluid
disabled={!selectedServer || isConnectingToServer}
type="submit"
loading={isConnectingToServer}
>
{t('connect_to_world.cta')}
</Button>
</Form>
</div>
</div>
)}
</PageLayout>
)
}
export default ConnectToWorld

View File

@ -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<Props, 'loggedInAddress' | 'isLoading' | 'previouslyLoadedServers' | 'identity' | 'worldsContentServerUrl'>
export type MapDispatchProps = Pick<Props, 'onSubmitConnectForm'>
export type MapDispatch = Dispatch
type Params = Record<string, never>
export type OwnProps = {
router: RouterProps<Params>
}

View File

@ -0,0 +1,2 @@
import ConnectToWorld from './ConnectToWorld.container'
export default ConnectToWorld

View File

@ -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))

View File

@ -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;
}

View File

@ -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<HTMLInputElement>) => {
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 (
<PageLayout>
{isLoading || isConnectingToServer ? (
<Loader active />
) : (
<div className={styles.MainPage}>
<div className={styles.infoContainer}>
<Divider />
<div>
<input
name="server"
value={selectedServer}
onChange={handleChange}
/>
<button onClick={handleClick}>Connect</button>
</div>
</div>
</div>
)}
</PageLayout>
);
}
export default MainPage;

View File

@ -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<Props, "loggedInAddress" | "isLoading" | "profileAddress" | "identity">
export type MapDispatchProps = Pick<Props, "onSubmitConnectForm">
export type MapDispatch = Dispatch
type Params = {
profileAddress?: string
}
export type OwnProps = {
router: RouterProps<Params>
}

View File

@ -1 +0,0 @@
export const nullAddress = '0x0000000000000000000000000000000000000000'

View File

@ -1,2 +0,0 @@
import MainPage from './MainPage.container'
export default MainPage

View File

@ -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;
}
}

View File

@ -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 (
<PageLayout>
<SignIn isConnected={isConnected} handleLoginConnect={onConnect} />
<div className={styles.content}>
<SignIn isConnected={isConnected} handleLoginConnect={onConnect} />
</div>
</PageLayout>
)
}

View File

@ -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<HTMLDivElemen
source?: Track.Source
publication?: TrackPublication
onParticipantClick?: (event: ParticipantClickEvent) => 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<HTMLDivElement>({
@ -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 (
<div style={{ position: "relative" }} {...elementProps}>
<div style={{ position: 'relative' }} {...elementProps}>
<ParticipantContextIfNeeded participant={trackRef.participant}>
{children ?? (
<>
{trackRef.publication?.kind === "video" ||
{trackRef.publication?.kind === 'video' ||
trackRef.source === Track.Source.Camera ||
trackRef.source === Track.Source.ScreenShare ? (
<VideoTrack
@ -135,7 +135,7 @@ export function ParticipantTile({
<div className="lk-participant-metadata-item">
{trackRef.source === Track.Source.Camera ? (
<>
<TrackMutedIndicator source={Track.Source.Microphone} show={"muted"}></TrackMutedIndicator>
<TrackMutedIndicator source={Track.Source.Microphone} show={'muted'}></TrackMutedIndicator>
<ParticipantName participant={participantWithProfile} />
</>
) : (

View File

@ -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 (
<div className="lk-video-conference" {...props}>
@ -119,7 +115,7 @@ export function VideoConference({ chatMessageFormatter, ...props }: VideoConfere
)}
<ControlBar controls={{ chat: true }} />
</div>
<Chat style={{ display: widgetState.showChat ? "flex" : "none" }} messageFormatter={chatMessageFormatter} />
<Chat style={{ display: widgetState.showChat ? 'flex' : 'none' }} messageFormatter={chatMessageFormatter} />
</LayoutContextProvider>
)}
<RoomAudioRenderer />

View File

@ -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<string[]> => {
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<SubdomainQueryResult>(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
}
}

View File

@ -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: <MainPage />,
path: '/',
element: <ConnectToWorld />
},
{
path: "/accounts/:profileAddress?",
element: <MainPage />,
path: 'sign-in',
element: <SignInPage />
},
{
path: "sign-in",
element: <SignInPage />,
},
{
path: "/meet/:server",
element: <Conference />,
},
path: '/meet/:server',
element: <Conference />
}
])
const component = (
@ -50,4 +46,4 @@ const component = (
</React.StrictMode>
)
ReactDOM.render(component, document.getElementById("root") as HTMLElement)
ReactDOM.render(component, document.getElementById('root') as HTMLElement)

View File

@ -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<typeof setServer>
export const setToken = createAction<{ token: string }>("Set Token")
export const setToken = createAction<{ token: string }>('Set Token')
export type SetTokenAction = ReturnType<typeof setToken>

View File

@ -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<ConferenceState>(INITIAL_STATE, (builder) =>
export const conferenceReducer = createReducer<ConferenceState>(INITIAL_STATE, builder =>
builder
.addCase(setServer, (state, action) => {
state.server = action.payload.server

View File

@ -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)

View File

@ -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<TranslationState, AnyAction>,
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

View File

@ -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}`
}

View File

@ -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 */
}
})
])
}

View File

@ -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) {

View File

@ -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:<name>.dcl.eth"
}
}

View File

@ -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:<name>.dcl.eth"
}
}

View File

@ -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:<name>.dcl.eth"
}
}

View File

@ -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
}

View File

@ -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<string>
}
export async function getUserAccount(
requestManager: RequestManager,
returnChecksum: boolean
): Promise<string | undefined> {
export async function getUserAccount(requestManager: RequestManager, returnChecksum: boolean): Promise<string | undefined> {
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<string, any> = {}
) {
export function signedFetch(url: string, identity: AuthIdentity, init?: FlatFetchInit, additionalMetadata: Record<string, any> = {}) {
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<string>
): Promise<ExplorerIdentity> {
export async function identityFromSigner(address: string, signer: (message: string) => Promise<string>): Promise<ExplorerIdentity> {
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
}
}

View File

@ -1,19 +1,19 @@
import { useEffect, useState } from "react"
import { useEffect, useState } from 'react'
export function useServerUrl(region?: string) {
const [serverUrl, setServerUrl] = useState<string | undefined>()
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')
}
})
})

3
src/utils/errors.ts Normal file
View File

@ -0,0 +1,3 @@
export function isErrorMessage(error: unknown): error is Error {
return error instanceof Error && 'message' in error
}

View File

@ -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<FlatFetchResponse> {
const response = await fetch(url, init)
const responseBodyType = init?.responseBodyType || "text"
const responseBodyType = init?.responseBodyType || 'text'
const headers: Record<string, string> = {}
@ -24,14 +24,14 @@ export async function flatFetch(url: string, init?: FlatFetchInit): Promise<Flat
ok: response.ok,
status: response.status,
statusText: response.statusText,
headers,
headers
}
switch (responseBodyType) {
case "json":
case 'json':
flatFetchResponse.json = await response.json()
break
case "text":
case 'text':
flatFetchResponse.text = await response.text()
break
}

View File

@ -1,12 +1,12 @@
import { RoomServiceClient } from "livekit-server-sdk"
import { RoomServiceClient } from 'livekit-server-sdk'
export function getRoomClient(): RoomServiceClient {
checkKeys()
return new RoomServiceClient(getLiveKitURL())
return new RoomServiceClient(getLiveKitUrl())
}
export function getLiveKitURL(region?: string | string[]): string {
let targetKey = "LIVEKIT_URL"
export function getLiveKitUrl(region?: string | string[]): string {
let targetKey = 'LIVEKIT_URL'
if (region && !Array.isArray(region)) {
targetKey = `LIVEKIT_URL_${region}`.toUpperCase()
}
@ -18,10 +18,10 @@ export function getLiveKitURL(region?: string | string[]): string {
}
function checkKeys() {
if (typeof process.env.LIVEKIT_API_KEY === "undefined") {
throw new Error("LIVEKIT_API_KEY is not defined")
if (typeof process.env.LIVEKIT_API_KEY === 'undefined') {
throw new Error('LIVEKIT_API_KEY is not defined')
}
if (typeof process.env.LIVEKIT_API_SECRET === "undefined") {
throw new Error("LIVEKIT_API_SECRET is not defined")
if (typeof process.env.LIVEKIT_API_SECRET === 'undefined') {
throw new Error('LIVEKIT_API_SECRET is not defined')
}
}

View File

@ -1,4 +1,4 @@
import { LocalAudioTrack, LocalVideoTrack } from "livekit-client"
import { LocalAudioTrack, LocalVideoTrack } from 'livekit-client'
export interface SessionProps {
roomName: string

View File

@ -0,0 +1 @@
export * from './worldServers'

View File

@ -0,0 +1,120 @@
import { PREVIOUSLY_LOADED_SERVERS_KEY, addServerToPreviouslyLoaded, getPreviouslyLoadedServers } from './worldServers'
describe('when getting the previously loaded servers from the local storage', () => {
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(','))
})
})
})

View File

@ -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(','))
}

View File

@ -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" }]
}