Skip to content

Commit

Permalink
WIP Continous Voice recording
Browse files Browse the repository at this point in the history
- Added a voice recording lock button
- Added a new SwiftUI View for expanded voice recording

Signed-off-by: rapterjet2004 <juliuslinus1@gmail.com>
  • Loading branch information
rapterjet2004 committed Oct 10, 2024
1 parent 9d697e4 commit 2572679
Show file tree
Hide file tree
Showing 3 changed files with 224 additions and 8 deletions.
4 changes: 4 additions & 0 deletions NextcloudTalk.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -590,6 +590,7 @@
847EFC7236336B67A1A89358 /* libPods-BroadcastUploadExtension.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 9A3D305FCD7BF7E727A62F35 /* libPods-BroadcastUploadExtension.a */; };
8789AE73BFCAA413B43319C0 /* libPods-ShareExtension.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 684807120F4439797973DF73 /* libPods-ShareExtension.a */; };
9993261EDAC77481FF4EF58A /* libPods-NextcloudTalk.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 4F7C31E9D74F550EAF89931B /* libPods-NextcloudTalk.a */; };
C65D252D2C7581A200157A89 /* ExpandedVoiceMessageRecordingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C65D252C2C7581A200157A89 /* ExpandedVoiceMessageRecordingView.swift */; };
DA1AEFC3270F1FA90088E519 /* DateLabelCustom.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA1AEFC2270F1FA90088E519 /* DateLabelCustom.swift */; };
DA66582B27B6992F00B46B11 /* UserProfileTableViewController+AvatarSetup.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA66582A27B6992F00B46B11 /* UserProfileTableViewController+AvatarSetup.swift */; };
DA66582D27B6A73800B46B11 /* UserProfileTableViewController+DelegateMethods.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA66582C27B6A73800B46B11 /* UserProfileTableViewController+DelegateMethods.swift */; };
Expand Down Expand Up @@ -1189,6 +1190,7 @@
9B81BB7A4920C391CC2CACFD /* libPods-NotificationServiceExtension.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-NotificationServiceExtension.a"; sourceTree = BUILT_PRODUCTS_DIR; };
A8F95DE6635ABC1E64CA8E4A /* Pods-BroadcastUploadExtension.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-BroadcastUploadExtension.release.xcconfig"; path = "Pods/Target Support Files/Pods-BroadcastUploadExtension/Pods-BroadcastUploadExtension.release.xcconfig"; sourceTree = "<group>"; };
B7874918820589BF8FD69BED /* Pods-NextcloudTalkTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NextcloudTalkTests.release.xcconfig"; path = "Pods/Target Support Files/Pods-NextcloudTalkTests/Pods-NextcloudTalkTests.release.xcconfig"; sourceTree = "<group>"; };
C65D252C2C7581A200157A89 /* ExpandedVoiceMessageRecordingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExpandedVoiceMessageRecordingView.swift; sourceTree = "<group>"; };
D6DF51D976DC0F681FF83F7B /* Pods-NotificationServiceExtension.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NotificationServiceExtension.debug.xcconfig"; path = "Pods/Target Support Files/Pods-NotificationServiceExtension/Pods-NotificationServiceExtension.debug.xcconfig"; sourceTree = "<group>"; };
D86091EC1125C3057B9A299B /* Pods-NotificationServiceExtension.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NotificationServiceExtension.release.xcconfig"; path = "Pods/Target Support Files/Pods-NotificationServiceExtension/Pods-NotificationServiceExtension.release.xcconfig"; sourceTree = "<group>"; };
DA1AEFC2270F1FA90088E519 /* DateLabelCustom.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateLabelCustom.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -1925,6 +1927,7 @@
1F66B72029FA7089003FB168 /* TypingIndicatorView.xib */,
2C0424992CA33681004772F6 /* AudioPlayerView.swift */,
2C0424962CA335C4004772F6 /* AudioPlayerView.xib */,
C65D252C2C7581A200157A89 /* ExpandedVoiceMessageRecordingView.swift */,
);
name = "Chat views";
sourceTree = "<group>";
Expand Down Expand Up @@ -2973,6 +2976,7 @@
1F0B0A722BA264540073FF8D /* MentionSuggestion.swift in Sources */,
2CA1CCCD1F181741002FE6A2 /* NCUser.m in Sources */,
1F77A6162AB9B161007B6037 /* ScreenCaptureController.m in Sources */,
C65D252D2C7581A200157A89 /* ExpandedVoiceMessageRecordingView.swift in Sources */,
2CF8AD3F2A0010FB00A4D3E6 /* MessageTranslationViewController.swift in Sources */,
2C21446E2BB5B54D005A6537 /* BaseChatTableViewCell+Location.swift in Sources */,
2C4230F72B207AB00013E1FA /* ContextChatViewController.swift in Sources */,
Expand Down
136 changes: 128 additions & 8 deletions NextcloudTalk/BaseChatViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import UIKit
import Realm
import ContactsUI
import QuickLook
import SwiftUI

@objcMembers public class BaseChatViewController: InputbarViewController,
UITextFieldDelegate,
Expand Down Expand Up @@ -80,6 +81,8 @@ import QuickLook
private var sendButtonTagMessage = 99
private var sendButtonTagVoice = 98

private var isVoiceRecordingLocked = false

private var actionTypeTranscribeVoiceMessage = "transcribe-voice-message"

private var imagePicker: UIImagePickerController?
Expand All @@ -89,6 +92,7 @@ import QuickLook
private var voiceMessageLongPressGesture: UILongPressGestureRecognizer?
private var recorder: AVAudioRecorder?
private var voiceMessageRecordingView: VoiceMessageRecordingView?
private var expandedUIHostingController: UIHostingController<ExpandedVoiceMessageRecordingView>?
private var longPressStartingPoint: CGPoint?
private var cancelHintLabelInitialPositionX: CGFloat?
private var recordCancelled: Bool = false
Expand Down Expand Up @@ -169,6 +173,22 @@ import QuickLook
return button
}()

private lazy var voiceRecordingLockButton: UIButton = {
let button = UIButton(frame: .init(x: 0, y: 0, width: 44, height: 44))

button.backgroundColor = .secondarySystemBackground
button.tintColor = .systemBlue
button.layer.cornerRadius = button.frame.size.height / 2
button.clipsToBounds = true
button.alpha = 0
button.translatesAutoresizingMaskIntoConstraints = false
button.setImage(UIImage(systemName: "lock.open"), for: .normal)

self.view.addSubview(button)

return button
}()

// MARK: - Init/Deinit

public init?(for room: NCRoom) {
Expand Down Expand Up @@ -264,7 +284,8 @@ import QuickLook
"unreadMessageButton": self.unreadMessageButton,
"textInputbar": self.textInputbar,
"scrollToBottomButton": self.scrollToBottomButton,
"autoCompletionView": self.autoCompletionView
"autoCompletionView": self.autoCompletionView,
"voiceRecordingLockButton": self.voiceRecordingLockButton
]

let metrics = [
Expand All @@ -281,7 +302,11 @@ import QuickLook
self.view.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "V:[scrollToBottomButton(44)]-10-[autoCompletionView]", metrics: metrics, views: views))
self.view.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "H:|-(>=0)-[scrollToBottomButton(44)]-(>=0)-|", metrics: metrics, views: views))

self.view.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "V:[voiceRecordingLockButton(44)]-64-[autoCompletionView]", metrics: metrics, views: views))
self.view.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "H:|-(>=0)-[voiceRecordingLockButton(44)]-(>=0)-|", metrics: metrics, views: views))

self.scrollToBottomButton.trailingAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.trailingAnchor, constant: -10).isActive = true
self.voiceRecordingLockButton.trailingAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.trailingAnchor, constant: -10).isActive = true

self.addMenuToLeftButton()

Expand Down Expand Up @@ -1492,6 +1517,72 @@ import QuickLook
self.voiceMessageRecordingView?.isHidden = true
}

// MARK: - Expanded voice message recording

func showExpandedVoiceMessageRecordingView(offset: Int) {
let expandedView = ExpandedVoiceMessageRecordingView(
deleteFunc: handleDelete, sendFunc: handleSend, recordFunc: handleRecord(isRecording:), timeElapsed: offset
)

let hostingController = UIHostingController(rootView: expandedView)
guard let expandedVoiceMessageRecordingView = hostingController.view else { return }

self.expandedUIHostingController = hostingController
self.view.addSubview(expandedVoiceMessageRecordingView)

expandedVoiceMessageRecordingView.translatesAutoresizingMaskIntoConstraints = false

let views = [
"expandedVoiceMessageRecordingView": expandedVoiceMessageRecordingView
]

expandedVoiceMessageRecordingView.bottomAnchor.constraint(equalTo: self.textInputbar.bottomAnchor).isActive = true
self.view.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "H:|[expandedVoiceMessageRecordingView]|", metrics: nil, views: views))
}

func handleDelete() {
self.recordCancelled = true
self.stopRecordingVoiceMessage()
handleCollapseVoiceRecording()
}

func handleSend() {
if let recorder = self.recorder, recorder.isRecording {
self.recordCancelled = false
self.stopRecordingVoiceMessage()
} else {
self.hideVoiceMessageRecordingView()
self.shareVoiceMessage()
}
handleCollapseVoiceRecording()
}

func handleRecord(isRecording: Bool) {
if isRecording {
if let recorder = self.recorder, !recorder.isRecording {
let session = AVAudioSession.sharedInstance()
try? session.setActive(true)
recorder.record()
print("Recording Restarted")
}
} else {
recordCancelled = true
if let recorder = self.recorder, recorder.isRecording {
recorder.stop()
let session = AVAudioSession.sharedInstance()
try? session.setActive(false)
print("Recording Stopped")
}
}
}

func handleCollapseVoiceRecording() {
self.isVoiceRecordingLocked = false
self.expandedUIHostingController?.removeFromParent()
self.expandedUIHostingController?.view.isHidden = true
self.textInputbar.bringSubviewToFront(self.textInputbar)
}

func setupAudioRecorder() {
guard let userDocumentDirectory = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true).last,
let outputFileURL = NSURL.fileURL(withPathComponents: [userDocumentDirectory, "voice-message-recording.m4a"])
Expand Down Expand Up @@ -1542,6 +1633,7 @@ import QuickLook
let session = AVAudioSession.sharedInstance()
try? session.setActive(true)
recorder.record()
print("Recording started")
}
}

Expand All @@ -1551,6 +1643,7 @@ import QuickLook
recorder.stop()
let session = AVAudioSession.sharedInstance()
try? session.setActive(false)
print("Recording Stopped")
}
}

Expand Down Expand Up @@ -1788,14 +1881,19 @@ import QuickLook
self.recordCancelled = false
self.longPressStartingPoint = point
self.cancelHintLabelInitialPositionX = voiceMessageRecordingView?.slideToCancelHintLabel?.frame.origin.x
self.voiceRecordingLockButton.alpha = 1
} else if gestureRecognizer.state == .ended {
print("Stop recording audio message")
self.shouldLockInterfaceOrientation(lock: false)
if let recordingTime = self.recorder?.currentTime {
// Mark record as cancelled if audio message is no longer than one second
self.recordCancelled = recordingTime < 1
self.resetVoiceRecordingLockButton()

if !isVoiceRecordingLocked {
if let recordingTime = self.recorder?.currentTime {
// Mark record as cancelled if audio message is no longer than one second
self.recordCancelled = recordingTime < 1
}
self.stopRecordingVoiceMessage()
print("Stop recording audio message")
}
self.stopRecordingVoiceMessage()
} else if gestureRecognizer.state == .changed {
guard let longPressStartingPoint,
let cancelHintLabelInitialPositionX,
Expand All @@ -1804,6 +1902,7 @@ import QuickLook
else { return }

let slideX = longPressStartingPoint.x - point.x
let slideY = longPressStartingPoint.y - point.y

// Only slide view to the left
if slideX > 0 {
Expand All @@ -1815,19 +1914,35 @@ import QuickLook
slideToCancelHintLabel.alpha = (maxSlideX - slideX) / 100

// Cancel recording if slided more than maxSlideX
if slideX > maxSlideX, !self.recordCancelled {
if slideX > maxSlideX, !self.recordCancelled, !isVoiceRecordingLocked {
print("Cancel recording audio message")

// 'Cancelled' feedback (three sequential weak booms)
AudioServicesPlaySystemSound(1521)
self.recordCancelled = true
self.stopRecordingVoiceMessage()
self.resetVoiceRecordingLockButton()
}
}

if slideY > 0 {
let maxSlideY = 64.0
if slideY > maxSlideY, !self.recordCancelled {
if !isVoiceRecordingLocked {
self.voiceRecordingLockButton.setImage(UIImage(systemName: "lock"), for: .normal)
let offset = self.voiceMessageRecordingView?.recordingTimeLabel?.getTimeCounted()
let intOffset = Int(offset!.magnitude)
showExpandedVoiceMessageRecordingView(offset: intOffset)
print("LOCKED")
isVoiceRecordingLocked = true
}
}
}
} else if gestureRecognizer.state == .cancelled || gestureRecognizer.state == .failed {
print("Gesture cancelled or failed -> Cancel recording audio message")
self.shouldLockInterfaceOrientation(lock: false)
self.recordCancelled = false
self.resetVoiceRecordingLockButton()
self.stopRecordingVoiceMessage()
}
}
Expand All @@ -1838,6 +1953,11 @@ import QuickLook
}
}

func resetVoiceRecordingLockButton() {
self.voiceRecordingLockButton.alpha = 0
self.voiceRecordingLockButton.setImage(UIImage(systemName: "lock.open"), for: .normal)
}

// MARK: - UIScrollViewDelegate methods

public override func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
Expand Down Expand Up @@ -2522,7 +2642,7 @@ import QuickLook
guard let message = self.message(for: indexPath) else { continue }

DispatchQueue.global(qos: .userInitiated).async {
guard message.messageId != kUnreadMessagesSeparatorIdentifier,
guard message.messageId != kUnreadMessagesSeparatorIdentifier,
message.messageId != kChatBlockSeparatorIdentifier
else { return }

Expand Down
92 changes: 92 additions & 0 deletions NextcloudTalk/ExpandedVoiceMessageRecordingView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
//
// SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
// SPDX-License-Identifier: GPL-3.0-or-later
//

import SwiftUI

func formatSeconds(seconds: Int) -> String {
let minutes = seconds / 60
let seconds = seconds % 60
return String(format: "%02d:%02d", minutes, seconds)

}

struct ExpandedVoiceMessageRecordingView: View {
var buttonPadding: CGFloat = 40
var deleteFunc: () -> Void
var sendFunc: () -> Void
var recordFunc: (Bool) -> Void

@State var isRecording = true
@State var timeElapsed: Int
@State var timeFormatted = ""
@State var timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()

var body: some View {
VStack {
Text("\(timeFormatted)")
.font(.largeTitle)
.bold()
.padding(.trailing, 10)
.frame(alignment: .center)
.border(.clear)
.onReceive(timer) { _ in
if isRecording {
timeElapsed += 1
timeFormatted = formatSeconds(seconds: timeElapsed)
}
}
HStack {
Button(action: { // Delete Recording
self.deleteFunc()
}, label: {
Label("", systemImage: "trash").font(.title2)
})
Spacer()
Button(action: { // End/Restart Recording
isRecording.toggle()

if isRecording {
timeElapsed = 0
timeFormatted = formatSeconds(seconds: 0)
timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
}

self.recordFunc(isRecording)

}, label: {
Label("", systemImage: isRecording ? "square.fill" : "arrow.clockwise.square").font(.title2)
})
Spacer()
Button(action: { // Send Recording
self.sendFunc()

}, label: {
Label("", systemImage: "paperplane").font(.title2)
})
}
.frame(maxWidth: .infinity, alignment: .center)
.padding(.horizontal, buttonPadding)
.padding(.bottom, 10)
.border(.clear)

}
.frame(maxWidth: .infinity)
.border(.clear)
.background(Color(NCAppBranding.backgroundColor()))
.onAppear {
timeFormatted = formatSeconds(seconds: timeElapsed)
}
}
}

//#Preview {

Check warning on line 84 in NextcloudTalk/ExpandedVoiceMessageRecordingView.swift

View workflow job for this annotation

GitHub Actions / SwiftLint

Comment Spacing Violation: Prefer at least one space after slashes for comments (comment_spacing)
// ExpandedVoiceMessageRecordingView(deleteFunc: {
// // unused atm
// }, sendFunc: {
// // unused atm
// }, recordFunc: { _ in
// // unused atm
// }, timeElapsed: 0)
//}

Check warning on line 92 in NextcloudTalk/ExpandedVoiceMessageRecordingView.swift

View workflow job for this annotation

GitHub Actions / SwiftLint

Comment Spacing Violation: Prefer at least one space after slashes for comments (comment_spacing)

0 comments on commit 2572679

Please sign in to comment.