Skip to content

Commit c553e75

Browse files
authored
Add larger reaction effects (#71)
Pressing and holding a reaction in the quick menu or the full reactions window now sends it with a larger effect, and larger effects from others play with the enlarged emoji, as on iOS. Before, macOS could neither send nor show them.
1 parent 70aa25f commit c553e75

16 files changed

Lines changed: 297 additions & 59 deletions

‎Telegram-Mac/packages-integration/hooks/ChatHistoryUIHooksInstall.swift‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -382,8 +382,8 @@ func installChatHistoryUIHooks() {
382382
return (context as! AccountContext).giftStickers
383383
}, messageNewReactions: { message, reaction, isTags in
384384
return message.newReactions(with: reaction, isTags: isTags)
385-
}, makeReactionsWindow: { context, peerId, selectedItems, react in
386-
return ReactionsWindowController(context as! AccountContext, peerId: peerId, selectedItems: selectedItems.map(mapChatHistoryEmojiSelectedItem), react: react)
385+
}, makeReactionsWindow: { context, peerId, selectedItems, react, reactLarge in
386+
return ReactionsWindowController(context as! AccountContext, peerId: peerId, selectedItems: selectedItems.map(mapChatHistoryEmojiSelectedItem), react: react, reactLarge: reactLarge)
387387
}, currencyAmount: { context, amount, comission in
388388
return (context as! AccountContext).amount(amount: amount, comission: comission)
389389
}, currentCountriesConfiguration: { context in

‎packages/ChatUI/Sources/ChatUI/AddReactionManager.swift‎

Lines changed: 61 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ import InlineStickerLayer
1818

1919

2020

21+
private let largeReactionHoldDuration: Double = 0.8
22+
2123
public final class ContextAddReactionsListView : View, StickerFramesCollector {
2224

2325

@@ -43,7 +45,7 @@ public final class ContextAddReactionsListView : View, StickerFramesCollector {
4345

4446
private let parentWindow: Window?
4547

46-
required init(frame frameRect: NSRect, context: any AccountContextProtocol, reaction: ContextReaction, add: @escaping(MessageReaction.Reaction, Bool, NSRect?)->Void, theme: TelegramPresentationTheme, message: Message?, parentWindow: Window? = nil) {
48+
required init(frame frameRect: NSRect, context: any AccountContextProtocol, reaction: ContextReaction, add: @escaping(MessageReaction.Reaction, Bool, NSRect?)->Void, addLarge: ((MessageReaction.Reaction, NSRect?)->Void)?, theme: TelegramPresentationTheme, message: Message?, parentWindow: Window? = nil) {
4749

4850
let size: NSSize = reaction.isSelected ? NSMakeSize(25, 24) : NSMakeSize(frameRect.width, 30)
4951
let rect = CGRect(origin: .zero, size: size)
@@ -152,13 +154,32 @@ public final class ContextAddReactionsListView : View, StickerFramesCollector {
152154
}
153155

154156
set(handler: { control in
155-
if let window = control.window {
157+
if let window = control.window, (control as? ReactionView)?.largeInvoked != true {
156158
let wrect = control.convert(control.frame.size.bounds, to: nil)
157159
let srect = window.convertToScreen(wrect)
158160
add(reaction.value, true, getWindow().convertFromScreen(srect))
159161
}
160162
}, for: .Click)
161163

164+
if let addLarge, !reaction.isSelected, reaction.value != .stars {
165+
self.handleLongEvent = false
166+
self.largeHoldEnabled = true
167+
set(handler: { control in
168+
(control as? ReactionView)?.startLargeHold(completion: { control in
169+
if let window = control.window {
170+
let wrect = control.convert(control.frame.size.bounds, to: nil)
171+
let srect = window.convertToScreen(wrect)
172+
NSHapticFeedbackManager.defaultPerformer.perform(.levelChange, performanceTime: .default)
173+
addLarge(reaction.value, getWindow().convertFromScreen(srect))
174+
AppMenu.closeAll()
175+
}
176+
})
177+
}, for: .Down)
178+
set(handler: { control in
179+
(control as? ReactionView)?.cancelLargeHold()
180+
}, for: .Up)
181+
}
182+
162183
if case .stars = reaction.value, let message {
163184
self.set(handler: { _ in
164185
showModal(with: Star_ReactionsController(context: context, source: .message(.init(message))), for: getWindow())
@@ -210,6 +231,40 @@ public final class ContextAddReactionsListView : View, StickerFramesCollector {
210231
return context.isLite(.emoji)
211232
}
212233

234+
private var largeHoldTimer: SwiftSignalKit.Timer?
235+
private var largeHoldEnabled: Bool = false
236+
fileprivate private(set) var largeInvoked: Bool = false
237+
238+
fileprivate func startLargeHold(completion: @escaping(ReactionView)->Void) {
239+
largeHoldTimer?.invalidate()
240+
self.layer?.zPosition = 1
241+
self.layer?.animateScaleCenter(from: 1, to: 1.5, duration: largeReactionHoldDuration, removeOnCompletion: false)
242+
largeHoldTimer = SwiftSignalKit.Timer(timeout: largeReactionHoldDuration, repeat: false, completion: { [weak self] in
243+
guard let self else {
244+
return
245+
}
246+
self.largeHoldTimer = nil
247+
if self.mouseInside() {
248+
self.largeInvoked = true
249+
completion(self)
250+
} else {
251+
self.cancelLargeHold()
252+
}
253+
}, queue: .mainQueue())
254+
largeHoldTimer?.start()
255+
}
256+
257+
fileprivate func cancelLargeHold() {
258+
guard !largeInvoked else {
259+
return
260+
}
261+
largeHoldTimer?.invalidate()
262+
largeHoldTimer = nil
263+
let current = self.layer?.presentation()?.value(forKeyPath: "transform.scale") as? CGFloat ?? 1
264+
self.layer?.animateScaleCenter(from: current, to: 1, duration: 0.2, removeOnCompletion: true)
265+
self.layer?.zPosition = 0
266+
}
267+
213268
private func apply(_ data: Data, key: String, policy: LottiePlayPolicy) {
214269
let animation = LottieAnimation(compressed: data, key: LottieAnimationEntryKey(key: .bundle("reaction_\(reaction.value)_\(key)"), size: player.frame.size), type: .lottie, cachePurpose: .none, playPolicy: policy, maximumFps: 60, runOnQueue: Queue(), metalSupport: false)
215270
player.set(animation, reset: true, saveContext: true, animated: false)
@@ -258,9 +313,9 @@ public final class ContextAddReactionsListView : View, StickerFramesCollector {
258313
break
259314
}
260315

261-
if previous == .Hover, state == .Highlight {
316+
if !largeHoldEnabled, previous == .Hover, state == .Highlight {
262317
self.layer?.animateScaleCenter(from: 1, to: 0.8, duration: 0.2, removeOnCompletion: false)
263-
} else if state == .Hover && previous == .Highlight {
318+
} else if !largeHoldEnabled, state == .Hover, previous == .Highlight {
264319
self.layer?.animateScaleCenter(from: 0.8, to: 1, duration: 0.2, removeOnCompletion: true)
265320
}
266321
previous = state
@@ -358,7 +413,7 @@ public final class ContextAddReactionsListView : View, StickerFramesCollector {
358413
private let presentation: TelegramPresentationTheme
359414
private let hasBubble: Bool
360415
private let aboveText: TextViewLayout?
361-
public required init(frame frameRect: NSRect, context: any AccountContextProtocol, list: [ContextReaction], add:@escaping(MessageReaction.Reaction, Bool, NSRect?)->Void, radiusLayer: CGFloat? = 15, revealReactions:((ContextAddReactionsListView & StickerFramesCollector)->Void)? = nil, presentation: TelegramPresentationTheme = theme, hasBubble: Bool = true, aboveText: TextViewLayout? = nil, message: Message? = nil, parentWindow: Window? = nil) {
416+
public required init(frame frameRect: NSRect, context: any AccountContextProtocol, list: [ContextReaction], add:@escaping(MessageReaction.Reaction, Bool, NSRect?)->Void, addLarge: ((MessageReaction.Reaction, NSRect?)->Void)? = nil, radiusLayer: CGFloat? = 15, revealReactions:((ContextAddReactionsListView & StickerFramesCollector)->Void)? = nil, presentation: TelegramPresentationTheme = theme, hasBubble: Bool = true, aboveText: TextViewLayout? = nil, message: Message? = nil, parentWindow: Window? = nil) {
362417
self.list = list
363418
self.showMore = ShowMore(frame: NSMakeRect(0, 0, 34, 34), theme: presentation)
364419
self.revealReactions = revealReactions
@@ -464,7 +519,7 @@ public final class ContextAddReactionsListView : View, StickerFramesCollector {
464519
for reaction in list {
465520
let add:(ContextReaction)->Void = { reaction in
466521
let itemSize = size.bounds
467-
let reaction = ReactionView(frame: NSMakeRect(x, y, itemSize.width, itemSize.height), context: context, reaction: reaction, add: add, theme: presentation, message: message, parentWindow: parentWindow)
522+
let reaction = ReactionView(frame: NSMakeRect(x, y, itemSize.width, itemSize.height), context: context, reaction: reaction, add: add, addLarge: addLarge, theme: presentation, message: message, parentWindow: parentWindow)
468523

469524
self.documentView.addSubview(reaction)
470525
x += size.width + 4

‎packages/ChatUI/Sources/ChatUI/ChatController.swift‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6115,12 +6115,12 @@ open class ChatController<Context: AccountContextProtocol>: TelegramGenericViewC
61156115
}
61166116
}
61176117

6118-
chatInteraction.runReactionEffect = { [weak self] value, messageId in
6118+
chatInteraction.runReactionEffect = { [weak self] value, messageId, isLarge in
61196119
guard let strongSelf = self else {
61206120
return
61216121
}
61226122
if strongSelf.isOnScreen {
6123-
strongSelf.emojiEffects.addReactionAnimation(value, index: nil, messageId: messageId, animationSize: NSMakeSize(80, 80), viewFrame: context.window.bounds, for: context.window.contentView!)
6123+
strongSelf.emojiEffects.addReactionAnimation(value, index: nil, messageId: messageId, animationSize: NSMakeSize(80, 80), isLarge: isLarge, viewFrame: context.window.bounds, for: context.window.contentView!)
61246124
}
61256125
}
61266126

‎packages/ChatUI/Sources/ChatUI/ChatHistoryUIHooks.swift‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -332,7 +332,7 @@ public struct ChatHistoryUIHooks {
332332
public let storyLayoutViewSize: () -> NSSize
333333
public let giftStickers: (any AccountContextProtocol) -> Signal<[TelegramMediaFile], NoError>
334334
public let messageNewReactions: (Message, UpdateMessageReaction, Bool) -> [UpdateMessageReaction]
335-
public let makeReactionsWindow: (any AccountContextProtocol, PeerId, [ChatHistoryEmojiSelectedItem], @escaping (StickerPackItem, NSRect?) -> Void) -> ChatHistoryReactionsWindow
335+
public let makeReactionsWindow: (any AccountContextProtocol, PeerId, [ChatHistoryEmojiSelectedItem], @escaping (StickerPackItem, NSRect?) -> Void, ((StickerPackItem, NSRect?) -> Void)?) -> ChatHistoryReactionsWindow
336336
public let currencyAmount: (any AccountContextProtocol, CurrencyAmount, Double) -> CurrencyAmount
337337
public let currentCountriesConfiguration: (any AccountContextProtocol) -> Atomic<CountriesConfiguration>
338338
public let reactionSettings: (any AccountContextProtocol) -> ReactionSettings
@@ -384,7 +384,7 @@ public struct ChatHistoryUIHooks {
384384
storyLayoutViewSize: @escaping () -> NSSize,
385385
giftStickers: @escaping (any AccountContextProtocol) -> Signal<[TelegramMediaFile], NoError>,
386386
messageNewReactions: @escaping (Message, UpdateMessageReaction, Bool) -> [UpdateMessageReaction],
387-
makeReactionsWindow: @escaping (any AccountContextProtocol, PeerId, [ChatHistoryEmojiSelectedItem], @escaping (StickerPackItem, NSRect?) -> Void) -> ChatHistoryReactionsWindow,
387+
makeReactionsWindow: @escaping (any AccountContextProtocol, PeerId, [ChatHistoryEmojiSelectedItem], @escaping (StickerPackItem, NSRect?) -> Void, ((StickerPackItem, NSRect?) -> Void)?) -> ChatHistoryReactionsWindow,
388388
currencyAmount: @escaping (any AccountContextProtocol, CurrencyAmount, Double) -> CurrencyAmount,
389389
currentCountriesConfiguration: @escaping (any AccountContextProtocol) -> Atomic<CountriesConfiguration>,
390390
reactionSettings: @escaping (any AccountContextProtocol) -> ReactionSettings,

‎packages/ChatUI/Sources/ChatUI/ChatInterfaceInteraction.swift‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -374,7 +374,7 @@ public final class ChatInteraction : InterfaceObserver {
374374

375375
public var runEmojiScreenEffect:(String, Message, Bool, Bool)->Void = { _, _, _, _ in }
376376
public var runPremiumScreenEffect:(Message, Bool, Bool)->Void = { _, _, _ in }
377-
public var runReactionEffect:(MessageReaction.Reaction, MessageId)->Void = { _, _ in }
377+
public var runReactionEffect:(MessageReaction.Reaction, MessageId, Bool)->Void = { _, _, _ in }
378378

379379
public var toggleSendAs: (PeerId)->Void = { _ in }
380380

@@ -1269,7 +1269,7 @@ public final class ChatInteraction : InterfaceObserver {
12691269
self.joinGroupCall = { _, _ in }
12701270
self.runEmojiScreenEffect = { _, _, _, _ in }
12711271
self.runPremiumScreenEffect = { _, _, _ in }
1272-
self.runReactionEffect = { _, _ in }
1272+
self.runReactionEffect = { _, _, _ in }
12731273
self.toggleSendAs = { _ in }
12741274
self.showDeleterSetup = { _ in }
12751275
self.showEmojiUseTooltip = { }

‎packages/ChatUI/Sources/ChatUI/ChatReactionsView.swift‎

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -230,7 +230,7 @@ public final class ChatReactionsLayout {
230230
public let context: any AccountContextProtocol
231231
public let message: Message
232232
public let openInfo: (PeerId)->Void
233-
public let runEffect:(MessageReaction.Reaction)->Void
233+
public let runEffect:(MessageReaction.Reaction, Bool)->Void
234234
public let canViewList: Bool
235235
public let avatars:[Avatar]
236236
public var rect: CGRect = .zero
@@ -256,7 +256,7 @@ public final class ChatReactionsLayout {
256256
return self.value.value
257257
}
258258

259-
public init(value: MessageReaction, recentPeers:[Peer], canViewList: Bool, message: Message, savedMessageTags: SavedMessageTags?, currentTag: MessageReaction.Reaction?, context: any AccountContextProtocol, mode: ChatReactionsLayout.Mode, index: Int, source: Source, presentation: Theme, action:@escaping(MessageReaction.Reaction, Bool)->Void, openInfo: @escaping (PeerId)->Void, runEffect:@escaping(MessageReaction.Reaction)->Void, starReact: @escaping()->Void) {
259+
public init(value: MessageReaction, recentPeers:[Peer], canViewList: Bool, message: Message, savedMessageTags: SavedMessageTags?, currentTag: MessageReaction.Reaction?, context: any AccountContextProtocol, mode: ChatReactionsLayout.Mode, index: Int, source: Source, presentation: Theme, action:@escaping(MessageReaction.Reaction, Bool)->Void, openInfo: @escaping (PeerId)->Void, runEffect:@escaping(MessageReaction.Reaction, Bool)->Void, starReact: @escaping()->Void) {
260260
self.value = value
261261
self.index = index
262262
self.message = message
@@ -503,7 +503,7 @@ public final class ChatReactionsLayout {
503503
public let mode: Mode
504504

505505

506-
public init(context: any AccountContextProtocol, message: Message, available: AvailableReactions?, peerAllowed: PeerAllowedReactions?, savedMessageTags: SavedMessageTags?, engine:Reactions, theme: TelegramPresentationTheme, renderType: ChatItemRenderType, currentTag: MessageReaction.Reaction?, isIncoming: Bool, isOutOfBounds: Bool, hasWallpaper: Bool, stateOverlayTextColor: NSColor, openInfo:@escaping(PeerId)->Void, runEffect: @escaping(MessageReaction.Reaction)->Void, tagAction:@escaping(MessageReaction.Reaction)->Void, starReact:@escaping()->Void) {
506+
public init(context: any AccountContextProtocol, message: Message, available: AvailableReactions?, peerAllowed: PeerAllowedReactions?, savedMessageTags: SavedMessageTags?, engine:Reactions, theme: TelegramPresentationTheme, renderType: ChatItemRenderType, currentTag: MessageReaction.Reaction?, isIncoming: Bool, isOutOfBounds: Bool, hasWallpaper: Bool, stateOverlayTextColor: NSColor, openInfo:@escaping(PeerId)->Void, runEffect: @escaping(MessageReaction.Reaction, Bool)->Void, tagAction:@escaping(MessageReaction.Reaction)->Void, starReact:@escaping()->Void) {
507507

508508
var mode: Mode = .full
509509
if message.id.peerId == context.peerId {
@@ -2468,18 +2468,22 @@ final class ChatReactionsView : ChatLayerSceneView {
24682468
let completed: (Bool)->Void = { [weak view, weak self] _ in
24692469
view?.unlockVisibility()
24702470
DispatchQueue.main.async {
2471-
view?.playEffect()
2472-
selected.runEffect(selected.value.value)
2471+
if !interactive.isLarge {
2472+
view?.playEffect()
2473+
}
2474+
selected.runEffect(selected.value.value, interactive.isLarge)
24732475
NSHapticFeedbackManager.defaultPerformer.perform(.levelChange, performanceTime: .default)
24742476
if selected.value.value == .stars, let window = self?.window as? Window {
24752477
PlayConfetti(for: window, stars: true)
24762478
}
24772479
}
24782480
}
2479-
parabollicReactionAnimation(layer, fromPoint: from, toPoint: to, window: layout.context.window, completion: completed)
2481+
parabollicReactionAnimation(layer, fromPoint: from, toPoint: to, window: layout.context.window, completion: completed, duration: interactive.isLarge ? 0.5 : 0.2)
24802482
} else {
2481-
view.playEffect()
2482-
selected.runEffect(selected.value.value)
2483+
if !interactive.isLarge {
2484+
view.playEffect()
2485+
}
2486+
selected.runEffect(selected.value.value, interactive.isLarge)
24832487
}
24842488
}
24852489
}
@@ -2495,9 +2499,10 @@ final class ChatReactionsView : ChatLayerSceneView {
24952499
let layout = currentLayout.message.effectiveReactions(currentLayout.context.peerId, isTags: currentLayout.context.peerId == currentLayout.message.id.peerId)
24962500
let peer = layout?.recentPeers.first(where: { $0.isUnseen || !checkUnseen })
24972501
if let peer = peer, let reaction = reactions.first(where: { $0.value.value == peer.value }) {
2498-
reaction.runEffect(reaction.value.value)
2499-
let view = getView(reaction.value.value)
2500-
view?.playEffect()
2502+
reaction.runEffect(reaction.value.value, peer.isLarge)
2503+
if !peer.isLarge {
2504+
getView(reaction.value.value)?.playEffect()
2505+
}
25012506
}
25022507
}
25032508

0 commit comments

Comments
 (0)