Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
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
12 changes: 12 additions & 0 deletions apple/Inline.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,9 @@
E609060C2FA1000100000001 /* InlineSyntaxHighlighting in Frameworks */ = {isa = PBXBuildFile; productRef = E5A100022FA0000100000001 /* InlineSyntaxHighlighting */; };
E95DD2F1FC76F473B6D31EB6 /* InlineMacWindow in Frameworks */ = {isa = PBXBuildFile; productRef = 7BABF642C785B35DB0C34BF2 /* InlineMacWindow */; };
F17C11012F9A000100000001 /* InlineCLIInstaller in Frameworks */ = {isa = PBXBuildFile; productRef = F17C11002F9A000100000000 /* InlineCLIInstaller */; };
F00200022FA2000100000001 /* InlineGrid in Frameworks */ = {isa = PBXBuildFile; productRef = F00200012FA2000100000001 /* InlineGrid */; };
F00200042FA2000100000001 /* InlineGrid in Frameworks */ = {isa = PBXBuildFile; productRef = F00200032FA2000100000001 /* InlineGrid */; };
F00200062FA2000100000001 /* InlineRTC in Frameworks */ = {isa = PBXBuildFile; productRef = F00200052FA2000100000001 /* InlineRTC */; };
/* End PBXBuildFile section */

/* Begin PBXContainerItemProxy section */
Expand Down Expand Up @@ -281,6 +284,8 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
F00200022FA2000100000001 /* InlineGrid in Frameworks */,
F00200062FA2000100000001 /* InlineRTC in Frameworks */,
E12407240000000100000003 /* InlineIntents in Frameworks */,
CD735FF02E493C8E00F7BFFE /* Translation in Frameworks */,
CD4E8E2C2E088F6B00CE6BC0 /* TextProcessing in Frameworks */,
Expand Down Expand Up @@ -312,6 +317,7 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
F00200042FA2000100000001 /* InlineGrid in Frameworks */,
A83010022F52000100000001 /* InlineAppIntents in Frameworks */,
A9E100042F50000100000001 /* MemojiKit in Frameworks */,
A9E100052F50000100000001 /* MemojiPickerUI in Frameworks */,
Expand Down Expand Up @@ -490,6 +496,8 @@
);
name = InlineIOS;
packageProductDependencies = (
F00200012FA2000100000001 /* InlineGrid */,
F00200052FA2000100000001 /* InlineRTC */,
E12407240000000100000002 /* InlineIntents */,
C35E950D2CAC497700D569AF /* GRDB */,
C3E6D5CA2CAE82B00048D2D8 /* GRDB */,
Expand Down Expand Up @@ -536,6 +544,7 @@
);
name = InlineMac;
packageProductDependencies = (
F00200032FA2000100000001 /* InlineGrid */,
A83010002F52000100000001 /* InlineAppIntents */,
A9E100022F50000100000001 /* MemojiKit */,
A9E100032F50000100000001 /* MemojiPickerUI */,
Expand Down Expand Up @@ -2349,6 +2358,9 @@
isa = XCSwiftPackageProductDependency;
productName = InlineCLIInstaller;
};
F00200012FA2000100000001 /* InlineGrid */ = {isa = XCSwiftPackageProductDependency; productName = InlineGrid; };
F00200032FA2000100000001 /* InlineGrid */ = {isa = XCSwiftPackageProductDependency; productName = InlineGrid; };
F00200052FA2000100000001 /* InlineRTC */ = {isa = XCSwiftPackageProductDependency; productName = InlineRTC; };
/* End XCSwiftPackageProductDependency section */
};
rootObject = CD2660FC2CA01CAE00EB186F /* Project object */;
Expand Down
2 changes: 2 additions & 0 deletions apple/InlineIOS/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,8 @@ private struct AuthedAppRoot: View {
.id(peer)
case let .chatInfo(chatItem):
ChatInfoView(chatItem: chatItem)
case let .grid(initialSpaceID):
GridDestinationView(initialSpaceID: initialSpaceID)
case let .spaceSettings(spaceId):
SpaceSettingsView(spaceId: spaceId)
case let .spaceIntegrations(spaceId):
Expand Down
24 changes: 22 additions & 2 deletions apple/InlineIOS/ExperimentalRootView.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import Auth
import InlineGrid
import InlineKit
import InlineUI
import Logger
Expand Down Expand Up @@ -132,6 +133,7 @@ private struct ExperimentalAuthedRootView: View {
@State private var lastContentRootTab: RootTab = .allChats
@State private var pendingSearchExit: PendingSearchExit?
@State private var isCreatingThread = false
@AppStorage(ExperimentalFeatureFlags.gridIOSKey) private var gridIOSEnabled = false
@State private var isCleaningOpenChats = false
@State private var iPadCommandRequest: IPadCommandRequest?
@State private var homeBootstrapTask: Task<HomeBootstrapOutcome, Never>?
Expand Down Expand Up @@ -456,6 +458,7 @@ private struct ExperimentalAuthedRootView: View {
.toolbar {
experimentalToolbarContent()
}
.gridHomeEntry(onOpen: openGrid)
.safeAreaInset(edge: .bottom, spacing: 0) {
IPadSidebarSwitcher(selection: iPadSidebarScopeBinding)
}
Expand All @@ -476,6 +479,7 @@ private struct ExperimentalAuthedRootView: View {
.toolbar {
experimentalToolbarContent()
}
.gridHomeEntry(isVisible: !isSearchActivePresentation, onOpen: openGrid)
.navigationDestination(for: Destination.self) { destination in
ExperimentalDestinationView(
nav: bindableNav,
Expand Down Expand Up @@ -892,6 +896,12 @@ private struct ExperimentalAuthedRootView: View {
)
}

private func openGrid() {
guard gridIOSEnabled,
GridHomeEntryProjection(store: GridRuntime.shared.rooms, spaceID: nil).isEligible else { return }
openHomeDestination(.grid(initialSpaceID: nav.activeSpaceId))
}

private func openHomeDestination(_ destination: Destination) {
if usesIPadSplitView {
router[IPadNavigationLane.canonicalTab] = [destination]
Expand Down Expand Up @@ -1191,7 +1201,11 @@ private struct ExperimentalAuthedRootView: View {
},
onManage: activeSpace.map { space in
{ openHomeDestination(.spaceSettings(spaceId: space.id)) }
}
},
onGrid: gridIOSEnabled
&& GridHomeEntryProjection(store: GridRuntime.shared.rooms, spaceID: nil).showsMenuEntry
? openGrid
: nil
)
// Fill the iPad toolbar target without changing the phone toolbar layout.
.frame(width: usesIPadSplitView ? 44 : 28, height: usesIPadSplitView ? 44 : 28)
Expand Down Expand Up @@ -1404,6 +1418,7 @@ private struct ExperimentalOverflowMenuButton: UIViewRepresentable {
let onInvite: () -> Void
let onMembers: (() -> Void)?
let onManage: (() -> Void)?
let onGrid: (() -> Void)?

func makeUIView(context: Context) -> UIButton {
let button = UIButton(type: .system)
Expand All @@ -1422,6 +1437,11 @@ private struct ExperimentalOverflowMenuButton: UIViewRepresentable {
}

private func makeMenu() -> UIMenu {
let grid = onGrid.map { onGrid in
UIAction(title: String(localized: "Grid"), image: UIImage(systemName: "square.grid.2x2")) { _ in
onGrid()
}
}
let archivedChats = UIAction(
title: "Archived Chats",
image: UIImage(systemName: "archivebox")
Expand Down Expand Up @@ -1537,7 +1557,7 @@ private struct ExperimentalOverflowMenuButton: UIViewRepresentable {
}

return UIMenu(
children: [viewSection] + (cleanupSection.map { [$0] } ?? []) + [spaceSection]
children: (grid.map { [$0] } ?? []) + [viewSection] + (cleanupSection.map { [$0] } ?? []) + [spaceSection]
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,8 @@ struct ExperimentalDestinationView: View {
}
case let .chatInfo(chatItem):
ChatInfoView(chatItem: chatItem)
case let .grid(initialSpaceID):
GridDestinationView(initialSpaceID: initialSpaceID)
case let .spaceSettings(spaceId):
SpaceSettingsView(spaceId: spaceId, usesRouterNavigation: usesRouterNavigation)
case let .spaceIntegrations(spaceId):
Expand Down
135 changes: 135 additions & 0 deletions apple/InlineIOS/Features/Grid/GridHomeEntry.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
import InlineGrid
import InlineKit
import InlineProtocol
import InlineUI
import SwiftUI

/// Discovery is deliberately separate from call ownership. A bounded avatar
/// preview must never decide which device owns the user's current call.
@MainActor
struct GridHomeEntryProjection {
let isEligible: Bool
let avatars: [GridAvatar]
let activeCount: Int

init(store: GridRoomService, spaceID: Int64?) {
let homes = spaceID.map { selectedID in
store.orderedHomeSpaces.filter { $0.spaceID == selectedID }
} ?? store.orderedHomeSpaces
isEligible = !homes.isEmpty
activeCount = homes.reduce(0) { $0 + Int($1.activeAvatarCount) }
var seen = Set<Int64>()
var preview: [GridAvatar] = []
for home in homes {
for avatar in home.recentAvatars where seen.insert(avatar.user.id).inserted {
preview.append(avatar)
if preview.count == 4 {
break
}
}
if preview.count == 4 {
break
}
}
avatars = preview
}

var showsPill: Bool {
isEligible && activeCount > 0 && !avatars.isEmpty
}

var showsMenuEntry: Bool {
isEligible
}
}

extension View {
func gridHomeEntry(isVisible: Bool = true, onOpen: @escaping () -> Void) -> some View {
modifier(GridHomeEntryModifier(isVisible: isVisible, onOpen: onOpen))
}
}

private struct GridHomeEntryModifier: ViewModifier {
let isVisible: Bool
let onOpen: () -> Void
@AppStorage(ExperimentalFeatureFlags.gridIOSKey) private var isEnabled = false

func body(content: Content) -> some View {
if isEnabled {
content.modifier(GridEnabledHomeEntryModifier(isVisible: isVisible, onOpen: onOpen))
} else {
content
}
}
}

private struct GridEnabledHomeEntryModifier: ViewModifier {
let isVisible: Bool
let onOpen: () -> Void
@Environment(\.scenePhase) private var scenePhase
private let store = GridRuntime.shared.rooms

func body(content: Content) -> some View {
let projection = GridHomeEntryProjection(store: store, spaceID: nil)
let showsPill = isVisible && projection.showsPill

Group {
if #available(iOS 26.0, *) {
content.safeAreaBar(edge: .top, spacing: 0) {
GridHomePill(
avatars: projection.avatars,
activeCount: projection.activeCount,
isVisible: showsPill,
onOpen: onOpen
)
}
} else {
content.safeAreaInset(edge: .top, spacing: 0) {
GridHomePill(
avatars: projection.avatars,
activeCount: projection.activeCount,
isVisible: showsPill,
onOpen: onOpen
)
}
}
}
.task { await store.loadHome() }
.onChange(of: scenePhase) { _, phase in
if phase == .active {
Task { await store.loadHome() }
}
}
}
}

private struct GridHomePill: View {
let avatars: [GridAvatar]
let activeCount: Int
let isVisible: Bool
let onOpen: () -> Void

var body: some View {
if isVisible {
Button(action: onOpen) {
HStack(spacing: -6) {
ForEach(avatars, id: \.user.id) { avatar in
UserAvatar(user: InlineKit.User(from: avatar.user), size: 22)
.overlay { Circle().stroke(.background, lineWidth: 1.5) }
}
}
.padding(.horizontal, 7)
.padding(.vertical, 4)
.background(.thinMaterial, in: Capsule())
.contentShape(Capsule())
.frame(minHeight: 44)
}
.buttonStyle(.plain)
.accessibilityElement(children: .ignore)
.accessibilityLabel("Grid, \(activeCount) people present")
.accessibilityHint("Open voice rooms")
.accessibilityIdentifier("gridHomePill")
.frame(maxWidth: .infinity)
}
}
}
Loading
Loading