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
12 changes: 0 additions & 12 deletions Convos/Assets.xcassets/convosIcon.imageset/Contents.json

This file was deleted.

3 changes: 0 additions & 3 deletions Convos/Assets.xcassets/convosIcon.imageset/Frame 3556.svg

This file was deleted.

16 changes: 0 additions & 16 deletions Convos/Assets.xcassets/convosIconLarge.imageset/Contents.json

This file was deleted.

This file was deleted.

12 changes: 0 additions & 12 deletions Convos/Assets.xcassets/convosIconRounded.imageset/Contents.json

This file was deleted.

This file was deleted.

4 changes: 2 additions & 2 deletions Convos/Conversation Detail/ConversationShareView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ struct ConversationShareOverlay: View {

@State private var showCard: Bool = false
@State private var isShareSheetPresented: Bool = false
@State private var conversationImage: Image = Image("convosIcon")
@State private var conversationImage: Image = Image("convosOrangeIcon")
@State private var qrCodeImage: UIImage?
@Environment(\.displayScale) private var displayScale: CGFloat

Expand Down Expand Up @@ -109,7 +109,7 @@ struct ConversationShareOverlay: View {
Text("Convos code")
.kerning(1.0)

Image("convosIcon")
Image("convosOrangeIcon")
.renderingMode(.template)
.resizable()
.frame(width: 14.0, height: 14.0)
Expand Down
5 changes: 5 additions & 0 deletions Convos/Conversation Detail/ConversationView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ struct ConversationView<MessagesBottomBar: View>: View {
displayName: $viewModel.myProfileViewModel.editingDisplayName,
messageText: $viewModel.messageText,
selectedAttachmentImage: $viewModel.selectedAttachmentImage,
pendingInviteCode: viewModel.pendingInvite?.code,
sendButtonEnabled: viewModel.sendButtonEnabled,
profileImage: $viewModel.myProfileViewModel.profileImage,
onboardingCoordinator: onboardingCoordinator,
Expand All @@ -49,6 +50,7 @@ struct ConversationView<MessagesBottomBar: View>: View {
onSendMessage: {
viewModel.onSendMessage(focusCoordinator: focusCoordinator)
},
onClearInvite: viewModel.clearPendingInvite,
onTapAvatar: viewModel.onTapAvatar(_:),
onTapInvite: viewModel.onTapInvite(_:),
onReaction: viewModel.onReaction(emoji:messageId:),
Expand Down Expand Up @@ -98,6 +100,9 @@ struct ConversationView<MessagesBottomBar: View>: View {
viewModel.onPhotoRemoved()
}
}
.onChange(of: viewModel.messageText) { _, _ in
viewModel.checkForInviteURL()
}
.animation(.easeOut, value: viewModel.explodeState)
.dynamicTypeSize(...DynamicTypeSize.xxxLarge)
.selfSizingSheet(isPresented: $viewModel.presentingConversationForked) {
Expand Down
41 changes: 39 additions & 2 deletions Convos/Conversation Detail/ConversationViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@ import Observation
import UIKit
import UserNotifications

struct PendingInvite {
let code: String
let fullURL: String
let range: Range<String.Index>
}

@MainActor
@Observable
class ConversationViewModel {
Expand Down Expand Up @@ -206,8 +212,10 @@ class ConversationViewModel {
var canToggleLock: Bool {
isCurrentUserSuperAdmin
}
var pendingInvite: PendingInvite?

var sendButtonEnabled: Bool {
!messageText.isEmpty || selectedAttachmentImage != nil
!messageText.isEmpty || selectedAttachmentImage != nil || pendingInvite != nil
}
private(set) var isSendingPhoto: Bool = false
var explodeState: ExplodeState = .ready
Expand Down Expand Up @@ -654,20 +662,23 @@ extension ConversationViewModel {
func onSendMessage(focusCoordinator: FocusCoordinator) {
let hasText = !messageText.isEmpty
let hasAttachment = selectedAttachmentImage != nil
let hasInvite = pendingInvite != nil

guard hasText || hasAttachment else { return }
guard hasText || hasAttachment || hasInvite else { return }

onboardingCoordinator.skipAddQuickname()

let prevMessageText = messageText
let replyTarget = replyingToMessage
let prevAttachmentImage = selectedAttachmentImage
let eagerUploadKey = currentEagerUploadKey
let prevInviteURL = pendingInvite?.fullURL

messageText = ""
replyingToMessage = nil
selectedAttachmentImage = nil
currentEagerUploadKey = nil
pendingInvite = nil
focusCoordinator.endEditing(for: .message, context: .conversation)

let messageWriter = cachedMessageWriter
Expand Down Expand Up @@ -702,6 +713,15 @@ extension ConversationViewModel {
photoTrackingKey = nil
}

if let inviteURL = prevInviteURL {
let inviteIsReply = replyTarget != nil && !hasAttachment && !hasText
if inviteIsReply, let replyTarget {
try await messageWriter.sendReply(text: inviteURL, afterPhoto: photoTrackingKey, toMessageWithClientId: replyTarget.base.id)
} else {
try await messageWriter.send(text: inviteURL, afterPhoto: photoTrackingKey)
}
}
Comment thread
macroscopeapp[bot] marked this conversation as resolved.

if hasText {
let textIsReply = replyTarget != nil && !hasAttachment
if textIsReply, let replyTarget {
Expand Down Expand Up @@ -1190,3 +1210,20 @@ extension UNUserNotificationCenter {
try? await add(request)
}
}

// MARK: - Invite URL Detection

extension ConversationViewModel {
func checkForInviteURL() {
guard pendingInvite == nil else { return }

if let result = InviteURLDetector.detectInviteURL(in: messageText) {
pendingInvite = PendingInvite(code: result.code, fullURL: result.fullURL, range: result.range)
messageText = InviteURLDetector.removeInviteURL(from: messageText, range: result.range)
}
}

func clearPendingInvite() {
pendingInvite = nil
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ struct MessageInviteView: View {
if let name = invite.conversationName, !name.isEmpty {
return "Pop into my convo \"\(name)\""
}
return "Pop into my convo before it explodes"
return "Pop into my convo"
}

var description: String {
Expand All @@ -88,7 +88,7 @@ struct MessageInviteView: View {
.resizable()
.aspectRatio(contentMode: .fill)
} else {
Image("convosIconLarge")
Image("convosOrangeIcon")
.resizable()
.tint(.colorTextPrimaryInverted)
.foregroundStyle(.colorTextPrimaryInverted)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ struct MessagesBottomBar<BottomBarContent: View>: View {
let emptyDisplayNamePlaceholder: String = "Somebody"
@Binding var messageText: String
@Binding var selectedAttachmentImage: UIImage?
var pendingInviteCode: String?
let sendButtonEnabled: Bool
@Binding var profileImage: UIImage?
@Binding var isPhotoPickerPresented: Bool
Expand All @@ -21,6 +22,7 @@ struct MessagesBottomBar<BottomBarContent: View>: View {
let messagesTextFieldEnabled: Bool
let onProfilePhotoTap: () -> Void
let onSendMessage: () -> Void
let onClearInvite: () -> Void
let onDisplayNameEndedEditing: () -> Void
let onProfileSettings: () -> Void
let onBaseHeightChanged: (CGFloat) -> Void
Expand Down Expand Up @@ -112,12 +114,14 @@ struct MessagesBottomBar<BottomBarContent: View>: View {
emptyDisplayNamePlaceholder: emptyDisplayNamePlaceholder,
messageText: $messageText,
selectedAttachmentImage: $selectedAttachmentImage,
pendingInviteCode: pendingInviteCode,
sendButtonEnabled: sendButtonEnabled,
focusState: $focusState,
animateAvatarForQuickname: onboardingCoordinator.shouldAnimateAvatarForQuicknameSetup,
messagesTextFieldEnabled: messagesTextFieldEnabled,
onProfilePhotoTap: onProfilePhotoTap,
onSendMessage: onSendMessage
onSendMessage: onSendMessage,
onClearInvite: onClearInvite
)
.opacity(messagesTextFieldEnabled ? 1.0 : 0.4)
.fixedSize(horizontal: false, vertical: true)
Expand Down Expand Up @@ -157,6 +161,7 @@ struct MessagesBottomBar<BottomBarContent: View>: View {
@Previewable @State var profileName: String = ""
@Previewable @State var messageText: String = ""
@Previewable @State var selectedAttachmentImage: UIImage?
@Previewable @State var pendingInviteCodePreview: String?
@Previewable @State var sendButtonEnabled: Bool = false
@Previewable @State var profileImage: UIImage?
@Previewable @State var isPhotoPickerPresented: Bool = false
Expand Down Expand Up @@ -196,6 +201,7 @@ struct MessagesBottomBar<BottomBarContent: View>: View {
displayName: $profileName,
messageText: $messageText,
selectedAttachmentImage: $selectedAttachmentImage,
pendingInviteCode: pendingInviteCodePreview,
sendButtonEnabled: sendButtonEnabled,
profileImage: $profileImage,
isPhotoPickerPresented: $isPhotoPickerPresented,
Expand All @@ -207,6 +213,7 @@ struct MessagesBottomBar<BottomBarContent: View>: View {
focusCoordinator.moveFocus(to: .displayName)
},
onSendMessage: {},
onClearInvite: { pendingInviteCodePreview = nil },
onDisplayNameEndedEditing: {
focusCoordinator.endEditing(for: .displayName)
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,19 @@ struct MessagesInputView: View {
let emptyDisplayNamePlaceholder: String
@Binding var messageText: String
@Binding var selectedAttachmentImage: UIImage?
var pendingInviteCode: String?
let sendButtonEnabled: Bool
@FocusState.Binding var focusState: MessagesViewInputFocus?
let animateAvatarForQuickname: Bool
let messagesTextFieldEnabled: Bool
private let focused: MessagesViewInputFocus = .message
let onProfilePhotoTap: () -> Void
let onSendMessage: () -> Void
let onClearInvite: () -> Void

private let attachmentPreviewSize: CGFloat = 80.0
@State private var isPoofing: Bool = false
@State private var isPoofingInvite: Bool = false

static var defaultHeight: CGFloat {
32.0
Expand All @@ -43,9 +46,13 @@ struct MessagesInputView: View {
}
}

private var hasAttachments: Bool {
selectedAttachmentImage != nil || pendingInviteCode != nil
}

var body: some View {
VStack(alignment: .leading, spacing: 0) {
if selectedAttachmentImage != nil {
if hasAttachments {
attachmentPreviewArea
}

Expand Down Expand Up @@ -122,6 +129,9 @@ struct MessagesInputView: View {
if let image = selectedAttachmentImage {
attachmentPreview(image: image)
}
if pendingInviteCode != nil {
inviteAttachmentPreview
}
}
.padding(.horizontal, DesignConstants.Spacing.step2x)
.padding(.bottom, DesignConstants.Spacing.step2x)
Expand Down Expand Up @@ -168,6 +178,61 @@ struct MessagesInputView: View {
.accessibilityIdentifier("remove-attachment-button")
}
}

@ViewBuilder
private var inviteAttachmentPreview: some View {
let invitePreviewWidth: CGFloat = 90
ZStack(alignment: .topTrailing) {
VStack(spacing: 0) {
ZStack {
Image("convosOrangeIcon")
.resizable()
.aspectRatio(contentMode: .fit)
.foregroundStyle(.colorTextPrimaryInverted)
.frame(width: 22, height: 22)
}
.frame(width: invitePreviewWidth, height: attachmentPreviewSize * 0.6)
.background(.colorFillPrimary)

Text("Invite")
.font(.caption2.weight(.medium))
.foregroundStyle(.colorTextPrimary)
.frame(maxWidth: .infinity)
.frame(height: attachmentPreviewSize * 0.4)
.background(.colorFillMinimal)
}
.frame(width: invitePreviewWidth, height: attachmentPreviewSize)
.clipShape(.rect(cornerRadius: DesignConstants.Spacing.step4x))
.scaleEffect(isPoofingInvite ? 1.3 : 1.0)
.blur(radius: isPoofingInvite ? 12.0 : 0.0)
.opacity(isPoofingInvite ? 0.0 : 1.0)
.accessibilityLabel("Invite attachment preview")
.accessibilityIdentifier("invite-attachment-preview")

Button {
withAnimation(.easeOut(duration: 0.2)) {
isPoofingInvite = true
}
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
onClearInvite()
isPoofingInvite = false
}
} label: {
Image(systemName: "xmark")
.font(.system(size: 10.0, weight: .bold))
.foregroundStyle(.white)
.frame(width: 20.0, height: 20.0)
.background(.black)
.clipShape(.circle)
.overlay(Circle().stroke(.white.opacity(0.6), lineWidth: 1.0))
}
.opacity(isPoofingInvite ? 0.0 : 1.0)
.padding(.top, DesignConstants.Spacing.step2x)
.padding(.trailing, DesignConstants.Spacing.step2x)
.accessibilityLabel("Remove invite")
.accessibilityIdentifier("remove-invite-button")
}
}
}

#Preview {
Expand All @@ -177,6 +242,7 @@ struct MessagesInputView: View {
@Previewable @State var sendButtonEnabled: Bool = false
@Previewable @State var profileImage: UIImage?
@Previewable @State var selectedAttachmentImage: UIImage?
@Previewable @State var pendingInviteCodePreview: String? = "test-invite-code"
@Previewable @State var animateAvatarForQuickname: Bool = false
@Previewable @FocusState var focusState: MessagesViewInputFocus?

Expand All @@ -199,12 +265,14 @@ struct MessagesInputView: View {
emptyDisplayNamePlaceholder: "Somebody",
messageText: $messageText,
selectedAttachmentImage: $selectedAttachmentImage,
pendingInviteCode: pendingInviteCodePreview,
sendButtonEnabled: sendButtonEnabled,
focusState: $focusState,
animateAvatarForQuickname: animateAvatarForQuickname,
messagesTextFieldEnabled: true,
onProfilePhotoTap: {},
onSendMessage: {}
onSendMessage: {},
onClearInvite: { pendingInviteCodePreview = nil }
)
.padding(DesignConstants.Spacing.step2x)
}
Expand Down
Loading
Loading