diff --git a/MatrixUtils.Web/Pages/Labs/Client/ClientComponents/ClientRoomList.razor b/MatrixUtils.Web/Pages/Labs/Client/ClientComponents/ClientRoomList.razor
new file mode 100644
index 0000000..b370080
--- /dev/null
+++ b/MatrixUtils.Web/Pages/Labs/Client/ClientComponents/ClientRoomList.razor
@@ -0,0 +1,15 @@
+@using ClientContext = MatrixUtils.Web.Pages.Labs.Client.Index.ClientContext
+@* user header and room list *@
+@foreach (var room in Data.SyncWrapper.Rooms) {
+ <LinkButton OnClick="@(async () => Data.SelectedRoom = room)" Color="@(Data.SelectedRoom == room ? "#FF00FF" : "")">
+ @room.RoomName
+ </LinkButton>
+ <br/>
+}
+
+@code {
+
+ [Parameter]
+ public ClientContext Data { get; set; } = null!;
+
+}
\ No newline at end of file
diff --git a/MatrixUtils.Web/Pages/Labs/Client/ClientComponents/ClientStatusList.razor b/MatrixUtils.Web/Pages/Labs/Client/ClientComponents/ClientStatusList.razor
new file mode 100644
index 0000000..c680c13
--- /dev/null
+++ b/MatrixUtils.Web/Pages/Labs/Client/ClientComponents/ClientStatusList.razor
@@ -0,0 +1,35 @@
+@using ClientContext = MatrixUtils.Web.Pages.Labs.Client.Index.ClientContext;
+@using System.Collections.ObjectModel
+
+@foreach (var ctx in Data) {
+ <pre>
+ @ctx.Homeserver.UserId - @ctx.SyncWrapper.Status
+ </pre>
+}
+
+@code {
+
+ [Parameter]
+ public ObservableCollection<ClientContext> Data { get; set; } = null!;
+
+ protected override void OnInitialized() {
+ Data.CollectionChanged += (_, e) => {
+ foreach (var item in e.NewItems?.Cast<ClientContext>() ?? []) {
+ item.SyncWrapper.PropertyChanged += (_, pe) => {
+ if (pe.PropertyName == nameof(item.SyncWrapper.Status))
+ StateHasChanged();
+ };
+ }
+
+ StateHasChanged();
+ };
+
+ Data.ToList().ForEach(ctx => {
+ ctx.SyncWrapper.PropertyChanged += (_, pe) => {
+ if (pe.PropertyName == nameof(ctx.SyncWrapper.Status))
+ StateHasChanged();
+ };
+ });
+ }
+
+}
\ No newline at end of file
diff --git a/MatrixUtils.Web/Pages/Labs/Client/ClientComponents/ClientSyncWrapper.cs b/MatrixUtils.Web/Pages/Labs/Client/ClientComponents/ClientSyncWrapper.cs
new file mode 100644
index 0000000..16051b8
--- /dev/null
+++ b/MatrixUtils.Web/Pages/Labs/Client/ClientComponents/ClientSyncWrapper.cs
@@ -0,0 +1,41 @@
+using System.Collections.ObjectModel;
+using ArcaneLibs;
+using LibMatrix;
+using LibMatrix.Helpers;
+using LibMatrix.Homeservers;
+using LibMatrix.Responses;
+using MatrixUtils.Abstractions;
+
+namespace MatrixUtils.Web.Pages.Client.ClientComponents;
+
+public class ClientSyncWrapper(AuthenticatedHomeserverGeneric homeserver) : NotifyPropertyChanged {
+ private SyncHelper _syncHelper = new SyncHelper(homeserver) {
+ MinimumDelay = TimeSpan.FromMilliseconds(2000),
+ IsInitialSync = false
+ };
+ private string _status = "Loading...";
+
+ public ObservableCollection<StateEvent> AccountData { get; set; } = new();
+ public ObservableCollection<RoomInfo> Rooms { get; set; } = new();
+
+ public string Status {
+ get => _status;
+ set => SetField(ref _status, value);
+ }
+
+ public async Task Start() {
+ Task.Yield();
+ var resp = _syncHelper.EnumerateSyncAsync();
+ Status = $"[{DateTime.Now:s}] Syncing...";
+ await foreach (var response in resp) {
+ Task.Yield();
+ Status = $"[{DateTime.Now:s}] {response.Rooms?.Join?.Count ?? 0 + response.Rooms?.Invite?.Count ?? 0 + response.Rooms?.Leave?.Count ?? 0} rooms, {response.AccountData?.Events?.Count ?? 0} account data, {response.ToDevice?.Events?.Count ?? 0} to-device, {response.DeviceLists?.Changed?.Count ?? 0} device lists, {response.Presence?.Events?.Count ?? 0} presence updates";
+ await HandleSyncResponse(response);
+ await Task.Yield();
+ }
+ }
+
+ private async Task HandleSyncResponse(SyncResponse resp) {
+
+ }
+}
\ No newline at end of file
diff --git a/MatrixUtils.Web/Pages/Labs/Client/ClientComponents/MatrixClient.razor b/MatrixUtils.Web/Pages/Labs/Client/ClientComponents/MatrixClient.razor
new file mode 100644
index 0000000..7d3e52a
--- /dev/null
+++ b/MatrixUtils.Web/Pages/Labs/Client/ClientComponents/MatrixClient.razor
@@ -0,0 +1,31 @@
+@using Index = MatrixUtils.Web.Pages.Labs.Client.Index
+@using MatrixUtils.Web.Pages.Client.ClientComponents
+
+<div class="container-fluid">
+ <div class="row">
+ <div class="col-3">
+ <ClientRoomList Data="@Data"/>
+ </div>
+ <div class="col-6">
+ @if (Data.SelectedRoom != null) {
+ <Index.RoomHeader Data="@Data"/>
+ <Index.RoomTimeline Data="@Data"/>
+ }
+ else {
+ <p>No room selected</p>
+ }
+ </div>
+ @if (Data.SelectedRoom != null) {
+ <div class="col-3">
+ <Index.UserList Data="@Data"/>
+ </div>
+ }
+ </div>
+</div>
+
+@code {
+
+ [Parameter]
+ public Index.ClientContext Data { get; set; } = null!;
+
+}
\ No newline at end of file
|