mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-08-02 00:17:02 +00:00
Merge commit 'c27ebb4787530b4812abaed5aacd168edb6afac9' into experimental-2
This commit is contained in:
commit
bd20f05b61
@ -5841,11 +5841,13 @@ Any member of this group will be able to see messages in the channel.";
|
|||||||
"Notification.ProximityReached" = "%1$@ is now within %2$@ from %3$@";
|
"Notification.ProximityReached" = "%1$@ is now within %2$@ from %3$@";
|
||||||
"Notification.ProximityReachedYou" = "%1$@ is now within %2$@ from you";
|
"Notification.ProximityReachedYou" = "%1$@ is now within %2$@ from you";
|
||||||
|
|
||||||
"Location.ProximityNotification.Title" = "Notification";
|
"Location.ProximityNotification.Title" = "Proximity Alert";
|
||||||
"Location.ProximityNotification.Notify" = "Notify me within %@";
|
"Location.ProximityNotification.Notify" = "Notify me within %@";
|
||||||
|
"Location.ProximityNotification.NotifyLong" = "Notify when %1$@ is within %2$@";
|
||||||
"Location.ProximityNotification.AlreadyClose" = "You are already closer than %@";
|
"Location.ProximityNotification.AlreadyClose" = "You are already closer than %@";
|
||||||
"Location.ProximityNotification.DistanceKM" = "KM";
|
"Location.ProximityNotification.DistanceKM" = "km";
|
||||||
"Location.ProximityNotification.DistanceMI" = "MI";
|
"Location.ProximityNotification.DistanceM" = "m";
|
||||||
|
"Location.ProximityNotification.DistanceMI" = "mi";
|
||||||
|
|
||||||
"Location.ProximityTip" = "Alert when %@ is close";
|
"Location.ProximityTip" = "Alert when %@ is close";
|
||||||
"Location.ProximityGroupTip" = "Alert when any group member is close";
|
"Location.ProximityGroupTip" = "Alert when any group member is close";
|
||||||
|
@ -29,6 +29,7 @@ public protocol UniversalVideoContentNode: class {
|
|||||||
func addPlaybackCompleted(_ f: @escaping () -> Void) -> Int
|
func addPlaybackCompleted(_ f: @escaping () -> Void) -> Int
|
||||||
func removePlaybackCompleted(_ index: Int)
|
func removePlaybackCompleted(_ index: Int)
|
||||||
func fetchControl(_ control: UniversalVideoNodeFetchControl)
|
func fetchControl(_ control: UniversalVideoNodeFetchControl)
|
||||||
|
func notifyPlaybackControlsHidden(_ hidden: Bool)
|
||||||
}
|
}
|
||||||
|
|
||||||
public protocol UniversalVideoContent {
|
public protocol UniversalVideoContent {
|
||||||
@ -319,6 +320,14 @@ public final class UniversalVideoNode: ASDisplayNode {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public func notifyPlaybackControlsHidden(_ hidden: Bool) {
|
||||||
|
self.manager.withUniversalVideoContent(id: self.content.id, { contentNode in
|
||||||
|
if let contentNode = contentNode {
|
||||||
|
contentNode.notifyPlaybackControlsHidden(hidden)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
@objc private func tapGesture(_ recognizer: UITapGestureRecognizer) {
|
@objc private func tapGesture(_ recognizer: UITapGestureRecognizer) {
|
||||||
if case .ended = recognizer.state {
|
if case .ended = recognizer.state {
|
||||||
self.decoration.tap()
|
self.decoration.tap()
|
||||||
|
@ -762,13 +762,7 @@ public final class AnimatedStickerNode: ASDisplayNode {
|
|||||||
return self.playbackStatus.get()
|
return self.playbackStatus.get()
|
||||||
}
|
}
|
||||||
|
|
||||||
public var autoplay = true {
|
public var autoplay = false
|
||||||
didSet {
|
|
||||||
if self.autoplay != oldValue {
|
|
||||||
self.updateIsPlaying()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public var visibility = false {
|
public var visibility = false {
|
||||||
didSet {
|
didSet {
|
||||||
@ -835,9 +829,12 @@ public final class AnimatedStickerNode: ASDisplayNode {
|
|||||||
strongSelf.directData = (directData, path, width, height, cachePathPrefix, source.fitzModifier)
|
strongSelf.directData = (directData, path, width, height, cachePathPrefix, source.fitzModifier)
|
||||||
}
|
}
|
||||||
if case let .still(position) = playbackMode {
|
if case let .still(position) = playbackMode {
|
||||||
strongSelf.play(firstFrame: true)
|
|
||||||
strongSelf.seekTo(position)
|
strongSelf.seekTo(position)
|
||||||
} else if strongSelf.isPlaying {
|
} else if strongSelf.isPlaying || strongSelf.autoplay {
|
||||||
|
if strongSelf.autoplay {
|
||||||
|
strongSelf.isSetUpForPlayback = false
|
||||||
|
strongSelf.isPlaying = true
|
||||||
|
}
|
||||||
strongSelf.play()
|
strongSelf.play()
|
||||||
} else if strongSelf.canDisplayFirstFrame {
|
} else if strongSelf.canDisplayFirstFrame {
|
||||||
strongSelf.play(firstFrame: true)
|
strongSelf.play(firstFrame: true)
|
||||||
@ -884,11 +881,14 @@ public final class AnimatedStickerNode: ASDisplayNode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func updateIsPlaying() {
|
private func updateIsPlaying() {
|
||||||
|
guard !self.autoplay else {
|
||||||
|
return
|
||||||
|
}
|
||||||
let isPlaying = self.visibility && self.isDisplaying
|
let isPlaying = self.visibility && self.isDisplaying
|
||||||
if self.isPlaying != isPlaying {
|
if self.isPlaying != isPlaying {
|
||||||
self.isPlaying = isPlaying
|
self.isPlaying = isPlaying
|
||||||
if isPlaying {
|
if isPlaying {
|
||||||
self.play(firstFrame: !self.autoplay)
|
self.play()
|
||||||
} else{
|
} else{
|
||||||
self.pause()
|
self.pause()
|
||||||
}
|
}
|
||||||
@ -905,6 +905,9 @@ public final class AnimatedStickerNode: ASDisplayNode {
|
|||||||
private var isSetUpForPlayback = false
|
private var isSetUpForPlayback = false
|
||||||
|
|
||||||
public func play(firstFrame: Bool = false) {
|
public func play(firstFrame: Bool = false) {
|
||||||
|
if case .once = self.playbackMode {
|
||||||
|
self.isPlaying = true
|
||||||
|
}
|
||||||
if self.isSetUpForPlayback {
|
if self.isSetUpForPlayback {
|
||||||
let directData = self.directData
|
let directData = self.directData
|
||||||
let cachedData = self.cachedData
|
let cachedData = self.cachedData
|
||||||
@ -1088,7 +1091,6 @@ public final class AnimatedStickerNode: ASDisplayNode {
|
|||||||
var maybeFrameSource: AnimatedStickerFrameSource? = frameSourceHolder.with { $0 }?.syncWith { $0 }?.value
|
var maybeFrameSource: AnimatedStickerFrameSource? = frameSourceHolder.with { $0 }?.syncWith { $0 }?.value
|
||||||
if case .timestamp = position {
|
if case .timestamp = position {
|
||||||
} else {
|
} else {
|
||||||
var maybeFrameSource: AnimatedStickerFrameSource?
|
|
||||||
if let directData = directData {
|
if let directData = directData {
|
||||||
maybeFrameSource = AnimatedStickerDirectFrameSource(queue: queue, data: directData.0, width: directData.2, height: directData.3, cachePathPrefix: directData.4, fitzModifier: directData.5)
|
maybeFrameSource = AnimatedStickerDirectFrameSource(queue: queue, data: directData.0, width: directData.2, height: directData.3, cachePathPrefix: directData.4, fitzModifier: directData.5)
|
||||||
if case .end = position {
|
if case .end = position {
|
||||||
|
@ -811,7 +811,7 @@ public func createPollController(context: AccountContext, peer: Peer, isQuiz: Bo
|
|||||||
if state.isQuiz {
|
if state.isQuiz {
|
||||||
kind = .quiz
|
kind = .quiz
|
||||||
if !state.solutionText.value.string.isEmpty {
|
if !state.solutionText.value.string.isEmpty {
|
||||||
let entities = generateTextEntities(state.solutionText.value.string, enabledTypes: .url, currentEntities: generateChatInputTextEntities(state.solutionText.value))
|
let entities = generateTextEntities(state.solutionText.value.string, enabledTypes: .allUrl, currentEntities: generateChatInputTextEntities(state.solutionText.value))
|
||||||
resolvedSolution = TelegramMediaPollResults.Solution(text: state.solutionText.value.string, entities: entities)
|
resolvedSolution = TelegramMediaPollResults.Solution(text: state.solutionText.value.string, entities: entities)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
@ -59,6 +59,7 @@ public final class DeviceLocationManager: NSObject {
|
|||||||
self.manager.distanceFilter = 5.0
|
self.manager.distanceFilter = 5.0
|
||||||
self.manager.activityType = .other
|
self.manager.activityType = .other
|
||||||
self.manager.pausesLocationUpdatesAutomatically = false
|
self.manager.pausesLocationUpdatesAutomatically = false
|
||||||
|
self.manager.headingFilter = 2.0
|
||||||
}
|
}
|
||||||
|
|
||||||
public func push(mode: DeviceLocationMode, updated: @escaping (CLLocation, Double?) -> Void) -> Disposable {
|
public func push(mode: DeviceLocationMode, updated: @escaping (CLLocation, Double?) -> Void) -> Disposable {
|
||||||
|
@ -60,7 +60,7 @@ public func messageFileMediaResourceStatus(context: AccountContext, file: Telegr
|
|||||||
mediaStatus = .playbackStatus(.playing)
|
mediaStatus = .playbackStatus(.playing)
|
||||||
case .paused:
|
case .paused:
|
||||||
mediaStatus = .playbackStatus(.paused)
|
mediaStatus = .playbackStatus(.paused)
|
||||||
case let .buffering(_, whilePlaying, _):
|
case let .buffering(_, whilePlaying, _, _):
|
||||||
if whilePlaying {
|
if whilePlaying {
|
||||||
mediaStatus = .playbackStatus(.playing)
|
mediaStatus = .playbackStatus(.playing)
|
||||||
} else {
|
} else {
|
||||||
@ -84,7 +84,7 @@ public func messageFileMediaResourceStatus(context: AccountContext, file: Telegr
|
|||||||
mediaStatus = .playbackStatus(.playing)
|
mediaStatus = .playbackStatus(.playing)
|
||||||
case .paused:
|
case .paused:
|
||||||
mediaStatus = .playbackStatus(.paused)
|
mediaStatus = .playbackStatus(.paused)
|
||||||
case let .buffering(_, whilePlaying, _):
|
case let .buffering(_, whilePlaying, _, _):
|
||||||
if whilePlaying {
|
if whilePlaying {
|
||||||
mediaStatus = .playbackStatus(.playing)
|
mediaStatus = .playbackStatus(.playing)
|
||||||
} else {
|
} else {
|
||||||
|
@ -265,6 +265,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
|
|||||||
private let overlayContentNode: UniversalVideoGalleryItemOverlayNode
|
private let overlayContentNode: UniversalVideoGalleryItemOverlayNode
|
||||||
|
|
||||||
private var videoNode: UniversalVideoNode?
|
private var videoNode: UniversalVideoNode?
|
||||||
|
private var videoNodeUserInteractionEnabled: Bool = false
|
||||||
private var videoFramePreview: FramePreview?
|
private var videoFramePreview: FramePreview?
|
||||||
private var pictureInPictureNode: UniversalVideoGalleryItemPictureInPictureNode?
|
private var pictureInPictureNode: UniversalVideoGalleryItemPictureInPictureNode?
|
||||||
private let statusButtonNode: HighlightableButtonNode
|
private let statusButtonNode: HighlightableButtonNode
|
||||||
@ -555,6 +556,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.videoNode = videoNode
|
self.videoNode = videoNode
|
||||||
|
self.videoNodeUserInteractionEnabled = disablePlayerControls || forceEnableUserInteraction
|
||||||
videoNode.isUserInteractionEnabled = disablePlayerControls || forceEnableUserInteraction
|
videoNode.isUserInteractionEnabled = disablePlayerControls || forceEnableUserInteraction
|
||||||
videoNode.backgroundColor = videoNode.ownsContentNode ? UIColor.black : UIColor(rgb: 0x333335)
|
videoNode.backgroundColor = videoNode.ownsContentNode ? UIColor.black : UIColor(rgb: 0x333335)
|
||||||
if item.fromPlayingVideo {
|
if item.fromPlayingVideo {
|
||||||
@ -634,6 +636,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
|
|||||||
var isPaused = true
|
var isPaused = true
|
||||||
var seekable = false
|
var seekable = false
|
||||||
var hasStarted = false
|
var hasStarted = false
|
||||||
|
var displayProgress = true
|
||||||
if let value = value {
|
if let value = value {
|
||||||
hasStarted = value.timestamp > 0
|
hasStarted = value.timestamp > 0
|
||||||
|
|
||||||
@ -648,7 +651,8 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
|
|||||||
case .playing:
|
case .playing:
|
||||||
isPaused = false
|
isPaused = false
|
||||||
playing = true
|
playing = true
|
||||||
case let .buffering(_, whilePlaying, _):
|
case let .buffering(_, whilePlaying, _, display):
|
||||||
|
displayProgress = display
|
||||||
initialBuffering = true
|
initialBuffering = true
|
||||||
isPaused = !whilePlaying
|
isPaused = !whilePlaying
|
||||||
var isStreaming = false
|
var isStreaming = false
|
||||||
@ -678,6 +682,10 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
|
|||||||
if !content.enableSound {
|
if !content.enableSound {
|
||||||
isPaused = false
|
isPaused = false
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
strongSelf.updateControlsVisibility(true)
|
||||||
|
strongSelf.controlsTimer?.invalidate()
|
||||||
|
strongSelf.controlsTimer = nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
seekable = value.duration >= 30.0
|
seekable = value.duration >= 30.0
|
||||||
@ -700,7 +708,11 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
|
|||||||
|
|
||||||
var fetching = false
|
var fetching = false
|
||||||
if initialBuffering {
|
if initialBuffering {
|
||||||
|
if displayProgress {
|
||||||
strongSelf.statusNode.transitionToState(.progress(color: .white, lineWidth: nil, value: nil, cancelEnabled: false), animated: false, completion: {})
|
strongSelf.statusNode.transitionToState(.progress(color: .white, lineWidth: nil, value: nil, cancelEnabled: false), animated: false, completion: {})
|
||||||
|
} else {
|
||||||
|
strongSelf.statusNode.transitionToState(.none, animated: false, completion: {})
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
var state: RadialStatusNodeState = .play(.white)
|
var state: RadialStatusNodeState = .play(.white)
|
||||||
|
|
||||||
@ -789,6 +801,9 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
|
|||||||
override func controlsVisibilityUpdated(isVisible: Bool) {
|
override func controlsVisibilityUpdated(isVisible: Bool) {
|
||||||
self.controlsTimer?.invalidate()
|
self.controlsTimer?.invalidate()
|
||||||
self.controlsTimer = nil
|
self.controlsTimer = nil
|
||||||
|
|
||||||
|
self.videoNode?.isUserInteractionEnabled = isVisible ? self.videoNodeUserInteractionEnabled : false
|
||||||
|
self.videoNode?.notifyPlaybackControlsHidden(!isVisible)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func updateDisplayPlaceholder(_ displayPlaceholder: Bool) {
|
private func updateDisplayPlaceholder(_ displayPlaceholder: Bool) {
|
||||||
|
@ -57,7 +57,7 @@ final class InstantPageArticleNode: ASDisplayNode, InstantPageNode {
|
|||||||
|
|
||||||
let imageReference = ImageMediaReference.webPage(webPage: WebpageReference(webPage), media: image)
|
let imageReference = ImageMediaReference.webPage(webPage: WebpageReference(webPage), media: image)
|
||||||
imageNode.setSignal(chatMessagePhoto(postbox: context.account.postbox, photoReference: imageReference))
|
imageNode.setSignal(chatMessagePhoto(postbox: context.account.postbox, photoReference: imageReference))
|
||||||
self.fetchedDisposable.set(chatMessagePhotoInteractiveFetched(context: context, photoReference: imageReference, storeToDownloadsPeerType: nil).start())
|
self.fetchedDisposable.set(chatMessagePhotoInteractiveFetched(context: context, photoReference: imageReference, displayAtSize: nil, storeToDownloadsPeerType: nil).start())
|
||||||
|
|
||||||
self.imageNode = imageNode
|
self.imageNode = imageNode
|
||||||
self.addSubnode(imageNode)
|
self.addSubnode(imageNode)
|
||||||
|
@ -74,12 +74,12 @@ final class InstantPageImageNode: ASDisplayNode, InstantPageNode {
|
|||||||
self.imageNode.setSignal(chatMessagePhoto(postbox: context.account.postbox, photoReference: imageReference))
|
self.imageNode.setSignal(chatMessagePhoto(postbox: context.account.postbox, photoReference: imageReference))
|
||||||
|
|
||||||
if !interactive || shouldDownloadMediaAutomatically(settings: context.sharedContext.currentAutomaticMediaDownloadSettings.with { $0 }, peerType: sourcePeerType, networkType: MediaAutoDownloadNetworkType(context.account.immediateNetworkType), authorPeerId: nil, contactsPeerIds: Set(), media: image) {
|
if !interactive || shouldDownloadMediaAutomatically(settings: context.sharedContext.currentAutomaticMediaDownloadSettings.with { $0 }, peerType: sourcePeerType, networkType: MediaAutoDownloadNetworkType(context.account.immediateNetworkType), authorPeerId: nil, contactsPeerIds: Set(), media: image) {
|
||||||
self.fetchedDisposable.set(chatMessagePhotoInteractiveFetched(context: context, photoReference: imageReference, storeToDownloadsPeerType: nil).start())
|
self.fetchedDisposable.set(chatMessagePhotoInteractiveFetched(context: context, photoReference: imageReference, displayAtSize: nil, storeToDownloadsPeerType: nil).start())
|
||||||
}
|
}
|
||||||
|
|
||||||
self.fetchControls = FetchControls(fetch: { [weak self] manual in
|
self.fetchControls = FetchControls(fetch: { [weak self] manual in
|
||||||
if let strongSelf = self {
|
if let strongSelf = self {
|
||||||
strongSelf.fetchedDisposable.set(chatMessagePhotoInteractiveFetched(context: context, photoReference: imageReference, storeToDownloadsPeerType: nil).start())
|
strongSelf.fetchedDisposable.set(chatMessagePhotoInteractiveFetched(context: context, photoReference: imageReference, displayAtSize: nil, storeToDownloadsPeerType: nil).start())
|
||||||
}
|
}
|
||||||
}, cancel: {
|
}, cancel: {
|
||||||
chatMessagePhotoCancelInteractiveFetch(account: context.account, photoReference: imageReference)
|
chatMessagePhotoCancelInteractiveFetch(account: context.account, photoReference: imageReference)
|
||||||
@ -133,7 +133,7 @@ final class InstantPageImageNode: ASDisplayNode, InstantPageNode {
|
|||||||
} else if let webPage = media.media as? TelegramMediaWebpage, case let .Loaded(content) = webPage.content, let image = content.image {
|
} else if let webPage = media.media as? TelegramMediaWebpage, case let .Loaded(content) = webPage.content, let image = content.image {
|
||||||
let imageReference = ImageMediaReference.webPage(webPage: WebpageReference(webPage), media: image)
|
let imageReference = ImageMediaReference.webPage(webPage: WebpageReference(webPage), media: image)
|
||||||
self.imageNode.setSignal(chatMessagePhoto(postbox: context.account.postbox, photoReference: imageReference))
|
self.imageNode.setSignal(chatMessagePhoto(postbox: context.account.postbox, photoReference: imageReference))
|
||||||
self.fetchedDisposable.set(chatMessagePhotoInteractiveFetched(context: context, photoReference: imageReference, storeToDownloadsPeerType: nil).start())
|
self.fetchedDisposable.set(chatMessagePhotoInteractiveFetched(context: context, photoReference: imageReference, displayAtSize: nil, storeToDownloadsPeerType: nil).start())
|
||||||
self.statusNode.transitionToState(.play(.white), animated: false, completion: {})
|
self.statusNode.transitionToState(.play(.white), animated: false, completion: {})
|
||||||
self.addSubnode(self.statusNode)
|
self.addSubnode(self.statusNode)
|
||||||
}
|
}
|
||||||
|
@ -1290,6 +1290,9 @@
|
|||||||
|
|
||||||
- (void)_seekToPosition:(NSTimeInterval)position manual:(bool)__unused manual
|
- (void)_seekToPosition:(NSTimeInterval)position manual:(bool)__unused manual
|
||||||
{
|
{
|
||||||
|
if (self.player == nil) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
CMTime targetTime = CMTimeMakeWithSeconds(position, NSEC_PER_SEC);
|
CMTime targetTime = CMTimeMakeWithSeconds(position, NSEC_PER_SEC);
|
||||||
|
|
||||||
if (CMTIME_COMPARE_INLINE(targetTime, !=, _chaseTime))
|
if (CMTIME_COMPARE_INLINE(targetTime, !=, _chaseTime))
|
||||||
|
@ -147,12 +147,17 @@ public final class ChatMessageLiveLocationPositionNode: ASDisplayNode {
|
|||||||
var hasPulse = false
|
var hasPulse = false
|
||||||
var heading: Double?
|
var heading: Double?
|
||||||
var coordinate: (Double, Double)?
|
var coordinate: (Double, Double)?
|
||||||
|
|
||||||
|
func degToRad(_ degrees: Double) -> Double {
|
||||||
|
return degrees * Double.pi / 180.0
|
||||||
|
}
|
||||||
|
|
||||||
switch mode {
|
switch mode {
|
||||||
case let .liveLocation(_, active, latitude, longitude, headingValue):
|
case let .liveLocation(_, active, latitude, longitude, headingValue):
|
||||||
backgroundImage = avatarBackgroundImage
|
backgroundImage = avatarBackgroundImage
|
||||||
hasPulse = active
|
hasPulse = active
|
||||||
coordinate = (latitude, longitude)
|
coordinate = (latitude, longitude)
|
||||||
heading = headingValue.flatMap { Double($0) }
|
heading = headingValue.flatMap { degToRad(Double($0)) }
|
||||||
case let .location(location):
|
case let .location(location):
|
||||||
let venueType = location?.venue?.type ?? ""
|
let venueType = location?.venue?.type ?? ""
|
||||||
let color = venueType.isEmpty ? theme.list.itemAccentColor : venueIconColor(type: venueType)
|
let color = venueType.isEmpty ? theme.list.itemAccentColor : venueIconColor(type: venueType)
|
||||||
@ -162,10 +167,6 @@ public final class ChatMessageLiveLocationPositionNode: ASDisplayNode {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func degToRad(_ degrees: Double) -> Double {
|
|
||||||
return degrees * Double.pi / 180.0
|
|
||||||
}
|
|
||||||
|
|
||||||
if heading == nil, let currentCoordinate = currentCoordinate, let coordinate = coordinate {
|
if heading == nil, let currentCoordinate = currentCoordinate, let coordinate = coordinate {
|
||||||
let lat1 = degToRad(currentCoordinate.0)
|
let lat1 = degToRad(currentCoordinate.0)
|
||||||
let lon1 = degToRad(currentCoordinate.1)
|
let lon1 = degToRad(currentCoordinate.1)
|
||||||
@ -248,7 +249,7 @@ public final class ChatMessageLiveLocationPositionNode: ASDisplayNode {
|
|||||||
strongSelf.arrowNode.isHidden = heading == nil || !hasPulse
|
strongSelf.arrowNode.isHidden = heading == nil || !hasPulse
|
||||||
strongSelf.arrowNode.position = CGPoint(x: 31.0, y: 64.0)
|
strongSelf.arrowNode.position = CGPoint(x: 31.0, y: 64.0)
|
||||||
|
|
||||||
strongSelf.arrowNode.transform = CATransform3DMakeRotation(CGFloat(heading ?? 0.0 / 180.0 * Double.pi), 0.0, 0.0, 1.0)
|
strongSelf.arrowNode.transform = CATransform3DMakeRotation(CGFloat(heading ?? 0), 0.0, 0.0, 1.0)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -74,7 +74,7 @@ private func generateLiveLocationIcon(theme: PresentationTheme, stop: Bool) -> U
|
|||||||
context.scaleBy(x: 1.0, y: -1.0)
|
context.scaleBy(x: 1.0, y: -1.0)
|
||||||
context.translateBy(x: -size.width / 2.0, y: -size.height / 2.0)
|
context.translateBy(x: -size.width / 2.0, y: -size.height / 2.0)
|
||||||
|
|
||||||
if let image = generateTintedImage(image: UIImage(bundleImageName: "Location/SendLocationIcon"), color: theme.chat.inputPanel.actionControlForegroundColor) {
|
if let image = generateTintedImage(image: UIImage(bundleImageName: stop ? "Location/SendLocationIcon" : "Location/SendLiveLocationIcon"), color: theme.chat.inputPanel.actionControlForegroundColor) {
|
||||||
context.draw(image.cgImage!, in: CGRect(origin: CGPoint(x: floor((size.width - image.size.width) / 2.0), y: floor((size.height - image.size.height) / 2.0)), size: image.size))
|
context.draw(image.cgImage!, in: CGRect(origin: CGPoint(x: floor((size.width - image.size.width) / 2.0), y: floor((size.height - image.size.height) / 2.0)), size: image.size))
|
||||||
}
|
}
|
||||||
})!
|
})!
|
||||||
|
@ -43,7 +43,7 @@ class LocationPinAnnotation: NSObject, MKAnnotation {
|
|||||||
let peer: Peer?
|
let peer: Peer?
|
||||||
let message: Message?
|
let message: Message?
|
||||||
let forcedSelection: Bool
|
let forcedSelection: Bool
|
||||||
var heading: Int32? {
|
@objc dynamic var heading: NSNumber? {
|
||||||
willSet {
|
willSet {
|
||||||
self.willChangeValue(forKey: "heading")
|
self.willChangeValue(forKey: "heading")
|
||||||
}
|
}
|
||||||
@ -91,7 +91,7 @@ class LocationPinAnnotation: NSObject, MKAnnotation {
|
|||||||
}
|
}
|
||||||
self.selfPeer = selfPeer
|
self.selfPeer = selfPeer
|
||||||
self.forcedSelection = false
|
self.forcedSelection = false
|
||||||
self.heading = heading
|
self.heading = heading.flatMap { NSNumber(value: $0) }
|
||||||
super.init()
|
super.init()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -167,6 +167,8 @@ class LocationPinAnnotationView: MKAnnotationView {
|
|||||||
|
|
||||||
var hasPulse = false
|
var hasPulse = false
|
||||||
|
|
||||||
|
var headingKvoToken: NSKeyValueObservation?
|
||||||
|
|
||||||
override class var layerClass: AnyClass {
|
override class var layerClass: AnyClass {
|
||||||
return LocationPinAnnotationLayer.self
|
return LocationPinAnnotationLayer.self
|
||||||
}
|
}
|
||||||
@ -233,6 +235,14 @@ class LocationPinAnnotationView: MKAnnotationView {
|
|||||||
self.annotation = annotation
|
self.annotation = annotation
|
||||||
}
|
}
|
||||||
|
|
||||||
|
required init?(coder aDecoder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
self.headingKvoToken?.invalidate()
|
||||||
|
}
|
||||||
|
|
||||||
var defaultZPosition: CGFloat {
|
var defaultZPosition: CGFloat {
|
||||||
if let annotation = self.annotation as? LocationPinAnnotation {
|
if let annotation = self.annotation as? LocationPinAnnotation {
|
||||||
if annotation.forcedSelection {
|
if annotation.forcedSelection {
|
||||||
@ -247,10 +257,6 @@ class LocationPinAnnotationView: MKAnnotationView {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
required init?(coder aDecoder: NSCoder) {
|
|
||||||
fatalError("init(coder:) has not been implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
override var annotation: MKAnnotation? {
|
override var annotation: MKAnnotation? {
|
||||||
didSet {
|
didSet {
|
||||||
if let annotation = self.annotation as? LocationPinAnnotation {
|
if let annotation = self.annotation as? LocationPinAnnotation {
|
||||||
@ -270,6 +276,18 @@ class LocationPinAnnotationView: MKAnnotationView {
|
|||||||
self.shadowNode.isHidden = true
|
self.shadowNode.isHidden = true
|
||||||
self.smallNode.isHidden = false
|
self.smallNode.isHidden = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let headingKvoToken = self.headingKvoToken {
|
||||||
|
self.headingKvoToken = nil
|
||||||
|
headingKvoToken.invalidate()
|
||||||
|
}
|
||||||
|
|
||||||
|
self.headingKvoToken = annotation.observe(\.heading, options: .new) { [weak self] (_, change) in
|
||||||
|
guard let heading = change.newValue else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
self?.updateHeading(heading)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else if let peer = annotation.peer {
|
else if let peer = annotation.peer {
|
||||||
self.iconNode.isHidden = true
|
self.iconNode.isHidden = true
|
||||||
@ -278,6 +296,12 @@ class LocationPinAnnotationView: MKAnnotationView {
|
|||||||
|
|
||||||
self.setPeer(context: annotation.context, theme: annotation.theme, peer: peer)
|
self.setPeer(context: annotation.context, theme: annotation.theme, peer: peer)
|
||||||
self.setSelected(true, animated: false)
|
self.setSelected(true, animated: false)
|
||||||
|
|
||||||
|
if let headingKvoToken = self.headingKvoToken {
|
||||||
|
self.headingKvoToken = nil
|
||||||
|
headingKvoToken.invalidate()
|
||||||
|
}
|
||||||
|
self.updateHeading(nil)
|
||||||
} else if let location = annotation.location {
|
} else if let location = annotation.location {
|
||||||
let venueType = location.venue?.type ?? ""
|
let venueType = location.venue?.type ?? ""
|
||||||
let color = venueType.isEmpty ? annotation.theme.list.itemAccentColor : venueIconColor(type: venueType)
|
let color = venueType.isEmpty ? annotation.theme.list.itemAccentColor : venueIconColor(type: venueType)
|
||||||
@ -299,15 +323,36 @@ class LocationPinAnnotationView: MKAnnotationView {
|
|||||||
self.setSelected(true, animated: false)
|
self.setSelected(true, animated: false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let avatarNode = self.avatarNode {
|
||||||
|
self.avatarNode = nil
|
||||||
|
avatarNode.removeFromSupernode()
|
||||||
|
}
|
||||||
|
|
||||||
if self.initialized && !self.appeared {
|
if self.initialized && !self.appeared {
|
||||||
self.appeared = true
|
self.appeared = true
|
||||||
self.animateAppearance()
|
self.animateAppearance()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let headingKvoToken = self.headingKvoToken {
|
||||||
|
self.headingKvoToken = nil
|
||||||
|
headingKvoToken.invalidate()
|
||||||
|
}
|
||||||
|
self.updateHeading(nil)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func updateHeading(_ heading: NSNumber?) {
|
||||||
|
if let heading = heading?.int32Value {
|
||||||
|
self.arrowNode.isHidden = false
|
||||||
|
self.arrowNode.transform = CATransform3DMakeRotation(CGFloat(heading) / 180.0 * CGFloat.pi, 0.0, 0.0, 1.0)
|
||||||
|
} else {
|
||||||
|
self.arrowNode.isHidden = true
|
||||||
|
self.arrowNode.transform = CATransform3DIdentity
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override func prepareForReuse() {
|
override func prepareForReuse() {
|
||||||
self.smallNode.isHidden = true
|
self.smallNode.isHidden = true
|
||||||
self.backgroundNode.isHidden = false
|
self.backgroundNode.isHidden = false
|
||||||
|
@ -161,8 +161,10 @@ private var unitValues: [Int32] = {
|
|||||||
|
|
||||||
private var smallUnitValues: [Int32] = {
|
private var smallUnitValues: [Int32] = {
|
||||||
var values: [Int32] = []
|
var values: [Int32] = []
|
||||||
for i in 0 ..< 100 {
|
values.append(0)
|
||||||
values.append(Int32(i))
|
values.append(5)
|
||||||
|
for i in 1 ..< 10 {
|
||||||
|
values.append(Int32(i * 10))
|
||||||
}
|
}
|
||||||
return values
|
return values
|
||||||
}()
|
}()
|
||||||
@ -185,6 +187,8 @@ class LocationDistancePickerScreenNode: ViewControllerTracingNode, UIScrollViewD
|
|||||||
private let doneButton: SolidRoundedButtonNode
|
private let doneButton: SolidRoundedButtonNode
|
||||||
|
|
||||||
private var pickerView: TimerPickerView?
|
private var pickerView: TimerPickerView?
|
||||||
|
private let unitLabelNode: ImmediateTextNode
|
||||||
|
private let smallUnitLabelNode: ImmediateTextNode
|
||||||
|
|
||||||
private var containerLayout: (ContainerViewLayout, CGFloat)?
|
private var containerLayout: (ContainerViewLayout, CGFloat)?
|
||||||
|
|
||||||
@ -251,11 +255,18 @@ class LocationDistancePickerScreenNode: ViewControllerTracingNode, UIScrollViewD
|
|||||||
self.doneButton = SolidRoundedButtonNode(theme: SolidRoundedButtonTheme(theme: self.presentationData.theme), height: 52.0, cornerRadius: 11.0, gloss: false)
|
self.doneButton = SolidRoundedButtonNode(theme: SolidRoundedButtonTheme(theme: self.presentationData.theme), height: 52.0, cornerRadius: 11.0, gloss: false)
|
||||||
self.doneButton.title = self.presentationData.strings.Conversation_Timer_Send
|
self.doneButton.title = self.presentationData.strings.Conversation_Timer_Send
|
||||||
|
|
||||||
|
self.unitLabelNode = ImmediateTextNode()
|
||||||
|
|
||||||
|
self.smallUnitLabelNode = ImmediateTextNode()
|
||||||
|
|
||||||
super.init()
|
super.init()
|
||||||
|
|
||||||
self.backgroundColor = nil
|
self.backgroundColor = nil
|
||||||
self.isOpaque = false
|
self.isOpaque = false
|
||||||
|
|
||||||
|
self.unitLabelNode.attributedText = NSAttributedString(string: self.usesMetricSystem ? self.presentationData.strings.Location_ProximityNotification_DistanceKM : self.presentationData.strings.Location_ProximityNotification_DistanceMI, font: Font.regular(15.0), textColor: textColor)
|
||||||
|
self.smallUnitLabelNode.attributedText = NSAttributedString(string: self.usesMetricSystem ? self.presentationData.strings.Location_ProximityNotification_DistanceM : "", font: Font.regular(15.0), textColor: textColor)
|
||||||
|
|
||||||
self.dimNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dimTapGesture(_:))))
|
self.dimNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dimTapGesture(_:))))
|
||||||
self.addSubnode(self.dimNode)
|
self.addSubnode(self.dimNode)
|
||||||
|
|
||||||
@ -272,6 +283,9 @@ class LocationDistancePickerScreenNode: ViewControllerTracingNode, UIScrollViewD
|
|||||||
self.contentContainerNode.addSubnode(self.cancelButton)
|
self.contentContainerNode.addSubnode(self.cancelButton)
|
||||||
self.contentContainerNode.addSubnode(self.doneButton)
|
self.contentContainerNode.addSubnode(self.doneButton)
|
||||||
|
|
||||||
|
self.contentContainerNode.addSubnode(self.unitLabelNode)
|
||||||
|
self.contentContainerNode.addSubnode(self.smallUnitLabelNode)
|
||||||
|
|
||||||
self.cancelButton.addTarget(self, action: #selector(self.cancelButtonPressed), forControlEvents: .touchUpInside)
|
self.cancelButton.addTarget(self, action: #selector(self.cancelButtonPressed), forControlEvents: .touchUpInside)
|
||||||
self.doneButton.pressed = { [weak self] in
|
self.doneButton.pressed = { [weak self] in
|
||||||
if let strongSelf = self, let pickerView = strongSelf.pickerView {
|
if let strongSelf = self, let pickerView = strongSelf.pickerView {
|
||||||
@ -280,7 +294,7 @@ class LocationDistancePickerScreenNode: ViewControllerTracingNode, UIScrollViewD
|
|||||||
let largeValue = unitValues[pickerView.selectedRow(inComponent: 0)]
|
let largeValue = unitValues[pickerView.selectedRow(inComponent: 0)]
|
||||||
let smallValue = smallUnitValues[pickerView.selectedRow(inComponent: 1)]
|
let smallValue = smallUnitValues[pickerView.selectedRow(inComponent: 1)]
|
||||||
var value = largeValue * 1000 + smallValue * 10
|
var value = largeValue * 1000 + smallValue * 10
|
||||||
if !strongSelf.usesMetricSystem() {
|
if !strongSelf.usesMetricSystem {
|
||||||
value = Int32(Double(value) * 1.60934)
|
value = Int32(Double(value) * 1.60934)
|
||||||
}
|
}
|
||||||
strongSelf.completion?(value)
|
strongSelf.completion?(value)
|
||||||
@ -313,18 +327,21 @@ class LocationDistancePickerScreenNode: ViewControllerTracingNode, UIScrollViewD
|
|||||||
pickerView.delegate = self
|
pickerView.delegate = self
|
||||||
pickerView.selectRow(0, inComponent: 0, animated: false)
|
pickerView.selectRow(0, inComponent: 0, animated: false)
|
||||||
|
|
||||||
if self.usesMetricSystem() {
|
if self.usesMetricSystem {
|
||||||
pickerView.selectRow(50, inComponent: 1, animated: false)
|
pickerView.selectRow(6, inComponent: 1, animated: false)
|
||||||
} else {
|
} else {
|
||||||
pickerView.selectRow(30, inComponent: 1, animated: false)
|
pickerView.selectRow(4, inComponent: 1, animated: false)
|
||||||
}
|
}
|
||||||
self.contentContainerNode.view.addSubview(pickerView)
|
self.contentContainerNode.view.addSubview(pickerView)
|
||||||
self.pickerView = pickerView
|
self.pickerView = pickerView
|
||||||
|
|
||||||
|
self.contentContainerNode.addSubnode(self.unitLabelNode)
|
||||||
|
self.contentContainerNode.addSubnode(self.smallUnitLabelNode)
|
||||||
|
|
||||||
self.updateDoneButtonTitle()
|
self.updateDoneButtonTitle()
|
||||||
}
|
}
|
||||||
|
|
||||||
private func usesMetricSystem() -> Bool {
|
private var usesMetricSystem: Bool {
|
||||||
let locale = localeWithStrings(self.presentationData.strings)
|
let locale = localeWithStrings(self.presentationData.strings)
|
||||||
if locale.identifier.hasSuffix("GB") {
|
if locale.identifier.hasSuffix("GB") {
|
||||||
return false
|
return false
|
||||||
@ -333,7 +350,7 @@ class LocationDistancePickerScreenNode: ViewControllerTracingNode, UIScrollViewD
|
|||||||
}
|
}
|
||||||
|
|
||||||
func numberOfComponents(in pickerView: UIPickerView) -> Int {
|
func numberOfComponents(in pickerView: UIPickerView) -> Int {
|
||||||
return 3
|
return 2
|
||||||
}
|
}
|
||||||
|
|
||||||
private func updateDoneButtonTitle() {
|
private func updateDoneButtonTitle() {
|
||||||
@ -348,7 +365,7 @@ class LocationDistancePickerScreenNode: ViewControllerTracingNode, UIScrollViewD
|
|||||||
let smallValue = smallUnitValues[selectedSmallRow]
|
let smallValue = smallUnitValues[selectedSmallRow]
|
||||||
|
|
||||||
var value = largeValue * 1000 + smallValue * 10
|
var value = largeValue * 1000 + smallValue * 10
|
||||||
if !self.usesMetricSystem() {
|
if !self.usesMetricSystem {
|
||||||
value = Int32(Double(value) * 1.60934)
|
value = Int32(Double(value) * 1.60934)
|
||||||
}
|
}
|
||||||
let distance = stringForDistance(strings: self.presentationData.strings, distance: CLLocationDistance(value))
|
let distance = stringForDistance(strings: self.presentationData.strings, distance: CLLocationDistance(value))
|
||||||
@ -377,7 +394,7 @@ class LocationDistancePickerScreenNode: ViewControllerTracingNode, UIScrollViewD
|
|||||||
let smallValue = smallUnitValues[pickerView.selectedRow(inComponent: 1)]
|
let smallValue = smallUnitValues[pickerView.selectedRow(inComponent: 1)]
|
||||||
|
|
||||||
var value = largeValue * 1000 + smallValue * 10
|
var value = largeValue * 1000 + smallValue * 10
|
||||||
if !self.usesMetricSystem() {
|
if !self.usesMetricSystem {
|
||||||
value = Int32(Double(value) * 1.60934)
|
value = Int32(Double(value) * 1.60934)
|
||||||
}
|
}
|
||||||
self.updated?(value)
|
self.updated?(value)
|
||||||
@ -409,11 +426,20 @@ class LocationDistancePickerScreenNode: ViewControllerTracingNode, UIScrollViewD
|
|||||||
if component == 0 {
|
if component == 0 {
|
||||||
let value = unitValues[row]
|
let value = unitValues[row]
|
||||||
string = "\(value)"
|
string = "\(value)"
|
||||||
} else if component == 1 {
|
|
||||||
let value = String(format: "%.2d", smallUnitValues[row])
|
|
||||||
string = ".\(value)"
|
|
||||||
} else {
|
} else {
|
||||||
string = self.usesMetricSystem() ? self.presentationData.strings.Location_ProximityNotification_DistanceKM : self.presentationData.strings.Location_ProximityNotification_DistanceMI
|
if self.usesMetricSystem {
|
||||||
|
let value = String(format: "%d", smallUnitValues[row] * 10)
|
||||||
|
string = "\(value)"
|
||||||
|
} else {
|
||||||
|
let value = smallUnitValues[row]
|
||||||
|
if value == 0 {
|
||||||
|
string = ".0"
|
||||||
|
} else if value == 5 {
|
||||||
|
string = ".05"
|
||||||
|
} else {
|
||||||
|
string = ".\(value / 10)"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return NSAttributedString(string: string, font: font, textColor: self.presentationData.theme.actionSheet.primaryTextColor)
|
return NSAttributedString(string: string, font: font, textColor: self.presentationData.theme.actionSheet.primaryTextColor)
|
||||||
}
|
}
|
||||||
@ -442,6 +468,9 @@ class LocationDistancePickerScreenNode: ViewControllerTracingNode, UIScrollViewD
|
|||||||
self.doneButton.updateTheme(SolidRoundedButtonTheme(theme: self.presentationData.theme))
|
self.doneButton.updateTheme(SolidRoundedButtonTheme(theme: self.presentationData.theme))
|
||||||
|
|
||||||
self.updateDoneButtonTitle()
|
self.updateDoneButtonTitle()
|
||||||
|
|
||||||
|
self.unitLabelNode.attributedText = NSAttributedString(string: self.usesMetricSystem ? self.presentationData.strings.Location_ProximityNotification_DistanceKM : self.presentationData.strings.Location_ProximityNotification_DistanceMI, font: Font.regular(15.0), textColor: self.presentationData.theme.actionSheet.primaryTextColor)
|
||||||
|
self.smallUnitLabelNode.attributedText = NSAttributedString(string: self.usesMetricSystem ? self.presentationData.strings.Location_ProximityNotification_DistanceM : "", font: Font.regular(15.0), textColor: self.presentationData.theme.actionSheet.primaryTextColor)
|
||||||
}
|
}
|
||||||
|
|
||||||
override func didLoad() {
|
override func didLoad() {
|
||||||
@ -465,8 +494,8 @@ class LocationDistancePickerScreenNode: ViewControllerTracingNode, UIScrollViewD
|
|||||||
func animateIn() {
|
func animateIn() {
|
||||||
self.dimNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
|
self.dimNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
|
||||||
|
|
||||||
let offset = self.bounds.size.height - self.contentBackgroundNode.frame.minY
|
let offset = self.contentContainerNode.frame.height
|
||||||
self.wrappingScrollNode.layer.animateBoundsOriginYAdditive(from: -offset, to: 0.0, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring)
|
self.wrappingScrollNode.layer.animatePosition(from: CGPoint(x: 0.0, y: offset), to: CGPoint(), duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, additive: true)
|
||||||
}
|
}
|
||||||
|
|
||||||
func animateOut(completion: (() -> Void)? = nil) {
|
func animateOut(completion: (() -> Void)? = nil) {
|
||||||
@ -485,8 +514,8 @@ class LocationDistancePickerScreenNode: ViewControllerTracingNode, UIScrollViewD
|
|||||||
internalCompletion()
|
internalCompletion()
|
||||||
})
|
})
|
||||||
|
|
||||||
let offset = self.bounds.size.height - self.contentBackgroundNode.frame.minY
|
let offset = self.contentContainerNode.frame.height
|
||||||
self.wrappingScrollNode.layer.animateBoundsOriginYAdditive(from: 0.0, to: -offset, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, completion: { _ in
|
self.wrappingScrollNode.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: offset), duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, additive: true, completion: { _ in
|
||||||
offsetCompleted = true
|
offsetCompleted = true
|
||||||
internalCompletion()
|
internalCompletion()
|
||||||
})
|
})
|
||||||
@ -556,7 +585,14 @@ class LocationDistancePickerScreenNode: ViewControllerTracingNode, UIScrollViewD
|
|||||||
let textSize = self.textNode.updateLayout(CGSize(width: width, height: titleHeight))
|
let textSize = self.textNode.updateLayout(CGSize(width: width, height: titleHeight))
|
||||||
transition.updateFrame(node: self.textNode, frame: CGRect(origin: CGPoint(x: floor((width - textSize.width) / 2.0), y: floor(doneButtonFrame.center.y - textSize.height / 2.0)), size: textSize))
|
transition.updateFrame(node: self.textNode, frame: CGRect(origin: CGPoint(x: floor((width - textSize.width) / 2.0), y: floor(doneButtonFrame.center.y - textSize.height / 2.0)), size: textSize))
|
||||||
|
|
||||||
self.pickerView?.frame = CGRect(origin: CGPoint(x: 0.0, y: 54.0), size: CGSize(width: contentFrame.width, height: pickerHeight))
|
let pickerFrame = CGRect(origin: CGPoint(x: 0.0, y: 54.0), size: CGSize(width: contentFrame.width, height: pickerHeight))
|
||||||
|
self.pickerView?.frame = pickerFrame
|
||||||
|
|
||||||
|
let unitLabelSize = self.unitLabelNode.updateLayout(CGSize(width: width, height: titleHeight))
|
||||||
|
transition.updateFrame(node: self.unitLabelNode, frame: CGRect(origin: CGPoint(x: floor(pickerFrame.width / 4.0) + 50.0, y: floor(pickerFrame.center.y - unitLabelSize.height / 2.0)), size: unitLabelSize))
|
||||||
|
|
||||||
|
let smallUnitLabelSize = self.smallUnitLabelNode.updateLayout(CGSize(width: width, height: titleHeight))
|
||||||
|
transition.updateFrame(node: self.smallUnitLabelNode, frame: CGRect(origin: CGPoint(x: floor(pickerFrame.width / 4.0 * 3.0) + 50.0, y: floor(pickerFrame.center.y - smallUnitLabelSize.height / 2.0)), size: smallUnitLabelSize))
|
||||||
|
|
||||||
transition.updateFrame(node: self.contentContainerNode, frame: contentContainerFrame)
|
transition.updateFrame(node: self.contentContainerNode, frame: contentContainerFrame)
|
||||||
}
|
}
|
||||||
|
@ -83,6 +83,11 @@ private class LocationMapView: MKMapView, UIGestureRecognizerDelegate {
|
|||||||
|
|
||||||
return pointInside
|
return pointInside
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public override func layoutSubviews() {
|
||||||
|
super.layoutSubviews()
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private let arrowImageSize = CGSize(width: 90.0, height: 90.0)
|
private let arrowImageSize = CGSize(width: 90.0, height: 90.0)
|
||||||
@ -110,14 +115,16 @@ func generateHeadingArrowImage() -> UIImage? {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
private func generateProximityDim(size: CGSize, rect: CGRect) -> UIImage {
|
private func generateProximityDim(size: CGSize) -> UIImage {
|
||||||
return generateImage(size, rotatedContext: { size, context in
|
return generateImage(size, rotatedContext: { size, context in
|
||||||
context.clear(CGRect(origin: CGPoint(), size: size))
|
context.clear(CGRect(origin: CGPoint(), size: size))
|
||||||
context.setFillColor(UIColor(rgb: 0x000000, alpha: 0.4).cgColor)
|
context.setFillColor(UIColor(rgb: 0x000000, alpha: 0.4).cgColor)
|
||||||
context.fill(CGRect(origin: CGPoint(), size: size))
|
context.fill(CGRect(origin: CGPoint(), size: size))
|
||||||
|
|
||||||
context.setBlendMode(.clear)
|
context.setBlendMode(.clear)
|
||||||
context.fillEllipse(in: rect)
|
|
||||||
|
let ellipseSize = CGSize(width: 260.0, height: 260.0)
|
||||||
|
context.fillEllipse(in: CGRect(origin: CGPoint(x: (size.width - ellipseSize.width) / 2.0, y: (size.height - ellipseSize.height) / 2.0), size: ellipseSize))
|
||||||
})!
|
})!
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -145,63 +152,6 @@ final class LocationMapNode: ASDisplayNode, MKMapViewDelegate {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class InvertedProximityCircle: NSObject, MKOverlay {
|
|
||||||
var coordinate: CLLocationCoordinate2D
|
|
||||||
var radius: Double
|
|
||||||
var alpha: CGFloat {
|
|
||||||
didSet {
|
|
||||||
self.alphaTransition = (oldValue, CACurrentMediaTime(), 0.3)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
var alphaTransition: (from: CGFloat, startTimestamp: Double, duration: Double)?
|
|
||||||
|
|
||||||
var boundingMapRect: MKMapRect {
|
|
||||||
return MKMapRect.world
|
|
||||||
}
|
|
||||||
|
|
||||||
init(center coord: CLLocationCoordinate2D, radius: Double, alpha: CGFloat = 0.0) {
|
|
||||||
self.coordinate = coord
|
|
||||||
self.radius = radius
|
|
||||||
self.alpha = alpha
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class InvertedProximityCircleRenderer: MKOverlayRenderer {
|
|
||||||
var radius: Double = 0.0
|
|
||||||
var fillColor: UIColor = UIColor(rgb: 0x000000, alpha: 0.5)
|
|
||||||
|
|
||||||
override func draw(_ mapRect: MKMapRect, zoomScale: MKZoomScale, in context: CGContext) {
|
|
||||||
guard let overlay = self.overlay as? InvertedProximityCircle else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var alpha: CGFloat = overlay.alpha
|
|
||||||
if let transition = overlay.alphaTransition {
|
|
||||||
var t = (CACurrentMediaTime() - transition.startTimestamp) / transition.duration
|
|
||||||
t = min(1.0, max(0.0, t))
|
|
||||||
alpha = transition.from + (alpha - transition.from) * CGFloat(t)
|
|
||||||
}
|
|
||||||
|
|
||||||
context.setAlpha(alpha)
|
|
||||||
|
|
||||||
let path = UIBezierPath(rect: CGRect(x: mapRect.origin.x, y: mapRect.origin.y, width: mapRect.size.width, height: mapRect.size.height))
|
|
||||||
let radiusInMap = overlay.radius * MKMapPointsPerMeterAtLatitude(overlay.coordinate.latitude) * 2.0
|
|
||||||
let mapSize: MKMapSize = MKMapSize(width: radiusInMap, height: radiusInMap)
|
|
||||||
let regionOrigin = MKMapPoint(overlay.coordinate)
|
|
||||||
var regionRect: MKMapRect = MKMapRect(origin: regionOrigin, size: mapSize)
|
|
||||||
regionRect = regionRect.offsetBy(dx: -radiusInMap / 2.0, dy: -radiusInMap / 2.0);
|
|
||||||
regionRect = regionRect.intersection(MKMapRect.world);
|
|
||||||
|
|
||||||
let excludePath: UIBezierPath = UIBezierPath(roundedRect: CGRect(x: regionRect.origin.x, y: regionRect.origin.y, width: regionRect.size.width, height: regionRect.size.height), cornerRadius: CGFloat(regionRect.size.width) / 2.0)
|
|
||||||
path.append(excludePath)
|
|
||||||
|
|
||||||
context.setFillColor(fillColor.cgColor);
|
|
||||||
context.addPath(path.cgPath);
|
|
||||||
context.fillPath(using: .evenOdd)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
private weak var currentInvertedCircleRenderer: InvertedProximityCircleRenderer?
|
|
||||||
|
|
||||||
private let locationPromise = Promise<CLLocation?>(nil)
|
private let locationPromise = Promise<CLLocation?>(nil)
|
||||||
|
|
||||||
private let pickerAnnotationContainerView: PickerAnnotationContainerView
|
private let pickerAnnotationContainerView: PickerAnnotationContainerView
|
||||||
@ -223,71 +173,31 @@ final class LocationMapNode: ASDisplayNode, MKMapViewDelegate {
|
|||||||
var annotationSelected: ((LocationPinAnnotation?) -> Void)?
|
var annotationSelected: ((LocationPinAnnotation?) -> Void)?
|
||||||
var userLocationAnnotationSelected: (() -> Void)?
|
var userLocationAnnotationSelected: (() -> Void)?
|
||||||
|
|
||||||
var indicatorOverlay: InvertedProximityCircle?
|
var proximityDimView = UIImageView()
|
||||||
var proximityIndicatorRadius: Double? {
|
var proximityIndicatorRadius: Double? {
|
||||||
didSet {
|
didSet {
|
||||||
if let activeProximityRadius = self.proximityIndicatorRadius {
|
if let radius = self.proximityIndicatorRadius, let mapView = self.mapView {
|
||||||
if let location = self.currentUserLocation, activeProximityRadius != oldValue {
|
if self.proximityDimView.image == nil {
|
||||||
let indicatorOverlay: InvertedProximityCircle
|
proximityDimView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||||
if let current = self.indicatorOverlay {
|
}
|
||||||
indicatorOverlay = current
|
|
||||||
indicatorOverlay.radius = activeProximityRadius
|
if oldValue == 0 {
|
||||||
self.mapView?.removeOverlay(indicatorOverlay)
|
UIView.transition(with: proximityDimView, duration: 0.3, options: .transitionCrossDissolve) {
|
||||||
self.mapView?.addOverlay(indicatorOverlay)
|
self.proximityDimView.image = generateProximityDim(size: mapView.bounds.size)
|
||||||
} else {
|
} completion: { _ in
|
||||||
indicatorOverlay = InvertedProximityCircle(center: location.coordinate, radius: activeProximityRadius)
|
|
||||||
self.mapView?.addOverlay(indicatorOverlay)
|
|
||||||
self.indicatorOverlay = indicatorOverlay
|
|
||||||
indicatorOverlay.alpha = 1.0
|
|
||||||
self.updateAnimations()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if let indicatorOverlay = self.indicatorOverlay {
|
if self.proximityDimView.image != nil {
|
||||||
indicatorOverlay.alpha = 0.0
|
UIView.transition(with: proximityDimView, duration: 0.3, options: .transitionCrossDissolve) {
|
||||||
self.updateAnimations()
|
self.proximityDimView.image = nil
|
||||||
}
|
} completion: { _ in
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private var animator: ConstantDisplayLinkAnimator?
|
|
||||||
private func updateAnimations() {
|
|
||||||
guard let mapView = self.mapView else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var animate = false
|
|
||||||
let timestamp = CACurrentMediaTime()
|
|
||||||
|
|
||||||
if let indicatorOverlay = self.indicatorOverlay, let transition = indicatorOverlay.alphaTransition {
|
|
||||||
if transition.startTimestamp + transition.duration < timestamp {
|
|
||||||
indicatorOverlay.alphaTransition = nil
|
|
||||||
if indicatorOverlay.alpha.isZero {
|
|
||||||
self.indicatorOverlay = nil
|
|
||||||
mapView.removeOverlay(indicatorOverlay)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
animate = true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if animate {
|
|
||||||
let animator: ConstantDisplayLinkAnimator
|
|
||||||
if let current = self.animator {
|
|
||||||
animator = current
|
|
||||||
} else {
|
|
||||||
animator = ConstantDisplayLinkAnimator(update: { [weak self] in
|
|
||||||
self?.updateAnimations()
|
|
||||||
})
|
|
||||||
self.animator = animator
|
|
||||||
}
|
}
|
||||||
animator.isPaused = false
|
|
||||||
} else {
|
|
||||||
self.animator?.isPaused = true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
self.currentInvertedCircleRenderer?.setNeedsDisplay(MKMapRect.world)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private var circleOverlay: MKCircle?
|
private var circleOverlay: MKCircle?
|
||||||
@ -372,8 +282,25 @@ final class LocationMapNode: ASDisplayNode, MKMapViewDelegate {
|
|||||||
var trackingMode: LocationTrackingMode = .none {
|
var trackingMode: LocationTrackingMode = .none {
|
||||||
didSet {
|
didSet {
|
||||||
self.mapView?.userTrackingMode = self.trackingMode.userTrackingMode
|
self.mapView?.userTrackingMode = self.trackingMode.userTrackingMode
|
||||||
|
if self.trackingMode == .followWithHeading && self.headingArrowView?.image != nil {
|
||||||
|
self.headingArrowView?.image = nil
|
||||||
|
} else if self.trackingMode != .followWithHeading && self.headingArrowView?.image == nil {
|
||||||
|
self.headingArrowView?.image = generateHeadingArrowImage()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var mapOffset: CGFloat = 0.0
|
||||||
|
func setMapCenter(coordinate: CLLocationCoordinate2D, radius: Double, insets: UIEdgeInsets, offset: CGFloat, animated: Bool = false) {
|
||||||
|
self.mapOffset = offset
|
||||||
|
self.ignoreRegionChanges = true
|
||||||
|
|
||||||
|
let mapRect = MKMapRect(region: MKCoordinateRegion(center: coordinate, latitudinalMeters: radius * 2.0, longitudinalMeters: radius * 2.0))
|
||||||
|
self.mapView?.setVisibleMapRect(mapRect, edgePadding: insets, animated: animated)
|
||||||
|
self.ignoreRegionChanges = false
|
||||||
|
|
||||||
|
self.proximityDimView.center = CGPoint(x: self.bounds.midX, y: self.bounds.midY + offset)
|
||||||
|
}
|
||||||
|
|
||||||
func setMapCenter(coordinate: CLLocationCoordinate2D, span: MKCoordinateSpan = defaultMapSpan, offset: CGPoint = CGPoint(), isUserLocation: Bool = false, hidePicker: Bool = false, animated: Bool = false) {
|
func setMapCenter(coordinate: CLLocationCoordinate2D, span: MKCoordinateSpan = defaultMapSpan, offset: CGPoint = CGPoint(), isUserLocation: Bool = false, hidePicker: Bool = false, animated: Bool = false) {
|
||||||
let region = MKCoordinateRegion(center: coordinate, span: span)
|
let region = MKCoordinateRegion(center: coordinate, span: span)
|
||||||
@ -474,6 +401,10 @@ final class LocationMapNode: ASDisplayNode, MKMapViewDelegate {
|
|||||||
} else if let view = view as? LocationPinAnnotationView {
|
} else if let view = view as? LocationPinAnnotationView {
|
||||||
view.setZPosition(view.defaultZPosition)
|
view.setZPosition(view.defaultZPosition)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let container = view.superview {
|
||||||
|
container.insertSubview(self.proximityDimView, at: 0)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -513,11 +444,7 @@ final class LocationMapNode: ASDisplayNode, MKMapViewDelegate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer {
|
func mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer {
|
||||||
if let invertedCircle = overlay as? InvertedProximityCircle {
|
if let circle = overlay as? MKCircle {
|
||||||
let renderer = InvertedProximityCircleRenderer(overlay: invertedCircle)
|
|
||||||
self.currentInvertedCircleRenderer = renderer
|
|
||||||
return renderer
|
|
||||||
} else if let circle = overlay as? MKCircle {
|
|
||||||
let renderer = ProximityCircleRenderer(circle: circle)
|
let renderer = ProximityCircleRenderer(circle: circle)
|
||||||
renderer.fillColor = .clear
|
renderer.fillColor = .clear
|
||||||
renderer.strokeColor = UIColor(rgb: 0xc3baaf)
|
renderer.strokeColor = UIColor(rgb: 0xc3baaf)
|
||||||
@ -529,17 +456,6 @@ final class LocationMapNode: ASDisplayNode, MKMapViewDelegate {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func mapView(_ mapView: MKMapView, didAdd renderers: [MKOverlayRenderer]) {
|
|
||||||
for renderer in renderers {
|
|
||||||
if let renderer = renderer as? InvertedProximityCircleRenderer {
|
|
||||||
renderer.alpha = 0.0
|
|
||||||
UIView.animate(withDuration: 0.3) {
|
|
||||||
renderer.alpha = 1.0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var distancesToAllAnnotations: Signal<[Double], NoError> {
|
var distancesToAllAnnotations: Signal<[Double], NoError> {
|
||||||
let poll = Signal<[LocationPinAnnotation], NoError> { [weak self] subscriber in
|
let poll = Signal<[LocationPinAnnotation], NoError> { [weak self] subscriber in
|
||||||
if let strongSelf = self {
|
if let strongSelf = self {
|
||||||
@ -676,9 +592,45 @@ final class LocationMapNode: ASDisplayNode, MKMapViewDelegate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if let updatedAnnotation = dict[annotation.id] {
|
if let updatedAnnotation = dict[annotation.id] {
|
||||||
|
func degToRad(_ degrees: Double) -> Double {
|
||||||
|
return degrees * Double.pi / 180.0
|
||||||
|
}
|
||||||
|
|
||||||
|
func radToDeg(_ radians: Double) -> Double {
|
||||||
|
return radians / Double.pi * 180.0
|
||||||
|
}
|
||||||
|
|
||||||
|
let currentCoordinate = annotation.coordinate
|
||||||
|
let coordinate = updatedAnnotation.coordinate
|
||||||
|
var heading = updatedAnnotation.heading
|
||||||
|
if heading == nil {
|
||||||
|
let previous = CLLocation(latitude: currentCoordinate.latitude, longitude: currentCoordinate.longitude)
|
||||||
|
let new = CLLocation(latitude: coordinate.latitude, longitude: coordinate.longitude)
|
||||||
|
|
||||||
|
if new.distance(from: previous) > 10 {
|
||||||
|
let lat1 = degToRad(currentCoordinate.latitude)
|
||||||
|
let lon1 = degToRad(currentCoordinate.longitude)
|
||||||
|
let lat2 = degToRad(coordinate.latitude)
|
||||||
|
let lon2 = degToRad(coordinate.longitude)
|
||||||
|
|
||||||
|
let dLat = lat2 - lat1
|
||||||
|
let dLon = lon2 - lon1
|
||||||
|
|
||||||
|
if dLat != 0 && dLon != 0 {
|
||||||
|
let y = sin(dLon) * cos(lat2)
|
||||||
|
let x = cos(lat1) * sin(lat2) - sin(lat1) * cos(lat2) * cos(dLon)
|
||||||
|
heading = NSNumber(value: radToDeg(atan2(y, x)))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
heading = annotation.heading
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
UIView.animate(withDuration: 0.2) {
|
UIView.animate(withDuration: 0.2) {
|
||||||
annotation.coordinate = updatedAnnotation.coordinate
|
annotation.coordinate = updatedAnnotation.coordinate
|
||||||
}
|
}
|
||||||
|
|
||||||
|
annotation.heading = heading
|
||||||
dict[annotation.id] = nil
|
dict[annotation.id] = nil
|
||||||
} else {
|
} else {
|
||||||
annotationsToRemove.insert(annotation)
|
annotationsToRemove.insert(annotation)
|
||||||
@ -765,6 +717,7 @@ final class LocationMapNode: ASDisplayNode, MKMapViewDelegate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func updateLayout(size: CGSize) {
|
func updateLayout(size: CGSize) {
|
||||||
|
self.proximityDimView.frame = CGRect(origin: CGPoint(x: 0.0, y: self.mapOffset), size: size)
|
||||||
self.pickerAnnotationContainerView.frame = CGRect(x: 0.0, y: floorToScreenPixels((size.height - size.width) / 2.0), width: size.width, height: size.width)
|
self.pickerAnnotationContainerView.frame = CGRect(x: 0.0, y: floorToScreenPixels((size.height - size.width) / 2.0), width: size.width, height: size.width)
|
||||||
if let pickerAnnotationView = self.pickerAnnotationView {
|
if let pickerAnnotationView = self.pickerAnnotationView {
|
||||||
pickerAnnotationView.center = CGPoint(x: self.pickerAnnotationContainerView.frame.width / 2.0, y: self.pickerAnnotationContainerView.frame.height / 2.0)
|
pickerAnnotationView.center = CGPoint(x: self.pickerAnnotationContainerView.frame.width / 2.0, y: self.pickerAnnotationContainerView.frame.height / 2.0)
|
||||||
|
@ -20,12 +20,14 @@ public class LocationViewParams {
|
|||||||
let stopLiveLocation: (MessageId?) -> Void
|
let stopLiveLocation: (MessageId?) -> Void
|
||||||
let openUrl: (String) -> Void
|
let openUrl: (String) -> Void
|
||||||
let openPeer: (Peer) -> Void
|
let openPeer: (Peer) -> Void
|
||||||
|
let showAll: Bool
|
||||||
|
|
||||||
public init(sendLiveLocation: @escaping (TelegramMediaMap) -> Void, stopLiveLocation: @escaping (MessageId?) -> Void, openUrl: @escaping (String) -> Void, openPeer: @escaping (Peer) -> Void) {
|
public init(sendLiveLocation: @escaping (TelegramMediaMap) -> Void, stopLiveLocation: @escaping (MessageId?) -> Void, openUrl: @escaping (String) -> Void, openPeer: @escaping (Peer) -> Void, showAll: Bool = false) {
|
||||||
self.sendLiveLocation = sendLiveLocation
|
self.sendLiveLocation = sendLiveLocation
|
||||||
self.stopLiveLocation = stopLiveLocation
|
self.stopLiveLocation = stopLiveLocation
|
||||||
self.openUrl = openUrl
|
self.openUrl = openUrl
|
||||||
self.openPeer = openPeer
|
self.openPeer = openPeer
|
||||||
|
self.showAll = showAll
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -42,14 +44,14 @@ class LocationViewInteraction {
|
|||||||
let goToCoordinate: (CLLocationCoordinate2D) -> Void
|
let goToCoordinate: (CLLocationCoordinate2D) -> Void
|
||||||
let requestDirections: () -> Void
|
let requestDirections: () -> Void
|
||||||
let share: () -> Void
|
let share: () -> Void
|
||||||
let setupProximityNotification: (Bool, CLLocationCoordinate2D?, MessageId?) -> Void
|
let setupProximityNotification: (Bool, MessageId?) -> Void
|
||||||
let updateSendActionHighlight: (Bool) -> Void
|
let updateSendActionHighlight: (Bool) -> Void
|
||||||
let sendLiveLocation: (CLLocationCoordinate2D, Int32?) -> Void
|
let sendLiveLocation: (Int32?) -> Void
|
||||||
let stopLiveLocation: () -> Void
|
let stopLiveLocation: () -> Void
|
||||||
let updateRightBarButton: (LocationViewRightBarButton) -> Void
|
let updateRightBarButton: (LocationViewRightBarButton) -> Void
|
||||||
let present: (ViewController) -> Void
|
let present: (ViewController) -> Void
|
||||||
|
|
||||||
init(toggleMapModeSelection: @escaping () -> Void, updateMapMode: @escaping (LocationMapMode) -> Void, toggleTrackingMode: @escaping () -> Void, goToCoordinate: @escaping (CLLocationCoordinate2D) -> Void, requestDirections: @escaping () -> Void, share: @escaping () -> Void, setupProximityNotification: @escaping (Bool, CLLocationCoordinate2D?, MessageId?) -> Void, updateSendActionHighlight: @escaping (Bool) -> Void, sendLiveLocation: @escaping (CLLocationCoordinate2D, Int32?) -> Void, stopLiveLocation: @escaping () -> Void, updateRightBarButton: @escaping (LocationViewRightBarButton) -> Void, present: @escaping (ViewController) -> Void) {
|
init(toggleMapModeSelection: @escaping () -> Void, updateMapMode: @escaping (LocationMapMode) -> Void, toggleTrackingMode: @escaping () -> Void, goToCoordinate: @escaping (CLLocationCoordinate2D) -> Void, requestDirections: @escaping () -> Void, share: @escaping () -> Void, setupProximityNotification: @escaping (Bool, MessageId?) -> Void, updateSendActionHighlight: @escaping (Bool) -> Void, sendLiveLocation: @escaping (Int32?) -> Void, stopLiveLocation: @escaping () -> Void, updateRightBarButton: @escaping (LocationViewRightBarButton) -> Void, present: @escaping (ViewController) -> Void) {
|
||||||
self.toggleMapModeSelection = toggleMapModeSelection
|
self.toggleMapModeSelection = toggleMapModeSelection
|
||||||
self.updateMapMode = updateMapMode
|
self.updateMapMode = updateMapMode
|
||||||
self.toggleTrackingMode = toggleTrackingMode
|
self.toggleTrackingMode = toggleTrackingMode
|
||||||
@ -73,6 +75,7 @@ public final class LocationViewController: ViewController {
|
|||||||
public var subject: Message
|
public var subject: Message
|
||||||
private var presentationData: PresentationData
|
private var presentationData: PresentationData
|
||||||
private var presentationDataDisposable: Disposable?
|
private var presentationDataDisposable: Disposable?
|
||||||
|
private var showAll: Bool
|
||||||
|
|
||||||
private let locationManager = LocationManager()
|
private let locationManager = LocationManager()
|
||||||
private var permissionDisposable: Disposable?
|
private var permissionDisposable: Disposable?
|
||||||
@ -84,6 +87,7 @@ public final class LocationViewController: ViewController {
|
|||||||
public init(context: AccountContext, subject: Message, params: LocationViewParams) {
|
public init(context: AccountContext, subject: Message, params: LocationViewParams) {
|
||||||
self.context = context
|
self.context = context
|
||||||
self.subject = subject
|
self.subject = subject
|
||||||
|
self.showAll = params.showAll
|
||||||
|
|
||||||
self.presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
self.presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||||
|
|
||||||
@ -173,16 +177,39 @@ public final class LocationViewController: ViewController {
|
|||||||
})
|
})
|
||||||
strongSelf.present(OpenInActionSheetController(context: context, item: .location(location: location, withDirections: false), additionalAction: shareAction, openUrl: params.openUrl), in: .window(.root), with: nil)
|
strongSelf.present(OpenInActionSheetController(context: context, item: .location(location: location, withDirections: false), additionalAction: shareAction, openUrl: params.openUrl), in: .window(.root), with: nil)
|
||||||
}
|
}
|
||||||
}, setupProximityNotification: { [weak self] reset, coordinate, messageId in
|
}, setupProximityNotification: { [weak self] reset, messageId in
|
||||||
guard let strongSelf = self else {
|
guard let strongSelf = self else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if reset {
|
if reset {
|
||||||
if let messageId = messageId {
|
if let messageId = messageId {
|
||||||
let _ = requestEditLiveLocation(postbox: context.account.postbox, network: context.account.network, stateManager: context.account.stateManager, messageId: messageId, stop: false, coordinate: nil, heading: nil, proximityNotificationRadius: 0).start()
|
strongSelf.controllerNode.updateState { state in
|
||||||
|
var state = state
|
||||||
|
state.cancellingProximityRadius = true
|
||||||
|
return state
|
||||||
|
}
|
||||||
|
|
||||||
|
let _ = requestEditLiveLocation(postbox: context.account.postbox, network: context.account.network, stateManager: context.account.stateManager, messageId: messageId, stop: false, coordinate: nil, heading: nil, proximityNotificationRadius: 0).start(completed: { [weak self] in
|
||||||
|
guard let strongSelf = self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
strongSelf.controllerNode.updateState { state in
|
||||||
|
var state = state
|
||||||
|
state.cancellingProximityRadius = false
|
||||||
|
return state
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
DeviceAccess.authorizeAccess(to: .location(.live), locationManager: strongSelf.locationManager, presentationData: strongSelf.presentationData, present: { c, a in
|
||||||
|
strongSelf.present(c, in: .window(.root), with: a)
|
||||||
|
}, openSettings: {
|
||||||
|
context.sharedContext.applicationBindings.openSettings()
|
||||||
|
}) { [weak self] authorized in
|
||||||
|
guard let strongSelf = self, authorized else {
|
||||||
|
return
|
||||||
|
}
|
||||||
strongSelf.controllerNode.setProximityIndicator(radius: 0)
|
strongSelf.controllerNode.setProximityIndicator(radius: 0)
|
||||||
|
|
||||||
let controller = LocationDistancePickerScreen(context: context, style: .default, distances: strongSelf.controllerNode.headerNode.mapNode.distancesToAllAnnotations, updated: { [weak self] distance in
|
let controller = LocationDistancePickerScreen(context: context, style: .default, distances: strongSelf.controllerNode.headerNode.mapNode.distancesToAllAnnotations, updated: { [weak self] distance in
|
||||||
@ -196,11 +223,26 @@ public final class LocationViewController: ViewController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if let messageId = messageId {
|
if let messageId = messageId {
|
||||||
let _ = requestEditLiveLocation(postbox: context.account.postbox, network: context.account.network, stateManager: context.account.stateManager, messageId: messageId, stop: false, coordinate: nil, heading: nil, proximityNotificationRadius: distance).start()
|
strongSelf.controllerNode.updateState { state in
|
||||||
} else if let coordinate = coordinate {
|
var state = state
|
||||||
|
state.updatingProximityRadius = distance
|
||||||
|
return state
|
||||||
|
}
|
||||||
|
|
||||||
|
let _ = requestEditLiveLocation(postbox: context.account.postbox, network: context.account.network, stateManager: context.account.stateManager, messageId: messageId, stop: false, coordinate: nil, heading: nil, proximityNotificationRadius: distance).start(completed: { [weak self] in
|
||||||
|
guard let strongSelf = self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
strongSelf.controllerNode.updateState { state in
|
||||||
|
var state = state
|
||||||
|
state.updatingProximityRadius = nil
|
||||||
|
return state
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else {
|
||||||
strongSelf.present(textAlertController(context: strongSelf.context, title: strongSelf.presentationData.strings.Location_LiveLocationRequired_Title, text: strongSelf.presentationData.strings.Location_LiveLocationRequired_Description, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Location_LiveLocationRequired_ShareLocation, action: {
|
strongSelf.present(textAlertController(context: strongSelf.context, title: strongSelf.presentationData.strings.Location_LiveLocationRequired_Title, text: strongSelf.presentationData.strings.Location_LiveLocationRequired_Description, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Location_LiveLocationRequired_ShareLocation, action: {
|
||||||
completion()
|
completion()
|
||||||
strongSelf.interaction?.sendLiveLocation(coordinate, distance)
|
strongSelf.interaction?.sendLiveLocation(distance)
|
||||||
}), TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_Cancel, action: {})], actionLayout: .vertical), in: .window(.root))
|
}), TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_Cancel, action: {})], actionLayout: .vertical), in: .window(.root))
|
||||||
}
|
}
|
||||||
completion()
|
completion()
|
||||||
@ -211,12 +253,13 @@ public final class LocationViewController: ViewController {
|
|||||||
})
|
})
|
||||||
strongSelf.present(controller, in: .window(.root))
|
strongSelf.present(controller, in: .window(.root))
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}, updateSendActionHighlight: { [weak self] highlighted in
|
}, updateSendActionHighlight: { [weak self] highlighted in
|
||||||
guard let strongSelf = self else {
|
guard let strongSelf = self else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
strongSelf.controllerNode.updateSendActionHighlight(highlighted)
|
strongSelf.controllerNode.updateSendActionHighlight(highlighted)
|
||||||
}, sendLiveLocation: { [weak self] coordinate, distance in
|
}, sendLiveLocation: { [weak self] distance in
|
||||||
guard let strongSelf = self else {
|
guard let strongSelf = self else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -230,19 +273,26 @@ public final class LocationViewController: ViewController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if let distance = distance {
|
if let distance = distance {
|
||||||
|
let _ = (strongSelf.controllerNode.coordinate
|
||||||
|
|> deliverOnMainQueue).start(next: { coordinate in
|
||||||
params.sendLiveLocation(TelegramMediaMap(coordinate: coordinate, liveBroadcastingTimeout: 30 * 60, proximityNotificationRadius: distance))
|
params.sendLiveLocation(TelegramMediaMap(coordinate: coordinate, liveBroadcastingTimeout: 30 * 60, proximityNotificationRadius: distance))
|
||||||
|
})
|
||||||
} else {
|
} else {
|
||||||
let _ = (context.account.postbox.loadedPeerWithId(subject.id.peerId)
|
let _ = (context.account.postbox.loadedPeerWithId(subject.id.peerId)
|
||||||
|> deliverOnMainQueue).start(next: { [weak self] peer in
|
|> deliverOnMainQueue).start(next: { peer in
|
||||||
let controller = ActionSheetController(presentationData: strongSelf.presentationData)
|
let controller = ActionSheetController(presentationData: strongSelf.presentationData)
|
||||||
var title = strongSelf.presentationData.strings.Map_LiveLocationGroupDescription
|
var title = strongSelf.presentationData.strings.Map_LiveLocationGroupDescription
|
||||||
if let user = peer as? TelegramUser {
|
if let user = peer as? TelegramUser {
|
||||||
title = strongSelf.presentationData.strings.Map_LiveLocationPrivateDescription(user.compactDisplayTitle).0
|
title = strongSelf.presentationData.strings.Map_LiveLocationPrivateDescription(user.compactDisplayTitle).0
|
||||||
}
|
}
|
||||||
|
|
||||||
let sendLiveLocationImpl: (Int32) -> Void = { [weak self, weak controller] period in
|
let sendLiveLocationImpl: (Int32) -> Void = { [weak controller] period in
|
||||||
controller?.dismissAnimated()
|
controller?.dismissAnimated()
|
||||||
|
|
||||||
|
let _ = (strongSelf.controllerNode.coordinate
|
||||||
|
|> deliverOnMainQueue).start(next: { coordinate in
|
||||||
params.sendLiveLocation(TelegramMediaMap(coordinate: coordinate, liveBroadcastingTimeout: period))
|
params.sendLiveLocation(TelegramMediaMap(coordinate: coordinate, liveBroadcastingTimeout: period))
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
controller.setItemGroups([
|
controller.setItemGroups([
|
||||||
@ -313,6 +363,13 @@ public final class LocationViewController: ViewController {
|
|||||||
|
|
||||||
self.displayNode = LocationViewControllerNode(context: self.context, presentationData: self.presentationData, subject: self.subject, interaction: interaction, locationManager: self.locationManager)
|
self.displayNode = LocationViewControllerNode(context: self.context, presentationData: self.presentationData, subject: self.subject, interaction: interaction, locationManager: self.locationManager)
|
||||||
self.displayNodeDidLoad()
|
self.displayNodeDidLoad()
|
||||||
|
|
||||||
|
self.controllerNode.onAnnotationsReady = { [weak self] in
|
||||||
|
guard let strongSelf = self, strongSelf.showAll else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
strongSelf.controllerNode.showAll()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func updateRightBarButton() {
|
private func updateRightBarButton() {
|
||||||
|
@ -142,8 +142,8 @@ private enum LocationViewEntry: Comparable, Identifiable {
|
|||||||
return LocationActionListItem(presentationData: ItemListPresentationData(presentationData), account: context.account, title: title, subtitle: subtitle, icon: beginTimeAndTimeout != nil ? .stopLiveLocation : .liveLocation, beginTimeAndTimeout: beginTimeAndTimeout, action: {
|
return LocationActionListItem(presentationData: ItemListPresentationData(presentationData), account: context.account, title: title, subtitle: subtitle, icon: beginTimeAndTimeout != nil ? .stopLiveLocation : .liveLocation, beginTimeAndTimeout: beginTimeAndTimeout, action: {
|
||||||
if beginTimeAndTimeout != nil {
|
if beginTimeAndTimeout != nil {
|
||||||
interaction?.stopLiveLocation()
|
interaction?.stopLiveLocation()
|
||||||
} else if let coordinate = coordinate {
|
} else {
|
||||||
interaction?.sendLiveLocation(coordinate, nil)
|
interaction?.sendLiveLocation(nil)
|
||||||
}
|
}
|
||||||
}, highlighted: { highlight in
|
}, highlighted: { highlight in
|
||||||
interaction?.updateSendActionHighlight(highlight)
|
interaction?.updateSendActionHighlight(highlight)
|
||||||
@ -176,12 +176,16 @@ struct LocationViewState {
|
|||||||
var displayingMapModeOptions: Bool
|
var displayingMapModeOptions: Bool
|
||||||
var selectedLocation: LocationViewLocation
|
var selectedLocation: LocationViewLocation
|
||||||
var trackingMode: LocationTrackingMode
|
var trackingMode: LocationTrackingMode
|
||||||
|
var updatingProximityRadius: Int32?
|
||||||
|
var cancellingProximityRadius: Bool
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
self.mapMode = .map
|
self.mapMode = .map
|
||||||
self.displayingMapModeOptions = false
|
self.displayingMapModeOptions = false
|
||||||
self.selectedLocation = .initial
|
self.selectedLocation = .initial
|
||||||
self.trackingMode = .none
|
self.trackingMode = .none
|
||||||
|
self.updatingProximityRadius = nil
|
||||||
|
self.cancellingProximityRadius = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -208,6 +212,9 @@ final class LocationViewControllerNode: ViewControllerTracingNode, CLLocationMan
|
|||||||
|
|
||||||
private var displayedProximityAlertTooltip = false
|
private var displayedProximityAlertTooltip = false
|
||||||
|
|
||||||
|
var reportedAnnotationsReady = false
|
||||||
|
var onAnnotationsReady: (() -> Void)?
|
||||||
|
|
||||||
init(context: AccountContext, presentationData: PresentationData, subject: Message, interaction: LocationViewInteraction, locationManager: LocationManager) {
|
init(context: AccountContext, presentationData: PresentationData, subject: Message, interaction: LocationViewInteraction, locationManager: LocationManager) {
|
||||||
self.context = context
|
self.context = context
|
||||||
self.presentationData = presentationData
|
self.presentationData = presentationData
|
||||||
@ -274,9 +281,6 @@ final class LocationViewControllerNode: ViewControllerTracingNode, CLLocationMan
|
|||||||
let _ = (liveLocations
|
let _ = (liveLocations
|
||||||
|> take(1)
|
|> take(1)
|
||||||
|> deliverOnMainQueue).start(next: { [weak self] messages in
|
|> deliverOnMainQueue).start(next: { [weak self] messages in
|
||||||
guard let strongSelf = self else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
var ownMessageId: MessageId?
|
var ownMessageId: MessageId?
|
||||||
for message in messages {
|
for message in messages {
|
||||||
if message.localTags.contains(.OutgoingLiveLocation) {
|
if message.localTags.contains(.OutgoingLiveLocation) {
|
||||||
@ -284,7 +288,7 @@ final class LocationViewControllerNode: ViewControllerTracingNode, CLLocationMan
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
interaction.setupProximityNotification(reset, strongSelf.headerNode.mapNode.currentUserLocation?.coordinate, ownMessageId)
|
interaction.setupProximityNotification(reset, ownMessageId)
|
||||||
|
|
||||||
let _ = ApplicationSpecificNotice.incrementLocationProximityAlertTip(accountManager: context.sharedContext.accountManager, count: 4).start()
|
let _ = ApplicationSpecificNotice.incrementLocationProximityAlertTip(accountManager: context.sharedContext.accountManager, count: 4).start()
|
||||||
})
|
})
|
||||||
@ -387,12 +391,13 @@ final class LocationViewControllerNode: ViewControllerTracingNode, CLLocationMan
|
|||||||
}
|
}
|
||||||
|
|
||||||
for message in effectiveLiveLocations {
|
for message in effectiveLiveLocations {
|
||||||
|
if let location = getLocation(from: message) {
|
||||||
if let channel = message.peers[message.id.peerId] as? TelegramChannel, case .broadcast = channel.info, message.threadId != nil {
|
if let channel = message.peers[message.id.peerId] as? TelegramChannel, case .broadcast = channel.info, message.threadId != nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
var liveBroadcastingTimeout: Int32 = 0
|
var liveBroadcastingTimeout: Int32 = 0
|
||||||
if let location = getLocation(from: message), let timeout = location.liveBroadcastingTimeout {
|
if let timeout = location.liveBroadcastingTimeout {
|
||||||
liveBroadcastingTimeout = timeout
|
liveBroadcastingTimeout = timeout
|
||||||
}
|
}
|
||||||
let remainingTime = max(0, message.timestamp + liveBroadcastingTimeout - currentTime)
|
let remainingTime = max(0, message.timestamp + liveBroadcastingTimeout - currentTime)
|
||||||
@ -411,6 +416,14 @@ final class LocationViewControllerNode: ViewControllerTracingNode, CLLocationMan
|
|||||||
}
|
}
|
||||||
index += 1
|
index += 1
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let currentProximityNotification = proximityNotification, currentProximityNotification && state.cancellingProximityRadius {
|
||||||
|
proximityNotification = false
|
||||||
|
} else if let radius = state.updatingProximityRadius {
|
||||||
|
proximityNotification = true
|
||||||
|
proximityNotificationRadius = radius
|
||||||
|
}
|
||||||
|
|
||||||
if subject.id.peerId.namespace != Namespaces.Peer.CloudUser, proximityNotification == nil {
|
if subject.id.peerId.namespace != Namespaces.Peer.CloudUser, proximityNotification == nil {
|
||||||
proximityNotification = false
|
proximityNotification = false
|
||||||
@ -465,6 +478,11 @@ final class LocationViewControllerNode: ViewControllerTracingNode, CLLocationMan
|
|||||||
}
|
}
|
||||||
if annotations != previousAnnotations {
|
if annotations != previousAnnotations {
|
||||||
strongSelf.headerNode.mapNode.annotations = annotations
|
strongSelf.headerNode.mapNode.annotations = annotations
|
||||||
|
|
||||||
|
if !strongSelf.reportedAnnotationsReady {
|
||||||
|
strongSelf.reportedAnnotationsReady = true
|
||||||
|
strongSelf.onAnnotationsReady?()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let _ = proximityNotification {
|
if let _ = proximityNotification {
|
||||||
@ -583,14 +601,32 @@ final class LocationViewControllerNode: ViewControllerTracingNode, CLLocationMan
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var initialized = false
|
||||||
private func dequeueTransition() {
|
private func dequeueTransition() {
|
||||||
guard let _ = self.validLayout, let transition = self.enqueuedTransitions.first else {
|
guard let _ = self.validLayout, let transition = self.enqueuedTransitions.first else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
self.enqueuedTransitions.remove(at: 0)
|
self.enqueuedTransitions.remove(at: 0)
|
||||||
|
|
||||||
|
let scrollToItem: ListViewScrollToItem?
|
||||||
|
if !self.initialized, transition.insertions.count > 0 {
|
||||||
|
var index: Int = 0
|
||||||
|
var offset: CGFloat = 0.0
|
||||||
|
if transition.insertions.count > 2 {
|
||||||
|
index = 2
|
||||||
|
offset = 40.0
|
||||||
|
} else if transition.insertions.count == 2 {
|
||||||
|
index = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
scrollToItem = ListViewScrollToItem(index: index, position: .bottom(offset), animated: false, curve: .Default(duration: nil), directionHint: .Up)
|
||||||
|
self.initialized = true
|
||||||
|
} else {
|
||||||
|
scrollToItem = nil
|
||||||
|
}
|
||||||
|
|
||||||
let options = ListViewDeleteAndInsertOptions()
|
let options = ListViewDeleteAndInsertOptions()
|
||||||
self.listNode.transaction(deleteIndices: transition.deletions, insertIndicesAndItems: transition.insertions, updateIndicesAndItems: transition.updates, options: options, updateSizeAndInsets: nil, updateOpaqueState: nil, completion: { _ in
|
self.listNode.transaction(deleteIndices: transition.deletions, insertIndicesAndItems: transition.insertions, updateIndicesAndItems: transition.updates, options: options, scrollToItem: scrollToItem, updateSizeAndInsets: nil, updateOpaqueState: nil, completion: { _ in
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -599,17 +635,33 @@ final class LocationViewControllerNode: ViewControllerTracingNode, CLLocationMan
|
|||||||
}
|
}
|
||||||
|
|
||||||
func setProximityIndicator(radius: Int32?) {
|
func setProximityIndicator(radius: Int32?) {
|
||||||
|
guard let (layout, navigationBarHeight) = self.validLayout else {
|
||||||
|
return
|
||||||
|
}
|
||||||
if let radius = radius {
|
if let radius = radius {
|
||||||
self.headerNode.forceIsHidden = true
|
self.headerNode.forceIsHidden = true
|
||||||
|
|
||||||
if var coordinate = self.headerNode.mapNode.currentUserLocation?.coordinate, let span = self.headerNode.mapNode.mapSpan {
|
if let coordinate = self.headerNode.mapNode.currentUserLocation?.coordinate {
|
||||||
coordinate.latitude -= span.latitudeDelta * 0.11
|
|
||||||
self.updateState { state in
|
self.updateState { state in
|
||||||
var state = state
|
var state = state
|
||||||
state.selectedLocation = .coordinate(coordinate, true)
|
state.selectedLocation = .custom
|
||||||
state.trackingMode = .none
|
state.trackingMode = .none
|
||||||
return state
|
return state
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var contentOffset: CGFloat = 0.0
|
||||||
|
if case let .known(offset) = self.listNode.visibleContentOffset() {
|
||||||
|
contentOffset = offset
|
||||||
|
}
|
||||||
|
|
||||||
|
let panelHeight: CGFloat = 349.0 + layout.intrinsicInsets.bottom
|
||||||
|
let inset = (layout.size.width - 260.0) / 2.0
|
||||||
|
let offset = panelHeight / 2.0 + 60.0 + inset + navigationBarHeight / 2.0
|
||||||
|
|
||||||
|
let point = CGPoint(x: layout.size.width / 2.0, y: navigationBarHeight + (layout.size.height - navigationBarHeight - panelHeight) / 2.0)
|
||||||
|
let convertedPoint = self.view.convert(point, to: self.headerNode.mapNode.view)
|
||||||
|
|
||||||
|
self.headerNode.mapNode.setMapCenter(coordinate: coordinate, radius: Double(radius), insets: UIEdgeInsets(top: navigationBarHeight, left: inset, bottom: offset - contentOffset, right: inset), offset: convertedPoint.y - self.headerNode.mapNode.frame.height / 2.0, animated: true)
|
||||||
}
|
}
|
||||||
|
|
||||||
self.headerNode.mapNode.proximityIndicatorRadius = Double(radius)
|
self.headerNode.mapNode.proximityIndicatorRadius = Double(radius)
|
||||||
@ -704,4 +756,15 @@ final class LocationViewControllerNode: ViewControllerTracingNode, CLLocationMan
|
|||||||
self.optionsNode.updateLayout(size: optionsFrame.size, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, transition: transition)
|
self.optionsNode.updateLayout(size: optionsFrame.size, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, transition: transition)
|
||||||
self.optionsNode.isUserInteractionEnabled = self.state.displayingMapModeOptions
|
self.optionsNode.isUserInteractionEnabled = self.state.displayingMapModeOptions
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var coordinate: Signal<CLLocationCoordinate2D, NoError> {
|
||||||
|
return self.headerNode.mapNode.userLocation
|
||||||
|
|> filter { location in
|
||||||
|
return location != nil
|
||||||
|
}
|
||||||
|
|> take(1)
|
||||||
|
|> map { location -> CLLocationCoordinate2D in
|
||||||
|
return location!.coordinate
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -111,6 +111,7 @@ private final class MediaPlayerContext {
|
|||||||
private var baseRate: Double
|
private var baseRate: Double
|
||||||
private let fetchAutomatically: Bool
|
private let fetchAutomatically: Bool
|
||||||
private var playAndRecord: Bool
|
private var playAndRecord: Bool
|
||||||
|
private var ambient: Bool
|
||||||
private var keepAudioSessionWhilePaused: Bool
|
private var keepAudioSessionWhilePaused: Bool
|
||||||
private var continuePlayingWithoutSoundOnLostAudioSession: Bool
|
private var continuePlayingWithoutSoundOnLostAudioSession: Bool
|
||||||
|
|
||||||
@ -132,7 +133,7 @@ private final class MediaPlayerContext {
|
|||||||
|
|
||||||
private var stoppedAtEnd = false
|
private var stoppedAtEnd = false
|
||||||
|
|
||||||
init(queue: Queue, audioSessionManager: ManagedAudioSession, playerStatus: Promise<MediaPlayerStatus>, audioLevelPipe: ValuePipe<Float>, postbox: Postbox, resourceReference: MediaResourceReference, tempFilePath: String?, streamable: MediaPlayerStreaming, video: Bool, preferSoftwareDecoding: Bool, playAutomatically: Bool, enableSound: Bool, baseRate: Double, fetchAutomatically: Bool, playAndRecord: Bool, keepAudioSessionWhilePaused: Bool, continuePlayingWithoutSoundOnLostAudioSession: Bool) {
|
init(queue: Queue, audioSessionManager: ManagedAudioSession, playerStatus: Promise<MediaPlayerStatus>, audioLevelPipe: ValuePipe<Float>, postbox: Postbox, resourceReference: MediaResourceReference, tempFilePath: String?, streamable: MediaPlayerStreaming, video: Bool, preferSoftwareDecoding: Bool, playAutomatically: Bool, enableSound: Bool, baseRate: Double, fetchAutomatically: Bool, playAndRecord: Bool, ambient: Bool, keepAudioSessionWhilePaused: Bool, continuePlayingWithoutSoundOnLostAudioSession: Bool) {
|
||||||
assert(queue.isCurrent())
|
assert(queue.isCurrent())
|
||||||
|
|
||||||
self.queue = queue
|
self.queue = queue
|
||||||
@ -149,6 +150,7 @@ private final class MediaPlayerContext {
|
|||||||
self.baseRate = baseRate
|
self.baseRate = baseRate
|
||||||
self.fetchAutomatically = fetchAutomatically
|
self.fetchAutomatically = fetchAutomatically
|
||||||
self.playAndRecord = playAndRecord
|
self.playAndRecord = playAndRecord
|
||||||
|
self.ambient = ambient
|
||||||
self.keepAudioSessionWhilePaused = keepAudioSessionWhilePaused
|
self.keepAudioSessionWhilePaused = keepAudioSessionWhilePaused
|
||||||
self.continuePlayingWithoutSoundOnLostAudioSession = continuePlayingWithoutSoundOnLostAudioSession
|
self.continuePlayingWithoutSoundOnLostAudioSession = continuePlayingWithoutSoundOnLostAudioSession
|
||||||
|
|
||||||
@ -292,12 +294,12 @@ private final class MediaPlayerContext {
|
|||||||
duration = max(duration, CMTimeGetSeconds(audioTrackFrameBuffer.duration))
|
duration = max(duration, CMTimeGetSeconds(audioTrackFrameBuffer.duration))
|
||||||
}
|
}
|
||||||
loadedDuration = duration
|
loadedDuration = duration
|
||||||
let status = MediaPlayerStatus(generationTimestamp: CACurrentMediaTime(), duration: duration, dimensions: CGSize(), timestamp: min(max(timestamp, 0.0), duration), baseRate: self.baseRate, seekId: self.seekId, status: .buffering(initial: false, whilePlaying: action == .play, progress: 0.0), soundEnabled: self.enableSound)
|
let status = MediaPlayerStatus(generationTimestamp: CACurrentMediaTime(), duration: duration, dimensions: CGSize(), timestamp: min(max(timestamp, 0.0), duration), baseRate: self.baseRate, seekId: self.seekId, status: .buffering(initial: false, whilePlaying: action == .play, progress: 0.0, display: true), soundEnabled: self.enableSound)
|
||||||
self.playerStatus.set(.single(status))
|
self.playerStatus.set(.single(status))
|
||||||
let _ = self.playerStatusValue.swap(status)
|
let _ = self.playerStatusValue.swap(status)
|
||||||
} else {
|
} else {
|
||||||
let duration = seekState?.duration ?? 0.0
|
let duration = seekState?.duration ?? 0.0
|
||||||
let status = MediaPlayerStatus(generationTimestamp: CACurrentMediaTime(), duration: duration, dimensions: CGSize(), timestamp: min(max(timestamp, 0.0), duration), baseRate: self.baseRate, seekId: self.seekId, status: .buffering(initial: false, whilePlaying: action == .play, progress: 0.0), soundEnabled: self.enableSound)
|
let status = MediaPlayerStatus(generationTimestamp: CACurrentMediaTime(), duration: duration, dimensions: CGSize(), timestamp: min(max(timestamp, 0.0), duration), baseRate: self.baseRate, seekId: self.seekId, status: .buffering(initial: false, whilePlaying: action == .play, progress: 0.0, display: true), soundEnabled: self.enableSound)
|
||||||
self.playerStatus.set(.single(status))
|
self.playerStatus.set(.single(status))
|
||||||
let _ = self.playerStatusValue.swap(status)
|
let _ = self.playerStatusValue.swap(status)
|
||||||
}
|
}
|
||||||
@ -368,7 +370,7 @@ private final class MediaPlayerContext {
|
|||||||
self.audioRenderer = nil
|
self.audioRenderer = nil
|
||||||
|
|
||||||
let queue = self.queue
|
let queue = self.queue
|
||||||
renderer = MediaPlayerAudioRenderer(audioSession: .manager(self.audioSessionManager), playAndRecord: self.playAndRecord, forceAudioToSpeaker: self.forceAudioToSpeaker, baseRate: self.baseRate, audioLevelPipe: self.audioLevelPipe, updatedRate: { [weak self] in
|
renderer = MediaPlayerAudioRenderer(audioSession: .manager(self.audioSessionManager), playAndRecord: self.playAndRecord, ambient: self.ambient, forceAudioToSpeaker: self.forceAudioToSpeaker, baseRate: self.baseRate, audioLevelPipe: self.audioLevelPipe, updatedRate: { [weak self] in
|
||||||
queue.async {
|
queue.async {
|
||||||
if let strongSelf = self {
|
if let strongSelf = self {
|
||||||
strongSelf.tick()
|
strongSelf.tick()
|
||||||
@ -446,7 +448,7 @@ private final class MediaPlayerContext {
|
|||||||
self.lastStatusUpdateTimestamp = nil
|
self.lastStatusUpdateTimestamp = nil
|
||||||
if self.enableSound {
|
if self.enableSound {
|
||||||
let queue = self.queue
|
let queue = self.queue
|
||||||
let renderer = MediaPlayerAudioRenderer(audioSession: .manager(self.audioSessionManager), playAndRecord: self.playAndRecord, forceAudioToSpeaker: self.forceAudioToSpeaker, baseRate: self.baseRate, audioLevelPipe: self.audioLevelPipe, updatedRate: { [weak self] in
|
let renderer = MediaPlayerAudioRenderer(audioSession: .manager(self.audioSessionManager), playAndRecord: self.playAndRecord, ambient: self.ambient, forceAudioToSpeaker: self.forceAudioToSpeaker, baseRate: self.baseRate, audioLevelPipe: self.audioLevelPipe, updatedRate: { [weak self] in
|
||||||
queue.async {
|
queue.async {
|
||||||
if let strongSelf = self {
|
if let strongSelf = self {
|
||||||
strongSelf.tick()
|
strongSelf.tick()
|
||||||
@ -873,7 +875,7 @@ private final class MediaPlayerContext {
|
|||||||
if case .playing = self.state {
|
if case .playing = self.state {
|
||||||
whilePlaying = true
|
whilePlaying = true
|
||||||
}
|
}
|
||||||
playbackStatus = .buffering(initial: false, whilePlaying: whilePlaying, progress: Float(bufferingProgress))
|
playbackStatus = .buffering(initial: false, whilePlaying: whilePlaying, progress: Float(bufferingProgress), display: true)
|
||||||
} else if !rate.isZero {
|
} else if !rate.isZero {
|
||||||
if reportRate.isZero {
|
if reportRate.isZero {
|
||||||
//playbackStatus = .buffering(initial: false, whilePlaying: true)
|
//playbackStatus = .buffering(initial: false, whilePlaying: true)
|
||||||
@ -924,7 +926,7 @@ private final class MediaPlayerContext {
|
|||||||
public enum MediaPlayerPlaybackStatus: Equatable {
|
public enum MediaPlayerPlaybackStatus: Equatable {
|
||||||
case playing
|
case playing
|
||||||
case paused
|
case paused
|
||||||
case buffering(initial: Bool, whilePlaying: Bool, progress: Float)
|
case buffering(initial: Bool, whilePlaying: Bool, progress: Float, display: Bool)
|
||||||
|
|
||||||
public static func ==(lhs: MediaPlayerPlaybackStatus, rhs: MediaPlayerPlaybackStatus) -> Bool {
|
public static func ==(lhs: MediaPlayerPlaybackStatus, rhs: MediaPlayerPlaybackStatus) -> Bool {
|
||||||
switch lhs {
|
switch lhs {
|
||||||
@ -940,8 +942,8 @@ public enum MediaPlayerPlaybackStatus: Equatable {
|
|||||||
} else {
|
} else {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
case let .buffering(initial, whilePlaying, progress):
|
case let .buffering(initial, whilePlaying, progress, display):
|
||||||
if case .buffering(initial, whilePlaying, progress) = rhs {
|
if case .buffering(initial, whilePlaying, progress, display) = rhs {
|
||||||
return true
|
return true
|
||||||
} else {
|
} else {
|
||||||
return false
|
return false
|
||||||
@ -998,10 +1000,10 @@ public final class MediaPlayer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public init(audioSessionManager: ManagedAudioSession, postbox: Postbox, resourceReference: MediaResourceReference, tempFilePath: String? = nil, streamable: MediaPlayerStreaming, video: Bool, preferSoftwareDecoding: Bool, playAutomatically: Bool = false, enableSound: Bool, baseRate: Double = 1.0, fetchAutomatically: Bool, playAndRecord: Bool = false, keepAudioSessionWhilePaused: Bool = false, continuePlayingWithoutSoundOnLostAudioSession: Bool = false) {
|
public init(audioSessionManager: ManagedAudioSession, postbox: Postbox, resourceReference: MediaResourceReference, tempFilePath: String? = nil, streamable: MediaPlayerStreaming, video: Bool, preferSoftwareDecoding: Bool, playAutomatically: Bool = false, enableSound: Bool, baseRate: Double = 1.0, fetchAutomatically: Bool, playAndRecord: Bool = false, ambient: Bool = false, keepAudioSessionWhilePaused: Bool = false, continuePlayingWithoutSoundOnLostAudioSession: Bool = false) {
|
||||||
let audioLevelPipe = self.audioLevelPipe
|
let audioLevelPipe = self.audioLevelPipe
|
||||||
self.queue.async {
|
self.queue.async {
|
||||||
let context = MediaPlayerContext(queue: self.queue, audioSessionManager: audioSessionManager, playerStatus: self.statusValue, audioLevelPipe: audioLevelPipe, postbox: postbox, resourceReference: resourceReference, tempFilePath: tempFilePath, streamable: streamable, video: video, preferSoftwareDecoding: preferSoftwareDecoding, playAutomatically: playAutomatically, enableSound: enableSound, baseRate: baseRate, fetchAutomatically: fetchAutomatically, playAndRecord: playAndRecord, keepAudioSessionWhilePaused: keepAudioSessionWhilePaused, continuePlayingWithoutSoundOnLostAudioSession: continuePlayingWithoutSoundOnLostAudioSession)
|
let context = MediaPlayerContext(queue: self.queue, audioSessionManager: audioSessionManager, playerStatus: self.statusValue, audioLevelPipe: audioLevelPipe, postbox: postbox, resourceReference: resourceReference, tempFilePath: tempFilePath, streamable: streamable, video: video, preferSoftwareDecoding: preferSoftwareDecoding, playAutomatically: playAutomatically, enableSound: enableSound, baseRate: baseRate, fetchAutomatically: fetchAutomatically, playAndRecord: playAndRecord, ambient: ambient, keepAudioSessionWhilePaused: keepAudioSessionWhilePaused, continuePlayingWithoutSoundOnLostAudioSession: continuePlayingWithoutSoundOnLostAudioSession)
|
||||||
self.contextRef = Unmanaged.passRetained(context)
|
self.contextRef = Unmanaged.passRetained(context)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -239,6 +239,7 @@ private final class AudioPlayerRendererContext {
|
|||||||
let audioSessionDisposable = MetaDisposable()
|
let audioSessionDisposable = MetaDisposable()
|
||||||
var audioSessionControl: ManagedAudioSessionControl?
|
var audioSessionControl: ManagedAudioSessionControl?
|
||||||
let playAndRecord: Bool
|
let playAndRecord: Bool
|
||||||
|
let ambient: Bool
|
||||||
var forceAudioToSpeaker: Bool {
|
var forceAudioToSpeaker: Bool {
|
||||||
didSet {
|
didSet {
|
||||||
if self.forceAudioToSpeaker != oldValue {
|
if self.forceAudioToSpeaker != oldValue {
|
||||||
@ -249,7 +250,7 @@ private final class AudioPlayerRendererContext {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
init(controlTimebase: CMTimebase, audioSession: MediaPlayerAudioSessionControl, playAndRecord: Bool, forceAudioToSpeaker: Bool, baseRate: Double, audioLevelPipe: ValuePipe<Float>, updatedRate: @escaping () -> Void, audioPaused: @escaping () -> Void) {
|
init(controlTimebase: CMTimebase, audioSession: MediaPlayerAudioSessionControl, playAndRecord: Bool, ambient: Bool, forceAudioToSpeaker: Bool, baseRate: Double, audioLevelPipe: ValuePipe<Float>, updatedRate: @escaping () -> Void, audioPaused: @escaping () -> Void) {
|
||||||
assert(audioPlayerRendererQueue.isCurrent())
|
assert(audioPlayerRendererQueue.isCurrent())
|
||||||
|
|
||||||
self.audioSession = audioSession
|
self.audioSession = audioSession
|
||||||
@ -262,6 +263,7 @@ private final class AudioPlayerRendererContext {
|
|||||||
self.audioPaused = audioPaused
|
self.audioPaused = audioPaused
|
||||||
|
|
||||||
self.playAndRecord = playAndRecord
|
self.playAndRecord = playAndRecord
|
||||||
|
self.ambient = ambient
|
||||||
|
|
||||||
self.audioStreamDescription = audioRendererNativeStreamDescription()
|
self.audioStreamDescription = audioRendererNativeStreamDescription()
|
||||||
|
|
||||||
@ -481,7 +483,7 @@ private final class AudioPlayerRendererContext {
|
|||||||
|
|
||||||
switch self.audioSession {
|
switch self.audioSession {
|
||||||
case let .manager(manager):
|
case let .manager(manager):
|
||||||
self.audioSessionDisposable.set(manager.push(audioSessionType: self.playAndRecord ? .playWithPossiblePortOverride : .play, outputMode: self.forceAudioToSpeaker ? .speakerIfNoHeadphones : .system, once: true, manualActivate: { [weak self] control in
|
self.audioSessionDisposable.set(manager.push(audioSessionType: self.ambient ? .ambient : (self.playAndRecord ? .playWithPossiblePortOverride : .play), outputMode: self.forceAudioToSpeaker ? .speakerIfNoHeadphones : .system, once: true, manualActivate: { [weak self] control in
|
||||||
audioPlayerRendererQueue.async {
|
audioPlayerRendererQueue.async {
|
||||||
if let strongSelf = self {
|
if let strongSelf = self {
|
||||||
strongSelf.audioSessionControl = control
|
strongSelf.audioSessionControl = control
|
||||||
@ -751,7 +753,7 @@ public final class MediaPlayerAudioRenderer {
|
|||||||
private let audioClock: CMClock
|
private let audioClock: CMClock
|
||||||
public let audioTimebase: CMTimebase
|
public let audioTimebase: CMTimebase
|
||||||
|
|
||||||
public init(audioSession: MediaPlayerAudioSessionControl, playAndRecord: Bool, forceAudioToSpeaker: Bool, baseRate: Double, audioLevelPipe: ValuePipe<Float>, updatedRate: @escaping () -> Void, audioPaused: @escaping () -> Void) {
|
public init(audioSession: MediaPlayerAudioSessionControl, playAndRecord: Bool, ambient: Bool, forceAudioToSpeaker: Bool, baseRate: Double, audioLevelPipe: ValuePipe<Float>, updatedRate: @escaping () -> Void, audioPaused: @escaping () -> Void) {
|
||||||
var audioClock: CMClock?
|
var audioClock: CMClock?
|
||||||
CMAudioClockCreate(allocator: nil, clockOut: &audioClock)
|
CMAudioClockCreate(allocator: nil, clockOut: &audioClock)
|
||||||
if audioClock == nil {
|
if audioClock == nil {
|
||||||
@ -764,7 +766,7 @@ public final class MediaPlayerAudioRenderer {
|
|||||||
self.audioTimebase = audioTimebase!
|
self.audioTimebase = audioTimebase!
|
||||||
|
|
||||||
audioPlayerRendererQueue.async {
|
audioPlayerRendererQueue.async {
|
||||||
let context = AudioPlayerRendererContext(controlTimebase: audioTimebase!, audioSession: audioSession, playAndRecord: playAndRecord, forceAudioToSpeaker: forceAudioToSpeaker, baseRate: baseRate, audioLevelPipe: audioLevelPipe, updatedRate: updatedRate, audioPaused: audioPaused)
|
let context = AudioPlayerRendererContext(controlTimebase: audioTimebase!, audioSession: audioSession, playAndRecord: playAndRecord, ambient: ambient, forceAudioToSpeaker: forceAudioToSpeaker, baseRate: baseRate, audioLevelPipe: audioLevelPipe, updatedRate: updatedRate, audioPaused: audioPaused)
|
||||||
self.contextRef = Unmanaged.passRetained(context)
|
self.contextRef = Unmanaged.passRetained(context)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -781,7 +781,7 @@ public final class MediaPlayerScrubbingNode: ASDisplayNode {
|
|||||||
default:
|
default:
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
if case .buffering(true, _, _) = statusValue.status {
|
if case .buffering(true, _, _, _) = statusValue.status {
|
||||||
//initialBuffering = true
|
//initialBuffering = true
|
||||||
} else if Double(0.0).isLess(than: statusValue.duration) {
|
} else if Double(0.0).isLess(than: statusValue.duration) {
|
||||||
if let scrubbingTimestampValue = self.scrubbingTimestampValue {
|
if let scrubbingTimestampValue = self.scrubbingTimestampValue {
|
||||||
|
@ -334,7 +334,7 @@ private enum ChannelInfoEntry: ItemListNodeEntry {
|
|||||||
arguments.tapAvatarAction()
|
arguments.tapAvatarAction()
|
||||||
}, context: arguments.avatarAndNameInfoContext, updatingImage: updatingAvatar)
|
}, context: arguments.avatarAndNameInfoContext, updatingImage: updatingAvatar)
|
||||||
case let .about(theme, text, value):
|
case let .about(theme, text, value):
|
||||||
return ItemListTextWithLabelItem(presentationData: presentationData, label: text, text: foldMultipleLineBreaks(value), enabledEntityTypes: [.url, .mention, .hashtag], multiline: true, sectionId: self.section, action: nil, longTapAction: {
|
return ItemListTextWithLabelItem(presentationData: presentationData, label: text, text: foldMultipleLineBreaks(value), enabledEntityTypes: [.allUrl, .mention, .hashtag], multiline: true, sectionId: self.section, action: nil, longTapAction: {
|
||||||
arguments.displayContextMenu(ChannelInfoEntryTag.about, value)
|
arguments.displayContextMenu(ChannelInfoEntryTag.about, value)
|
||||||
}, linkItemAction: { action, itemLink in
|
}, linkItemAction: { action, itemLink in
|
||||||
arguments.aboutLinkAction(action, itemLink)
|
arguments.aboutLinkAction(action, itemLink)
|
||||||
|
@ -507,7 +507,7 @@ private enum GroupInfoEntry: ItemListNodeEntry {
|
|||||||
arguments.changeProfilePhoto()
|
arguments.changeProfilePhoto()
|
||||||
})
|
})
|
||||||
case let .about(theme, text):
|
case let .about(theme, text):
|
||||||
return ItemListMultilineTextItem(presentationData: presentationData, text: foldMultipleLineBreaks(text), enabledEntityTypes: [.url, .mention, .hashtag], sectionId: self.section, style: .blocks, longTapAction: {
|
return ItemListMultilineTextItem(presentationData: presentationData, text: foldMultipleLineBreaks(text), enabledEntityTypes: [.allUrl, .mention, .hashtag], sectionId: self.section, style: .blocks, longTapAction: {
|
||||||
arguments.displayAboutContextMenu(text)
|
arguments.displayAboutContextMenu(text)
|
||||||
}, linkItemAction: { action, itemLink in
|
}, linkItemAction: { action, itemLink in
|
||||||
arguments.aboutLinkAction(action, itemLink)
|
arguments.aboutLinkAction(action, itemLink)
|
||||||
|
@ -410,7 +410,7 @@ private enum UserInfoEntry: ItemListNodeEntry {
|
|||||||
case let .about(theme, peer, text, value):
|
case let .about(theme, peer, text, value):
|
||||||
var enabledEntityTypes: EnabledEntityTypes = []
|
var enabledEntityTypes: EnabledEntityTypes = []
|
||||||
if let peer = peer as? TelegramUser, let _ = peer.botInfo {
|
if let peer = peer as? TelegramUser, let _ = peer.botInfo {
|
||||||
enabledEntityTypes = [.url, .mention, .hashtag]
|
enabledEntityTypes = [.allUrl, .mention, .hashtag]
|
||||||
}
|
}
|
||||||
return ItemListTextWithLabelItem(presentationData: presentationData, label: text, text: foldMultipleLineBreaks(value), enabledEntityTypes: enabledEntityTypes, multiline: true, sectionId: self.section, action: nil, longTapAction: {
|
return ItemListTextWithLabelItem(presentationData: presentationData, label: text, text: foldMultipleLineBreaks(value), enabledEntityTypes: enabledEntityTypes, multiline: true, sectionId: self.section, action: nil, longTapAction: {
|
||||||
arguments.displayAboutContextMenu(value)
|
arguments.displayAboutContextMenu(value)
|
||||||
|
@ -37,8 +37,8 @@ private let progressiveRangeMap: [(Int, [Int])] = [
|
|||||||
(Int(Int32.max), [2, 3, 4])
|
(Int(Int32.max), [2, 3, 4])
|
||||||
]
|
]
|
||||||
|
|
||||||
public func representationFetchRangeForDisplayAtSize(representation: TelegramMediaImageRepresentation, dimension: Int) -> Range<Int>? {
|
public func representationFetchRangeForDisplayAtSize(representation: TelegramMediaImageRepresentation, dimension: Int?) -> Range<Int>? {
|
||||||
if representation.progressiveSizes.count > 1 {
|
if representation.progressiveSizes.count > 1, let dimension = dimension {
|
||||||
var largestByteSize = Int(representation.progressiveSizes[0])
|
var largestByteSize = Int(representation.progressiveSizes[0])
|
||||||
for (maxDimension, byteSizes) in progressiveRangeMap {
|
for (maxDimension, byteSizes) in progressiveRangeMap {
|
||||||
largestByteSize = Int(representation.progressiveSizes[byteSizes.last!])
|
largestByteSize = Int(representation.progressiveSizes[byteSizes.last!])
|
||||||
@ -1684,10 +1684,10 @@ public func standaloneChatMessagePhotoInteractiveFetched(account: Account, photo
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public func chatMessagePhotoInteractiveFetched(context: AccountContext, photoReference: ImageMediaReference, displayAtSize: Int = 1000, storeToDownloadsPeerType: MediaAutoDownloadPeerType?) -> Signal<FetchResourceSourceType, FetchResourceError> {
|
public func chatMessagePhotoInteractiveFetched(context: AccountContext, photoReference: ImageMediaReference, displayAtSize: Int?, storeToDownloadsPeerType: MediaAutoDownloadPeerType?) -> Signal<FetchResourceSourceType, FetchResourceError> {
|
||||||
if let largestRepresentation = largestRepresentationForPhoto(photoReference.media) {
|
if let largestRepresentation = largestRepresentationForPhoto(photoReference.media) {
|
||||||
var fetchRange: (Range<Int>, MediaBoxFetchPriority)?
|
var fetchRange: (Range<Int>, MediaBoxFetchPriority)?
|
||||||
if let range = representationFetchRangeForDisplayAtSize(representation: largestRepresentation, dimension: displayAtSize) {
|
if let displayAtSize = displayAtSize, let range = representationFetchRangeForDisplayAtSize(representation: largestRepresentation, dimension: displayAtSize) {
|
||||||
fetchRange = (range, .default)
|
fetchRange = (range, .default)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -146,9 +146,9 @@ public enum ManagedSlotMachineAnimationState: Equatable {
|
|||||||
|
|
||||||
public final class SlotMachineAnimationNode: ASDisplayNode {
|
public final class SlotMachineAnimationNode: ASDisplayNode {
|
||||||
private let backNode: ManagedAnimationNode
|
private let backNode: ManagedAnimationNode
|
||||||
private let leftReelNode: ManagedAnimationNode
|
private let leftReelNode: DiceAnimatedStickerNode
|
||||||
private let centerReelNode: ManagedAnimationNode
|
private let centerReelNode: DiceAnimatedStickerNode
|
||||||
private let rightReelNode: ManagedAnimationNode
|
private let rightReelNode: DiceAnimatedStickerNode
|
||||||
private let frontNode: ManagedAnimationNode
|
private let frontNode: ManagedAnimationNode
|
||||||
|
|
||||||
private var diceState: ManagedSlotMachineAnimationState? = nil
|
private var diceState: ManagedSlotMachineAnimationState? = nil
|
||||||
@ -161,9 +161,10 @@ public final class SlotMachineAnimationNode: ASDisplayNode {
|
|||||||
public init(size: CGSize = CGSize(width: 184.0, height: 184.0)) {
|
public init(size: CGSize = CGSize(width: 184.0, height: 184.0)) {
|
||||||
self.animationSize = size
|
self.animationSize = size
|
||||||
self.backNode = ManagedAnimationNode(size: self.animationSize)
|
self.backNode = ManagedAnimationNode(size: self.animationSize)
|
||||||
self.leftReelNode = ManagedAnimationNode(size: self.animationSize)
|
let reelSize = CGSize(width: 384.0, height: 384.0)
|
||||||
self.centerReelNode = ManagedAnimationNode(size: self.animationSize)
|
self.leftReelNode = DiceAnimatedStickerNode(size: reelSize)
|
||||||
self.rightReelNode = ManagedAnimationNode(size: self.animationSize)
|
self.centerReelNode = DiceAnimatedStickerNode(size: reelSize)
|
||||||
|
self.rightReelNode = DiceAnimatedStickerNode(size: reelSize)
|
||||||
self.frontNode = ManagedAnimationNode(size: self.animationSize)
|
self.frontNode = ManagedAnimationNode(size: self.animationSize)
|
||||||
|
|
||||||
super.init()
|
super.init()
|
||||||
@ -261,7 +262,7 @@ class DiceAnimatedStickerNode: ASDisplayNode {
|
|||||||
self.intrinsicSize = size
|
self.intrinsicSize = size
|
||||||
|
|
||||||
self.animationNode = AnimatedStickerNode()
|
self.animationNode = AnimatedStickerNode()
|
||||||
self.animationNode.visibility = true
|
self.animationNode.autoplay = true
|
||||||
|
|
||||||
super.init()
|
super.init()
|
||||||
|
|
||||||
@ -281,6 +282,15 @@ class DiceAnimatedStickerNode: ASDisplayNode {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var initialized = false
|
||||||
|
override func didLoad() {
|
||||||
|
super.didLoad()
|
||||||
|
|
||||||
|
self.initialized = true
|
||||||
|
self.advanceState()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
private func advanceState() {
|
private func advanceState() {
|
||||||
guard !self.trackStack.isEmpty else {
|
guard !self.trackStack.isEmpty else {
|
||||||
return
|
return
|
||||||
@ -329,7 +339,7 @@ class DiceAnimatedStickerNode: ASDisplayNode {
|
|||||||
self.trackStack.append(item)
|
self.trackStack.append(item)
|
||||||
self.didTryAdvancingState = false
|
self.didTryAdvancingState = false
|
||||||
|
|
||||||
if !self.animationNode.isPlaying {
|
if !self.animationNode.isPlaying && self.initialized {
|
||||||
self.advanceState()
|
self.advanceState()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,6 +4,7 @@ import AVFoundation
|
|||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
public enum ManagedAudioSessionType: Equatable {
|
public enum ManagedAudioSessionType: Equatable {
|
||||||
|
case ambient
|
||||||
case play
|
case play
|
||||||
case playWithPossiblePortOverride
|
case playWithPossiblePortOverride
|
||||||
case record(speaker: Bool)
|
case record(speaker: Bool)
|
||||||
@ -22,6 +23,8 @@ public enum ManagedAudioSessionType: Equatable {
|
|||||||
|
|
||||||
private func nativeCategoryForType(_ type: ManagedAudioSessionType, headphones: Bool, outputMode: AudioSessionOutputMode) -> AVAudioSession.Category {
|
private func nativeCategoryForType(_ type: ManagedAudioSessionType, headphones: Bool, outputMode: AudioSessionOutputMode) -> AVAudioSession.Category {
|
||||||
switch type {
|
switch type {
|
||||||
|
case .ambient:
|
||||||
|
return .ambient
|
||||||
case .play:
|
case .play:
|
||||||
return .playback
|
return .playback
|
||||||
case .record, .voiceCall, .videoCall:
|
case .record, .voiceCall, .videoCall:
|
||||||
@ -665,7 +668,7 @@ public final class ManagedAudioSession {
|
|||||||
print("ManagedAudioSession setting category for \(type) (native: \(nativeCategory))")
|
print("ManagedAudioSession setting category for \(type) (native: \(nativeCategory))")
|
||||||
var options: AVAudioSession.CategoryOptions = []
|
var options: AVAudioSession.CategoryOptions = []
|
||||||
switch type {
|
switch type {
|
||||||
case .play:
|
case .play, .ambient:
|
||||||
break
|
break
|
||||||
case .playWithPossiblePortOverride:
|
case .playWithPossiblePortOverride:
|
||||||
if case .playAndRecord = nativeCategory {
|
if case .playAndRecord = nativeCategory {
|
||||||
|
@ -331,7 +331,7 @@ public final class MediaNavigationAccessoryHeaderNode: ASDisplayNode, UIScrollVi
|
|||||||
switch status {
|
switch status {
|
||||||
case .paused:
|
case .paused:
|
||||||
paused = true
|
paused = true
|
||||||
case let .buffering(_, whilePlaying, _):
|
case let .buffering(_, whilePlaying, _, _):
|
||||||
paused = !whilePlaying
|
paused = !whilePlaying
|
||||||
case .playing:
|
case .playing:
|
||||||
paused = false
|
paused = false
|
||||||
|
@ -34,7 +34,7 @@ private final class PresentationCallToneRenderer {
|
|||||||
|
|
||||||
self.toneRenderer = MediaPlayerAudioRenderer(audioSession: .custom({ control in
|
self.toneRenderer = MediaPlayerAudioRenderer(audioSession: .custom({ control in
|
||||||
return controlImpl?(control) ?? EmptyDisposable
|
return controlImpl?(control) ?? EmptyDisposable
|
||||||
}), playAndRecord: false, forceAudioToSpeaker: false, baseRate: 1.0, audioLevelPipe: self.audioLevelPipe, updatedRate: {}, audioPaused: {})
|
}), playAndRecord: false, ambient: false, forceAudioToSpeaker: false, baseRate: 1.0, audioLevelPipe: self.audioLevelPipe, updatedRate: {}, audioPaused: {})
|
||||||
|
|
||||||
controlImpl = { [weak self] control in
|
controlImpl = { [weak self] control in
|
||||||
queue.async {
|
queue.async {
|
||||||
|
File diff suppressed because it is too large
Load Diff
Binary file not shown.
@ -5232,9 +5232,15 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, unpinMessage: { [weak self] id, askForConfirmation in
|
}, unpinMessage: { [weak self] id, askForConfirmation, contextController in
|
||||||
if let strongSelf = self {
|
let impl: () -> Void = {
|
||||||
if let peer = strongSelf.presentationInterfaceState.renderedPeer?.peer {
|
guard let strongSelf = self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
guard let peer = strongSelf.presentationInterfaceState.renderedPeer?.peer else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if strongSelf.canManagePin() {
|
if strongSelf.canManagePin() {
|
||||||
let action: () -> Void = {
|
let action: () -> Void = {
|
||||||
if let strongSelf = self {
|
if let strongSelf = self {
|
||||||
@ -5296,12 +5302,51 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
|||||||
),
|
),
|
||||||
in: .current
|
in: .current
|
||||||
)
|
)
|
||||||
|
} else {
|
||||||
|
if case .pinnedMessages = strongSelf.presentationInterfaceState.subject {
|
||||||
|
strongSelf.chatDisplayNode.historyNode.pendingRemovedMessages.insert(id)
|
||||||
|
strongSelf.present(
|
||||||
|
UndoOverlayController(
|
||||||
|
presentationData: strongSelf.presentationData,
|
||||||
|
content: .messagesUnpinned(
|
||||||
|
title: strongSelf.presentationData.strings.Chat_MessagesUnpinned(1),
|
||||||
|
text: "",
|
||||||
|
undo: true,
|
||||||
|
isHidden: false
|
||||||
|
),
|
||||||
|
elevatedLayout: false,
|
||||||
|
action: { action in
|
||||||
|
guard let strongSelf = self else {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
switch action {
|
||||||
|
case .commit:
|
||||||
|
let _ = (requestUpdatePinnedMessage(account: strongSelf.context.account, peerId: peer.id, update: .clear(id: id))
|
||||||
|
|> deliverOnMainQueue).start(completed: {
|
||||||
|
Queue.mainQueue().after(1.0, {
|
||||||
|
guard let strongSelf = self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
strongSelf.chatDisplayNode.historyNode.pendingRemovedMessages.remove(id)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
case .undo:
|
||||||
|
strongSelf.chatDisplayNode.historyNode.pendingRemovedMessages.remove(id)
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
),
|
||||||
|
in: .current
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
disposable.set((requestUpdatePinnedMessage(account: strongSelf.context.account, peerId: peer.id, update: .clear(id: id))
|
disposable.set((requestUpdatePinnedMessage(account: strongSelf.context.account, peerId: peer.id, update: .clear(id: id))
|
||||||
|> deliverOnMainQueue).start())
|
|> deliverOnMainQueue).start())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
if askForConfirmation {
|
if askForConfirmation {
|
||||||
strongSelf.present(textAlertController(context: strongSelf.context, title: nil, text: strongSelf.presentationData.strings.Conversation_UnpinMessageAlert, actions: [TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Conversation_Unpin, action: {
|
strongSelf.present(textAlertController(context: strongSelf.context, title: nil, text: strongSelf.presentationData.strings.Conversation_UnpinMessageAlert, actions: [TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Conversation_Unpin, action: {
|
||||||
action()
|
action()
|
||||||
@ -5357,6 +5402,13 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let contextController = contextController {
|
||||||
|
contextController.dismiss(completion: {
|
||||||
|
impl()
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
impl()
|
||||||
}
|
}
|
||||||
}, unpinAllMessages: { [weak self] in
|
}, unpinAllMessages: { [weak self] in
|
||||||
guard let strongSelf = self else {
|
guard let strongSelf = self else {
|
||||||
@ -8094,7 +8146,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
|||||||
|
|
||||||
public func displayPromoAnnouncement(text: String) {
|
public func displayPromoAnnouncement(text: String) {
|
||||||
let psaText: String = text
|
let psaText: String = text
|
||||||
let psaEntities: [MessageTextEntity] = generateTextEntities(psaText, enabledTypes: .url)
|
let psaEntities: [MessageTextEntity] = generateTextEntities(psaText, enabledTypes: .allUrl)
|
||||||
|
|
||||||
var found = false
|
var found = false
|
||||||
self.forEachController({ controller in
|
self.forEachController({ controller in
|
||||||
@ -8187,7 +8239,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
|||||||
psaText = string
|
psaText = string
|
||||||
}
|
}
|
||||||
|
|
||||||
let psaEntities: [MessageTextEntity] = generateTextEntities(psaText, enabledTypes: .url)
|
let psaEntities: [MessageTextEntity] = generateTextEntities(psaText, enabledTypes: .allUrl)
|
||||||
|
|
||||||
let messageId = item.message.id
|
let messageId = item.message.id
|
||||||
|
|
||||||
|
@ -8,7 +8,7 @@ import AccountContext
|
|||||||
import TelegramPresentationData
|
import TelegramPresentationData
|
||||||
|
|
||||||
|
|
||||||
func chatHistoryEntriesForView(location: ChatLocation, view: MessageHistoryView, includeUnreadEntry: Bool, includeEmptyEntry: Bool, includeChatInfoEntry: Bool, includeSearchEntry: Bool, reverse: Bool, groupMessages: Bool, selectedMessages: Set<MessageId>?, presentationData: ChatPresentationData, historyAppearsCleared: Bool, pendingUnpinnedAllMessages: Bool, associatedData: ChatMessageItemAssociatedData, updatingMedia: [MessageId: ChatUpdatingMessageMedia], customChannelDiscussionReadState: MessageId?, customThreadOutgoingReadState: MessageId?) -> [ChatHistoryEntry] {
|
func chatHistoryEntriesForView(location: ChatLocation, view: MessageHistoryView, includeUnreadEntry: Bool, includeEmptyEntry: Bool, includeChatInfoEntry: Bool, includeSearchEntry: Bool, reverse: Bool, groupMessages: Bool, selectedMessages: Set<MessageId>?, presentationData: ChatPresentationData, historyAppearsCleared: Bool, pendingUnpinnedAllMessages: Bool, pendingRemovedMessages: Set<MessageId>, associatedData: ChatMessageItemAssociatedData, updatingMedia: [MessageId: ChatUpdatingMessageMedia], customChannelDiscussionReadState: MessageId?, customThreadOutgoingReadState: MessageId?) -> [ChatHistoryEntry] {
|
||||||
if historyAppearsCleared {
|
if historyAppearsCleared {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
@ -34,6 +34,10 @@ func chatHistoryEntriesForView(location: ChatLocation, view: MessageHistoryView,
|
|||||||
var message = entry.message
|
var message = entry.message
|
||||||
var isRead = entry.isRead
|
var isRead = entry.isRead
|
||||||
|
|
||||||
|
if pendingRemovedMessages.contains(message.id) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
if let customThreadOutgoingReadState = customThreadOutgoingReadState {
|
if let customThreadOutgoingReadState = customThreadOutgoingReadState {
|
||||||
isRead = customThreadOutgoingReadState >= message.id
|
isRead = customThreadOutgoingReadState >= message.id
|
||||||
}
|
}
|
||||||
|
@ -565,6 +565,15 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private let pendingRemovedMessagesPromise = ValuePromise<Set<MessageId>>(Set())
|
||||||
|
var pendingRemovedMessages: Set<MessageId> = Set() {
|
||||||
|
didSet {
|
||||||
|
if self.pendingRemovedMessages != oldValue {
|
||||||
|
self.pendingRemovedMessagesPromise.set(self.pendingRemovedMessages)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private(set) var isScrollAtBottomPosition = false
|
private(set) var isScrollAtBottomPosition = false
|
||||||
public var isScrollAtBottomPositionUpdated: (() -> Void)?
|
public var isScrollAtBottomPositionUpdated: (() -> Void)?
|
||||||
|
|
||||||
@ -846,10 +855,11 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode {
|
|||||||
automaticDownloadNetworkType,
|
automaticDownloadNetworkType,
|
||||||
self.historyAppearsClearedPromise.get(),
|
self.historyAppearsClearedPromise.get(),
|
||||||
self.pendingUnpinnedAllMessagesPromise.get(),
|
self.pendingUnpinnedAllMessagesPromise.get(),
|
||||||
|
self.pendingRemovedMessagesPromise.get(),
|
||||||
animatedEmojiStickers,
|
animatedEmojiStickers,
|
||||||
customChannelDiscussionReadState,
|
customChannelDiscussionReadState,
|
||||||
customThreadOutgoingReadState
|
customThreadOutgoingReadState
|
||||||
).start(next: { [weak self] update, chatPresentationData, selectedMessages, updatingMedia, networkType, historyAppearsCleared, pendingUnpinnedAllMessages, animatedEmojiStickers, customChannelDiscussionReadState, customThreadOutgoingReadState in
|
).start(next: { [weak self] update, chatPresentationData, selectedMessages, updatingMedia, networkType, historyAppearsCleared, pendingUnpinnedAllMessages, pendingRemovedMessages, animatedEmojiStickers, customChannelDiscussionReadState, customThreadOutgoingReadState in
|
||||||
func applyHole() {
|
func applyHole() {
|
||||||
Queue.mainQueue().async {
|
Queue.mainQueue().async {
|
||||||
if let strongSelf = self {
|
if let strongSelf = self {
|
||||||
@ -930,7 +940,7 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode {
|
|||||||
|
|
||||||
let associatedData = extractAssociatedData(chatLocation: chatLocation, view: view, automaticDownloadNetworkType: networkType, animatedEmojiStickers: animatedEmojiStickers, subject: subject)
|
let associatedData = extractAssociatedData(chatLocation: chatLocation, view: view, automaticDownloadNetworkType: networkType, animatedEmojiStickers: animatedEmojiStickers, subject: subject)
|
||||||
|
|
||||||
let filteredEntries = chatHistoryEntriesForView(location: chatLocation, view: view, includeUnreadEntry: mode == .bubbles, includeEmptyEntry: mode == .bubbles && tagMask == nil, includeChatInfoEntry: mode == .bubbles, includeSearchEntry: includeSearchEntry && tagMask != nil, reverse: reverse, groupMessages: mode == .bubbles, selectedMessages: selectedMessages, presentationData: chatPresentationData, historyAppearsCleared: historyAppearsCleared, pendingUnpinnedAllMessages: pendingUnpinnedAllMessages, associatedData: associatedData, updatingMedia: updatingMedia, customChannelDiscussionReadState: customChannelDiscussionReadState, customThreadOutgoingReadState: customThreadOutgoingReadState)
|
let filteredEntries = chatHistoryEntriesForView(location: chatLocation, view: view, includeUnreadEntry: mode == .bubbles, includeEmptyEntry: mode == .bubbles && tagMask == nil, includeChatInfoEntry: mode == .bubbles, includeSearchEntry: includeSearchEntry && tagMask != nil, reverse: reverse, groupMessages: mode == .bubbles, selectedMessages: selectedMessages, presentationData: chatPresentationData, historyAppearsCleared: historyAppearsCleared, pendingUnpinnedAllMessages: pendingUnpinnedAllMessages, pendingRemovedMessages: pendingRemovedMessages, associatedData: associatedData, updatingMedia: updatingMedia, customChannelDiscussionReadState: customChannelDiscussionReadState, customThreadOutgoingReadState: customThreadOutgoingReadState)
|
||||||
let lastHeaderId = filteredEntries.last.flatMap { listMessageDateHeaderId(timestamp: $0.index.timestamp) } ?? 0
|
let lastHeaderId = filteredEntries.last.flatMap { listMessageDateHeaderId(timestamp: $0.index.timestamp) } ?? 0
|
||||||
let processedView = ChatHistoryView(originalView: view, filteredEntries: filteredEntries, associatedData: associatedData, lastHeaderId: lastHeaderId, id: id)
|
let processedView = ChatHistoryView(originalView: view, filteredEntries: filteredEntries, associatedData: associatedData, lastHeaderId: lastHeaderId, id: id)
|
||||||
let previousValueAndVersion = previousView.swap((processedView, update.1, selectedMessages))
|
let previousValueAndVersion = previousView.swap((processedView, update.1, selectedMessages))
|
||||||
@ -1149,6 +1159,15 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let selectionRecognizer = ChatHistoryListSelectionRecognizer(target: self, action: #selector(self.selectionPanGesture(_:)))
|
let selectionRecognizer = ChatHistoryListSelectionRecognizer(target: self, action: #selector(self.selectionPanGesture(_:)))
|
||||||
|
selectionRecognizer.shouldBegin = { [weak self] in
|
||||||
|
guard let strongSelf = self else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if case .pinnedMessages = strongSelf.subject {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
self.view.addGestureRecognizer(selectionRecognizer)
|
self.view.addGestureRecognizer(selectionRecognizer)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -111,6 +111,9 @@ private func canEditMessage(accountPeerId: PeerId, limitsConfiguration: LimitsCo
|
|||||||
} else if let _ = media as? TelegramMediaPoll {
|
} else if let _ = media as? TelegramMediaPoll {
|
||||||
hasUneditableAttributes = true
|
hasUneditableAttributes = true
|
||||||
break
|
break
|
||||||
|
} else if let _ = media as? TelegramMediaDice {
|
||||||
|
hasUneditableAttributes = true
|
||||||
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -687,9 +690,8 @@ func contextMenuForChatPresentationIntefaceState(chatPresentationInterfaceState:
|
|||||||
if let pinnedSelectedMessageId = pinnedSelectedMessageId {
|
if let pinnedSelectedMessageId = pinnedSelectedMessageId {
|
||||||
actions.append(.action(ContextMenuActionItem(text: chatPresentationInterfaceState.strings.Conversation_Unpin, icon: { theme in
|
actions.append(.action(ContextMenuActionItem(text: chatPresentationInterfaceState.strings.Conversation_Unpin, icon: { theme in
|
||||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Unpin"), color: theme.actionSheet.primaryTextColor)
|
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Unpin"), color: theme.actionSheet.primaryTextColor)
|
||||||
}, action: { _, f in
|
}, action: { c, _ in
|
||||||
interfaceInteraction.unpinMessage(pinnedSelectedMessageId, false)
|
interfaceInteraction.unpinMessage(pinnedSelectedMessageId, false, c)
|
||||||
f(.default)
|
|
||||||
})))
|
})))
|
||||||
} else {
|
} else {
|
||||||
actions.append(.action(ContextMenuActionItem(text: chatPresentationInterfaceState.strings.Conversation_Pin, icon: { theme in
|
actions.append(.action(ContextMenuActionItem(text: chatPresentationInterfaceState.strings.Conversation_Pin, icon: { theme in
|
||||||
|
@ -201,7 +201,7 @@ class ChatMessageActionBubbleContentNode: ChatMessageBubbleContentNode {
|
|||||||
strongSelf.insertSubnode(imageNode, at: 0)
|
strongSelf.insertSubnode(imageNode, at: 0)
|
||||||
strongSelf.insertSubnode(strongSelf.mediaBackgroundNode, at: 0)
|
strongSelf.insertSubnode(strongSelf.mediaBackgroundNode, at: 0)
|
||||||
}
|
}
|
||||||
strongSelf.fetchDisposable.set(chatMessagePhotoInteractiveFetched(context: item.context, photoReference: .message(message: MessageReference(item.message), media: image), storeToDownloadsPeerType: nil).start())
|
strongSelf.fetchDisposable.set(chatMessagePhotoInteractiveFetched(context: item.context, photoReference: .message(message: MessageReference(item.message), media: image), displayAtSize: nil, storeToDownloadsPeerType: nil).start())
|
||||||
let updateImageSignal = chatMessagePhoto(postbox: item.context.account.postbox, photoReference: .message(message: MessageReference(item.message), media: image), synchronousLoad: synchronousLoads)
|
let updateImageSignal = chatMessagePhoto(postbox: item.context.account.postbox, photoReference: .message(message: MessageReference(item.message), media: image), synchronousLoad: synchronousLoads)
|
||||||
|
|
||||||
imageNode.setSignal(updateImageSignal, attemptSynchronously: synchronousLoads)
|
imageNode.setSignal(updateImageSignal, attemptSynchronously: synchronousLoads)
|
||||||
|
@ -722,7 +722,12 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
|
|||||||
|
|
||||||
let dateText = stringForMessageTimestampStatus(accountPeerId: item.context.account.peerId, message: item.message, dateTimeFormat: item.presentationData.dateTimeFormat, nameDisplayOrder: item.presentationData.nameDisplayOrder, strings: item.presentationData.strings, format: .minimal, reactionCount: dateReactionCount)
|
let dateText = stringForMessageTimestampStatus(accountPeerId: item.context.account.peerId, message: item.message, dateTimeFormat: item.presentationData.dateTimeFormat, nameDisplayOrder: item.presentationData.nameDisplayOrder, strings: item.presentationData.strings, format: .minimal, reactionCount: dateReactionCount)
|
||||||
|
|
||||||
let (dateAndStatusSize, dateAndStatusApply) = makeDateAndStatusLayout(item.context, item.presentationData, edited, viewCount, dateText, statusType, CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), dateReactions, dateReplies, item.message.tags.contains(.pinned) && !item.associatedData.isInPinnedListMode)
|
var isReplyThread = false
|
||||||
|
if case .replyThread = item.chatLocation {
|
||||||
|
isReplyThread = true
|
||||||
|
}
|
||||||
|
|
||||||
|
let (dateAndStatusSize, dateAndStatusApply) = makeDateAndStatusLayout(item.context, item.presentationData, edited, viewCount, dateText, statusType, CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), dateReactions, dateReplies, item.message.tags.contains(.pinned) && !item.associatedData.isInPinnedListMode && !isReplyThread)
|
||||||
|
|
||||||
var viaBotApply: (TextNodeLayout, () -> TextNode)?
|
var viaBotApply: (TextNodeLayout, () -> TextNode)?
|
||||||
var replyInfoApply: (CGSize, () -> ChatMessageReplyInfoNode)?
|
var replyInfoApply: (CGSize, () -> ChatMessageReplyInfoNode)?
|
||||||
@ -1274,7 +1279,7 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
|
|||||||
return .optionalAction({
|
return .optionalAction({
|
||||||
if shouldPlay {
|
if shouldPlay {
|
||||||
let _ = (appConfiguration
|
let _ = (appConfiguration
|
||||||
|> deliverOnMainQueue).start(next: { [weak self] appConfiguration in
|
|> deliverOnMainQueue).start(next: { [weak self, weak animationNode] appConfiguration in
|
||||||
guard let strongSelf = self else {
|
guard let strongSelf = self else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -1282,7 +1287,7 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
|
|||||||
for (emoji, file) in emojiSounds.sounds {
|
for (emoji, file) in emojiSounds.sounds {
|
||||||
if emoji.unicodeScalars.first == firstScalar {
|
if emoji.unicodeScalars.first == firstScalar {
|
||||||
let mediaManager = item.context.sharedContext.mediaManager
|
let mediaManager = item.context.sharedContext.mediaManager
|
||||||
let mediaPlayer = MediaPlayer(audioSessionManager: mediaManager.audioSession, postbox: item.context.account.postbox, resourceReference: .standalone(resource: file.resource), streamable: .none, video: false, preferSoftwareDecoding: false, enableSound: true, fetchAutomatically: true)
|
let mediaPlayer = MediaPlayer(audioSessionManager: mediaManager.audioSession, postbox: item.context.account.postbox, resourceReference: .standalone(resource: file.resource), streamable: .none, video: false, preferSoftwareDecoding: false, enableSound: true, fetchAutomatically: true, ambient: true)
|
||||||
mediaPlayer.togglePlayPause()
|
mediaPlayer.togglePlayPause()
|
||||||
mediaPlayer.actionAtEnd = .action({ [weak self] in
|
mediaPlayer.actionAtEnd = .action({ [weak self] in
|
||||||
self?.mediaPlayer = nil
|
self?.mediaPlayer = nil
|
||||||
@ -1315,9 +1320,10 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
break
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
animationNode?.play()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -416,6 +416,11 @@ final class ChatMessageAttachedContentNode: ASDisplayNode {
|
|||||||
|
|
||||||
var skipStandardStatus = false
|
var skipStandardStatus = false
|
||||||
|
|
||||||
|
var isReplyThread = false
|
||||||
|
if case .replyThread = chatLocation {
|
||||||
|
isReplyThread = true
|
||||||
|
}
|
||||||
|
|
||||||
if let (media, flags) = mediaAndFlags {
|
if let (media, flags) = mediaAndFlags {
|
||||||
if let file = media as? TelegramMediaFile {
|
if let file = media as? TelegramMediaFile {
|
||||||
if file.mimeType == "application/x-tgtheme-ios", let size = file.size, size < 16 * 1024 {
|
if file.mimeType == "application/x-tgtheme-ios", let size = file.size, size < 16 * 1024 {
|
||||||
@ -424,7 +429,7 @@ final class ChatMessageAttachedContentNode: ASDisplayNode {
|
|||||||
refineContentImageLayout = refineLayout
|
refineContentImageLayout = refineLayout
|
||||||
} else if file.isInstantVideo {
|
} else if file.isInstantVideo {
|
||||||
let automaticDownload = shouldDownloadMediaAutomatically(settings: automaticDownloadSettings, peerType: associatedData.automaticDownloadPeerType, networkType: associatedData.automaticDownloadNetworkType, authorPeerId: message.author?.id, contactsPeerIds: associatedData.contactsPeerIds, media: file)
|
let automaticDownload = shouldDownloadMediaAutomatically(settings: automaticDownloadSettings, peerType: associatedData.automaticDownloadPeerType, networkType: associatedData.automaticDownloadNetworkType, authorPeerId: message.author?.id, contactsPeerIds: associatedData.contactsPeerIds, media: file)
|
||||||
let (videoLayout, apply) = contentInstantVideoLayout(ChatMessageBubbleContentItem(context: context, controllerInteraction: controllerInteraction, message: message, read: messageRead, chatLocation: chatLocation, presentationData: presentationData, associatedData: associatedData, attributes: attributes, isItemPinned: message.tags.contains(.pinned), isItemEdited: false), constrainedSize.width - horizontalInsets.left - horizontalInsets.right, CGSize(width: 212.0, height: 212.0), .bubble, automaticDownload)
|
let (videoLayout, apply) = contentInstantVideoLayout(ChatMessageBubbleContentItem(context: context, controllerInteraction: controllerInteraction, message: message, read: messageRead, chatLocation: chatLocation, presentationData: presentationData, associatedData: associatedData, attributes: attributes, isItemPinned: message.tags.contains(.pinned) && !isReplyThread, isItemEdited: false), constrainedSize.width - horizontalInsets.left - horizontalInsets.right, CGSize(width: 212.0, height: 212.0), .bubble, automaticDownload)
|
||||||
initialWidth = videoLayout.contentSize.width + videoLayout.overflowLeft + videoLayout.overflowRight
|
initialWidth = videoLayout.contentSize.width + videoLayout.overflowLeft + videoLayout.overflowRight
|
||||||
contentInstantVideoSizeAndApply = (videoLayout, apply)
|
contentInstantVideoSizeAndApply = (videoLayout, apply)
|
||||||
} else if file.isVideo {
|
} else if file.isVideo {
|
||||||
@ -474,7 +479,7 @@ final class ChatMessageAttachedContentNode: ASDisplayNode {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let (_, refineLayout) = contentFileLayout(context, presentationData, message, associatedData, chatLocation, attributes, message.tags.contains(.pinned) && !associatedData.isInPinnedListMode, false, file, automaticDownload, message.effectivelyIncoming(context.account.peerId), false, associatedData.forcedResourceStatus, statusType, nil, CGSize(width: constrainedSize.width - horizontalInsets.left - horizontalInsets.right, height: constrainedSize.height))
|
let (_, refineLayout) = contentFileLayout(context, presentationData, message, associatedData, chatLocation, attributes, message.tags.contains(.pinned) && !associatedData.isInPinnedListMode && !isReplyThread, false, file, automaticDownload, message.effectivelyIncoming(context.account.peerId), false, associatedData.forcedResourceStatus, statusType, nil, CGSize(width: constrainedSize.width - horizontalInsets.left - horizontalInsets.right, height: constrainedSize.height))
|
||||||
refineContentFileLayout = refineLayout
|
refineContentFileLayout = refineLayout
|
||||||
}
|
}
|
||||||
} else if let image = media as? TelegramMediaImage {
|
} else if let image = media as? TelegramMediaImage {
|
||||||
@ -572,7 +577,7 @@ final class ChatMessageAttachedContentNode: ASDisplayNode {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
statusSizeAndApply = statusLayout(context, presentationData, edited, viewCount, dateText, statusType, textConstrainedSize, dateReactions, dateReplies, message.tags.contains(.pinned) && !associatedData.isInPinnedListMode)
|
statusSizeAndApply = statusLayout(context, presentationData, edited, viewCount, dateText, statusType, textConstrainedSize, dateReactions, dateReplies, message.tags.contains(.pinned) && !associatedData.isInPinnedListMode && !isReplyThread)
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
break
|
break
|
||||||
|
@ -1088,6 +1088,12 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
|
|||||||
var needShareButton = false
|
var needShareButton = false
|
||||||
if case .pinnedMessages = item.associatedData.subject {
|
if case .pinnedMessages = item.associatedData.subject {
|
||||||
needShareButton = true
|
needShareButton = true
|
||||||
|
for media in item.message.media {
|
||||||
|
if let _ = media as? TelegramMediaExpiredContent {
|
||||||
|
needShareButton = false
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
} else if case let .replyThread(replyThreadMessage) = item.chatLocation, replyThreadMessage.effectiveTopId == item.message.id {
|
} else if case let .replyThread(replyThreadMessage) = item.chatLocation, replyThreadMessage.effectiveTopId == item.message.id {
|
||||||
needShareButton = false
|
needShareButton = false
|
||||||
allowFullWidth = true
|
allowFullWidth = true
|
||||||
@ -1287,6 +1293,10 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if case .replyThread = item.chatLocation {
|
||||||
|
isItemPinned = false
|
||||||
|
}
|
||||||
|
|
||||||
var mosaicStartIndex: Int?
|
var mosaicStartIndex: Int?
|
||||||
var mosaicRange: Range<Int>?
|
var mosaicRange: Range<Int>?
|
||||||
for i in 0 ..< contentPropertiesAndPrepareLayouts.count {
|
for i in 0 ..< contentPropertiesAndPrepareLayouts.count {
|
||||||
@ -1526,7 +1536,12 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
mosaicStatusSizeAndApply = mosaicStatusLayout(item.context, item.presentationData, edited, viewCount, dateText, statusType, CGSize(width: 200.0, height: CGFloat.greatestFiniteMagnitude), dateReactions, dateReplies, message.tags.contains(.pinned) && !item.associatedData.isInPinnedListMode)
|
var isReplyThread = false
|
||||||
|
if case .replyThread = item.chatLocation {
|
||||||
|
isReplyThread = true
|
||||||
|
}
|
||||||
|
|
||||||
|
mosaicStatusSizeAndApply = mosaicStatusLayout(item.context, item.presentationData, edited, viewCount, dateText, statusType, CGSize(width: 200.0, height: CGFloat.greatestFiniteMagnitude), dateReactions, dateReplies, message.tags.contains(.pinned) && !item.associatedData.isInPinnedListMode && !isReplyThread)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -196,7 +196,12 @@ class ChatMessageContactBubbleContentNode: ChatMessageBubbleContentNode {
|
|||||||
var statusApply: ((Bool) -> Void)?
|
var statusApply: ((Bool) -> Void)?
|
||||||
|
|
||||||
if let statusType = statusType {
|
if let statusType = statusType {
|
||||||
let (size, apply) = statusLayout(item.context, item.presentationData, edited, viewCount, dateText, statusType, CGSize(width: constrainedSize.width, height: CGFloat.greatestFiniteMagnitude), dateReactions, dateReplies, item.message.tags.contains(.pinned) && !item.associatedData.isInPinnedListMode)
|
var isReplyThread = false
|
||||||
|
if case .replyThread = item.chatLocation {
|
||||||
|
isReplyThread = true
|
||||||
|
}
|
||||||
|
|
||||||
|
let (size, apply) = statusLayout(item.context, item.presentationData, edited, viewCount, dateText, statusType, CGSize(width: constrainedSize.width, height: CGFloat.greatestFiniteMagnitude), dateReactions, dateReplies, item.message.tags.contains(.pinned) && !item.associatedData.isInPinnedListMode && !isReplyThread)
|
||||||
statusSize = size
|
statusSize = size
|
||||||
statusApply = apply
|
statusApply = apply
|
||||||
}
|
}
|
||||||
|
@ -278,7 +278,12 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let (videoLayout, videoApply) = makeVideoLayout(ChatMessageBubbleContentItem(context: item.context, controllerInteraction: item.controllerInteraction, message: item.message, read: item.read, chatLocation: item.chatLocation, presentationData: item.presentationData, associatedData: item.associatedData, attributes: item.content.firstMessageAttributes, isItemPinned: item.message.tags.contains(.pinned), isItemEdited: false), params.width - params.leftInset - params.rightInset - avatarInset, displaySize, .free, automaticDownload)
|
var isReplyThread = false
|
||||||
|
if case .replyThread = item.chatLocation {
|
||||||
|
isReplyThread = true
|
||||||
|
}
|
||||||
|
|
||||||
|
let (videoLayout, videoApply) = makeVideoLayout(ChatMessageBubbleContentItem(context: item.context, controllerInteraction: item.controllerInteraction, message: item.message, read: item.read, chatLocation: item.chatLocation, presentationData: item.presentationData, associatedData: item.associatedData, attributes: item.content.firstMessageAttributes, isItemPinned: item.message.tags.contains(.pinned) && !isReplyThread, isItemEdited: false), params.width - params.leftInset - params.rightInset - avatarInset, displaySize, .free, automaticDownload)
|
||||||
|
|
||||||
let videoFrame = CGRect(origin: CGPoint(x: (incoming ? (params.leftInset + layoutConstants.bubble.edgeInset + avatarInset + layoutConstants.bubble.contentInsets.left) : (params.width - params.rightInset - videoLayout.contentSize.width - layoutConstants.bubble.edgeInset - layoutConstants.bubble.contentInsets.left - deliveryFailedInset)), y: 0.0), size: videoLayout.contentSize)
|
let videoFrame = CGRect(origin: CGPoint(x: (incoming ? (params.leftInset + layoutConstants.bubble.edgeInset + avatarInset + layoutConstants.bubble.contentInsets.left) : (params.width - params.rightInset - videoLayout.contentSize.width - layoutConstants.bubble.edgeInset - layoutConstants.bubble.contentInsets.left - deliveryFailedInset)), y: 0.0), size: videoLayout.contentSize)
|
||||||
|
|
||||||
|
@ -832,7 +832,7 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
|
|||||||
if isAudio && !isVoice {
|
if isAudio && !isVoice {
|
||||||
state = .play
|
state = .play
|
||||||
} else {
|
} else {
|
||||||
if adjustedProgress.isEqual(to: 1.0), (message.flags.contains(.Unsent) || wasCheck) {
|
if message.groupingKey != nil, adjustedProgress.isEqual(to: 1.0), (message.flags.contains(.Unsent) || wasCheck) {
|
||||||
state = .check(appearance: nil)
|
state = .check(appearance: nil)
|
||||||
} else {
|
} else {
|
||||||
state = .progress(value: CGFloat(adjustedProgress), cancelEnabled: true, appearance: nil)
|
state = .progress(value: CGFloat(adjustedProgress), cancelEnabled: true, appearance: nil)
|
||||||
|
@ -285,7 +285,13 @@ class ChatMessageInteractiveInstantVideoNode: ASDisplayNode {
|
|||||||
} else {
|
} else {
|
||||||
maxDateAndStatusWidth = width - videoFrame.midX - 85.0
|
maxDateAndStatusWidth = width - videoFrame.midX - 85.0
|
||||||
}
|
}
|
||||||
let (dateAndStatusSize, dateAndStatusApply) = makeDateAndStatusLayout(item.context, item.presentationData, edited && !sentViaBot, viewCount, dateText, statusType, CGSize(width: max(1.0, maxDateAndStatusWidth), height: CGFloat.greatestFiniteMagnitude), dateReactions, dateReplies, item.message.tags.contains(.pinned) && !item.associatedData.isInPinnedListMode)
|
|
||||||
|
var isReplyThread = false
|
||||||
|
if case .replyThread = item.chatLocation {
|
||||||
|
isReplyThread = true
|
||||||
|
}
|
||||||
|
|
||||||
|
let (dateAndStatusSize, dateAndStatusApply) = makeDateAndStatusLayout(item.context, item.presentationData, edited && !sentViaBot, viewCount, dateText, statusType, CGSize(width: max(1.0, maxDateAndStatusWidth), height: CGFloat.greatestFiniteMagnitude), dateReactions, dateReplies, item.message.tags.contains(.pinned) && !item.associatedData.isInPinnedListMode && !isReplyThread)
|
||||||
|
|
||||||
var contentSize = imageSize
|
var contentSize = imageSize
|
||||||
var dateAndStatusOverflow = false
|
var dateAndStatusOverflow = false
|
||||||
|
@ -486,9 +486,9 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio
|
|||||||
updatedFetchControls = FetchControls(fetch: { manual in
|
updatedFetchControls = FetchControls(fetch: { manual in
|
||||||
if let strongSelf = self {
|
if let strongSelf = self {
|
||||||
if !manual {
|
if !manual {
|
||||||
strongSelf.fetchDisposable.set(chatMessagePhotoInteractiveFetched(context: context, photoReference: .message(message: MessageReference(message), media: image), displayAtSize: 600, storeToDownloadsPeerType: storeToDownloadsPeerType).start())
|
strongSelf.fetchDisposable.set(chatMessagePhotoInteractiveFetched(context: context, photoReference: .message(message: MessageReference(message), media: image), displayAtSize: isSecretMedia ? nil : 600, storeToDownloadsPeerType: storeToDownloadsPeerType).start())
|
||||||
} else if let representation = largestRepresentationForPhoto(image) {
|
} else if let representation = largestRepresentationForPhoto(image) {
|
||||||
strongSelf.fetchDisposable.set(messageMediaImageInteractiveFetched(context: context, message: message, image: image, resource: representation.resource, range: representationFetchRangeForDisplayAtSize(representation: representation, dimension: 600), storeToDownloadsPeerType: storeToDownloadsPeerType).start())
|
strongSelf.fetchDisposable.set(messageMediaImageInteractiveFetched(context: context, message: message, image: image, resource: representation.resource, range: representationFetchRangeForDisplayAtSize(representation: representation, dimension: isSecretMedia ? nil : 600), storeToDownloadsPeerType: storeToDownloadsPeerType).start())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, cancel: {
|
}, cancel: {
|
||||||
|
@ -246,7 +246,12 @@ class ChatMessageMapBubbleContentNode: ChatMessageBubbleContentNode {
|
|||||||
var statusApply: ((Bool) -> Void)?
|
var statusApply: ((Bool) -> Void)?
|
||||||
|
|
||||||
if let statusType = statusType {
|
if let statusType = statusType {
|
||||||
let (size, apply) = statusLayout(item.context, item.presentationData, edited, viewCount, dateText, statusType, CGSize(width: constrainedSize.width, height: CGFloat.greatestFiniteMagnitude), dateReactions, dateReplies, item.message.tags.contains(.pinned) && !item.associatedData.isInPinnedListMode)
|
var isReplyThread = false
|
||||||
|
if case .replyThread = item.chatLocation {
|
||||||
|
isReplyThread = true
|
||||||
|
}
|
||||||
|
|
||||||
|
let (size, apply) = statusLayout(item.context, item.presentationData, edited, viewCount, dateText, statusType, CGSize(width: constrainedSize.width, height: CGFloat.greatestFiniteMagnitude), dateReactions, dateReplies, item.message.tags.contains(.pinned) && !item.associatedData.isInPinnedListMode && !isReplyThread)
|
||||||
statusSize = size
|
statusSize = size
|
||||||
statusApply = apply
|
statusApply = apply
|
||||||
}
|
}
|
||||||
|
@ -231,7 +231,12 @@ class ChatMessageMediaBubbleContentNode: ChatMessageBubbleContentNode {
|
|||||||
var statusApply: ((Bool) -> Void)?
|
var statusApply: ((Bool) -> Void)?
|
||||||
|
|
||||||
if let statusType = statusType {
|
if let statusType = statusType {
|
||||||
let (size, apply) = statusLayout(item.context, item.presentationData, edited, viewCount, dateText, statusType, CGSize(width: imageSize.width - 30.0, height: CGFloat.greatestFiniteMagnitude), dateReactions, dateReplies, item.message.tags.contains(.pinned) && !item.associatedData.isInPinnedListMode)
|
var isReplyThread = false
|
||||||
|
if case .replyThread = item.chatLocation {
|
||||||
|
isReplyThread = true
|
||||||
|
}
|
||||||
|
|
||||||
|
let (size, apply) = statusLayout(item.context, item.presentationData, edited, viewCount, dateText, statusType, CGSize(width: imageSize.width - 30.0, height: CGFloat.greatestFiniteMagnitude), dateReactions, dateReplies, item.message.tags.contains(.pinned) && !item.associatedData.isInPinnedListMode && !isReplyThread)
|
||||||
statusSize = size
|
statusSize = size
|
||||||
statusApply = apply
|
statusApply = apply
|
||||||
}
|
}
|
||||||
|
@ -1074,7 +1074,12 @@ class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode {
|
|||||||
var statusApply: ((Bool) -> Void)?
|
var statusApply: ((Bool) -> Void)?
|
||||||
|
|
||||||
if let statusType = statusType {
|
if let statusType = statusType {
|
||||||
let (size, apply) = statusLayout(item.context, item.presentationData, edited, viewCount, dateText, statusType, textConstrainedSize, dateReactions, dateReplies, item.message.tags.contains(.pinned) && !item.associatedData.isInPinnedListMode)
|
var isReplyThread = false
|
||||||
|
if case .replyThread = item.chatLocation {
|
||||||
|
isReplyThread = true
|
||||||
|
}
|
||||||
|
|
||||||
|
let (size, apply) = statusLayout(item.context, item.presentationData, edited, viewCount, dateText, statusType, textConstrainedSize, dateReactions, dateReplies, item.message.tags.contains(.pinned) && !item.associatedData.isInPinnedListMode && !isReplyThread)
|
||||||
statusSize = size
|
statusSize = size
|
||||||
statusApply = apply
|
statusApply = apply
|
||||||
}
|
}
|
||||||
|
@ -105,7 +105,12 @@ class ChatMessageRestrictedBubbleContentNode: ChatMessageBubbleContentNode {
|
|||||||
var statusApply: ((Bool) -> Void)?
|
var statusApply: ((Bool) -> Void)?
|
||||||
|
|
||||||
if let statusType = statusType {
|
if let statusType = statusType {
|
||||||
let (size, apply) = statusLayout(item.context, item.presentationData, edited, viewCount, dateText, statusType, textConstrainedSize, dateReactions, dateReplies, item.message.tags.contains(.pinned) && !item.associatedData.isInPinnedListMode)
|
var isReplyThread = false
|
||||||
|
if case .replyThread = item.chatLocation {
|
||||||
|
isReplyThread = true
|
||||||
|
}
|
||||||
|
|
||||||
|
let (size, apply) = statusLayout(item.context, item.presentationData, edited, viewCount, dateText, statusType, textConstrainedSize, dateReactions, dateReplies, item.message.tags.contains(.pinned) && !item.associatedData.isInPinnedListMode && isReplyThread)
|
||||||
statusSize = size
|
statusSize = size
|
||||||
statusApply = apply
|
statusApply = apply
|
||||||
}
|
}
|
||||||
|
@ -384,7 +384,12 @@ class ChatMessageStickerItemNode: ChatMessageItemView {
|
|||||||
|
|
||||||
let dateText = stringForMessageTimestampStatus(accountPeerId: item.context.account.peerId, message: item.message, dateTimeFormat: item.presentationData.dateTimeFormat, nameDisplayOrder: item.presentationData.nameDisplayOrder, strings: item.presentationData.strings, format: .regular, reactionCount: dateReactionCount)
|
let dateText = stringForMessageTimestampStatus(accountPeerId: item.context.account.peerId, message: item.message, dateTimeFormat: item.presentationData.dateTimeFormat, nameDisplayOrder: item.presentationData.nameDisplayOrder, strings: item.presentationData.strings, format: .regular, reactionCount: dateReactionCount)
|
||||||
|
|
||||||
let (dateAndStatusSize, dateAndStatusApply) = makeDateAndStatusLayout(item.context, item.presentationData, edited, viewCount, dateText, statusType, CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), dateReactions, dateReplies, item.message.tags.contains(.pinned) && !item.associatedData.isInPinnedListMode)
|
var isReplyThread = false
|
||||||
|
if case .replyThread = item.chatLocation {
|
||||||
|
isReplyThread = true
|
||||||
|
}
|
||||||
|
|
||||||
|
let (dateAndStatusSize, dateAndStatusApply) = makeDateAndStatusLayout(item.context, item.presentationData, edited, viewCount, dateText, statusType, CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), dateReactions, dateReplies, item.message.tags.contains(.pinned) && !item.associatedData.isInPinnedListMode && !isReplyThread)
|
||||||
|
|
||||||
var viaBotApply: (TextNodeLayout, () -> TextNode)?
|
var viaBotApply: (TextNodeLayout, () -> TextNode)?
|
||||||
var replyInfoApply: (CGSize, () -> ChatMessageReplyInfoNode)?
|
var replyInfoApply: (CGSize, () -> ChatMessageReplyInfoNode)?
|
||||||
|
@ -169,7 +169,12 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
|
|||||||
var statusApply: ((Bool) -> Void)?
|
var statusApply: ((Bool) -> Void)?
|
||||||
|
|
||||||
if let statusType = statusType {
|
if let statusType = statusType {
|
||||||
let (size, apply) = statusLayout(item.context, item.presentationData, edited, viewCount, dateText, statusType, textConstrainedSize, dateReactions, dateReplies, item.message.tags.contains(.pinned) && !item.associatedData.isInPinnedListMode)
|
var isReplyThread = false
|
||||||
|
if case .replyThread = item.chatLocation {
|
||||||
|
isReplyThread = true
|
||||||
|
}
|
||||||
|
|
||||||
|
let (size, apply) = statusLayout(item.context, item.presentationData, edited, viewCount, dateText, statusType, textConstrainedSize, dateReactions, dateReplies, item.message.tags.contains(.pinned) && !item.associatedData.isInPinnedListMode && !isReplyThread)
|
||||||
statusSize = size
|
statusSize = size
|
||||||
statusApply = apply
|
statusApply = apply
|
||||||
}
|
}
|
||||||
|
@ -122,7 +122,7 @@ final class ChatMessageWebpageBubbleContentNode: ChatMessageBubbleContentNode {
|
|||||||
|
|
||||||
if let textValue = webpage.text, !textValue.isEmpty {
|
if let textValue = webpage.text, !textValue.isEmpty {
|
||||||
text = textValue
|
text = textValue
|
||||||
var entityTypes: EnabledEntityTypes = [.url]
|
var entityTypes: EnabledEntityTypes = [.allUrl]
|
||||||
switch type {
|
switch type {
|
||||||
case .twitter, .instagram:
|
case .twitter, .instagram:
|
||||||
entityTypes.insert(.mention)
|
entityTypes.insert(.mention)
|
||||||
|
@ -95,7 +95,7 @@ final class ChatPanelInterfaceInteraction {
|
|||||||
let sendSticker: (FileMediaReference, ASDisplayNode, CGRect) -> Bool
|
let sendSticker: (FileMediaReference, ASDisplayNode, CGRect) -> Bool
|
||||||
let unblockPeer: () -> Void
|
let unblockPeer: () -> Void
|
||||||
let pinMessage: (MessageId, ContextController?) -> Void
|
let pinMessage: (MessageId, ContextController?) -> Void
|
||||||
let unpinMessage: (MessageId, Bool) -> Void
|
let unpinMessage: (MessageId, Bool, ContextController?) -> Void
|
||||||
let unpinAllMessages: () -> Void
|
let unpinAllMessages: () -> Void
|
||||||
let openPinnedList: (MessageId) -> Void
|
let openPinnedList: (MessageId) -> Void
|
||||||
let shareAccountContact: () -> Void
|
let shareAccountContact: () -> Void
|
||||||
@ -174,7 +174,7 @@ final class ChatPanelInterfaceInteraction {
|
|||||||
sendSticker: @escaping (FileMediaReference, ASDisplayNode, CGRect) -> Bool,
|
sendSticker: @escaping (FileMediaReference, ASDisplayNode, CGRect) -> Bool,
|
||||||
unblockPeer: @escaping () -> Void,
|
unblockPeer: @escaping () -> Void,
|
||||||
pinMessage: @escaping (MessageId, ContextController?) -> Void,
|
pinMessage: @escaping (MessageId, ContextController?) -> Void,
|
||||||
unpinMessage: @escaping (MessageId, Bool) -> Void,
|
unpinMessage: @escaping (MessageId, Bool, ContextController?) -> Void,
|
||||||
unpinAllMessages: @escaping () -> Void,
|
unpinAllMessages: @escaping () -> Void,
|
||||||
openPinnedList: @escaping (MessageId) -> Void,
|
openPinnedList: @escaping (MessageId) -> Void,
|
||||||
shareAccountContact: @escaping () -> Void,
|
shareAccountContact: @escaping () -> Void,
|
||||||
|
@ -325,6 +325,7 @@ final class ChatPinnedMessageTitlePanelNode: ChatTitleAccessoryPanelNode {
|
|||||||
titleStrings.append(.text(0, NSAttributedString(string: "\(strings.Conversation_PinnedMessage) ", font: Font.medium(15.0), textColor: theme.chat.inputPanel.panelControlAccentColor)))
|
titleStrings.append(.text(0, NSAttributedString(string: "\(strings.Conversation_PinnedMessage) ", font: Font.medium(15.0), textColor: theme.chat.inputPanel.panelControlAccentColor)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !message.containsSecretMedia {
|
||||||
for media in message.media {
|
for media in message.media {
|
||||||
if let image = media as? TelegramMediaImage {
|
if let image = media as? TelegramMediaImage {
|
||||||
updatedMediaReference = .message(message: MessageReference(message), media: image)
|
updatedMediaReference = .message(message: MessageReference(message), media: image)
|
||||||
@ -338,14 +339,8 @@ final class ChatPinnedMessageTitlePanelNode: ChatTitleAccessoryPanelNode {
|
|||||||
imageDimensions = representation.dimensions.cgSize
|
imageDimensions = representation.dimensions.cgSize
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
}/* else if let poll = media as? TelegramMediaPoll {
|
|
||||||
switch poll.kind {
|
|
||||||
case .poll:
|
|
||||||
titleString = strings.Conversation_PinnedPoll
|
|
||||||
case .quiz:
|
|
||||||
titleString = strings.Conversation_PinnedQuiz
|
|
||||||
}
|
}
|
||||||
}*/
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if isReplyThread {
|
if isReplyThread {
|
||||||
@ -462,7 +457,7 @@ final class ChatPinnedMessageTitlePanelNode: ChatTitleAccessoryPanelNode {
|
|||||||
|
|
||||||
@objc func closePressed() {
|
@objc func closePressed() {
|
||||||
if let interfaceInteraction = self.interfaceInteraction, let message = self.currentMessage {
|
if let interfaceInteraction = self.interfaceInteraction, let message = self.currentMessage {
|
||||||
interfaceInteraction.unpinMessage(message.message.id, true)
|
interfaceInteraction.unpinMessage(message.message.id, true, nil)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -100,7 +100,7 @@ final class ChatRecentActionsController: TelegramBaseController {
|
|||||||
return false
|
return false
|
||||||
}, unblockPeer: {
|
}, unblockPeer: {
|
||||||
}, pinMessage: { _, _ in
|
}, pinMessage: { _, _ in
|
||||||
}, unpinMessage: { _, _ in
|
}, unpinMessage: { _, _, _ in
|
||||||
}, unpinAllMessages: {
|
}, unpinAllMessages: {
|
||||||
}, openPinnedList: { _ in
|
}, openPinnedList: { _ in
|
||||||
}, shareAccountContact: {
|
}, shareAccountContact: {
|
||||||
|
@ -176,7 +176,7 @@ final class ChatRecordingPreviewInputPanelNode: ChatInputPanelNode {
|
|||||||
|> deliverOnMainQueue).start(next: { [weak self] status in
|
|> deliverOnMainQueue).start(next: { [weak self] status in
|
||||||
if let strongSelf = self {
|
if let strongSelf = self {
|
||||||
switch status.status {
|
switch status.status {
|
||||||
case .playing, .buffering(_, true, _):
|
case .playing, .buffering(_, true, _, _):
|
||||||
strongSelf.playButton.isHidden = true
|
strongSelf.playButton.isHidden = true
|
||||||
default:
|
default:
|
||||||
strongSelf.playButton.isHidden = false
|
strongSelf.playButton.isHidden = false
|
||||||
|
@ -214,7 +214,7 @@ final class ManagedAudioRecorderContext {
|
|||||||
}
|
}
|
||||||
return ActionDisposable {
|
return ActionDisposable {
|
||||||
}
|
}
|
||||||
}), playAndRecord: true, forceAudioToSpeaker: false, baseRate: 1.0, audioLevelPipe: ValuePipe<Float>(), updatedRate: {
|
}), playAndRecord: true, ambient: false, forceAudioToSpeaker: false, baseRate: 1.0, audioLevelPipe: ValuePipe<Float>(), updatedRate: {
|
||||||
}, audioPaused: {})
|
}, audioPaused: {})
|
||||||
self.toneRenderer = toneRenderer
|
self.toneRenderer = toneRenderer
|
||||||
|
|
||||||
|
@ -150,7 +150,7 @@ public final class MediaManagerImpl: NSObject, MediaManager {
|
|||||||
switch value.status.status {
|
switch value.status.status {
|
||||||
case .playing:
|
case .playing:
|
||||||
isPlaying = true
|
isPlaying = true
|
||||||
case .buffering(_, true, _):
|
case .buffering(_, true, _, _):
|
||||||
isPlaying = true
|
isPlaying = true
|
||||||
default:
|
default:
|
||||||
break
|
break
|
||||||
@ -230,7 +230,7 @@ public final class MediaManagerImpl: NSObject, MediaManager {
|
|||||||
updatedGlobalControlOptions.insert(.next)
|
updatedGlobalControlOptions.insert(.next)
|
||||||
updatedGlobalControlOptions.insert(.seek)
|
updatedGlobalControlOptions.insert(.seek)
|
||||||
switch state.status.status {
|
switch state.status.status {
|
||||||
case .playing, .buffering(_, true, _):
|
case .playing, .buffering(_, true, _, _):
|
||||||
updatedGlobalControlOptions.insert(.pause)
|
updatedGlobalControlOptions.insert(.pause)
|
||||||
default:
|
default:
|
||||||
updatedGlobalControlOptions.insert(.play)
|
updatedGlobalControlOptions.insert(.play)
|
||||||
@ -384,7 +384,7 @@ public final class MediaManagerImpl: NSObject, MediaManager {
|
|||||||
switch state.status.status {
|
switch state.status.status {
|
||||||
case .playing:
|
case .playing:
|
||||||
isPlaying = true
|
isPlaying = true
|
||||||
case let .buffering(_, whilePlaying, _):
|
case let .buffering(_, whilePlaying, _, _):
|
||||||
isPlaying = whilePlaying
|
isPlaying = whilePlaying
|
||||||
default:
|
default:
|
||||||
break
|
break
|
||||||
|
@ -73,7 +73,7 @@ func openChatMessageImpl(_ params: OpenChatMessageParams) -> Bool {
|
|||||||
params.context.liveLocationManager?.cancelLiveLocation(peerId: messageId?.peerId ?? params.message.id.peerId)
|
params.context.liveLocationManager?.cancelLiveLocation(peerId: messageId?.peerId ?? params.message.id.peerId)
|
||||||
}, openUrl: params.openUrl, openPeer: { peer in
|
}, openUrl: params.openUrl, openPeer: { peer in
|
||||||
params.openPeer(peer, .info)
|
params.openPeer(peer, .info)
|
||||||
})
|
}, showAll: params.modal)
|
||||||
let controller = LocationViewController(context: params.context, subject: params.message, params: controllerParams)
|
let controller = LocationViewController(context: params.context, subject: params.message, params: controllerParams)
|
||||||
controller.navigationPresentation = .modal
|
controller.navigationPresentation = .modal
|
||||||
params.navigationController?.pushViewController(controller)
|
params.navigationController?.pushViewController(controller)
|
||||||
|
@ -304,7 +304,7 @@ final class OverlayPlayerControlsNode: ASDisplayNode {
|
|||||||
isPaused = false
|
isPaused = false
|
||||||
case .paused:
|
case .paused:
|
||||||
isPaused = true
|
isPaused = true
|
||||||
case let .buffering(_, whilePlaying, _):
|
case let .buffering(_, whilePlaying, _, _):
|
||||||
isPaused = !whilePlaying
|
isPaused = !whilePlaying
|
||||||
}
|
}
|
||||||
if strongSelf.currentIsPaused != isPaused {
|
if strongSelf.currentIsPaused != isPaused {
|
||||||
|
@ -311,7 +311,7 @@ final class PeerInfoAvatarListItemNode: ASDisplayNode {
|
|||||||
var bufferingProgress: Float?
|
var bufferingProgress: Float?
|
||||||
if isMediaStreamable(resource: videoContent.fileReference.media.resource) {
|
if isMediaStreamable(resource: videoContent.fileReference.media.resource) {
|
||||||
if let playerStatus = self.playerStatus {
|
if let playerStatus = self.playerStatus {
|
||||||
if case let .buffering(_, _, progress) = playerStatus.status {
|
if case let .buffering(_, _, progress, _) = playerStatus.status {
|
||||||
bufferingProgress = progress
|
bufferingProgress = progress
|
||||||
} else if case .playing = playerStatus.status {
|
} else if case .playing = playerStatus.status {
|
||||||
bufferingProgress = nil
|
bufferingProgress = nil
|
||||||
|
@ -406,7 +406,7 @@ final class PeerInfoSelectionPanelNode: ASDisplayNode {
|
|||||||
return false
|
return false
|
||||||
}, unblockPeer: {
|
}, unblockPeer: {
|
||||||
}, pinMessage: { _, _ in
|
}, pinMessage: { _, _ in
|
||||||
}, unpinMessage: { _, _ in
|
}, unpinMessage: { _, _, _ in
|
||||||
}, unpinAllMessages: {
|
}, unpinAllMessages: {
|
||||||
}, openPinnedList: { _ in
|
}, openPinnedList: { _ in
|
||||||
}, shareAccountContact: {
|
}, shareAccountContact: {
|
||||||
@ -621,7 +621,8 @@ private final class PeerInfoInteraction {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private let enabledBioEntities: EnabledEntityTypes = [.url, .mention, .hashtag]
|
private let enabledPublicBioEntities: EnabledEntityTypes = [.allUrl, .mention, .hashtag]
|
||||||
|
private let enabledPrivateBioEntities: EnabledEntityTypes = [.internalUrl, .mention, .hashtag]
|
||||||
|
|
||||||
private enum SettingsSection: Int, CaseIterable {
|
private enum SettingsSection: Int, CaseIterable {
|
||||||
case edit
|
case edit
|
||||||
@ -927,11 +928,11 @@ private func infoItems(data: PeerInfoScreenData?, context: AccountContext, prese
|
|||||||
}
|
}
|
||||||
if let cachedData = data.cachedData as? CachedUserData {
|
if let cachedData = data.cachedData as? CachedUserData {
|
||||||
if user.isScam {
|
if user.isScam {
|
||||||
items[.peerInfo]!.append(PeerInfoScreenLabeledValueItem(id: 0, label: user.botInfo == nil ? presentationData.strings.Profile_About : presentationData.strings.Profile_BotInfo, text: user.botInfo != nil ? presentationData.strings.UserInfo_ScamBotWarning : presentationData.strings.UserInfo_ScamUserWarning, textColor: .primary, textBehavior: .multiLine(maxLines: 100, enabledEntities: user.botInfo != nil ? enabledBioEntities : []), action: nil, requestLayout: {
|
items[.peerInfo]!.append(PeerInfoScreenLabeledValueItem(id: 0, label: user.botInfo == nil ? presentationData.strings.Profile_About : presentationData.strings.Profile_BotInfo, text: user.botInfo != nil ? presentationData.strings.UserInfo_ScamBotWarning : presentationData.strings.UserInfo_ScamUserWarning, textColor: .primary, textBehavior: .multiLine(maxLines: 100, enabledEntities: user.botInfo != nil ? enabledPrivateBioEntities : []), action: nil, requestLayout: {
|
||||||
interaction.requestLayout()
|
interaction.requestLayout()
|
||||||
}))
|
}))
|
||||||
} else if let about = cachedData.about, !about.isEmpty {
|
} else if let about = cachedData.about, !about.isEmpty {
|
||||||
items[.peerInfo]!.append(PeerInfoScreenLabeledValueItem(id: 0, label: user.botInfo == nil ? presentationData.strings.Profile_About : presentationData.strings.Profile_BotInfo, text: about, textColor: .primary, textBehavior: .multiLine(maxLines: 100, enabledEntities: enabledBioEntities), action: nil, longTapAction: bioContextAction, linkItemAction: bioLinkAction, requestLayout: {
|
items[.peerInfo]!.append(PeerInfoScreenLabeledValueItem(id: 0, label: user.botInfo == nil ? presentationData.strings.Profile_About : presentationData.strings.Profile_BotInfo, text: about, textColor: .primary, textBehavior: .multiLine(maxLines: 100, enabledEntities: enabledPrivateBioEntities), action: nil, longTapAction: bioContextAction, linkItemAction: bioLinkAction, requestLayout: {
|
||||||
interaction.requestLayout()
|
interaction.requestLayout()
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
@ -1015,11 +1016,11 @@ private func infoItems(data: PeerInfoScreenData?, context: AccountContext, prese
|
|||||||
}
|
}
|
||||||
if let cachedData = data.cachedData as? CachedChannelData {
|
if let cachedData = data.cachedData as? CachedChannelData {
|
||||||
if channel.isScam {
|
if channel.isScam {
|
||||||
items[.peerInfo]!.append(PeerInfoScreenLabeledValueItem(id: ItemAbout, label: presentationData.strings.Channel_AboutItem, text: presentationData.strings.GroupInfo_ScamGroupWarning, textColor: .primary, textBehavior: .multiLine(maxLines: 100, enabledEntities: enabledBioEntities), action: nil, requestLayout: {
|
items[.peerInfo]!.append(PeerInfoScreenLabeledValueItem(id: ItemAbout, label: presentationData.strings.Channel_AboutItem, text: presentationData.strings.GroupInfo_ScamGroupWarning, textColor: .primary, textBehavior: .multiLine(maxLines: 100, enabledEntities: enabledPublicBioEntities), action: nil, requestLayout: {
|
||||||
interaction.requestLayout()
|
interaction.requestLayout()
|
||||||
}))
|
}))
|
||||||
} else if let about = cachedData.about, !about.isEmpty {
|
} else if let about = cachedData.about, !about.isEmpty {
|
||||||
items[.peerInfo]!.append(PeerInfoScreenLabeledValueItem(id: ItemAbout, label: presentationData.strings.Channel_AboutItem, text: about, textColor: .primary, textBehavior: .multiLine(maxLines: 100, enabledEntities: enabledBioEntities), action: nil, longTapAction: bioContextAction, linkItemAction: bioLinkAction, requestLayout: {
|
items[.peerInfo]!.append(PeerInfoScreenLabeledValueItem(id: ItemAbout, label: presentationData.strings.Channel_AboutItem, text: about, textColor: .primary, textBehavior: .multiLine(maxLines: 100, enabledEntities: enabledPublicBioEntities), action: nil, longTapAction: bioContextAction, linkItemAction: bioLinkAction, requestLayout: {
|
||||||
interaction.requestLayout()
|
interaction.requestLayout()
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
@ -1051,11 +1052,11 @@ private func infoItems(data: PeerInfoScreenData?, context: AccountContext, prese
|
|||||||
} else if let group = data.peer as? TelegramGroup {
|
} else if let group = data.peer as? TelegramGroup {
|
||||||
if let cachedData = data.cachedData as? CachedGroupData {
|
if let cachedData = data.cachedData as? CachedGroupData {
|
||||||
if group.isScam {
|
if group.isScam {
|
||||||
items[.peerInfo]!.append(PeerInfoScreenLabeledValueItem(id: 0, label: presentationData.strings.PeerInfo_GroupAboutItem, text: presentationData.strings.GroupInfo_ScamGroupWarning, textColor: .primary, textBehavior: .multiLine(maxLines: 100, enabledEntities: enabledBioEntities), action: nil, requestLayout: {
|
items[.peerInfo]!.append(PeerInfoScreenLabeledValueItem(id: 0, label: presentationData.strings.PeerInfo_GroupAboutItem, text: presentationData.strings.GroupInfo_ScamGroupWarning, textColor: .primary, textBehavior: .multiLine(maxLines: 100, enabledEntities: enabledPublicBioEntities), action: nil, requestLayout: {
|
||||||
interaction.requestLayout()
|
interaction.requestLayout()
|
||||||
}))
|
}))
|
||||||
} else if let about = cachedData.about, !about.isEmpty {
|
} else if let about = cachedData.about, !about.isEmpty {
|
||||||
items[.peerInfo]!.append(PeerInfoScreenLabeledValueItem(id: 0, label: presentationData.strings.PeerInfo_GroupAboutItem, text: about, textColor: .primary, textBehavior: .multiLine(maxLines: 100, enabledEntities: enabledBioEntities), action: nil, longTapAction: bioContextAction, linkItemAction: bioLinkAction, requestLayout: {
|
items[.peerInfo]!.append(PeerInfoScreenLabeledValueItem(id: 0, label: presentationData.strings.PeerInfo_GroupAboutItem, text: about, textColor: .primary, textBehavior: .multiLine(maxLines: 100, enabledEntities: enabledPublicBioEntities), action: nil, longTapAction: bioContextAction, linkItemAction: bioLinkAction, requestLayout: {
|
||||||
interaction.requestLayout()
|
interaction.requestLayout()
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
@ -14,7 +14,7 @@ final class GenericEmbedImplementation: WebEmbedImplementation {
|
|||||||
|
|
||||||
init(url: String) {
|
init(url: String) {
|
||||||
self.url = url
|
self.url = url
|
||||||
self.status = MediaPlayerStatus(generationTimestamp: 0.0, duration: 0.0, dimensions: CGSize(), timestamp: 0.0, baseRate: 1.0, seekId: 0, status: .buffering(initial: true, whilePlaying: true, progress: 0.0), soundEnabled: true)
|
self.status = MediaPlayerStatus(generationTimestamp: 0.0, duration: 0.0, dimensions: CGSize(), timestamp: 0.0, baseRate: 1.0, seekId: 0, status: .buffering(initial: true, whilePlaying: true, progress: 0.0, display: true), soundEnabled: true)
|
||||||
}
|
}
|
||||||
|
|
||||||
func setup(_ webView: WKWebView, userContentController: WKUserContentController, evaluateJavaScript: @escaping (String, ((Any?) -> Void)?) -> Void, updateStatus: @escaping (MediaPlayerStatus) -> Void, onPlaybackStarted: @escaping () -> Void) {
|
func setup(_ webView: WKWebView, userContentController: WKUserContentController, evaluateJavaScript: @escaping (String, ((Any?) -> Void)?) -> Void, updateStatus: @escaping (MediaPlayerStatus) -> Void, onPlaybackStarted: @escaping () -> Void) {
|
||||||
|
@ -451,4 +451,7 @@ private final class NativeVideoContentNode: ASDisplayNode, UniversalVideoContent
|
|||||||
self.postbox.mediaBox.cancelInteractiveResourceFetch(self.fileReference.media.resource)
|
self.postbox.mediaBox.cancelInteractiveResourceFetch(self.fileReference.media.resource)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func notifyPlaybackControlsHidden(_ hidden: Bool) {
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -37,7 +37,7 @@ final class PictureInPictureVideoControlsNode: ASDisplayNode {
|
|||||||
case .playing:
|
case .playing:
|
||||||
self.playButton.isHidden = true
|
self.playButton.isHidden = true
|
||||||
self.pauseButton.isHidden = false
|
self.pauseButton.isHidden = false
|
||||||
case let .buffering(_, whilePlaying, _):
|
case let .buffering(_, whilePlaying, _, _):
|
||||||
if whilePlaying {
|
if whilePlaying {
|
||||||
self.playButton.isHidden = true
|
self.playButton.isHidden = true
|
||||||
self.pauseButton.isHidden = false
|
self.pauseButton.isHidden = false
|
||||||
|
@ -315,7 +315,7 @@ private final class PlatformVideoContentNode: ASDisplayNode, UniversalVideoConte
|
|||||||
self.isBuffering = false
|
self.isBuffering = false
|
||||||
}
|
}
|
||||||
if self.isBuffering {
|
if self.isBuffering {
|
||||||
status = .buffering(initial: false, whilePlaying: isPlaying, progress: 0.0)
|
status = .buffering(initial: false, whilePlaying: isPlaying, progress: 0.0, display: true)
|
||||||
} else {
|
} else {
|
||||||
status = isPlaying ? .playing : .paused
|
status = isPlaying ? .playing : .paused
|
||||||
}
|
}
|
||||||
@ -326,7 +326,7 @@ private final class PlatformVideoContentNode: ASDisplayNode, UniversalVideoConte
|
|||||||
let status: MediaPlayerPlaybackStatus
|
let status: MediaPlayerPlaybackStatus
|
||||||
self.isBuffering = true
|
self.isBuffering = true
|
||||||
if self.isBuffering {
|
if self.isBuffering {
|
||||||
status = .buffering(initial: false, whilePlaying: isPlaying, progress: 0.0)
|
status = .buffering(initial: false, whilePlaying: isPlaying, progress: 0.0, display: true)
|
||||||
} else {
|
} else {
|
||||||
status = isPlaying ? .playing : .paused
|
status = isPlaying ? .playing : .paused
|
||||||
}
|
}
|
||||||
@ -337,7 +337,7 @@ private final class PlatformVideoContentNode: ASDisplayNode, UniversalVideoConte
|
|||||||
let status: MediaPlayerPlaybackStatus
|
let status: MediaPlayerPlaybackStatus
|
||||||
self.isBuffering = false
|
self.isBuffering = false
|
||||||
if self.isBuffering {
|
if self.isBuffering {
|
||||||
status = .buffering(initial: false, whilePlaying: isPlaying, progress: 0.0)
|
status = .buffering(initial: false, whilePlaying: isPlaying, progress: 0.0, display: true)
|
||||||
} else {
|
} else {
|
||||||
status = isPlaying ? .playing : .paused
|
status = isPlaying ? .playing : .paused
|
||||||
}
|
}
|
||||||
@ -381,7 +381,7 @@ private final class PlatformVideoContentNode: ASDisplayNode, UniversalVideoConte
|
|||||||
func play() {
|
func play() {
|
||||||
assert(Queue.mainQueue().isCurrent())
|
assert(Queue.mainQueue().isCurrent())
|
||||||
if !self.initializedStatus {
|
if !self.initializedStatus {
|
||||||
self._status.set(MediaPlayerStatus(generationTimestamp: 0.0, duration: Double(self.approximateDuration), dimensions: CGSize(), timestamp: 0.0, baseRate: 1.0, seekId: 0, status: .buffering(initial: true, whilePlaying: true, progress: 0.0), soundEnabled: true))
|
self._status.set(MediaPlayerStatus(generationTimestamp: 0.0, duration: Double(self.approximateDuration), dimensions: CGSize(), timestamp: 0.0, baseRate: 1.0, seekId: 0, status: .buffering(initial: true, whilePlaying: true, progress: 0.0, display: true), soundEnabled: true))
|
||||||
}
|
}
|
||||||
if !self.hasAudioSession {
|
if !self.hasAudioSession {
|
||||||
self.audioSessionDisposable.set(self.audioSessionManager.push(audioSessionType: .play, activate: { [weak self] _ in
|
self.audioSessionDisposable.set(self.audioSessionManager.push(audioSessionType: .play, activate: { [weak self] _ in
|
||||||
@ -448,4 +448,7 @@ private final class PlatformVideoContentNode: ASDisplayNode, UniversalVideoConte
|
|||||||
|
|
||||||
func fetchControl(_ control: UniversalVideoNodeFetchControl) {
|
func fetchControl(_ control: UniversalVideoNodeFetchControl) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func notifyPlaybackControlsHidden(_ hidden: Bool) {
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -44,7 +44,7 @@ private final class SystemVideoContentNode: ASDisplayNode, UniversalVideoContent
|
|||||||
private let playbackCompletedListeners = Bag<() -> Void>()
|
private let playbackCompletedListeners = Bag<() -> Void>()
|
||||||
|
|
||||||
private var initializedStatus = false
|
private var initializedStatus = false
|
||||||
private var statusValue = MediaPlayerStatus(generationTimestamp: 0.0, duration: 0.0, dimensions: CGSize(), timestamp: 0.0, baseRate: 1.0, seekId: 0, status: .buffering(initial: true, whilePlaying: false, progress: 0.0), soundEnabled: true)
|
private var statusValue = MediaPlayerStatus(generationTimestamp: 0.0, duration: 0.0, dimensions: CGSize(), timestamp: 0.0, baseRate: 1.0, seekId: 0, status: .buffering(initial: true, whilePlaying: false, progress: 0.0, display: true), soundEnabled: true)
|
||||||
private var isBuffering = true
|
private var isBuffering = true
|
||||||
private let _status = ValuePromise<MediaPlayerStatus>()
|
private let _status = ValuePromise<MediaPlayerStatus>()
|
||||||
var status: Signal<MediaPlayerStatus, NoError> {
|
var status: Signal<MediaPlayerStatus, NoError> {
|
||||||
@ -169,7 +169,7 @@ private final class SystemVideoContentNode: ASDisplayNode, UniversalVideoContent
|
|||||||
let isPlaying = !self.player.rate.isZero
|
let isPlaying = !self.player.rate.isZero
|
||||||
let status: MediaPlayerPlaybackStatus
|
let status: MediaPlayerPlaybackStatus
|
||||||
if self.isBuffering {
|
if self.isBuffering {
|
||||||
status = .buffering(initial: false, whilePlaying: isPlaying, progress: 0.0)
|
status = .buffering(initial: false, whilePlaying: isPlaying, progress: 0.0, display: true)
|
||||||
} else {
|
} else {
|
||||||
status = isPlaying ? .playing : .paused
|
status = isPlaying ? .playing : .paused
|
||||||
}
|
}
|
||||||
@ -180,7 +180,7 @@ private final class SystemVideoContentNode: ASDisplayNode, UniversalVideoContent
|
|||||||
let status: MediaPlayerPlaybackStatus
|
let status: MediaPlayerPlaybackStatus
|
||||||
self.isBuffering = true
|
self.isBuffering = true
|
||||||
if self.isBuffering {
|
if self.isBuffering {
|
||||||
status = .buffering(initial: false, whilePlaying: isPlaying, progress: 0.0)
|
status = .buffering(initial: false, whilePlaying: isPlaying, progress: 0.0, display: true)
|
||||||
} else {
|
} else {
|
||||||
status = isPlaying ? .playing : .paused
|
status = isPlaying ? .playing : .paused
|
||||||
}
|
}
|
||||||
@ -191,7 +191,7 @@ private final class SystemVideoContentNode: ASDisplayNode, UniversalVideoContent
|
|||||||
let status: MediaPlayerPlaybackStatus
|
let status: MediaPlayerPlaybackStatus
|
||||||
self.isBuffering = false
|
self.isBuffering = false
|
||||||
if self.isBuffering {
|
if self.isBuffering {
|
||||||
status = .buffering(initial: false, whilePlaying: isPlaying, progress: 0.0)
|
status = .buffering(initial: false, whilePlaying: isPlaying, progress: 0.0, display: true)
|
||||||
} else {
|
} else {
|
||||||
status = isPlaying ? .playing : .paused
|
status = isPlaying ? .playing : .paused
|
||||||
}
|
}
|
||||||
@ -219,7 +219,7 @@ private final class SystemVideoContentNode: ASDisplayNode, UniversalVideoContent
|
|||||||
func play() {
|
func play() {
|
||||||
assert(Queue.mainQueue().isCurrent())
|
assert(Queue.mainQueue().isCurrent())
|
||||||
if !self.initializedStatus {
|
if !self.initializedStatus {
|
||||||
self._status.set(MediaPlayerStatus(generationTimestamp: 0.0, duration: Double(self.approximateDuration), dimensions: CGSize(), timestamp: 0.0, baseRate: 1.0, seekId: self.seekId, status: .buffering(initial: true, whilePlaying: true, progress: 0.0), soundEnabled: true))
|
self._status.set(MediaPlayerStatus(generationTimestamp: 0.0, duration: Double(self.approximateDuration), dimensions: CGSize(), timestamp: 0.0, baseRate: 1.0, seekId: self.seekId, status: .buffering(initial: true, whilePlaying: true, progress: 0.0, display: true), soundEnabled: true))
|
||||||
}
|
}
|
||||||
if !self.hasAudioSession {
|
if !self.hasAudioSession {
|
||||||
self.audioSessionDisposable.set(self.audioSessionManager.push(audioSessionType: .play, activate: { [weak self] _ in
|
self.audioSessionDisposable.set(self.audioSessionManager.push(audioSessionType: .play, activate: { [weak self] _ in
|
||||||
@ -287,5 +287,8 @@ private final class SystemVideoContentNode: ASDisplayNode, UniversalVideoContent
|
|||||||
|
|
||||||
func fetchControl(_ control: UniversalVideoNodeFetchControl) {
|
func fetchControl(_ control: UniversalVideoNodeFetchControl) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func notifyPlaybackControlsHidden(_ hidden: Bool) {
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -20,7 +20,7 @@ final class TwitchEmbedImplementation: WebEmbedImplementation {
|
|||||||
|
|
||||||
init(url: String) {
|
init(url: String) {
|
||||||
self.url = url
|
self.url = url
|
||||||
self.status = MediaPlayerStatus(generationTimestamp: 0.0, duration: 0.0, dimensions: CGSize(), timestamp: 0.0, baseRate: 1.0, seekId: 0, status: .buffering(initial: true, whilePlaying: true, progress: 0.0), soundEnabled: true)
|
self.status = MediaPlayerStatus(generationTimestamp: 0.0, duration: 0.0, dimensions: CGSize(), timestamp: 0.0, baseRate: 1.0, seekId: 0, status: .buffering(initial: true, whilePlaying: true, progress: 0.0, display: true), soundEnabled: true)
|
||||||
}
|
}
|
||||||
|
|
||||||
func setup(_ webView: WKWebView, userContentController: WKUserContentController, evaluateJavaScript: @escaping (String, ((Any?) -> Void)?) -> Void, updateStatus: @escaping (MediaPlayerStatus) -> Void, onPlaybackStarted: @escaping () -> Void) {
|
func setup(_ webView: WKWebView, userContentController: WKUserContentController, evaluateJavaScript: @escaping (String, ((Any?) -> Void)?) -> Void, updateStatus: @escaping (MediaPlayerStatus) -> Void, onPlaybackStarted: @escaping () -> Void) {
|
||||||
|
@ -97,7 +97,7 @@ final class VimeoEmbedImplementation: WebEmbedImplementation {
|
|||||||
init(videoId: String, timestamp: Int = 0) {
|
init(videoId: String, timestamp: Int = 0) {
|
||||||
self.videoId = videoId
|
self.videoId = videoId
|
||||||
self.timestamp = timestamp
|
self.timestamp = timestamp
|
||||||
self.status = MediaPlayerStatus(generationTimestamp: 0.0, duration: 0.0, dimensions: CGSize(), timestamp: Double(timestamp), baseRate: 1.0, seekId: 0, status: .buffering(initial: true, whilePlaying: true, progress: 0.0), soundEnabled: true)
|
self.status = MediaPlayerStatus(generationTimestamp: 0.0, duration: 0.0, dimensions: CGSize(), timestamp: Double(timestamp), baseRate: 1.0, seekId: 0, status: .buffering(initial: true, whilePlaying: true, progress: 0.0, display: true), soundEnabled: true)
|
||||||
}
|
}
|
||||||
|
|
||||||
func setup(_ webView: WKWebView, userContentController: WKUserContentController, evaluateJavaScript: @escaping (String, ((Any?) -> Void)?) -> Void, updateStatus: @escaping (MediaPlayerStatus) -> Void, onPlaybackStarted: @escaping () -> Void) {
|
func setup(_ webView: WKWebView, userContentController: WKUserContentController, evaluateJavaScript: @escaping (String, ((Any?) -> Void)?) -> Void, updateStatus: @escaping (MediaPlayerStatus) -> Void, onPlaybackStarted: @escaping () -> Void) {
|
||||||
@ -221,7 +221,7 @@ final class VimeoEmbedImplementation: WebEmbedImplementation {
|
|||||||
playbackStatus = .paused
|
playbackStatus = .paused
|
||||||
newTimestamp = 0.0
|
newTimestamp = 0.0
|
||||||
default:
|
default:
|
||||||
playbackStatus = .buffering(initial: true, whilePlaying: false, progress: 0.0)
|
playbackStatus = .buffering(initial: true, whilePlaying: false, progress: 0.0, display: true)
|
||||||
}
|
}
|
||||||
|
|
||||||
self.status = MediaPlayerStatus(generationTimestamp: self.status.generationTimestamp, duration: Double(duration), dimensions: self.status.dimensions, timestamp: newTimestamp, baseRate: 1.0, seekId: self.status.seekId, status: playbackStatus, soundEnabled: true)
|
self.status = MediaPlayerStatus(generationTimestamp: self.status.generationTimestamp, duration: Double(duration), dimensions: self.status.dimensions, timestamp: newTimestamp, baseRate: 1.0, seekId: self.status.seekId, status: playbackStatus, soundEnabled: true)
|
||||||
|
@ -211,4 +211,10 @@ final class WebEmbedPlayerNode: ASDisplayNode, WKNavigationDelegate {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func notifyPlaybackControlsHidden(_ hidden: Bool) {
|
||||||
|
if impl is YoutubeEmbedImplementation {
|
||||||
|
self.webView.isUserInteractionEnabled = !hidden
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -184,4 +184,8 @@ final class WebEmbedVideoContentNode: ASDisplayNode, UniversalVideoContentNode {
|
|||||||
|
|
||||||
func fetchControl(_ control: UniversalVideoNodeFetchControl) {
|
func fetchControl(_ control: UniversalVideoNodeFetchControl) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func notifyPlaybackControlsHidden(_ hidden: Bool) {
|
||||||
|
self.playerNode.notifyPlaybackControlsHidden(hidden)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -117,11 +117,11 @@ final class YoutubeEmbedImplementation: WebEmbedImplementation {
|
|||||||
|
|
||||||
init(videoId: String, timestamp: Int = 0) {
|
init(videoId: String, timestamp: Int = 0) {
|
||||||
self.videoId = videoId
|
self.videoId = videoId
|
||||||
self.timestamp = 0
|
self.timestamp = timestamp
|
||||||
if self.timestamp > 0 {
|
if self.timestamp > 0 {
|
||||||
self.ignoreEarlierTimestamps = true
|
self.ignoreEarlierTimestamps = true
|
||||||
}
|
}
|
||||||
self.status = MediaPlayerStatus(generationTimestamp: 0.0, duration: 0.0, dimensions: CGSize(), timestamp: Double(timestamp), baseRate: 1.0, seekId: 0, status: .buffering(initial: true, whilePlaying: true, progress: 0.0), soundEnabled: true)
|
self.status = MediaPlayerStatus(generationTimestamp: 0.0, duration: 0.0, dimensions: CGSize(), timestamp: Double(timestamp), baseRate: 1.0, seekId: 0, status: .buffering(initial: true, whilePlaying: true, progress: 0.0, display: true), soundEnabled: true)
|
||||||
|
|
||||||
self.benchmarkStartTime = CFAbsoluteTimeGetCurrent()
|
self.benchmarkStartTime = CFAbsoluteTimeGetCurrent()
|
||||||
}
|
}
|
||||||
@ -286,16 +286,16 @@ final class YoutubeEmbedImplementation: WebEmbedImplementation {
|
|||||||
playbackStatus = .paused
|
playbackStatus = .paused
|
||||||
newTimestamp = 0.0
|
newTimestamp = 0.0
|
||||||
} else {
|
} else {
|
||||||
playbackStatus = .buffering(initial: false, whilePlaying: true, progress: 0.0)
|
playbackStatus = .buffering(initial: false, whilePlaying: true, progress: 0.0, display: false)
|
||||||
}
|
}
|
||||||
case 1:
|
case 1:
|
||||||
playbackStatus = .playing
|
playbackStatus = .playing
|
||||||
case 2:
|
case 2:
|
||||||
playbackStatus = .paused
|
playbackStatus = .paused
|
||||||
case 3:
|
case 3:
|
||||||
playbackStatus = .buffering(initial: false, whilePlaying: true, progress: 0.0)
|
playbackStatus = .buffering(initial: false, whilePlaying: true, progress: 0.0, display: false)
|
||||||
default:
|
default:
|
||||||
playbackStatus = .buffering(initial: true, whilePlaying: false, progress: 0.0)
|
playbackStatus = .buffering(initial: true, whilePlaying: false, progress: 0.0, display: false)
|
||||||
}
|
}
|
||||||
|
|
||||||
if case .playing = playbackStatus, !self.started {
|
if case .playing = playbackStatus, !self.started {
|
||||||
|
@ -85,12 +85,13 @@ public struct EnabledEntityTypes: OptionSet {
|
|||||||
public static let command = EnabledEntityTypes(rawValue: 1 << 0)
|
public static let command = EnabledEntityTypes(rawValue: 1 << 0)
|
||||||
public static let mention = EnabledEntityTypes(rawValue: 1 << 1)
|
public static let mention = EnabledEntityTypes(rawValue: 1 << 1)
|
||||||
public static let hashtag = EnabledEntityTypes(rawValue: 1 << 2)
|
public static let hashtag = EnabledEntityTypes(rawValue: 1 << 2)
|
||||||
public static let url = EnabledEntityTypes(rawValue: 1 << 3)
|
public static let allUrl = EnabledEntityTypes(rawValue: 1 << 3)
|
||||||
public static let phoneNumber = EnabledEntityTypes(rawValue: 1 << 4)
|
public static let phoneNumber = EnabledEntityTypes(rawValue: 1 << 4)
|
||||||
public static let timecode = EnabledEntityTypes(rawValue: 1 << 5)
|
public static let timecode = EnabledEntityTypes(rawValue: 1 << 5)
|
||||||
public static let external = EnabledEntityTypes(rawValue: 1 << 6)
|
public static let external = EnabledEntityTypes(rawValue: 1 << 6)
|
||||||
|
public static let internalUrl = EnabledEntityTypes(rawValue: 1 << 7)
|
||||||
|
|
||||||
public static let all: EnabledEntityTypes = [.command, .mention, .hashtag, .url, .phoneNumber]
|
public static let all: EnabledEntityTypes = [.command, .mention, .hashtag, .allUrl, .phoneNumber]
|
||||||
}
|
}
|
||||||
|
|
||||||
private func commitEntity(_ utf16: String.UTF16View, _ type: CurrentEntityType, _ range: Range<String.UTF16View.Index>, _ enabledTypes: EnabledEntityTypes, _ entities: inout [MessageTextEntity], mediaDuration: Double? = nil) {
|
private func commitEntity(_ utf16: String.UTF16View, _ type: CurrentEntityType, _ range: Range<String.UTF16View.Index>, _ enabledTypes: EnabledEntityTypes, _ entities: inout [MessageTextEntity], mediaDuration: Double? = nil) {
|
||||||
@ -160,11 +161,11 @@ public func generateTextEntities(_ text: String, enabledTypes: EnabledEntityType
|
|||||||
let utf16 = text.utf16
|
let utf16 = text.utf16
|
||||||
|
|
||||||
var detector: NSDataDetector?
|
var detector: NSDataDetector?
|
||||||
if enabledTypes.contains(.phoneNumber) && enabledTypes.contains(.url) {
|
if enabledTypes.contains(.phoneNumber) && (enabledTypes.contains(.allUrl) || enabledTypes.contains(.internalUrl)) {
|
||||||
detector = dataAndPhoneNumberDetector
|
detector = dataAndPhoneNumberDetector
|
||||||
} else if enabledTypes.contains(.phoneNumber) {
|
} else if enabledTypes.contains(.phoneNumber) {
|
||||||
detector = phoneNumberDetector
|
detector = phoneNumberDetector
|
||||||
} else if enabledTypes.contains(.url) {
|
} else if enabledTypes.contains(.allUrl) || enabledTypes.contains(.internalUrl) {
|
||||||
detector = dataDetector
|
detector = dataDetector
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -179,6 +180,21 @@ public func generateTextEntities(_ text: String, enabledTypes: EnabledEntityType
|
|||||||
if let lowerBound = lowerBound, let upperBound = upperBound {
|
if let lowerBound = lowerBound, let upperBound = upperBound {
|
||||||
let type: MessageTextEntityType
|
let type: MessageTextEntityType
|
||||||
if result.resultType == NSTextCheckingResult.CheckingType.link {
|
if result.resultType == NSTextCheckingResult.CheckingType.link {
|
||||||
|
if !enabledTypes.contains(.allUrl) && enabledTypes.contains(.internalUrl) {
|
||||||
|
guard let url = result.url else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if url.scheme != "tg" {
|
||||||
|
guard let host = url.host?.lowercased() else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if host == "telegram.org" || host == "t.me" {
|
||||||
|
} else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
type = .Url
|
type = .Url
|
||||||
} else {
|
} else {
|
||||||
type = .PhoneNumber
|
type = .PhoneNumber
|
||||||
|
@ -198,7 +198,7 @@ final class WebSearchVideoGalleryItemNode: ZoomableContentGalleryItemNode {
|
|||||||
switch value.status {
|
switch value.status {
|
||||||
case .playing:
|
case .playing:
|
||||||
isPaused = false
|
isPaused = false
|
||||||
case let .buffering(_, whilePlaying, _):
|
case let .buffering(_, whilePlaying, _, _):
|
||||||
initialBuffering = true
|
initialBuffering = true
|
||||||
isPaused = !whilePlaying
|
isPaused = !whilePlaying
|
||||||
var isStreaming = false
|
var isStreaming = false
|
||||||
|
Loading…
x
Reference in New Issue
Block a user