mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-03 06:55:59 +00:00
Voice messages UI polish (#1959)
* Add VoiceMessageButton * Add VoiceMessageRecordBadge * Make composer transition faster * Use VoiceMessageButton * Adjust recording badge * Update localisation key * Remove animation in VoiceMessageButton * Fix preview tests * Cleanup * Update preview tests
This commit is contained in:
@@ -602,6 +602,7 @@
|
||||
A021827B528F1EDC9101CA58 /* AppCoordinatorProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = FBC776F301D374A3298C69DA /* AppCoordinatorProtocol.swift */; };
|
||||
A0A0D2A9564BDA3FDE2E360F /* FormattedBodyText.swift in Sources */ = {isa = PBXBuildFile; fileRef = F73FF1A33198F5FAE9D34B1F /* FormattedBodyText.swift */; };
|
||||
A0D7E5BD0298A97DCBDCE40B /* Prefire in Frameworks */ = {isa = PBXBuildFile; productRef = 2629CF48B33643CD5F69C612 /* Prefire */; };
|
||||
A0FAB2BA2AE92AB5008F20B3 /* VoiceMessageButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = A0FAB2B92AE92AB5008F20B3 /* VoiceMessageButton.swift */; };
|
||||
A10D6CCDE2010C09EEA1A593 /* HomeScreenRoomList.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7661EFFCAA307A97D71132A /* HomeScreenRoomList.swift */; };
|
||||
A14A9419105A1CD42F0511C4 /* UserIndicatorModalView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E43005941B3A2C9671E23C85 /* UserIndicatorModalView.swift */; };
|
||||
A17FAD2EBC53E17B5FD384DB /* InviteUsersScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 22730A30C50AC2E3D5BA8642 /* InviteUsersScreenViewModelProtocol.swift */; };
|
||||
@@ -1566,6 +1567,7 @@
|
||||
A05707BF550D770168A406DB /* LoginViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginViewModelTests.swift; sourceTree = "<group>"; };
|
||||
A057F2FDC14866C3026A89A4 /* NotificationManagerProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationManagerProtocol.swift; sourceTree = "<group>"; };
|
||||
A0A01AECFF54281CF35909A6 /* MessageComposer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageComposer.swift; sourceTree = "<group>"; };
|
||||
A0FAB2B92AE92AB5008F20B3 /* VoiceMessageButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VoiceMessageButton.swift; sourceTree = "<group>"; };
|
||||
A12D3B1BCF920880CA8BBB6B /* UserIndicatorControllerProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserIndicatorControllerProtocol.swift; sourceTree = "<group>"; };
|
||||
A16CD2C62CB7DB78A4238485 /* ReportContentScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportContentScreenCoordinator.swift; sourceTree = "<group>"; };
|
||||
A1BF12A5E7C76777C4BF0F2B /* TimelineItemMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineItemMenu.swift; sourceTree = "<group>"; };
|
||||
@@ -3857,6 +3859,7 @@
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
AD0FF64B0E6470F66F42E182 /* EstimatedWaveformView.swift */,
|
||||
A0FAB2B92AE92AB5008F20B3 /* VoiceMessageButton.swift */,
|
||||
FB9EABCA9348DFA27439A809 /* WaveformCursorView.swift */,
|
||||
94028A227645FA880B966211 /* WaveformSource.swift */,
|
||||
03BA7958A4BB9C22CA8884EF /* WaveformViewDragGestureModifier.swift */,
|
||||
@@ -5574,6 +5577,7 @@
|
||||
C0090506A52A1991BAF4BA68 /* NotificationSettingsChatType.swift in Sources */,
|
||||
AA93B3F9B5DD097DEF79F981 /* NotificationSettingsEditScreen.swift in Sources */,
|
||||
53A59720F4729D9BBFFB7CAB /* NotificationSettingsEditScreenCoordinator.swift in Sources */,
|
||||
A0FAB2BA2AE92AB5008F20B3 /* VoiceMessageButton.swift in Sources */,
|
||||
4EA1CE0E88EA68E862FF0EA2 /* NotificationSettingsEditScreenModels.swift in Sources */,
|
||||
119AE9A3FC6E0606C1146528 /* NotificationSettingsEditScreenRoomCell.swift in Sources */,
|
||||
D5FE90A6AF5FD5AE91BD37C7 /* NotificationSettingsEditScreenViewModel.swift in Sources */,
|
||||
|
||||
@@ -262,7 +262,7 @@
|
||||
{
|
||||
"identity" : "swiftui-introspect",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/siteline/SwiftUI-Introspect",
|
||||
"location" : "https://github.com/siteline/SwiftUI-Introspect.git",
|
||||
"state" : {
|
||||
"revision" : "b94da693e57eaf79d16464b8b7c90d09cba4e290",
|
||||
"version" : "0.9.2"
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
//
|
||||
// Copyright 2023 New Vector Ltd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct VoiceMessageButton: View {
|
||||
@ScaledMetric private var buttonSize: CGFloat
|
||||
@ScaledMetric private var imageWidth: CGFloat = 12
|
||||
@ScaledMetric private var imageHeight: CGFloat = 14
|
||||
|
||||
enum State {
|
||||
case loading
|
||||
case playing
|
||||
case paused
|
||||
}
|
||||
|
||||
enum Size {
|
||||
case small
|
||||
case medium
|
||||
}
|
||||
|
||||
let state: State
|
||||
let action: () -> Void
|
||||
|
||||
init(state: State, size: Size, action: @escaping () -> Void) {
|
||||
switch size {
|
||||
case .small:
|
||||
_buttonSize = .init(wrappedValue: 30)
|
||||
case .medium:
|
||||
_buttonSize = .init(wrappedValue: 36)
|
||||
}
|
||||
|
||||
self.state = state
|
||||
self.action = action
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
Button(action: action) {
|
||||
buttonLabel
|
||||
.frame(width: buttonSize, height: buttonSize)
|
||||
}
|
||||
.animation(nil, value: state)
|
||||
.buttonStyle(.plain)
|
||||
.disabled(state == .loading)
|
||||
.background(Circle().foregroundColor(.compound.bgCanvasDefault))
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var buttonLabel: some View {
|
||||
switch state {
|
||||
case .loading:
|
||||
ProgressView()
|
||||
case .playing, .paused:
|
||||
let imageAsset = state == .playing ? Asset.Images.mediaPause : Asset.Images.mediaPlay
|
||||
let accessibilityLabel = state == .playing ? L10n.a11yPause : L10n.a11yPlay
|
||||
let offset: CGFloat = state == .playing ? 0 : 2
|
||||
|
||||
Image(asset: imageAsset)
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.frame(width: imageWidth, height: imageHeight)
|
||||
.offset(x: offset)
|
||||
.foregroundColor(.compound.iconSecondary)
|
||||
.accessibilityLabel(accessibilityLabel)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension VoiceMessageButton.State {
|
||||
init(state: AudioPlayerPlaybackState) {
|
||||
switch state {
|
||||
case .loading:
|
||||
self = .loading
|
||||
case .playing:
|
||||
self = .playing
|
||||
default:
|
||||
self = .paused
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct VoiceMessageButton_Previews: PreviewProvider, TestablePreview {
|
||||
static var previews: some View {
|
||||
VStack {
|
||||
HStack {
|
||||
VoiceMessageButton(state: .paused, size: .small, action: { })
|
||||
VoiceMessageButton(state: .paused, size: .medium, action: { })
|
||||
}
|
||||
HStack {
|
||||
VoiceMessageButton(state: .playing, size: .small, action: { })
|
||||
VoiceMessageButton(state: .playing, size: .medium, action: { })
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
.background(Color.gray)
|
||||
}
|
||||
}
|
||||
@@ -85,8 +85,8 @@ struct CompletionSuggestionView: View {
|
||||
}
|
||||
|
||||
func body(content: Content) -> some View {
|
||||
var topPadding: CGFloat = isFirst ? Constants.topPadding : Constants.listItemPadding
|
||||
var bottomPadding: CGFloat = Constants.listItemPadding
|
||||
let topPadding: CGFloat = isFirst ? Constants.topPadding : Constants.listItemPadding
|
||||
let bottomPadding: CGFloat = Constants.listItemPadding
|
||||
|
||||
return content
|
||||
.padding(.top, topPadding)
|
||||
|
||||
@@ -94,7 +94,7 @@ struct ComposerToolbar: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
.animation(.elementDefault, value: context.viewState.composerMode)
|
||||
.animation(.linear(duration: 0.15), value: context.viewState.composerMode)
|
||||
}
|
||||
|
||||
private var bottomBar: some View {
|
||||
|
||||
@@ -29,9 +29,7 @@ struct VoiceMessagePreviewComposer: View {
|
||||
let onPlay: () -> Void
|
||||
let onPause: () -> Void
|
||||
let onSeek: (Double) -> Void
|
||||
|
||||
@ScaledMetric private var playPauseButtonSize = 32
|
||||
@ScaledMetric private var playPauseImagePadding = 8
|
||||
|
||||
@State var dragState: WaveformViewDragState = .inactive
|
||||
|
||||
private static let elapsedTimeFormatter: DateFormatter = {
|
||||
@@ -55,7 +53,9 @@ struct VoiceMessagePreviewComposer: View {
|
||||
var body: some View {
|
||||
HStack {
|
||||
HStack {
|
||||
playPauseButton
|
||||
VoiceMessageButton(state: .init(state: playerState.playbackState),
|
||||
size: .small,
|
||||
action: onPlayPause)
|
||||
Text(timeLabelContent)
|
||||
.lineLimit(1)
|
||||
.font(.compound.bodySMSemibold)
|
||||
@@ -112,32 +112,6 @@ struct VoiceMessagePreviewComposer: View {
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var playPauseButton: some View {
|
||||
Button {
|
||||
onPlayPause()
|
||||
} label: {
|
||||
ZStack {
|
||||
Circle()
|
||||
.foregroundColor(.compound.bgCanvasDefault)
|
||||
if playerState.playbackState == .loading {
|
||||
ProgressView()
|
||||
} else {
|
||||
Image(asset: playerState.playbackState == .playing ? Asset.Images.mediaPause : Asset.Images.mediaPlay)
|
||||
.resizable()
|
||||
.padding(playPauseImagePadding)
|
||||
.offset(x: playerState.playbackState == .playing ? 0 : 2)
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.foregroundColor(.compound.iconSecondary)
|
||||
.accessibilityLabel(playerState.playbackState == .playing ? L10n.a11yPause : L10n.a11yPlay)
|
||||
}
|
||||
}
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.disabled(playerState.playbackState == .loading)
|
||||
.frame(width: playPauseButtonSize, height: playPauseButtonSize)
|
||||
}
|
||||
|
||||
private func onPlayPause() {
|
||||
if playerState.playbackState == .playing {
|
||||
onPause()
|
||||
|
||||
@@ -43,9 +43,9 @@ struct VoiceMessageRecordingView: View {
|
||||
|
||||
var body: some View {
|
||||
HStack {
|
||||
Circle()
|
||||
VoiceMessageRecordingBadge()
|
||||
.frame(width: recordingIndicatorSize, height: recordingIndicatorSize)
|
||||
.foregroundColor(.red)
|
||||
|
||||
Text(timeLabelContent)
|
||||
.lineLimit(1)
|
||||
.font(.compound.bodySMSemibold)
|
||||
@@ -61,6 +61,21 @@ struct VoiceMessageRecordingView: View {
|
||||
}
|
||||
}
|
||||
|
||||
private struct VoiceMessageRecordingBadge: View {
|
||||
@State private var opacity: CGFloat = 0
|
||||
|
||||
var body: some View {
|
||||
Circle()
|
||||
.foregroundColor(.red)
|
||||
.opacity(opacity)
|
||||
.onAppear {
|
||||
withElementAnimation(.easeOut(duration: 1).repeatForever(autoreverses: true)) {
|
||||
opacity = 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct VoiceMessageRecordingView_Previews: PreviewProvider, TestablePreview {
|
||||
static let recorderState = AudioRecorderState()
|
||||
|
||||
|
||||
@@ -48,7 +48,7 @@ class StaticLocationScreenViewModel: StaticLocationScreenViewModelType, StaticLo
|
||||
let action: () -> Void = { [weak self] in self?.actionsSubject.send(.openSystemSettings) }
|
||||
state.bindings.alertInfo = .init(locationSharingViewError: .missingAuthorization,
|
||||
primaryButton: .init(title: L10n.actionNotNow, role: .cancel, action: nil),
|
||||
secondaryButton: .init(title: L10n.actionOpenSettings, action: action))
|
||||
secondaryButton: .init(title: L10n.commonSettings, action: action))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -962,7 +962,7 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
|
||||
state.bindings.confirmationAlertInfo = .init(id: .init(),
|
||||
title: L10n.dialogPermissionMicrophoneTitleIos(InfoPlistReader.main.bundleDisplayName),
|
||||
message: L10n.dialogPermissionMicrophoneDescriptionIos,
|
||||
primaryButton: .init(title: L10n.actionOpenSettings, action: { [weak self] in self?.openSystemSettings() }),
|
||||
primaryButton: .init(title: L10n.commonSettings, action: { [weak self] in self?.openSystemSettings() }),
|
||||
secondaryButton: .init(title: L10n.actionNotNow, role: .cancel, action: nil))
|
||||
default:
|
||||
MXLog.error("failed to start voice message recording: \(error)")
|
||||
|
||||
+3
-30
@@ -29,9 +29,6 @@ struct VoiceMessageRoomPlaybackView: View {
|
||||
|
||||
private let feedbackGenerator = UIImpactFeedbackGenerator(style: .heavy)
|
||||
@State private var sendFeedback = false
|
||||
|
||||
@ScaledMetric private var playPauseButtonSize = 32
|
||||
@ScaledMetric private var playPauseImagePadding = 8
|
||||
|
||||
private static let elapsedTimeFormatter: DateFormatter = {
|
||||
let dateFormatter = DateFormatter()
|
||||
@@ -66,7 +63,9 @@ struct VoiceMessageRoomPlaybackView: View {
|
||||
var body: some View {
|
||||
HStack {
|
||||
HStack {
|
||||
playPauseButton
|
||||
VoiceMessageButton(state: .init(state: playerState.playbackState),
|
||||
size: .medium,
|
||||
action: onPlayPause)
|
||||
Text(timeLabelContent)
|
||||
.lineLimit(1)
|
||||
.font(.compound.bodySMSemibold)
|
||||
@@ -101,32 +100,6 @@ struct VoiceMessageRoomPlaybackView: View {
|
||||
.padding(.leading, 2)
|
||||
.padding(.trailing, 8)
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
var playPauseButton: some View {
|
||||
Button {
|
||||
onPlayPause()
|
||||
} label: {
|
||||
ZStack {
|
||||
Circle()
|
||||
.foregroundColor(.compound.bgCanvasDefault)
|
||||
if playerState.playbackState == .loading {
|
||||
ProgressView()
|
||||
} else {
|
||||
Image(asset: playerState.playbackState == .playing ? Asset.Images.mediaPause : Asset.Images.mediaPlay)
|
||||
.resizable()
|
||||
.padding(playPauseImagePadding)
|
||||
.offset(x: playerState.playbackState == .playing ? 0 : 2)
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.foregroundColor(.compound.iconSecondary)
|
||||
.accessibilityLabel(playerState.playbackState == .playing ? L10n.a11yPause : L10n.a11yPlay)
|
||||
}
|
||||
}
|
||||
}
|
||||
.disabled(playerState.playbackState == .loading)
|
||||
.frame(width: playPauseButtonSize,
|
||||
height: playPauseButtonSize)
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var waveformView: some View {
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:4a5e4b21a9286f303e23fc85ac6ea0f108a37be0a22b02fd944dfe004de93e99
|
||||
size 93494
|
||||
oid sha256:09e3015e6bc13ff47decd46943fdee10c9c4b79d1361143f6d35e48c0822e5f8
|
||||
size 93714
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:1a1b3d820749df64b0d88ea7b04d95cfdd36745e0ba583fa6ad59f19fdca382b
|
||||
size 67186
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:e94b55edca09448dfa1309331d238f029a86b40061bad197ef8cd5c6d421a1b4
|
||||
size 62636
|
||||
oid sha256:22f419d23e9c8df9325e84bc44e0d706a88b08335a6a5e9bbc8be3deef08d961
|
||||
size 61935
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:f08e9a60a5e93a67714b869c77e16abd6252e96515aec9951c2fac8a206950e1
|
||||
size 61376
|
||||
oid sha256:be68924b6f15ba61c3e9e18a942c724df55f6656146a3f933a74f2f0880568bf
|
||||
size 61158
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:cced4619918ea4c66bddbc0c3b957b6239c2b11a4b40b41c08a0f4d84da1d944
|
||||
size 70256
|
||||
oid sha256:c9bbb2a5218b2055e27ed7ab7770acd0e8e26973505bd28f979cfc43bffd0a64
|
||||
size 71007
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:6086d159d868c2e0e9d80e19453c24fca783384864b9e6660c785cd4fd7c0986
|
||||
size 66757
|
||||
oid sha256:60cce5e7f560c36f75ae5737736ab539be31bbf094172aae9d9034f3f4fb4786
|
||||
size 67320
|
||||
|
||||
Reference in New Issue
Block a user