diff --git a/src/App.tsx b/src/App.tsx index 6d7d1e1e..b87f587c 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -24,7 +24,6 @@ import { RegisterPage } from "./auth/RegisterPage"; import { RoomPage } from "./room/RoomPage"; import { ClientProvider } from "./ClientContext"; import { ErrorPage, LoadingPage } from "./FullScreenView"; -import { DisconnectedBanner } from "./DisconnectedBanner"; import { Initializer } from "./initializer"; import { widget } from "./widget"; import { useTheme } from "./useTheme"; @@ -86,7 +85,6 @@ export const App: FC = ({ vm }) => { } > - } /> } /> diff --git a/src/AppBar.tsx b/src/AppBar.tsx index e70bb50d..aaa7565e 100644 --- a/src/AppBar.tsx +++ b/src/AppBar.tsx @@ -61,7 +61,11 @@ export const AppBar: FC = ({ children }) => { style={{ display: hidden ? "none" : "block" }} className={styles.bar} > -
+
diff --git a/src/Header.tsx b/src/Header.tsx index 577410f8..cffc3402 100644 --- a/src/Header.tsx +++ b/src/Header.tsx @@ -17,27 +17,38 @@ import Logo from "./icons/Logo.svg?react"; import { Avatar, Size } from "./Avatar"; import { EncryptionLock } from "./room/EncryptionLock"; import { useMediaQuery } from "./useMediaQuery"; +import { DisconnectedBanner } from "./DisconnectedBanner"; interface HeaderProps extends HTMLAttributes { ref?: Ref; children: ReactNode; className?: string; + /** + * Whether the header should display an informational banner whenever the + * client is disconnected from the homeserver. + * @default true + */ + disconnectedBanner?: boolean; } export const Header: FC = ({ ref, children, className, + disconnectedBanner = true, ...rest }) => { return ( -
- {children} -
+ <> +
+ {children} +
+ {disconnectedBanner && } + ); }; diff --git a/src/room/InCallView.tsx b/src/room/InCallView.tsx index f9bd681c..d4026099 100644 --- a/src/room/InCallView.tsx +++ b/src/room/InCallView.tsx @@ -508,7 +508,11 @@ export const InCallView: FC = ({ break; case "standard": header = ( -
+
this.matrixRTCSession.memberships), ); - private readonly matrixRTCConnected$ = this.scope.behavior( - this.memberships$.pipe( - map((ms) => - ms.some( - (m) => m.sender === this.userId && m.deviceId === this.deviceId, + private readonly matrixConnected$ = this.scope.behavior( + // To consider ourselves connected to MatrixRTC, we check the following: + and$( + // The client is connected to the sync loop + ( + fromEvent(this.matrixRoom.client, ClientEvent.Sync) as Observable< + [SyncState] + > + ).pipe( + startWith([this.matrixRoom.client.getSyncState()]), + map(([state]) => state === SyncState.Syncing), + ), + // We can see our own call membership + this.memberships$.pipe( + map((ms) => + ms.some( + (m) => m.sender === this.userId && m.deviceId === this.deviceId, + ), ), ), ), ); + // TODO: Account for LiveKit connection state too + private readonly connected$ = this.matrixConnected$; + + /** + * Whether we should tell the user that we're reconnecting to the call. + */ public readonly reconnecting$ = this.scope.behavior( - this.matrixRTCConnected$.pipe( + this.connected$.pipe( // We are reconnecting if we previously had some successful initial // connection but are now disconnected scan( @@ -1533,7 +1554,7 @@ export class CallViewModel extends ViewModel { // Pause all media tracks when we're disconnected from MatrixRTC, because it // can be an unpleasant surprise for the app to say 'reconnecting' and yet // still be transmitting your media to others. - this.matrixRTCConnected$.pipe(this.scope.bind()).subscribe((connected) => { + this.matrixConnected$.pipe(this.scope.bind()).subscribe((connected) => { const publications = this.livekitRoom.localParticipant.trackPublications.values(); if (connected) { diff --git a/src/utils/observable.ts b/src/utils/observable.ts index 22f7c455..1c3a3be7 100644 --- a/src/utils/observable.ts +++ b/src/utils/observable.ts @@ -7,6 +7,7 @@ Please see LICENSE in the repository root for full details. import { type Observable, + combineLatest, concat, defer, finalize, @@ -86,3 +87,11 @@ export function getValue(state$: Observable): T { if (value === nothing) throw new Error("Not a state Observable"); return value; } + +/** + * Creates an Observable that has a value of true whenever all its inputs are + * true. + */ +export function and$(...inputs: Observable[]): Observable { + return combineLatest(inputs, (...flags) => flags.every((flag) => flag)); +}