Improve SoundEffectManager & HapticManager (#1662)

* Remove unnecessary vars and switches

* Improve SoundEffectManager call-site API

* Improve HapticManager call-site API
This commit is contained in:
Théo Arrouye 2023-11-07 02:22:36 -08:00 committed by GitHub
parent 6e1e83cace
commit 4266ac4b42
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 75 additions and 105 deletions

View file

@ -14,8 +14,8 @@ extension IceCubesApp {
} }
} }
HapticManager.shared.fireHaptic(of: .tabSelection) HapticManager.shared.fireHaptic(.tabSelection)
SoundEffectManager.shared.playSound(of: .tabSelection) SoundEffectManager.shared.playSound(.tabSelection)
selectedTab = newTab selectedTab = newTab

View file

@ -75,7 +75,7 @@ struct SideBarView<Content: View>: View {
Button { Button {
if account.id == appAccounts.currentAccount.id { if account.id == appAccounts.currentAccount.id {
selectedTab = .profile selectedTab = .profile
SoundEffectManager.shared.playSound(of: .tabSelection) SoundEffectManager.shared.playSound(.tabSelection)
} else { } else {
var transation = Transaction() var transation = Transaction()
transation.disablesAnimations = true transation.disablesAnimations = true
@ -114,7 +114,7 @@ struct SideBarView<Content: View>: View {
} }
} }
selectedTab = tab selectedTab = tab
SoundEffectManager.shared.playSound(of: .tabSelection) SoundEffectManager.shared.playSound(.tabSelection)
if tab == .notifications { if tab == .notifications {
if let token = appAccounts.currentAccount.oauthToken { if let token = appAccounts.currentAccount.oauthToken {
userPreferences.notificationsCount[token] = 0 userPreferences.notificationsCount[token] = 0

View file

@ -118,12 +118,12 @@ public struct AccountDetailView: View {
} }
.refreshable { .refreshable {
Task { Task {
SoundEffectManager.shared.playSound(of: .pull) SoundEffectManager.shared.playSound(.pull)
HapticManager.shared.fireHaptic(of: .dataRefresh(intensity: 0.3)) HapticManager.shared.fireHaptic(.dataRefresh(intensity: 0.3))
await viewModel.fetchAccount() await viewModel.fetchAccount()
await viewModel.fetchNewestStatuses() await viewModel.fetchNewestStatuses()
HapticManager.shared.fireHaptic(of: .dataRefresh(intensity: 0.7)) HapticManager.shared.fireHaptic(.dataRefresh(intensity: 0.7))
SoundEffectManager.shared.playSound(of: .refresh) SoundEffectManager.shared.playSound(.refresh)
} }
} }
.onChange(of: watcher.latestEvent?.id) { .onChange(of: watcher.latestEvent?.id) {

View file

@ -48,13 +48,13 @@ public struct AppAccountView: View {
let account = viewModel.account let account = viewModel.account
{ {
routerPath.navigate(to: .accountSettingsWithAccount(account: account, appAccount: viewModel.appAccount)) routerPath.navigate(to: .accountSettingsWithAccount(account: account, appAccount: viewModel.appAccount))
HapticManager.shared.fireHaptic(of: .buttonPress) HapticManager.shared.fireHaptic(.buttonPress)
} else { } else {
var transation = Transaction() var transation = Transaction()
transation.disablesAnimations = true transation.disablesAnimations = true
withTransaction(transation) { withTransaction(transation) {
appAccounts.currentAccount = viewModel.appAccount appAccounts.currentAccount = viewModel.appAccount
HapticManager.shared.fireHaptic(of: .notification(.success)) HapticManager.shared.fireHaptic(.notification(.success))
} }
} }
} label: { } label: {

View file

@ -43,7 +43,7 @@ public struct AppAccountsSelectorView: View {
public var body: some View { public var body: some View {
Button { Button {
isPresented.toggle() isPresented.toggle()
HapticManager.shared.fireHaptic(of: .buttonPress) HapticManager.shared.fireHaptic(.buttonPress)
} label: { } label: {
labelView labelView
} }
@ -109,7 +109,7 @@ public struct AppAccountsSelectorView: View {
Section { Section {
Button { Button {
isPresented = false isPresented = false
HapticManager.shared.fireHaptic(of: .buttonPress) HapticManager.shared.fireHaptic(.buttonPress)
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
routerPath.presentedSheet = .addAccount routerPath.presentedSheet = .addAccount
} }
@ -142,7 +142,7 @@ public struct AppAccountsSelectorView: View {
private var settingsButton: some View { private var settingsButton: some View {
Button { Button {
isPresented = false isPresented = false
HapticManager.shared.fireHaptic(of: .buttonPress) HapticManager.shared.fireHaptic(.buttonPress)
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
routerPath.presentedSheet = .settings routerPath.presentedSheet = .settings
} }

View file

@ -81,7 +81,7 @@ struct ConversationsListRow: View {
} }
.accessibilityAction(.magicTap) { .accessibilityAction(.magicTap) {
if let lastStatus = conversation.lastStatus { if let lastStatus = conversation.lastStatus {
HapticManager.shared.fireHaptic(of: .notification(.success)) HapticManager.shared.fireHaptic(.notification(.success))
routerPath.presentedSheet = .replyToStatusEditor(status: lastStatus) routerPath.presentedSheet = .replyToStatusEditor(status: lastStatus)
} }
} }
@ -182,7 +182,7 @@ struct ConversationsListRow: View {
var replyAction: some View { var replyAction: some View {
if let lastStatus = conversation.lastStatus { if let lastStatus = conversation.lastStatus {
Button("status.action.reply") { Button("status.action.reply") {
HapticManager.shared.fireHaptic(of: .notification(.success)) HapticManager.shared.fireHaptic(.notification(.success))
routerPath.presentedSheet = .replyToStatusEditor(status: lastStatus) routerPath.presentedSheet = .replyToStatusEditor(status: lastStatus)
} }
} else { } else {
@ -195,7 +195,7 @@ struct ConversationsListRow: View {
if let lastStatus = conversation.lastStatus { if let lastStatus = conversation.lastStatus {
if lastStatus.account.id != currentAccount.account?.id { if lastStatus.account.id != currentAccount.account?.id {
Button("@\(lastStatus.account.username)") { Button("@\(lastStatus.account.username)") {
HapticManager.shared.fireHaptic(of: .notification(.success)) HapticManager.shared.fireHaptic(.notification(.success))
routerPath.navigate(to: .accountDetail(id: lastStatus.account.id)) routerPath.navigate(to: .accountDetail(id: lastStatus.account.id))
} }
} }
@ -205,18 +205,18 @@ struct ConversationsListRow: View {
case .url: case .url:
if UIApplication.shared.canOpenURL(link.url) { if UIApplication.shared.canOpenURL(link.url) {
Button("accessibility.tabs.timeline.content-link-\(link.title)") { Button("accessibility.tabs.timeline.content-link-\(link.title)") {
HapticManager.shared.fireHaptic(of: .notification(.success)) HapticManager.shared.fireHaptic(.notification(.success))
_ = routerPath.handle(url: link.url) _ = routerPath.handle(url: link.url)
} }
} }
case .hashtag: case .hashtag:
Button("accessibility.tabs.timeline.content-hashtag-\(link.title)") { Button("accessibility.tabs.timeline.content-hashtag-\(link.title)") {
HapticManager.shared.fireHaptic(of: .notification(.success)) HapticManager.shared.fireHaptic(.notification(.success))
_ = routerPath.handle(url: link.url) _ = routerPath.handle(url: link.url)
} }
case .mention: case .mention:
Button("\(link.title)") { Button("\(link.title)") {
HapticManager.shared.fireHaptic(of: .notification(.success)) HapticManager.shared.fireHaptic(.notification(.success))
_ = routerPath.handle(url: link.url) _ = routerPath.handle(url: link.url)
} }
} }

View file

@ -105,11 +105,11 @@ public struct ConversationsListView: View {
// note: this Task wrapper should not be necessary, but it reportedly crashes without it // note: this Task wrapper should not be necessary, but it reportedly crashes without it
// when refreshing on an empty list // when refreshing on an empty list
Task { Task {
SoundEffectManager.shared.playSound(of: .pull) SoundEffectManager.shared.playSound(.pull)
HapticManager.shared.fireHaptic(of: .dataRefresh(intensity: 0.3)) HapticManager.shared.fireHaptic(.dataRefresh(intensity: 0.3))
await viewModel.fetchConversations() await viewModel.fetchConversations()
HapticManager.shared.fireHaptic(of: .dataRefresh(intensity: 0.7)) HapticManager.shared.fireHaptic(.dataRefresh(intensity: 0.7))
SoundEffectManager.shared.playSound(of: .refresh) SoundEffectManager.shared.playSound(.refresh)
} }
} }
.onAppear { .onAppear {

View file

@ -30,7 +30,7 @@ public struct StatusEditorToolbarItem: ToolbarContent {
openWindow(value: WindowDestination.newStatusEditor(visibility: visibility)) openWindow(value: WindowDestination.newStatusEditor(visibility: visibility))
} else { } else {
routerPath.presentedSheet = .newStatusEditor(visibility: visibility) routerPath.presentedSheet = .newStatusEditor(visibility: visibility)
HapticManager.shared.fireHaptic(of: .buttonPress) HapticManager.shared.fireHaptic(.buttonPress)
} }
} }
} label: { } label: {

View file

@ -25,7 +25,7 @@ public class HapticManager {
} }
@MainActor @MainActor
public func fireHaptic(of type: HapticType) { public func fireHaptic(_ type: HapticType) {
guard supportsHaptics else { return } guard supportsHaptics else { return }
switch type { switch type {

View file

@ -11,14 +11,7 @@ public class SoundEffectManager {
case pull, refresh, tootSent, tabSelection, bookmark, boost, favorite, share case pull, refresh, tootSent, tabSelection, bookmark, boost, favorite, share
} }
var pullId: SystemSoundID = 0 private var systemSoundIDs: [SoundEffect: SystemSoundID] = [:]
var refreshId: SystemSoundID = 1
var tootSentId: SystemSoundID = 2
var tabSelectionId: SystemSoundID = 3
var bookmarkId: SystemSoundID = 4
var boostId: SystemSoundID = 5
var favoriteId: SystemSoundID = 6
var shareId: SystemSoundID = 7
private let userPreferences = UserPreferences.shared private let userPreferences = UserPreferences.shared
@ -27,49 +20,26 @@ public class SoundEffectManager {
} }
private func registerSounds() { private func registerSounds() {
for effect in SoundEffect.allCases { SoundEffect.allCases.forEach { effect in
if let url = Bundle.main.url(forResource: effect.rawValue, withExtension: "wav") { guard let url = Bundle.main.url(forResource: effect.rawValue, withExtension: "wav") else { return }
switch effect { register(url: url, for: effect)
case .pull:
AudioServicesCreateSystemSoundID(url as CFURL, &pullId)
case .refresh:
AudioServicesCreateSystemSoundID(url as CFURL, &refreshId)
case .tootSent:
AudioServicesCreateSystemSoundID(url as CFURL, &tootSentId)
case .tabSelection:
AudioServicesCreateSystemSoundID(url as CFURL, &tabSelectionId)
case .bookmark:
AudioServicesCreateSystemSoundID(url as CFURL, &bookmarkId)
case .boost:
AudioServicesCreateSystemSoundID(url as CFURL, &boostId)
case .favorite:
AudioServicesCreateSystemSoundID(url as CFURL, &favoriteId)
case .share:
AudioServicesCreateSystemSoundID(url as CFURL, &shareId)
}
}
} }
} }
public func playSound(of type: SoundEffect) { private func register(url: URL, for effect: SoundEffect) {
guard userPreferences.soundEffectEnabled else { return } var soundId: SystemSoundID = .init()
switch type { AudioServicesCreateSystemSoundID(url as CFURL, &soundId)
case .pull: systemSoundIDs[effect] = soundId
AudioServicesPlaySystemSound(pullId) }
case .refresh:
AudioServicesPlaySystemSound(refreshId) public func playSound(_ effect: SoundEffect) {
case .tootSent: guard
AudioServicesPlaySystemSound(tootSentId) userPreferences.soundEffectEnabled,
case .tabSelection: let soundId = systemSoundIDs[effect]
AudioServicesPlaySystemSound(tabSelectionId) else {
case .bookmark: return
AudioServicesPlaySystemSound(bookmarkId)
case .boost:
AudioServicesPlaySystemSound(boostId)
case .favorite:
AudioServicesPlaySystemSound(favoriteId)
case .share:
AudioServicesPlaySystemSound(shareId)
} }
AudioServicesPlaySystemSound(soundId)
} }
} }

View file

@ -82,11 +82,11 @@ public struct ExploreView: View {
} }
.refreshable { .refreshable {
Task { Task {
SoundEffectManager.shared.playSound(of: .pull) SoundEffectManager.shared.playSound(.pull)
HapticManager.shared.fireHaptic(of: .dataRefresh(intensity: 0.3)) HapticManager.shared.fireHaptic(.dataRefresh(intensity: 0.3))
await viewModel.fetchTrending() await viewModel.fetchTrending()
HapticManager.shared.fireHaptic(of: .dataRefresh(intensity: 0.7)) HapticManager.shared.fireHaptic(.dataRefresh(intensity: 0.7))
SoundEffectManager.shared.playSound(of: .refresh) SoundEffectManager.shared.playSound(.refresh)
} }
} }
.listStyle(.grouped) .listStyle(.grouped)

View file

@ -203,7 +203,7 @@ struct NotificationRowView: View {
private var accessibilityUserActions: some View { private var accessibilityUserActions: some View {
ForEach(notification.accounts) { account in ForEach(notification.accounts) { account in
Button("@\(account.username)") { Button("@\(account.username)") {
HapticManager.shared.fireHaptic(of: .notification(.success)) HapticManager.shared.fireHaptic(.notification(.success))
routerPath.navigate(to: .accountDetail(id: account.id)) routerPath.navigate(to: .accountDetail(id: account.id))
} }
} }

View file

@ -93,11 +93,11 @@ public struct NotificationsListView: View {
await viewModel.fetchNotifications() await viewModel.fetchNotifications()
} }
.refreshable { .refreshable {
SoundEffectManager.shared.playSound(of: .pull) SoundEffectManager.shared.playSound(.pull)
HapticManager.shared.fireHaptic(of: .dataRefresh(intensity: 0.3)) HapticManager.shared.fireHaptic(.dataRefresh(intensity: 0.3))
await viewModel.fetchNotifications() await viewModel.fetchNotifications()
HapticManager.shared.fireHaptic(of: .dataRefresh(intensity: 0.7)) HapticManager.shared.fireHaptic(.dataRefresh(intensity: 0.7))
SoundEffectManager.shared.playSound(of: .refresh) SoundEffectManager.shared.playSound(.refresh)
} }
.onChange(of: watcher.latestEvent?.id) { .onChange(of: watcher.latestEvent?.id) {
if let latestEvent = watcher.latestEvent { if let latestEvent = watcher.latestEvent {

View file

@ -213,7 +213,7 @@ public struct StatusEditorView: View {
let status = await viewModel.postStatus() let status = await viewModel.postStatus()
if status != nil { if status != nil {
close() close()
SoundEffectManager.shared.playSound(of: .tootSent) SoundEffectManager.shared.playSound(.tootSent)
NotificationCenter.default.post(name: .shareSheetClose, NotificationCenter.default.post(name: .shareSheetClose,
object: nil) object: nil)
if !viewModel.mode.isInShareExtension, !preferences.requestedReview, !ProcessInfo.processInfo.isMacCatalystApp { if !viewModel.mode.isInShareExtension, !preferences.requestedReview, !ProcessInfo.processInfo.isMacCatalystApp {

View file

@ -192,7 +192,7 @@ import SwiftUI
case let .edit(status): case let .edit(status):
postStatus = try await client.put(endpoint: Statuses.editStatus(id: status.id, json: data)) postStatus = try await client.put(endpoint: Statuses.editStatus(id: status.id, json: data))
} }
HapticManager.shared.fireHaptic(of: .notification(.success)) HapticManager.shared.fireHaptic(.notification(.success))
if hasExplicitlySelectedLanguage, let selectedLanguage { if hasExplicitlySelectedLanguage, let selectedLanguage {
preferences?.markLanguageAsSelected(isoCode: selectedLanguage) preferences?.markLanguageAsSelected(isoCode: selectedLanguage)
} }
@ -204,7 +204,7 @@ import SwiftUI
showPostingErrorAlert = true showPostingErrorAlert = true
} }
isPosting = false isPosting = false
HapticManager.shared.fireHaptic(of: .notification(.error)) HapticManager.shared.fireHaptic(.notification(.error))
return nil return nil
} }
} }

View file

@ -195,19 +195,19 @@ public struct StatusRowView: View {
private var accessibilityActions: some View { private var accessibilityActions: some View {
// Add reply and quote, which are lost when the swipe actions are removed // Add reply and quote, which are lost when the swipe actions are removed
Button("status.action.reply") { Button("status.action.reply") {
HapticManager.shared.fireHaptic(of: .notification(.success)) HapticManager.shared.fireHaptic(.notification(.success))
viewModel.routerPath.presentedSheet = .replyToStatusEditor(status: viewModel.status) viewModel.routerPath.presentedSheet = .replyToStatusEditor(status: viewModel.status)
} }
Button("settings.swipeactions.status.action.quote") { Button("settings.swipeactions.status.action.quote") {
HapticManager.shared.fireHaptic(of: .notification(.success)) HapticManager.shared.fireHaptic(.notification(.success))
viewModel.routerPath.presentedSheet = .quoteStatusEditor(status: viewModel.status) viewModel.routerPath.presentedSheet = .quoteStatusEditor(status: viewModel.status)
} }
.disabled(viewModel.status.visibility == .direct || viewModel.status.visibility == .priv) .disabled(viewModel.status.visibility == .direct || viewModel.status.visibility == .priv)
if viewModel.finalStatus.mediaAttachments.isEmpty == false { if viewModel.finalStatus.mediaAttachments.isEmpty == false {
Button("accessibility.status.media-viewer-action.label") { Button("accessibility.status.media-viewer-action.label") {
HapticManager.shared.fireHaptic(of: .notification(.success)) HapticManager.shared.fireHaptic(.notification(.success))
let attachments = viewModel.finalStatus.mediaAttachments let attachments = viewModel.finalStatus.mediaAttachments
if ProcessInfo.processInfo.isMacCatalystApp { if ProcessInfo.processInfo.isMacCatalystApp {
openWindow(value: WindowDestination.mediaViewer(attachments: attachments, openWindow(value: WindowDestination.mediaViewer(attachments: attachments,
@ -225,14 +225,14 @@ public struct StatusRowView: View {
} }
Button("@\(viewModel.status.account.username)") { Button("@\(viewModel.status.account.username)") {
HapticManager.shared.fireHaptic(of: .notification(.success)) HapticManager.shared.fireHaptic(.notification(.success))
viewModel.routerPath.navigate(to: .accountDetail(id: viewModel.status.account.id)) viewModel.routerPath.navigate(to: .accountDetail(id: viewModel.status.account.id))
} }
// Add a reference to the post creator // Add a reference to the post creator
if viewModel.status.account != viewModel.finalStatus.account { if viewModel.status.account != viewModel.finalStatus.account {
Button("@\(viewModel.finalStatus.account.username)") { Button("@\(viewModel.finalStatus.account.username)") {
HapticManager.shared.fireHaptic(of: .notification(.success)) HapticManager.shared.fireHaptic(.notification(.success))
viewModel.routerPath.navigate(to: .accountDetail(id: viewModel.finalStatus.account.id)) viewModel.routerPath.navigate(to: .accountDetail(id: viewModel.finalStatus.account.id))
} }
} }
@ -243,18 +243,18 @@ public struct StatusRowView: View {
case .url: case .url:
if UIApplication.shared.canOpenURL(link.url) { if UIApplication.shared.canOpenURL(link.url) {
Button("accessibility.tabs.timeline.content-link-\(link.title)") { Button("accessibility.tabs.timeline.content-link-\(link.title)") {
HapticManager.shared.fireHaptic(of: .notification(.success)) HapticManager.shared.fireHaptic(.notification(.success))
_ = viewModel.routerPath.handle(url: link.url) _ = viewModel.routerPath.handle(url: link.url)
} }
} }
case .hashtag: case .hashtag:
Button("accessibility.tabs.timeline.content-hashtag-\(link.title)") { Button("accessibility.tabs.timeline.content-hashtag-\(link.title)") {
HapticManager.shared.fireHaptic(of: .notification(.success)) HapticManager.shared.fireHaptic(.notification(.success))
_ = viewModel.routerPath.handle(url: link.url) _ = viewModel.routerPath.handle(url: link.url)
} }
case .mention: case .mention:
Button("\(link.title)") { Button("\(link.title)") {
HapticManager.shared.fireHaptic(of: .notification(.success)) HapticManager.shared.fireHaptic(.notification(.success))
_ = viewModel.routerPath.handle(url: link.url) _ = viewModel.routerPath.handle(url: link.url)
} }
} }

View file

@ -201,23 +201,23 @@ struct StatusRowActionsView: View {
return return
} }
} }
HapticManager.shared.fireHaptic(of: .notification(.success)) HapticManager.shared.fireHaptic(.notification(.success))
switch action { switch action {
case .respond: case .respond:
SoundEffectManager.shared.playSound(of: .share) SoundEffectManager.shared.playSound(.share)
if ProcessInfo.processInfo.isMacCatalystApp { if ProcessInfo.processInfo.isMacCatalystApp {
openWindow(value: WindowDestination.replyToStatusEditor(status: viewModel.localStatus ?? viewModel.status)) openWindow(value: WindowDestination.replyToStatusEditor(status: viewModel.localStatus ?? viewModel.status))
} else { } else {
viewModel.routerPath.presentedSheet = .replyToStatusEditor(status: viewModel.localStatus ?? viewModel.status) viewModel.routerPath.presentedSheet = .replyToStatusEditor(status: viewModel.localStatus ?? viewModel.status)
} }
case .favorite: case .favorite:
SoundEffectManager.shared.playSound(of: .favorite) SoundEffectManager.shared.playSound(.favorite)
await statusDataController.toggleFavorite(remoteStatus: viewModel.localStatusId) await statusDataController.toggleFavorite(remoteStatus: viewModel.localStatusId)
case .bookmark: case .bookmark:
SoundEffectManager.shared.playSound(of: .bookmark) SoundEffectManager.shared.playSound(.bookmark)
await statusDataController.toggleBookmark(remoteStatus: viewModel.localStatusId) await statusDataController.toggleBookmark(remoteStatus: viewModel.localStatusId)
case .boost: case .boost:
SoundEffectManager.shared.playSound(of: .boost) SoundEffectManager.shared.playSound(.boost)
await statusDataController.toggleReblog(remoteStatus: viewModel.localStatusId) await statusDataController.toggleReblog(remoteStatus: viewModel.localStatusId)
default: default:
break break

View file

@ -83,7 +83,7 @@ struct StatusRowSwipeView: View {
@ViewBuilder @ViewBuilder
private func makeSwipeButtonForRouterPath(action: StatusAction, destination: SheetDestination) -> some View { private func makeSwipeButtonForRouterPath(action: StatusAction, destination: SheetDestination) -> some View {
Button { Button {
HapticManager.shared.fireHaptic(of: .notification(.success)) HapticManager.shared.fireHaptic(.notification(.success))
viewModel.routerPath.presentedSheet = destination viewModel.routerPath.presentedSheet = destination
} label: { } label: {
makeSwipeLabel(action: action, style: preferences.swipeActionsIconStyle) makeSwipeLabel(action: action, style: preferences.swipeActionsIconStyle)
@ -94,7 +94,7 @@ struct StatusRowSwipeView: View {
private func makeSwipeButtonForTask(action: StatusAction, privateBoost: Bool = false, task: @escaping () async -> Void) -> some View { private func makeSwipeButtonForTask(action: StatusAction, privateBoost: Bool = false, task: @escaping () async -> Void) -> some View {
Button { Button {
Task { Task {
HapticManager.shared.fireHaptic(of: .notification(.success)) HapticManager.shared.fireHaptic(.notification(.success))
await task() await task()
} }
} label: { } label: {

View file

@ -20,7 +20,7 @@ import SwiftUI
func removeStatus(status: Status) { func removeStatus(status: Status) {
if !disableUpdate, let index = pendingStatuses.firstIndex(of: status.id) { if !disableUpdate, let index = pendingStatuses.firstIndex(of: status.id) {
pendingStatuses.removeSubrange(index ... (pendingStatuses.count - 1)) pendingStatuses.removeSubrange(index ... (pendingStatuses.count - 1))
HapticManager.shared.fireHaptic(of: .timeline) HapticManager.shared.fireHaptic(.timeline)
} }
} }

View file

@ -109,11 +109,11 @@ public struct TimelineView: View {
viewModel.isTimelineVisible = false viewModel.isTimelineVisible = false
} }
.refreshable { .refreshable {
SoundEffectManager.shared.playSound(of: .pull) SoundEffectManager.shared.playSound(.pull)
HapticManager.shared.fireHaptic(of: .dataRefresh(intensity: 0.3)) HapticManager.shared.fireHaptic(.dataRefresh(intensity: 0.3))
await viewModel.pullToRefresh() await viewModel.pullToRefresh()
HapticManager.shared.fireHaptic(of: .dataRefresh(intensity: 0.7)) HapticManager.shared.fireHaptic(.dataRefresh(intensity: 0.7))
SoundEffectManager.shared.playSound(of: .refresh) SoundEffectManager.shared.playSound(.refresh)
} }
.onChange(of: watcher.latestEvent?.id) { .onChange(of: watcher.latestEvent?.id) {
if let latestEvent = watcher.latestEvent { if let latestEvent = watcher.latestEvent {