From 4ec67dcb6421fe4a1d05bb50362a29da959e0d57 Mon Sep 17 00:00:00 2001 From: talksik Date: Mon, 3 Jan 2022 01:19:40 -0800 Subject: [PATCH] nicer ui to play things and moving things all to the view model for listening to messages and much cleaner --- nirvana-ios.xcodeproj/project.pbxproj | 4 + .../Views/InnerCircle/CircleFooterView.swift | 10 +- .../Views/InnerCircle/CircleGridView.swift | 105 ++----------- .../InnerCircle/CircleNavigationView.swift | 1 - .../InnerCircle/InnerCircleViewModel.swift | 145 +++++++++++++++++- .../Views/Templates/ProgressBarView.swift | 28 ++++ 6 files changed, 192 insertions(+), 101 deletions(-) create mode 100644 nirvana-ios/Views/Templates/ProgressBarView.swift diff --git a/nirvana-ios.xcodeproj/project.pbxproj b/nirvana-ios.xcodeproj/project.pbxproj index 4bd2675..af50bd7 100644 --- a/nirvana-ios.xcodeproj/project.pbxproj +++ b/nirvana-ios.xcodeproj/project.pbxproj @@ -16,6 +16,7 @@ 8909199E278155920025F4BB /* UserStatusView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8909199D278155920025F4BB /* UserStatusView.swift */; }; 890919A42781845D0025F4BB /* AlertToast in Frameworks */ = {isa = PBXBuildFile; productRef = 890919A32781845D0025F4BB /* AlertToast */; }; 890919A7278299590025F4BB /* RuleBook.swift in Sources */ = {isa = PBXBuildFile; fileRef = 890919A6278299590025F4BB /* RuleBook.swift */; }; + 890919AB2782DC760025F4BB /* ProgressBarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 890919AA2782DC760025F4BB /* ProgressBarView.swift */; }; 890C245D277BF628009B4A30 /* AgoraRtcKit in Frameworks */ = {isa = PBXBuildFile; productRef = 890C245C277BF628009B4A30 /* AgoraRtcKit */; }; 890C2460277BF896009B4A30 /* AgoraViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 890C245F277BF896009B4A30 /* AgoraViewController.swift */; }; 890C2462277BF935009B4A30 /* CallView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 890C2461277BF935009B4A30 /* CallView.swift */; }; @@ -107,6 +108,7 @@ 8909199B277EFDF30025F4BB /* joinSound.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = joinSound.mp3; sourceTree = ""; }; 8909199D278155920025F4BB /* UserStatusView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserStatusView.swift; sourceTree = ""; }; 890919A6278299590025F4BB /* RuleBook.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RuleBook.swift; sourceTree = ""; }; + 890919AA2782DC760025F4BB /* ProgressBarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProgressBarView.swift; sourceTree = ""; }; 890C245F277BF896009B4A30 /* AgoraViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AgoraViewController.swift; sourceTree = ""; }; 890C2461277BF935009B4A30 /* CallView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallView.swift; sourceTree = ""; }; 890C2463277BFA97009B4A30 /* AgroRep.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AgroRep.swift; sourceTree = ""; }; @@ -332,6 +334,7 @@ 89288ECA27675B9F00E962C4 /* OnboardingTemplateView.swift */, 89BCABEC276D2B05009E9B10 /* WavesGlassBackgroundView.swift */, 8909199D278155920025F4BB /* UserStatusView.swift */, + 890919AA2782DC760025F4BB /* ProgressBarView.swift */, ); path = Templates; sourceTree = ""; @@ -674,6 +677,7 @@ 89F05BA6276ACD830002E9C7 /* ContactsPickerView.swift in Sources */, 8904DEC8277055E90071E6CA /* OnboardingOneView.swift in Sources */, 896CE7022770892F00DB474E /* CircleGridView.swift in Sources */, + 890919AB2782DC760025F4BB /* ProgressBarView.swift in Sources */, 89091995277DD9F80025F4BB /* ConvoFooterView.swift in Sources */, 8909199E278155920025F4BB /* UserStatusView.swift in Sources */, 89BDCDEA27697F9C00577829 /* UserMenu.swift in Sources */, diff --git a/nirvana-ios/Views/InnerCircle/CircleFooterView.swift b/nirvana-ios/Views/InnerCircle/CircleFooterView.swift index a40d25a..e925eac 100644 --- a/nirvana-ios/Views/InnerCircle/CircleFooterView.swift +++ b/nirvana-ios/Views/InnerCircle/CircleFooterView.swift @@ -36,7 +36,12 @@ struct CircleFooterView: View { .clipShape(Circle()) .overlay(alignment: .topTrailing) { // user status - UserStatusView(status: self.selectedFriend!.userStatus, size: 10) + ZStack(alignment: .topTrailing) { + ProgressBarView(progress: self.innerCircleVM.messagesListeningProgress, color: self.myTurn ?? false ? Color.orange : NirvanaColor.dimTeal) + .frame(width: 40, height: 40) + + UserStatusView(status: self.selectedFriend!.userStatus, size: 10) + } } .padding(5) .contextMenu { @@ -79,13 +84,12 @@ struct CircleFooterView: View { .font(.caption2) .foregroundColor(.gray) } - - } } Spacer() + } .frame(maxWidth: .infinity, maxHeight: 60) // 60 is the height of the footer control big circle .background(Color.white.opacity(0.5)) diff --git a/nirvana-ios/Views/InnerCircle/CircleGridView.swift b/nirvana-ios/Views/InnerCircle/CircleGridView.swift index b9168b2..2fd6761 100644 --- a/nirvana-ios/Views/InnerCircle/CircleGridView.swift +++ b/nirvana-ios/Views/InnerCircle/CircleGridView.swift @@ -16,9 +16,6 @@ struct CircleGridView: View { @EnvironmentObject var navigationStack: NavigationStack @EnvironmentObject var convoVM: ConvoViewModel - @State var queuePlayer = AVQueuePlayer() - @State var timeObserverToken: Any? - @GestureState var dragState = DragState.inactive let universalSize = UIScreen.main.bounds @@ -158,6 +155,11 @@ struct CircleGridView: View { .blur(radius: 8) .cornerRadius(100) + if self.haveNewMessageFromFriend(friendDbId: friendId) { + ProgressBarView(progress: self.selectedFriendIndex == friendId ? self.innerCircleVM.messagesListeningProgress : Float(1), color: Color.orange) + } + + // user status UserStatusView(status: self.authSessionStore.relevantUsersDict[friendId]?.userStatus, size: 20, padding: 5) @@ -191,7 +193,7 @@ struct CircleGridView: View { self.activateHaptics() // stop any player still playing of a message - self.queuePlayer.removeAllItems() + self.innerCircleVM.stopPlayingAnyAudio() // if friend and I are online, and I am not in a convo, start convo immediately with them if self.authSessionStore.relevantUsersDict[friendId]?.userStatus == .online @@ -523,9 +525,9 @@ extension CircleGridView { self.convoVM.toast = .alreadyInCall return } - - // clearing the player to make room for this friend's convo or to deselect this user - self.queuePlayer.removeAllItems() + + // when deselecting, want to stop playing + self.innerCircleVM.stopPlayingAnyAudio() // if user had previously selected user, put nil as a toggle if self.selectedFriendIndex == friendId { @@ -535,17 +537,13 @@ extension CircleGridView { self.selectedFriendIndex = friendId } - // 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...the array is sorted from backend so that the // most recent comes first // ["sarth": [ME, HIM...]] -> play nothing // ["sarth": [HIM, HIM, me, 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] = [] - var AVAssets: [AVAsset] = [] + var audioUrls: [URL] = [] let messagesRelatedToFriend = self.authSessionStore.relevantMessagesByUserDict[friendId] ?? [] if messagesRelatedToFriend.count == 0 { @@ -553,7 +551,6 @@ extension CircleGridView { } for message in messagesRelatedToFriend { - print("message: the sender is \(message.senderId) and senttime: \(message.sentTimestamp)") // if it's starting to get to my messages then don't play if message.senderId == self.authSessionStore.user?.id { break @@ -561,88 +558,12 @@ extension CircleGridView { // 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) - let playerAsset: AVAsset = AVAsset(url: audioUrl) - AVAssets.append(playerAsset) - AVPlayerItems.append(playerMessage) + audioUrls.append(audioUrl) } } - // start playing if there are messages to listen to - if AVPlayerItems.count > 0 { - print("have \(AVPlayerItems.count) messages to play") - - // reverse the items because we want to listen to the most recent messages in order - AVPlayerItems = AVPlayerItems.reversed() - 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!!!") - - // notify every half second -// let timeScale = CMTimeScale(NSEC_PER_SEC) -// let time = CMTime(seconds: 0.5, preferredTimescale: timeScale) - -// timeObserverToken = queuePlayer.addPeriodicTimeObserver(forInterval: time, queue: .main) {time in -// // update player transport UI -// print("periodic time observer: \(time)") -// // if the current playeritem is the latest one: -// // 1. deselect this user -// // 2. update databse that I listened to this -// if queuePlayer.currentItem == AVPlayerItems.last { -// // hide the footer now...ehhh don't need to -//// DispatchQueue.main.asyncAfter(deadline: .now() + 2) { -//// self.selectedFriendIndex = nil -//// } -// } -// } - - // TODO: right now not updating all of that - // update the listencount and firstlistentimestamp of those messages in firestore - // this should update ui to show that there is no message to show - } - } - - - - func addBoundaryTimeObserver(playerAssets: [AVAsset]) { - var totalDuration = CMTime.zero - for item in playerAssets { - print(item.duration) - totalDuration = CMTimeAdd(item.duration, totalDuration) - } - print(totalDuration) - - // Divide the total duration into quarters. - let interval = CMTimeMultiplyByFloat64(totalDuration, multiplier: 0.95) - var currentTime = CMTime.zero - var times = [NSValue]() - - // Calculate boundary times - while currentTime < totalDuration { - currentTime = currentTime + interval - times.append(NSValue(time:currentTime)) - } - - print(times) - - timeObserverToken = queuePlayer.addBoundaryTimeObserver(forTimes: times, - queue: DispatchQueue.main) { - // Update UI - print("activated time boundary") - } - - } - - func removeBoundaryTimeObserver() { - if let timeObserverToken = timeObserverToken { - self.queuePlayer.removeTimeObserver(timeObserverToken) - self.timeObserverToken = nil - } + // plays everything from vm + self.innerCircleVM.playAssets(audioUrls: audioUrls) } enum DragState { diff --git a/nirvana-ios/Views/InnerCircle/CircleNavigationView.swift b/nirvana-ios/Views/InnerCircle/CircleNavigationView.swift index 615d9d3..da2a023 100644 --- a/nirvana-ios/Views/InnerCircle/CircleNavigationView.swift +++ b/nirvana-ios/Views/InnerCircle/CircleNavigationView.swift @@ -128,7 +128,6 @@ struct CircleNavigationView: View { .animation(.spring()) } } - } } diff --git a/nirvana-ios/Views/InnerCircle/InnerCircleViewModel.swift b/nirvana-ios/Views/InnerCircle/InnerCircleViewModel.swift index c9b7c7a..cdddb4c 100644 --- a/nirvana-ios/Views/InnerCircle/InnerCircleViewModel.swift +++ b/nirvana-ios/Views/InnerCircle/InnerCircleViewModel.swift @@ -7,10 +7,12 @@ import Foundation import AVFoundation +import AVKit import AlertToast import SwiftUI -class InnerCircleViewModel: ObservableObject { + +class InnerCircleViewModel: NSObject, ObservableObject { @Published var toast: Toast? { didSet { self.showToast = true @@ -67,13 +69,14 @@ class InnerCircleViewModel: ObservableObject { } } } - - + var audioRecorder : AVAudioRecorder! - var audioPlayer : AVAudioPlayer! + var queuePlayer = AVQueuePlayer() @Published var isRecording : Bool = false + @Published var messagesListeningProgress: Float = 0.0 + let audioSession = AVAudioSession.sharedInstance() private var audioLocalUrl: URL? @@ -83,7 +86,9 @@ class InnerCircleViewModel: ObservableObject { private let pushNotificationService = PushNotificationService() - init() { + override init() { + super.init() + // separate set up for listening vs recording do { try audioSession.setCategory(.playAndRecord, mode: .default, options: [.duckOthers, .allowBluetooth, .allowBluetoothA2DP]) @@ -238,3 +243,133 @@ extension InnerCircleViewModel { } } + +// everything to do with listening to messages +extension InnerCircleViewModel { + func stopPlayingAnyAudio() { + self.queuePlayer.removeAllItems() + } + + // TODO: print statements slowing it down? + func playAssets(audioUrls: [URL]) { + // clearing the player to make room for this friend's convo or to deselect this user + self.stopPlayingAnyAudio() + + var totalMessagesDurationSeconds = CMTime.zero + var AVPlayerItems: [AVPlayerItem] = [] + for url in audioUrls { + let asset = AVAsset(url: url) + let playerItem = AVPlayerItem(asset: asset) + + totalMessagesDurationSeconds = CMTimeAdd(totalMessagesDurationSeconds, asset.duration) + + // notification for when each playeritem is done playing +// NotificationCenter.default.addObserver(self, selector: #selector(self.playerDidFinishPlaying(sender:)), name: NSNotification.Name.AVPlayerItemDidPlayToEndTime, object: playerItem) + + + AVPlayerItems.append(playerItem) + } + + if AVPlayerItems.count <= 0 { + print("no messages to play...send a message to user") + return + } + + // start playing if there are messages to listen to + print("have \(AVPlayerItems.count) messages and \(totalMessagesDurationSeconds) seconds to play") + + // reverse the items because we want to listen to the most recent messages in order + AVPlayerItems = AVPlayerItems.reversed() + + // TODO: make sure these options are viable for different scenarios + self.queuePlayer = AVQueuePlayer(items: AVPlayerItems) + self.queuePlayer.automaticallyWaitsToMinimizeStalling = false + self.queuePlayer.playImmediately(atRate: 1) +// queuePlayer.play() + + print("player queued up items!!!") + + self.addBoundaryTimeObserver(totalDuration: totalMessagesDurationSeconds) + + // TODO: right now not updating all of that + // update the listencount and firstlistentimestamp of those messages in firestore + // this should update ui to show that there is no message to show + + } + + func addBoundaryTimeObserver(totalDuration: CMTime) { + var multiplier: Float64 = 0.05 + + // reset progress + self.messagesListeningProgress = Float(multiplier * 2) + + var times = [NSValue]() + // Set initial time to zero + var currentTime = CMTime.zero + // Divide the asset's duration into quarters. + let interval = CMTimeMultiplyByFloat64(totalDuration, multiplier: multiplier) + + // Build boundary times based on multiplier + while currentTime <= totalDuration { + currentTime = currentTime + interval + times.append(NSValue(time: currentTime)) + } + // this last one to make sure we get a full loop + times.append(NSValue(time: totalDuration)) + + // Add time observer. Observe boundary time changes on the main queue. + // TODO: not hitting the last one/100% + self.queuePlayer.addBoundaryTimeObserver(forTimes: times, queue: .main) { [weak self] in + // Update UI + self?.messagesListeningProgress += Float(multiplier) + } + } + +// func periodTimeObserver() { + + // notify every half second +// let timeScale = CMTimeScale(NSEC_PER_SEC) +// let time = CMTime(seconds: 0.5, preferredTimescale: timeScale) + + + // have the total duration, let's say 100 + + // want to keep adding to our backend: seconds listened so far + + // devide by the total duration seconds to get the progress + +// var totalListened: Double = 0.0 +// var lastValue: Double = 0.0 +// queuePlayer.addPeriodicTimeObserver(forInterval: time, queue: .main) { [weak self] TOTime in +// // reset counter once we get to subsequent playeritems +// if TOTime.seconds < lastValue { +// totalListened += (TOTime.seconds - 0) +// } else { +// totalListened += (TOTime.seconds - lastValue) +// } +// print("\(TOTime.seconds)") +// print("total seconds listened so far \(totalListened)") +// +// self?.messagesListeningProgress = Float(totalListened / totalMessagesDurationSeconds) +// +// lastValue = TOTime.seconds +// } +// } + +// func removeBoundaryTimeObserver() { +// if let timeObserverToken = timeObserverToken { +// self.queuePlayer.removeTimeObserver(timeObserverToken) +// self.timeObserverToken = nil +// } +// } + +} + +extension InnerCircleViewModel { +// @objc func playerDidFinishPlaying(sender: Notification) { +// // Your code here +// print("finished playing an item") +// +// self.numberofMessagesToPlay -= 1 +// } +} diff --git a/nirvana-ios/Views/Templates/ProgressBarView.swift b/nirvana-ios/Views/Templates/ProgressBarView.swift new file mode 100644 index 0000000..3d3538a --- /dev/null +++ b/nirvana-ios/Views/Templates/ProgressBarView.swift @@ -0,0 +1,28 @@ +// +// ProgressBarView.swift +// nirvana-ios +// +// Created by Arjun Patel on 1/2/22. +// + +import SwiftUI + +struct ProgressBarView: View { + var progress: Float + var color: Color = Color.orange + + var body: some View { + Circle() + .trim(from: 0.0, to: CGFloat(min(self.progress, 1.0))) + .stroke(style: StrokeStyle(lineWidth: 2.0, lineCap: .round, lineJoin: .round)) + .foregroundColor(color) + .rotationEffect(Angle(degrees: 270.0)) + .animation(.linear) + } +} + +//struct ProgressBarView_Previews: PreviewProvider { +// static var previews: some View { +// ProgressBarView() +// } +//}