diff --git a/config.json b/config.json index b7abfda1e..bf57109c7 100644 --- a/config.json +++ b/config.json @@ -3,13 +3,10 @@ "homeserverList": ["thirdroom.io", "matrix.org"], "onboardingVersion": 1, "repositoryRoomIdOrAlias": "#repository-room:thirdroom.io", - "oidc": { - "clientConfigs": { - "https://id.thirdroom.io/realms/thirdroom/": { - "client_id": "thirdroom", - "uris": ["http://localhost:3000", "https://thirdroom.io"], - "guestKeycloakIdpHint": "guest" - } + "staticOidcClients": { + "https://id.thirdroom.io/realms/thirdroom/": { + "client_id": "thirdroom", + "guestKeycloakIdpHint": "guest" } } } diff --git a/package.json b/package.json index 81719cacb..36f4bdb81 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,7 @@ "@gltf-transform/core": "^2.4.3", "@gltf-transform/extensions": "^2.4.3", "@gltf-transform/functions": "^2.4.3", + "@matrix-org/olm": "https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.14.tgz", "@monaco-editor/react": "^4.4.6", "@radix-ui/react-checkbox": "^1.0.1", "@radix-ui/react-dialog": "^0.1.7", @@ -43,7 +44,7 @@ "@sentry/react": "^7.13.0", "@sentry/tracing": "^7.13.0", "@thi.ng/malloc": "^6.1.19", - "@thirdroom/hydrogen-view-sdk": "0.0.29", + "@thirdroom/hydrogen-view-sdk": "0.1.2", "@thirdroom/manifold-editor-components": "^0.0.5", "@thirdroom/ringbuffer": "^0.0.1", "@webxr-input-profiles/assets": "^1.0.13", diff --git a/src/engine/network/createMatrixNetworkInterface.ts b/src/engine/network/createMatrixNetworkInterface.ts index 53d7bd491..43d444f99 100644 --- a/src/engine/network/createMatrixNetworkInterface.ts +++ b/src/engine/network/createMatrixNetworkInterface.ts @@ -19,12 +19,23 @@ function memberComparator(a: Member, b: Member): number { function isOlderThanLocalHost(groupCall: GroupCall, member: Member): boolean { if (groupCall.eventTimestamp === member.eventTimestamp) { - return groupCall.deviceIndex! < member.deviceIndex; + return groupCall.deviceIndex! <= member.deviceIndex; } return groupCall.eventTimestamp! < member.eventTimestamp; } +function getReliableHost(groupCall: GroupCall): Member | undefined { + const sortedMembers = Array.from(new Map(groupCall.members).values()) + .sort(memberComparator) + .filter((member) => member.isConnected && member.dataChannel); + + if (sortedMembers.length === 0) return undefined; + if (isOlderThanLocalHost(groupCall, sortedMembers[0])) return undefined; + + return sortedMembers[0]; +} + export async function createMatrixNetworkInterface( ctx: IMainThreadContext, client: Client, @@ -52,91 +63,56 @@ export async function createMatrixNetworkInterface( // Wait for that member to be connected and return their user id // If the member hasn't connected in 10 seconds, return the longest connected user id - let hostId: string | undefined; - return new Promise((resolve) => { let timeout: number | undefined = undefined; + const reliableHost = getReliableHost(groupCall); + if (reliableHost) { + resolve(reliableHost.userId); + return; + } + if (groupCall.members.size === 0) { + resolve(userId); + return; + } + const unsubscribe = groupCall.members.subscribe({ - onAdd(_key, member) { - // The host connected, resolve with their id - // NOTE: Do we also need to check for older events here? Maybe there's an older member and we - // haven't received their event yet? - if (hostId && member.userId === hostId && member.isConnected && member.dataChannel) { + onAdd() { + const host = getReliableHost(groupCall); + if (host) { clearTimeout(timeout); unsubscribe(); - resolve(hostId); + resolve(host.userId); } }, - onRemove(_key, member) { - if (hostId && member.userId === hostId) { - // The current host disconnected, pick the next best host - const sortedMembers = Array.from(new Map(groupCall.members).values()).sort(memberComparator); - - // If there are no other members, you're the host - if (sortedMembers.length === 0 || !isOlderThanLocalHost(groupCall, sortedMembers[0])) { - clearTimeout(timeout); - unsubscribe(); - resolve(userId); - } else { - const nextHost = sortedMembers[0]; - - // If the next best host is connected then resolve with their id - if (nextHost.isConnected && member.dataChannel) { - clearTimeout(timeout); - unsubscribe(); - resolve(nextHost.userId); - } else { - hostId = nextHost.userId; - } - } + onRemove() { + const host = getReliableHost(groupCall); + if (host) { + clearTimeout(timeout); + unsubscribe(); + resolve(host.userId); } }, onReset() { throw new Error("Unexpected reset of groupCall.members"); }, - onUpdate(_key, member) { - // The host connected, resolve with their id - if (hostId && member.userId === hostId && member.isConnected && member.dataChannel) { + onUpdate() { + const host = getReliableHost(groupCall); + if (host) { clearTimeout(timeout); unsubscribe(); - resolve(hostId); + resolve(host.userId); } }, }); + // wait if any member to become reliable. + // resolve otherwise timeout = window.setTimeout(() => { - // The host hasn't connected yet after 10 seconds. Use the oldest connected host instead. unsubscribe(); - - const sortedConnectedMembers = Array.from(new Map(groupCall.members).values()) - .sort(memberComparator) - .filter((member) => member.isConnected && member.dataChannel); - - if (sortedConnectedMembers.length > 0 && isOlderThanLocalHost(groupCall, sortedConnectedMembers[0])) { - resolve(sortedConnectedMembers[0].userId); - } else { - resolve(userId); - } + const host = getReliableHost(groupCall); + resolve(host?.userId ?? userId); }, 10000); - - const initialSortedMembers = Array.from(new Map(groupCall.members).values()).sort(memberComparator); - - if (initialSortedMembers.length === 0 || !isOlderThanLocalHost(groupCall, initialSortedMembers[0])) { - clearTimeout(timeout); - unsubscribe(); - resolve(userId); - } else { - const hostMember = initialSortedMembers[0]; - - if (hostMember.isConnected && hostMember.dataChannel) { - clearTimeout(timeout); - unsubscribe(); - resolve(hostMember.userId); - } else { - hostId = hostMember.userId; - } - } }); } @@ -176,22 +152,13 @@ export async function createMatrixNetworkInterface( // Of the connected members find the one whose member event is oldest // If the member has multiple devices get the device with the lowest device index - const sortedConnectedMembers = Array.from(new Map(groupCall.members).values()) - .sort(memberComparator) - .filter((member) => member.isConnected && member.dataChannel); + const reliableHost = getReliableHost(groupCall); - if (sortedConnectedMembers.length === 0 || isOlderThanLocalHost(groupCall, sortedConnectedMembers[0])) { - setHost(ctx, userId); - } else { + if (reliableHost) { // TODO: use powerlevels to determine host - // find youngest member for now - const hostMember = sortedConnectedMembers.sort((a, b) => { - if (a.eventTimestamp === b.eventTimestamp) { - return a.deviceIndex! > b.deviceIndex ? 1 : -1; - } - return a.eventTimestamp! > b.eventTimestamp ? 1 : -1; - })[0]; - setHost(ctx, hostMember.userId); + setHost(ctx, reliableHost.userId); + } else { + setHost(ctx, userId); } } diff --git a/src/hydrogen-view-sdk.d.ts b/src/hydrogen-view-sdk.d.ts index 42b857684..e3369a057 100644 --- a/src/hydrogen-view-sdk.d.ts +++ b/src/hydrogen-view-sdk.d.ts @@ -364,6 +364,13 @@ declare module "@thirdroom/hydrogen-view-sdk" { config: { defaultHomeServer: string; + staticClients: Record< + string, + { + client_id: string; + guestKeycloakIdpHint: string; + } + >; [key: string]: any; }; encoding: any; @@ -401,15 +408,14 @@ declare module "@thirdroom/hydrogen-view-sdk" { expires_in?: number; }; type IssuerUri = string; - interface ClientConfig { + export interface OidcClientConfig { client_id: string; - client_secret?: string; - uris: string[]; } + export type StaticOidcClientsConfig = Record; export class OidcApi { constructor(options: { issuer: string; - clientConfigs: Record; + staticClients?: StaticOidcClientsConfig; request: RequestFunction; encoding: any; crypto: any; @@ -952,6 +958,22 @@ declare module "@thirdroom/hydrogen-view-sdk" { token?: (loginToken: string) => ILoginMethod; } + export enum FeatureFlag { + Calls = 1 << 0, + CrossSigning = 1 << 1, + } + + export class FeatureSet { + constructor(public readonly flags: number = 0); + withFeature(flag: FeatureFlag): FeatureSet; + withoutFeature(flag: FeatureFlag): FeatureSet; + isFeatureEnabled(flag: FeatureFlag): boolean; + get calls(): boolean; + get crossSigning(): boolean; + static async load(settingsStorage: SettingsStorage): Promise; + async store(settingsStorage: SettingsStorage): Promise; + } + export interface ClientOptions { deviceName?: string; } @@ -965,7 +987,7 @@ declare module "@thirdroom/hydrogen-view-sdk" { loadStatus: ObservableValue; - constructor(platform: Platform, options?: ClientOptions); + constructor(platform: Platform, features = new FeatureSet(0), options?: ClientOptions); get loginFailure(): LoginFailure; startWithExistingSession(sessionId: string): Promise; @@ -1119,6 +1141,11 @@ declare module "@thirdroom/hydrogen-view-sdk" { get sender(): string; } + export class DateTile extends SimpleTile { + get relativeDate(): string; + get machineReadableDate(): string; + } + export class GapTile extends SimpleTile { constructor(entry: any, options: SimpleTileOptions); fill(): boolean; diff --git a/src/ui/views/HydrogenRootView.tsx b/src/ui/views/HydrogenRootView.tsx index 2ce0c2af4..88c32d5a1 100644 --- a/src/ui/views/HydrogenRootView.tsx +++ b/src/ui/views/HydrogenRootView.tsx @@ -16,6 +16,8 @@ import { OIDCLoginMethod, ILoginMethod, ISessionInfo, + FeatureSet, + FeatureFlag, } from "@thirdroom/hydrogen-view-sdk"; import downloadSandboxPath from "@thirdroom/hydrogen-view-sdk/download-sandbox.html?url"; import workerPath from "@thirdroom/hydrogen-view-sdk/main.js?url"; @@ -132,19 +134,10 @@ function initHydrogen() { }; const oidcClientId = document.location.hostname === "thirdroom.io" ? "thirdroom" : "thirdroom_dev"; - const oidcUris = ((): string[] => { - if (document.location.hostname === "thirdroom.io") { - return ["https://thirdroom.io"]; - } - - const { protocol, hostname, port } = document.location; - return [`${protocol}//${hostname}${port ? `:${port}` : ""}`]; - })(); const config = { ...configData }; - config.oidc.clientConfigs["https://id.thirdroom.io/realms/thirdroom/"] = { + config.staticOidcClients["https://id.thirdroom.io/realms/thirdroom/"] = { client_id: oidcClientId, - uris: oidcUris, guestKeycloakIdpHint: "guest", }; @@ -153,11 +146,12 @@ function initHydrogen() { }; const platform = new Platform({ container, assetPaths, config, options }); + const features = new FeatureSet(FeatureFlag.Calls); const navigation = new Navigation(allowsChild); platform.setNavigation(navigation); - const client = new Client(platform, { deviceName: "Third Room" }); + const client = new Client(platform, features, { deviceName: "Third Room" }); hydrogenInstance = { client, @@ -274,7 +268,7 @@ async function getOidcLoginMethod(platform: Platform, urlCreator: URLRouter, sta return new OIDCLoginMethod({ oidcApi: new OidcApi({ issuer, - clientConfigs: platform.config.oidc.clientConfigs, + staticClients: platform.config.staticOidcClients, clientId, urlCreator, request: platform.request, diff --git a/src/ui/views/login/LoginView.tsx b/src/ui/views/login/LoginView.tsx index 44bce3435..189c61802 100644 --- a/src/ui/views/login/LoginView.tsx +++ b/src/ui/views/login/LoginView.tsx @@ -131,7 +131,7 @@ async function startOIDCLogin( function getMatchingClientConfig(platform: Platform, issuer: string) { const normalisedIssuer = `${issuer}${issuer.endsWith("/") ? "" : "/"}`; - return platform.config.oidc.clientConfigs[normalisedIssuer]; + return platform.config.staticOidcClients[normalisedIssuer]; } export default function LoginView() { @@ -209,7 +209,7 @@ export default function LoginView() { const { issuer } = result.oidc; const oidcApi = new OidcApi({ issuer, - clientConfigs: platform.config.oidc.clientConfigs, + staticClients: platform.config.staticOidcClients, request: platform.request, encoding: platform.encoding, crypto: platform.crypto, diff --git a/src/ui/views/session/chat/tiles/ChatDate.css b/src/ui/views/session/chat/tiles/ChatDate.css new file mode 100644 index 000000000..6e4362797 --- /dev/null +++ b/src/ui/views/session/chat/tiles/ChatDate.css @@ -0,0 +1,3 @@ +.ChatDate { + margin: var(--sp-sm) auto; +} diff --git a/src/ui/views/session/chat/tiles/ChatDate.tsx b/src/ui/views/session/chat/tiles/ChatDate.tsx new file mode 100644 index 000000000..fd2b0b64f --- /dev/null +++ b/src/ui/views/session/chat/tiles/ChatDate.tsx @@ -0,0 +1,18 @@ +import { TemplateView, DateTile, Builder } from "@thirdroom/hydrogen-view-sdk"; + +import "./ChatDate.css"; + +export class ChatDate extends TemplateView { + constructor(vm: DateTile) { + super(vm); + } + + render(t: Builder, vm: DateTile) { + return t.p( + { className: "ChatDate Text Text-b2 Text--surface Text--semi-bold" }, + t.time({ dateTime: vm.machineReadableDate }, vm.relativeDate) + ); + } + + onClick() {} +} diff --git a/src/ui/views/session/chat/tiles/index.tsx b/src/ui/views/session/chat/tiles/index.tsx index 402c08b90..29a9325b5 100644 --- a/src/ui/views/session/chat/tiles/index.tsx +++ b/src/ui/views/session/chat/tiles/index.tsx @@ -4,6 +4,7 @@ import { ChatGap } from "./ChatGap"; import { ChatMessage } from "./ChatMessage"; import { ChatAnnouncement } from "./ChatAnnouncement"; import { ChatImage } from "./ChatImage"; +import { ChatDate } from "./ChatDate"; export function viewClassForTile(vm: SimpleTile): TileViewConstructor { switch (vm.shape) { @@ -16,6 +17,8 @@ export function viewClassForTile(vm: SimpleTile): TileViewConstructor { return ChatMessage; case "image": return ChatImage; + case "date-header": + return ChatDate; default: throw new Error( `Tiles of shape "${vm.shape}" are not supported, check the tileClassForEntry function in the view model` diff --git a/src/ui/views/session/world-chat/tiles/ChatDate.ts b/src/ui/views/session/world-chat/tiles/ChatDate.ts new file mode 100644 index 000000000..3607a8245 --- /dev/null +++ b/src/ui/views/session/world-chat/tiles/ChatDate.ts @@ -0,0 +1,13 @@ +import { TemplateView, DateTile, Builder } from "@thirdroom/hydrogen-view-sdk"; + +export class ChatDate extends TemplateView { + constructor(vm: DateTile) { + super(vm); + } + + render(t: Builder, vm: DateTile) { + return t.span({ className: "inline-flex" }, ""); + } + + onClick() {} +} diff --git a/src/ui/views/session/world-chat/tiles/index.tsx b/src/ui/views/session/world-chat/tiles/index.tsx index c1fca9325..318f83022 100644 --- a/src/ui/views/session/world-chat/tiles/index.tsx +++ b/src/ui/views/session/world-chat/tiles/index.tsx @@ -3,6 +3,7 @@ import { SimpleTile, TileViewConstructor } from "@thirdroom/hydrogen-view-sdk"; import { TextMessageView } from "./TextMessageView"; import { AnnouncementView } from "./AnnouncementView"; import { WorldChatGap } from "./WorldChatGap"; +import { ChatDate } from "./ChatDate"; export function viewClassForTile(vm: SimpleTile): TileViewConstructor { switch (vm.shape) { @@ -13,6 +14,8 @@ export function viewClassForTile(vm: SimpleTile): TileViewConstructor { case "message": case "message-status": return TextMessageView; + case "date-header": + return ChatDate; default: throw new Error( `Tiles of shape "${vm.shape}" are not supported, check the tileClassForEntry function in the view model` diff --git a/src/ui/views/session/world/WorldLoading.tsx b/src/ui/views/session/world/WorldLoading.tsx index 15eb81776..b011dcd2f 100644 --- a/src/ui/views/session/world/WorldLoading.tsx +++ b/src/ui/views/session/world/WorldLoading.tsx @@ -73,7 +73,10 @@ export function WorldLoading({ world, loading, error }: { world: Room; loading: title={world.name ?? world.canonicalAlias ?? "Unknown World"} desc={error.message} options={ - <> +
+ - - +
} /> diff --git a/src/ui/views/session/world/WorldRootView.tsx b/src/ui/views/session/world/WorldRootView.tsx index 7d55cbbde..74ed73d17 100644 --- a/src/ui/views/session/world/WorldRootView.tsx +++ b/src/ui/views/session/world/WorldRootView.tsx @@ -22,6 +22,7 @@ async function getWorldContent(world: Room) { export default function WorldRootView() { const { entered, loading } = useAtomValue(worldAtom); + const setWorld = useSetAtom(worldAtom); const { session } = useHydrogen(true); const isMounted = useIsMounted(); const [error, setError] = useState(); @@ -41,8 +42,7 @@ export default function WorldRootView() { (async () => { try { const content = await getWorldContent(navigatedWorld); - if (!content) return; - await loadWorld(navigatedWorld, content); + await loadWorld(navigatedWorld, content ?? {}); await enterWorld(navigatedWorld); } catch (err) { setError(err as Error); @@ -50,7 +50,7 @@ export default function WorldRootView() { } })(); } - }, [navigatedWorld, reloadId, selectWorld, enterWorld, loadWorld, exitWorld]); + }, [navigatedWorld, reloadId, selectWorld, enterWorld, loadWorld, exitWorld, setWorld]); /** * Selects the world we are entered into for display in the overlay diff --git a/yarn.lock b/yarn.lock index cfd40cb4c..964415225 100644 --- a/yarn.lock +++ b/yarn.lock @@ -741,9 +741,9 @@ "@jridgewell/resolve-uri" "3.1.0" "@jridgewell/sourcemap-codec" "1.4.14" -"@matrix-org/olm@https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.8.tgz": - version "3.2.8" - resolved "https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.8.tgz#8d53636d045e1776e2a2ec6613e57330dd9ce856" +"@matrix-org/olm@https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.14.tgz": + version "3.2.14" + resolved "https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.14.tgz#acd96c00a881d0f462e1f97a56c73742c8dbc984" "@monaco-editor/loader@^1.3.2": version "1.3.2" @@ -1569,12 +1569,12 @@ "@thi.ng/checks" "^3.3.1" "@thi.ng/errors" "^2.2.2" -"@thirdroom/hydrogen-view-sdk@0.0.29": - version "0.0.29" - resolved "https://registry.yarnpkg.com/@thirdroom/hydrogen-view-sdk/-/hydrogen-view-sdk-0.0.29.tgz#2dc2d903d95da8119e2bb08e63ebcb3edf23e1d3" - integrity sha512-8iv/RmKl+ipJejVQplehsPlwXZqEvL5n7Rr0n7W0RpzgOn2d0Rn34bdvezqpuoMlMeM89n7xPypJOw6QhQhfhg== +"@thirdroom/hydrogen-view-sdk@0.1.2": + version "0.1.2" + resolved "https://registry.yarnpkg.com/@thirdroom/hydrogen-view-sdk/-/hydrogen-view-sdk-0.1.2.tgz#49f2c4c1ad4e786ba47d779b17e95ee10c8597a9" + integrity sha512-RsIuz62dEwDqpCXr7jgmxAGC4by2pkUFtpi28zGxqBNkxIc2WQ20qe/9yDDcQ+UGZgAo1t5Tjc3Sz1bhqkzuvg== dependencies: - "@matrix-org/olm" "https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.8.tgz" + "@matrix-org/olm" "https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.14.tgz" another-json "^0.2.0" base64-arraybuffer "^0.2.0" dompurify "^2.3.0"