diff --git a/Images.xcassets/Chat List/LiveLocationPanelIcon.imageset/Contents.json b/Images.xcassets/Chat List/LiveLocationPanelIcon.imageset/Contents.json new file mode 100644 index 0000000000..cd67ea51af --- /dev/null +++ b/Images.xcassets/Chat List/LiveLocationPanelIcon.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "LiveLocationTitlePin@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "LiveLocationTitlePin@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Images.xcassets/Chat List/LiveLocationPanelIcon.imageset/LiveLocationTitlePin@2x.png b/Images.xcassets/Chat List/LiveLocationPanelIcon.imageset/LiveLocationTitlePin@2x.png new file mode 100644 index 0000000000..602e0beea6 Binary files /dev/null and b/Images.xcassets/Chat List/LiveLocationPanelIcon.imageset/LiveLocationTitlePin@2x.png differ diff --git a/Images.xcassets/Chat List/LiveLocationPanelIcon.imageset/LiveLocationTitlePin@3x.png b/Images.xcassets/Chat List/LiveLocationPanelIcon.imageset/LiveLocationTitlePin@3x.png new file mode 100644 index 0000000000..ad7f68c913 Binary files /dev/null and b/Images.xcassets/Chat List/LiveLocationPanelIcon.imageset/LiveLocationTitlePin@3x.png differ diff --git a/TelegramUI.xcodeproj/project.pbxproj b/TelegramUI.xcodeproj/project.pbxproj index 5f577ea00f..be0a3ce12b 100644 --- a/TelegramUI.xcodeproj/project.pbxproj +++ b/TelegramUI.xcodeproj/project.pbxproj @@ -86,6 +86,7 @@ D048EA8B1F4F298A00188713 /* InstantPageSettingsThemeItemNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D048EA8A1F4F298A00188713 /* InstantPageSettingsThemeItemNode.swift */; }; D048EA8D1F4F299A00188713 /* InstantPageSettingsSwitchItemNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D048EA8C1F4F299A00188713 /* InstantPageSettingsSwitchItemNode.swift */; }; D048EA8F1F4F2A9C00188713 /* InstantPageSettingsItemNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D048EA8E1F4F2A9C00188713 /* InstantPageSettingsItemNode.swift */; }; + D04B26EC20082EB50053A58C /* LocationBroadcastPanelWavesNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04B26EB20082EB50053A58C /* LocationBroadcastPanelWavesNode.swift */; }; D04B4D111EEA04D400711AF6 /* MapResources.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04B4D101EEA04D400711AF6 /* MapResources.swift */; }; D04B4D131EEA0A6500711AF6 /* ChatMessageMapBubbleContentNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04B4D121EEA0A6500711AF6 /* ChatMessageMapBubbleContentNode.swift */; }; D04B4D661EEA993A00711AF6 /* LegacyLocationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04B4D651EEA993A00711AF6 /* LegacyLocationController.swift */; }; @@ -137,6 +138,7 @@ D091C7A61F8ECEA300D7DE13 /* SettingsThemeWallpaperNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D091C7A51F8ECEA300D7DE13 /* SettingsThemeWallpaperNode.swift */; }; D09250041FE5363D003F693F /* ExperimentalSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = D09250031FE5363D003F693F /* ExperimentalSettings.swift */; }; D09250061FE5371D003F693F /* GlobalExperimentalSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = D09250051FE5371D003F693F /* GlobalExperimentalSettings.swift */; }; + D09394132007F5BB00997F31 /* LocationBroadcastNavigationAccessoryPanel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D09394122007F5BB00997F31 /* LocationBroadcastNavigationAccessoryPanel.swift */; }; D0943AF61FDAAE7E001522CC /* MultipleAvatarsNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0943AF51FDAAE7E001522CC /* MultipleAvatarsNode.swift */; }; D0943AFE1FDAE454001522CC /* ChatMultipleAvatarsNavigationNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0943AFD1FDAE454001522CC /* ChatMultipleAvatarsNavigationNode.swift */; }; D0943B001FDAE852001522CC /* ChatFeedNavigationInputPanelNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0943AFF1FDAE852001522CC /* ChatFeedNavigationInputPanelNode.swift */; }; @@ -1209,6 +1211,7 @@ D049EAE51E44AD5600A2CD3A /* ChatMediaInputMetaSectionItemNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatMediaInputMetaSectionItemNode.swift; sourceTree = ""; }; D049EAED1E44BB3200A2CD3A /* ChatListRecentPeersListItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatListRecentPeersListItem.swift; sourceTree = ""; }; D049EAF21E44DE2500A2CD3A /* AuthorizationSequenceController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AuthorizationSequenceController.swift; sourceTree = ""; }; + D04B26EB20082EB50053A58C /* LocationBroadcastPanelWavesNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationBroadcastPanelWavesNode.swift; sourceTree = ""; }; D04B4D101EEA04D400711AF6 /* MapResources.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MapResources.swift; sourceTree = ""; }; D04B4D121EEA0A6500711AF6 /* ChatMessageMapBubbleContentNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatMessageMapBubbleContentNode.swift; sourceTree = ""; }; D04B4D651EEA993A00711AF6 /* LegacyLocationController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LegacyLocationController.swift; sourceTree = ""; }; @@ -1423,6 +1426,7 @@ D091C7A51F8ECEA300D7DE13 /* SettingsThemeWallpaperNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsThemeWallpaperNode.swift; sourceTree = ""; }; D09250031FE5363D003F693F /* ExperimentalSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExperimentalSettings.swift; sourceTree = ""; }; D09250051FE5371D003F693F /* GlobalExperimentalSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlobalExperimentalSettings.swift; sourceTree = ""; }; + D09394122007F5BB00997F31 /* LocationBroadcastNavigationAccessoryPanel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationBroadcastNavigationAccessoryPanel.swift; sourceTree = ""; }; D0943AF51FDAAE7E001522CC /* MultipleAvatarsNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultipleAvatarsNode.swift; sourceTree = ""; }; D0943AFD1FDAE454001522CC /* ChatMultipleAvatarsNavigationNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatMultipleAvatarsNavigationNode.swift; sourceTree = ""; }; D0943AFF1FDAE852001522CC /* ChatFeedNavigationInputPanelNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatFeedNavigationInputPanelNode.swift; sourceTree = ""; }; @@ -2764,6 +2768,8 @@ D0736F2B1DF4DC2400F2C02A /* MediaNavigationAccessoryContainerNode.swift */, D0736F2D1DF4E54A00F2C02A /* MediaNavigationAccessoryHeaderNode.swift */, D0177B811DFAEA5400A5083A /* MediaNavigationAccessoryItemListNode.swift */, + D09394122007F5BB00997F31 /* LocationBroadcastNavigationAccessoryPanel.swift */, + D04B26EB20082EB50053A58C /* LocationBroadcastPanelWavesNode.swift */, ); name = "Telegram Controller"; sourceTree = ""; @@ -5114,6 +5120,7 @@ D0EC6CCF1EB9F58800EBF1C3 /* GeoLocation.swift in Sources */, D0EC6EA71EBA0FB000EBF1C3 /* BlockingQueue.cpp in Sources */, D0EC6CD01EB9F58800EBF1C3 /* PerformanceSpinner.swift in Sources */, + D09394132007F5BB00997F31 /* LocationBroadcastNavigationAccessoryPanel.swift in Sources */, D0471B5C1EFEB4F30074D609 /* BotPaymentFieldItemNode.swift in Sources */, D0C27B3D1F4B454800A4E170 /* InstantPagePlayableVideoNode.swift in Sources */, D0EC6CD11EB9F58800EBF1C3 /* UrlHandling.swift in Sources */, @@ -5167,6 +5174,7 @@ D0EC6FEF1EBA18E800EBF1C3 /* VoIPController.cpp in Sources */, D0EC6CED1EB9F58800EBF1C3 /* StringPluralization.swift in Sources */, D020A9DC1FEAE6E7008C66F7 /* OverlayPlayerControllerNode.swift in Sources */, + D04B26EC20082EB50053A58C /* LocationBroadcastPanelWavesNode.swift in Sources */, D0EC6FC61EBA135100EBF1C3 /* complex_fft.c in Sources */, D0EC6CEE1EB9F58800EBF1C3 /* InAppNotificationSettings.swift in Sources */, D0EC6CEF1EB9F58800EBF1C3 /* PresentationPasscodeSettings.swift in Sources */, diff --git a/TelegramUI.xcodeproj/xcuserdata/peter.xcuserdatad/xcschemes/xcschememanagement.plist b/TelegramUI.xcodeproj/xcuserdata/peter.xcuserdatad/xcschemes/xcschememanagement.plist index 4dab2d7cd2..2545a615e8 100644 --- a/TelegramUI.xcodeproj/xcuserdata/peter.xcuserdatad/xcschemes/xcschememanagement.plist +++ b/TelegramUI.xcodeproj/xcuserdata/peter.xcuserdatad/xcschemes/xcschememanagement.plist @@ -7,7 +7,7 @@ TelegramUI.xcscheme orderHint - 13 + 3 SuppressBuildableAutocreation diff --git a/TelegramUI/ChatController.swift b/TelegramUI/ChatController.swift index 6a0d929fc1..ed2f3e259d 100644 --- a/TelegramUI/ChatController.swift +++ b/TelegramUI/ChatController.swift @@ -149,10 +149,14 @@ public final class ChatController: TelegramController { self.messageId = messageId self.botStart = botStart + let locationBroadcastPanelSource: LocationBroadcastPanelSource + switch chatLocation { - case .peer: + case let .peer(peerId): + locationBroadcastPanelSource = .peer(peerId) self.chatLocationInfoData = .peer(Promise()) case .group: + locationBroadcastPanelSource = .none self.chatLocationInfoData = .group(Promise()) } @@ -166,7 +170,7 @@ public final class ChatController: TelegramController { if case .overlay = mode { enableMediaAccessoryPanel = false } - super.init(account: account, navigationBarTheme: NavigationBarTheme(rootControllerTheme: self.presentationData.theme), enableMediaAccessoryPanel: enableMediaAccessoryPanel) + super.init(account: account, navigationBarTheme: NavigationBarTheme(rootControllerTheme: self.presentationData.theme), enableMediaAccessoryPanel: enableMediaAccessoryPanel, locationBroadcastPanelSource: locationBroadcastPanelSource) self.navigationItem.backBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Back, style: .plain, target: nil, action: nil) diff --git a/TelegramUI/ChatListController.swift b/TelegramUI/ChatListController.swift index f2a8e71035..aac0214b26 100644 --- a/TelegramUI/ChatListController.swift +++ b/TelegramUI/ChatListController.swift @@ -39,7 +39,7 @@ public class ChatListController: TelegramController, UIViewControllerPreviewingD self.titleView = NetworkStatusTitleView(theme: self.presentationData.theme) - super.init(account: account, navigationBarTheme: NavigationBarTheme(rootControllerTheme: self.presentationData.theme), enableMediaAccessoryPanel: true) + super.init(account: account, navigationBarTheme: NavigationBarTheme(rootControllerTheme: self.presentationData.theme), enableMediaAccessoryPanel: true, locationBroadcastPanelSource: .summary) self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBar.style.style diff --git a/TelegramUI/HashtagSearchController.swift b/TelegramUI/HashtagSearchController.swift index 8afeb2ceeb..83009ca4da 100644 --- a/TelegramUI/HashtagSearchController.swift +++ b/TelegramUI/HashtagSearchController.swift @@ -22,7 +22,7 @@ final class HashtagSearchController: TelegramController { self.presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } - super.init(account: account, navigationBarTheme: NavigationBarTheme(rootControllerTheme: self.presentationData.theme), enableMediaAccessoryPanel: true) + super.init(account: account, navigationBarTheme: NavigationBarTheme(rootControllerTheme: self.presentationData.theme), enableMediaAccessoryPanel: true, locationBroadcastPanelSource: .none) self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBar.style.style diff --git a/TelegramUI/LiveLocationManager.swift b/TelegramUI/LiveLocationManager.swift index 899af109e4..b3c55d742f 100644 --- a/TelegramUI/LiveLocationManager.swift +++ b/TelegramUI/LiveLocationManager.swift @@ -213,4 +213,13 @@ public final class LiveLocationManager { self.pollingOnceValue = true } } + + func internalMessageForPeerId(_ peerId: PeerId) -> MessageId? { + for id in self.broadcastToMessageIds { + if id.peerId == peerId { + return id + } + } + return nil + } } diff --git a/TelegramUI/LiveLocationSummaryManager.swift b/TelegramUI/LiveLocationSummaryManager.swift index ed2d475d0f..98439f05f3 100644 --- a/TelegramUI/LiveLocationSummaryManager.swift +++ b/TelegramUI/LiveLocationSummaryManager.swift @@ -82,7 +82,7 @@ private final class LiveLocationPeerSummaryContext { private let peerId: PeerId private let becameEmpty: () -> Void - private var peers: [Peer] = [] { + private var peers: [Peer]? = nil { didSet { assert(self.queue.isCurrent()) @@ -106,7 +106,7 @@ private final class LiveLocationPeerSummaryContext { } } } - private var subscribers = Bag<([Peer]) -> Void>() + private var subscribers = Bag<([Peer]?) -> Void>() var isEmpty: Bool { return !self.isActive && self.subscribers.isEmpty @@ -126,7 +126,7 @@ private final class LiveLocationPeerSummaryContext { self.peerDisposable.dispose() } - func subscribe(_ f: @escaping ([Peer]) -> Void) -> Disposable { + func subscribe(_ f: @escaping ([Peer]?) -> Void) -> Disposable { let index = self.subscribers.add({ next in f(next) }) @@ -165,7 +165,7 @@ private final class LiveLocationPeerSummaryContext { })) } else { self.peerDisposable.set(nil) - self.peers = [] + self.peers = nil } } } @@ -195,21 +195,6 @@ final class LiveLocationSummaryManager { peerIds.insert(id.peerId) } - var removedPeerIds: [PeerId] = [] - for peerId in self.peerContexts.keys { - if !peerIds.contains(peerId) { - removedPeerIds.append(peerId) - } - } - - for peerId in removedPeerIds { - if let _ = self.peerContexts[peerId] { - self.peerContexts.removeValue(forKey: peerId) - } else { - assertionFailure() - } - } - for peerId in peerIds { if self.peerContexts[peerId] == nil { let context = LiveLocationPeerSummaryContext(queue: self.queue, accountPeerId: self.accountPeerId, viewTracker: self.viewTracker, peerId: peerId, becameEmpty: { [weak self] in @@ -232,7 +217,7 @@ final class LiveLocationSummaryManager { return self.globalContext.subscribe() } - func peersBroadcastingTo(peerId: PeerId) -> Signal<[Peer], NoError> { + func peersBroadcastingTo(peerId: PeerId) -> Signal<[Peer]?, NoError> { let queue = self.queue return Signal { [weak self] subscriber in let disposable = MetaDisposable() @@ -247,6 +232,7 @@ final class LiveLocationSummaryManager { strongSelf.peerContexts.removeValue(forKey: peerId) } }) + strongSelf.peerContexts[peerId] = context } disposable.set(context.subscribe({ next in diff --git a/TelegramUI/LocationBroadcastNavigationAccessoryPanel.swift b/TelegramUI/LocationBroadcastNavigationAccessoryPanel.swift new file mode 100644 index 0000000000..97846386b0 --- /dev/null +++ b/TelegramUI/LocationBroadcastNavigationAccessoryPanel.swift @@ -0,0 +1,184 @@ +import Foundation +import AsyncDisplayKit +import Display +import TelegramCore +import Postbox + +private let titleFont = Font.regular(12.0) +private let subtitleFont = Font.regular(10.0) + +enum LocationBroadcastNavigationAccessoryPanelMode { + case summary + case peer +} + +final class LocationBroadcastNavigationAccessoryPanel: ASDisplayNode { + private var theme: PresentationTheme + private var strings: PresentationStrings + + private let tapAction: () -> Void + private let close: () -> Void + + private let contentNode: ASDisplayNode + + private let iconNode: ASImageNode + private let wavesNode: LocationBroadcastPanelWavesNode + private let titleNode: TextNode + private let subtitleNode: TextNode + private let closeButton: HighlightableButtonNode + private let separatorNode: ASDisplayNode + + private var validLayout: CGSize? + private var peersAndMode: ([Peer], LocationBroadcastNavigationAccessoryPanelMode)? + + init(theme: PresentationTheme, strings: PresentationStrings, tapAction: @escaping () -> Void, close: @escaping () -> Void) { + self.theme = theme + self.strings = strings + + self.tapAction = tapAction + self.close = close + + self.contentNode = ASDisplayNode() + self.contentNode.backgroundColor = self.theme.rootController.navigationBar.backgroundColor + + self.iconNode = ASImageNode() + self.iconNode.isLayerBacked = true + self.iconNode.displayWithoutProcessing = true + self.iconNode.displaysAsynchronously = false + self.iconNode.image = PresentationResourcesRootController.navigationLiveLocationIcon(self.theme) + + self.wavesNode = LocationBroadcastPanelWavesNode(color: self.theme.rootController.navigationBar.accentTextColor) + + self.titleNode = TextNode() + self.titleNode.isLayerBacked = true + + self.subtitleNode = TextNode() + self.subtitleNode.isLayerBacked = true + + self.closeButton = HighlightableButtonNode() + self.closeButton.setImage(PresentationResourcesRootController.navigationPlayerCloseButton(self.theme), for: []) + self.closeButton.hitTestSlop = UIEdgeInsetsMake(-8.0, -8.0, -8.0, -8.0) + self.closeButton.displaysAsynchronously = false + + self.separatorNode = ASDisplayNode() + self.separatorNode.isLayerBacked = true + self.separatorNode.backgroundColor = theme.rootController.navigationBar.separatorColor + + super.init() + + self.addSubnode(self.contentNode) + + self.contentNode.addSubnode(self.iconNode) + self.contentNode.addSubnode(self.wavesNode) + self.contentNode.addSubnode(self.titleNode) + self.contentNode.addSubnode(self.subtitleNode) + self.contentNode.addSubnode(self.closeButton) + self.contentNode.addSubnode(self.separatorNode) + + self.closeButton.addTarget(self, action: #selector(self.closePressed), forControlEvents: .touchUpInside) + } + + override func didLoad() { + super.didLoad() + + let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:))) + self.view.addGestureRecognizer(tapRecognizer) + } + + func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) { + self.validLayout = size + + transition.updateFrame(node: self.contentNode, frame: CGRect(origin: CGPoint(), size: size)) + + let titleString = NSAttributedString(string: self.strings.Conversation_LiveLocation, font: titleFont, textColor: self.theme.rootController.navigationBar.primaryTextColor) + var subtitleString: NSAttributedString? + if let (peers, mode) = self.peersAndMode { + switch mode { + case .summary: + let text: String + if peers.count == 1 { + text = self.strings.DialogList_LiveLocationSharingTo(peers[0].displayTitle).0 + } else { + text = self.strings.DialogList_LiveLocationChatsCount(Int32(peers.count)) + } + subtitleString = NSAttributedString(string: text, font: subtitleFont, textColor: self.theme.rootController.navigationBar.secondaryTextColor) + case .peer: + if peers.count == 0 { + subtitleString = NSAttributedString(string: self.strings.Conversation_LiveLocationYou, font: subtitleFont, textColor: self.theme.rootController.navigationBar.accentTextColor) + } else { + let otherString: String + if peers.count == 1 { + otherString = peers[0].compactDisplayTitle + } else { + otherString = self.strings.Conversation_LiveLocationMembersCount(Int32(peers.count)) + } + let rawText = self.strings.Conversation_LiveLocationYouAnd(otherString).0.replacingOccurrences(of: "*", with: "**") + let body = MarkdownAttributeSet(font: subtitleFont, textColor: self.theme.rootController.navigationBar.secondaryTextColor) + let accent = MarkdownAttributeSet(font: subtitleFont, textColor: self.theme.rootController.navigationBar.accentTextColor) + subtitleString = parseMarkdownIntoAttributedString(rawText, attributes: MarkdownAttributes(body: body, bold: accent, link: body, linkAttribute: { _ in nil })) + } + + } + } + let makeTitleLayout = TextNode.asyncLayout(self.titleNode) + let makeSubtitleLayout = TextNode.asyncLayout(self.subtitleNode) + + let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: titleString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .middle, constrainedSize: CGSize(width: size.width - 80.0, height: 100.0), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + let (subtitleLayout, subtitleApply) = makeSubtitleLayout(TextNodeLayoutArguments(attributedString: subtitleString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .middle, constrainedSize: CGSize(width: size.width - 80.0, height: 100.0), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + + let _ = titleApply() + let _ = subtitleApply() + + let minimizedTitleOffset: CGFloat = subtitleString == nil ? 6.0 : 0.0 + + let minimizedTitleFrame = CGRect(origin: CGPoint(x: floor((size.width - titleLayout.size.width) / 2.0), y: 4.0 + minimizedTitleOffset), size: titleLayout.size) + let minimizedSubtitleFrame = CGRect(origin: CGPoint(x: floor((size.width - subtitleLayout.size.width) / 2.0), y: 20.0), size: subtitleLayout.size) + + if let image = self.iconNode.image { + transition.updateFrame(node: self.iconNode, frame: CGRect(origin: CGPoint(x: 7.0, y: 8.0), size: image.size)) + transition.updateFrame(node: self.wavesNode, frame: CGRect(origin: CGPoint(x: -2.0, y: -4.0), size: CGSize(width: 48.0, height: 48.0))) + } + + transition.updateFrame(node: self.titleNode, frame: minimizedTitleFrame) + transition.updateFrame(node: self.subtitleNode, frame: minimizedSubtitleFrame) + + let closeButtonSize = self.closeButton.measure(CGSize(width: 100.0, height: 100.0)) + transition.updateFrame(node: self.closeButton, frame: CGRect(origin: CGPoint(x: bounds.size.width - 18.0 - closeButtonSize.width, y: minimizedTitleFrame.minY + 8.0), size: closeButtonSize)) + + transition.updateFrame(node: self.separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: size.height - UIScreenPixel), size: CGSize(width: size.width, height: UIScreenPixel))) + } + + func update(peers: [Peer], mode: LocationBroadcastNavigationAccessoryPanelMode) { + self.peersAndMode = (peers, mode) + if let size = validLayout { + self.updateLayout(size: size, transition: .immediate) + } + } + + func animateIn(_ transition: ContainedViewLayoutTransition) { + self.clipsToBounds = true + let contentPosition = self.contentNode.layer.position + transition.animatePosition(node: self.contentNode, from: CGPoint(x: contentPosition.x, y: contentPosition.y - 37.0), completion: { [weak self] _ in + self?.clipsToBounds = false + }) + } + + func animateOut(_ transition: ContainedViewLayoutTransition, completion: @escaping () -> Void) { + self.clipsToBounds = true + let contentPosition = self.contentNode.layer.position + transition.animatePosition(node: self.contentNode, to: CGPoint(x: contentPosition.x, y: contentPosition.y - 37.0), removeOnCompletion: false, completion: { [weak self] _ in + self?.clipsToBounds = false + completion() + }) + } + + @objc func closePressed() { + self.close() + } + + @objc func tapGesture(_ recognizer: UITapGestureRecognizer) { + if case .ended = recognizer.state { + self.tapAction() + } + } +} diff --git a/TelegramUI/LocationBroadcastPanelWavesNode.swift b/TelegramUI/LocationBroadcastPanelWavesNode.swift new file mode 100644 index 0000000000..3b2d3a8f71 --- /dev/null +++ b/TelegramUI/LocationBroadcastPanelWavesNode.swift @@ -0,0 +1,129 @@ +import Foundation +import Display +import AsyncDisplayKit + +import LegacyComponents + +private final class LocationBroadcastPanelWavesNodeParams: NSObject { + let color: UIColor + let progress: CGFloat + + init(color: UIColor, progress: CGFloat) { + self.color = color + self.progress = progress + + super.init() + } +} + +private func degToRad(_ degrees: CGFloat) -> CGFloat { + return degrees * CGFloat.pi / 180.0 +} + +final class LocationBroadcastPanelWavesNode: ASDisplayNode { + private var color: UIColor + + private var effectiveProgress: CGFloat = 0.0 { + didSet { + self.setNeedsDisplay() + } + } + + init(color: UIColor) { + self.color = color + + super.init() + + self.isLayerBacked = true + self.isOpaque = false + } + + override func willEnterHierarchy() { + super.willEnterHierarchy() + + self.pop_removeAnimation(forKey: "indefiniteProgress") + + let animation = POPBasicAnimation() + animation.property = POPAnimatableProperty.property(withName: "progress", initializer: { property in + property?.readBlock = { node, values in + values?.pointee = (node as! LocationBroadcastPanelWavesNode).effectiveProgress + } + property?.writeBlock = { node, values in + (node as! LocationBroadcastPanelWavesNode).effectiveProgress = values!.pointee + } + property?.threshold = 0.01 + }) as! POPAnimatableProperty + animation.fromValue = CGFloat(0.0) as NSNumber + animation.toValue = CGFloat(1.0) as NSNumber + animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionLinear) + animation.duration = 2.5 + animation.repeatForever = true + self.pop_add(animation, forKey: "indefiniteProgress") + } + + override func didExitHierarchy() { + super.didExitHierarchy() + + self.pop_removeAnimation(forKey: "indefiniteProgress") + } + + override func drawParameters(forAsyncLayer layer: _ASDisplayLayer) -> NSObjectProtocol? { + let t = CACurrentMediaTime() + let value: CGFloat = CGFloat(t.truncatingRemainder(dividingBy: 2.0)) / 2.0 + return LocationBroadcastPanelWavesNodeParams(color: self.color, progress: value) + } + + @objc override class func draw(_ bounds: CGRect, withParameters parameters: Any?, isCancelled: () -> Bool, isRasterizing: Bool) { + let context = UIGraphicsGetCurrentContext()! + + if !isRasterizing { + context.setBlendMode(.copy) + context.setFillColor(UIColor.clear.cgColor) + context.fill(bounds) + } + + if let parameters = parameters as? LocationBroadcastPanelWavesNodeParams { + let center = CGPoint(x: bounds.width / 2.0, y: bounds.height / 2.0) + let length: CGFloat = 9.0 + + context.setFillColor(parameters.color.cgColor) + + let draw: (CGContext, CGFloat, Bool) -> Void = { context, pos, right in + let path = CGMutablePath() + + path.addArc(center: center, radius: length * pos + 7.0, startAngle: right ? degToRad(-26.0) : degToRad(154.0), endAngle: right ? degToRad(26.0) : degToRad(206.0), clockwise: false) + + let strokedArc = path.copy(strokingWithWidth: 1.65, lineCap: .round, lineJoin: .miter, miterLimit: 10.0) + + context.addPath(strokedArc) + + context.fillPath() + } + + let position = parameters.progress + var alpha = position / 0.5 + if alpha > 1.0 { + alpha = 2.0 - alpha + } + context.setAlpha(alpha * 0.7) + + draw(context, position, false) + draw(context, position, true) + + var progress = parameters.progress + 0.5 + if progress > 1.0 { + progress = progress - 1.0 + } + + let largerPos = progress + var largerAlpha = largerPos / 0.5 + if largerAlpha > 1.0 { + largerAlpha = 2.0 - largerAlpha + } + context.setAlpha(largerAlpha * 0.7) + + draw(context, largerPos, false) + draw(context, largerPos, true) + } + } +} diff --git a/TelegramUI/MediaNavigationAccessoryHeaderNode.swift b/TelegramUI/MediaNavigationAccessoryHeaderNode.swift index 33b646abad..ed6cf7a29f 100644 --- a/TelegramUI/MediaNavigationAccessoryHeaderNode.swift +++ b/TelegramUI/MediaNavigationAccessoryHeaderNode.swift @@ -5,8 +5,6 @@ import SwiftSignalKit private let titleFont = Font.regular(12.0) private let subtitleFont = Font.regular(10.0) -private let maximizedTitleFont = Font.bold(17.0) -private let maximizedSubtitleFont = Font.regular(12.0) final class MediaNavigationAccessoryHeaderNode: ASDisplayNode { static let minimizedHeight: CGFloat = 37.0 diff --git a/TelegramUI/OpenChatMessage.swift b/TelegramUI/OpenChatMessage.swift index 4c5da29d5d..cad58d98bd 100644 --- a/TelegramUI/OpenChatMessage.swift +++ b/TelegramUI/OpenChatMessage.swift @@ -78,7 +78,8 @@ func openChatMessage(account: Account, message: Message, reverseMessageGalleryOr present(legacyLocationController(message: message, mapMedia: mapMedia, account: account, openPeer: { peer in openPeer(peer, .info) }, sendLiveLocation: { coordinate, period in - + let outMessage: EnqueueMessage = .message(text: "", attributes: [], media: TelegramMediaMap(latitude: coordinate.latitude, longitude: coordinate.longitude, geoPlace: nil, venue: nil, liveBroadcastingTimeout: period), replyToMessageId: nil, localGroupingKey: nil) + let _ = enqueueMessages(account: account, peerId: message.id.peerId, messages: [outMessage]).start() }, stopLiveLocation: { account.telegramApplicationContext.liveLocationManager?.cancelLiveLocation(peerId: message.id.peerId) }), nil) diff --git a/TelegramUI/PeerMediaCollectionController.swift b/TelegramUI/PeerMediaCollectionController.swift index c6a08eeff3..1229afec10 100644 --- a/TelegramUI/PeerMediaCollectionController.swift +++ b/TelegramUI/PeerMediaCollectionController.swift @@ -44,7 +44,7 @@ public class PeerMediaCollectionController: TelegramController { self.presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } self.interfaceState = PeerMediaCollectionInterfaceState(theme: self.presentationData.theme, strings: self.presentationData.strings) - super.init(account: account, navigationBarTheme: NavigationBarTheme(rootControllerTheme: self.presentationData.theme).withUpdatedSeparatorColor(self.presentationData.theme.rootController.navigationBar.backgroundColor), enableMediaAccessoryPanel: true) + super.init(account: account, navigationBarTheme: NavigationBarTheme(rootControllerTheme: self.presentationData.theme).withUpdatedSeparatorColor(self.presentationData.theme.rootController.navigationBar.backgroundColor), enableMediaAccessoryPanel: true, locationBroadcastPanelSource: .none) self.title = self.presentationData.strings.SharedMedia_TitleAll diff --git a/TelegramUI/PresentationResourceKey.swift b/TelegramUI/PresentationResourceKey.swift index ce991cd53b..b30f1c2976 100644 --- a/TelegramUI/PresentationResourceKey.swift +++ b/TelegramUI/PresentationResourceKey.swift @@ -19,6 +19,8 @@ enum PresentationResourceKey: Int32 { case navigationSearchIcon case navigationPlayerCloseButton + case navigationLiveLocationIcon + case navigationPlayerPlayIcon case navigationPlayerPauseIcon case navigationPlayerMaximizedPlayIcon diff --git a/TelegramUI/PresentationResourcesRootController.swift b/TelegramUI/PresentationResourcesRootController.swift index 1b0fc7d2e3..afcdf2c6f0 100644 --- a/TelegramUI/PresentationResourcesRootController.swift +++ b/TelegramUI/PresentationResourcesRootController.swift @@ -112,6 +112,12 @@ struct PresentationResourcesRootController { }) } + static func navigationLiveLocationIcon(_ theme: PresentationTheme) -> UIImage? { + return theme.image(PresentationResourceKey.navigationLiveLocationIcon.rawValue, { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat List/LiveLocationPanelIcon"), color: theme.rootController.navigationBar.accentTextColor) + }) + } + static func navigationPlayerMaximizedPlayIcon(_ theme: PresentationTheme) -> UIImage? { return theme.image(PresentationResourceKey.navigationPlayerMaximizedPlayIcon.rawValue, { theme in return generateTintedImage(image: UIImage(bundleImageName: "GlobalMusicPlayer/Play"), color: theme.rootController.navigationBar.primaryTextColor) diff --git a/TelegramUI/PresentationTheme.swift b/TelegramUI/PresentationTheme.swift index 122a442b3d..4589fc5b2a 100644 --- a/TelegramUI/PresentationTheme.swift +++ b/TelegramUI/PresentationTheme.swift @@ -6,6 +6,10 @@ public enum PresentationThemeParsingError: Error { case generic } +public final class PresentationThemeColorPlaceholder { + +} + public final class PresentationThemeRootTabBar { public let backgroundColor: UIColor public let separatorColor: UIColor diff --git a/TelegramUI/TelegramController.swift b/TelegramUI/TelegramController.swift index 4d55e5082a..4dbb944429 100644 --- a/TelegramUI/TelegramController.swift +++ b/TelegramUI/TelegramController.swift @@ -4,16 +4,52 @@ import TelegramCore import SwiftSignalKit import Postbox +enum LocationBroadcastPanelSource { + case none + case summary + case peer(PeerId) +} + +private func presentLiveLocationController(account: Account, peerId: PeerId, controller: ViewController) { + if let id = account.telegramApplicationContext.liveLocationManager?.internalMessageForPeerId(peerId) { + let _ = (account.postbox.modify { modifier -> Message? in + return modifier.getMessage(id) + } |> deliverOnMainQueue).start(next: { [weak controller] message in + if let message = message, let strongController = controller { + let _ = openChatMessage(account: account, message: message, reverseMessageGalleryOrder: false, navigationController: strongController.navigationController as? NavigationController, dismissInput: { + controller?.view.endEditing(true) + }, present: { c, a in + controller?.present(c, in: .window(.root), with: a) + }, transitionNode: { _, _ in + return nil + }, addToTransitionSurface: { _ in + }, openUrl: { _ in + }, openPeer: { peer, navigation in + }, callPeer: { _ in + }, sendSticker: { _ in + }, setupTemporaryHiddenMedia: { _, _, _ in + }) + } + }) + } +} + public class TelegramController: ViewController { private let account: Account let enableMediaAccessoryPanel: Bool + let locationBroadcastPanelSource: LocationBroadcastPanelSource private var mediaStatusDisposable: Disposable? + private var locationBroadcastDisposable: Disposable? private(set) var playlistStateAndType: (SharedMediaPlaylistItem, MusicPlaybackSettingsOrder, MediaManagerPlayerType)? private var mediaAccessoryPanel: (MediaNavigationAccessoryPanel, MediaManagerPlayerType)? + private var locationBroadcastMode: LocationBroadcastNavigationAccessoryPanelMode? + private var locationBroadcastPeers: [Peer]? + private var locationBroadcastAccessoryPanel: LocationBroadcastNavigationAccessoryPanel? + private var dismissingPanel: ASDisplayNode? override public var navigationHeight: CGFloat { @@ -21,17 +57,21 @@ public class TelegramController: ViewController { if let _ = self.mediaAccessoryPanel { height += 36.0 } + if let _ = self.locationBroadcastAccessoryPanel { + height += 36.0 + } return height } - init(account: Account, navigationBarTheme: NavigationBarTheme?, enableMediaAccessoryPanel: Bool) { + init(account: Account, navigationBarTheme: NavigationBarTheme?, enableMediaAccessoryPanel: Bool, locationBroadcastPanelSource: LocationBroadcastPanelSource) { self.account = account self.enableMediaAccessoryPanel = enableMediaAccessoryPanel + self.locationBroadcastPanelSource = locationBroadcastPanelSource super.init(navigationBarTheme: navigationBarTheme) - if let applicationContext = account.applicationContext as? TelegramApplicationContext { - self.mediaStatusDisposable = (applicationContext.mediaManager.globalMediaPlayerState + if enableMediaAccessoryPanel { + self.mediaStatusDisposable = (account.telegramApplicationContext.mediaManager.globalMediaPlayerState |> deliverOnMainQueue).start(next: { [weak self] playlistStateAndType in if let strongSelf = self, strongSelf.enableMediaAccessoryPanel { if !arePlaylistItemsEqual(strongSelf.playlistStateAndType?.0, playlistStateAndType?.0.item) || @@ -46,10 +86,52 @@ public class TelegramController: ViewController { } }) } + + if let liveLocationManager = account.telegramApplicationContext.liveLocationManager { + switch locationBroadcastPanelSource { + case .none: + self.locationBroadcastMode = nil + case .summary, .peer: + let signal: Signal<[Peer]?, NoError> + switch locationBroadcastPanelSource { + case let .peer(peerId): + self.locationBroadcastMode = .peer + signal = liveLocationManager.summaryManager.peersBroadcastingTo(peerId: peerId) + default: + self.locationBroadcastMode = .summary + signal = liveLocationManager.summaryManager.broadcastingToPeers() + |> map { $0.isEmpty ? nil : $0 } + + } + + self.locationBroadcastDisposable = (signal + |> deliverOnMainQueue).start(next: { [weak self] peers in + if let strongSelf = self { + var updated = false + if let current = strongSelf.locationBroadcastPeers, let peers = peers { + updated = !arePeerArraysEqual(current, peers) + } else if (strongSelf.locationBroadcastPeers != nil) != (peers != nil) { + updated = true + } + + if updated { + let wasEmpty = strongSelf.locationBroadcastPeers == nil + strongSelf.locationBroadcastPeers = peers + if wasEmpty != (peers == nil) { + strongSelf.requestLayout(transition: .animated(duration: 0.4, curve: .spring)) + } else if let peers = peers, let locationBroadcastMode = strongSelf.locationBroadcastMode { + strongSelf.locationBroadcastAccessoryPanel?.update(peers: peers, mode: locationBroadcastMode) + } + } + } + }) + } + } } deinit { self.mediaStatusDisposable?.dispose() + self.locationBroadcastDisposable?.dispose() } required public init(coder aDecoder: NSCoder) { @@ -59,10 +141,148 @@ public class TelegramController: ViewController { public override func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { super.containerLayoutUpdated(layout, transition: transition) - if let (item, _, type) = self.playlistStateAndType { - let navigationHeight = super.navigationHeight + let navigationHeight = super.navigationHeight + + var additionalHeight: CGFloat = 0.0 + + if let locationBroadcastPeers = self.locationBroadcastPeers, let locationBroadcastMode = self.locationBroadcastMode { let panelHeight = MediaNavigationAccessoryHeaderNode.minimizedHeight - let panelFrame = CGRect(origin: CGPoint(x: 0.0, y: navigationHeight.isZero ? -panelHeight : (navigationHeight + UIScreenPixel)), size: CGSize(width: layout.size.width, height: panelHeight)) + let panelFrame = CGRect(origin: CGPoint(x: 0.0, y: navigationHeight.isZero ? -panelHeight : (navigationHeight + additionalHeight + UIScreenPixel)), size: CGSize(width: layout.size.width, height: panelHeight)) + additionalHeight += panelHeight + + let locationBroadcastAccessoryPanel: LocationBroadcastNavigationAccessoryPanel + if let current = self.locationBroadcastAccessoryPanel { + locationBroadcastAccessoryPanel = current + transition.updateFrame(node: locationBroadcastAccessoryPanel, frame: panelFrame) + locationBroadcastAccessoryPanel.updateLayout(size: panelFrame.size, transition: transition) + } else { + let presentationData = self.account.telegramApplicationContext.currentPresentationData.with { $0 } + locationBroadcastAccessoryPanel = LocationBroadcastNavigationAccessoryPanel(theme: presentationData.theme, strings: presentationData.strings, tapAction: { [weak self] in + if let strongSelf = self { + switch strongSelf.locationBroadcastPanelSource { + case .none: + break + case .summary: + if let locationBroadcastPeers = strongSelf.locationBroadcastPeers { + if locationBroadcastPeers.count == 1 { + presentLiveLocationController(account: strongSelf.account, peerId: locationBroadcastPeers[0].id, controller: strongSelf) + } else { + let presentationData = strongSelf.account.telegramApplicationContext.currentPresentationData.with { $0 } + let controller = ActionSheetController(presentationTheme: presentationData.theme) + let dismissAction: () -> Void = { [weak controller] in + controller?.dismissAnimated() + } + var items: [ActionSheetItem] = [] + if !locationBroadcastPeers.isEmpty { + items.append(ActionSheetTextItem(title: presentationData.strings.LiveLocation_MenuChatsCount(Int32(locationBroadcastPeers.count)))) + for peer in locationBroadcastPeers { + items.append(ActionSheetButtonItem(title: peer.displayTitle, action: { + dismissAction() + if let strongSelf = self { + presentLiveLocationController(account: strongSelf.account, peerId: peer.id, controller: strongSelf) + } + })) + } + items.append(ActionSheetButtonItem(title: presentationData.strings.LiveLocation_MenuStopAll, color: .destructive, action: { + dismissAction() + for peer in locationBroadcastPeers { + self?.account.telegramApplicationContext.liveLocationManager?.cancelLiveLocation(peerId: peer.id) + } + })) + } + controller.setItemGroups([ + ActionSheetItemGroup(items: items), + ActionSheetItemGroup(items: [ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, action: { dismissAction() })]) + ]) + strongSelf.view.endEditing(true) + strongSelf.present(controller, in: .window(.root), with: ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) + } + } + case let .peer(peerId): + presentLiveLocationController(account: strongSelf.account, peerId: peerId, controller: strongSelf) + } + } + }, close: { [weak self] in + if let strongSelf = self { + var closePeers: [Peer]? + var closePeerId: PeerId? + switch strongSelf.locationBroadcastPanelSource { + case .none: + break + case .summary: + if let locationBroadcastPeers = strongSelf.locationBroadcastPeers { + if locationBroadcastPeers.count > 1 { + closePeers = locationBroadcastPeers + } else { + closePeerId = locationBroadcastPeers.first?.id + } + } + case let .peer(peerId): + closePeerId = peerId + } + let presentationData = strongSelf.account.telegramApplicationContext.currentPresentationData.with { $0 } + let controller = ActionSheetController(presentationTheme: presentationData.theme) + let dismissAction: () -> Void = { [weak controller] in + controller?.dismissAnimated() + } + var items: [ActionSheetItem] = [] + if let closePeers = closePeers, !closePeers.isEmpty { + items.append(ActionSheetTextItem(title: presentationData.strings.LiveLocation_MenuChatsCount(Int32(closePeers.count)))) + for peer in closePeers { + items.append(ActionSheetButtonItem(title: peer.displayTitle, action: { + dismissAction() + if let strongSelf = self { + presentLiveLocationController(account: strongSelf.account, peerId: peer.id, controller: strongSelf) + } + })) + } + items.append(ActionSheetButtonItem(title: presentationData.strings.LiveLocation_MenuStopAll, color: .destructive, action: { + dismissAction() + for peer in closePeers { + self?.account.telegramApplicationContext.liveLocationManager?.cancelLiveLocation(peerId: peer.id) + } + })) + } else if let closePeerId = closePeerId { + items.append(ActionSheetButtonItem(title: presentationData.strings.Map_StopLiveLocation, color: .destructive, action: { + dismissAction() + self?.account.telegramApplicationContext.liveLocationManager?.cancelLiveLocation(peerId: closePeerId) + })) + } + controller.setItemGroups([ + ActionSheetItemGroup(items: items), + ActionSheetItemGroup(items: [ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, action: { dismissAction() })]) + ]) + strongSelf.view.endEditing(true) + strongSelf.present(controller, in: .window(.root), with: ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) + } + }) + if let navigationBar = self.navigationBar { + self.displayNode.insertSubnode(locationBroadcastAccessoryPanel, aboveSubnode: navigationBar) + } else { + self.displayNode.addSubnode(locationBroadcastAccessoryPanel) + } + self.locationBroadcastAccessoryPanel = locationBroadcastAccessoryPanel + locationBroadcastAccessoryPanel.frame = panelFrame + locationBroadcastAccessoryPanel.update(peers: locationBroadcastPeers, mode: locationBroadcastMode) + locationBroadcastAccessoryPanel.updateLayout(size: panelFrame.size, transition: .immediate) + if transition.isAnimated { + locationBroadcastAccessoryPanel.animateIn(transition) + } + } + } else if let locationBroadcastAccessoryPanel = self.locationBroadcastAccessoryPanel { + self.locationBroadcastAccessoryPanel = nil + if transition.isAnimated { + locationBroadcastAccessoryPanel.animateOut(transition, completion: { [weak locationBroadcastAccessoryPanel] in + locationBroadcastAccessoryPanel?.removeFromSupernode() + }) + } else { + locationBroadcastAccessoryPanel.removeFromSupernode() + } + } + + if let (item, _, type) = self.playlistStateAndType { + let panelHeight = MediaNavigationAccessoryHeaderNode.minimizedHeight + let panelFrame = CGRect(origin: CGPoint(x: 0.0, y: navigationHeight.isZero ? -panelHeight : (navigationHeight + additionalHeight + UIScreenPixel)), size: CGSize(width: layout.size.width, height: panelHeight)) if let (mediaAccessoryPanel, mediaType) = self.mediaAccessoryPanel, mediaType == type { transition.updateFrame(layer: mediaAccessoryPanel.layer, frame: panelFrame) mediaAccessoryPanel.updateLayout(size: panelFrame.size, transition: transition) diff --git a/submodules/libtgvoip b/submodules/libtgvoip index dd43702ee0..6fff5b379d 160000 --- a/submodules/libtgvoip +++ b/submodules/libtgvoip @@ -1 +1 @@ -Subproject commit dd43702ee02336fe7f5d26228dd5caa90be131e2 +Subproject commit 6fff5b379d66cbb637d497c2f3a115e2487ec25f