Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Continuous Voice recording #1786

Merged
merged 1 commit into from
Nov 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)
Loading