Skip to content

Commit d97ac69

Browse files
yewreekaclaude
andcommitted
Show pending invites in home view (#366)
## Show Pending Invites in Home View Display conversations in the "invite accepted, waiting to join" state in the home view conversations list. ### Changes **Core (ConvosCore):** - `Conversation.isPendingInvite` computed property (`isDraft && !hasJoined`) - `ConversationsRepository` query updated to include drafts with an `inviteTag` - Unit tests and mock helpers **Home View (Conversations List):** - Pending invites show with `4m · Verifying` subtitle (relative timestamp + "Verifying") - Swipe actions restricted: delete only (no explode/read/mute) - Context menu restricted: delete only (no pin/read/mute/explode) - `.pendingInvites` filter added alongside `.unread` / `.exploding` **Conversation Detail:** - "Verifying" card with QR code icon in lava color, tappable → opens https://learn.convos.org/verifying - Description: "See and send messages after your access is verified" - Bottom bar dimmed and disabled for pending invites - Share/add-to-conversation disabled - Conversation info tap/long-press blocked - Detects transition from pending → approved state - Always allow interactive dismiss (removed `interactiveDismissDisabled`) **Conversation Creation:** - Removed confirmation dialog from close button (pending invites now visible on home) **Docs & QA:** - PRD at `docs/plans/show-pending-invites-home-view.md` - QA test 23 covering full flow Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 9fbfdac commit d97ac69

22 files changed

Lines changed: 730 additions & 100 deletions

Convos/Conversation Creation/NewConversationView.swift

Lines changed: 2 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ struct NewConversationView: View {
55
let viewModel: NewConversationViewModel
66
@Bindable var quicknameViewModel: QuicknameSettingsViewModel
77
@State private var hasShownScannerOnAppear: Bool = false
8-
@State private var presentingJoiningStateInfo: Bool = false
98
@State private var sidebarWidth: CGFloat = 0.0
109
@State private var focusCoordinator: FocusCoordinator = FocusCoordinator(horizontalSizeClass: nil)
1110

@@ -51,21 +50,9 @@ struct NewConversationView: View {
5150
.toolbar {
5251
if !viewModel.showingFullScreenScanner {
5352
ToolbarItem(placement: .topBarLeading) {
54-
let closeAction = {
55-
if viewModel.conversationViewModel?.onboardingCoordinator.isWaitingForInviteAcceptance == true {
56-
presentingJoiningStateInfo = true
57-
} else {
58-
dismiss()
59-
}
53+
Button(role: .close) {
54+
dismiss()
6055
}
61-
Button(role: .close, action: closeAction)
62-
.confirmationDialog("This convo will appear on your home screen after someone approves you",
63-
isPresented: $presentingJoiningStateInfo,
64-
titleVisibility: .visible) {
65-
Button("Continue") {
66-
dismiss()
67-
}
68-
}
6956
}
7057
}
7158
}

Convos/Conversation Detail/AddToConversationMenu.swift

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,20 @@ struct AddToConversationMenu: View {
99

1010
private var isAssistantEnabled: Bool { FeatureFlags.shared.isAssistantEnabled }
1111

12+
private var labelColor: Color {
13+
if !isEnabled {
14+
return .colorTextSecondary.opacity(0.4)
15+
}
16+
return isFull ? .colorTextSecondary : .colorTextPrimary
17+
}
18+
1219
var body: some View {
1320
if isAssistantEnabled {
1421
menuView
1522
} else {
1623
Button(action: onConvoCode) {
1724
Image(systemName: "plus")
18-
.foregroundStyle(isFull ? .colorTextSecondary : .colorTextPrimary)
25+
.foregroundStyle(labelColor)
1926
}
2027
.disabled(!isEnabled)
2128
.accessibilityLabel("Add to conversation")
@@ -63,7 +70,7 @@ struct AddToConversationMenu: View {
6370
}
6471
} label: {
6572
Image(systemName: "plus")
66-
.foregroundStyle(isFull ? .colorTextSecondary : .colorTextPrimary)
73+
.foregroundStyle(labelColor)
6774
}
6875
.disabled(!isEnabled)
6976
.accessibilityLabel("Add to conversation")

Convos/Conversation Detail/Conversation Detail Drawer/InviteAcceptedView.swift

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,20 +3,24 @@ import SwiftUI
33
struct InviteAcceptedView: View {
44
@State private var showingDescription: Bool = false
55

6+
@Environment(\.openURL) private var openURL: OpenURLAction
7+
68
var body: some View {
7-
Group {
9+
Button {
10+
openURL(Constant.learnMoreURL)
11+
} label: {
812
VStack(spacing: DesignConstants.Spacing.step2x) {
913
HStack {
10-
Image(systemName: "checkmark.circle.fill")
14+
Image(systemName: "qrcode")
1115
.font(.footnote)
12-
.foregroundStyle(.colorGreen)
13-
Text("Invite accepted")
16+
.foregroundStyle(.colorLava)
17+
Text("Verifying")
1418
.foregroundStyle(.colorTextPrimary)
1519
}
1620
.font(.body)
1721

1822
if showingDescription {
19-
Text("See and send messages after someone approves you.")
23+
Text("See and send messages after your access is verified")
2024
.font(.caption)
2125
.foregroundStyle(.colorTextSecondary)
2226
.multilineTextAlignment(.center)
@@ -25,12 +29,14 @@ struct InviteAcceptedView: View {
2529
.transition(.blurReplace)
2630
.animation(.spring(duration: 0.4, bounce: 0.2), value: showingDescription)
2731
.padding(DesignConstants.Spacing.step6x)
32+
.frame(maxWidth: .infinity)
33+
.background(.colorFillMinimal)
34+
.clipShape(RoundedRectangle(cornerRadius: DesignConstants.CornerRadius.mediumLarge))
2835
}
29-
.frame(maxWidth: .infinity)
30-
.background(.colorFillMinimal)
31-
.clipShape(RoundedRectangle(cornerRadius: DesignConstants.CornerRadius.mediumLarge))
36+
.buttonStyle(.plain)
3237
.accessibilityElement(children: .combine)
33-
.accessibilityLabel("Invite accepted. See and send messages after someone approves you.")
38+
.accessibilityIdentifier("invite-accepted-view")
39+
.accessibilityLabel("Verifying. See and send messages after your access is verified.")
3440
.onAppear {
3541
DispatchQueue.main
3642
.asyncAfter(deadline: .now() + ConversationOnboardingState.waitingForInviteAcceptanceDelay) {
@@ -40,6 +46,11 @@ struct InviteAcceptedView: View {
4046
}
4147
}
4248
}
49+
50+
private enum Constant {
51+
// swiftlint:disable:next force_unwrapping
52+
static let learnMoreURL: URL = URL(string: "https://learn.convos.org/verifying")!
53+
}
4354
}
4455

4556
#Preview {

Convos/Conversation Detail/ConversationView.swift

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -178,7 +178,6 @@ struct ConversationView<MessagesBottomBar: View>: View {
178178
quicknameViewModel: quicknameViewModel
179179
)
180180
.background(.colorBackgroundSurfaceless)
181-
.interactiveDismissDisabled(viewModel.conversationViewModel?.onboardingCoordinator.isWaitingForInviteAcceptance == true)
182181
}
183182
.sheet(item: $viewModel.presentingProfileForMember) { member in
184183
NavigationStack {

Convos/Conversation Detail/ConversationViewModel.swift

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,9 @@ class ConversationViewModel {
6767
applyPendingDraftEdits()
6868
_editingIncludeInfoInPublicPreview = nil
6969
}
70+
if oldValue.isPendingInvite, !conversation.isPendingInvite {
71+
inviteWasAccepted()
72+
}
7073
if !isEditingConversationName { editingConversationName = conversation.name ?? "" }
7174
if !isEditingDescription { editingDescription = conversation.description ?? "" }
7275
}
@@ -104,6 +107,9 @@ class ConversationViewModel {
104107
if let expiresAt = scheduledExplosionDate {
105108
return ExplosionDurationFormatter.countdown(until: expiresAt)
106109
}
110+
if isWaitingForInviteAcceptance {
111+
return conversation.membersCountString
112+
}
107113
return conversation.shouldShowQuickEdit ? "Customize" : conversation.membersCountString
108114
}
109115
var conversationNamePlaceholder: String = "Convo name"
@@ -384,6 +390,9 @@ class ConversationViewModel {
384390
observe()
385391
loadPhotoPreferences()
386392

393+
if conversation.isPendingInvite {
394+
onboardingCoordinator.isWaitingForInviteAcceptance = true
395+
}
387396
startOnboarding()
388397
}
389398

@@ -538,6 +547,7 @@ class ConversationViewModel {
538547
}
539548

540549
func onConversationInfoTap(focusCoordinator: FocusCoordinator) {
550+
guard !isWaitingForInviteAcceptance else { return }
541551
if conversation.shouldShowQuickEdit {
542552
focusCoordinator.moveFocus(to: .conversationName)
543553
} else {
@@ -546,6 +556,7 @@ class ConversationViewModel {
546556
}
547557

548558
func onConversationInfoLongPress(focusCoordinator: FocusCoordinator) {
559+
guard !isWaitingForInviteAcceptance else { return }
549560
UIImpactFeedbackGenerator(style: .medium).impactOccurred()
550561
focusCoordinator.moveFocus(to: .conversationName)
551562
}

Convos/Conversation Detail/Messages/Messages View Controller/View Controller/Views/MessagesBottomBar.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@ struct MessagesBottomBar<BottomBarContent: View>: View {
9999
private var collapsedInputView: some View {
100100
HStack(alignment: .bottom, spacing: DesignConstants.Spacing.step2x) {
101101
MessagesMediaInputView(isPhotoPickerPresented: $isPhotoPickerPresented)
102+
.opacity(messagesTextFieldEnabled ? 1.0 : 0.4)
102103
.frame(width: DesignConstants.Spacing.step12x, height: DesignConstants.Spacing.step12x)
103104
.clipShape(.circle)
104105
.glassEffect(.regular.interactive(), in: .circle)
@@ -118,12 +119,14 @@ struct MessagesBottomBar<BottomBarContent: View>: View {
118119
onProfilePhotoTap: onProfilePhotoTap,
119120
onSendMessage: onSendMessage
120121
)
122+
.opacity(messagesTextFieldEnabled ? 1.0 : 0.4)
121123
.fixedSize(horizontal: false, vertical: true)
122124
.clipShape(.rect(cornerRadius: 26.0))
123125
.glassEffect(.regular.interactive(), in: .rect(cornerRadius: 26.0))
124126
.glassEffectID("input", in: namespace)
125127
.glassEffectTransition(.matchedGeometry)
126128
}
129+
.disabled(!messagesTextFieldEnabled)
127130
}
128131

129132
@ViewBuilder

Convos/Conversations List/ConversationContextMenuContent.swift

Lines changed: 27 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -9,34 +9,38 @@ func conversationContextMenuContent(
99
onExplode: @escaping () -> Void,
1010
onDelete: @escaping () -> Void
1111
) -> some View {
12-
ControlGroup {
13-
let togglePinAction = { viewModel.togglePin(conversation: conversation) }
14-
Button(action: togglePinAction) {
15-
Label(
16-
conversation.isPinned ? "Unfav" : "Fav",
17-
systemImage: conversation.isPinned ? "star.slash.fill" : "star.fill"
18-
)
19-
}
12+
let isPending = conversation.isPendingInvite
2013

21-
let toggleReadAction = { viewModel.toggleReadState(conversation: conversation) }
22-
Button(action: toggleReadAction) {
23-
Label(
24-
conversation.isUnread ? "Read" : "Unread",
25-
systemImage: conversation.isUnread ? "checkmark.message.fill" : "message.badge.fill"
26-
)
27-
}
14+
if !isPending {
15+
ControlGroup {
16+
let togglePinAction = { viewModel.togglePin(conversation: conversation) }
17+
Button(action: togglePinAction) {
18+
Label(
19+
conversation.isPinned ? "Unfav" : "Fav",
20+
systemImage: conversation.isPinned ? "star.slash.fill" : "star.fill"
21+
)
22+
}
23+
24+
let toggleReadAction = { viewModel.toggleReadState(conversation: conversation) }
25+
Button(action: toggleReadAction) {
26+
Label(
27+
conversation.isUnread ? "Read" : "Unread",
28+
systemImage: conversation.isUnread ? "checkmark.message.fill" : "message.badge.fill"
29+
)
30+
}
2831

29-
let toggleMuteAction = { viewModel.toggleMute(conversation: conversation) }
30-
Button(action: toggleMuteAction) {
31-
Label(
32-
conversation.isMuted ? "Unmute" : "Mute",
33-
systemImage: conversation.isMuted ? "bell.fill" : "bell.slash.fill"
34-
)
32+
let toggleMuteAction = { viewModel.toggleMute(conversation: conversation) }
33+
Button(action: toggleMuteAction) {
34+
Label(
35+
conversation.isMuted ? "Unmute" : "Mute",
36+
systemImage: conversation.isMuted ? "bell.fill" : "bell.slash.fill"
37+
)
38+
}
3539
}
40+
.accessibilityIdentifier("context-menu-pin")
3641
}
37-
.accessibilityIdentifier("context-menu-pin")
3842

39-
if conversation.creator.isCurrentUser {
43+
if !isPending && conversation.creator.isCurrentUser {
4044
Button(action: onExplode) {
4145
Text("Explode")
4246
Text("For everyone")

Convos/Conversations List/ConversationsListItem.swift

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -72,8 +72,11 @@ struct ConversationsListItem: View {
7272
private var lastMessage: MessagePreview? { conversation.lastMessage }
7373
private var createdAt: Date { conversation.createdAt }
7474

75+
private var isPendingInvite: Bool { conversation.isPendingInvite }
76+
7577
private var accessibilityDescription: String {
7678
var parts: [String] = [title]
79+
if isPendingInvite { parts.append("verifying") }
7780
if isUnread { parts.append("unread") }
7881
if isMuted { parts.append("muted") }
7982
if let message = lastMessage {
@@ -85,14 +88,18 @@ struct ConversationsListItem: View {
8588
var body: some View {
8689
ListItemView(
8790
title: title,
88-
isMuted: isMuted,
89-
isUnread: isUnread,
91+
isMuted: isPendingInvite ? false : isMuted,
92+
isUnread: isPendingInvite ? false : isUnread,
9093
leadingContent: {
9194
ConversationAvatarView(conversation: conversation, conversationImage: nil)
9295
},
9396
subtitle: {
9497
HStack(spacing: DesignConstants.Spacing.stepX) {
95-
if let message = lastMessage {
98+
if isPendingInvite {
99+
RelativeDateLabel(date: createdAt)
100+
Text("·").foregroundStyle(.colorTextTertiary)
101+
Text("Verifying")
102+
} else if let message = lastMessage {
96103
RelativeDateLabel(date: message.createdAt)
97104
Text("·").foregroundStyle(.colorTextTertiary)
98105
Text(message.text)
@@ -113,6 +120,10 @@ struct ConversationsListItem: View {
113120
}
114121
}
115122

116-
#Preview {
123+
#Preview("Regular") {
117124
ConversationsListItem(conversation: .mock())
118125
}
126+
127+
#Preview("Pending Invite") {
128+
ConversationsListItem(conversation: .mockPendingInvite())
129+
}

0 commit comments

Comments
 (0)