mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-12-24 07:05:35 +00:00
Voice message transcription UI improvements
This commit is contained in:
@@ -128,6 +128,7 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
|
||||
|
||||
private var audioTranscriptionButton: ComponentHostView<Empty>?
|
||||
private let textNode: TextNode
|
||||
private let textClippingNode: ASDisplayNode
|
||||
private var textSelectionNode: TextSelectionNode?
|
||||
|
||||
var updateIsTextSelectionActive: ((Bool) -> Void)?
|
||||
@@ -203,6 +204,7 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
|
||||
return false
|
||||
}
|
||||
}
|
||||
private var isWaitingForCollapse: Bool = false
|
||||
|
||||
override init() {
|
||||
self.titleNode = TextNode()
|
||||
@@ -240,6 +242,10 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
|
||||
self.textNode.displaysAsynchronously = false
|
||||
self.textNode.isUserInteractionEnabled = false
|
||||
|
||||
self.textClippingNode = ASDisplayNode()
|
||||
self.textClippingNode.clipsToBounds = true
|
||||
self.textClippingNode.addSubnode(self.textNode)
|
||||
|
||||
self.dateAndStatusNode = ChatMessageDateAndStatusNode()
|
||||
|
||||
self.consumableContentNode = ASImageNode()
|
||||
@@ -375,17 +381,6 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
|
||||
return
|
||||
}
|
||||
strongSelf.transcribeDisposable = nil
|
||||
/*if let result = result {
|
||||
strongSelf.transcribedText = .success(EngineAudioTranscriptionResult.Success(id: 0, text: result))
|
||||
} else {
|
||||
strongSelf.transcribedText = .error
|
||||
}
|
||||
if strongSelf.transcribedText != nil {
|
||||
strongSelf.audioTranscriptionState = .expanded
|
||||
} else {
|
||||
strongSelf.audioTranscriptionState = .collapsed
|
||||
}
|
||||
strongSelf.requestUpdateLayout(true)*/
|
||||
})
|
||||
} else {
|
||||
self.transcribeDisposable = (context.engine.messages.transcribeAudio(messageId: message.id)
|
||||
@@ -394,9 +389,6 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
|
||||
return
|
||||
}
|
||||
strongSelf.transcribeDisposable = nil
|
||||
/*strongSelf.audioTranscriptionState = .expanded
|
||||
strongSelf.transcribedText = result
|
||||
strongSelf.requestUpdateLayout(true)*/
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -404,9 +396,10 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
|
||||
switch self.audioTranscriptionState {
|
||||
case .expanded:
|
||||
self.audioTranscriptionState = .collapsed
|
||||
self.isWaitingForCollapse = true
|
||||
self.requestUpdateLayout(true)
|
||||
case .collapsed:
|
||||
self.audioTranscriptionState = .expanded
|
||||
self.audioTranscriptionState = .inProgress
|
||||
self.requestUpdateLayout(true)
|
||||
default:
|
||||
break
|
||||
@@ -414,7 +407,7 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
|
||||
}
|
||||
}
|
||||
|
||||
func asyncLayout() -> (Arguments) -> (CGFloat, (CGSize) -> (CGFloat, (CGFloat) -> (CGSize, (Bool, ListViewItemUpdateAnimation) -> Void))) {
|
||||
func asyncLayout() -> (Arguments) -> (CGFloat, (CGSize) -> (CGFloat, (CGFloat) -> (CGSize, (Bool, ListViewItemUpdateAnimation, ListViewItemApply?) -> Void))) {
|
||||
let currentFile = self.file
|
||||
|
||||
let titleAsyncLayout = TextNode.asyncLayout(self.titleNode)
|
||||
@@ -807,7 +800,7 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
|
||||
streamingCacheStatusFrame = CGRect()
|
||||
}
|
||||
|
||||
return (fittedLayoutSize, { [weak self] synchronousLoads, animation in
|
||||
return (fittedLayoutSize, { [weak self] synchronousLoads, animation, info in
|
||||
if let strongSelf = self {
|
||||
strongSelf.context = arguments.context
|
||||
strongSelf.presentationData = arguments.presentationData
|
||||
@@ -825,6 +818,16 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
|
||||
|
||||
if let updatedAudioTranscriptionState = updatedAudioTranscriptionState {
|
||||
strongSelf.audioTranscriptionState = updatedAudioTranscriptionState
|
||||
|
||||
switch updatedAudioTranscriptionState {
|
||||
case .expanded:
|
||||
info?.setInvertOffsetDirection()
|
||||
default:
|
||||
break
|
||||
}
|
||||
} else if strongSelf.isWaitingForCollapse {
|
||||
strongSelf.isWaitingForCollapse = false
|
||||
info?.setInvertOffsetDirection()
|
||||
}
|
||||
|
||||
if let consumableContentIcon = consumableContentIcon {
|
||||
@@ -849,7 +852,7 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
|
||||
if textString == nil, strongSelf.textNode.supernode != nil, animation.isAnimated {
|
||||
if let snapshotView = strongSelf.textNode.view.snapshotContentTree() {
|
||||
snapshotView.frame = strongSelf.textNode.frame
|
||||
strongSelf.view.insertSubview(snapshotView, aboveSubview: strongSelf.textNode.view)
|
||||
strongSelf.textClippingNode.view.insertSubview(snapshotView, aboveSubview: strongSelf.textNode.view)
|
||||
|
||||
snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak snapshotView] _ in
|
||||
snapshotView?.removeFromSuperview()
|
||||
@@ -859,24 +862,72 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
|
||||
|
||||
let _ = textApply()
|
||||
let textFrame = CGRect(origin: CGPoint(x: arguments.layoutConstants.text.bubbleInsets.left - arguments.layoutConstants.file.bubbleInsets.left, y: statusReferenceFrame.maxY + 1.0), size: textLayout.size)
|
||||
strongSelf.textNode.frame = textFrame
|
||||
let textClippingFrame = CGRect(origin: textFrame.origin, size: CGSize(width: textFrame.width, height: textFrame.height + 8.0))
|
||||
if textString != nil {
|
||||
if strongSelf.textNode.supernode == nil {
|
||||
strongSelf.addSubnode(strongSelf.textNode)
|
||||
strongSelf.textClippingNode.frame = textClippingFrame
|
||||
strongSelf.textNode.frame = CGRect(origin: CGPoint(), size: textFrame.size)
|
||||
|
||||
if strongSelf.textClippingNode.supernode == nil {
|
||||
strongSelf.addSubnode(strongSelf.textClippingNode)
|
||||
if animation.isAnimated {
|
||||
strongSelf.textNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||
|
||||
strongSelf.textClippingNode.frame = CGRect(origin: textClippingFrame.origin, size: CGSize(width: textClippingFrame.width, height: 0.0))
|
||||
animation.animator.updateFrame(layer: strongSelf.textClippingNode.layer, frame: textClippingFrame, completion: nil)
|
||||
|
||||
if let maskImage = generateGradientImage(size: CGSize(width: 8.0, height: 10.0), colors: [UIColor.black, UIColor.black, UIColor.clear], locations: [0.0, 0.1, 1.0], direction: .vertical) {
|
||||
let maskView = UIImageView(image: maskImage.stretchableImage(withLeftCapWidth: 0, topCapHeight: 1))
|
||||
strongSelf.textClippingNode.view.mask = maskView
|
||||
|
||||
maskView.frame = CGRect(origin: CGPoint(), size: CGSize(width: textClippingFrame.width, height: maskImage.size.height))
|
||||
animation.animator.updateFrame(layer: maskView.layer, frame: CGRect(origin: CGPoint(), size: textClippingFrame.size), completion: { [weak maskView] _ in
|
||||
maskView?.removeFromSuperview()
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
strongSelf.textClippingNode.view.mask = nil
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if strongSelf.textNode.supernode != nil {
|
||||
strongSelf.textNode.removeFromSupernode()
|
||||
if strongSelf.textClippingNode.supernode != nil {
|
||||
if animation.isAnimated {
|
||||
if let maskImage = generateGradientImage(size: CGSize(width: 8.0, height: 10.0), colors: [UIColor.black, UIColor.black, UIColor.clear], locations: [0.0, 0.1, 1.0], direction: .vertical) {
|
||||
let maskView = UIImageView(image: maskImage.stretchableImage(withLeftCapWidth: 0, topCapHeight: 1))
|
||||
maskView.frame = CGRect(origin: CGPoint(), size: strongSelf.textClippingNode.bounds.size)
|
||||
|
||||
strongSelf.textClippingNode.view.mask = maskView
|
||||
|
||||
animation.animator.updateFrame(layer: maskView.layer, frame: CGRect(origin: CGPoint(), size: CGSize(width: strongSelf.textClippingNode.bounds.width, height: maskImage.size.height)), completion: { [weak maskView] _ in
|
||||
maskView?.removeFromSuperview()
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
strongSelf.textClippingNode.view.mask = nil
|
||||
})
|
||||
}
|
||||
|
||||
animation.animator.updateFrame(layer: strongSelf.textClippingNode.layer, frame: CGRect(origin: strongSelf.textClippingNode.frame.origin, size: CGSize(width: strongSelf.textClippingNode.bounds.width, height: 0.0)), completion: nil)
|
||||
|
||||
strongSelf.textNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { completed in
|
||||
guard let strongSelf = self, completed else {
|
||||
return
|
||||
}
|
||||
|
||||
strongSelf.textClippingNode.removeFromSupernode()
|
||||
strongSelf.textNode.layer.removeAllAnimations()
|
||||
})
|
||||
} else {
|
||||
strongSelf.textClippingNode.removeFromSupernode()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let textSelectionNode = strongSelf.textSelectionNode {
|
||||
let shouldUpdateLayout = textSelectionNode.frame.size != textFrame.size
|
||||
textSelectionNode.frame = textFrame
|
||||
textSelectionNode.highlightAreaNode.frame = textFrame
|
||||
textSelectionNode.frame = CGRect(origin: CGPoint(), size: textFrame.size)
|
||||
textSelectionNode.highlightAreaNode.frame = CGRect(origin: CGPoint(), size: textFrame.size)
|
||||
if shouldUpdateLayout {
|
||||
textSelectionNode.updateLayout()
|
||||
}
|
||||
@@ -906,83 +957,6 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
|
||||
scrubbingFrame.size.width -= 30.0 + 4.0
|
||||
}
|
||||
|
||||
/*if strongSelf.waveformScrubbingNode == nil {
|
||||
let waveformScrubbingNode = MediaPlayerScrubbingNode(content: .custom(backgroundNode: strongSelf.waveformNode, foregroundContentNode: strongSelf.waveformForegroundNode))
|
||||
waveformScrubbingNode.hitTestSlop = UIEdgeInsets(top: -10.0, left: 0.0, bottom: -10.0, right: 0.0)
|
||||
waveformScrubbingNode.seek = { timestamp in
|
||||
if let strongSelf = self, let context = strongSelf.context, let message = strongSelf.message, let type = peerMessageMediaPlayerType(message) {
|
||||
context.sharedContext.mediaManager.playlistControl(.seek(timestamp), type: type)
|
||||
}
|
||||
}
|
||||
waveformScrubbingNode.status = strongSelf.playbackStatus.get()
|
||||
strongSelf.waveformScrubbingNode = waveformScrubbingNode
|
||||
strongSelf.addSubnode(waveformScrubbingNode)
|
||||
}
|
||||
|
||||
if case .inProgress = audioTranscriptionState {
|
||||
if strongSelf.waveformShimmerNode == nil {
|
||||
let waveformShimmerNode = ShimmerEffectNode()
|
||||
strongSelf.waveformShimmerNode = waveformShimmerNode
|
||||
strongSelf.addSubnode(waveformShimmerNode)
|
||||
|
||||
let waveformMaskNode = AudioWaveformNode()
|
||||
strongSelf.waveformMaskNode = waveformMaskNode
|
||||
waveformShimmerNode.view.mask = waveformMaskNode.view
|
||||
}
|
||||
|
||||
if let audioWaveform = audioWaveform, let waveformShimmerNode = strongSelf.waveformShimmerNode, let waveformMaskNode = strongSelf.waveformMaskNode {
|
||||
waveformShimmerNode.frame = scrubbingFrame
|
||||
waveformShimmerNode.updateAbsoluteRect(scrubbingFrame, within: CGSize(width: scrubbingFrame.size.width + 60.0, height: scrubbingFrame.size.height + 4.0))
|
||||
|
||||
var shapes: [ShimmerEffectNode.Shape] = []
|
||||
shapes.append(.rect(rect: CGRect(origin: CGPoint(), size: scrubbingFrame.size)))
|
||||
waveformShimmerNode.update(
|
||||
backgroundColor: .blue,
|
||||
foregroundColor: messageTheme.mediaInactiveControlColor,
|
||||
shimmeringColor: messageTheme.mediaActiveControlColor,
|
||||
shapes: shapes,
|
||||
horizontal: true,
|
||||
effectSize: 60.0,
|
||||
globalTimeOffset: false,
|
||||
duration: 0.7,
|
||||
size: scrubbingFrame.size
|
||||
)
|
||||
|
||||
waveformMaskNode.frame = CGRect(origin: CGPoint(), size: scrubbingFrame.size)
|
||||
waveformMaskNode.setup(color: .black, gravity: .bottom, waveform: audioWaveform)
|
||||
}
|
||||
} else {
|
||||
if let waveformShimmerNode = strongSelf.waveformShimmerNode {
|
||||
strongSelf.waveformShimmerNode = nil
|
||||
if animation.isAnimated {
|
||||
waveformShimmerNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak waveformShimmerNode] _ in
|
||||
waveformShimmerNode?.removeFromSupernode()
|
||||
})
|
||||
} else {
|
||||
waveformShimmerNode.removeFromSupernode()
|
||||
}
|
||||
}
|
||||
strongSelf.waveformMaskNode = nil
|
||||
}
|
||||
|
||||
if let waveformScrubbingNode = strongSelf.waveformScrubbingNode {
|
||||
waveformScrubbingNode.frame = scrubbingFrame
|
||||
//animation.animator.updateFrame(layer: waveformScrubbingNode.layer, frame: scrubbingFrame, completion: nil)
|
||||
//waveformScrubbingNode.update(size: scrubbingFrame.size, animator: animation.animator)
|
||||
}
|
||||
let waveformColor: UIColor
|
||||
if arguments.incoming {
|
||||
if consumableContentIcon != nil {
|
||||
waveformColor = messageTheme.mediaActiveControlColor
|
||||
} else {
|
||||
waveformColor = messageTheme.mediaInactiveControlColor
|
||||
}
|
||||
} else {
|
||||
waveformColor = messageTheme.mediaInactiveControlColor
|
||||
}
|
||||
strongSelf.waveformNode.setup(color: waveformColor, gravity: .bottom, waveform: audioWaveform)
|
||||
strongSelf.waveformForegroundNode.setup(color: messageTheme.mediaActiveControlColor, gravity: .bottom, waveform: audioWaveform)*/
|
||||
|
||||
let waveformView: ComponentHostView<Empty>
|
||||
let waveformTransition: Transition
|
||||
if let current = strongSelf.waveformView {
|
||||
@@ -1070,10 +1044,6 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
/*if let waveformScrubbingNode = strongSelf.waveformScrubbingNode {
|
||||
strongSelf.waveformScrubbingNode = nil
|
||||
waveformScrubbingNode.removeFromSupernode()
|
||||
}*/
|
||||
if let waveformView = strongSelf.waveformView {
|
||||
strongSelf.waveformView = nil
|
||||
waveformView.removeFromSuperview()
|
||||
@@ -1144,7 +1114,6 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
|
||||
}))
|
||||
}
|
||||
|
||||
//strongSelf.waveformNode.displaysAsynchronously = !arguments.presentationData.isPreview
|
||||
strongSelf.statusNode?.displaysAsynchronously = !arguments.presentationData.isPreview
|
||||
strongSelf.statusNode?.frame = CGRect(origin: CGPoint(), size: progressFrame.size)
|
||||
|
||||
@@ -1530,12 +1499,12 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
|
||||
self.fetchingCompactTextNode.frame = CGRect(origin: self.descriptionNode.frame.origin, size: fetchingCompactSize)
|
||||
}
|
||||
|
||||
static func asyncLayout(_ node: ChatMessageInteractiveFileNode?) -> (Arguments) -> (CGFloat, (CGSize) -> (CGFloat, (CGFloat) -> (CGSize, (Bool, ListViewItemUpdateAnimation) -> ChatMessageInteractiveFileNode))) {
|
||||
static func asyncLayout(_ node: ChatMessageInteractiveFileNode?) -> (Arguments) -> (CGFloat, (CGSize) -> (CGFloat, (CGFloat) -> (CGSize, (Bool, ListViewItemUpdateAnimation, ListViewItemApply?) -> ChatMessageInteractiveFileNode))) {
|
||||
let currentAsyncLayout = node?.asyncLayout()
|
||||
|
||||
return { arguments in
|
||||
var fileNode: ChatMessageInteractiveFileNode
|
||||
var fileLayout: (Arguments) -> (CGFloat, (CGSize) -> (CGFloat, (CGFloat) -> (CGSize, (Bool, ListViewItemUpdateAnimation) -> Void)))
|
||||
var fileLayout: (Arguments) -> (CGFloat, (CGSize) -> (CGFloat, (CGFloat) -> (CGSize, (Bool, ListViewItemUpdateAnimation, ListViewItemApply?) -> Void)))
|
||||
|
||||
if let node = node, let currentAsyncLayout = currentAsyncLayout {
|
||||
fileNode = node
|
||||
@@ -1553,8 +1522,8 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
|
||||
return (finalWidth, { boundingWidth in
|
||||
let (finalSize, apply) = finalLayout(boundingWidth)
|
||||
|
||||
return (finalSize, { synchronousLoads, animation in
|
||||
apply(synchronousLoads, animation)
|
||||
return (finalSize, { synchronousLoads, animation, applyInfo in
|
||||
apply(synchronousLoads, animation, applyInfo)
|
||||
return fileNode
|
||||
})
|
||||
})
|
||||
@@ -1577,7 +1546,7 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
|
||||
|
||||
func updateIsExtractedToContextPreview(_ value: Bool) {
|
||||
if value {
|
||||
if self.textSelectionNode == nil, self.textNode.supernode != nil, let item = self.arguments, !item.associatedData.isCopyProtectionEnabled && !item.message.isCopyProtected(), let rootNode = item.controllerInteraction.chatControllerNode() {
|
||||
if self.textSelectionNode == nil, self.textClippingNode.supernode != nil, let item = self.arguments, !item.associatedData.isCopyProtectionEnabled && !item.message.isCopyProtected(), let rootNode = item.controllerInteraction.chatControllerNode() {
|
||||
let selectionColor: UIColor
|
||||
let knobColor: UIColor
|
||||
if item.message.effectivelyIncoming(item.context.account.peerId) {
|
||||
@@ -1599,8 +1568,8 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
|
||||
item.controllerInteraction.performTextSelectionAction(item.message.stableId, text, action)
|
||||
})
|
||||
self.textSelectionNode = textSelectionNode
|
||||
self.addSubnode(textSelectionNode)
|
||||
self.insertSubnode(textSelectionNode.highlightAreaNode, belowSubnode: self.textNode)
|
||||
self.textClippingNode.addSubnode(textSelectionNode)
|
||||
self.textClippingNode.insertSubnode(textSelectionNode.highlightAreaNode, belowSubnode: self.textNode)
|
||||
textSelectionNode.frame = self.textNode.frame
|
||||
textSelectionNode.highlightAreaNode.frame = self.textNode.frame
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user