Redesign haptic patterns per animation and play on selection
Rewrite all 10 celebration haptic patterns to match visual timing and feel distinct: confetti gets playful falling taps, explosion gets a deep boom, shatter gets glassy cracks, morph gets liquid breathing, etc. Play the matching haptic when selecting a new vote animation in customization (respects haptic feedback toggle). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -80,219 +80,229 @@ final class HapticFeedbackManager {
|
||||
}
|
||||
}
|
||||
|
||||
// Confetti: burst upward, then scattered taps falling down
|
||||
// Confetti: poppy burst upward → checkmark spring → light scattered taps falling like confetti
|
||||
// Visual: 0.0 launch + glow | 0.3 checkmark spring | 0.5 confetti falling | 1.3 fade
|
||||
private func confettiPattern() throws -> CHHapticPattern {
|
||||
var events: [CHHapticEvent] = []
|
||||
|
||||
// Initial burst
|
||||
// Cannon pop — sharp, bright, like a party popper
|
||||
events.append(CHHapticEvent(
|
||||
eventType: .hapticTransient,
|
||||
parameters: [
|
||||
CHHapticEventParameter(parameterID: .hapticIntensity, value: 1.0),
|
||||
CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.8)
|
||||
CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.9),
|
||||
CHHapticEventParameter(parameterID: .hapticSharpness, value: 1.0)
|
||||
],
|
||||
relativeTime: 0.0
|
||||
))
|
||||
|
||||
// Glow swell
|
||||
events.append(CHHapticEvent(
|
||||
eventType: .hapticContinuous,
|
||||
parameters: [
|
||||
CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.4),
|
||||
CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.3)
|
||||
],
|
||||
relativeTime: 0.1,
|
||||
duration: 0.3
|
||||
))
|
||||
|
||||
// Scattered confetti taps falling
|
||||
let confettiTimes: [Double] = [0.5, 0.6, 0.65, 0.75, 0.85, 0.95, 1.1, 1.2]
|
||||
for (i, time) in confettiTimes.enumerated() {
|
||||
let intensity = Float(0.6 - Double(i) * 0.05)
|
||||
// Checkmark spring-in at 0.3s
|
||||
events.append(CHHapticEvent(
|
||||
eventType: .hapticTransient,
|
||||
parameters: [
|
||||
CHHapticEventParameter(parameterID: .hapticIntensity, value: max(intensity, 0.15)),
|
||||
CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.4)
|
||||
CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.7),
|
||||
CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.5)
|
||||
],
|
||||
relativeTime: 0.3
|
||||
))
|
||||
|
||||
// Confetti pieces falling — light, playful, irregular taps with varying sharpness
|
||||
let confettiTimes: [Double] = [0.55, 0.65, 0.72, 0.82, 0.93, 1.05, 1.15, 1.25, 1.35, 1.45]
|
||||
let sharpnesses: [Float] = [0.9, 0.5, 0.8, 0.3, 0.7, 0.4, 0.6, 0.3, 0.5, 0.2]
|
||||
for (i, time) in confettiTimes.enumerated() {
|
||||
let intensity = Float(0.4 - Double(i) * 0.03)
|
||||
events.append(CHHapticEvent(
|
||||
eventType: .hapticTransient,
|
||||
parameters: [
|
||||
CHHapticEventParameter(parameterID: .hapticIntensity, value: max(intensity, 0.1)),
|
||||
CHHapticEventParameter(parameterID: .hapticSharpness, value: sharpnesses[i])
|
||||
],
|
||||
relativeTime: time
|
||||
))
|
||||
}
|
||||
|
||||
// Final checkmark thud
|
||||
return try CHHapticPattern(events: events, parameters: [])
|
||||
}
|
||||
|
||||
// Vortex: spirals spin 0–0.5s → ring snaps at 0.3s → checkmark pops 0.5s → settles 0.7s
|
||||
// Character: whirring buildup with accelerating ticks, then a tight satisfying snap
|
||||
private func vortexPattern() throws -> CHHapticPattern {
|
||||
var events: [CHHapticEvent] = []
|
||||
|
||||
// Spinning wind-up — accelerating ticks like a roulette wheel slowing in reverse
|
||||
let tickTimes: [Double] = [0.0, 0.07, 0.13, 0.18, 0.22, 0.25, 0.27, 0.29, 0.30]
|
||||
for (i, time) in tickTimes.enumerated() {
|
||||
let progress = Float(i) / Float(tickTimes.count)
|
||||
events.append(CHHapticEvent(
|
||||
eventType: .hapticTransient,
|
||||
parameters: [
|
||||
CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.8),
|
||||
CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.5)
|
||||
CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.15 + progress * 0.4),
|
||||
CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.2 + progress * 0.3)
|
||||
],
|
||||
relativeTime: 1.4
|
||||
relativeTime: time
|
||||
))
|
||||
}
|
||||
|
||||
// Ring appears — soft thrum
|
||||
events.append(CHHapticEvent(
|
||||
eventType: .hapticContinuous,
|
||||
parameters: [
|
||||
CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.5),
|
||||
CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.2)
|
||||
],
|
||||
relativeTime: 0.3,
|
||||
duration: 0.2
|
||||
))
|
||||
|
||||
// Checkmark pops — the payoff snap
|
||||
events.append(CHHapticEvent(
|
||||
eventType: .hapticTransient,
|
||||
parameters: [
|
||||
CHHapticEventParameter(parameterID: .hapticIntensity, value: 1.0),
|
||||
CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.6)
|
||||
],
|
||||
relativeTime: 0.5
|
||||
))
|
||||
|
||||
// Checkmark settles — soft bounce
|
||||
events.append(CHHapticEvent(
|
||||
eventType: .hapticTransient,
|
||||
parameters: [
|
||||
CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.3),
|
||||
CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.3)
|
||||
],
|
||||
relativeTime: 0.7
|
||||
))
|
||||
|
||||
return try CHHapticPattern(events: events, parameters: [])
|
||||
}
|
||||
|
||||
// Vortex: spinning wind-up then a satisfying snap
|
||||
private func vortexPattern() throws -> CHHapticPattern {
|
||||
// Explosion: particles fly out 0–0.6s → glow 0.2–0.6s → checkmark drops in 0.3s → fades 1.4s
|
||||
// Character: massive singular BOOM then debris scattering away — the biggest hit of any animation
|
||||
private func explosionPattern() throws -> CHHapticPattern {
|
||||
var events: [CHHapticEvent] = []
|
||||
|
||||
// Spiral wind-up (continuous, rising intensity)
|
||||
events.append(CHHapticEvent(
|
||||
eventType: .hapticContinuous,
|
||||
parameters: [
|
||||
CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.2),
|
||||
CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.3)
|
||||
],
|
||||
relativeTime: 0.0,
|
||||
duration: 0.5
|
||||
))
|
||||
|
||||
// Ring snap
|
||||
// THE explosion — maximum everything, low sharpness for deep boom
|
||||
events.append(CHHapticEvent(
|
||||
eventType: .hapticTransient,
|
||||
parameters: [
|
||||
CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.9),
|
||||
CHHapticEventParameter(parameterID: .hapticIntensity, value: 1.0),
|
||||
CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.1)
|
||||
],
|
||||
relativeTime: 0.0
|
||||
))
|
||||
|
||||
// Shockwave rumble — deep, low, fading
|
||||
events.append(CHHapticEvent(
|
||||
eventType: .hapticContinuous,
|
||||
parameters: [
|
||||
CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.7),
|
||||
CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.1)
|
||||
],
|
||||
relativeTime: 0.02,
|
||||
duration: 0.35
|
||||
))
|
||||
|
||||
// Debris particles hitting — scattered sharp ticks as pieces fly
|
||||
let debrisTimes: [Double] = [0.12, 0.18, 0.25, 0.32, 0.40]
|
||||
for (i, time) in debrisTimes.enumerated() {
|
||||
events.append(CHHapticEvent(
|
||||
eventType: .hapticTransient,
|
||||
parameters: [
|
||||
CHHapticEventParameter(parameterID: .hapticIntensity, value: Float(0.5 - Double(i) * 0.08)),
|
||||
CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.9)
|
||||
],
|
||||
relativeTime: time
|
||||
))
|
||||
}
|
||||
|
||||
// Checkmark drops in — solid medium thud (not as big as the explosion)
|
||||
events.append(CHHapticEvent(
|
||||
eventType: .hapticTransient,
|
||||
parameters: [
|
||||
CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.6),
|
||||
CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.4)
|
||||
],
|
||||
relativeTime: 0.55
|
||||
))
|
||||
|
||||
let curve = CHHapticParameterCurve(
|
||||
parameterID: .hapticIntensityControl,
|
||||
controlPoints: [
|
||||
CHHapticParameterCurve.ControlPoint(relativeTime: 0.0, value: 1.0),
|
||||
CHHapticParameterCurve.ControlPoint(relativeTime: 0.15, value: 0.6),
|
||||
CHHapticParameterCurve.ControlPoint(relativeTime: 0.4, value: 0.1)
|
||||
],
|
||||
relativeTime: 0.0
|
||||
)
|
||||
|
||||
return try CHHapticPattern(events: events, parameterCurves: [curve])
|
||||
}
|
||||
|
||||
// Flip: card flips at 0.2s → checkmark scales 0.4s → shimmer sweeps 0.5–1.3s
|
||||
// Character: clean, elegant — a crisp two-beat flip then a silky shimmer glide
|
||||
private func flipPattern() throws -> CHHapticPattern {
|
||||
var events: [CHHapticEvent] = []
|
||||
|
||||
// Card lifts off surface — gentle click
|
||||
events.append(CHHapticEvent(
|
||||
eventType: .hapticTransient,
|
||||
parameters: [
|
||||
CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.3),
|
||||
CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.7)
|
||||
],
|
||||
relativeTime: 0.5
|
||||
relativeTime: 0.2
|
||||
))
|
||||
|
||||
// Card slaps down — the satisfying flip landing
|
||||
events.append(CHHapticEvent(
|
||||
eventType: .hapticTransient,
|
||||
parameters: [
|
||||
CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.8),
|
||||
CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.4)
|
||||
],
|
||||
relativeTime: 0.4
|
||||
))
|
||||
|
||||
// Checkmark bounce
|
||||
events.append(CHHapticEvent(
|
||||
eventType: .hapticTransient,
|
||||
parameters: [
|
||||
CHHapticEventParameter(parameterID: .hapticIntensity, value: 1.0),
|
||||
CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.6)
|
||||
],
|
||||
relativeTime: 0.7
|
||||
))
|
||||
|
||||
// Settle
|
||||
events.append(CHHapticEvent(
|
||||
eventType: .hapticTransient,
|
||||
parameters: [
|
||||
CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.4),
|
||||
CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.4)
|
||||
],
|
||||
relativeTime: 0.9
|
||||
))
|
||||
|
||||
let curve = CHHapticParameterCurve(
|
||||
parameterID: .hapticIntensityControl,
|
||||
controlPoints: [
|
||||
CHHapticParameterCurve.ControlPoint(relativeTime: 0.0, value: 0.2),
|
||||
CHHapticParameterCurve.ControlPoint(relativeTime: 0.4, value: 0.7),
|
||||
CHHapticParameterCurve.ControlPoint(relativeTime: 0.5, value: 1.0)
|
||||
],
|
||||
relativeTime: 0.0
|
||||
)
|
||||
|
||||
return try CHHapticPattern(events: events, parameterCurves: [curve])
|
||||
}
|
||||
|
||||
// Explosion: big bang then particles scatter
|
||||
private func explosionPattern() throws -> CHHapticPattern {
|
||||
var events: [CHHapticEvent] = []
|
||||
|
||||
// Big explosion impact
|
||||
events.append(CHHapticEvent(
|
||||
eventType: .hapticTransient,
|
||||
parameters: [
|
||||
CHHapticEventParameter(parameterID: .hapticIntensity, value: 1.0),
|
||||
CHHapticEventParameter(parameterID: .hapticSharpness, value: 1.0)
|
||||
],
|
||||
relativeTime: 0.0
|
||||
))
|
||||
|
||||
// Debris rumble
|
||||
events.append(CHHapticEvent(
|
||||
eventType: .hapticContinuous,
|
||||
parameters: [
|
||||
CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.6),
|
||||
CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.8)
|
||||
],
|
||||
relativeTime: 0.05,
|
||||
duration: 0.4
|
||||
))
|
||||
|
||||
// Glow pulse
|
||||
events.append(CHHapticEvent(
|
||||
eventType: .hapticContinuous,
|
||||
parameters: [
|
||||
CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.3),
|
||||
CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.2)
|
||||
],
|
||||
relativeTime: 0.3,
|
||||
duration: 0.3
|
||||
))
|
||||
|
||||
// Checkmark landing
|
||||
events.append(CHHapticEvent(
|
||||
eventType: .hapticTransient,
|
||||
parameters: [
|
||||
CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.8),
|
||||
CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.5)
|
||||
CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.3)
|
||||
],
|
||||
relativeTime: 0.6
|
||||
))
|
||||
|
||||
return try CHHapticPattern(events: events, parameters: [])
|
||||
}
|
||||
|
||||
// Flip: a quick flip-over thud
|
||||
private func flipPattern() throws -> CHHapticPattern {
|
||||
var events: [CHHapticEvent] = []
|
||||
|
||||
// Card lift
|
||||
events.append(CHHapticEvent(
|
||||
eventType: .hapticTransient,
|
||||
parameters: [
|
||||
CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.4),
|
||||
CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.3)
|
||||
],
|
||||
relativeTime: 0.0
|
||||
))
|
||||
|
||||
// Mid-flip whoosh
|
||||
// Shimmer sweeping across — very light, high-sharpness continuous (like a sparkle)
|
||||
events.append(CHHapticEvent(
|
||||
eventType: .hapticContinuous,
|
||||
parameters: [
|
||||
CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.5),
|
||||
CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.4)
|
||||
],
|
||||
relativeTime: 0.2,
|
||||
duration: 0.2
|
||||
))
|
||||
|
||||
// Card lands
|
||||
events.append(CHHapticEvent(
|
||||
eventType: .hapticTransient,
|
||||
parameters: [
|
||||
CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.9),
|
||||
CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.6)
|
||||
],
|
||||
relativeTime: 0.4
|
||||
))
|
||||
|
||||
// Shimmer glide
|
||||
events.append(CHHapticEvent(
|
||||
eventType: .hapticContinuous,
|
||||
parameters: [
|
||||
CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.2),
|
||||
CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.7)
|
||||
CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.15),
|
||||
CHHapticEventParameter(parameterID: .hapticSharpness, value: 1.0)
|
||||
],
|
||||
relativeTime: 0.5,
|
||||
duration: 0.4
|
||||
duration: 0.8
|
||||
))
|
||||
|
||||
return try CHHapticPattern(events: events, parameters: [])
|
||||
let shimmerCurve = CHHapticParameterCurve(
|
||||
parameterID: .hapticIntensityControl,
|
||||
controlPoints: [
|
||||
CHHapticParameterCurve.ControlPoint(relativeTime: 0.5, value: 0.0),
|
||||
CHHapticParameterCurve.ControlPoint(relativeTime: 0.7, value: 1.0),
|
||||
CHHapticParameterCurve.ControlPoint(relativeTime: 1.3, value: 0.0)
|
||||
],
|
||||
relativeTime: 0.0
|
||||
)
|
||||
|
||||
return try CHHapticPattern(events: events, parameterCurves: [shimmerCurve])
|
||||
}
|
||||
|
||||
// Shatter: crack apart then reform
|
||||
// Shatter: explode out 0–0.5s → pause → converge 0.6–1.1s → checkmark 1.1s → fade 1.8s
|
||||
// Character: glass breaking — sharp crack, scattered sharp hits, silence, then magnetic pull + click
|
||||
private func shatterPattern() throws -> CHHapticPattern {
|
||||
var events: [CHHapticEvent] = []
|
||||
|
||||
// Shatter crack
|
||||
// Glass crack — extremely sharp and crisp
|
||||
events.append(CHHapticEvent(
|
||||
eventType: .hapticTransient,
|
||||
parameters: [
|
||||
@@ -302,289 +312,406 @@ final class HapticFeedbackManager {
|
||||
relativeTime: 0.0
|
||||
))
|
||||
|
||||
// Flying shards
|
||||
let shardTimes: [Double] = [0.05, 0.1, 0.15, 0.2, 0.3]
|
||||
// Shards flying — sharp, glassy ticks scattering outward
|
||||
let shardTimes: [Double] = [0.04, 0.08, 0.13, 0.19, 0.26, 0.35, 0.45]
|
||||
for (i, time) in shardTimes.enumerated() {
|
||||
events.append(CHHapticEvent(
|
||||
eventType: .hapticTransient,
|
||||
parameters: [
|
||||
CHHapticEventParameter(parameterID: .hapticIntensity, value: Float(0.7 - Double(i) * 0.1)),
|
||||
CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.8)
|
||||
CHHapticEventParameter(parameterID: .hapticIntensity, value: Float(0.6 - Double(i) * 0.07)),
|
||||
CHHapticEventParameter(parameterID: .hapticSharpness, value: 1.0)
|
||||
],
|
||||
relativeTime: time
|
||||
))
|
||||
}
|
||||
|
||||
// Reform convergence
|
||||
// ---- silence / gap while pieces float (0.5–0.6s) ----
|
||||
|
||||
// Reform — pieces pulling back, low magnetic hum building
|
||||
events.append(CHHapticEvent(
|
||||
eventType: .hapticContinuous,
|
||||
parameters: [
|
||||
CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.4),
|
||||
CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.3)
|
||||
CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.2),
|
||||
CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.1)
|
||||
],
|
||||
relativeTime: 0.6,
|
||||
duration: 0.4
|
||||
duration: 0.5
|
||||
))
|
||||
|
||||
// Checkmark snap
|
||||
events.append(CHHapticEvent(
|
||||
eventType: .hapticTransient,
|
||||
parameters: [
|
||||
CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.8),
|
||||
CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.5)
|
||||
let reformCurve = CHHapticParameterCurve(
|
||||
parameterID: .hapticIntensityControl,
|
||||
controlPoints: [
|
||||
CHHapticParameterCurve.ControlPoint(relativeTime: 0.6, value: 0.1),
|
||||
CHHapticParameterCurve.ControlPoint(relativeTime: 1.0, value: 0.6)
|
||||
],
|
||||
relativeTime: 1.1
|
||||
))
|
||||
relativeTime: 0.0
|
||||
)
|
||||
|
||||
return try CHHapticPattern(events: events, parameters: [])
|
||||
}
|
||||
|
||||
// Pulse Wave: expanding rings
|
||||
private func pulseWavePattern() throws -> CHHapticPattern {
|
||||
var events: [CHHapticEvent] = []
|
||||
|
||||
// Three expanding wave pulses
|
||||
let waveTimes: [Double] = [0.0, 0.1, 0.2]
|
||||
for (i, time) in waveTimes.enumerated() {
|
||||
events.append(CHHapticEvent(
|
||||
eventType: .hapticTransient,
|
||||
parameters: [
|
||||
CHHapticEventParameter(parameterID: .hapticIntensity, value: Float(0.8 - Double(i) * 0.15)),
|
||||
CHHapticEventParameter(parameterID: .hapticSharpness, value: Float(0.6 - Double(i) * 0.1))
|
||||
],
|
||||
relativeTime: time
|
||||
))
|
||||
}
|
||||
|
||||
// Stamp down (checkmark)
|
||||
events.append(CHHapticEvent(
|
||||
eventType: .hapticTransient,
|
||||
parameters: [
|
||||
CHHapticEventParameter(parameterID: .hapticIntensity, value: 1.0),
|
||||
CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.7)
|
||||
],
|
||||
relativeTime: 0.3
|
||||
))
|
||||
|
||||
// Stamp settle
|
||||
events.append(CHHapticEvent(
|
||||
eventType: .hapticTransient,
|
||||
parameters: [
|
||||
CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.3),
|
||||
CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.4)
|
||||
],
|
||||
relativeTime: 0.45
|
||||
))
|
||||
|
||||
return try CHHapticPattern(events: events, parameters: [])
|
||||
}
|
||||
|
||||
// Fireworks: multiple bursts at staggered intervals
|
||||
private func fireworksPattern() throws -> CHHapticPattern {
|
||||
var events: [CHHapticEvent] = []
|
||||
|
||||
// 4 firework bursts, each staggered
|
||||
let burstTimes: [Double] = [0.0, 0.15, 0.3, 0.45]
|
||||
for time in burstTimes {
|
||||
// Launch whoosh
|
||||
events.append(CHHapticEvent(
|
||||
eventType: .hapticContinuous,
|
||||
parameters: [
|
||||
CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.3),
|
||||
CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.5)
|
||||
],
|
||||
relativeTime: time,
|
||||
duration: 0.1
|
||||
))
|
||||
|
||||
// Burst pop
|
||||
// Final snap together — satisfying click as pieces lock in
|
||||
events.append(CHHapticEvent(
|
||||
eventType: .hapticTransient,
|
||||
parameters: [
|
||||
CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.9),
|
||||
CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.8)
|
||||
CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.6)
|
||||
],
|
||||
relativeTime: time + 0.1
|
||||
relativeTime: 1.1
|
||||
))
|
||||
|
||||
return try CHHapticPattern(events: events, parameterCurves: [reformCurve])
|
||||
}
|
||||
|
||||
// Final checkmark
|
||||
// Pulse: waves at 0.0, 0.1, 0.2 → stamp checkmark at 0.2s with rotation → fade 1.2s
|
||||
// Character: rhythmic heartbeat pulses then a heavy rubber-stamp DOWN
|
||||
private func pulseWavePattern() throws -> CHHapticPattern {
|
||||
var events: [CHHapticEvent] = []
|
||||
|
||||
// Three wave pulses — each softer and more diffuse, like ripples spreading
|
||||
events.append(CHHapticEvent(
|
||||
eventType: .hapticTransient,
|
||||
parameters: [
|
||||
CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.8),
|
||||
CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.5)
|
||||
CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.6),
|
||||
CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.2)
|
||||
],
|
||||
relativeTime: 0.7
|
||||
relativeTime: 0.0
|
||||
))
|
||||
events.append(CHHapticEvent(
|
||||
eventType: .hapticTransient,
|
||||
parameters: [
|
||||
CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.4),
|
||||
CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.15)
|
||||
],
|
||||
relativeTime: 0.1
|
||||
))
|
||||
events.append(CHHapticEvent(
|
||||
eventType: .hapticTransient,
|
||||
parameters: [
|
||||
CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.25),
|
||||
CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.1)
|
||||
],
|
||||
relativeTime: 0.2
|
||||
))
|
||||
|
||||
// STAMP DOWN — heavy, authoritative, like a rubber stamp slamming
|
||||
events.append(CHHapticEvent(
|
||||
eventType: .hapticTransient,
|
||||
parameters: [
|
||||
CHHapticEventParameter(parameterID: .hapticIntensity, value: 1.0),
|
||||
CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.3)
|
||||
],
|
||||
relativeTime: 0.25
|
||||
))
|
||||
|
||||
// Stamp reverb — brief low rumble from the impact
|
||||
events.append(CHHapticEvent(
|
||||
eventType: .hapticContinuous,
|
||||
parameters: [
|
||||
CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.3),
|
||||
CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.05)
|
||||
],
|
||||
relativeTime: 0.27,
|
||||
duration: 0.2
|
||||
))
|
||||
|
||||
return try CHHapticPattern(events: events, parameters: [])
|
||||
}
|
||||
|
||||
// Morph: flowing organic movement
|
||||
// Fireworks: 4 bursts staggered at 0.0/0.15/0.30/0.45 → checkmark at 0.6s → fade 2.0s
|
||||
// Character: distinct pops at different heights/intensities, each with a quick sparkle trail
|
||||
private func fireworksPattern() throws -> CHHapticPattern {
|
||||
var events: [CHHapticEvent] = []
|
||||
|
||||
// Burst 1 — big opening burst
|
||||
events.append(CHHapticEvent(
|
||||
eventType: .hapticTransient,
|
||||
parameters: [
|
||||
CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.9),
|
||||
CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.7)
|
||||
],
|
||||
relativeTime: 0.0
|
||||
))
|
||||
// Sparkle trail
|
||||
events.append(CHHapticEvent(
|
||||
eventType: .hapticTransient,
|
||||
parameters: [
|
||||
CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.3),
|
||||
CHHapticEventParameter(parameterID: .hapticSharpness, value: 1.0)
|
||||
],
|
||||
relativeTime: 0.08
|
||||
))
|
||||
|
||||
// Burst 2 — smaller, higher pitch
|
||||
events.append(CHHapticEvent(
|
||||
eventType: .hapticTransient,
|
||||
parameters: [
|
||||
CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.6),
|
||||
CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.9)
|
||||
],
|
||||
relativeTime: 0.15
|
||||
))
|
||||
events.append(CHHapticEvent(
|
||||
eventType: .hapticTransient,
|
||||
parameters: [
|
||||
CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.2),
|
||||
CHHapticEventParameter(parameterID: .hapticSharpness, value: 1.0)
|
||||
],
|
||||
relativeTime: 0.22
|
||||
))
|
||||
|
||||
// Burst 3 — deep thump
|
||||
events.append(CHHapticEvent(
|
||||
eventType: .hapticTransient,
|
||||
parameters: [
|
||||
CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.8),
|
||||
CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.3)
|
||||
],
|
||||
relativeTime: 0.30
|
||||
))
|
||||
events.append(CHHapticEvent(
|
||||
eventType: .hapticTransient,
|
||||
parameters: [
|
||||
CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.25),
|
||||
CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.8)
|
||||
],
|
||||
relativeTime: 0.37
|
||||
))
|
||||
|
||||
// Burst 4 — grand finale, biggest
|
||||
events.append(CHHapticEvent(
|
||||
eventType: .hapticTransient,
|
||||
parameters: [
|
||||
CHHapticEventParameter(parameterID: .hapticIntensity, value: 1.0),
|
||||
CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.5)
|
||||
],
|
||||
relativeTime: 0.45
|
||||
))
|
||||
events.append(CHHapticEvent(
|
||||
eventType: .hapticTransient,
|
||||
parameters: [
|
||||
CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.4),
|
||||
CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.9)
|
||||
],
|
||||
relativeTime: 0.52
|
||||
))
|
||||
|
||||
// Checkmark — gentle after the show
|
||||
events.append(CHHapticEvent(
|
||||
eventType: .hapticTransient,
|
||||
parameters: [
|
||||
CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.5),
|
||||
CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.4)
|
||||
],
|
||||
relativeTime: 0.65
|
||||
))
|
||||
|
||||
return try CHHapticPattern(events: events, parameters: [])
|
||||
}
|
||||
|
||||
// Morph: blobs expand 0–0.4s → morph/shift 0.4–0.9s → converge 0.9–1.3s → checkmark 1.2s
|
||||
// Character: liquid, organic — continuous flowing hum that breathes and undulates
|
||||
private func morphPattern() throws -> CHHapticPattern {
|
||||
var events: [CHHapticEvent] = []
|
||||
|
||||
// Expand outward
|
||||
// Blob expand — soft, round, growing
|
||||
events.append(CHHapticEvent(
|
||||
eventType: .hapticContinuous,
|
||||
parameters: [
|
||||
CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.4),
|
||||
CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.2)
|
||||
CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.3),
|
||||
CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.05)
|
||||
],
|
||||
relativeTime: 0.0,
|
||||
duration: 0.4
|
||||
))
|
||||
|
||||
// Morph shift
|
||||
// Morph shift — blobs moving, undulating, slightly sharper
|
||||
events.append(CHHapticEvent(
|
||||
eventType: .hapticContinuous,
|
||||
parameters: [
|
||||
CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.5),
|
||||
CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.3)
|
||||
CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.4),
|
||||
CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.1)
|
||||
],
|
||||
relativeTime: 0.4,
|
||||
duration: 0.5
|
||||
))
|
||||
|
||||
// Converge
|
||||
// Intensity curve for the morph — breathing/pulsing feel
|
||||
let morphCurve = CHHapticParameterCurve(
|
||||
parameterID: .hapticIntensityControl,
|
||||
controlPoints: [
|
||||
CHHapticParameterCurve.ControlPoint(relativeTime: 0.0, value: 0.3),
|
||||
CHHapticParameterCurve.ControlPoint(relativeTime: 0.2, value: 0.5),
|
||||
CHHapticParameterCurve.ControlPoint(relativeTime: 0.4, value: 0.3),
|
||||
CHHapticParameterCurve.ControlPoint(relativeTime: 0.6, value: 0.6),
|
||||
CHHapticParameterCurve.ControlPoint(relativeTime: 0.8, value: 0.4),
|
||||
CHHapticParameterCurve.ControlPoint(relativeTime: 0.9, value: 0.8)
|
||||
],
|
||||
relativeTime: 0.0
|
||||
)
|
||||
|
||||
// Converge — blobs snap together, soft thud
|
||||
events.append(CHHapticEvent(
|
||||
eventType: .hapticTransient,
|
||||
parameters: [
|
||||
CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.6),
|
||||
CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.4)
|
||||
CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.5),
|
||||
CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.15)
|
||||
],
|
||||
relativeTime: 0.9
|
||||
))
|
||||
|
||||
// Checkmark reveal
|
||||
// Checkmark — soft glow reveal, not sharp
|
||||
events.append(CHHapticEvent(
|
||||
eventType: .hapticTransient,
|
||||
parameters: [
|
||||
CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.8),
|
||||
CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.5)
|
||||
CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.6),
|
||||
CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.2)
|
||||
],
|
||||
relativeTime: 1.2
|
||||
))
|
||||
|
||||
return try CHHapticPattern(events: events, parameters: [])
|
||||
return try CHHapticPattern(events: events, parameterCurves: [morphCurve])
|
||||
}
|
||||
|
||||
// Tunnel: rushing zoom then arrival
|
||||
// Tunnel: 6 rings zoom in (each 0.1s delay) → starburst rotates → checkmark 0.5s → fade 1.5s
|
||||
// Character: accelerating whoosh — rings pass you faster and faster, then BAM you arrive
|
||||
private func tunnelPattern() throws -> CHHapticPattern {
|
||||
var events: [CHHapticEvent] = []
|
||||
|
||||
// Rings rushing inward (6 quick taps, increasing intensity)
|
||||
for i in 0..<6 {
|
||||
let time = Double(i) * 0.08
|
||||
// Rings passing — accelerating rhythm (gaps get shorter), each ring is a whoosh
|
||||
// Visual: rings at 0.0, 0.1, 0.2, 0.3, 0.4, 0.5 with scale animation
|
||||
let ringTimes: [Double] = [0.0, 0.08, 0.14, 0.19, 0.23, 0.26]
|
||||
for (i, time) in ringTimes.enumerated() {
|
||||
let progress = Float(i) / Float(ringTimes.count)
|
||||
// Each ring hit gets more intense and slightly sharper
|
||||
events.append(CHHapticEvent(
|
||||
eventType: .hapticTransient,
|
||||
parameters: [
|
||||
CHHapticEventParameter(parameterID: .hapticIntensity, value: Float(0.2 + Double(i) * 0.12)),
|
||||
CHHapticEventParameter(parameterID: .hapticSharpness, value: Float(0.3 + Double(i) * 0.1))
|
||||
CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.15 + progress * 0.5),
|
||||
CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.4 + progress * 0.3)
|
||||
],
|
||||
relativeTime: time
|
||||
))
|
||||
}
|
||||
|
||||
// Warp speed continuous
|
||||
// Warp speed — continuous buzzy rush as you fly through
|
||||
events.append(CHHapticEvent(
|
||||
eventType: .hapticContinuous,
|
||||
parameters: [
|
||||
CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.5),
|
||||
CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.6)
|
||||
CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.6),
|
||||
CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.7)
|
||||
],
|
||||
relativeTime: 0.3,
|
||||
relativeTime: 0.28,
|
||||
duration: 0.2
|
||||
))
|
||||
|
||||
// Arrival impact
|
||||
events.append(CHHapticEvent(
|
||||
eventType: .hapticTransient,
|
||||
parameters: [
|
||||
CHHapticEventParameter(parameterID: .hapticIntensity, value: 1.0),
|
||||
CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.7)
|
||||
],
|
||||
relativeTime: 0.5
|
||||
))
|
||||
|
||||
// Settle bounce
|
||||
events.append(CHHapticEvent(
|
||||
eventType: .hapticTransient,
|
||||
parameters: [
|
||||
CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.4),
|
||||
CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.4)
|
||||
],
|
||||
relativeTime: 0.7
|
||||
))
|
||||
|
||||
return try CHHapticPattern(events: events, parameters: [])
|
||||
}
|
||||
|
||||
// Gravity: fall, bounce, bounce, settle
|
||||
private func gravityPattern() throws -> CHHapticPattern {
|
||||
var events: [CHHapticEvent] = []
|
||||
|
||||
// Falling whoosh
|
||||
events.append(CHHapticEvent(
|
||||
eventType: .hapticContinuous,
|
||||
parameters: [
|
||||
CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.3),
|
||||
CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.3)
|
||||
],
|
||||
relativeTime: 0.0,
|
||||
duration: 0.35
|
||||
))
|
||||
|
||||
// First impact (heaviest)
|
||||
events.append(CHHapticEvent(
|
||||
eventType: .hapticTransient,
|
||||
parameters: [
|
||||
CHHapticEventParameter(parameterID: .hapticIntensity, value: 1.0),
|
||||
CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.8)
|
||||
],
|
||||
relativeTime: 0.4
|
||||
))
|
||||
|
||||
// First bounce up & down
|
||||
events.append(CHHapticEvent(
|
||||
eventType: .hapticTransient,
|
||||
parameters: [
|
||||
CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.6),
|
||||
CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.6)
|
||||
],
|
||||
relativeTime: 0.6
|
||||
))
|
||||
|
||||
// Second bounce (smaller)
|
||||
events.append(CHHapticEvent(
|
||||
eventType: .hapticTransient,
|
||||
parameters: [
|
||||
CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.35),
|
||||
CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.5)
|
||||
],
|
||||
relativeTime: 0.75
|
||||
))
|
||||
|
||||
// Final settle
|
||||
events.append(CHHapticEvent(
|
||||
eventType: .hapticTransient,
|
||||
parameters: [
|
||||
CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.2),
|
||||
CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.3)
|
||||
],
|
||||
relativeTime: 0.85
|
||||
))
|
||||
|
||||
let curve = CHHapticParameterCurve(
|
||||
let warpCurve = CHHapticParameterCurve(
|
||||
parameterID: .hapticIntensityControl,
|
||||
controlPoints: [
|
||||
CHHapticParameterCurve.ControlPoint(relativeTime: 0.0, value: 0.1),
|
||||
CHHapticParameterCurve.ControlPoint(relativeTime: 0.35, value: 0.8)
|
||||
CHHapticParameterCurve.ControlPoint(relativeTime: 0.28, value: 0.4),
|
||||
CHHapticParameterCurve.ControlPoint(relativeTime: 0.38, value: 1.0),
|
||||
CHHapticParameterCurve.ControlPoint(relativeTime: 0.48, value: 0.8)
|
||||
],
|
||||
relativeTime: 0.0
|
||||
)
|
||||
|
||||
return try CHHapticPattern(events: events, parameterCurves: [curve])
|
||||
// Arrival — big thud as you land at the destination
|
||||
events.append(CHHapticEvent(
|
||||
eventType: .hapticTransient,
|
||||
parameters: [
|
||||
CHHapticEventParameter(parameterID: .hapticIntensity, value: 1.0),
|
||||
CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.5)
|
||||
],
|
||||
relativeTime: 0.5
|
||||
))
|
||||
|
||||
// Starburst shimmer — light rotating sparkle
|
||||
events.append(CHHapticEvent(
|
||||
eventType: .hapticContinuous,
|
||||
parameters: [
|
||||
CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.15),
|
||||
CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.9)
|
||||
],
|
||||
relativeTime: 0.55,
|
||||
duration: 0.3
|
||||
))
|
||||
|
||||
return try CHHapticPattern(events: events, parameterCurves: [warpCurve])
|
||||
}
|
||||
|
||||
// Gravity: falls 0–0.4s → impact 0.4s → bounce up 0.4–0.6 → down 0.6 → up 0.6–0.75 → down 0.75 → settle 0.85
|
||||
// Character: weighty — falling acceleration, HEAVY impact, realistic diminishing bounces
|
||||
private func gravityPattern() throws -> CHHapticPattern {
|
||||
var events: [CHHapticEvent] = []
|
||||
|
||||
// Falling — accelerating continuous that builds
|
||||
events.append(CHHapticEvent(
|
||||
eventType: .hapticContinuous,
|
||||
parameters: [
|
||||
CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.15),
|
||||
CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.2)
|
||||
],
|
||||
relativeTime: 0.0,
|
||||
duration: 0.38
|
||||
))
|
||||
|
||||
let fallCurve = CHHapticParameterCurve(
|
||||
parameterID: .hapticIntensityControl,
|
||||
controlPoints: [
|
||||
CHHapticParameterCurve.ControlPoint(relativeTime: 0.0, value: 0.05),
|
||||
CHHapticParameterCurve.ControlPoint(relativeTime: 0.2, value: 0.2),
|
||||
CHHapticParameterCurve.ControlPoint(relativeTime: 0.38, value: 0.9)
|
||||
],
|
||||
relativeTime: 0.0
|
||||
)
|
||||
|
||||
// FIRST IMPACT — heaviest, low and boomy (this is a heavy object hitting ground)
|
||||
events.append(CHHapticEvent(
|
||||
eventType: .hapticTransient,
|
||||
parameters: [
|
||||
CHHapticEventParameter(parameterID: .hapticIntensity, value: 1.0),
|
||||
CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.15)
|
||||
],
|
||||
relativeTime: 0.4
|
||||
))
|
||||
// Dust/ring burst from impact
|
||||
events.append(CHHapticEvent(
|
||||
eventType: .hapticContinuous,
|
||||
parameters: [
|
||||
CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.4),
|
||||
CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.3)
|
||||
],
|
||||
relativeTime: 0.41,
|
||||
duration: 0.12
|
||||
))
|
||||
|
||||
// Bounce 1 — lands again, lighter
|
||||
events.append(CHHapticEvent(
|
||||
eventType: .hapticTransient,
|
||||
parameters: [
|
||||
CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.55),
|
||||
CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.2)
|
||||
],
|
||||
relativeTime: 0.6
|
||||
))
|
||||
|
||||
// Bounce 2 — smaller
|
||||
events.append(CHHapticEvent(
|
||||
eventType: .hapticTransient,
|
||||
parameters: [
|
||||
CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.3),
|
||||
CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.25)
|
||||
],
|
||||
relativeTime: 0.75
|
||||
))
|
||||
|
||||
// Final settle — tiny tap
|
||||
events.append(CHHapticEvent(
|
||||
eventType: .hapticTransient,
|
||||
parameters: [
|
||||
CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.12),
|
||||
CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.2)
|
||||
],
|
||||
relativeTime: 0.85
|
||||
))
|
||||
|
||||
return try CHHapticPattern(events: events, parameterCurves: [fallCurve])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -495,6 +495,7 @@ struct CelebrationAnimationPickerCompact: View {
|
||||
@AppStorage(UserDefaultsStore.Keys.celebrationAnimation.rawValue, store: GroupUserDefaults.groupDefaults) private var celebrationAnimationIndex: Int = 0
|
||||
@AppStorage(UserDefaultsStore.Keys.moodTint.rawValue, store: GroupUserDefaults.groupDefaults) private var moodTint: MoodTints = .Default
|
||||
@AppStorage(UserDefaultsStore.Keys.theme.rawValue, store: GroupUserDefaults.groupDefaults) private var theme: Theme = .system
|
||||
@AppStorage(UserDefaultsStore.Keys.hapticFeedbackEnabled.rawValue, store: GroupUserDefaults.groupDefaults) private var hapticFeedbackEnabled = true
|
||||
@Environment(\.colorScheme) private var colorScheme
|
||||
|
||||
// Preview state
|
||||
@@ -586,6 +587,9 @@ struct CelebrationAnimationPickerCompact: View {
|
||||
Task { @MainActor in
|
||||
try? await Task.sleep(for: .seconds(0.5))
|
||||
guard previewAnimation == animation else { return }
|
||||
if hapticFeedbackEnabled {
|
||||
HapticFeedbackManager.shared.play(for: animation)
|
||||
}
|
||||
withAnimation(.easeInOut(duration: 0.3)) {
|
||||
showPreviewCelebration = true
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user