good haptics and setting up for recording
This commit is contained in:
@@ -39,6 +39,7 @@ struct FooterControlsView: View {
|
|||||||
var body: some View {
|
var body: some View {
|
||||||
ZStack {
|
ZStack {
|
||||||
Button(action: {
|
Button(action: {
|
||||||
|
|
||||||
}, label: {
|
}, label: {
|
||||||
Image(systemName: self.isHolding ? "waveform" : "mic.fill")
|
Image(systemName: self.isHolding ? "waveform" : "mic.fill")
|
||||||
.foregroundColor(.white)
|
.foregroundColor(.white)
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ struct CircleFooterView: View {
|
|||||||
print("calling user now")
|
print("calling user now")
|
||||||
|
|
||||||
if self.selectedFriend?.phoneNumber != nil {
|
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)!
|
let strUrl: String = tel.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)!
|
||||||
|
|
||||||
UIApplication.shared.open(URL(string: strUrl)!, options: [:], completionHandler: nil)
|
UIApplication.shared.open(URL(string: strUrl)!, options: [:], completionHandler: nil)
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ struct CircleGridView: View {
|
|||||||
@EnvironmentObject var navigationStack: NavigationStack
|
@EnvironmentObject var navigationStack: NavigationStack
|
||||||
|
|
||||||
@State var queuePlayer = AVQueuePlayer()
|
@State var queuePlayer = AVQueuePlayer()
|
||||||
|
@GestureState var dragState = DragState.inactive
|
||||||
|
|
||||||
let universalSize = UIScreen.main.bounds
|
let universalSize = UIScreen.main.bounds
|
||||||
|
|
||||||
@@ -44,6 +45,8 @@ struct CircleGridView: View {
|
|||||||
var body: some View {
|
var body: some View {
|
||||||
gridContent
|
gridContent
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let longPressMinDuration = 0.5
|
||||||
|
|
||||||
private var gridContent: some View {
|
private var gridContent: some View {
|
||||||
// main communication hub
|
// main communication hub
|
||||||
@@ -93,74 +96,53 @@ struct CircleGridView: View {
|
|||||||
.id(value) // id for scrollviewreader
|
.id(value) // id for scrollviewreader
|
||||||
.frame(height: Self.size)
|
.frame(height: Self.size)
|
||||||
.onTapGesture {
|
.onTapGesture {
|
||||||
// clearing the player to make room for this friend's convo or to deselect this user
|
self.handleTap(friendIndex: value, friend: friend)
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.onLongPressGesture {
|
.gesture(
|
||||||
// haptics for recording
|
LongPressGesture(minimumDuration: longPressMinDuration)
|
||||||
let impactHeavy = UIImpactFeedbackGenerator(style: .heavy)
|
.onEnded {_ in // on activation of long press
|
||||||
impactHeavy.impactOccurred()
|
// stop any player still playing of a message
|
||||||
|
self.queuePlayer.removeAllItems()
|
||||||
// start recording
|
|
||||||
|
print("activated long press!")
|
||||||
// store in db
|
self.selectedFriendIndex = value
|
||||||
}
|
|
||||||
// .animation(Animation.easeInOut(duration: 2).repeatForever(autoreverses: true), value: self.usersWithNewMessage)
|
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())
|
.animation(Animation.spring())
|
||||||
}
|
}
|
||||||
} // TODO: add padding based on if we are on any cornering item to allow the bubble to enlargen
|
} // 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
|
} // scrollview reader
|
||||||
}
|
}
|
||||||
|
|
||||||
private func haveNewMessageFromFriend(friendDbId: String) -> Bool {
|
private func haveNewMessageFromFriend(friendDbId: String) -> Bool {
|
||||||
if self.authSessionStore.user != nil {
|
if self.authSessionStore.user != nil {
|
||||||
let userId = self.authSessionStore.user!.id // O(1) // currUser who is signed in
|
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)
|
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
|
// getting the proxy of an individual item
|
||||||
// and decoding into a scale that the item should take
|
// and decoding into a scale that the item should take
|
||||||
private func getScale(proxy: GeometryProxy, itemNumber: Int) -> CGFloat {
|
private func getScale(proxy: GeometryProxy, itemNumber: Int) -> CGFloat {
|
||||||
@@ -264,8 +256,118 @@ struct CircleGridView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct CircleGridView_Previews: PreviewProvider {
|
|
||||||
static var previews: some View {
|
// extension for handling the gestures and actions
|
||||||
CircleGridView(selectedFriendIndex: Binding.constant(nil)).environmentObject(AuthSessionStore())
|
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 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]
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user