Skip to content

Commit 42a3e9d

Browse files
authored
Merge pull request #4307 from anyproto/ios-5521-implement-demo-1-1-spaces
IOS-5521 Implement Demo 1-1 Spaces
2 parents c31eba2 + b5d765b commit 42a3e9d

File tree

29 files changed

+340
-143
lines changed

29 files changed

+340
-143
lines changed

.claude/CODE_REVIEW_GUIDE.md

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,44 @@ LinkWidgetViewContainer(
112112
)
113113
```
114114

115+
### Mistake: Flagging `try?` with Middleware Calls as Silent Error Handling
116+
117+
**Scenario**: A PR uses `try?` when calling middleware/service methods.
118+
119+
**Wrong Analysis**:
120+
- ❌ "Error is silently ignored, should use `try await` for user feedback"
121+
- ❌ "AsyncStandardButton won't show error toast"
122+
123+
**Why This Is Wrong**:
124+
- All middleware errors are **automatically logged** internally
125+
- Using `try?` is intentional when the operation should fail silently from user's perspective
126+
- Not every failure needs user-facing feedback (e.g., creating a space quietly fails)
127+
128+
**Correct Approach**:
129+
1. Middleware calls already have internal error logging
130+
2. `try?` = intentional silent failure (logged but no user toast)
131+
3. `try await` = propagate error for user feedback (toast/alert)
132+
4. Both are valid patterns depending on UX requirements
133+
134+
**Example**:
135+
```swift
136+
// ✅ VALID: Silent failure - error is logged internally, user sees nothing
137+
if let spaceId = try? await workspaceService.createOneToOneSpace(...) {
138+
pageNavigation?.open(.spaceChat(...))
139+
}
140+
141+
// ✅ ALSO VALID: Propagate error - AsyncStandardButton shows toast
142+
func onConnect() async throws {
143+
let spaceId = try await workspaceService.createOneToOneSpace(...)
144+
pageNavigation?.open(.spaceChat(...))
145+
}
146+
```
147+
148+
**When to flag `try?`**:
149+
- Only if there's evidence the error SHOULD be shown to users
150+
- If the surrounding code expects error handling (e.g., catch blocks nearby)
151+
- If it's a user-initiated action that typically needs feedback
152+
115153
## Analysis Checklist
116154

117155
Before suggesting removal of "unused" code:

.claude/skills/ios-dev-guidelines/SKILL.md

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,42 @@ extension Container {
6868
@Injected(\.chatService) private var chatService
6969
```
7070

71+
### Async Button Actions
72+
**Prefer `AsyncStandardButton` over manual loading state management** for cleaner code:
73+
74+
```swift
75+
// ❌ AVOID: Manual loading state
76+
struct MyView: View {
77+
@State private var isLoading = false
78+
79+
var body: some View {
80+
StandardButton(.text("Connect"), inProgress: isLoading, style: .secondaryLarge) {
81+
isLoading = true
82+
Task {
83+
await viewModel.connect()
84+
isLoading = false
85+
}
86+
}
87+
}
88+
}
89+
90+
// ✅ PREFERRED: AsyncStandardButton handles loading state automatically
91+
struct MyView: View {
92+
var body: some View {
93+
AsyncStandardButton(Loc.connect, style: .secondaryLarge) {
94+
await viewModel.connect()
95+
}
96+
}
97+
}
98+
```
99+
100+
**Benefits of `AsyncStandardButton`**:
101+
- Manages `inProgress` state internally
102+
- Shows error toast automatically on failure
103+
- Provides haptic feedback (selection on tap, error on failure)
104+
- Cleaner ViewModel (no `@Published var isLoading` needed)
105+
- Action is `async throws` - just throw errors, they're handled
106+
71107
## 🗂️ Project Structure
72108

73109
```

Anytype.xcodeproj/project.pbxproj

Lines changed: 12 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -209,14 +209,14 @@
209209
/* End PBXFileReference section */
210210

211211
/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */
212-
2E30581A2D81DF7F00BF4F25 /* Exceptions for "AnytypeNotificationServiceExtension" folder in "AnytypeNotificationServiceExtension" target */ = {
212+
2E30581A2D81DF7F00BF4F25 /* PBXFileSystemSynchronizedBuildFileExceptionSet */ = {
213213
isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
214214
membershipExceptions = (
215215
Info.plist,
216216
);
217217
target = 2E30580C2D81DF7E00BF4F25 /* AnytypeNotificationServiceExtension */;
218218
};
219-
3D0DC1052DFC9BC000E555FD /* Exceptions for "AnytypeTests" folder in "AnytypeTests" target */ = {
219+
3D0DC1052DFC9BC000E555FD /* PBXFileSystemSynchronizedBuildFileExceptionSet */ = {
220220
isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
221221
membershipExceptions = (
222222
AnytypeTests.xctestplan,
@@ -229,7 +229,7 @@
229229
);
230230
target = 03C5C53F22DFD90C00DD91DE /* AnytypeTests */;
231231
};
232-
3D69777F2CAE9EC5003F5A91 /* Exceptions for "Supporting files" folder in "Anytype" target */ = {
232+
3D69777F2CAE9EC5003F5A91 /* PBXFileSystemSynchronizedBuildFileExceptionSet */ = {
233233
isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
234234
membershipExceptions = (
235235
/Localized/LaunchScreen.storyboard,
@@ -238,7 +238,7 @@
238238
);
239239
target = 0303ECFA22D8EDAA005C552B /* Anytype */;
240240
};
241-
3D6989E52CAEA5E1003F5A91 /* Exceptions for "AnytypeShareExtension" folder in "AnytypeShareExtension" target */ = {
241+
3D6989E52CAEA5E1003F5A91 /* PBXFileSystemSynchronizedBuildFileExceptionSet */ = {
242242
isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
243243
membershipExceptions = (
244244
/Localized/MainInterface.storyboard,
@@ -249,66 +249,14 @@
249249
/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */
250250

251251
/* Begin PBXFileSystemSynchronizedRootGroup section */
252-
2E30580E2D81DF7E00BF4F25 /* AnytypeNotificationServiceExtension */ = {
253-
isa = PBXFileSystemSynchronizedRootGroup;
254-
exceptions = (
255-
2E30581A2D81DF7F00BF4F25 /* Exceptions for "AnytypeNotificationServiceExtension" folder in "AnytypeNotificationServiceExtension" target */,
256-
);
257-
path = AnytypeNotificationServiceExtension;
258-
sourceTree = "<group>";
259-
};
260-
3D0DC0E12DFC9BC000E555FD /* AnytypeTests */ = {
261-
isa = PBXFileSystemSynchronizedRootGroup;
262-
exceptions = (
263-
3D0DC1052DFC9BC000E555FD /* Exceptions for "AnytypeTests" folder in "AnytypeTests" target */,
264-
);
265-
path = AnytypeTests;
266-
sourceTree = "<group>";
267-
};
268-
3D69777D2CAE9EC5003F5A91 /* Supporting files */ = {
269-
isa = PBXFileSystemSynchronizedRootGroup;
270-
exceptions = (
271-
3D69777F2CAE9EC5003F5A91 /* Exceptions for "Supporting files" folder in "Anytype" target */,
272-
);
273-
path = "Supporting files";
274-
sourceTree = "<group>";
275-
};
276-
3D6977812CAE9ED0003F5A91 /* Video */ = {
277-
isa = PBXFileSystemSynchronizedRootGroup;
278-
exceptions = (
279-
);
280-
path = Video;
281-
sourceTree = "<group>";
282-
};
283-
3D6977912CAE9EEC003F5A91 /* Preview Content */ = {
284-
isa = PBXFileSystemSynchronizedRootGroup;
285-
exceptions = (
286-
);
287-
path = "Preview Content";
288-
sourceTree = "<group>";
289-
};
290-
3D6989E22CAEA5E1003F5A91 /* AnytypeShareExtension */ = {
291-
isa = PBXFileSystemSynchronizedRootGroup;
292-
exceptions = (
293-
3D6989E52CAEA5E1003F5A91 /* Exceptions for "AnytypeShareExtension" folder in "AnytypeShareExtension" target */,
294-
);
295-
path = AnytypeShareExtension;
296-
sourceTree = "<group>";
297-
};
298-
3D69B4E82CAEA9CB003F5A91 /* Sources */ = {
299-
isa = PBXFileSystemSynchronizedRootGroup;
300-
exceptions = (
301-
);
302-
path = Sources;
303-
sourceTree = "<group>";
304-
};
305-
7424EFB92D79C71B00E51377 /* Modules */ = {
306-
isa = PBXFileSystemSynchronizedRootGroup;
307-
exceptions = (
308-
);
309-
path = Modules;
310-
sourceTree = "<group>";
311-
};
252+
2E30580E2D81DF7E00BF4F25 /* AnytypeNotificationServiceExtension */ = {isa = PBXFileSystemSynchronizedRootGroup; exceptions = (2E30581A2D81DF7F00BF4F25 /* PBXFileSystemSynchronizedBuildFileExceptionSet */, ); explicitFileTypes = {}; explicitFolders = (); path = AnytypeNotificationServiceExtension; sourceTree = "<group>"; };
253+
3D0DC0E12DFC9BC000E555FD /* AnytypeTests */ = {isa = PBXFileSystemSynchronizedRootGroup; exceptions = (3D0DC1052DFC9BC000E555FD /* PBXFileSystemSynchronizedBuildFileExceptionSet */, ); explicitFileTypes = {}; explicitFolders = (); path = AnytypeTests; sourceTree = "<group>"; };
254+
3D69777D2CAE9EC5003F5A91 /* Supporting files */ = {isa = PBXFileSystemSynchronizedRootGroup; exceptions = (3D69777F2CAE9EC5003F5A91 /* PBXFileSystemSynchronizedBuildFileExceptionSet */, ); explicitFileTypes = {}; explicitFolders = (); path = "Supporting files"; sourceTree = "<group>"; };
255+
3D6977812CAE9ED0003F5A91 /* Video */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = Video; sourceTree = "<group>"; };
256+
3D6977912CAE9EEC003F5A91 /* Preview Content */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = "Preview Content"; sourceTree = "<group>"; };
257+
3D6989E22CAEA5E1003F5A91 /* AnytypeShareExtension */ = {isa = PBXFileSystemSynchronizedRootGroup; exceptions = (3D6989E52CAEA5E1003F5A91 /* PBXFileSystemSynchronizedBuildFileExceptionSet */, ); explicitFileTypes = {}; explicitFolders = (); path = AnytypeShareExtension; sourceTree = "<group>"; };
258+
3D69B4E82CAEA9CB003F5A91 /* Sources */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = Sources; sourceTree = "<group>"; };
259+
7424EFB92D79C71B00E51377 /* Modules */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = Modules; sourceTree = "<group>"; };
312260
/* End PBXFileSystemSynchronizedRootGroup section */
313261

314262
/* Begin PBXFrameworksBuildPhase section */
@@ -628,8 +576,6 @@
628576
);
629577
dependencies = (
630578
);
631-
fileSystemSynchronizedGroups = (
632-
);
633579
name = AnytypeShareExtension;
634580
packageProductDependencies = (
635581
2ACDC9B32B553F840063BFBD /* SharedContentManager */,
@@ -654,8 +600,6 @@
654600
);
655601
dependencies = (
656602
);
657-
fileSystemSynchronizedGroups = (
658-
);
659603
name = AnytypeWidgetExtension;
660604
packageProductDependencies = (
661605
2A41A68C2B84EBBC00EAE6E6 /* DeepLinks */,

Anytype/Sources/Analytics/Converters/SpaceUXType+Analytics.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ extension SpaceUxType {
99
return "Space"
1010
case .stream:
1111
return "Stream"
12+
case .oneToOne:
13+
return "OneToOne"
1214
case .UNRECOGNIZED:
1315
return "UNRECOGNIZED"
1416
case .none:

Anytype/Sources/PresentationLayer/Modules/Chat/ChatView.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ struct ChatView: View {
100100
disableAddButton: model.sendMessageTaskInProgress,
101101
sendButtonIsLoading: model.sendButtonIsLoading,
102102
createObjectTypes: model.typesForCreateObject,
103-
conversationType: model.conversationType,
103+
spaceUxType: model.spaceUxType,
104104
onTapAddObject: {
105105
model.onTapAddObjectToMessage()
106106
},
@@ -158,7 +158,7 @@ struct ChatView: View {
158158

159159
private var emptyView: some View {
160160
ConversationEmptyStateView(
161-
conversationType: model.conversationType,
161+
spaceUxType: model.spaceUxType,
162162
participantPermissions: model.participantPermissions,
163163
addMembersAction: {
164164
model.onTapInviteLink()

Anytype/Sources/PresentationLayer/Modules/Chat/ChatViewModel.swift

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -126,9 +126,7 @@ final class ChatViewModel: MessageModuleOutput, ChatActionProviderHandler {
126126
@ObservationIgnored
127127
var showEmptyState: Bool { mesageBlocks.isEmpty && dataLoaded }
128128
@ObservationIgnored
129-
var conversationType: ConversationType {
130-
participantSpaceView?.spaceView.uxType.asConversationType ?? .chat
131-
}
129+
var spaceUxType: SpaceUxType { participantSpaceView?.spaceView.uxType ?? .data }
132130
@ObservationIgnored
133131
var participantPermissions: ParticipantPermissions? { participantSpaceView?.participant?.permission }
134132

Anytype/Sources/PresentationLayer/Modules/Chat/Models/ChatType.swift

Lines changed: 0 additions & 23 deletions
This file was deleted.

Anytype/Sources/PresentationLayer/Modules/Chat/Subviews/ConversationEmptyStateView/ConversationEmptyStateView.swift

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,16 @@ import Services
44

55
struct ConversationEmptyStateView: View {
66

7-
let conversationType: ConversationType
7+
let spaceUxType: SpaceUxType
88
let participantPermissions: ParticipantPermissions?
99
let addMembersAction: (() -> Void)?
1010
let qrCodeAction: (() -> Void)?
11-
11+
1212
var body: some View {
13-
switch conversationType {
14-
case .chat:
15-
chatEmptyStateView
16-
case .stream:
13+
if spaceUxType.isStream {
1714
streamEmptyStateView
15+
} else {
16+
chatEmptyStateView
1817
}
1918
}
2019

Anytype/Sources/PresentationLayer/Modules/Chat/Subviews/InputPanel/Input/ChatInput.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ struct ChatInput: View {
1111
let disableAddButton: Bool
1212
let sendButtonIsLoading: Bool
1313
let createObjectTypes: [ObjectType]
14-
let conversationType: ConversationType
14+
let spaceUxType: SpaceUxType
1515
let onTapAddObject: () -> Void
1616
let onTapAddMedia: () -> Void
1717
let onTapAddFiles: () -> Void
@@ -84,7 +84,7 @@ struct ChatInput: View {
8484
private var input: some View {
8585
ZStack(alignment: .topLeading) {
8686
if text.string.isEmpty {
87-
Text(conversationType.isChat ? Loc.Message.Input.Chat.emptyPlaceholder : Loc.Message.Input.Stream.emptyPlaceholder)
87+
Text(spaceUxType.isStream ? Loc.Message.Input.Stream.emptyPlaceholder : Loc.Message.Input.Chat.emptyPlaceholder)
8888
.anytypeStyle(.chatText)
8989
.foregroundColor(.Text.tertiary)
9090
.padding(.top, 18)

Anytype/Sources/PresentationLayer/Modules/Profile/ProfileView.swift

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
11
import SwiftUI
22
import Services
3+
import AnytypeCore
34

45

56
struct ProfileView: View {
67
@StateObject private var model: ProfileViewModel
7-
8+
@Environment(\.pageNavigation) private var pageNavigation
9+
810
init(info: ObjectInfo) {
911
_model = StateObject(wrappedValue: ProfileViewModel(info: info))
1012
}
11-
13+
1214
var body: some View {
1315
Group {
1416
if let details = model.details {
@@ -17,9 +19,12 @@ struct ProfileView: View {
1719
emptyView
1820
}
1921
}
20-
21-
22-
.task { await model.setupSubscriptions() }
22+
.onAppear {
23+
model.pageNavigation = pageNavigation
24+
}
25+
.task {
26+
await model.setupSubscriptions()
27+
}
2328
.sheet(isPresented: $model.showSettings) {
2429
SettingsCoordinatorView()
2530
}
@@ -73,10 +78,11 @@ struct ProfileView: View {
7378
AnytypeText(details.displayName, style: .caption1Regular).foregroundColor(.Text.secondary).lineLimit(1)
7479
Spacer.fixedHeight(4)
7580
AnytypeText(details.description, style: .previewTitle2Regular)
76-
Spacer.fixedHeight(78)
81+
connectButton
82+
Spacer.fixedHeight(32)
7783
}
7884
}
79-
85+
8086
private func viewWithoutDescription(_ details: ObjectDetails) -> some View {
8187
Group {
8288
Spacer.fixedHeight(30)
@@ -85,7 +91,20 @@ struct ProfileView: View {
8591
AnytypeText(details.name, style: .heading).lineLimit(1)
8692
Spacer.fixedHeight(4)
8793
AnytypeText(details.displayName, style: .caption1Regular).foregroundColor(.Text.secondary).lineLimit(1)
88-
Spacer.fixedHeight(84)
94+
connectButton
95+
Spacer.fixedHeight(32)
96+
}
97+
}
98+
99+
@ViewBuilder
100+
private var connectButton: some View {
101+
if FeatureFlags.demoOneToOneSpaces, !model.isOwner {
102+
Spacer.fixedHeight(24)
103+
AsyncStandardButton(Loc.connect, style: .secondaryLarge) {
104+
await model.onConnect()
105+
}
106+
} else {
107+
Spacer.fixedHeight(52)
89108
}
90109
}
91110
}

0 commit comments

Comments
 (0)