good haptics and setting up for recording

This commit is contained in:
talksik
2021-12-22 04:05:23 -08:00
parent ae506a7715
commit 33916a1663
4 changed files with 231 additions and 73 deletions
@@ -39,6 +39,7 @@ struct FooterControlsView: View {
var body: some View {
ZStack {
Button(action: {
}, label: {
Image(systemName: self.isHolding ? "waveform" : "mic.fill")
.foregroundColor(.white)
@@ -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)
@@ -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
}
}
@@ -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]
}
}