Compare commits

..

33 Commits

Author SHA1 Message Date
Timo K
121d87b1d0 Merge branch 'livekit' into toger5/contribut-issue-first 2026-05-05 14:39:52 +02:00
Timo
b276eeee75 Merge pull request #3927 from element-hq/toger5/back-button-press-control-on-esc
Refactor of Escape keyboard event bahvior (allow mute/emoji shortcuts after closing a modal with escape)
2026-05-05 00:39:53 +08:00
Robin
5ca50fdb63 Merge pull request #3913 from element-hq/fkwp/feature_relax_homeserver_offline
[Feature] add grace period before reporting the sync loop as disconnected
2026-05-04 18:18:27 +02:00
Robin
7916360b52 Convert all grace period tests to marble tests 2026-05-04 18:01:41 +02:00
Robin
83a9a2a86c Merge pull request #3903 from element-hq/edge-to-edge
Allow calls to display edge-to-edge on mobile
2026-05-04 15:40:08 +02:00
fkwp
0dc8fab256 linting 2026-05-04 13:36:26 +02:00
fkwp
69757b06ce prettier 2026-05-04 13:27:57 +02:00
fkwp
a081b40fb2 add marble test 2026-05-04 13:24:19 +02:00
fkwp
88b5db6675 fix rtsSession initial state 2026-05-04 10:41:48 +02:00
fkwp
22f16174ef addressing review comments 2026-05-04 09:16:55 +02:00
Robin
0470dabff2 Merge pull request #3940 from element-hq/renovate/all-minor-patch
Update all non-major dependencies
2026-05-01 16:10:27 +02:00
renovate[bot]
5a30666939 Update all non-major dependencies 2026-05-01 13:53:45 +00:00
Robin
f093946a29 Allow calls to display edge-to-edge on mobile
By adding viewport-fit=cover to the <meta name="viewport"> header, the page now requests to be displayed edge-to-edge across the entire screen. This gives us control over what we display around camera cut-outs and system navigation UI, if the user agent supports it. I then adjusted the styles of various UI elements to ensure that they still lie within the screen's safe area.
2026-05-01 12:14:40 +02:00
fkwp
6c25048b92 Merge branch 'livekit' into fkwp/feature_relax_homeserver_offline 2026-04-30 21:02:24 +02:00
fkwp
2e4ca9b761 use 10 Seconds of grace period in config.sample.json 2026-04-30 20:54:46 +02:00
fkwp
42aae307f9 use latest stable version of jwt service 2026-04-30 20:53:18 +02:00
fkwp
24c2845958 move starWith to corresponding branch in switchMap 2026-04-30 20:50:45 +02:00
renovate[bot]
51209aa825 Update dependency livekit-client to v2.18.8 (#3934)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-30 20:12:17 +02:00
Timo K
d5b218283d add max reply SLA 2026-04-30 13:14:13 +02:00
Timo K
d4a3c8bd80 Contributing.md update. Issue First policy 2026-04-27 13:57:38 +02:00
fkwp
39ef815d8b Merge branch 'livekit' into fkwp/feature_relax_homeserver_offline 2026-04-23 17:29:14 +02:00
fkwp
0abc32b80e prettier 2026-04-23 15:47:40 +02:00
fkwp
c1f821ca0f Update default sync disconnect grace period to 10000ms in configuration and related functions 2026-04-23 15:33:00 +02:00
fkwp
283c606b9d Add tests for grace period handling in createHomeserverConnected$ 2026-04-22 21:46:25 +02:00
fkwp
ee343935a4 Merge branch 'fkwp/delegation_of_delayed_events' into fkwp/feature_relax_homeserver_offline 2026-04-22 21:11:23 +02:00
fkwp
032ec662e9 bump lk-jwt-service 2026-04-22 21:08:49 +02:00
fkwp
9fccd49d7d Improve the handling of the grace period for the home server connection to support both immediate and delayed emissions. 2026-04-22 21:08:31 +02:00
fkwp
9f2db7cfc3 Merge branch 'livekit' into fkwp/delegation_of_delayed_events 2026-04-22 20:42:00 +02:00
fkwp
9e2eef09d4 Add support for a grace period for /sync (aka homeserver disconnected) interruptions 2026-04-22 15:18:50 +02:00
fkwp
cbb9e7a289 Merge branch 'livekit' into fkwp/delegation_of_delayed_events 2026-04-01 11:46:29 +02:00
fkwp
4006a8d9f9 Merge branch 'livekit' into fkwp/delegation_of_delayed_events 2026-03-27 15:29:16 +01:00
fkwp
7b1eb03e2a add ca certs to SFU containers such that webhooks are accepted 2026-02-06 11:54:39 +01:00
fkwp
d3a4234f4a setup webhooks for SFUs 2026-02-06 11:54:22 +01:00
19 changed files with 1031 additions and 699 deletions

View File

@@ -1,4 +1,11 @@
<!-- Thanks for submitting a PR! Please ensure the following requirements are met in order for us to review your PR -->
<!-- Thanks for submitting a PR! Please read CONTRIBUTING.md before you start. -->
> [!IMPORTANT]
> **Features and UI changes require a pre-approved issue.**
> Every PR must have a linked issue
> that a maintainer has reviewed and approved **before you started writing code**.
> PRs that don't meet this requirement will not be reviewed.
> See [CONTRIBUTING.md](https://github.com/element-hq/element-call/blob/livekit/CONTRIBUTING.md) for ElementCall decided for this approach.
## Content
@@ -6,34 +13,34 @@
## Motivation and context
<!-- Provide link to the corresponding issue if applicable or explain the context -->
<!-- Provide a link to the pre-approved issue, or explain the context for a bug fix -->
## Screenshots / GIFs
<!--
You can use a table like this to show screenshots comparison.
Uncomment this markdown table below and edit the last line `|||`:
|copy screenshot of before here|copy screenshot of after here|
You can use a table like this to show a before/after comparison.
Uncomment the markdown table below and fill in the last line:
|Before|After|
|-|-|
|||
-->
-->
## Tests
<!-- Explain how you tested your development -->
<!-- Explain how you tested your changes -->
- Step 1
- Step 2
- Step ...
-
## Checklist
- [ ] I have read through [CONTRIBUTING.md](https://github.com/element-hq/element-call/blob/livekit/CONTRIBUTING.md).
- [ ] Pull request includes screenshots or videos if containing UI changes
- [ ] Tests written for new code (and old code if feasible).
- [ ] A linked, pre-approved issue exists for this feature or UI change.
- [ ] I have read [CONTRIBUTING.md](https://github.com/element-hq/element-call/blob/livekit/CONTRIBUTING.md) in full.
- [ ] Pull request includes screenshots or videos for any UI changes.
- [ ] Tests written for new code (and existing touched code where feasible).
- [ ] Linter and other CI checks pass.
- [ ] I have licensed the changes to Element by completing the [Contributor License Agreement (CLA)](https://cla-assistant.io/element-hq/element-call)

View File

@@ -1,3 +1,71 @@
# Contributing code to Element
Element follows the same pattern as the [matrix-js-sdk](https://github.com/matrix-org/matrix-js-sdk/blob/develop/CONTRIBUTING.md).
# Contributing to Element Call
Element Call is a native Matrix video conferencing application built on
[MatrixRTC (MSC4143)](https://github.com/matrix-org/matrix-spec-proposals/pull/4143)
and [LiveKit](https://livekit.io/). It runs in multiple deployment contexts — as a
standalone web app and as a widget embedded in Element Web, Element X iOS, and
Element X Android. It is also the primary R&D foundation for MatrixRTC, which means
its architecture, maintainability, and flexibility are held to a high standard.
We welcome contributions from the community. This document explains how to
contribute effectively so that both you and the maintainers get the best outcome.
## Issue First Policy
> [!IMPORTANT]
> Before writing a single line of code for a new feature or UI change, you **must**
> open an issue and have the approach agreed with the maintainers.
>
> **We will not review or merge feature or UI pull requests that arrive without a
> corresponding, pre-approved issue.**
This is not gatekeeping — it's how we prevent wasted effort on both sides. Element
Call must work correctly across multiple deployment contexts and meet specific product
and design requirements. It is also a fast-moving codebase that underpins ongoing
MatrixRTC development. A PR that looks reasonable in isolation can easily conflict
with in-progress work, planned architecture changes, or design decisions that haven't
been publicly documented yet.
The issue is where we resolve all of that **before** anyone writes code.
**Bug fixes** are no exception — most confirmed bugs should already have an issue anyways, existing issues that are marked as bugs have an implicit maintainer approval. If the solution for the bug is controversial it is highly recommended to discuss the approach in the issue before opening a PR.
## Contribution Workflow
1. **Open an issue** using the [Enhancement request](https://github.com/element-hq/element-call/issues/new?template=enhancement.yml) template.
2. **Wait for feedback.** A maintainer will comment on the issue **within two weeks**. The use case and approach will get dicussed.
This may involve questions, suggestions, or a request to adjust scope.
This also allows to bring design and product into the loop before code gets created.
3. **Get a green light.** Wait for explicit approval from a maintainer before starting
implementation.
4. **Implement.** Write the code against the agreed approach.
5. **Open a PR.** Link to the issue in your PR description and satisfy the checklist
in the PR template.
## Code Quality
Element Call moves fast and the codebase must stay clean and maintainable.
- **Take responsibility for AI-generated code.** AI tools can be a useful aid, but we expect all the generated code to be understood and reasoned about by the contributor. Questions by the maintainers should be answered without just forwarding them to AI. The maintainers also have access to AI tools. If your contribution is just transporting messages between LLM <-> maintaines all our time is better used if the maintainers decide to interact with AI for this specific problem by themselves.
- **Think across deployment contexts.** Changes must work correctly in both standalone
and widget modes. Consider how your change interacts with Element Web, Element X
iOS, and Element X Android.
- **Write tests.** New functionality should be covered by tests. Where it is feasible,
existing uncovered code touched by your PR should also gain tests.
## Contributor License Agreement
All contributors must sign the
[Element Contributor License Agreement](https://cla-assistant.io/element-hq/element-call)
before their contribution can be merged. The CLA assistant bot will prompt you
automatically when you open a PR.
## Getting Help
The best place to ask questions about Element Call development is the MatrixRTC room:
**[#matrixRtc:matrix.org](https://matrix.to/#/#matrixrtc:matrix.org)**

View File

@@ -10,7 +10,7 @@
<meta
name="viewport"
content="width=device-width, initial-scale=1.0, maximum-scale=1.0"
content="viewport-fit=cover, width=device-width, initial-scale=1.0, maximum-scale=1.0"
/>
<title><%- brand %></title>
<script>

View File

@@ -153,7 +153,7 @@
"glob": "^10.5.0",
"qs": "^6.14.1",
"js-yaml": "^4.1.1",
"esbuild": "^0.27.7"
"esbuild": "^0.28.0"
}
},
"packageManager": "pnpm@10.33.0"

1417
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,10 +1,9 @@
.bar {
block-size: 64px;
flex-shrink: 0;
}
.bar > header {
position: absolute;
position: sticky;
inset-inline: 0;
inset-block-start: 0;
block-size: 64px;

View File

@@ -12,7 +12,9 @@ Please see LICENSE in the repository root for full details.
align-items: center;
user-select: none;
flex-shrink: 0;
padding-inline: var(--inline-content-inset);
padding-left: var(--content-inset-left);
padding-right: var(--content-inset-right);
padding-top: env(safe-area-inset-top);
}
.nav {

View File

@@ -14,7 +14,11 @@ Please see LICENSE in the repository root for full details.
grid-template-areas: ". buttons layout";
align-items: center;
gap: var(--cpd-space-3x);
padding: var(--cpd-space-10x) var(--cpd-space-6x);
/* Ensure that footer lies within the safe area */
padding-left: calc(env(safe-area-inset-left) + var(--cpd-space-6x));
padding-right: calc(env(safe-area-inset-right) + var(--cpd-space-6x));
padding-block: var(--cpd-space-10x)
calc(env(safe-area-inset-bottom) + var(--cpd-space-10x));
background: linear-gradient(
180deg,
rgba(0, 0, 0, 0) 0%,
@@ -118,13 +122,15 @@ Once we exceed 500 we hide everything except the buttons.
@media (max-height: 800px) {
.footer {
padding-block: var(--cpd-space-8x);
padding-block: var(--cpd-space-8x)
calc(env(safe-area-inset-bottom) + var(--cpd-space-8x));
}
}
@media (max-height: 400px) {
.footer {
padding-block: var(--cpd-space-4x);
padding-block: var(--cpd-space-4x)
calc(env(safe-area-inset-bottom) + var(--cpd-space-4x));
}
}
@@ -140,7 +146,9 @@ Once we exceed 500 we hide everything except the buttons.
}
.footer {
padding-block-start: var(--cpd-space-3x);
padding-block-end: var(--cpd-space-2x);
padding-block-end: calc(
env(safe-area-inset-bottom) + var(--cpd-space-2x)
);
}
}
}

View File

@@ -157,6 +157,8 @@ export const UnavailableMediaDevices: Story = {
...Default,
args: {
...Default.args,
audioEnabled: false,
videoEnabled: false,
toggleAudio: undefined,
toggleVideo: undefined,
audioOutputSwitcher: undefined,

View File

@@ -97,6 +97,13 @@ export interface ConfigOptions {
enable_video?: boolean;
};
/**
* Grace period in milliseconds to wait before reporting the sync loop as disconnected.
* This allows brief sync interruptions without triggering a reconnection message.
* Default is 10000ms (10 seconds). Set to 0 to disable the grace period.
*/
sync_disconnect_grace_period_ms?: number;
/**
* These are low level options that are used to configure the MatrixRTC session.
* Take care when changing these options.
@@ -155,6 +162,7 @@ export interface ResolvedConfigOptions extends ConfigOptions {
server_name: string;
};
};
sync_disconnect_grace_period_ms: number;
ssla: string;
}
@@ -168,5 +176,6 @@ export const DEFAULT_CONFIG: ResolvedConfigOptions = {
features: {
feature_use_device_session_member_events: true,
},
sync_disconnect_grace_period_ms: 10000,
ssla: "https://static.element.io/legal/element-software-and-services-license-agreement-uk-1.pdf",
};

View File

@@ -266,6 +266,20 @@ export function Grid<
}, []),
useCallback(() => window.innerHeight, []),
);
const orientation = useSyncExternalStore(
useCallback((onChange) => {
// Support for the change event is experimental
// https://developer.mozilla.org/en-US/docs/Web/API/Screen/change_event#browser_compatibility
(screen as unknown as EventTarget).addEventListener?.("change", onChange);
return (): void =>
(screen as unknown as EventTarget).removeEventListener?.(
"change",
onChange,
);
}, []),
useCallback(() => window.innerHeight, []),
);
const [layoutRoot, setLayoutRoot] = useState<HTMLElement | null>(null);
const [generation, setGeneration] = useState<number | null>(null);
const [visibleTilesCallback, setVisibleTilesCallback] =
@@ -336,10 +350,10 @@ export function Grid<
}
return result;
// The rects may change due to the grid resizing or updating to a new
// generation, but eslint can't statically verify this
// The rects may change due to the grid resizing, changing orientation, or
// updating to a new generation, but eslint can't statically verify this
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [gridRoot, layoutRoot, tiles, gridBounds, generation]);
}, [gridRoot, layoutRoot, tiles, gridBounds, orientation, generation]);
// The height of the portion of the grid visible at any given time
const visibleHeight = useMemo(

View File

@@ -31,8 +31,9 @@ Please see LICENSE in the repository root for full details.
position: absolute;
inline-size: 404px;
block-size: 233px;
inset-block: 0;
inset-inline: var(--cpd-space-3x);
/* Ensure that spotlight tile lies within the safe area */
inset: 0 calc(env(safe-area-inset-right) + var(--cpd-space-3x)) 0
calc(env(safe-area-inset-left) + var(--cpd-space-3x));
}
.fixed > .slot[data-block-alignment="start"] {

View File

@@ -18,7 +18,11 @@ Please see LICENSE in the repository root for full details.
position: absolute;
inline-size: 135px;
block-size: 160px;
inset: var(--cpd-space-4x);
/* Ensure that PiP lies within the safe area */
inset: calc(env(safe-area-inset-top) + var(--cpd-space-4x))
var(--content-inset-right)
calc(env(safe-area-inset-bottom) + var(--cpd-space-4x))
var(--content-inset-left);
}
@media (min-width: 600px) {

View File

@@ -37,10 +37,20 @@ layer(compound);
--cpd-color-border-accent: var(--cpd-color-green-800);
/* The distance to inset non-full-width content from the edge of the window
along the inline axis. This ramps up from 16px for typical mobile windows, to
96px for typical desktop windows. */
--inline-content-inset: min(
var(--cpd-space-24x),
max(var(--cpd-space-4x), calc((100vw - 900px) / 3))
96px for typical desktop windows, and accounts for the safe area. */
--content-inset-left: calc(
env(safe-area-inset-left) +
min(
var(--cpd-space-24x),
max(var(--cpd-space-4x), calc((100vw - 900px) / 3))
)
);
--content-inset-right: calc(
env(safe-area-inset-right) +
min(
var(--cpd-space-24x),
max(var(--cpd-space-4x), calc((100vw - 900px) / 3))
)
);
--small-drop-shadow: 0px 1.2px 2.4px 0px rgba(0, 0, 0, 0.15);
--big-drop-shadow: 0px 0px 24px 0px #1b1d221a;

View File

@@ -57,7 +57,8 @@ Please see LICENSE in the repository root for full details.
flex: 1;
flex-direction: column;
align-items: center;
padding-inline: var(--inline-content-inset);
padding-left: var(--content-inset-left);
padding-right: var(--content-inset-right);
}
.logo {

View File

@@ -6,7 +6,8 @@ Please see LICENSE in the repository root for full details.
*/
.preview {
margin-inline: var(--inline-content-inset);
margin-left: var(--content-inset-left);
margin-right: var(--content-inset-right);
min-block-size: 0;
block-size: 50vh;
border-radius: var(--cpd-space-4x);
@@ -80,6 +81,7 @@ video.mirror {
}
.buttonBar {
padding-inline: var(--inline-content-inset);
padding-left: var(--content-inset-left);
padding-right: var(--content-inset-right);
}
}

View File

@@ -13,6 +13,7 @@ import { MembershipManagerEvent, Status } from "matrix-js-sdk/lib/matrixrtc";
import { ObservableScope } from "../../ObservableScope";
import { createHomeserverConnected$ } from "./HomeserverConnected";
import { testScope, withTestScheduler } from "../../../utils/test";
/**
* Minimal stub of a Matrix client sufficient for our tests:
@@ -96,19 +97,20 @@ describe("createHomeserverConnected$", () => {
// LLM generated test cases. They are a bit overkill but I improved the mocking so it is
// easy enough to read them so I think they can stay.
// Note: gracePeriodMs is set to 0 to avoid debouncing delays in tests
it("is false when sync state is not Syncing", () => {
const hsConnected = createHomeserverConnected$(scope, client, session);
const hsConnected = createHomeserverConnected$(scope, client, session, 0);
expect(hsConnected.combined$.value).toBe(false);
});
it("remains false while membership status is not Connected even if sync is Syncing", () => {
const hsConnected = createHomeserverConnected$(scope, client, session);
const hsConnected = createHomeserverConnected$(scope, client, session, 0);
client.setSyncState(SyncState.Syncing);
expect(hsConnected.combined$.value).toBe(false); // membership still disconnected
});
it("is false when membership status transitions to Connected but ProbablyLeft is true", () => {
const hsConnected = createHomeserverConnected$(scope, client, session);
const hsConnected = createHomeserverConnected$(scope, client, session, 0);
// Make sync loop OK
client.setSyncState(SyncState.Syncing);
// Indicate probable leave before connection
@@ -118,7 +120,7 @@ describe("createHomeserverConnected$", () => {
});
it("becomes true only when all three conditions are satisfied", () => {
const hsConnected = createHomeserverConnected$(scope, client, session);
const hsConnected = createHomeserverConnected$(scope, client, session, 0);
// 1. Sync loop connected
client.setSyncState(SyncState.Syncing);
expect(hsConnected.combined$.value).toBe(false); // not yet membership connected
@@ -128,7 +130,7 @@ describe("createHomeserverConnected$", () => {
});
it("drops back to false when sync loop leaves Syncing", () => {
const hsConnected = createHomeserverConnected$(scope, client, session);
const hsConnected = createHomeserverConnected$(scope, client, session, 0);
// Reach connected state
client.setSyncState(SyncState.Syncing);
session.setMembershipStatus(Status.Connected);
@@ -140,7 +142,7 @@ describe("createHomeserverConnected$", () => {
});
it("drops back to false when membership status becomes disconnected", () => {
const hsConnected = createHomeserverConnected$(scope, client, session);
const hsConnected = createHomeserverConnected$(scope, client, session, 0);
client.setSyncState(SyncState.Syncing);
session.setMembershipStatus(Status.Connected);
expect(hsConnected.combined$.value).toBe(true);
@@ -150,7 +152,7 @@ describe("createHomeserverConnected$", () => {
});
it("drops to false when ProbablyLeft is emitted after being true", () => {
const hsConnected = createHomeserverConnected$(scope, client, session);
const hsConnected = createHomeserverConnected$(scope, client, session, 0);
client.setSyncState(SyncState.Syncing);
session.setMembershipStatus(Status.Connected);
expect(hsConnected.combined$.value).toBe(true);
@@ -160,7 +162,7 @@ describe("createHomeserverConnected$", () => {
});
it("recovers to true if ProbablyLeft becomes false again while other conditions remain true", () => {
const hsConnected = createHomeserverConnected$(scope, client, session);
const hsConnected = createHomeserverConnected$(scope, client, session, 0);
client.setSyncState(SyncState.Syncing);
session.setMembershipStatus(Status.Connected);
expect(hsConnected.combined$.value).toBe(true);
@@ -174,7 +176,7 @@ describe("createHomeserverConnected$", () => {
});
it("composite sequence reflects each individual failure reason", () => {
const hsConnected = createHomeserverConnected$(scope, client, session);
const hsConnected = createHomeserverConnected$(scope, client, session, 0);
// Initially false (sync error + disconnected + not probably left)
expect(hsConnected.combined$.value).toBe(false);
@@ -200,3 +202,62 @@ describe("createHomeserverConnected$", () => {
expect(hsConnected.combined$.value).toBe(false);
});
});
describe("createHomeserverConnected$ - Grace Period", () => {
const GRACE_PERIOD = 5;
function marbleTest(
syncStateMarbles: string,
expectedConnectedMarbles: string,
): void {
withTestScheduler(({ behavior, schedule, expectObservable }) => {
const syncState$ = behavior(syncStateMarbles, {
s: SyncState.Syncing,
e: SyncState.Error,
});
const client = new MockMatrixClient(syncState$.value);
schedule(syncStateMarbles, {
s: () => client.setSyncState(SyncState.Syncing),
e: () => client.setSyncState(SyncState.Error),
});
const session = new MockMatrixRTCSession({
membershipStatus: Status.Connected,
probablyLeft: false,
});
const hsConnected = createHomeserverConnected$(
testScope(),
client,
session,
GRACE_PERIOD,
);
expectObservable(hsConnected.combined$).toBe(expectedConnectedMarbles, {
y: true,
n: false,
});
});
}
it("respects gracePeriodMs: stays true during grace period and flips false after", () => {
// - Initial state: Everything is connected
// - Sync error occurs -> should remain connected due to grace period
// - After grace period, not connected
marbleTest("se", "y-----n");
// If the sync error takes longer to occur, it should take equally long for
// the connection state to change
marbleTest("s--e", "y-------n");
});
it("recovers immediately if sync returns during grace period", () => {
// - Initial state: Connected
// - Sync error occurs
// - Sync recovers BEFORE the grace period expires
// - Connection state remains constant
marbleTest("se--s", "y");
});
it("flips to true IMMEDIATELY even if a grace period was pending", () => {
// - Initial error: connection eventually flips to false
// - Back to Syncing -> Must be connected immediately (synchronously)
marbleTest("e-----s", "y----ny");
});
});

View File

@@ -12,9 +12,20 @@ import {
type MatrixRTCSession,
} from "matrix-js-sdk/lib/matrixrtc";
import { ClientEvent, type MatrixClient, SyncState } from "matrix-js-sdk";
import { fromEvent, startWith, map, tap, type Observable } from "rxjs";
import {
fromEvent,
startWith,
map,
tap,
type Observable,
distinctUntilChanged,
switchMap,
of,
delay,
} from "rxjs";
import { logger as rootLogger } from "matrix-js-sdk/lib/logger";
import { Config } from "../../../config/Config";
import { type ObservableScope } from "../../ObservableScope";
import { type Behavior } from "../../Behavior";
import { and$ } from "../../../utils/observable";
@@ -35,28 +46,46 @@ export interface HomeserverConnected {
* for the purposes of a MatrixRTC session.
*
* Becomes FALSE if ANY sub-condition is fulfilled:
* 1. Sync loop is not in SyncState.Syncing
* 1. Sync loop is not in SyncState.Syncing (after grace period)
* 2. membershipStatus !== Status.Connected
* 3. probablyLeft === true
*
* @param scope - The observable scope for lifecycle management.
* @param client - The Matrix client to monitor sync state.
* @param matrixRTCSession - The RTC session to monitor membership.
* @param gracePeriodMs - Grace period in milliseconds to wait before reporting sync disconnect.
* If not provided, uses the config value (default 10000ms).
*/
export function createHomeserverConnected$(
scope: ObservableScope,
client: NodeStyleEventEmitter & Pick<MatrixClient, "getSyncState">,
matrixRTCSession: NodeStyleEventEmitter &
Pick<MatrixRTCSession, "membershipStatus" | "probablyLeft">,
gracePeriodMs?: number,
): HomeserverConnected {
// Get grace period from parameter or config (default 10000ms)
const graceMs = gracePeriodMs ?? Config.get().sync_disconnect_grace_period_ms;
const syncing$ = (
fromEvent(client, ClientEvent.Sync) as Observable<[SyncState]>
).pipe(
startWith([client.getSyncState()]),
map(([state]) => state === SyncState.Syncing),
distinctUntilChanged(),
switchMap((isSyncing) => {
if (isSyncing || graceMs <= 0) {
return of(isSyncing);
}
return of(false).pipe(delay(graceMs), startWith(true));
}),
distinctUntilChanged(),
);
const rtsSession$ = scope.behavior<Status>(
fromEvent(matrixRTCSession, MembershipManagerEvent.StatusChanged).pipe(
map(() => matrixRTCSession.membershipStatus ?? Status.Unknown),
),
Status.Unknown,
matrixRTCSession.membershipStatus ?? Status.Unknown,
);
const membershipConnected$ = rtsSession$.pipe(

View File

@@ -33,6 +33,12 @@ Please see LICENSE in the repository root for full details.
--media-view-fg-inset: 10px;
}
.maximised .item {
/* Ensure that foreground elements lie within the safe area */
--media-view-fg-inset: 10px calc(env(safe-area-inset-right) + 10px) 10px
calc(env(safe-area-inset-left) + 10px);
}
.item.snap {
scroll-snap-align: start;
}