diff --git a/nirvana-ios/Views/FooterControlsView/FooterControlsView.swift b/nirvana-ios/Views/FooterControlsView/FooterControlsView.swift index 5cf1c5d..5913745 100644 --- a/nirvana-ios/Views/FooterControlsView/FooterControlsView.swift +++ b/nirvana-ios/Views/FooterControlsView/FooterControlsView.swift @@ -39,6 +39,7 @@ struct FooterControlsView: View { var body: some View { ZStack { Button(action: { + }, label: { Image(systemName: self.isHolding ? "waveform" : "mic.fill") .foregroundColor(.white) diff --git a/nirvana-ios/Views/InnerCircle/CircleFooterView.swift b/nirvana-ios/Views/InnerCircle/CircleFooterView.swift index 0811c97..530b38b 100644 --- a/nirvana-ios/Views/InnerCircle/CircleFooterView.swift +++ b/nirvana-ios/Views/InnerCircle/CircleFooterView.swift @@ -52,7 +52,7 @@ struct CircleFooterView: View { print("calling user now") if self.selectedFriend?.phoneNumber != nil { - let tel: String = "tel://\(self.selectedFriend?.phoneNumber)" + let tel: String = "tel://\(self.selectedFriend!.phoneNumber!)" let strUrl: String = tel.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)! UIApplication.shared.open(URL(string: strUrl)!, options: [:], completionHandler: nil) diff --git a/nirvana-ios/Views/InnerCircle/CircleGridView.swift b/nirvana-ios/Views/InnerCircle/CircleGridView.swift index 83eef6f..557f795 100644 --- a/nirvana-ios/Views/InnerCircle/CircleGridView.swift +++ b/nirvana-ios/Views/InnerCircle/CircleGridView.swift @@ -14,6 +14,7 @@ struct CircleGridView: View { @EnvironmentObject var navigationStack: NavigationStack @State var queuePlayer = AVQueuePlayer() + @GestureState var dragState = DragState.inactive let universalSize = UIScreen.main.bounds @@ -44,6 +45,8 @@ struct CircleGridView: View { var body: some View { gridContent } + + let longPressMinDuration = 0.5 private var gridContent: some View { // main communication hub @@ -93,74 +96,53 @@ struct CircleGridView: View { .id(value) // id for scrollviewreader .frame(height: Self.size) .onTapGesture { - // clearing the player to make room for this friend's convo or to deselect this user - self.queuePlayer.removeAllItems() - - // if user had previously selected user, put nil as a toggle - if self.selectedFriendIndex == value { - self.selectedFriendIndex = nil - return - } else { - self.selectedFriendIndex = value - } - - // TODO: OPTIMIZATION...buffer and load all AVAssets to create AVPlayerItems before a tap happens...but this can also cause load in background if user is not playing a message right now...this isn't an optimization of the data/firestore but rather the player - - - - // I want to play the last x messages if I was the receiver - // ["sarth": [me, me]] -> play nothing - // ["sarth": [me, him, him, him]] -> play his two messages - - // traverse through reversed list of messages and add to audio player queue - // TODO: protect against force unwraps - var AVPlayerItems: [AVPlayerItem] = [] - let messagesRelatedToFriend = self.authSessionStore.friendMessagesDict[friend.id!]!.reversed() - print("have \(messagesRelatedToFriend.count) messages to play") - - for message in messagesRelatedToFriend { - // if it's me then don't play - if message.senderId == self.authSessionStore.user?.id { - break - } - // if I already listened to this "last" message, then break as well - if message.listenCount >= 1 { - break - } - - // only add to queue if we can convert the database url to a valid url here - if let audioUrl = URL(string: message.audioDataUrl) { - let playerMessage: AVPlayerItem = AVPlayerItem(url: audioUrl) - AVPlayerItems.append(playerMessage) - } - } - - // start playing if there are messages to listen to - if AVPlayerItems.count > 0 { - queuePlayer = AVQueuePlayer(items: AVPlayerItems) - - // TODO: make sure these options are viable for different scenarios - queuePlayer.automaticallyWaitsToMinimizeStalling = false - queuePlayer.playImmediately(atRate: 1) -// queuePlayer.play() - - print("player queued up items!!!") - - - // update the listencount and firstlistentimestamp of those messages in firestore - // this should update ui to show that there is no message to show - } + self.handleTap(friendIndex: value, friend: friend) } - .onLongPressGesture { - // haptics for recording - let impactHeavy = UIImpactFeedbackGenerator(style: .heavy) - impactHeavy.impactOccurred() - - // start recording - - // store in db - } -// .animation(Animation.easeInOut(duration: 2).repeatForever(autoreverses: true), value: self.usersWithNewMessage) + .gesture( + LongPressGesture(minimumDuration: longPressMinDuration) + .onEnded {_ in // on activation of long press + // stop any player still playing of a message + self.queuePlayer.removeAllItems() + + print("activated long press!") + self.selectedFriendIndex = value + + self.activateHaptics() + + self.record() + } + .sequenced(before: + DragGesture(minimumDistance: 0, coordinateSpace: .local) + ) + .updating(self.$dragState) { gestureValue, state, transaction in +// print("in updating: \(gestureValue) \(state), \(transaction)") + switch gestureValue { + // Long press begins. + case .first(true): + state = .pressing + // Long press confirmed, dragging may begin. + case .second(true, let drag): + state = .dragging(translation: drag?.translation ?? .zero) + // Dragging ended or the long press cancelled. + default: + state = .inactive + } + } + .onEnded { gestureValue in + guard case .second(true, let drag?) = gestureValue else { return } + + self.recordingGestureDeactived() + } + ) +// .simultaneousGesture( +// DragGesture(minimumDistance: 0, coordinateSpace: .local) +// .onChanged {_ in +// self.recordingGestureActive(friendIndex: value, friend: friend) +// } +// .onEnded {_ in +// self.recordingGestureDeactived() +// } +// ) .animation(Animation.spring()) } } // TODO: add padding based on if we are on any cornering item to allow the bubble to enlargen @@ -174,7 +156,7 @@ struct CircleGridView: View { } } // scrollview reader } - + private func haveNewMessageFromFriend(friendDbId: String) -> Bool { if self.authSessionStore.user != nil { let userId = self.authSessionStore.user!.id // O(1) // currUser who is signed in @@ -197,7 +179,17 @@ struct CircleGridView: View { return Color.white.opacity(0.4) } - +} + +struct CircleGridView_Previews: PreviewProvider { + static var previews: some View { + CircleGridView(selectedFriendIndex: Binding.constant(nil)).environmentObject(AuthSessionStore()) + } +} + + +// extension for the bubble math for the grid +extension CircleGridView { // getting the proxy of an individual item // and decoding into a scale that the item should take private func getScale(proxy: GeometryProxy, itemNumber: Int) -> CGFloat { @@ -264,8 +256,118 @@ struct CircleGridView: View { } } -struct CircleGridView_Previews: PreviewProvider { - static var previews: some View { - CircleGridView(selectedFriendIndex: Binding.constant(nil)).environmentObject(AuthSessionStore()) + +// extension for handling the gestures and actions +extension CircleGridView { + // listening to messages + private func handleTap(friendIndex: Int, friend: User) { + print("tap gesture activated") + + // clearing the player to make room for this friend's convo or to deselect this user + self.queuePlayer.removeAllItems() + + // if user had previously selected user, put nil as a toggle + if self.selectedFriendIndex == friendIndex { + self.selectedFriendIndex = nil + return + } else { + self.selectedFriendIndex = friendIndex + } + + // TODO: OPTIMIZATION...buffer and load all AVAssets to create AVPlayerItems before a tap happens...but this can also cause load in background if user is not playing a message right now...this isn't an optimization of the data/firestore but rather the player + + // I want to play the last x messages if I was the receiver + // ["sarth": [me, me]] -> play nothing + // ["sarth": [me, him, him, him]] -> play his two messages + + // traverse through reversed list of messages and add to audio player queue + // TODO: protect against force unwraps + var AVPlayerItems: [AVPlayerItem] = [] + let messagesRelatedToFriend = self.authSessionStore.friendMessagesDict[friend.id!]!.reversed() + print("have \(messagesRelatedToFriend.count) messages to play") + + for message in messagesRelatedToFriend { + // if it's me then don't play + if message.senderId == self.authSessionStore.user?.id { + break + } + // if I already listened to this "last" message, then break as well + if message.listenCount >= 1 { + break + } + + // only add to queue if we can convert the database url to a valid url here + if let audioUrl = URL(string: message.audioDataUrl) { + let playerMessage: AVPlayerItem = AVPlayerItem(url: audioUrl) + AVPlayerItems.append(playerMessage) + } + } + + // start playing if there are messages to listen to + if AVPlayerItems.count > 0 { + queuePlayer = AVQueuePlayer(items: AVPlayerItems) + + // TODO: make sure these options are viable for different scenarios + queuePlayer.automaticallyWaitsToMinimizeStalling = false + queuePlayer.playImmediately(atRate: 1) +// queuePlayer.play() + + print("player queued up items!!!") + + + // update the listencount and firstlistentimestamp of those messages in firestore + // this should update ui to show that there is no message to show + } + } + + enum DragState { + case inactive + case pressing + case dragging(translation: CGSize) + + var translation: CGSize { + switch self { + case .inactive, .pressing: + return .zero + case .dragging(let translation): + return translation + } + } + + var isActive: Bool { + switch self { + case .inactive: + return false + case .pressing, .dragging: + return true + } + } + + var isDragging: Bool { + switch self { + case .inactive, .pressing: + return false + case .dragging: + return true + } + } + } + + private func activateHaptics() { + // haptics for recording + print("got haptics on") + let impactHeavy = UIImpactFeedbackGenerator(style: .heavy) + impactHeavy.impactOccurred() + } + + private func recordingGestureDeactived() { + // stop recording + print("stopped recording") + + self.selectedFriendIndex = nil + } + + private func record() { + // call parent view model/environment object vm functions } } diff --git a/nirvana-ios/Views/InnerCircle/InnerCircleViewModel.swift b/nirvana-ios/Views/InnerCircle/InnerCircleViewModel.swift index 9ae73a7..a634326 100644 --- a/nirvana-ios/Views/InnerCircle/InnerCircleViewModel.swift +++ b/nirvana-ios/Views/InnerCircle/InnerCircleViewModel.swift @@ -6,3 +6,58 @@ // import Foundation +import AVFoundation + +class InnerCircleViewModel: ObservableObject { + var audioRecorder : AVAudioRecorder! + var audioPlayer : AVAudioPlayer! + + @Published var isRecording : Bool = false + + func startRecording() { + let recordingSession = AVAudioSession.sharedInstance() + do { + try recordingSession.setCategory(.playAndRecord, mode: .default) + try recordingSession.setActive(true) + } catch { + print("Can not setup the Recording") + } + + let path = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0] + let fileName = path.appendingPathComponent("\(UUID().uuidString).m4a") + + let settings = [ + AVFormatIDKey: Int(kAudioFormatMPEG4AAC), + AVSampleRateKey: 12000, + AVNumberOfChannelsKey: 1, + AVEncoderAudioQualityKey: AVAudioQuality.high.rawValue + ] + + + do { + audioRecorder = try AVAudioRecorder(url: fileName, settings: settings) + audioRecorder.prepareToRecord() + audioRecorder.record() + isRecording = true + + } catch { + print("Failed to Setup the Recording") + } + } + + func stopRecording(){ + audioRecorder.stop() + isRecording = false + + // upload to the cloud storage + + // store in firestore for receiving user to automatically get notified + } + + func getDocumentsDirectory() -> URL { + let paths = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask) + return paths[0] + } + +} +