Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions Sources/MactrixApp/Models/WindowState.swift
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ public final class WindowState {
public var selectedScreen: SelectedScreen = .none

public var selectedRoomId: String?
public var selectedSpaceId: String?
public var inspectorVisible: Bool = false

public var inspectorContent: InspectorContent = .roomInfo
Expand Down Expand Up @@ -129,17 +130,20 @@ public final class WindowState {
extension WindowState: @MainActor RawRepresentable {
public struct SceneStorageRepresentation: Codable {
let selectedRoomId: String?
let selectedSpaceId: String?
let inspectorVisible: Bool
let sidebarSections: SidebarSectionCollapsibility

@MainActor init(windowState: WindowState) {
self.selectedRoomId = windowState.selectedRoomId
self.selectedSpaceId = windowState.selectedSpaceId
self.inspectorVisible = windowState.inspectorVisible
self.sidebarSections = windowState.sidebarSections
}

@MainActor func restore(windowState: WindowState) {
windowState.selectedRoomId = selectedRoomId
windowState.selectedSpaceId = selectedSpaceId
windowState.inspectorVisible = inspectorVisible
windowState.sidebarSections = sidebarSections
}
Expand Down
69 changes: 69 additions & 0 deletions Sources/MactrixApp/Views/Sidebar/SidebarGroupsList.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import MactrixUI
import MatrixIntegration
import MatrixRustSDK
import SwiftUI

struct SidebarGroupsList: View {
@Environment(AppState.self) var appState

var spaces: [SidebarSpaceRoom] {
appState.matrixClient?.spaceService.spaceRooms ?? []
}

var body: some View {
ScrollView {
VStack(spacing: 8) {
GroupButton(id: nil, title: "Home") {
Image(systemName: "house.fill")
.font(.title2)

}
ForEach(spaces) { space in
GroupButton(id: space.id, title: space.spaceRoom.displayName) {
RoomRowAvatarImage(
avatarUrl: space.spaceRoom.avatarUrl, placeholderSystemImage: "square.grid.2x2.fill",
imageLoader: appState.matrixClient
)
}
}
}
.frame(width: 62)
}
}

}

private struct GroupButton<Content: View>: View {
@Environment(WindowState.self) var windowState

let id: String?
let title: String
let content: () -> Content

var body: some View {
Button {
windowState.selectedSpaceId = id
} label: {
content()
.frame(width: 42, height: 42)
.clipShape(Circle())
.background {
Circle()
.fill(.background)
.shadow(color: .black.opacity(0.1), radius: 4, x: 0, y: 0)
}
.overlay {
Circle()
.strokeBorder(
windowState.selectedSpaceId == id ? Color.accentColor : .clear,
lineWidth: 2
)
}
}
.focusEffectDisabled()
.buttonStyle(.plain)
.help(title)
.accessibilityLabel(title)
.accessibilityAddTraits(windowState.selectedSpaceId == id ? .isSelected : [])
}
}
116 changes: 93 additions & 23 deletions Sources/MactrixApp/Views/Sidebar/SidebarView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,44 +7,112 @@ struct SidebarView: View {
@Environment(AppState.self) var appState
@Environment(WindowState.self) var windowState

@State private var searchText: String = ""
@State private var members: [MatrixRustSDK.RoomMember] = []
@State private var membersLoading = false
@State private var membersError: String?

var visibleRooms: [SidebarRoom] {
let joinedRooms = appState.matrixClient?.rooms ?? []
let spaceService = appState.matrixClient?.spaceService

let selectedSpaceRoom = spaceService?.spaceRooms.first {
$0.id == windowState.selectedSpaceId
}

if let selectedSpaceRoom {
switch selectedSpaceRoom.children {
case .error:
return []
case .loading:
Task { await selectedSpaceRoom.loadChildren() }
return []
case .loaded(let children):
return joinedRooms.filter { room in
children.rooms.contains { $0.spaceRoom.id == room.id }
}
}
}

let roomsWithParents = spaceService?.roomsWithJoinedSpaceParents ?? []
return joinedRooms.filter { !roomsWithParents.contains($0.id) }
}

var selectedSpace: SidebarSpaceRoom? {
spaces.first { $0.id == windowState.selectedSpaceId }
}

var childSpaces: [SidebarSpaceRoom] {
guard let selectedSpace, case .loaded(let children) = selectedSpace.children else { return [] }
return children.rooms.filter { $0.spaceRoom.roomType == .space }
}

var favorites: [SidebarRoom] {
(appState.matrixClient?.rooms ?? [])
.filter { $0.roomInfo?.isFavourite == true }
visibleRooms.filter { $0.roomInfo?.isFavourite == true }
}

var directs: [SidebarRoom] {
(appState.matrixClient?.rooms ?? [])
.filter { room in
let isDirect = room.roomInfo?.isDirect == true
let favoriteIDs = Set(favorites.map { $0.id })
return isDirect && !favoriteIDs.contains(room.id)
}
visibleRooms.filter { room in
let isDirect = room.roomInfo?.isDirect == true
let favoriteIDs = Set(favorites.map { $0.id })
return isDirect && !favoriteIDs.contains(room.id)
}
}

var rooms: [SidebarRoom] {
(appState.matrixClient?.rooms ?? [])
.filter { room in
let isSpace = room.room.isSpace()
let isDirect = room.roomInfo?.isDirect == true
let favoriteIDs = Set(favorites.map(\.id))
return !isSpace && !isDirect && !favoriteIDs.contains(room.id)
}
visibleRooms.filter { room in
let isSpace = room.room.isSpace()
let isDirect = room.roomInfo?.isDirect == true
let favoriteIDs = Set(favorites.map(\.id))
return !isSpace && !isDirect && !favoriteIDs.contains(room.id)
}
}

var spaces: [SidebarSpaceRoom] {
appState.matrixClient?.spaceService.spaceRooms ?? []
}

var body: some View {
@Bindable var windowState = windowState
HStack(spacing: 0) {
SidebarGroupsList()
Divider()
listView
// force list view to re-render to avoid a rendering bug
.id(windowState.selectedSpaceId)
}
.safeAreaInset(edge: .bottom, spacing: 0) {
Group {
if #available(macOS 26.0, *) {
statusContent
.glassEffect(
.regular,
in: ConcentricRectangle(corners: .concentric(minimum: .fixed(12)))
)
} else {
statusContent
.background(
.regularMaterial,
in: RoundedRectangle(cornerRadius: 12, style: .continuous)
)
}
}
.padding(8)
}
}

List(selection: $windowState.selectedRoomId) {
private var statusContent: some View {
VStack(spacing: 8) {
SidebarSyncStateView()

SessionVerificationStatusView()
}
.frame(maxWidth: .infinity)
.padding(12)
}

@ViewBuilder
var listView: some View {
@Bindable var windowState = windowState

List(selection: $windowState.selectedRoomId) {
if !favorites.isEmpty {
Section("Favorites", isExpanded: $windowState.sidebarSections.favorites) {
ForEach(favorites) { room in
Expand Down Expand Up @@ -92,13 +160,15 @@ struct SidebarView: View {
}
}

Section("Spaces", isExpanded: $windowState.sidebarSections.spaces) {
ForEach(spaces) { space in
SpaceDisclosureGroup(space: space)
if !childSpaces.isEmpty {
Section("Spaces", isExpanded: $windowState.sidebarSections.spaces) {
ForEach(childSpaces) { space in
SpaceDisclosureGroup(space: space)
}
}
}
}
.navigationSplitViewColumnWidth(min: 150, ideal: 200, max: nil)
.navigationSplitViewColumnWidth(min: 230, ideal: 300, max: nil)
.toolbar {
AppCommands.createRoomButton(windowState: windowState)
}
Expand Down
7 changes: 7 additions & 0 deletions Sources/MatrixIntegration/AsyncSDKListener.swift
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,13 @@ where Element == [MatrixRustSDK.SpaceListUpdate] {
}
}

extension AsyncSDKListener: SpaceServiceSpaceFiltersListener
where Element == [MatrixRustSDK.SpaceFilterUpdate] {
func onUpdate(filterUpdates: [MatrixRustSDK.SpaceFilterUpdate]) {
publishValue(filterUpdates)
}
}

extension AsyncSDKListener: TimelineListener where Element == [MatrixRustSDK.TimelineDiff] {
func onUpdate(diff: [TimelineDiff]) {
publishValue(diff)
Expand Down
76 changes: 70 additions & 6 deletions Sources/MatrixIntegration/LiveSpaceService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,28 +5,92 @@ import OSLog

@MainActor @Observable
public final class LiveSpaceService {
weak private let client: MatrixRustSDK.ClientProtocol?
public let spaceService: SpaceService

public var spaceRooms: [SidebarSpaceRoom] = []

public private(set) var roomsWithJoinedSpaceParents: Set<String> = []

@ObservationIgnored private var spaceHandle: TaskHandle?
@ObservationIgnored private var filtersHandle: TaskHandle?
@ObservationIgnored private var setupTask: Task<Void, Never>?
@ObservationIgnored private var spacesTask: Task<Void, Never>?
@ObservationIgnored private var filtersTask: Task<Void, Never>?
@ObservationIgnored private var refreshTask: Task<Void, Never>?
@ObservationIgnored private var parentsNeedsRefresh = false
@ObservationIgnored private var spaceGraphLoaded = false

public init(spaceService: SpaceService) {
public init(client: MatrixRustSDK.ClientProtocol, spaceService: SpaceService) {
self.client = client
self.spaceService = spaceService

Task {
await self.listenToJoinedSpaces()
setupTask = Task { [weak self] in
await self?.listenToJoinedSpaces()
await self?.listenToSpaceFilters()

// initializes the spaces graph
_ = await spaceService.topLevelJoinedSpaces()
guard !Task.isCancelled, let self else { return }
self.spaceGraphLoaded = true
self.refreshRoomsWithParents()
}
}

deinit {
setupTask?.cancel()
spacesTask?.cancel()
filtersTask?.cancel()
refreshTask?.cancel()
spaceHandle?.cancel()
filtersHandle?.cancel()
}

private func listenToSpaceFilters() async {
let listener = AsyncSDKListener<[SpaceFilterUpdate]>()
filtersHandle = await spaceService.subscribeToSpaceFilters(listener: listener)
filtersTask = Task { [weak self] in
for await _ in listener {
guard let self else { break }
self.refreshRoomsWithParents()
}
}
}

private func refreshRoomsWithParents() {
parentsNeedsRefresh = true
guard spaceGraphLoaded, refreshTask == nil else { return }

let joinedSpaces = await spaceService.topLevelJoinedSpaces()
Logger.liveSpaceService.debug("Joined spaces: \(joinedSpaces)")
let service = spaceService
refreshTask = Task { [weak self] in
while self?.parentsNeedsRefresh == true && !Task.isCancelled {
guard let rooms = self?.client?.rooms() else { return }
self?.parentsNeedsRefresh = false
var children: Set<String> = []
for room in rooms {
guard !Task.isCancelled else { return }
do {
let parents = try await service.joinedParentIdsOfChild(childId: room.id())
if !parents.isEmpty {
children.insert(room.id())
}
} catch {
Logger.liveSpaceService.error("Failed to look up joined space parents: \(error)")
}
}
if let self {
self.roomsWithJoinedSpaceParents = children
self.refreshTask = nil
}
}
}
}

private func listenToJoinedSpaces() async {
let listener = AsyncSDKListener<[SpaceListUpdate]>()
self.spaceHandle = await self.spaceService.subscribeToTopLevelJoinedSpaces(listener: listener)

Task { [weak self] in
spacesTask = Task { [weak self] in
for await roomUpdates in listener {
guard let self else { break }

Expand Down
13 changes: 5 additions & 8 deletions Sources/MatrixIntegration/MatrixClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,9 @@ public class MatrixClient {
.setSessionDelegate(sessionDelegate: self)
.build()

spaceService = LiveSpaceService(spaceService: await client.spaceService())
try await client.restoreSession(session: userSession.session)

spaceService = LiveSpaceService(client: self.client, spaceService: await client.spaceService())
clientDelegateHandle = try? client.setDelegate(delegate: self)
}

Expand All @@ -43,7 +45,7 @@ public class MatrixClient {
self.storePassphrase = storePassphrase
self.client = client

spaceService = LiveSpaceService(spaceService: await client.spaceService())
spaceService = LiveSpaceService(client: self.client, spaceService: await client.spaceService())
clientDelegateHandle = try? self.client.setDelegate(delegate: self)
}

Expand Down Expand Up @@ -105,12 +107,7 @@ public class MatrixClient {
public static func attemptRestore() async throws -> MatrixClient? {
guard let userSession = try UserSession.loadUserFromKeychain() else { return nil }

let matrixClient = try await MatrixClient(userSession: userSession)

// Restore the client using the session.
try await matrixClient.client.restoreSession(session: userSession.session)

return matrixClient
return try await MatrixClient(userSession: userSession)
}

public func reset() async throws {
Expand Down
Loading
Loading