mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-07-31 15:37:01 +00:00
Merge commit '354be3e42499a871ecd808c334a9370f6e1d4279'
This commit is contained in:
commit
5cd85de807
@ -597,7 +597,7 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, UIScroll
|
||||
}
|
||||
|
||||
let scrubberFrame = CGRect(origin: CGPoint(x: leftInset, y: scrubberY), size: CGSize(width: width - leftInset - rightInset, height: 34.0))
|
||||
scrubberView.updateLayout(size: size, leftInset: leftInset, rightInset: rightInset)
|
||||
scrubberView.updateLayout(size: size, leftInset: leftInset, rightInset: rightInset, transition: .immediate)
|
||||
transition.updateFrame(layer: scrubberView.layer, frame: scrubberFrame)
|
||||
}
|
||||
transition.updateAlpha(node: self.textNode, alpha: displayCaption ? 1.0 : 0.0)
|
||||
|
@ -21,6 +21,10 @@ final class ChatVideoGalleryItemScrubberView: UIView {
|
||||
private var playbackStatus: MediaPlayerStatus?
|
||||
|
||||
private var fetchStatusDisposable = MetaDisposable()
|
||||
private var scrubbingDisposable = MetaDisposable()
|
||||
|
||||
private var leftTimestampNodePushed = false
|
||||
private var rightTimestampNodePushed = false
|
||||
|
||||
var hideWhenDurationIsUnknown = false {
|
||||
didSet {
|
||||
@ -103,11 +107,16 @@ final class ChatVideoGalleryItemScrubberView: UIView {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.scrubbingDisposable.dispose()
|
||||
self.fetchStatusDisposable.dispose()
|
||||
}
|
||||
|
||||
func setStatusSignal(_ status: Signal<MediaPlayerStatus, NoError>?) {
|
||||
let mappedStatus: Signal<MediaPlayerStatus, NoError>?
|
||||
if let status = status {
|
||||
mappedStatus = combineLatest(status, self.scrubberNode.scrubbingTimestamp) |> map { status, scrubbingTimestamp -> MediaPlayerStatus in
|
||||
return MediaPlayerStatus(generationTimestamp: status.generationTimestamp, duration: status.duration, dimensions: status.dimensions, timestamp: scrubbingTimestamp ?? status.timestamp, baseRate: status.baseRate, seekId: status.seekId, status: status.status, soundEnabled: status.soundEnabled)
|
||||
return MediaPlayerStatus(generationTimestamp: scrubbingTimestamp != nil ? 0 : status.generationTimestamp, duration: status.duration, dimensions: status.dimensions, timestamp: scrubbingTimestamp ?? status.timestamp, baseRate: status.baseRate, seekId: status.seekId, status: status.status, soundEnabled: status.soundEnabled)
|
||||
}
|
||||
} else {
|
||||
mappedStatus = nil
|
||||
@ -115,6 +124,30 @@ final class ChatVideoGalleryItemScrubberView: UIView {
|
||||
self.scrubberNode.status = mappedStatus
|
||||
self.leftTimestampNode.status = mappedStatus
|
||||
self.rightTimestampNode.status = mappedStatus
|
||||
|
||||
self.scrubbingDisposable.set((self.scrubberNode.scrubbingPosition
|
||||
|> deliverOnMainQueue).start(next: { [weak self] value in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
let leftTimestampNodePushed: Bool
|
||||
let rightTimestampNodePushed: Bool
|
||||
if let value = value {
|
||||
leftTimestampNodePushed = value < 0.16
|
||||
rightTimestampNodePushed = value > 0.84
|
||||
} else {
|
||||
leftTimestampNodePushed = false
|
||||
rightTimestampNodePushed = false
|
||||
}
|
||||
if leftTimestampNodePushed != strongSelf.leftTimestampNodePushed || rightTimestampNodePushed != strongSelf.rightTimestampNodePushed {
|
||||
strongSelf.leftTimestampNodePushed = leftTimestampNodePushed
|
||||
strongSelf.rightTimestampNodePushed = rightTimestampNodePushed
|
||||
|
||||
if let layout = strongSelf.containerLayout {
|
||||
strongSelf.updateLayout(size: layout.0, leftInset: layout.1, rightInset: layout.2, transition: .animated(duration: 0.35, curve: .spring))
|
||||
}
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
func setBufferingStatusSignal(_ status: Signal<(IndexSet, Int)?, NoError>?) {
|
||||
@ -139,7 +172,7 @@ final class ChatVideoGalleryItemScrubberView: UIView {
|
||||
strongSelf.fileSizeNode.attributedText = NSAttributedString(string: text, font: textFont, textColor: .white)
|
||||
|
||||
if let (size, leftInset, rightInset) = strongSelf.containerLayout {
|
||||
strongSelf.updateLayout(size: size, leftInset: leftInset, rightInset: rightInset)
|
||||
strongSelf.updateLayout(size: size, leftInset: leftInset, rightInset: rightInset, transition: .immediate)
|
||||
}
|
||||
}
|
||||
}))
|
||||
@ -151,22 +184,25 @@ final class ChatVideoGalleryItemScrubberView: UIView {
|
||||
}
|
||||
}
|
||||
|
||||
func updateLayout(size: CGSize, leftInset: CGFloat, rightInset: CGFloat) {
|
||||
func updateLayout(size: CGSize, leftInset: CGFloat, rightInset: CGFloat, transition: ContainedViewLayoutTransition) {
|
||||
self.containerLayout = (size, leftInset, rightInset)
|
||||
|
||||
let scrubberHeight: CGFloat = 14.0
|
||||
let scrubberInset: CGFloat
|
||||
let timestampOffset: CGFloat
|
||||
let leftTimestampOffset: CGFloat
|
||||
let rightTimestampOffset: CGFloat
|
||||
if size.width > size.height {
|
||||
scrubberInset = 58.0
|
||||
timestampOffset = 4.0
|
||||
leftTimestampOffset = 4.0
|
||||
rightTimestampOffset = 4.0
|
||||
} else {
|
||||
scrubberInset = 13.0
|
||||
timestampOffset = 22.0
|
||||
leftTimestampOffset = 22.0 + (self.leftTimestampNodePushed ? 8.0 : 0.0)
|
||||
rightTimestampOffset = 22.0 + (self.rightTimestampNodePushed ? 8.0 : 0.0)
|
||||
}
|
||||
|
||||
self.leftTimestampNode.frame = CGRect(origin: CGPoint(x: 12.0, y: timestampOffset), size: CGSize(width: 60.0, height: 20.0))
|
||||
self.rightTimestampNode.frame = CGRect(origin: CGPoint(x: size.width - leftInset - rightInset - 60.0 - 12.0, y: timestampOffset), size: CGSize(width: 60.0, height: 20.0))
|
||||
transition.updateFrame(node: self.leftTimestampNode, frame: CGRect(origin: CGPoint(x: 12.0, y: leftTimestampOffset), size: CGSize(width: 60.0, height: 20.0)))
|
||||
transition.updateFrame(node: self.rightTimestampNode, frame: CGRect(origin: CGPoint(x: size.width - leftInset - rightInset - 60.0 - 12.0, y: rightTimestampOffset), size: CGSize(width: 60.0, height: 20.0)))
|
||||
|
||||
let fileSize = self.fileSizeNode.measure(size)
|
||||
self.fileSizeNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - fileSize.width) / 2.0), y: 22.0), size: fileSize)
|
||||
|
@ -23,6 +23,8 @@ private final class MediaPlayerScrubbingNodeButton: ASDisplayNode {
|
||||
var endScrubbing: ((Bool) -> Void)?
|
||||
var updateScrubbing: ((CGFloat) -> Void)?
|
||||
|
||||
var highlighted: ((Bool) -> Void)?
|
||||
|
||||
private var scrubbingStartLocation: CGPoint?
|
||||
|
||||
override func didLoad() {
|
||||
@ -32,6 +34,28 @@ private final class MediaPlayerScrubbingNodeButton: ASDisplayNode {
|
||||
self.view.addGestureRecognizer(UIPanGestureRecognizer(target: self, action: #selector(self.panGesture(_:))))
|
||||
}
|
||||
|
||||
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
|
||||
super.touchesBegan(touches, with: event)
|
||||
|
||||
self.highlighted?(true)
|
||||
}
|
||||
|
||||
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
|
||||
super.touchesEnded(touches, with: event)
|
||||
|
||||
if self.scrubbingStartLocation == nil {
|
||||
self.highlighted?(false)
|
||||
}
|
||||
}
|
||||
|
||||
override func touchesCancelled(_ touches: Set<UITouch>?, with event: UIEvent?) {
|
||||
super.touchesCancelled(touches, with: event)
|
||||
|
||||
if self.scrubbingStartLocation == nil {
|
||||
self.highlighted?(false)
|
||||
}
|
||||
}
|
||||
|
||||
@objc func panGesture(_ recognizer: UIPanGestureRecognizer) {
|
||||
var location = recognizer.location(in: self.view)
|
||||
location.x -= self.bounds.minX
|
||||
@ -50,6 +74,7 @@ private final class MediaPlayerScrubbingNodeButton: ASDisplayNode {
|
||||
let delta = location.x - scrubbingStartLocation.x
|
||||
self.updateScrubbing?(delta / self.bounds.size.width)
|
||||
self.endScrubbing?(recognizer.state == .ended)
|
||||
self.highlighted?(false)
|
||||
}
|
||||
default:
|
||||
break
|
||||
@ -94,9 +119,10 @@ private final class StandardMediaPlayerScrubbingNodeContentNode {
|
||||
let foregroundNode: MediaPlayerScrubbingForegroundNode
|
||||
let handle: MediaPlayerScrubbingNodeHandle
|
||||
let handleNode: ASDisplayNode?
|
||||
let highlightedHandleNode: ASDisplayNode?
|
||||
let handleNodeContainer: MediaPlayerScrubbingNodeButton?
|
||||
|
||||
init(lineHeight: CGFloat, lineCap: MediaPlayerScrubbingNodeCap, backgroundNode: ASImageNode, bufferingNode: MediaPlayerScrubbingBufferingNode, foregroundContentNode: ASImageNode, foregroundNode: MediaPlayerScrubbingForegroundNode, handle: MediaPlayerScrubbingNodeHandle, handleNode: ASDisplayNode?, handleNodeContainer: MediaPlayerScrubbingNodeButton?) {
|
||||
init(lineHeight: CGFloat, lineCap: MediaPlayerScrubbingNodeCap, backgroundNode: ASImageNode, bufferingNode: MediaPlayerScrubbingBufferingNode, foregroundContentNode: ASImageNode, foregroundNode: MediaPlayerScrubbingForegroundNode, handle: MediaPlayerScrubbingNodeHandle, handleNode: ASDisplayNode?, highlightedHandleNode: ASDisplayNode?, handleNodeContainer: MediaPlayerScrubbingNodeButton?) {
|
||||
self.lineHeight = lineHeight
|
||||
self.lineCap = lineCap
|
||||
self.backgroundNode = backgroundNode
|
||||
@ -105,6 +131,7 @@ private final class StandardMediaPlayerScrubbingNodeContentNode {
|
||||
self.foregroundNode = foregroundNode
|
||||
self.handle = handle
|
||||
self.handleNode = handleNode
|
||||
self.highlightedHandleNode = highlightedHandleNode
|
||||
self.handleNodeContainer = handleNodeContainer
|
||||
}
|
||||
}
|
||||
@ -195,6 +222,11 @@ public final class MediaPlayerScrubbingNode: ASDisplayNode {
|
||||
return self._scrubbingTimestamp.get()
|
||||
}
|
||||
|
||||
private let _scrubbingPosition = Promise<Double?>(nil)
|
||||
public var scrubbingPosition: Signal<Double?, NoError> {
|
||||
return self._scrubbingPosition.get()
|
||||
}
|
||||
|
||||
public var ignoreSeekId: Int?
|
||||
|
||||
public var enableScrubbing: Bool = true {
|
||||
@ -288,6 +320,7 @@ public final class MediaPlayerScrubbingNode: ASDisplayNode {
|
||||
foregroundNode.clipsToBounds = true
|
||||
|
||||
var handleNodeImpl: ASImageNode?
|
||||
var highlightedHandleNodeImpl: ASImageNode?
|
||||
var handleNodeContainerImpl: MediaPlayerScrubbingNodeButton?
|
||||
|
||||
switch scrubberHandle {
|
||||
@ -304,18 +337,27 @@ public final class MediaPlayerScrubbingNode: ASDisplayNode {
|
||||
handleNodeContainerImpl = handleNodeContainer
|
||||
case .circle:
|
||||
let handleNode = ASImageNode()
|
||||
handleNode.image = generateFilledCircleImage(diameter: lineHeight + 4.0, color: foregroundColor)
|
||||
handleNode.image = generateFilledCircleImage(diameter: lineHeight + 3.0, color: foregroundColor)
|
||||
handleNode.isLayerBacked = true
|
||||
handleNodeImpl = handleNode
|
||||
|
||||
let highlightedHandleNode = ASImageNode()
|
||||
let highlightedHandleImage = generateFilledCircleImage(diameter: lineHeight + 3.0 + 20.0, color: foregroundColor)!
|
||||
highlightedHandleNode.image = highlightedHandleImage
|
||||
highlightedHandleNode.bounds = CGRect(origin: CGPoint(), size: highlightedHandleImage.size)
|
||||
highlightedHandleNode.isLayerBacked = true
|
||||
highlightedHandleNode.transform = CATransform3DMakeScale(0.1875, 0.1875, 1.0)
|
||||
highlightedHandleNodeImpl = highlightedHandleNode
|
||||
|
||||
let handleNodeContainer = MediaPlayerScrubbingNodeButton()
|
||||
handleNodeContainer.addSubnode(handleNode)
|
||||
handleNodeContainer.addSubnode(highlightedHandleNode)
|
||||
handleNodeContainerImpl = handleNodeContainer
|
||||
}
|
||||
|
||||
handleNodeContainerImpl?.isUserInteractionEnabled = enableScrubbing
|
||||
|
||||
return .standard(StandardMediaPlayerScrubbingNodeContentNode(lineHeight: lineHeight, lineCap: lineCap, backgroundNode: backgroundNode, bufferingNode: bufferingNode, foregroundContentNode: foregroundContentNode, foregroundNode: foregroundNode, handle: scrubberHandle, handleNode: handleNodeImpl, handleNodeContainer: handleNodeContainerImpl))
|
||||
return .standard(StandardMediaPlayerScrubbingNodeContentNode(lineHeight: lineHeight, lineCap: lineCap, backgroundNode: backgroundNode, bufferingNode: bufferingNode, foregroundContentNode: foregroundContentNode, foregroundNode: foregroundNode, handle: scrubberHandle, handleNode: handleNodeImpl, highlightedHandleNode: highlightedHandleNodeImpl, handleNodeContainer: handleNodeContainerImpl))
|
||||
case let .custom(backgroundNode, foregroundContentNode):
|
||||
let foregroundNode = MediaPlayerScrubbingForegroundNode()
|
||||
foregroundNode.isLayerBacked = true
|
||||
@ -373,12 +415,39 @@ public final class MediaPlayerScrubbingNode: ASDisplayNode {
|
||||
|
||||
if let handleNodeContainer = node.handleNodeContainer {
|
||||
self.addSubnode(handleNodeContainer)
|
||||
handleNodeContainer.highlighted = { [weak self] highlighted in
|
||||
if let strongSelf = self, let highlightedHandleNode = node.highlightedHandleNode, let statusValue = strongSelf.statusValue, Double(0.0).isLess(than: statusValue.duration) {
|
||||
if highlighted {
|
||||
strongSelf.displayLink?.isPaused = true
|
||||
|
||||
var timestamp = statusValue.timestamp
|
||||
if statusValue.generationTimestamp > 0 {
|
||||
let currentTimestamp = CACurrentMediaTime()
|
||||
timestamp = timestamp + (currentTimestamp - statusValue.generationTimestamp) * statusValue.baseRate
|
||||
}
|
||||
strongSelf.scrubbingTimestampValue = timestamp
|
||||
strongSelf._scrubbingTimestamp.set(.single(strongSelf.scrubbingTimestampValue))
|
||||
strongSelf._scrubbingPosition.set(.single(strongSelf.scrubbingTimestampValue.flatMap { $0 / statusValue.duration }))
|
||||
|
||||
highlightedHandleNode.layer.animateSpring(from: 0.1875 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.65, initialVelocity: 0.0, damping: 80.0, removeOnCompletion: false)
|
||||
} else {
|
||||
strongSelf.scrubbingTimestampValue = nil
|
||||
strongSelf._scrubbingTimestamp.set(.single(nil))
|
||||
strongSelf._scrubbingPosition.set(.single(nil))
|
||||
strongSelf.updateProgressAnimations()
|
||||
|
||||
highlightedHandleNode.layer.animateSpring(from: 1.0 as NSNumber, to: 0.1875 as NSNumber, keyPath: "transform.scale", duration: 0.65, initialVelocity: 0.0, damping: 120.0, removeOnCompletion: false)
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
handleNodeContainer.beginScrubbing = { [weak self] in
|
||||
if let strongSelf = self {
|
||||
if let statusValue = strongSelf.statusValue, Double(0.0).isLess(than: statusValue.duration) {
|
||||
strongSelf.scrubbingBeginTimestamp = statusValue.timestamp
|
||||
strongSelf.scrubbingTimestampValue = statusValue.timestamp
|
||||
strongSelf._scrubbingTimestamp.set(.single(strongSelf.scrubbingTimestampValue))
|
||||
strongSelf._scrubbingPosition.set(.single(strongSelf.scrubbingTimestampValue.flatMap { $0 / statusValue.duration }))
|
||||
strongSelf.update?(strongSelf.scrubbingTimestampValue, CGFloat(statusValue.timestamp / statusValue.duration))
|
||||
strongSelf.updateProgressAnimations()
|
||||
}
|
||||
@ -390,6 +459,7 @@ public final class MediaPlayerScrubbingNode: ASDisplayNode {
|
||||
let timestampValue = max(0.0, min(statusValue.duration, scrubbingBeginTimestamp + statusValue.duration * Double(addedFraction)))
|
||||
strongSelf.scrubbingTimestampValue = timestampValue
|
||||
strongSelf._scrubbingTimestamp.set(.single(strongSelf.scrubbingTimestampValue))
|
||||
strongSelf._scrubbingPosition.set(.single(strongSelf.scrubbingTimestampValue.flatMap { $0 / statusValue.duration }))
|
||||
strongSelf.update?(timestampValue, CGFloat(timestampValue / statusValue.duration))
|
||||
strongSelf.updateProgressAnimations()
|
||||
}
|
||||
@ -401,6 +471,7 @@ public final class MediaPlayerScrubbingNode: ASDisplayNode {
|
||||
let scrubbingTimestampValue = strongSelf.scrubbingTimestampValue
|
||||
strongSelf.scrubbingTimestampValue = nil
|
||||
strongSelf._scrubbingTimestamp.set(.single(nil))
|
||||
strongSelf._scrubbingPosition.set(.single(nil))
|
||||
if let scrubbingTimestampValue = scrubbingTimestampValue, apply {
|
||||
if let statusValue = strongSelf.statusValue {
|
||||
switch statusValue.status {
|
||||
@ -508,7 +579,14 @@ public final class MediaPlayerScrubbingNode: ASDisplayNode {
|
||||
node.foregroundContentNode.backgroundColor = foregroundColor
|
||||
}
|
||||
if let handleNode = node.handleNode as? ASImageNode {
|
||||
handleNode.image = generateHandleBackground(color: foregroundColor)
|
||||
switch node.handle {
|
||||
case .line:
|
||||
handleNode.image = generateHandleBackground(color: foregroundColor)
|
||||
case .circle:
|
||||
handleNode.image = generateFilledCircleImage(diameter: node.lineHeight + 3.0, color: foregroundColor)
|
||||
case .none:
|
||||
break
|
||||
}
|
||||
}
|
||||
case .custom:
|
||||
break
|
||||
@ -522,7 +600,7 @@ public final class MediaPlayerScrubbingNode: ASDisplayNode {
|
||||
|
||||
if !self.isInHierarchyValue {
|
||||
needsAnimation = false
|
||||
} else if let _ = scrubbingTimestampValue {
|
||||
} else if let _ = self.scrubbingTimestampValue {
|
||||
needsAnimation = false
|
||||
} else if let statusValue = self.statusValue {
|
||||
if case .buffering(true, _) = statusValue.status {
|
||||
@ -594,10 +672,16 @@ public final class MediaPlayerScrubbingNode: ASDisplayNode {
|
||||
|
||||
if let handleNode = node.handleNode {
|
||||
var handleSize: CGSize = CGSize(width: 2.0, height: bounds.size.height)
|
||||
var handleOffset: CGFloat = 0.0
|
||||
if case .circle = node.handle, let handleNode = handleNode as? ASImageNode, let image = handleNode.image {
|
||||
handleSize = image.size
|
||||
handleOffset = -1.0 + UIScreenPixel
|
||||
}
|
||||
handleNode.frame = CGRect(origin: CGPoint(x: -handleSize.width / 2.0, y: floor((bounds.size.height - handleSize.height) / 2.0) + handleOffset), size: handleSize)
|
||||
|
||||
if let highlightedHandleNode = node.highlightedHandleNode {
|
||||
highlightedHandleNode.position = handleNode.position
|
||||
}
|
||||
handleNode.frame = CGRect(origin: CGPoint(x: -handleSize.width / 2.0, y: floor((bounds.size.height - handleSize.height) / 2.0)), size: handleSize)
|
||||
}
|
||||
|
||||
if let handleNodeContainer = node.handleNodeContainer {
|
||||
@ -605,7 +689,7 @@ public final class MediaPlayerScrubbingNode: ASDisplayNode {
|
||||
}
|
||||
|
||||
if let (timestamp, duration) = timestampAndDuration {
|
||||
if let scrubbingTimestampValue = scrubbingTimestampValue {
|
||||
if let scrubbingTimestampValue = self.scrubbingTimestampValue {
|
||||
var progress = CGFloat(scrubbingTimestampValue / duration)
|
||||
if progress.isNaN || !progress.isFinite {
|
||||
progress = 0.0
|
||||
|
@ -586,13 +586,13 @@ public final class ShareController: ViewController {
|
||||
url = "https://t.me/\(addressName)/\(message.id.id)"
|
||||
}
|
||||
}
|
||||
var peer: Peer?
|
||||
var peerId: PeerId?
|
||||
if let authorPeerId = message.author?.id {
|
||||
peer = message.peers[authorPeerId]
|
||||
peerId = authorPeerId
|
||||
} else if let mainPeer = messageMainPeer(message) {
|
||||
peer = mainPeer
|
||||
peerId = mainPeer.id
|
||||
}
|
||||
collectableItems.append(CollectableExternalShareItem(url: url, text: message.text, author: peer?.displayTitle, timestamp: message.timestamp, mediaReference: selectedMedia.flatMap({ AnyMediaReference.message(message: MessageReference(message), media: $0) })))
|
||||
collectableItems.append(CollectableExternalShareItem(url: url, text: message.text, author: nil, timestamp: message.timestamp, mediaReference: selectedMedia.flatMap({ AnyMediaReference.message(message: MessageReference(message), media: $0) })))
|
||||
}
|
||||
case .fromExternal:
|
||||
break
|
||||
|
@ -145,13 +145,21 @@ func applyUpdateMessage(postbox: Postbox, stateManager: AccountStateManager, mes
|
||||
}
|
||||
|
||||
if forwardInfo == nil {
|
||||
for media in media {
|
||||
inner: for media in message.media {
|
||||
if let file = media as? TelegramMediaFile {
|
||||
if file.isSticker {
|
||||
sentStickers.append(file)
|
||||
} else if file.isVideo && file.isAnimated {
|
||||
sentGifs.append(file)
|
||||
for attribute in file.attributes {
|
||||
switch attribute {
|
||||
case let .Sticker(_, packReference, _):
|
||||
if packReference != nil {
|
||||
sentStickers.append(file)
|
||||
}
|
||||
case .Animated:
|
||||
sentGifs.append(file)
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
break inner
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -296,13 +304,21 @@ func applyUpdateGroupMessages(postbox: Postbox, stateManager: AccountStateManage
|
||||
}
|
||||
|
||||
if storeForwardInfo == nil {
|
||||
for media in media {
|
||||
inner: for media in message.media {
|
||||
if let file = media as? TelegramMediaFile {
|
||||
if file.isSticker {
|
||||
sentStickers.append(file)
|
||||
} else if file.isVideo && file.isAnimated {
|
||||
sentGifs.append(file)
|
||||
for attribute in file.attributes {
|
||||
switch attribute {
|
||||
case let .Sticker(_, packReference, _):
|
||||
if packReference != nil {
|
||||
sentStickers.append(file)
|
||||
}
|
||||
case .Animated:
|
||||
sentGifs.append(file)
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
break inner
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -5326,7 +5326,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
legacyController.bind(controller: controller)
|
||||
legacyController.deferScreenEdgeGestures = [.top]
|
||||
|
||||
configureLegacyAssetPicker(controller, context: strongSelf.context, peer: peer, initialCaption: inputText.string, hasSchedule: !strongSelf.presentationInterfaceState.isScheduledMessages && peer.id.namespace != Namespaces.Peer.SecretChat, presentWebSearch: { [weak self, weak legacyController] in
|
||||
configureLegacyAssetPicker(controller, context: strongSelf.context, peer: peer, initialCaption: inputText.string, hasSchedule: !strongSelf.presentationInterfaceState.isScheduledMessages && peer.id.namespace != Namespaces.Peer.SecretChat, presentWebSearch: editingMedia ? nil : { [weak self, weak legacyController] in
|
||||
if let strongSelf = self {
|
||||
let controller = WebSearchController(context: strongSelf.context, peer: peer, configuration: searchBotsConfiguration, mode: .media(completion: { results, selectionState, editingState, silentPosting in
|
||||
if let legacyController = legacyController {
|
||||
|
@ -1616,7 +1616,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate {
|
||||
let aspectRatio = min(image.size.width, image.size.height) / maxSide
|
||||
if isMemoji || (imageHasTransparency(cgImage) && aspectRatio > 0.85) {
|
||||
self.paste(.sticker(image, isMemoji))
|
||||
return false
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -31,8 +31,8 @@ private func generateShareIcon(theme: PresentationTheme) -> UIImage? {
|
||||
})
|
||||
}
|
||||
|
||||
private let titleFont = Font.medium(16.0)
|
||||
private let descriptionFont = Font.regular(12.0)
|
||||
private let titleFont = Font.semibold(17.0)
|
||||
private let descriptionFont = Font.regular(17.0)
|
||||
|
||||
private func stringsForDisplayData(_ data: SharedMediaPlaybackDisplayData?, theme: PresentationTheme) -> (NSAttributedString?, NSAttributedString?) {
|
||||
var titleString: NSAttributedString?
|
||||
@ -109,6 +109,10 @@ final class OverlayPlayerControlsNode: ASDisplayNode {
|
||||
private var currentFileReference: FileMediaReference?
|
||||
private var statusDisposable: Disposable?
|
||||
|
||||
private var scrubbingDisposable: Disposable?
|
||||
private var leftDurationLabelPushed = false
|
||||
private var rightDurationLabelPushed = false
|
||||
|
||||
private var validLayout: (width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, maxHeight: CGFloat)?
|
||||
|
||||
init(account: Account, accountManager: AccountManager, theme: PresentationTheme, status: Signal<(Account, SharedMediaPlayerItemPlaybackStateOrLoading)?, NoError>) {
|
||||
@ -223,96 +227,121 @@ final class OverlayPlayerControlsNode: ASDisplayNode {
|
||||
self.leftDurationLabel.status = mappedStatus
|
||||
self.rightDurationLabel.status = mappedStatus
|
||||
|
||||
self.scrubbingDisposable = (self.scrubberNode.scrubbingPosition
|
||||
|> deliverOnMainQueue).start(next: { [weak self] value in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
let leftDurationLabelPushed: Bool
|
||||
let rightDurationLabelPushed: Bool
|
||||
if let value = value {
|
||||
leftDurationLabelPushed = value < 0.16
|
||||
rightDurationLabelPushed = value > (strongSelf.rateButton.isHidden ? 0.84 : 0.74)
|
||||
} else {
|
||||
leftDurationLabelPushed = false
|
||||
rightDurationLabelPushed = false
|
||||
}
|
||||
if leftDurationLabelPushed != strongSelf.leftDurationLabelPushed || rightDurationLabelPushed != strongSelf.rightDurationLabelPushed {
|
||||
strongSelf.leftDurationLabelPushed = leftDurationLabelPushed
|
||||
strongSelf.rightDurationLabelPushed = rightDurationLabelPushed
|
||||
|
||||
if let layout = strongSelf.validLayout {
|
||||
strongSelf.updateLayout(width: layout.0, leftInset: layout.1, rightInset: layout.2, maxHeight: layout.3, transition: .animated(duration: 0.35, curve: .spring))
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
self.statusDisposable = (delayedStatus
|
||||
|> deliverOnMainQueue).start(next: { [weak self] value in
|
||||
if let strongSelf = self {
|
||||
var valueItemId: SharedMediaPlaylistItemId?
|
||||
if let (_, value) = value, case let .state(state) = value {
|
||||
valueItemId = state.item.id
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
var valueItemId: SharedMediaPlaylistItemId?
|
||||
if let (_, value) = value, case let .state(state) = value {
|
||||
valueItemId = state.item.id
|
||||
}
|
||||
if !areSharedMediaPlaylistItemIdsEqual(valueItemId, strongSelf.currentItemId) {
|
||||
strongSelf.currentItemId = valueItemId
|
||||
strongSelf.scrubberNode.ignoreSeekId = nil
|
||||
}
|
||||
strongSelf.shareNode.isHidden = false
|
||||
var displayData: SharedMediaPlaybackDisplayData?
|
||||
if let (_, valueOrLoading) = value, case let .state(value) = valueOrLoading {
|
||||
let isPaused: Bool
|
||||
switch value.status.status {
|
||||
case .playing:
|
||||
isPaused = false
|
||||
case .paused:
|
||||
isPaused = true
|
||||
case let .buffering(_, whilePlaying):
|
||||
isPaused = !whilePlaying
|
||||
}
|
||||
if !areSharedMediaPlaylistItemIdsEqual(valueItemId, strongSelf.currentItemId) {
|
||||
strongSelf.currentItemId = valueItemId
|
||||
strongSelf.scrubberNode.ignoreSeekId = nil
|
||||
}
|
||||
strongSelf.shareNode.isHidden = false
|
||||
var displayData: SharedMediaPlaybackDisplayData?
|
||||
if let (_, valueOrLoading) = value, case let .state(value) = valueOrLoading {
|
||||
let isPaused: Bool
|
||||
switch value.status.status {
|
||||
case .playing:
|
||||
isPaused = false
|
||||
case .paused:
|
||||
isPaused = true
|
||||
case let .buffering(_, whilePlaying):
|
||||
isPaused = !whilePlaying
|
||||
}
|
||||
if strongSelf.currentIsPaused != isPaused {
|
||||
strongSelf.currentIsPaused = isPaused
|
||||
|
||||
strongSelf.updatePlayPauseButton(paused: isPaused)
|
||||
}
|
||||
if strongSelf.currentIsPaused != isPaused {
|
||||
strongSelf.currentIsPaused = isPaused
|
||||
|
||||
strongSelf.playPauseButton.isEnabled = true
|
||||
strongSelf.backwardButton.isEnabled = true
|
||||
strongSelf.forwardButton.isEnabled = true
|
||||
|
||||
displayData = value.item.displayData
|
||||
|
||||
if value.order != strongSelf.currentOrder {
|
||||
strongSelf.updateOrder?(value.order)
|
||||
strongSelf.currentOrder = value.order
|
||||
strongSelf.updateOrderButton(value.order)
|
||||
}
|
||||
if value.looping != strongSelf.currentLooping {
|
||||
strongSelf.currentLooping = value.looping
|
||||
strongSelf.updateLoopButton(value.looping)
|
||||
}
|
||||
|
||||
let baseRate: AudioPlaybackRate
|
||||
if !value.status.baseRate.isEqual(to: 1.0) {
|
||||
baseRate = .x2
|
||||
} else {
|
||||
baseRate = .x1
|
||||
}
|
||||
if baseRate != strongSelf.currentRate {
|
||||
strongSelf.currentRate = baseRate
|
||||
strongSelf.updateRateButton(baseRate)
|
||||
}
|
||||
|
||||
if let displayData = displayData, case let .music(_, _, _, long) = displayData, long {
|
||||
strongSelf.rateButton.isHidden = false
|
||||
} else {
|
||||
strongSelf.rateButton.isHidden = true
|
||||
}
|
||||
} else {
|
||||
strongSelf.playPauseButton.isEnabled = false
|
||||
strongSelf.backwardButton.isEnabled = false
|
||||
strongSelf.forwardButton.isEnabled = false
|
||||
strongSelf.rateButton.isHidden = true
|
||||
displayData = nil
|
||||
strongSelf.updatePlayPauseButton(paused: isPaused)
|
||||
}
|
||||
|
||||
if strongSelf.displayData != displayData {
|
||||
strongSelf.displayData = displayData
|
||||
|
||||
if let (_, valueOrLoading) = value, case let .state(value) = valueOrLoading, let source = value.item.playbackData?.source {
|
||||
switch source {
|
||||
case let .telegramFile(fileReference):
|
||||
strongSelf.currentFileReference = fileReference
|
||||
if let size = fileReference.media.size {
|
||||
strongSelf.scrubberNode.bufferingStatus = strongSelf.postbox.mediaBox.resourceRangesStatus(fileReference.media.resource)
|
||||
|> map { ranges -> (IndexSet, Int) in
|
||||
return (ranges, size)
|
||||
}
|
||||
} else {
|
||||
strongSelf.scrubberNode.bufferingStatus = nil
|
||||
}
|
||||
}
|
||||
} else {
|
||||
strongSelf.scrubberNode.bufferingStatus = nil
|
||||
}
|
||||
strongSelf.updateLabels(transition: .immediate)
|
||||
strongSelf.playPauseButton.isEnabled = true
|
||||
strongSelf.backwardButton.isEnabled = true
|
||||
strongSelf.forwardButton.isEnabled = true
|
||||
|
||||
displayData = value.item.displayData
|
||||
|
||||
if value.order != strongSelf.currentOrder {
|
||||
strongSelf.updateOrder?(value.order)
|
||||
strongSelf.currentOrder = value.order
|
||||
strongSelf.updateOrderButton(value.order)
|
||||
}
|
||||
if value.looping != strongSelf.currentLooping {
|
||||
strongSelf.currentLooping = value.looping
|
||||
strongSelf.updateLoopButton(value.looping)
|
||||
}
|
||||
|
||||
let baseRate: AudioPlaybackRate
|
||||
if !value.status.baseRate.isEqual(to: 1.0) {
|
||||
baseRate = .x2
|
||||
} else {
|
||||
baseRate = .x1
|
||||
}
|
||||
if baseRate != strongSelf.currentRate {
|
||||
strongSelf.currentRate = baseRate
|
||||
strongSelf.updateRateButton(baseRate)
|
||||
}
|
||||
|
||||
if let displayData = displayData, case let .music(_, _, _, long) = displayData, long {
|
||||
strongSelf.rateButton.isHidden = false
|
||||
} else {
|
||||
strongSelf.rateButton.isHidden = true
|
||||
}
|
||||
} else {
|
||||
strongSelf.playPauseButton.isEnabled = false
|
||||
strongSelf.backwardButton.isEnabled = false
|
||||
strongSelf.forwardButton.isEnabled = false
|
||||
strongSelf.rateButton.isHidden = true
|
||||
displayData = nil
|
||||
}
|
||||
|
||||
if strongSelf.displayData != displayData {
|
||||
strongSelf.displayData = displayData
|
||||
|
||||
if let (_, valueOrLoading) = value, case let .state(value) = valueOrLoading, let source = value.item.playbackData?.source {
|
||||
switch source {
|
||||
case let .telegramFile(fileReference):
|
||||
strongSelf.currentFileReference = fileReference
|
||||
if let size = fileReference.media.size {
|
||||
strongSelf.scrubberNode.bufferingStatus = strongSelf.postbox.mediaBox.resourceRangesStatus(fileReference.media.resource)
|
||||
|> map { ranges -> (IndexSet, Int) in
|
||||
return (ranges, size)
|
||||
}
|
||||
} else {
|
||||
strongSelf.scrubberNode.bufferingStatus = nil
|
||||
}
|
||||
}
|
||||
} else {
|
||||
strongSelf.scrubberNode.bufferingStatus = nil
|
||||
}
|
||||
strongSelf.updateLabels(transition: .immediate)
|
||||
}
|
||||
})
|
||||
|
||||
@ -332,6 +361,7 @@ final class OverlayPlayerControlsNode: ASDisplayNode {
|
||||
|
||||
deinit {
|
||||
self.statusDisposable?.dispose()
|
||||
self.scrubbingDisposable?.dispose()
|
||||
}
|
||||
|
||||
override func didLoad() {
|
||||
@ -378,7 +408,7 @@ final class OverlayPlayerControlsNode: ASDisplayNode {
|
||||
|
||||
let sideInset: CGFloat = 20.0
|
||||
|
||||
let infoLabelsLeftInset: CGFloat = 64.0
|
||||
let infoLabelsLeftInset: CGFloat = 60.0
|
||||
let infoLabelsRightInset: CGFloat = 32.0
|
||||
|
||||
let infoVerticalOrigin: CGFloat = panelHeight - OverlayPlayerControlsNode.basePanelHeight + 36.0
|
||||
@ -392,7 +422,7 @@ final class OverlayPlayerControlsNode: ASDisplayNode {
|
||||
transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(x: self.isExpanded ? floor((width - titleLayout.size.width) / 2.0) : (leftInset + sideInset + infoLabelsLeftInset), y: infoVerticalOrigin + 1.0), size: titleLayout.size))
|
||||
let _ = titleApply()
|
||||
|
||||
transition.updateFrame(node: self.descriptionNode, frame: CGRect(origin: CGPoint(x: self.isExpanded ? floor((width - descriptionLayout.size.width) / 2.0) : (leftInset + sideInset + infoLabelsLeftInset), y: infoVerticalOrigin + 27.0), size: descriptionLayout.size))
|
||||
transition.updateFrame(node: self.descriptionNode, frame: CGRect(origin: CGPoint(x: self.isExpanded ? floor((width - descriptionLayout.size.width) / 2.0) : (leftInset + sideInset + infoLabelsLeftInset), y: infoVerticalOrigin + 26.0), size: descriptionLayout.size))
|
||||
let _ = descriptionApply()
|
||||
|
||||
var albumArt: SharedMediaPlaybackAlbumArt?
|
||||
@ -469,7 +499,6 @@ final class OverlayPlayerControlsNode: ASDisplayNode {
|
||||
func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, maxHeight: CGFloat, transition: ContainedViewLayoutTransition) -> CGFloat {
|
||||
self.validLayout = (width, leftInset, rightInset, maxHeight)
|
||||
|
||||
|
||||
let panelHeight = OverlayPlayerControlsNode.heightForLayout(width: width, leftInset: leftInset, rightInset: rightInset, maxHeight: maxHeight, isExpanded: self.isExpanded)
|
||||
|
||||
transition.updateFrame(node: self.separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: panelHeight), size: CGSize(width: width, height: UIScreenPixel)))
|
||||
@ -562,10 +591,14 @@ final class OverlayPlayerControlsNode: ASDisplayNode {
|
||||
let scrubberVerticalOrigin: CGFloat = infoVerticalOrigin + 64.0
|
||||
|
||||
transition.updateFrame(node: self.scrubberNode, frame: CGRect(origin: CGPoint(x: leftInset + sideInset, y: scrubberVerticalOrigin - 8.0), size: CGSize(width: width - sideInset * 2.0 - leftInset - rightInset, height: 10.0 + 8.0 * 2.0)))
|
||||
transition.updateFrame(node: self.leftDurationLabel, frame: CGRect(origin: CGPoint(x: leftInset + sideInset, y: scrubberVerticalOrigin + 14.0), size: CGSize(width: 100.0, height: 20.0)))
|
||||
transition.updateFrame(node: self.rightDurationLabel, frame: CGRect(origin: CGPoint(x: width - sideInset - rightInset - 100.0, y: scrubberVerticalOrigin + 14.0), size: CGSize(width: 100.0, height: 20.0)))
|
||||
|
||||
transition.updateFrame(node: self.rateButton, frame: CGRect(origin: CGPoint(x: width - sideInset - rightInset - 100.0 + 24.0, y: scrubberVerticalOrigin + 10.0), size: CGSize(width: 24.0, height: 24.0)))
|
||||
var leftLabelVerticalOffset: CGFloat = self.leftDurationLabelPushed ? 6.0 : 0.0
|
||||
transition.updateFrame(node: self.leftDurationLabel, frame: CGRect(origin: CGPoint(x: leftInset + sideInset, y: scrubberVerticalOrigin + 14.0 + leftLabelVerticalOffset), size: CGSize(width: 100.0, height: 20.0)))
|
||||
|
||||
var rightLabelVerticalOffset: CGFloat = self.rightDurationLabelPushed ? 6.0 : 0.0
|
||||
transition.updateFrame(node: self.rightDurationLabel, frame: CGRect(origin: CGPoint(x: width - sideInset - rightInset - 100.0, y: scrubberVerticalOrigin + 14.0 + rightLabelVerticalOffset), size: CGSize(width: 100.0, height: 20.0)))
|
||||
|
||||
transition.updateFrame(node: self.rateButton, frame: CGRect(origin: CGPoint(x: width - sideInset - rightInset - 100.0 + 24.0, y: scrubberVerticalOrigin + 10.0 + rightLabelVerticalOffset), size: CGSize(width: 24.0, height: 24.0)))
|
||||
|
||||
transition.updateFrame(node: self.backgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: -8.0), size: CGSize(width: width, height: panelHeight + 8.0)))
|
||||
|
||||
|
@ -293,9 +293,21 @@ private final class WalletQrScanScreenNode: ViewControllerTracingNode, UIScrollV
|
||||
|
||||
let sideInset: CGFloat = 66.0
|
||||
let titleSpacing: CGFloat = 48.0
|
||||
|
||||
let bounds = CGRect(origin: CGPoint(), size: layout.size)
|
||||
|
||||
if case .tablet = layout.deviceMetrics.type {
|
||||
if case .landscape = layout.orientation {
|
||||
let rotation: CGFloat
|
||||
if UIDevice.current.orientation == .landscapeLeft {
|
||||
rotation = CGFloat.pi / 2.0
|
||||
} else {
|
||||
rotation = -CGFloat.pi / 2.0
|
||||
}
|
||||
self.previewNode.transform = CATransform3DMakeRotation(CGFloat.pi / 2.0, 0.0, 0.0, 1.0)
|
||||
} else {
|
||||
self.previewNode.transform = CATransform3DIdentity
|
||||
}
|
||||
}
|
||||
transition.updateFrame(node: self.previewNode, frame: bounds)
|
||||
transition.updateFrame(node: self.fadeNode, frame: bounds)
|
||||
|
||||
@ -307,27 +319,26 @@ private final class WalletQrScanScreenNode: ViewControllerTracingNode, UIScrollV
|
||||
let dimRect: CGRect
|
||||
let controlsAlpha: CGFloat
|
||||
if let focusedRect = self.focusedRect {
|
||||
dimAlpha = 1.0
|
||||
controlsAlpha = 0.0
|
||||
dimAlpha = 1.0
|
||||
let side = max(bounds.width * focusedRect.width, bounds.height * focusedRect.height) * 0.6
|
||||
let center = CGPoint(x: (1.0 - focusedRect.center.y) * bounds.width, y: focusedRect.center.x * bounds.height)
|
||||
dimRect = CGRect(x: center.x - side / 2.0, y: center.y - side / 2.0, width: side, height: side)
|
||||
} else {
|
||||
dimAlpha = 0.625
|
||||
controlsAlpha = 1.0
|
||||
dimAlpha = 0.625
|
||||
dimRect = CGRect(x: dimInset, y: dimHeight, width: layout.size.width - dimInset * 2.0, height: layout.size.height - dimHeight * 2.0)
|
||||
}
|
||||
|
||||
transition.updateFrame(node: self.topDimNode, frame: CGRect(x: 0.0, y: 0.0, width: layout.size.width, height: dimRect.minY))
|
||||
transition.updateFrame(node: self.bottomDimNode, frame: CGRect(x: 0.0, y: dimRect.maxY, width: layout.size.width, height: max(0.0, layout.size.height - dimRect.maxY)))
|
||||
transition.updateFrame(node: self.leftDimNode, frame: CGRect(x: 0.0, y: dimRect.minY, width: max(0.0, dimRect.minX), height: dimRect.height))
|
||||
transition.updateFrame(node: self.rightDimNode, frame: CGRect(x: dimRect.maxX, y: dimRect.minY, width: max(0.0, layout.size.width - dimRect.maxX), height: dimRect.height))
|
||||
|
||||
transition.updateAlpha(node: self.topDimNode, alpha: dimAlpha)
|
||||
transition.updateAlpha(node: self.bottomDimNode, alpha: dimAlpha)
|
||||
transition.updateAlpha(node: self.leftDimNode, alpha: dimAlpha)
|
||||
transition.updateAlpha(node: self.rightDimNode, alpha: dimAlpha)
|
||||
|
||||
transition.updateFrame(node: self.topDimNode, frame: CGRect(x: 0.0, y: 0.0, width: layout.size.width, height: dimRect.minY))
|
||||
transition.updateFrame(node: self.bottomDimNode, frame: CGRect(x: 0.0, y: dimRect.maxY, width: layout.size.width, height: max(0.0, layout.size.height - dimRect.maxY)))
|
||||
transition.updateFrame(node: self.leftDimNode, frame: CGRect(x: 0.0, y: dimRect.minY, width: max(0.0, dimRect.minX), height: dimRect.height))
|
||||
transition.updateFrame(node: self.rightDimNode, frame: CGRect(x: dimRect.maxX, y: dimRect.minY, width: max(0.0, layout.size.width - dimRect.maxX), height: dimRect.height))
|
||||
transition.updateFrame(node: self.frameNode, frame: dimRect.insetBy(dx: -2.0, dy: -2.0))
|
||||
|
||||
let buttonSize = CGSize(width: 72.0, height: 72.0)
|
||||
|
@ -155,8 +155,10 @@ private final class WalletQrViewScreenNode: ViewControllerTracingNode {
|
||||
let imageFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - imageSize.width) / 2.0), y: floor((layout.size.height - imageSize.height - layout.intrinsicInsets.bottom) / 2.0)), size: imageSize)
|
||||
transition.updateFrame(node: self.imageNode, frame: imageFrame)
|
||||
|
||||
let iconFrame = imageFrame.insetBy(dx: 106.0, dy: 106.0).offsetBy(dx: 0.0, dy: -2.0)
|
||||
self.iconNode.updateLayout(size: iconFrame.size)
|
||||
transition.updateFrameAsPositionAndBounds(node: self.iconNode, frame: iconFrame)
|
||||
let iconSide = floor(imageSide * 0.24)
|
||||
let iconSize = CGSize(width: iconSide, height: iconSide)
|
||||
self.iconNode.updateLayout(size: iconSize)
|
||||
transition.updateBounds(node: self.iconNode, bounds: CGRect(origin: CGPoint(), size: iconSize))
|
||||
transition.updatePosition(node: self.iconNode, position: imageFrame.center.offsetBy(dx: 0.0, dy: -1.0))
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user