Voice message record button hapitcs (#2013)

* Move tooltip logics in VoiceMessageRecordingButton

* Add hapitc feedback when recording a voice message

* Cleanup
This commit is contained in:
Alfonso Grillo
2023-11-02 12:58:19 +01:00
committed by GitHub
parent b97dba43d8
commit 2e090e38da
4 changed files with 73 additions and 65 deletions

View File

@@ -42,3 +42,9 @@ struct ViewFrameReader: View {
}
}
}
extension View {
func readFrame(_ frame: Binding<CGRect>, in coordinateSpace: CoordinateSpace = .local) -> some View {
background(ViewFrameReader(frame: frame, coordinateSpace: coordinateSpace))
}
}

View File

@@ -27,15 +27,6 @@ struct ComposerToolbar: View {
@ScaledMetric private var sendButtonIconSize = 16
@ScaledMetric(relativeTo: .title) private var spinnerSize = 44
@ScaledMetric(relativeTo: .title) private var closeRTEButtonSize = 30
@State private var voiceMessageRecordingStartTime: Date?
@State private var showVoiceMessageRecordingTooltip = false
@ScaledMetric private var voiceMessageTooltipPointerHeight = 6
@State private var voiceMessageRecordingButtonFrame: CGRect = .zero
private let voiceMessageMinimumRecordingDuration = 1.0
private let voiceMessageTooltipDuration = 1.0
@State private var frame: CGRect = .zero
@Environment(\.verticalSizeClass) private var verticalSizeClass
@@ -64,12 +55,6 @@ struct ComposerToolbar: View {
.offset(y: -frame.height)
}
}
.overlay(alignment: .bottomTrailing) {
if showVoiceMessageRecordingTooltip {
voiceMessageRecordingButtonTooltipView
.offset(y: -frame.height - voiceMessageTooltipPointerHeight)
}
}
.alert(item: $context.alertInfo)
}
@@ -84,7 +69,7 @@ struct ComposerToolbar: View {
private var topBar: some View {
topBarLayout {
mainTopBarContent
if !context.composerActionsEnabled {
if context.viewState.isUploading {
ProgressView()
@@ -95,9 +80,6 @@ struct ComposerToolbar: View {
.padding(.leading, 3)
} else {
voiceMessageRecordingButton
.background {
ViewFrameReader(frame: $voiceMessageRecordingButtonFrame, coordinateSpace: .global)
}
.padding(.leading, 4)
}
}
@@ -121,7 +103,7 @@ struct ComposerToolbar: View {
private var topBarLayout: some Layout {
HStackLayout(alignment: .bottom, spacing: 5)
}
@ViewBuilder
private var mainTopBarContent: some View {
ZStack(alignment: .bottom) {
@@ -139,13 +121,13 @@ struct ComposerToolbar: View {
.padding(.trailing, context.composerActionsEnabled ? 4 : 0)
}
.opacity(context.viewState.isVoiceMessageModeActivated ? 0 : 1)
if context.viewState.isVoiceMessageModeActivated {
voiceMessageContent
}
}
}
private var closeRTEButton: some View {
Button {
context.composerActionsEnabled = false
@@ -249,7 +231,7 @@ struct ComposerToolbar: View {
}
// MARK: - Voice message
@ViewBuilder
private var voiceMessageContent: some View {
// Display the voice message composer above to keep the focus and keep the keyboard open if it's already open.
@@ -270,22 +252,17 @@ struct ComposerToolbar: View {
private var voiceMessageRecordingButton: some View {
VoiceMessageRecordingButton {
showVoiceMessageRecordingTooltip = false
voiceMessageRecordingStartTime = Date.now
context.send(viewAction: .startVoiceMessageRecording)
} stopRecording: {
if let voiceMessageRecordingStartTime, Date.now.timeIntervalSince(voiceMessageRecordingStartTime) < voiceMessageMinimumRecordingDuration {
context.send(viewAction: .cancelVoiceMessageRecording)
withElementAnimation {
showVoiceMessageRecordingTooltip = true
}
} else {
} stopRecording: { minimumRecordTimeReached in
if minimumRecordTimeReached {
context.send(viewAction: .stopVoiceMessageRecording)
} else {
context.send(viewAction: .cancelVoiceMessageRecording)
}
}
.padding(4)
}
private var voiceMessageTrashButton: some View {
Button(role: .destructive) {
context.send(viewAction: .deleteVoiceMessageRecording)
@@ -297,22 +274,6 @@ struct ComposerToolbar: View {
.accessibilityLabel(L10n.a11yDelete)
}
private var voiceMessageRecordingButtonTooltipView: some View {
VoiceMessageRecordingButtonTooltipView(text: L10n.screenRoomVoiceMessageTooltip,
pointerHeight: voiceMessageTooltipPointerHeight,
pointerLocation: voiceMessageRecordingButtonFrame.midX,
pointerLocationCoordinateSpace: .global)
.allowsHitTesting(false)
.onAppear {
DispatchQueue.main.asyncAfter(deadline: .now() + voiceMessageTooltipDuration) {
withElementAnimation {
showVoiceMessageRecordingTooltip = false
}
}
}
.padding(.horizontal, 8)
}
private func voiceMessagePreviewComposer(audioPlayerState: AudioPlayerState, waveform: WaveformSource) -> some View {
VoiceMessagePreviewComposer(playerState: audioPlayerState, waveform: waveform) {
context.send(viewAction: .startVoiceMessagePlayback)
@@ -376,7 +337,7 @@ extension ComposerToolbar {
wysiwygViewModel: wysiwygViewModel,
keyCommandHandler: { _ in false })
}
static func textWithVoiceMessage(focused: Bool = true) -> ComposerToolbar {
let wysiwygViewModel = WysiwygComposerViewModel()
var composerViewModel: ComposerToolbarViewModel {

View File

@@ -18,33 +18,72 @@ import Compound
import SwiftUI
struct VoiceMessageRecordingButton: View {
@State private var buttonPressed = false
var startRecording: (() -> Void)?
var stopRecording: (() -> Void)?
var stopRecording: ((_ minimumRecordTimeReached: Bool) -> Void)?
@ScaledMetric private var tooltipPointerHeight = 6
@State private var buttonPressed = false
@State private var recordingStartTime: Date?
@State private var showTooltip = false
@State private var frame: CGRect = .zero
private let minimumRecordingDuration = 1.0
private let tooltipDuration = 1.0
private let impactFeedbackGenerator = UIImpactFeedbackGenerator()
private let notificationFeedbackGenerator = UINotificationFeedbackGenerator()
var body: some View {
Button { } label: {
voiceMessageButtonImage
CompoundIcon(buttonPressed ? \.micOnSolid : \.micOnOutline)
.foregroundColor(.compound.iconSecondary)
.padding(EdgeInsets(top: 6, leading: 6, bottom: 6, trailing: 6))
}
.readFrame($frame, in: .global)
.accessibilityLabel(L10n.a11yVoiceMessageRecord)
.onLongPressGesture { } onPressingChanged: { isPressing in
buttonPressed = isPressing
if isPressing {
// Start recording
showTooltip = false
recordingStartTime = Date.now
impactFeedbackGenerator.impactOccurred()
startRecording?()
} else {
// Stop recording
stopRecording?()
if let recordingStartTime, Date.now.timeIntervalSince(recordingStartTime) < minimumRecordingDuration {
withElementAnimation {
showTooltip = true
}
notificationFeedbackGenerator.notificationOccurred(.error)
stopRecording?(false)
} else {
impactFeedbackGenerator.impactOccurred()
stopRecording?(true)
}
}
}
.overlay(alignment: .bottomTrailing) {
if showTooltip {
tooltipView
.offset(y: -frame.height - tooltipPointerHeight)
}
}
}
@ViewBuilder
private var voiceMessageButtonImage: some View {
CompoundIcon(buttonPressed ? \.micOnSolid : \.micOnOutline)
.foregroundColor(.compound.iconSecondary)
.padding(EdgeInsets(top: 6, leading: 6, bottom: 6, trailing: 6))
.accessibilityLabel(L10n.a11yVoiceMessageRecord)
private var tooltipView: some View {
VoiceMessageRecordingButtonTooltipView(text: L10n.screenRoomVoiceMessageTooltip,
pointerHeight: tooltipPointerHeight,
pointerLocation: frame.midX,
pointerLocationCoordinateSpace: .global)
.allowsHitTesting(false)
.fixedSize()
.onAppear {
DispatchQueue.main.asyncAfter(deadline: .now() + tooltipDuration) {
withElementAnimation {
showTooltip = false
}
}
}
}
}

View File

@@ -141,8 +141,10 @@ class AudioRecorder: NSObject, AudioRecorderProtocol, AVAudioRecorderDelegate {
AVEncoderAudioQualityKey: AVAudioQuality.high.rawValue]
do {
try AVAudioSession.sharedInstance().setCategory(.playAndRecord, mode: .default)
try AVAudioSession.sharedInstance().setActive(true)
let audioSession = AVAudioSession.sharedInstance()
try audioSession.setAllowHapticsAndSystemSoundsDuringRecording(true)
try audioSession.setCategory(.playAndRecord, mode: .default)
try audioSession.setActive(true)
let url = URL.temporaryDirectory.appendingPathComponent("voice-message-\(recordID.identifier).m4a")
let audioRecorder = try AVAudioRecorder(url: url, settings: settings)
audioRecorder.delegate = self