Merge commit '888ad578c0e181b22662a261b464845eea1145b7'

This commit is contained in:
Peter
2019-07-02 20:15:56 +03:00
61 changed files with 1089 additions and 515 deletions
+4 -4
View File
@@ -198,6 +198,7 @@
09EBE2AF22B004EA00F670AB /* BlackIconIpad@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 09EBE2A322B004E900F670AB /* BlackIconIpad@2x.png */; };
09EBE2B022B004EA00F670AB /* BlueFilledIconLargeIpad@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 09EBE2A422B004E900F670AB /* BlueFilledIconLargeIpad@2x.png */; };
09EC0DE322C6339700E7185B /* IntentMessages.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09EC0DE222C6339700E7185B /* IntentMessages.swift */; };
09EC5CDA22CBBF9600292E42 /* telegram_plane1@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 09EC5CD922CBBF9600292E42 /* telegram_plane1@2x.png */; };
09FDAEE62140477F00BF856F /* MtProtoKitDynamic.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 09FDAEE52140477F00BF856F /* MtProtoKitDynamic.framework */; };
D000CACF21FB6E380011B15D /* NotificationService.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = D000CAC821FB6E370011B15D /* NotificationService.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
D001D5AA1F878DA300DF975A /* PhoneCountries.txt in Resources */ = {isa = PBXBuildFile; fileRef = D001D5A91F878DA300DF975A /* PhoneCountries.txt */; };
@@ -316,7 +317,6 @@
D08DB0BC213F4D1D00F2ADBF /* start_arrow@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = D050F2311E49DEDE00988324 /* start_arrow@2x.png */; };
D08DB0BD213F4D1D00F2ADBF /* start_arrow_ipad.png in Resources */ = {isa = PBXBuildFile; fileRef = D050F2321E49DEDE00988324 /* start_arrow_ipad.png */; };
D08DB0BE213F4D1D00F2ADBF /* start_arrow_ipad@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = D050F2331E49DEDE00988324 /* start_arrow_ipad@2x.png */; };
D08DB0BF213F4D1D00F2ADBF /* telegram_plane1@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = D050F2341E49DEDE00988324 /* telegram_plane1@2x.png */; };
D08DB0C0213F4D1D00F2ADBF /* telegram_sphere@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = D050F2351E49DEDE00988324 /* telegram_sphere@2x.png */; };
D096C2BE1CC3C021006D814E /* Display.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D096C2BD1CC3C021006D814E /* Display.framework */; };
D096C2BF1CC3C021006D814E /* Display.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = D096C2BD1CC3C021006D814E /* Display.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
@@ -841,6 +841,7 @@
09EBE2A322B004E900F670AB /* BlackIconIpad@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "BlackIconIpad@2x.png"; sourceTree = "<group>"; };
09EBE2A422B004E900F670AB /* BlueFilledIconLargeIpad@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "BlueFilledIconLargeIpad@2x.png"; sourceTree = "<group>"; };
09EC0DE222C6339700E7185B /* IntentMessages.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IntentMessages.swift; sourceTree = "<group>"; };
09EC5CD922CBBF9600292E42 /* telegram_plane1@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "telegram_plane1@2x.png"; sourceTree = "<group>"; };
09FDAEE52140477F00BF856F /* MtProtoKitDynamic.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = MtProtoKitDynamic.framework; sourceTree = BUILT_PRODUCTS_DIR; };
D000CAC021FB6E160011B15D /* NotificationService-HockeyApp.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "NotificationService-HockeyApp.entitlements"; sourceTree = "<group>"; };
D000CAC121FB6E160011B15D /* NotificationService-Fork.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "NotificationService-Fork.entitlements"; sourceTree = "<group>"; };
@@ -960,7 +961,6 @@
D050F2311E49DEDE00988324 /* start_arrow@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "start_arrow@2x.png"; sourceTree = "<group>"; };
D050F2321E49DEDE00988324 /* start_arrow_ipad.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = start_arrow_ipad.png; sourceTree = "<group>"; };
D050F2331E49DEDE00988324 /* start_arrow_ipad@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "start_arrow_ipad@2x.png"; sourceTree = "<group>"; };
D050F2341E49DEDE00988324 /* telegram_plane1@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "telegram_plane1@2x.png"; sourceTree = "<group>"; };
D050F2351E49DEDE00988324 /* telegram_sphere@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "telegram_sphere@2x.png"; sourceTree = "<group>"; };
D051DB0C215E5E2300F30F92 /* NotificationContent.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = NotificationContent.entitlements; sourceTree = "<group>"; };
D052974422B0073E004ABAF6 /* WhiteFilledIcon@3x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "WhiteFilledIcon@3x.png"; sourceTree = "<group>"; };
@@ -2173,6 +2173,7 @@
D050F21B1E49DEDE00988324 /* intro */ = {
isa = PBXGroup;
children = (
09EC5CD922CBBF9600292E42 /* telegram_plane1@2x.png */,
D050F21C1E49DEDE00988324 /* fast_arrow@2x.png */,
D050F21D1E49DEDE00988324 /* fast_arrow_shadow@2x.png */,
D050F21E1E49DEDE00988324 /* fast_body@2x.png */,
@@ -2197,7 +2198,6 @@
D050F2311E49DEDE00988324 /* start_arrow@2x.png */,
D050F2321E49DEDE00988324 /* start_arrow_ipad.png */,
D050F2331E49DEDE00988324 /* start_arrow_ipad@2x.png */,
D050F2341E49DEDE00988324 /* telegram_plane1@2x.png */,
D050F2351E49DEDE00988324 /* telegram_sphere@2x.png */,
);
name = intro;
@@ -2750,6 +2750,7 @@
D0CE6F66213EDA4400BCD44B /* AppIntentVocabulary.plist in Resources */,
D08DB0B8213F4D1D00F2ADBF /* powerful_mask@2x.png in Resources */,
D08DB0B4213F4D1D00F2ADBF /* knot_down@2x.png in Resources */,
09EC5CDA22CBBF9600292E42 /* telegram_plane1@2x.png in Resources */,
D08DB0BC213F4D1D00F2ADBF /* start_arrow@2x.png in Resources */,
D08DB0B6213F4D1D00F2ADBF /* powerful_infinity@2x.png in Resources */,
09EBE2AA22B004EA00F670AB /* BlueIconLargeIpad@2x.png in Resources */,
@@ -2834,7 +2835,6 @@
D021D4D9219CAEDD0064BEBA /* Config-Fork.xcconfig in Resources */,
D08DB0AD213F4D1D00F2ADBF /* ic_cam@2x.png in Resources */,
090E777622A6945900CD99F5 /* BlackClassicIcon@3x.png in Resources */,
D08DB0BF213F4D1D00F2ADBF /* telegram_plane1@2x.png in Resources */,
D0E8B8B02044496C00605593 /* voip_ringback.caf in Resources */,
09E9600822C23FF200B13673 /* BlackNotificationIcon.png in Resources */,
09EBE2A922B004EA00F670AB /* BlackFilledIconLargeIpad@2x.png in Resources */,
Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.7 KiB

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.8 KiB

@@ -3,7 +3,7 @@
{
"size" : "24x24",
"idiom" : "watch",
"filename" : "NotificationIcon38@2x.png",
"filename" : "Watch48@2x.png",
"scale" : "2x",
"role" : "notificationCenter",
"subtype" : "38mm"
@@ -11,7 +11,7 @@
{
"size" : "27.5x27.5",
"idiom" : "watch",
"filename" : "NotificationIcon42@2x.png",
"filename" : "Watch55@2x.png",
"scale" : "2x",
"role" : "notificationCenter",
"subtype" : "42mm"
@@ -19,21 +19,21 @@
{
"size" : "29x29",
"idiom" : "watch",
"filename" : "CompanionIcon@2x.png",
"filename" : "Simple@58x58.png",
"role" : "companionSettings",
"scale" : "2x"
},
{
"size" : "29x29",
"idiom" : "watch",
"filename" : "CompanionIcon@3x.png",
"filename" : "Simple@87x87.png",
"role" : "companionSettings",
"scale" : "3x"
},
{
"size" : "40x40",
"idiom" : "watch",
"filename" : "Icon@2x.png",
"filename" : "Simple@80x80.png",
"scale" : "2x",
"role" : "appLauncher",
"subtype" : "38mm"
@@ -41,7 +41,7 @@
{
"size" : "44x44",
"idiom" : "watch",
"filename" : "Home88@2x.png",
"filename" : "Watch88@2x.png",
"scale" : "2x",
"role" : "appLauncher",
"subtype" : "40mm"
@@ -49,7 +49,7 @@
{
"size" : "50x50",
"idiom" : "watch",
"filename" : "Home100@2x.png",
"filename" : "Watch100@2x.png",
"scale" : "2x",
"role" : "appLauncher",
"subtype" : "44mm"
@@ -57,7 +57,7 @@
{
"size" : "86x86",
"idiom" : "watch",
"filename" : "ShortLook38@2x.png",
"filename" : "Watch172@2x.png",
"scale" : "2x",
"role" : "quickLook",
"subtype" : "38mm"
@@ -65,7 +65,7 @@
{
"size" : "98x98",
"idiom" : "watch",
"filename" : "ShortLook42@2x.png",
"filename" : "Watch196@2x.png",
"scale" : "2x",
"role" : "quickLook",
"subtype" : "42mm"
@@ -73,7 +73,7 @@
{
"size" : "108x108",
"idiom" : "watch",
"filename" : "ShortLook44@2x.png",
"filename" : "Watch216@2x.png",
"scale" : "2x",
"role" : "quickLook",
"subtype" : "44mm"
@@ -81,7 +81,7 @@
{
"size" : "1024x1024",
"idiom" : "watch-marketing",
"filename" : "T-Ipad_1024.png",
"filename" : "Simple-iTunesArtwork.png",
"scale" : "1x"
}
],
Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 159 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

@@ -179,7 +179,7 @@ public final class DeviceAccess {
func statusForCellularState(_ state: CTCellularDataRestrictedState) -> AccessType? {
switch state {
case .restricted:
return .allowed
return .denied
case .notRestricted:
return .allowed
default:
+6 -9
View File
@@ -1177,10 +1177,10 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture
var bottomItemEdge: CGFloat = 0.0
for i in 0 ..< self.itemNodes.count {
if let index = itemNodes[i].index {
if let index = self.itemNodes[i].index {
if index == 0 {
topItemFound = true
topItemEdge = itemNodes[0].apparentFrame.origin.y
topItemEdge = self.itemNodes[0].apparentFrame.origin.y
break
}
}
@@ -1194,9 +1194,9 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture
var completeHeight = effectiveInsets.top + effectiveInsets.bottom
if let index = itemNodes[itemNodes.count - 1].index, index == self.items.count - 1 {
if let index = self.itemNodes[self.itemNodes.count - 1].index, index == self.items.count - 1 {
bottomItemFound = true
bottomItemEdge = itemNodes[itemNodes.count - 1].apparentFrame.maxY
bottomItemEdge = self.itemNodes[self.itemNodes.count - 1].apparentFrame.maxY
}
topItemEdge -= effectiveInsets.top
@@ -1223,12 +1223,12 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture
let wasIgnoringScrollingEvents = self.ignoreScrollingEvents
self.ignoreScrollingEvents = true
if topItemFound && bottomItemFound {
self.scroller.contentSize = CGSize(width: self.visibleSize.width, height: completeHeight)
if self.stackFromBottom {
self.lastContentOffset = CGPoint(x: 0.0, y: -topItemEdge)
} else {
self.lastContentOffset = CGPoint(x: 0.0, y: -topItemEdge)
}
self.scroller.contentSize = CGSize(width: self.visibleSize.width, height: completeHeight)
self.scroller.contentOffset = self.lastContentOffset
} else if topItemFound {
self.scroller.contentSize = CGSize(width: self.visibleSize.width, height: infiniteScrollSize * 2.0)
@@ -1346,7 +1346,7 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture
private func deleteAndInsertItemsTransaction(deleteIndices: [ListViewDeleteItem], insertIndicesAndItems: [ListViewInsertItem], updateIndicesAndItems: [ListViewUpdateItem], options: ListViewDeleteAndInsertOptions, scrollToItem: ListViewScrollToItem?, additionalScrollDistance: CGFloat, updateSizeAndInsets: ListViewUpdateSizeAndInsets?, stationaryItemRange: (Int, Int)?, updateOpaqueState: Any?, completion: @escaping () -> Void) {
if deleteIndices.isEmpty && insertIndicesAndItems.isEmpty && updateIndicesAndItems.isEmpty && scrollToItem == nil {
if let updateSizeAndInsets = updateSizeAndInsets , (self.items.count == 0 || (updateSizeAndInsets.size == self.visibleSize && updateSizeAndInsets.insets == self.insets)) {
if let updateSizeAndInsets = updateSizeAndInsets, (self.items.count == 0 || (updateSizeAndInsets.size == self.visibleSize && updateSizeAndInsets.insets == self.insets)) {
self.visibleSize = updateSizeAndInsets.size
self.insets = updateSizeAndInsets.insets
self.headerInsets = updateSizeAndInsets.headerInsets ?? self.insets
@@ -2457,7 +2457,6 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture
var headerNodesTransition: (ContainedViewLayoutTransition, Bool, CGFloat) = (.immediate, false, 0.0)
var deferredUpdateVisible = false
var insetTransitionOffset: CGFloat = 0.0
if let updateSizeAndInsets = updateSizeAndInsets {
if self.insets != updateSizeAndInsets.insets || self.headerInsets != updateSizeAndInsets.headerInsets || !self.visibleSize.height.isEqual(to: updateSizeAndInsets.size.height) {
@@ -2602,8 +2601,6 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture
let (snappedTopInset, snapToBoundsOffset) = self.snapToBounds(snapTopItem: scrollToItem != nil, stackFromBottom: self.stackFromBottom, updateSizeAndInsets: updateSizeAndInsets, scrollToItem: scrollToItem)
if !snappedTopInset.isZero && previousApparentFrames.isEmpty {
let offsetFix = snappedTopInset
for itemNode in self.itemNodes {
let position = itemNode.position
itemNode.position = CGPoint(x: position.x, y: position.y + snappedTopInset)
+18 -12
View File
@@ -257,7 +257,9 @@ open class ViewControllerPresentationArguments {
}
}
self.navigationBar?.requestContainerLayout = { [weak self] transition in
self?.requestLayout(transition: transition)
if let strongSelf = self, strongSelf.isNodeLoaded, let validLayout = strongSelf.validLayout {
strongSelf.updateNavigationBarLayout(validLayout, transition: transition)
}
}
self.navigationBar?.item = self.navigationItem
self.automaticallyAdjustsScrollViewInsets = false
@@ -275,17 +277,7 @@ open class ViewControllerPresentationArguments {
}
open func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
self.validLayout = layout
if !self.isViewLoaded {
self.loadView()
}
transition.updateFrame(node: self.displayNode, frame: CGRect(origin: self.view.frame.origin, size: layout.size))
if let _ = layout.statusBarHeight {
self.statusBar.frame = CGRect(origin: CGPoint(), size: CGSize(width: layout.size.width, height: 40.0))
}
private func updateNavigationBarLayout(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
let statusBarHeight: CGFloat = layout.statusBarHeight ?? 0.0
let navigationBarHeight: CGFloat = max(20.0, statusBarHeight) + (self.navigationBar?.contentHeight ?? 44.0)
let navigationBarOffset: CGFloat
@@ -318,6 +310,20 @@ open class ViewControllerPresentationArguments {
transition.updateFrame(node: navigationBar, frame: navigationBarFrame)
navigationBar.setHidden(!self.displayNavigationBar, animated: transition.isAnimated)
}
}
open func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
self.validLayout = layout
if !self.isViewLoaded {
self.loadView()
}
transition.updateFrame(node: self.displayNode, frame: CGRect(origin: self.view.frame.origin, size: layout.size))
if let _ = layout.statusBarHeight {
self.statusBar.frame = CGRect(origin: CGPoint(), size: CGSize(width: layout.size.width, height: 40.0))
}
self.updateNavigationBarLayout(layout, transition: transition)
self.presentationContext.containerLayoutUpdated(layout, transition: transition)
@@ -7,6 +7,7 @@
objects = {
/* Begin PBXBuildFile section */
09EC5CD822CBB86B00292E42 /* telegram_plane1@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 09EC5CD722CBB86A00292E42 /* telegram_plane1@2x.png */; };
D0A25B8922B5C0CF00AD4DE8 /* LegacyComponents.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D0A25B8822B5C0CF00AD4DE8 /* LegacyComponents.framework */; };
D0A25B8A22B5C0CF00AD4DE8 /* LegacyComponents.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = D0A25B8822B5C0CF00AD4DE8 /* LegacyComponents.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
D0AE304922B1D77E0058D3BC /* RMIntro.h in Headers */ = {isa = PBXBuildFile; fileRef = D0AE304722B1D77E0058D3BC /* RMIntro.h */; settings = {ATTRIBUTES = (Public, ); }; };
@@ -56,7 +57,6 @@
D0AE30C022B1D8ED0058D3BC /* powerful_infinity_white@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = D0AE308422B1D8ED0058D3BC /* powerful_infinity_white@2x.png */; };
D0AE30C122B1D8ED0058D3BC /* private_screw@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = D0AE308522B1D8ED0058D3BC /* private_screw@2x.png */; };
D0AE30C222B1D8ED0058D3BC /* powerful_infinity@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = D0AE308622B1D8ED0058D3BC /* powerful_infinity@2x.png */; };
D0AE30C322B1D8ED0058D3BC /* telegram_plane@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = D0AE308722B1D8ED0058D3BC /* telegram_plane@2x.png */; };
D0AE30C422B1D8ED0058D3BC /* ic_smile@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = D0AE308822B1D8ED0058D3BC /* ic_smile@2x.png */; };
D0AE30C522B1D8ED0058D3BC /* RMGeometry.m in Sources */ = {isa = PBXBuildFile; fileRef = D0AE308922B1D8ED0058D3BC /* RMGeometry.m */; };
D0AE30C622B1D8ED0058D3BC /* RMIntroViewController.h in Headers */ = {isa = PBXBuildFile; fileRef = D0AE308A22B1D8ED0058D3BC /* RMIntroViewController.h */; settings = {ATTRIBUTES = (Public, ); }; };
@@ -84,6 +84,7 @@
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
09EC5CD722CBB86A00292E42 /* telegram_plane1@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "telegram_plane1@2x.png"; sourceTree = "<group>"; };
D0A25B8822B5C0CF00AD4DE8 /* LegacyComponents.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = LegacyComponents.framework; sourceTree = BUILT_PRODUCTS_DIR; };
D0AE304422B1D77E0058D3BC /* RMIntro.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = RMIntro.framework; sourceTree = BUILT_PRODUCTS_DIR; };
D0AE304722B1D77E0058D3BC /* RMIntro.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RMIntro.h; sourceTree = "<group>"; };
@@ -134,7 +135,6 @@
D0AE308422B1D8ED0058D3BC /* powerful_infinity_white@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "powerful_infinity_white@2x.png"; sourceTree = "<group>"; };
D0AE308522B1D8ED0058D3BC /* private_screw@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "private_screw@2x.png"; sourceTree = "<group>"; };
D0AE308622B1D8ED0058D3BC /* powerful_infinity@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "powerful_infinity@2x.png"; sourceTree = "<group>"; };
D0AE308722B1D8ED0058D3BC /* telegram_plane@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "telegram_plane@2x.png"; sourceTree = "<group>"; };
D0AE308822B1D8ED0058D3BC /* ic_smile@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "ic_smile@2x.png"; sourceTree = "<group>"; };
D0AE308922B1D8ED0058D3BC /* RMGeometry.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RMGeometry.m; sourceTree = "<group>"; };
D0AE308A22B1D8ED0058D3BC /* RMIntroViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RMIntroViewController.h; sourceTree = "<group>"; };
@@ -249,6 +249,7 @@
D0AE306E22B1D8ED0058D3BC /* Resources */ = {
isa = PBXGroup;
children = (
09EC5CD722CBB86A00292E42 /* telegram_plane1@2x.png */,
D0AE306F22B1D8ED0058D3BC /* ic_bubble_dot@2x.png */,
D0AE307022B1D8ED0058D3BC /* powerful_mask@2x.png */,
D0AE307122B1D8ED0058D3BC /* telegram_sphere@2x.png */,
@@ -273,7 +274,6 @@
D0AE308422B1D8ED0058D3BC /* powerful_infinity_white@2x.png */,
D0AE308522B1D8ED0058D3BC /* private_screw@2x.png */,
D0AE308622B1D8ED0058D3BC /* powerful_infinity@2x.png */,
D0AE308722B1D8ED0058D3BC /* telegram_plane@2x.png */,
D0AE308822B1D8ED0058D3BC /* ic_smile@2x.png */,
);
path = Resources;
@@ -402,6 +402,7 @@
D0AE30B522B1D8ED0058D3BC /* start_arrow_ipad@2x.png in Resources */,
D0AE30BD22B1D8ED0058D3BC /* fast_spiral@2x.png in Resources */,
D0AE30CD22B1D8ED0058D3BC /* .gitignore in Resources */,
09EC5CD822CBB86B00292E42 /* telegram_plane1@2x.png in Resources */,
D0AE30B822B1D8ED0058D3BC /* fast_arrow@2x.png in Resources */,
D0AE30AC22B1D8ED0058D3BC /* powerful_mask@2x.png in Resources */,
D0AE30B322B1D8ED0058D3BC /* fast_arrow_shadow@2x.png in Resources */,
@@ -414,7 +415,6 @@
D0AE30B222B1D8ED0058D3BC /* start_arrow_ipad.png in Resources */,
D0AE30C422B1D8ED0058D3BC /* ic_smile@2x.png in Resources */,
D0AE30B922B1D8ED0058D3BC /* ic_pencil@2x.png in Resources */,
D0AE30C322B1D8ED0058D3BC /* telegram_plane@2x.png in Resources */,
D0AE30B422B1D8ED0058D3BC /* ic_cam@2x.png in Resources */,
D0AE30B622B1D8ED0058D3BC /* fast_body@2x.png in Resources */,
D0AE30BC22B1D8ED0058D3BC /* ic_pin@2x.png in Resources */,
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.9 KiB

After

Width:  |  Height:  |  Size: 8.1 KiB

@@ -1,6 +1,7 @@
import Foundation
import UIKit
import CallKit
import Intents
import AVFoundation
import Postbox
import TelegramCore
@@ -49,6 +50,7 @@ public final class CallKitIntegration {
func startCall(account: Account, peerId: PeerId, displayTitle: String) {
if #available(iOSApplicationExtension 10.0, iOS 10.0, *) {
(sharedProviderDelegate as? CallKitProviderDelegate)?.startCall(account: account, peerId: peerId, displayTitle: displayTitle)
self.donateIntent(peerId: peerId, displayTitle: displayTitle)
}
}
@@ -75,6 +77,20 @@ public final class CallKitIntegration {
(sharedProviderDelegate as? CallKitProviderDelegate)?.reportOutgoingCallConnected(uuid: uuid, at: date)
}
}
private func donateIntent(peerId: PeerId, displayTitle: String) {
if #available(iOSApplicationExtension 10.0, iOS 10.0, *) {
let handle = INPersonHandle(value: "tg\(peerId.id)", type: .unknown)
let contact = INPerson(personHandle: handle, nameComponents: nil, displayName: displayTitle, image: nil, contactIdentifier: nil, customIdentifier: "tg\(peerId.id)")
let intent = INStartAudioCallIntent(contacts: [contact])
let interaction = INInteraction(intent: intent, response: nil)
interaction.direction = .outgoing
interaction.donate { _ in
}
}
}
}
@available(iOSApplicationExtension 10.0, iOS 10.0, *)
@@ -68,8 +68,6 @@ public final class PresentationCallManager {
private var callSettings: VoiceCallSettings?
private var callSettingsDisposable: Disposable?
public var callRequested: ((_ accountPeerId: PeerId, _ peerId: PeerId) -> Void)?
public static var voipMaxLayer: Int32 {
return OngoingCallContext.maxLayer
}
@@ -316,8 +314,6 @@ public final class PresentationCallManager {
} else {
begin()
}
self.callRequested?(account.peerId, peerId)
} else {
let begin: () -> Void = { [weak self] in
guard let strongSelf = self else {
@@ -333,7 +329,6 @@ public final class PresentationCallManager {
} else {
begin()
}
self.callRequested?(account.peerId, peerId)
}
return .requested
}
@@ -107,6 +107,7 @@ public func searchStickers(account: Account, query: String, scope: SearchSticker
let currentItems = Set<MediaId>(result.map { $0.file.fileId })
var recentItems: [TelegramMediaFile] = []
var recentAnimatedItems: [TelegramMediaFile] = []
var recentItemsIds = Set<MediaId>()
var matchingRecentItemsIds = Set<MediaId>()
@@ -118,14 +119,19 @@ public func searchStickers(account: Account, query: String, scope: SearchSticker
matchingRecentItemsIds.insert(file.fileId)
}
recentItemsIds.insert(file.fileId)
recentItems.append(file)
if file.isAnimatedSticker {
recentAnimatedItems.append(file)
} else {
recentItems.append(file)
}
break
}
}
}
}
var installed: [FoundStickerItem] = []
var installedItems: [FoundStickerItem] = []
var installedAnimatedItems: [FoundStickerItem] = []
for item in transaction.searchItemCollection(namespace: Namespaces.ItemCollection.CloudStickerPacks, query: .exact(ValueBoxKey(query))) {
if let item = item as? StickerPackItem {
if !currentItems.contains(item.file.fileId) {
@@ -138,7 +144,11 @@ public func searchStickers(account: Account, query: String, scope: SearchSticker
}
}
if !recentItemsIds.contains(item.file.fileId) {
installed.append(FoundStickerItem(file: item.file, stringRepresentations: stringRepresentations))
if item.file.isAnimatedSticker {
installedAnimatedItems.append(FoundStickerItem(file: item.file, stringRepresentations: stringRepresentations))
} else {
installedItems.append(FoundStickerItem(file: item.file, stringRepresentations: stringRepresentations))
}
} else {
matchingRecentItemsIds.insert(item.file.fileId)
}
@@ -146,13 +156,20 @@ public func searchStickers(account: Account, query: String, scope: SearchSticker
}
}
for file in recentAnimatedItems {
if matchingRecentItemsIds.contains(file.fileId) {
result.append(FoundStickerItem(file: file, stringRepresentations: [query]))
}
}
for file in recentItems {
if matchingRecentItemsIds.contains(file.fileId) {
result.append(FoundStickerItem(file: file, stringRepresentations: [query]))
}
}
result.append(contentsOf: installed)
result.append(contentsOf: installedAnimatedItems)
result.append(contentsOf: installedItems)
}
let cached = transaction.retrieveItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedStickerQueryResults, key: CachedStickerQueryResult.cacheKey(query))) as? CachedStickerQueryResult
@@ -586,8 +586,8 @@ final class SharedApplicationContext {
self.window?.rootViewController?.dismiss(animated: true, completion: nil)
}, getAvailableAlternateIcons: {
if #available(iOS 10.3, *) {
var icons = [PresentationAppIcon(name: "Blue", imageName: "BlueIcon", isDefault: false),
PresentationAppIcon(name: "Black", imageName: "BlackIcon", isDefault: false),
var icons = [PresentationAppIcon(name: "Blue", imageName: "BlueIcon", isDefault: buildConfig.isAppStoreBuild),
PresentationAppIcon(name: "Black", imageName: "BlackIcon", isDefault: buildConfig.isInternalBuild),
PresentationAppIcon(name: "BlueClassic", imageName: "BlueClassicIcon", isDefault: false),
PresentationAppIcon(name: "BlackClassic", imageName: "BlackClassicIcon", isDefault: false),
PresentationAppIcon(name: "BlueFilled", imageName: "BlueFilledIcon", isDefault: false),
@@ -312,13 +312,13 @@ private enum ChannelInfoEntry: ItemListNodeEntry {
arguments.tapAvatarAction()
}, context: arguments.avatarAndNameInfoContext, updatingImage: updatingAvatar)
case let .about(theme, text, value):
return ItemListTextWithLabelItem(theme: theme, label: text, text: foldMultipleLineBreaks(value), enabledEntitiyTypes: [.url, .mention, .hashtag], multiline: true, sectionId: self.section, action: nil, longTapAction: {
return ItemListTextWithLabelItem(theme: theme, label: text, text: foldMultipleLineBreaks(value), enabledEntityTypes: [.url, .mention, .hashtag], multiline: true, sectionId: self.section, action: nil, longTapAction: {
arguments.displayContextMenu(ChannelInfoEntryTag.about, value)
}, linkItemAction: { action, itemLink in
arguments.aboutLinkAction(action, itemLink)
}, tag: ChannelInfoEntryTag.about)
case let .addressName(theme, text, value):
return ItemListTextWithLabelItem(theme: theme, label: text, text: "https://t.me/\(value)", textColor: .accent, enabledEntitiyTypes: [], multiline: false, sectionId: self.section, action: {
return ItemListTextWithLabelItem(theme: theme, label: text, text: "https://t.me/\(value)", textColor: .accent, enabledEntityTypes: [], multiline: false, sectionId: self.section, action: {
arguments.displayAddressNameContextMenu("https://t.me/\(value)")
}, longTapAction: {
arguments.displayContextMenu(ChannelInfoEntryTag.link, "https://t.me/\(value)")
@@ -1157,7 +1157,7 @@ public func channelInfoController(context: AccountContext, peerId: PeerId) -> Vi
}
aboutLinkActionImpl = { [weak controller] action, itemLink in
if let controller = controller {
handlePeerInfoAboutTextAction(context: context, peerId: peerId, navigateDisposable: navigateDisposable, controller: controller, action: action, itemLink: itemLink)
handleTextLinkAction(context: context, peerId: peerId, navigateDisposable: navigateDisposable, controller: controller, action: action, itemLink: itemLink)
}
}
endEditingImpl = {
@@ -2954,7 +2954,7 @@ public final class ChatController: TelegramController, GalleryHiddenMediaTarget,
self?.deleteMediaRecording()
}, sendRecordedMedia: { [weak self] in
self?.sendMediaRecording()
}, displayRestrictedInfo: { [weak self] subject in
}, displayRestrictedInfo: { [weak self] subject, displayType in
guard let strongSelf = self else {
return
}
@@ -3006,37 +3006,42 @@ public final class ChatController: TelegramController, GalleryHiddenMediaTarget,
strongSelf.recordingModeFeedback?.error()
var rect: CGRect?
let isStickers: Bool = subject == .stickers
switch subject {
case .stickers:
rect = strongSelf.chatDisplayNode.frameForStickersButton()
if var rectValue = rect, let actionRect = strongSelf.chatDisplayNode.frameForInputActionButton() {
rectValue.origin.y = actionRect.minY
rect = rectValue
switch displayType {
case .tooltip:
var rect: CGRect?
let isStickers: Bool = subject == .stickers
switch subject {
case .stickers:
rect = strongSelf.chatDisplayNode.frameForStickersButton()
if var rectValue = rect, let actionRect = strongSelf.chatDisplayNode.frameForInputActionButton() {
rectValue.origin.y = actionRect.minY
rect = rectValue
}
case .mediaRecording:
rect = strongSelf.chatDisplayNode.frameForInputActionButton()
}
case .mediaRecording:
rect = strongSelf.chatDisplayNode.frameForInputActionButton()
}
if let tooltipController = strongSelf.mediaRestrictedTooltipController, strongSelf.mediaRestrictedTooltipControllerMode == isStickers {
tooltipController.content = .text(banDescription)
} else if let rect = rect {
strongSelf.mediaRestrictedTooltipController?.dismiss()
let tooltipController = TooltipController(content: .text(banDescription))
strongSelf.mediaRestrictedTooltipController = tooltipController
strongSelf.mediaRestrictedTooltipControllerMode = isStickers
tooltipController.dismissed = { [weak tooltipController] in
if let strongSelf = self, let tooltipController = tooltipController, strongSelf.mediaRestrictedTooltipController === tooltipController {
strongSelf.mediaRestrictedTooltipController = nil
if let tooltipController = strongSelf.mediaRestrictedTooltipController, strongSelf.mediaRestrictedTooltipControllerMode == isStickers {
tooltipController.content = .text(banDescription)
} else if let rect = rect {
strongSelf.mediaRestrictedTooltipController?.dismiss()
let tooltipController = TooltipController(content: .text(banDescription))
strongSelf.mediaRestrictedTooltipController = tooltipController
strongSelf.mediaRestrictedTooltipControllerMode = isStickers
tooltipController.dismissed = { [weak tooltipController] in
if let strongSelf = self, let tooltipController = tooltipController, strongSelf.mediaRestrictedTooltipController === tooltipController {
strongSelf.mediaRestrictedTooltipController = nil
}
}
strongSelf.present(tooltipController, in: .window(.root), with: TooltipControllerPresentationArguments(sourceNodeAndRect: {
if let strongSelf = self {
return (strongSelf.chatDisplayNode, rect)
}
return nil
}))
}
}
strongSelf.present(tooltipController, in: .window(.root), with: TooltipControllerPresentationArguments(sourceNodeAndRect: {
if let strongSelf = self {
return (strongSelf.chatDisplayNode, rect)
}
return nil
}))
case .alert:
strongSelf.present(textAlertController(context: strongSelf.context, title: nil, text: banDescription, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root))
}
}
}, displayVideoUnmuteTip: { [weak self] location in
@@ -3607,6 +3612,8 @@ public final class ChatController: TelegramController, GalleryHiddenMediaTarget,
self.failedMessageEventsDisposable.set((self.context.account.pendingMessageManager.failedMessageEvents(peerId: peerId)
|> deliverOnMainQueue).start(next: { [weak self] reason in
if let strongSelf = self {
let subjectFlags: TelegramChatBannedRightsFlags = .banSendMedia
let text: String
let moreInfo: Bool
switch reason {
@@ -3617,8 +3624,8 @@ public final class ChatController: TelegramController, GalleryHiddenMediaTarget,
text = strongSelf.presentationData.strings.Conversation_SendMessageErrorGroupRestricted
moreInfo = true
case .mediaRestricted:
text = strongSelf.presentationData.strings.Conversation_DefaultRestrictedMedia
moreInfo = false
strongSelf.interfaceInteraction?.displayRestrictedInfo(.mediaRecording, .alert)
return
}
let actions: [TextAlertAction]
if moreInfo {
@@ -3631,27 +3638,6 @@ public final class ChatController: TelegramController, GalleryHiddenMediaTarget,
strongSelf.present(textAlertController(context: strongSelf.context, title: nil, text: text, actions: actions), in: .window(.root))
}
}))
/*case let .group(groupId):
let unreadCountsKey: PostboxViewKey = .unreadCounts(items: [.group(groupId), .total(nil)])
self.chatUnreadCountDisposable = (self.context.account.postbox.combinedView(keys: [unreadCountsKey]) |> deliverOnMainQueue).start(next: { [weak self] views in
if let strongSelf = self {
var unreadCount: Int32 = 0
var totalCount: Int32 = 0
if let view = views.views[unreadCountsKey] as? UnreadMessageCountsView {
if let count = view.count(for: .group(groupId)) {
unreadCount = count
}
if let (_, state) = view.total() {
let inAppSettings = strongSelf.context.sharedContext.currentInAppNotificationSettings.with { $0 }
let (count, _) = renderedTotalUnreadCount(inAppSettings: inAppSettings, totalUnreadState: state)
totalCount = count
}
}
strongSelf.chatDisplayNode.navigateButtons.unreadCount = unreadCount
}
})*/
}
self.interfaceInteraction = interfaceInteraction
@@ -837,21 +837,6 @@ public class ChatListController: TelegramController, UIViewControllerPreviewingD
}
}
/*self.chatListDisplayNode.chatListNode.contentOffsetChanged = { [weak self] offset in
if let strongSelf = self, let searchContentNode = strongSelf.searchContentNode {
var progress: CGFloat = 0.0
switch offset {
case let .known(offset):
progress = max(0.0, (searchContentNode.nominalHeight - max(0.0, offset - 76.0))) / searchContentNode.nominalHeight
case .none:
progress = 1.0
default:
break
}
searchContentNode.updateExpansionProgress(progress)
}
}*/
self.chatListDisplayNode.chatListNode.contentScrollingEnded = { [weak self] listView in
if let strongSelf = self, let searchContentNode = strongSelf.searchContentNode {
return fixListNodeScrolling(listView, searchNode: searchContentNode)
@@ -67,7 +67,7 @@ private let boundingImageSize = CGSize(width: 28.0, height: 28.0)
private let highlightSize = CGSize(width: 35.0, height: 35.0)
private let verticalOffset: CGFloat = 3.0
private enum StickerPackThumbnailItem: Equatable {
enum StickerPackThumbnailItem: Equatable {
case still(TelegramMediaImageRepresentation)
case animated(MediaResource)
@@ -37,6 +37,11 @@ enum ChatPanelRestrictionInfoSubject {
case stickers
}
enum ChatPanelRestrictionInfoDisplayType {
case tooltip
case alert
}
final class ChatPanelInterfaceInteraction {
let setupReplyMessage: (MessageId) -> Void
let setupEditMessage: (MessageId?) -> Void
@@ -73,7 +78,7 @@ final class ChatPanelInterfaceInteraction {
let lockMediaRecording: () -> Void
let deleteRecordedMedia: () -> Void
let sendRecordedMedia: () -> Void
let displayRestrictedInfo: (ChatPanelRestrictionInfoSubject) -> Void
let displayRestrictedInfo: (ChatPanelRestrictionInfoSubject, ChatPanelRestrictionInfoDisplayType) -> Void
let displayVideoUnmuteTip: (CGPoint?) -> Void
let switchMediaRecordingMode: () -> Void
let setupMessageAutoremoveTimeout: () -> Void
@@ -102,7 +107,7 @@ final class ChatPanelInterfaceInteraction {
let reportPeerIrrelevantGeoLocation: () -> Void
let statuses: ChatPanelInterfaceInteractionStatuses?
init(setupReplyMessage: @escaping (MessageId) -> Void, setupEditMessage: @escaping (MessageId?) -> Void, beginMessageSelection: @escaping ([MessageId]) -> Void, deleteSelectedMessages: @escaping () -> Void, reportSelectedMessages: @escaping () -> Void, reportMessages: @escaping ([Message]) -> Void, deleteMessages: @escaping ([Message]) -> Void, forwardSelectedMessages: @escaping () -> Void, forwardCurrentForwardMessages: @escaping () -> Void, forwardMessages: @escaping ([Message]) -> Void, shareSelectedMessages: @escaping () -> Void, updateTextInputStateAndMode: @escaping ((ChatTextInputState, ChatInputMode) -> (ChatTextInputState, ChatInputMode)) -> Void, updateInputModeAndDismissedButtonKeyboardMessageId: @escaping ((ChatPresentationInterfaceState) -> (ChatInputMode, MessageId?)) -> Void, openStickers: @escaping () -> Void, editMessage: @escaping () -> Void, beginMessageSearch: @escaping (ChatSearchDomain, String) -> Void, dismissMessageSearch: @escaping () -> Void, updateMessageSearch: @escaping (String) -> Void, navigateMessageSearch: @escaping (ChatPanelSearchNavigationAction) -> Void, openCalendarSearch: @escaping () -> Void, toggleMembersSearch: @escaping (Bool) -> Void, navigateToMessage: @escaping (MessageId) -> Void, navigateToChat: @escaping (PeerId) -> Void, openPeerInfo: @escaping () -> Void, togglePeerNotifications: @escaping () -> Void, sendContextResult: @escaping (ChatContextResultCollection, ChatContextResult) -> Void, sendBotCommand: @escaping (Peer, String) -> Void, sendBotStart: @escaping (String?) -> Void, botSwitchChatWithPayload: @escaping (PeerId, String) -> Void, beginMediaRecording: @escaping (Bool) -> Void, finishMediaRecording: @escaping (ChatFinishMediaRecordingAction) -> Void, stopMediaRecording: @escaping () -> Void, lockMediaRecording: @escaping () -> Void, deleteRecordedMedia: @escaping () -> Void, sendRecordedMedia: @escaping () -> Void, displayRestrictedInfo: @escaping (ChatPanelRestrictionInfoSubject) -> Void, displayVideoUnmuteTip: @escaping (CGPoint?) -> Void, switchMediaRecordingMode: @escaping () -> Void, setupMessageAutoremoveTimeout: @escaping () -> Void, sendSticker: @escaping (FileMediaReference) -> Void, unblockPeer: @escaping () -> Void, pinMessage: @escaping (MessageId) -> Void, unpinMessage: @escaping () -> Void, shareAccountContact: @escaping () -> Void, reportPeer: @escaping () -> Void, presentPeerContact: @escaping () -> Void, dismissReportPeer: @escaping () -> Void, deleteChat: @escaping () -> Void, beginCall: @escaping () -> Void, toggleMessageStickerStarred: @escaping (MessageId) -> Void, presentController: @escaping (ViewController, Any?) -> Void, getNavigationController: @escaping () -> NavigationController?, presentGlobalOverlayController: @escaping (ViewController, Any?) -> Void, navigateFeed: @escaping () -> Void, openGrouping: @escaping () -> Void, toggleSilentPost: @escaping () -> Void, requestUnvoteInMessage: @escaping (MessageId) -> Void, requestStopPollInMessage: @escaping (MessageId) -> Void, updateInputLanguage: @escaping ((String?) -> String?) -> Void, unarchiveChat: @escaping () -> Void, openLinkEditing: @escaping () -> Void, reportPeerIrrelevantGeoLocation: @escaping () -> Void, statuses: ChatPanelInterfaceInteractionStatuses?) {
init(setupReplyMessage: @escaping (MessageId) -> Void, setupEditMessage: @escaping (MessageId?) -> Void, beginMessageSelection: @escaping ([MessageId]) -> Void, deleteSelectedMessages: @escaping () -> Void, reportSelectedMessages: @escaping () -> Void, reportMessages: @escaping ([Message]) -> Void, deleteMessages: @escaping ([Message]) -> Void, forwardSelectedMessages: @escaping () -> Void, forwardCurrentForwardMessages: @escaping () -> Void, forwardMessages: @escaping ([Message]) -> Void, shareSelectedMessages: @escaping () -> Void, updateTextInputStateAndMode: @escaping ((ChatTextInputState, ChatInputMode) -> (ChatTextInputState, ChatInputMode)) -> Void, updateInputModeAndDismissedButtonKeyboardMessageId: @escaping ((ChatPresentationInterfaceState) -> (ChatInputMode, MessageId?)) -> Void, openStickers: @escaping () -> Void, editMessage: @escaping () -> Void, beginMessageSearch: @escaping (ChatSearchDomain, String) -> Void, dismissMessageSearch: @escaping () -> Void, updateMessageSearch: @escaping (String) -> Void, navigateMessageSearch: @escaping (ChatPanelSearchNavigationAction) -> Void, openCalendarSearch: @escaping () -> Void, toggleMembersSearch: @escaping (Bool) -> Void, navigateToMessage: @escaping (MessageId) -> Void, navigateToChat: @escaping (PeerId) -> Void, openPeerInfo: @escaping () -> Void, togglePeerNotifications: @escaping () -> Void, sendContextResult: @escaping (ChatContextResultCollection, ChatContextResult) -> Void, sendBotCommand: @escaping (Peer, String) -> Void, sendBotStart: @escaping (String?) -> Void, botSwitchChatWithPayload: @escaping (PeerId, String) -> Void, beginMediaRecording: @escaping (Bool) -> Void, finishMediaRecording: @escaping (ChatFinishMediaRecordingAction) -> Void, stopMediaRecording: @escaping () -> Void, lockMediaRecording: @escaping () -> Void, deleteRecordedMedia: @escaping () -> Void, sendRecordedMedia: @escaping () -> Void, displayRestrictedInfo: @escaping (ChatPanelRestrictionInfoSubject, ChatPanelRestrictionInfoDisplayType) -> Void, displayVideoUnmuteTip: @escaping (CGPoint?) -> Void, switchMediaRecordingMode: @escaping () -> Void, setupMessageAutoremoveTimeout: @escaping () -> Void, sendSticker: @escaping (FileMediaReference) -> Void, unblockPeer: @escaping () -> Void, pinMessage: @escaping (MessageId) -> Void, unpinMessage: @escaping () -> Void, shareAccountContact: @escaping () -> Void, reportPeer: @escaping () -> Void, presentPeerContact: @escaping () -> Void, dismissReportPeer: @escaping () -> Void, deleteChat: @escaping () -> Void, beginCall: @escaping () -> Void, toggleMessageStickerStarred: @escaping (MessageId) -> Void, presentController: @escaping (ViewController, Any?) -> Void, getNavigationController: @escaping () -> NavigationController?, presentGlobalOverlayController: @escaping (ViewController, Any?) -> Void, navigateFeed: @escaping () -> Void, openGrouping: @escaping () -> Void, toggleSilentPost: @escaping () -> Void, requestUnvoteInMessage: @escaping (MessageId) -> Void, requestStopPollInMessage: @escaping (MessageId) -> Void, updateInputLanguage: @escaping ((String?) -> String?) -> Void, unarchiveChat: @escaping () -> Void, openLinkEditing: @escaping () -> Void, reportPeerIrrelevantGeoLocation: @escaping () -> Void, statuses: ChatPanelInterfaceInteractionStatuses?) {
self.setupReplyMessage = setupReplyMessage
self.setupEditMessage = setupEditMessage
self.beginMessageSelection = beginMessageSelection
@@ -74,7 +74,7 @@ final class ChatRecentActionsController: TelegramController {
}, lockMediaRecording: {
}, deleteRecordedMedia: {
}, sendRecordedMedia: {
}, displayRestrictedInfo: { _ in
}, displayRestrictedInfo: { _, _ in
}, displayVideoUnmuteTip: { _ in
}, switchMediaRecordingMode: {
}, setupMessageAutoremoveTimeout: {
@@ -348,7 +348,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate {
self.addSubnode(self.actionButtons)
self.actionButtons.micButton.recordingDisabled = { [weak self] in
self?.interfaceInteraction?.displayRestrictedInfo(.mediaRecording)
self?.interfaceInteraction?.displayRestrictedInfo(.mediaRecording, .tooltip)
}
self.actionButtons.micButton.beginRecording = { [weak self] in
@@ -1578,7 +1578,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate {
if enabled {
self.interfaceInteraction?.openStickers()
} else {
self.interfaceInteraction?.displayRestrictedInfo(.stickers)
self.interfaceInteraction?.displayRestrictedInfo(.stickers, .tooltip)
}
case .keyboard:
self.interfaceInteraction?.updateInputModeAndDismissedButtonKeyboardMessageId({ state in
@@ -387,10 +387,10 @@ private enum DeviceContactInfoEntry: ItemListNodeEntry {
arguments.performAction(.addToExisting)
})
case let .company(_, theme, title, value, selected):
return ItemListTextWithLabelItem(theme: theme, label: title, text: value, style: arguments.isPlain ? .plain : .blocks, enabledEntitiyTypes: [], multiline: true, selected: selected, sectionId: self.section, action: {
return ItemListTextWithLabelItem(theme: theme, label: title, text: value, style: arguments.isPlain ? .plain : .blocks, enabledEntityTypes: [], multiline: true, selected: selected, sectionId: self.section, action: {
}, tag: nil)
case let .phoneNumber(_, index, theme, title, label, value, selected, isInteractionEnabled):
return ItemListTextWithLabelItem(theme: theme, label: title, text: value, style: arguments.isPlain ? .plain : .blocks, textColor: .accent, enabledEntitiyTypes: [], multiline: false, selected: selected, sectionId: self.section, action: isInteractionEnabled ? {
return ItemListTextWithLabelItem(theme: theme, label: title, text: value, style: arguments.isPlain ? .plain : .blocks, textColor: .accent, enabledEntityTypes: [], multiline: false, selected: selected, sectionId: self.section, action: isInteractionEnabled ? {
if selected != nil {
arguments.toggleSelection(.phoneNumber(label, value))
} else {
@@ -424,7 +424,7 @@ private enum DeviceContactInfoEntry: ItemListNodeEntry {
arguments.addPhoneNumber()
})
case let .email(_, index, theme, title, label, value, selected):
return ItemListTextWithLabelItem(theme: theme, label: title, text: value, style: arguments.isPlain ? .plain : .blocks, textColor: .accent, enabledEntitiyTypes: [], multiline: false, selected: selected, sectionId: self.section, action: {
return ItemListTextWithLabelItem(theme: theme, label: title, text: value, style: arguments.isPlain ? .plain : .blocks, textColor: .accent, enabledEntityTypes: [], multiline: false, selected: selected, sectionId: self.section, action: {
if selected != nil {
arguments.toggleSelection(.email(label, value))
} else {
@@ -436,7 +436,7 @@ private enum DeviceContactInfoEntry: ItemListNodeEntry {
}
}, tag: DeviceContactInfoEntryTag.info(index))
case let .url(_, index, theme, title, label, value, selected):
return ItemListTextWithLabelItem(theme: theme, label: title, text: value, style: arguments.isPlain ? .plain : .blocks, textColor: .accent, enabledEntitiyTypes: [], multiline: false, selected: selected, sectionId: self.section, action: {
return ItemListTextWithLabelItem(theme: theme, label: title, text: value, style: arguments.isPlain ? .plain : .blocks, textColor: .accent, enabledEntityTypes: [], multiline: false, selected: selected, sectionId: self.section, action: {
if selected != nil {
arguments.toggleSelection(.url(label, value))
} else {
@@ -475,7 +475,7 @@ private enum DeviceContactInfoEntry: ItemListNodeEntry {
}
}, tag: DeviceContactInfoEntryTag.info(index))
case let .birthday(_, theme, title, value, text, selected):
return ItemListTextWithLabelItem(theme: theme, label: title, text: text, style: arguments.isPlain ? .plain : .blocks, textColor: .accent, enabledEntitiyTypes: [], multiline: true, selected: selected, sectionId: self.section, action: {
return ItemListTextWithLabelItem(theme: theme, label: title, text: text, style: arguments.isPlain ? .plain : .blocks, textColor: .accent, enabledEntityTypes: [], multiline: true, selected: selected, sectionId: self.section, action: {
if selected != nil {
arguments.toggleSelection(.birthday)
} else {
@@ -504,7 +504,7 @@ private enum DeviceContactInfoEntry: ItemListNodeEntry {
}
}, tag: DeviceContactInfoEntryTag.birthday)
case let .socialProfile(_, index, theme, title, value, text, selected):
return ItemListTextWithLabelItem(theme: theme, label: title, text: text, style: arguments.isPlain ? .plain : .blocks, textColor: .accent, enabledEntitiyTypes: [], multiline: true, selected: selected, sectionId: self.section, action: {
return ItemListTextWithLabelItem(theme: theme, label: title, text: text, style: arguments.isPlain ? .plain : .blocks, textColor: .accent, enabledEntityTypes: [], multiline: true, selected: selected, sectionId: self.section, action: {
if selected != nil {
arguments.toggleSelection(.socialProfile(value))
} else if value.url.count > 0 {
@@ -516,7 +516,7 @@ private enum DeviceContactInfoEntry: ItemListNodeEntry {
}
}, tag: DeviceContactInfoEntryTag.info(index))
case let .instantMessenger(_, index, theme, title, value, text, selected):
return ItemListTextWithLabelItem(theme: theme, label: title, text: text, style: arguments.isPlain ? .plain : .blocks, textColor: .accent, enabledEntitiyTypes: [], multiline: true, selected: selected, sectionId: self.section, action: {
return ItemListTextWithLabelItem(theme: theme, label: title, text: text, style: arguments.isPlain ? .plain : .blocks, textColor: .accent, enabledEntityTypes: [], multiline: true, selected: selected, sectionId: self.section, action: {
if selected != nil {
arguments.toggleSelection(.instantMessenger(value))
}
@@ -461,7 +461,7 @@ private enum GroupInfoEntry: ItemListNodeEntry {
arguments.changeProfilePhoto()
})
case let .about(theme, text):
return ItemListMultilineTextItem(theme: theme, text: foldMultipleLineBreaks(text), enabledEntitiyTypes: [.url, .mention, .hashtag], sectionId: self.section, style: .blocks, longTapAction: {
return ItemListMultilineTextItem(theme: theme, text: foldMultipleLineBreaks(text), enabledEntityTypes: [.url, .mention, .hashtag], sectionId: self.section, style: .blocks, longTapAction: {
arguments.displayAboutContextMenu(text)
}, linkItemAction: { action, itemLink in
arguments.aboutLinkAction(action, itemLink)
@@ -2362,7 +2362,7 @@ public func groupInfoController(context: AccountContext, peerId originalPeerId:
|> take(1)
|> deliverOnMainQueue).start(next: { peerView in
if let controller = controller {
handlePeerInfoAboutTextAction(context: context, peerId: peerView.peerId, navigateDisposable: navigateDisposable, controller: controller, action: action, itemLink: itemLink)
handleTextLinkAction(context: context, peerId: peerView.peerId, navigateDisposable: navigateDisposable, controller: controller, action: action, itemLink: itemLink)
}
})
}
@@ -2416,149 +2416,3 @@ public func groupInfoController(context: AccountContext, peerId originalPeerId:
}
return controller
}
func handlePeerInfoAboutTextAction(context: AccountContext, peerId: PeerId, navigateDisposable: MetaDisposable, controller: ViewController, action: TextLinkItemActionType, itemLink: TextLinkItem) {
let presentImpl: (ViewController, Any?) -> Void = { controllerToPresent, _ in
controller.present(controllerToPresent, in: .window(.root))
}
let openResolvedPeerImpl: (PeerId?, ChatControllerInteractionNavigateToPeer) -> Void = { [weak controller] peerId, navigation in
openResolvedUrl(.peer(peerId, navigation), context: context, navigationController: (controller?.navigationController as? NavigationController), openPeer: { (peerId, navigation) in
switch navigation {
case let .chat(_, messageId):
if let navigationController = controller?.navigationController as? NavigationController {
navigateToChatController(navigationController: navigationController, context: context, chatLocation: .peer(peerId), messageId: messageId, keepStack: .always)
}
case .info:
let peerSignal: Signal<Peer?, NoError>
peerSignal = context.account.postbox.loadedPeerWithId(peerId) |> map(Optional.init)
navigateDisposable.set((peerSignal |> take(1) |> deliverOnMainQueue).start(next: { peer in
if let controller = controller, let peer = peer {
if let infoController = peerInfoController(context: context, peer: peer) {
(controller.navigationController as? NavigationController)?.pushViewController(infoController)
}
}
}))
default:
break
}
}, present: presentImpl, dismissInput: {})
}
let openLinkImpl: (String) -> Void = { [weak controller] url in
navigateDisposable.set((resolveUrl(account: context.account, url: url) |> deliverOnMainQueue).start(next: { result in
if let controller = controller {
switch result {
case let .externalUrl(url):
context.sharedContext.applicationBindings.openUrl(url)
case let .peer(peerId, _):
openResolvedPeerImpl(peerId, .default)
case let .channelMessage(peerId, messageId):
if let navigationController = controller.navigationController as? NavigationController {
navigateToChatController(navigationController: navigationController, context: context, chatLocation: .peer(peerId), messageId: messageId)
}
case let .stickerPack(name):
controller.present(StickerPackPreviewController(context: context, stickerPack: .name(name), parentNavigationController: controller.navigationController as? NavigationController), in: .window(.root))
case let .instantView(webpage, anchor):
(controller.navigationController as? NavigationController)?.pushViewController(InstantPageController(context: context, webPage: webpage, sourcePeerType: .group, anchor: anchor))
case let .join(link):
controller.present(JoinLinkPreviewController(context: context, link: link, navigateToPeer: { peerId in
openResolvedPeerImpl(peerId, .chat(textInputState: nil, messageId: nil))
}), in: .window(.root))
default:
break
}
}
}))
}
let openPeerMentionImpl: (String) -> Void = { mention in
navigateDisposable.set((resolvePeerByName(account: context.account, name: mention, ageLimit: 10) |> take(1) |> deliverOnMainQueue).start(next: { peerId in
openResolvedPeerImpl(peerId, .default)
}))
}
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
switch action {
case .tap:
switch itemLink {
case let .url(url):
openLinkImpl(url)
case let .mention(mention):
openPeerMentionImpl(mention)
case let .hashtag(_, hashtag):
let peerSignal = context.account.postbox.loadedPeerWithId(peerId)
let _ = (peerSignal
|> deliverOnMainQueue).start(next: { peer in
let searchController = HashtagSearchController(context: context, peer: peer, query: hashtag)
(controller.navigationController as? NavigationController)?.pushViewController(searchController)
})
}
case .longTap:
switch itemLink {
case let .url(url):
let canOpenIn = availableOpenInOptions(context: context, item: .url(url: url)).count > 1
let openText = canOpenIn ? presentationData.strings.Conversation_FileOpenIn : presentationData.strings.Conversation_LinkDialogOpen
let actionSheet = ActionSheetController(presentationTheme: presentationData.theme)
actionSheet.setItemGroups([ActionSheetItemGroup(items: [
ActionSheetTextItem(title: url),
ActionSheetButtonItem(title: openText, color: .accent, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
openLinkImpl(url)
}),
ActionSheetButtonItem(title: presentationData.strings.ShareMenu_CopyShareLink, color: .accent, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
UIPasteboard.general.string = url
}),
ActionSheetButtonItem(title: presentationData.strings.Conversation_AddToReadingList, color: .accent, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
if let link = URL(string: url) {
let _ = try? SSReadingList.default()?.addItem(with: link, title: nil, previewText: nil)
}
})
]), ActionSheetItemGroup(items: [
ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, color: .accent, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
})
])])
controller.present(actionSheet, in: .window(.root))
case let .mention(mention):
let actionSheet = ActionSheetController(presentationTheme: presentationData.theme)
actionSheet.setItemGroups([ActionSheetItemGroup(items: [
ActionSheetTextItem(title: mention),
ActionSheetButtonItem(title: presentationData.strings.Conversation_LinkDialogOpen, color: .accent, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
openPeerMentionImpl(mention)
}),
ActionSheetButtonItem(title: presentationData.strings.Conversation_LinkDialogCopy, color: .accent, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
UIPasteboard.general.string = mention
})
]), ActionSheetItemGroup(items: [
ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, color: .accent, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
})
])])
controller.present(actionSheet, in: .window(.root))
case let .hashtag(_, hashtag):
let actionSheet = ActionSheetController(presentationTheme: presentationData.theme)
actionSheet.setItemGroups([ActionSheetItemGroup(items: [
ActionSheetTextItem(title: hashtag),
ActionSheetButtonItem(title: presentationData.strings.Conversation_LinkDialogOpen, color: .accent, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
let searchController = HashtagSearchController(context: context, peer: nil, query: hashtag)
(controller.navigationController as? NavigationController)?.pushViewController(searchController)
}),
ActionSheetButtonItem(title: presentationData.strings.Conversation_LinkDialogCopy, color: .accent, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
UIPasteboard.general.string = hashtag
})
]), ActionSheetItemGroup(items: [
ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, color: .accent, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
})
])])
controller.present(actionSheet, in: .window(.root))
}
}
}
@@ -80,9 +80,11 @@ private let iconTextBackgroundImage = generateStretchableFilledCircleImage(radiu
final class HorizontalListContextResultsChatInputPanelItemNode: ListViewItemNode {
private let imageNodeBackground: ASDisplayNode
private let imageNode: TransformImageNode
private var animationNode: AnimatedStickerNode?
private var videoLayer: (SoftwareVideoThumbnailLayer, SoftwareVideoLayerFrameManager, SampleBufferLayer)?
private var currentImageResource: TelegramMediaResource?
private var currentVideoFile: TelegramMediaFile?
private var currentAnimatedStickerFile: TelegramMediaFile?
private var resourceStatus: MediaResourceStatus?
private(set) var item: HorizontalListContextResultsChatInputPanelItem?
private var statusDisposable = MetaDisposable()
@@ -188,6 +190,7 @@ final class HorizontalListContextResultsChatInputPanelItemNode: ListViewItemNode
let imageLayout = self.imageNode.asyncLayout()
let currentImageResource = self.currentImageResource
let currentVideoFile = self.currentVideoFile
let currentAnimatedStickerFile = self.currentAnimatedStickerFile
return { [weak self] item, params, mergedTop, mergedBottom in
let height = params.width
@@ -199,6 +202,7 @@ final class HorizontalListContextResultsChatInputPanelItemNode: ListViewItemNode
var imageResource: TelegramMediaResource?
var stickerFile: TelegramMediaFile?
var animatedStickerFile: TelegramMediaFile?
var videoFile: TelegramMediaFile?
var imageDimensions: CGSize?
switch item.result {
@@ -231,7 +235,10 @@ final class HorizontalListContextResultsChatInputPanelItemNode: ListViewItemNode
} else if let largestRepresentation = largestImageRepresentation(file.previewRepresentations) {
imageDimensions = largestRepresentation.dimensions
}
if file.isSticker {
if file.isAnimatedSticker {
animatedStickerFile = file
imageResource = smallestImageRepresentation(file.previewRepresentations)?.resource
} else if file.isSticker {
stickerFile = file
imageResource = file.resource
} else {
@@ -286,6 +293,15 @@ final class HorizontalListContextResultsChatInputPanelItemNode: ListViewItemNode
updatedVideoFile = true
}
var updatedAnimatedStickerFile = false
if let currentAnimatedStickerFile = currentAnimatedStickerFile, let animatedStickerFile = animatedStickerFile {
if !currentAnimatedStickerFile.isEqual(to: animatedStickerFile) {
updatedAnimatedStickerFile = true
}
} else if (currentAnimatedStickerFile != nil) != (animatedStickerFile != nil) {
updatedAnimatedStickerFile = true
}
if updatedImageResource {
if let imageResource = imageResource {
if let stickerFile = stickerFile {
@@ -300,7 +316,6 @@ final class HorizontalListContextResultsChatInputPanelItemNode: ListViewItemNode
}
}
let nodeLayout = ListViewItemNodeLayout(contentSize: CGSize(width: height, height: croppedImageDimensions.width + sideInset), insets: UIEdgeInsets())
return (nodeLayout, { _ in
@@ -348,6 +363,30 @@ final class HorizontalListContextResultsChatInputPanelItemNode: ListViewItemNode
}
}
if updatedAnimatedStickerFile {
if let animationNode = strongSelf.animationNode {
strongSelf.animationNode = nil
animationNode.removeFromSupernode()
}
if let animatedStickerFile = animatedStickerFile {
let animationNode: AnimatedStickerNode
if let currentAnimationNode = strongSelf.animationNode {
animationNode = currentAnimationNode
} else {
animationNode = AnimatedStickerNode()
animationNode.transform = CATransform3DMakeRotation(CGFloat.pi / 2.0, 0.0, 0.0, 1.0)
animationNode.visibility = true
strongSelf.addSubnode(animationNode)
strongSelf.animationNode = animationNode
}
animationNode.started = { [weak self] in
self?.imageNode.alpha = 0.0
}
animationNode.setup(account: item.account, resource: animatedStickerFile.resource, width: 140, height: 140, mode: .cached)
}
}
let progressSize = CGSize(width: 24.0, height: 24.0)
let progressFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((nodeLayout.contentSize.width - progressSize.width) / 2.0), y: floorToScreenPixels((nodeLayout.contentSize.height - progressSize.height) / 2.0)), size: progressSize)
@@ -363,20 +402,18 @@ final class HorizontalListContextResultsChatInputPanelItemNode: ListViewItemNode
if let strongSelf = strongSelf {
strongSelf.resourceStatus = status
let state: RadialStatusNodeState
let statusForegroundColor: UIColor = .white
switch status {
case let .Fetching(_, progress):
state = .progress(color: statusForegroundColor, lineWidth: nil, value: CGFloat(max(progress, 0.2)), cancelEnabled: false)
case .Remote:
state = .download(statusForegroundColor)
case .Local:
state = .none
case let .Fetching(_, progress):
state = .progress(color: statusForegroundColor, lineWidth: nil, value: CGFloat(max(progress, 0.2)), cancelEnabled: false)
case .Remote:
state = .download(statusForegroundColor)
case .Local:
state = .none
}
strongSelf.statusNode.transitionToState(state, completion: { })
}
}
@@ -391,6 +428,12 @@ final class HorizontalListContextResultsChatInputPanelItemNode: ListViewItemNode
layer.layer.bounds = CGRect(origin: CGPoint(), size: CGSize(width: croppedImageDimensions.width, height: croppedImageDimensions.height))
layer.layer.position = CGPoint(x: height / 2.0, y: (nodeLayout.contentSize.height - sideInset) / 2.0 + sideInset)
}
if let animationNode = strongSelf.animationNode {
animationNode.bounds = CGRect(origin: CGPoint(), size: CGSize(width: croppedImageDimensions.width, height: croppedImageDimensions.height))
animationNode.position = CGPoint(x: height / 2.0, y: (nodeLayout.contentSize.height - sideInset) / 2.0 + sideInset)
animationNode.updateLayout(size: croppedImageDimensions)
}
}
})
}
@@ -302,9 +302,6 @@ final class HorizontalStickersChatContextPanelNode: ChatInputContextPanelNode {
self.backgroundNode.image = backgroundCenterImage(theme)
self.backgroundLeftNode.image = backgroundLeftImage(theme)
self.backgroundRightNode.image = backgroundLeftImage(theme)
// if let currentEntries = self.currentEntries {
// self.updateToEntries(entries: currentEntries, forceUpdate: true)
// }
}
}
@@ -191,14 +191,14 @@ private func inviteContactsEntries(accountPeer: Peer?, sortedContacts: [(DeviceC
return entries
}
private func preparedInviteContactsTransition(account: Account, from fromEntries: [InviteContactsEntry], to toEntries: [InviteContactsEntry], sortedContacts: [(DeviceContactStableId, DeviceContactBasicData, Int32)]?, interaction: InviteContactsInteraction, firstTime: Bool, isLoading: Bool, animated: Bool) -> InviteContactsTransition {
private func preparedInviteContactsTransition(account: Account, from fromEntries: [InviteContactsEntry], to toEntries: [InviteContactsEntry], sortedContacts: [(DeviceContactStableId, DeviceContactBasicData, Int32)]?, interaction: InviteContactsInteraction, isLoading: Bool, firstTime: Bool, crossfade: Bool) -> InviteContactsTransition {
let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries)
let deletions = deleteIndices.map { ListViewDeleteItem(index: $0, directionHint: nil) }
let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(account: account, interaction: interaction), directionHint: nil) }
let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(account: account, interaction: interaction), directionHint: nil) }
return InviteContactsTransition(deletions: deletions, insertions: insertions, updates: updates, sortedContacts: sortedContacts, firstTime: firstTime, isLoading: isLoading, animated: animated)
return InviteContactsTransition(deletions: deletions, insertions: insertions, updates: updates, sortedContacts: sortedContacts, isLoading: isLoading, firstTime: firstTime, crossfade: crossfade)
}
private struct InviteContactsTransition {
@@ -206,9 +206,9 @@ private struct InviteContactsTransition {
let insertions: [ListViewInsertItem]
let updates: [ListViewUpdateItem]
let sortedContacts: [(DeviceContactStableId, DeviceContactBasicData, Int32)]?
let firstTime: Bool
let isLoading: Bool
let animated: Bool
let firstTime: Bool
let crossfade: Bool
}
final class InviteContactsControllerNode: ASDisplayNode {
@@ -229,7 +229,7 @@ final class InviteContactsControllerNode: ASDisplayNode {
var requestShareTelegram: (() -> Void)?
var requestShare: (([(DeviceContactBasicData, Int32)]) -> Void)?
let currentSortedContacts = Atomic<[(DeviceContactStableId, DeviceContactBasicData, Int32)]>(value: [])
let currentSortedContacts = Atomic<[(DeviceContactStableId, DeviceContactBasicData, Int32)]?>(value: nil)
var selectionState = InviteContactsGroupSelectionState() {
didSet {
@@ -290,6 +290,7 @@ final class InviteContactsControllerNode: ASDisplayNode {
})
self.backgroundColor = self.presentationData.theme.chatList.backgroundColor
self.listNode.backgroundColor = self.presentationData.theme.chatList.backgroundColor
self.addSubnode(self.listNode)
self.addSubnode(self.countPanelNode)
@@ -310,7 +311,6 @@ final class InviteContactsControllerNode: ASDisplayNode {
})
let account = self.context.account
var firstTime: Int32 = 1
let selectionStateSignal = self.selectionStatePromise.get()
let transition: Signal<InviteContactsTransition, NoError>
let themeAndStringsPromise = self.themeAndStringsPromise
@@ -387,36 +387,24 @@ final class InviteContactsControllerNode: ASDisplayNode {
return result
}
}
|> beforeNext { sortedContacts in
if let sortedContacts = sortedContacts {
let _ = currentSortedContacts.swap(sortedContacts)
}
}
let processingQueue = Queue()
transition = (combineLatest(.single(nil) |> then(sortedContacts), selectionStateSignal, themeAndStringsPromise.get())
|> mapToQueue { sortedContacts, selectionState, themeAndStrings -> Signal<InviteContactsTransition, NoError> in
transition = (combineLatest(.single(nil) |> then(sortedContacts), selectionStateSignal, themeAndStringsPromise.get(), .single(true) |> delay(0.2, queue: Queue.mainQueue()))
|> mapToQueue { sortedContacts, selectionState, themeAndStrings, ready -> Signal<InviteContactsTransition, NoError> in
guard sortedContacts != nil || ready else {
return .never()
}
let signal = deferred { () -> Signal<InviteContactsTransition, NoError> in
let entries = inviteContactsEntries(accountPeer: nil, sortedContacts: sortedContacts, selectionState: selectionState, theme: themeAndStrings.0, strings: themeAndStrings.1, nameSortOrder: themeAndStrings.2, nameDisplayOrder: themeAndStrings.3, interaction: interaction)
let previous = previousEntries.swap(entries)
let animated: Bool
if let previous = previous {
animated = (entries.count - previous.count) < 20
} else {
animated = false
}
return .single(preparedInviteContactsTransition(account: context.account, from: previous ?? [], to: entries, sortedContacts: sortedContacts, interaction: interaction, firstTime: previous == nil, isLoading: false, animated: animated))
let previousContacts = currentSortedContacts.with { $0 }
let crossfade = previous != nil && previousContacts == nil
return .single(preparedInviteContactsTransition(account: context.account, from: previous ?? [], to: entries, sortedContacts: sortedContacts, interaction: interaction, isLoading: sortedContacts == nil, firstTime: previous == nil, crossfade: crossfade))
}
return signal
|> runOn(processingQueue)
// if OSAtomicCompareAndSwap32(1, 0, &firstTime) {
// return signal
// |> runOn(Queue.mainQueue())
// } else {
// return signal
// |> runOn(processingQueue)
// }
})
|> deliverOnMainQueue
@@ -424,12 +412,10 @@ final class InviteContactsControllerNode: ASDisplayNode {
self?.enqueueTransition(transition)
})
self.enqueueTransition(InviteContactsTransition(deletions: [], insertions: [], updates: [], sortedContacts: [], firstTime: true, isLoading: true, animated: false))
shareImpl = { [weak self] in
if let strongSelf = self {
var result: [(DeviceContactBasicData, Int32)] = []
for contact in (strongSelf.currentSortedContacts.with { $0 }) {
for contact in (strongSelf.currentSortedContacts.with { $0 } ?? []) {
if strongSelf.selectionState.selectedContactIndices[contact.0] != nil {
result.append((contact.1, contact.2))
}
@@ -448,6 +434,7 @@ final class InviteContactsControllerNode: ASDisplayNode {
private func updateThemeAndStrings() {
self.backgroundColor = self.presentationData.theme.chatList.backgroundColor
self.listNode.backgroundColor = self.presentationData.theme.chatList.backgroundColor
self.searchDisplayController?.updatePresentationData(self.presentationData)
}
@@ -571,8 +558,8 @@ final class InviteContactsControllerNode: ASDisplayNode {
if transition.firstTime {
options.insert(.Synchronous)
options.insert(.LowLatency)
} else if transition.animated {
options.insert(.AnimateInsertion)
} else if transition.crossfade {
options.insert(.AnimateCrossfade)
}
self.listNode.transaction(deleteIndices: transition.deletions, insertIndicesAndItems: transition.insertions, updateIndicesAndItems: transition.updates, options: options, updateOpaqueState: nil, completion: { [weak self] _ in
if let strongSelf = self {
@@ -582,10 +569,16 @@ final class InviteContactsControllerNode: ASDisplayNode {
let activityIndicator = ActivityIndicator(type: .custom(strongSelf.presentationData.theme.list.itemAccentColor, 22.0, 1.0, false))
strongSelf.activityIndicator = activityIndicator
strongSelf.insertSubnode(activityIndicator, aboveSubnode: strongSelf.listNode)
if let (layout, navigationHeight, actualNavigationBarHeight) = strongSelf.validLayout {
strongSelf.containerLayoutUpdated(layout, navigationBarHeight: navigationHeight, actualNavigationBarHeight: actualNavigationBarHeight, transition: .immediate)
}
} else if !transition.isLoading, let activityIndicator = strongSelf.activityIndicator {
strongSelf.activityIndicator = nil
activityIndicator.removeFromSupernode()
}
let _ = strongSelf.currentSortedContacts.swap(transition.sortedContacts)
}
})
}
@@ -593,7 +586,7 @@ final class InviteContactsControllerNode: ASDisplayNode {
}
func selectAll() {
let ids = self.currentSortedContacts.with { $0 }.map { $0.0 }
let ids = self.currentSortedContacts.with { $0 }?.map { $0.0 } ?? []
var allSelected = true
for id in ids {
if self.selectionState.selectedContactIndices[id] == nil {
@@ -19,7 +19,7 @@ enum TextLinkItem {
class ItemListMultilineTextItem: ListViewItem, ItemListItem {
let theme: PresentationTheme
let text: String
let enabledEntitiyTypes: EnabledEntityTypes
let enabledEntityTypes: EnabledEntityTypes
let sectionId: ItemListSectionId
let style: ItemListStyle
let action: (() -> Void)?
@@ -30,10 +30,10 @@ class ItemListMultilineTextItem: ListViewItem, ItemListItem {
let selectable: Bool
init(theme: PresentationTheme, text: String, enabledEntitiyTypes: EnabledEntityTypes, sectionId: ItemListSectionId, style: ItemListStyle, action: (() -> Void)? = nil, longTapAction: (() -> Void)? = nil, linkItemAction: ((TextLinkItemActionType, TextLinkItem) -> Void)? = nil, tag: Any? = nil) {
init(theme: PresentationTheme, text: String, enabledEntityTypes: EnabledEntityTypes, sectionId: ItemListSectionId, style: ItemListStyle, action: (() -> Void)? = nil, longTapAction: (() -> Void)? = nil, linkItemAction: ((TextLinkItemActionType, TextLinkItem) -> Void)? = nil, tag: Any? = nil) {
self.theme = theme
self.text = text
self.enabledEntitiyTypes = enabledEntitiyTypes
self.enabledEntityTypes = enabledEntityTypes
self.sectionId = sectionId
self.style = style
self.action = action
@@ -184,7 +184,7 @@ class ItemListMultilineTextItemNode: ListViewItemNode {
leftInset = 16.0 + params.rightInset
}
let entities = generateTextEntities(item.text, enabledTypes: item.enabledEntitiyTypes)
let entities = generateTextEntities(item.text, enabledTypes: item.enabledEntityTypes)
let string = stringWithAppliedEntities(item.text, entities: entities, baseColor: textColor, linkColor: item.theme.list.itemAccentColor, baseFont: titleFont, linkFont: titleFont, boldFont: titleBoldFont, italicFont: titleItalicFont, boldItalicFont: titleBoldItalicFont, fixedFont: titleFixedFont)
let (titleLayout, titleApply) = makeTextLayout(TextNodeLayoutArguments(attributedString: string, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: params.width - params.leftInset - params.rightInset - 20.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
@@ -121,6 +121,8 @@ private let titleFont = Font.bold(15.0)
private let statusFont = Font.regular(14.0)
class ItemListStickerPackItemNode: ItemListRevealOptionsItemNode {
private var currentThumbnailItem: StickerPackThumbnailItem?
private let backgroundNode: ASDisplayNode
private let topStripeNode: ASDisplayNode
private let bottomStripeNode: ASDisplayNode
@@ -128,6 +130,7 @@ class ItemListStickerPackItemNode: ItemListRevealOptionsItemNode {
private var disabledOverlayNode: ASDisplayNode?
fileprivate let imageNode: TransformImageNode
private var animationNode: AnimatedStickerNode?
private let unreadNode: ASImageNode
private let titleNode: TextNode
private let statusNode: TextNode
@@ -153,6 +156,17 @@ class ItemListStickerPackItemNode: ItemListRevealOptionsItemNode {
}
}
override var visibility: ListViewItemNodeVisibility {
didSet {
let wasVisible = oldValue != .none
let isVisible = self.visibility != .none
if wasVisible != isVisible {
self.animationNode?.visibility = isVisible
}
}
}
init() {
self.backgroundNode = ASDisplayNode()
self.backgroundNode.isLayerBacked = true
@@ -230,13 +244,7 @@ class ItemListStickerPackItemNode: ItemListRevealOptionsItemNode {
let editableControlLayout = ItemListEditableControlNode.asyncLayout(self.editableControlNode)
let reorderControlLayout = ItemListEditableReorderControlNode.asyncLayout(self.reorderControlNode)
var previousThumbnailItem: TelegramMediaImageRepresentation?
if let thumbnail = self.layoutParams?.0.packInfo.thumbnail {
previousThumbnailItem = thumbnail
} else if let item = self.layoutParams?.0.topItem, let dimensions = item.file.dimensions, let resource = chatMessageStickerResource(file: item.file, small: true) as? TelegramMediaResource {
previousThumbnailItem = TelegramMediaImageRepresentation(dimensions: dimensions, resource: resource)
}
var previousThumbnailItem = self.currentThumbnailItem
var currentDisabledOverlayNode = self.disabledOverlayNode
let currentItem = self.layoutParams?.0
@@ -324,31 +332,47 @@ class ItemListStickerPackItemNode: ItemListRevealOptionsItemNode {
currentDisabledOverlayNode = nil
}
var thumbnailItem: TelegramMediaImageRepresentation?
var thumbnailItem: StickerPackThumbnailItem?
var resourceReference: MediaResourceReference?
if let thumbnail = item.packInfo.thumbnail {
thumbnailItem = thumbnail
resourceReference = MediaResourceReference.stickerPackThumbnail(stickerPack: .id(id: item.packInfo.id.id, accessHash: item.packInfo.accessHash), resource: thumbnail.resource)
} else if let item = item.topItem, let dimensions = item.file.dimensions, let resource = chatMessageStickerResource(file: item.file, small: true) as? TelegramMediaResource {
thumbnailItem = TelegramMediaImageRepresentation(dimensions: dimensions, resource: resource)
resourceReference = MediaResourceReference.media(media: .standalone(media: item.file), resource: resource)
}
let fileUpdated = thumbnailItem != previousThumbnailItem
var imageApply: (() -> Void)?
var imageSize: CGSize = CGSize(width: 34.0, height: 34.0)
if let thumbnailItem = thumbnailItem {
let imageBoundingSize = CGSize(width: 34.0, height: 34.0)
imageSize = thumbnailItem.dimensions.aspectFitted(imageBoundingSize)
imageApply = makeImageLayout(TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: imageSize, intrinsicInsets: UIEdgeInsets()))
if item.packInfo.flags.contains(.isAnimated) {
thumbnailItem = .animated(thumbnail.resource)
resourceReference = MediaResourceReference.stickerPackThumbnail(stickerPack: .id(id: item.packInfo.id.id, accessHash: item.packInfo.accessHash), resource: thumbnail.resource)
} else {
thumbnailItem = .still(thumbnail)
resourceReference = MediaResourceReference.stickerPackThumbnail(stickerPack: .id(id: item.packInfo.id.id, accessHash: item.packInfo.accessHash), resource: thumbnail.resource)
}
} else if let item = item.topItem {
if item.file.isAnimatedSticker {
thumbnailItem = .animated(item.file.resource)
resourceReference = MediaResourceReference.media(media: .standalone(media: item.file), resource: item.file.resource)
} else if let dimensions = item.file.dimensions, let resource = chatMessageStickerResource(file: item.file, small: true) as? TelegramMediaResource {
thumbnailItem = .still(TelegramMediaImageRepresentation(dimensions: dimensions, resource: resource))
resourceReference = MediaResourceReference.media(media: .standalone(media: item.file), resource: resource)
}
}
var updatedImageSignal: Signal<(TransformImageArguments) -> DrawingContext?, NoError>?
var updatedFetchSignal: Signal<FetchResourceSourceType, FetchResourceError>?
let imageBoundingSize = CGSize(width: 34.0, height: 34.0)
var imageApply: (() -> Void)?
let fileUpdated = thumbnailItem != previousThumbnailItem
var imageSize: CGSize?
if fileUpdated {
if let thumbnailItem = thumbnailItem {
updatedImageSignal = chatMessageStickerPackThumbnail(postbox: item.account.postbox, representation: thumbnailItem)
switch thumbnailItem {
case let .still(representation):
let stillImageSize = representation.dimensions.aspectFitted(imageBoundingSize)
imageSize = stillImageSize
imageApply = makeImageLayout(TransformImageArguments(corners: ImageCorners(), imageSize: stillImageSize, boundingSize: stillImageSize, intrinsicInsets: UIEdgeInsets()))
updatedImageSignal = chatMessageStickerPackThumbnail(postbox: item.account.postbox, representation: representation)
case .animated:
imageSize = imageBoundingSize
}
if let resourceReference = resourceReference {
updatedFetchSignal = fetchedMediaResource(postbox: item.account.postbox, reference: resourceReference)
}
@@ -361,6 +385,10 @@ class ItemListStickerPackItemNode: ItemListRevealOptionsItemNode {
return (layout, { [weak self] animated in
if let strongSelf = self {
strongSelf.layoutParams = (item, params, neighbors)
if fileUpdated {
strongSelf.currentThumbnailItem = thumbnailItem
}
if let _ = updatedTheme {
strongSelf.topStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor
@@ -516,7 +544,27 @@ class ItemListStickerPackItemNode: ItemListRevealOptionsItemNode {
transition.updateFrame(node: strongSelf.statusNode, frame: CGRect(origin: CGPoint(x: leftInset + revealOffset + editingOffset, y: 32.0), size: statusLayout.size))
let boundingSize = CGSize(width: 34.0, height: 34.0)
transition.updateFrame(node: strongSelf.imageNode, frame: CGRect(origin: CGPoint(x: params.leftInset + revealOffset + editingOffset + 15.0 + floor((boundingSize.width - imageSize.width) / 2.0), y: 11.0 + floor((boundingSize.height - imageSize.height) / 2.0)), size: imageSize))
if let thumbnailItem = thumbnailItem, let imageSize = imageSize {
let imageFrame = CGRect(origin: CGPoint(x: params.leftInset + revealOffset + editingOffset + 15.0 + floor((boundingSize.width - imageSize.width) / 2.0), y: 11.0 + floor((boundingSize.height - imageSize.height) / 2.0)), size: imageSize)
switch thumbnailItem {
case .still:
transition.updateFrame(node: strongSelf.imageNode, frame: imageFrame)
case let .animated(resource):
let animationNode: AnimatedStickerNode
if let current = strongSelf.animationNode {
animationNode = current
} else {
animationNode = AnimatedStickerNode()
strongSelf.animationNode = animationNode
strongSelf.addSubnode(animationNode)
animationNode.setup(account: item.account, resource: resource, width: 80, height: 80, mode: .cached)
animationNode.visibility = strongSelf.visibility != .none
}
if let animationNode = strongSelf.animationNode {
transition.updateFrame(node: animationNode, frame: imageFrame)
}
}
}
if let updatedImageSignal = updatedImageSignal {
strongSelf.imageNode.setSignal(updatedImageSignal)
@@ -18,7 +18,7 @@ final class ItemListTextWithLabelItem: ListViewItem, ItemListItem {
let style: ItemListStyle
let labelColor: ItemListTextWithLabelItemTextColor
let textColor: ItemListTextWithLabelItemTextColor
let enabledEntitiyTypes: EnabledEntityTypes
let enabledEntityTypes: EnabledEntityTypes
let multiline: Bool
let selected: Bool?
let sectionId: ItemListSectionId
@@ -28,14 +28,14 @@ final class ItemListTextWithLabelItem: ListViewItem, ItemListItem {
let tag: Any?
init(theme: PresentationTheme, label: String, text: String, style: ItemListStyle = .plain, labelColor: ItemListTextWithLabelItemTextColor = .primary, textColor: ItemListTextWithLabelItemTextColor = .primary, enabledEntitiyTypes: EnabledEntityTypes, multiline: Bool, selected: Bool? = nil, sectionId: ItemListSectionId, action: (() -> Void)?, longTapAction: (() -> Void)? = nil, linkItemAction: ((TextLinkItemActionType, TextLinkItem) -> Void)? = nil, tag: Any? = nil) {
init(theme: PresentationTheme, label: String, text: String, style: ItemListStyle = .plain, labelColor: ItemListTextWithLabelItemTextColor = .primary, textColor: ItemListTextWithLabelItemTextColor = .primary, enabledEntityTypes: EnabledEntityTypes, multiline: Bool, selected: Bool? = nil, sectionId: ItemListSectionId, action: (() -> Void)?, longTapAction: (() -> Void)? = nil, linkItemAction: ((TextLinkItemActionType, TextLinkItem) -> Void)? = nil, tag: Any? = nil) {
self.theme = theme
self.label = label
self.text = text
self.style = style
self.labelColor = labelColor
self.textColor = textColor
self.enabledEntitiyTypes = enabledEntitiyTypes
self.enabledEntityTypes = enabledEntityTypes
self.multiline = multiline
self.selected = selected
self.sectionId = sectionId
@@ -199,7 +199,7 @@ class ItemListTextWithLabelItemNode: ListViewItemNode {
}
let (labelLayout, labelApply) = makeLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.label, font: labelFont, textColor: labelColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftOffset - leftInset - rightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
let entities = generateTextEntities(item.text, enabledTypes: item.enabledEntitiyTypes)
let entities = generateTextEntities(item.text, enabledTypes: item.enabledEntityTypes)
let baseColor: UIColor
switch item.textColor {
case .primary:
@@ -66,7 +66,7 @@ private let titleFont = Font.bold(16.0)
private let statusFont = Font.regular(15.0)
private let buttonFont = Font.medium(13.0)
private final class TrendingTopItemNode: ASDisplayNode {
final class TrendingTopItemNode: ASDisplayNode {
private let imageNode: TransformImageNode
private var animationNode: AnimatedStickerNode?
public private(set) var file: TelegramMediaFile? = nil
@@ -346,7 +346,7 @@ public class PeerMediaCollectionController: TelegramController {
}, lockMediaRecording: {
}, deleteRecordedMedia: {
}, sendRecordedMedia: {
}, displayRestrictedInfo: { _ in
}, displayRestrictedInfo: { _, _ in
}, displayVideoUnmuteTip: { _ in
}, switchMediaRecordingMode: {
}, setupMessageAutoremoveTimeout: {
@@ -78,30 +78,6 @@ private let titleFont = Font.bold(16.0)
private let statusFont = Font.regular(15.0)
private let buttonFont = Font.medium(13.0)
private final class TrendingTopItemNode: TransformImageNode {
var file: TelegramMediaFile? = nil
let loadDisposable = MetaDisposable()
var currentIsPreviewing = false
func updatePreviewing(animated: Bool, isPreviewing: Bool) {
if self.currentIsPreviewing != isPreviewing {
self.currentIsPreviewing = isPreviewing
if isPreviewing {
if animated {
self.layer.animateSpring(from: 1.0 as NSNumber, to: 0.8 as NSNumber, keyPath: "transform.scale", duration: 0.4, removeOnCompletion: false)
}
} else {
self.layer.removeAnimation(forKey: "transform.scale")
if animated {
self.layer.animateSpring(from: 0.8 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.5)
}
}
}
}
}
class StickerPaneSearchGlobalItemNode: GridItemNode {
private let titleNode: TextNode
private let descriptionNode: TextNode
@@ -115,6 +91,16 @@ class StickerPaneSearchGlobalItemNode: GridItemNode {
private var appliedItem: StickerPaneSearchGlobalItem?
private let preloadDisposable = MetaDisposable()
override var isVisibleInGrid: Bool {
didSet {
if oldValue != self.isVisibleInGrid {
for node in self.itemNodes {
node.visibility = self.isVisibleInGrid
}
}
}
}
override init() {
self.titleNode = TextNode()
self.titleNode.isUserInteractionEnabled = false
@@ -270,9 +256,14 @@ class StickerPaneSearchGlobalItemNode: GridItemNode {
strongSelf.unreadNode.frame = CGRect(origin: CGPoint(x: titleFrame.maxX + 2.0, y: titleFrame.minY + 7.0), size: image.size)
}
var offset: CGFloat = params.leftInset + leftInset
let itemSize = CGSize(width: 68.0, height: 68.0)
let sideInset: CGFloat = 2.0
let availableWidth = params.width - params.leftInset - params.rightInset - sideInset * 2.0
var itemSide: CGFloat = floor(availableWidth / 5.0)
itemSide = min(itemSide, 75.0)
let itemSize = CGSize(width: itemSide, height: itemSide)
var offset = sideInset
let itemSpacing = (max(0, availableWidth - 5.0 * itemSide - sideInset * 2.0)) / 4.0
for i in 0 ..< topItems.count {
let file = topItems[i].file
let node: TrendingTopItemNode
@@ -280,20 +271,17 @@ class StickerPaneSearchGlobalItemNode: GridItemNode {
node = strongSelf.itemNodes[i]
} else {
node = TrendingTopItemNode()
node.contentAnimations = [.subsequentUpdates]
node.visibility = strongSelf.isVisibleInGrid
strongSelf.itemNodes.append(node)
strongSelf.addSubnode(node)
}
if file.fileId != node.file?.fileId {
node.file = file
node.setSignal(chatMessageSticker(account: item.account, file: file, small: true))
node.loadDisposable.set(freeMediaFileResourceInteractiveFetched(account: item.account, fileReference: stickerPackFileReference(file), resource: chatMessageStickerResource(file: file, small: true)).start())
node.setup(account: item.account, item: topItems[i], itemSize: itemSize, synchronousLoads: false)
}
if let dimensions = file.dimensions {
let imageSize = dimensions.aspectFitted(itemSize)
node.asyncLayout()(TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: imageSize, intrinsicInsets: UIEdgeInsets()))()
node.frame = CGRect(origin: CGPoint(x: offset, y: 48.0 + topOffset), size: imageSize)
offset += imageSize.width + 4.0
offset += itemSize.width + itemSpacing
}
}
@@ -0,0 +1,157 @@
import Foundation
import UIKit
import TelegramCore
import Postbox
import Display
import SwiftSignalKit
import TelegramUIPreferences
import SafariServices
func handleTextLinkAction(context: AccountContext, peerId: PeerId?, navigateDisposable: MetaDisposable, controller: ViewController, action: TextLinkItemActionType, itemLink: TextLinkItem) {
let presentImpl: (ViewController, Any?) -> Void = { controllerToPresent, _ in
controller.present(controllerToPresent, in: .window(.root))
}
let openResolvedPeerImpl: (PeerId?, ChatControllerInteractionNavigateToPeer) -> Void = { [weak controller] peerId, navigation in
openResolvedUrl(.peer(peerId, navigation), context: context, navigationController: (controller?.navigationController as? NavigationController), openPeer: { (peerId, navigation) in
switch navigation {
case let .chat(_, messageId):
if let navigationController = controller?.navigationController as? NavigationController {
navigateToChatController(navigationController: navigationController, context: context, chatLocation: .peer(peerId), messageId: messageId, keepStack: .always)
}
case .info:
let peerSignal: Signal<Peer?, NoError>
peerSignal = context.account.postbox.loadedPeerWithId(peerId) |> map(Optional.init)
navigateDisposable.set((peerSignal |> take(1) |> deliverOnMainQueue).start(next: { peer in
if let controller = controller, let peer = peer {
if let infoController = peerInfoController(context: context, peer: peer) {
(controller.navigationController as? NavigationController)?.pushViewController(infoController)
}
}
}))
default:
break
}
}, present: presentImpl, dismissInput: {})
}
let openLinkImpl: (String) -> Void = { [weak controller] url in
navigateDisposable.set((resolveUrl(account: context.account, url: url) |> deliverOnMainQueue).start(next: { result in
if let controller = controller {
switch result {
case let .externalUrl(url):
context.sharedContext.applicationBindings.openUrl(url)
case let .peer(peerId, _):
openResolvedPeerImpl(peerId, .default)
case let .channelMessage(peerId, messageId):
if let navigationController = controller.navigationController as? NavigationController {
navigateToChatController(navigationController: navigationController, context: context, chatLocation: .peer(peerId), messageId: messageId)
}
case let .stickerPack(name):
controller.present(StickerPackPreviewController(context: context, stickerPack: .name(name), parentNavigationController: controller.navigationController as? NavigationController), in: .window(.root))
case let .instantView(webpage, anchor):
(controller.navigationController as? NavigationController)?.pushViewController(InstantPageController(context: context, webPage: webpage, sourcePeerType: .group, anchor: anchor))
case let .join(link):
controller.present(JoinLinkPreviewController(context: context, link: link, navigateToPeer: { peerId in
openResolvedPeerImpl(peerId, .chat(textInputState: nil, messageId: nil))
}), in: .window(.root))
default:
break
}
}
}))
}
let openPeerMentionImpl: (String) -> Void = { mention in
navigateDisposable.set((resolvePeerByName(account: context.account, name: mention, ageLimit: 10) |> take(1) |> deliverOnMainQueue).start(next: { peerId in
openResolvedPeerImpl(peerId, .default)
}))
}
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
switch action {
case .tap:
switch itemLink {
case let .url(url):
openLinkImpl(url)
case let .mention(mention):
openPeerMentionImpl(mention)
case let .hashtag(_, hashtag):
if let peerId = peerId {
let peerSignal = context.account.postbox.loadedPeerWithId(peerId)
let _ = (peerSignal
|> deliverOnMainQueue).start(next: { peer in
let searchController = HashtagSearchController(context: context, peer: peer, query: hashtag)
(controller.navigationController as? NavigationController)?.pushViewController(searchController)
})
}
}
case .longTap:
switch itemLink {
case let .url(url):
let canOpenIn = availableOpenInOptions(context: context, item: .url(url: url)).count > 1
let openText = canOpenIn ? presentationData.strings.Conversation_FileOpenIn : presentationData.strings.Conversation_LinkDialogOpen
let actionSheet = ActionSheetController(presentationTheme: presentationData.theme)
actionSheet.setItemGroups([ActionSheetItemGroup(items: [
ActionSheetTextItem(title: url),
ActionSheetButtonItem(title: openText, color: .accent, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
openLinkImpl(url)
}),
ActionSheetButtonItem(title: presentationData.strings.ShareMenu_CopyShareLink, color: .accent, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
UIPasteboard.general.string = url
}),
ActionSheetButtonItem(title: presentationData.strings.Conversation_AddToReadingList, color: .accent, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
if let link = URL(string: url) {
let _ = try? SSReadingList.default()?.addItem(with: link, title: nil, previewText: nil)
}
})
]), ActionSheetItemGroup(items: [
ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, color: .accent, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
})
])])
controller.present(actionSheet, in: .window(.root))
case let .mention(mention):
let actionSheet = ActionSheetController(presentationTheme: presentationData.theme)
actionSheet.setItemGroups([ActionSheetItemGroup(items: [
ActionSheetTextItem(title: mention),
ActionSheetButtonItem(title: presentationData.strings.Conversation_LinkDialogOpen, color: .accent, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
openPeerMentionImpl(mention)
}),
ActionSheetButtonItem(title: presentationData.strings.Conversation_LinkDialogCopy, color: .accent, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
UIPasteboard.general.string = mention
})
]), ActionSheetItemGroup(items: [
ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, color: .accent, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
})
])])
controller.present(actionSheet, in: .window(.root))
case let .hashtag(_, hashtag):
let actionSheet = ActionSheetController(presentationTheme: presentationData.theme)
actionSheet.setItemGroups([ActionSheetItemGroup(items: [
ActionSheetTextItem(title: hashtag),
ActionSheetButtonItem(title: presentationData.strings.Conversation_LinkDialogOpen, color: .accent, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
let searchController = HashtagSearchController(context: context, peer: nil, query: hashtag)
(controller.navigationController as? NavigationController)?.pushViewController(searchController)
}),
ActionSheetButtonItem(title: presentationData.strings.Conversation_LinkDialogCopy, color: .accent, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
UIPasteboard.general.string = hashtag
})
]), ActionSheetItemGroup(items: [
ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, color: .accent, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
})
])])
controller.present(actionSheet, in: .window(.root))
}
}
}
@@ -322,9 +322,17 @@ public func themeSettingsController(context: AccountContext, focusOnItemTag: The
let _ = telegramWallpapers(postbox: context.account.postbox, network: context.account.network).start()
let availableAppIcons: Signal<[PresentationAppIcon], NoError> = .single(context.sharedContext.applicationBindings.getAvailableAlternateIcons())
let currentAppIcon: PresentationAppIcon?
let appIcons = context.sharedContext.applicationBindings.getAvailableAlternateIcons()
if let alternateIconName = context.sharedContext.applicationBindings.getAlternateIconName() {
currentAppIcon = appIcons.filter { $0.name == alternateIconName }.first
} else {
currentAppIcon = appIcons.filter { $0.isDefault }.first
}
let availableAppIcons: Signal<[PresentationAppIcon], NoError> = .single(appIcons)
let currentAppIconName = ValuePromise<String?>()
currentAppIconName.set(context.sharedContext.applicationBindings.getAlternateIconName() ?? "Blue")
currentAppIconName.set(currentAppIcon?.name ?? "Blue")
let arguments = ThemeSettingsControllerArguments(context: context, selectTheme: { theme in
let _ = (context.sharedContext.accountManager.transaction { transaction -> Void in
@@ -8,9 +8,11 @@ import TelegramPresentationData
private final class UpdateInfoControllerArguments {
let openAppStorePage: () -> Void
let linkAction: (TextLinkItemActionType, TextLinkItem) -> Void
init(openAppStorePage: @escaping () -> Void) {
init(openAppStorePage: @escaping () -> Void, linkAction: @escaping (TextLinkItemActionType, TextLinkItem) -> Void) {
self.openAppStorePage = openAppStorePage
self.linkAction = linkAction
}
}
@@ -20,7 +22,7 @@ private enum UpdateInfoControllerSection: Int32 {
}
private enum UpdateInfoControllerEntry: ItemListNodeEntry {
case info(PresentationTheme, String, String, [MessageTextEntity])
case info(PresentationTheme, PresentationAppIcon?, String, String, [MessageTextEntity])
case update(PresentationTheme, String)
var section: ItemListSectionId {
@@ -43,8 +45,8 @@ private enum UpdateInfoControllerEntry: ItemListNodeEntry {
static func ==(lhs: UpdateInfoControllerEntry, rhs: UpdateInfoControllerEntry) -> Bool {
switch lhs {
case let .info(lhsTheme, lhsTitle, lhsText, lhsEntities):
if case let .info(rhsTheme, rhsTitle, rhsText, rhsEntities) = rhs, lhsTheme === rhsTheme, lhsTitle == rhsTitle, lhsText == rhsText, lhsEntities == rhsEntities {
case let .info(lhsTheme, lhsIcon, lhsTitle, lhsText, lhsEntities):
if case let .info(rhsTheme, rhsIcon, rhsTitle, rhsText, rhsEntities) = rhs, lhsTheme === rhsTheme, lhsIcon == rhsIcon, lhsTitle == rhsTitle, lhsText == rhsText, lhsEntities == rhsEntities {
return true
} else {
return false
@@ -64,9 +66,10 @@ private enum UpdateInfoControllerEntry: ItemListNodeEntry {
func item(_ arguments: UpdateInfoControllerArguments) -> ListViewItem {
switch self {
case let .info(theme, title, text, entities):
let text = stringWithAppliedEntities(text, entities: entities, baseColor: theme.list.itemPrimaryTextColor, linkColor: theme.list.itemAccentColor, baseFont: Font.regular(14.0), linkFont: Font.regular(14.0), boldFont: Font.bold(14.0), italicFont: Font.italic(14.0), boldItalicFont: Font.semiboldItalic(14.0), fixedFont: Font.monospace(14.0))
return ItemListSectionHeaderItem(theme: theme, text: text.string, sectionId: self.section)
case let .info(theme, icon, title, text, entities):
return UpdateInfoItem(theme: theme, appIcon: icon, title: title, text: text, entities: entities, sectionId: self.section, style: .blocks, linkItemAction: { action, itemLink in
arguments.linkAction(action, itemLink)
})
case let .update(theme, title):
return ItemListActionItem(theme: theme, title: title, kind: .generic, alignment: .center, sectionId: self.section, style: .blocks, action: {
arguments.openAppStorePage()
@@ -75,10 +78,10 @@ private enum UpdateInfoControllerEntry: ItemListNodeEntry {
}
}
private func updateInfoControllerEntries(theme: PresentationTheme, strings: PresentationStrings, appUpdateInfo: AppUpdateInfo) -> [UpdateInfoControllerEntry] {
private func updateInfoControllerEntries(theme: PresentationTheme, strings: PresentationStrings, appIcon: PresentationAppIcon?, appUpdateInfo: AppUpdateInfo) -> [UpdateInfoControllerEntry] {
var entries: [UpdateInfoControllerEntry] = []
entries.append(.info(theme, strings.Update_AppVersion(appUpdateInfo.version).0, appUpdateInfo.text, appUpdateInfo.entities))
entries.append(.info(theme, appIcon, strings.Update_AppVersion(appUpdateInfo.version).0, appUpdateInfo.text, appUpdateInfo.entities))
entries.append(.update(theme, strings.Update_UpdateApp))
return entries
@@ -86,24 +89,48 @@ private func updateInfoControllerEntries(theme: PresentationTheme, strings: Pres
public func updateInfoController(context: AccountContext, appUpdateInfo: AppUpdateInfo) -> ViewController {
var dismissImpl: (() -> Void)?
var linkActionImpl: ((TextLinkItemActionType, TextLinkItem) -> Void)?
let actionsDisposable = DisposableSet()
let navigateDisposable = MetaDisposable()
actionsDisposable.add(navigateDisposable)
let arguments = UpdateInfoControllerArguments(openAppStorePage: {
context.sharedContext.applicationBindings.openAppStorePage()
}, linkAction: { action, itemLink in
linkActionImpl?(action, itemLink)
})
let signal = context.sharedContext.presentationData
|> deliverOnMainQueue
|> map { presentationData -> (ItemListControllerState, (ItemListNodeState<UpdateInfoControllerEntry>, UpdateInfoControllerEntry.ItemGenerationArguments)) in
let leftNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Update_Skip), style: .regular, enabled: true, action: {
let appIcon: PresentationAppIcon?
let appIcons = context.sharedContext.applicationBindings.getAvailableAlternateIcons()
if let alternateIconName = context.sharedContext.applicationBindings.getAlternateIconName() {
appIcon = appIcons.filter { $0.name == alternateIconName }.first
} else {
appIcon = appIcons.filter { $0.isDefault }.first
}
let leftNavigationButton = appUpdateInfo.popup ? ItemListNavigationButton(content: .text(presentationData.strings.Update_Skip), style: .regular, enabled: true, action: {
dismissImpl?()
})
}) : nil
let controllerState = ItemListControllerState(theme: presentationData.theme, title: .text(presentationData.strings.Update_Title), leftNavigationButton: leftNavigationButton, rightNavigationButton: nil, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back))
let listState = ItemListNodeState(entries: updateInfoControllerEntries(theme: presentationData.theme, strings: presentationData.strings, appUpdateInfo: appUpdateInfo), style: .blocks, animateChanges: false)
let listState = ItemListNodeState(entries: updateInfoControllerEntries(theme: presentationData.theme, strings: presentationData.strings, appIcon: appIcon, appUpdateInfo: appUpdateInfo), style: .blocks, animateChanges: false)
return (controllerState, (listState, arguments))
}
|> afterDisposed {
actionsDisposable.dispose()
}
let controller = ItemListController(sharedContext: context.sharedContext, state: signal)
linkActionImpl = { [weak controller] action, itemLink in
if let strongController = controller {
handleTextLinkAction(context: context, peerId: nil, navigateDisposable: navigateDisposable, controller: strongController, action: action, itemLink: itemLink)
}
}
dismissImpl = { [weak controller] in
controller?.view.endEditing(true)
controller?.dismiss()
@@ -0,0 +1,443 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import SwiftSignalKit
import TelegramCore
import TelegramPresentationData
private func generateBorderImage(theme: PresentationTheme, bordered: Bool) -> UIImage? {
return generateImage(CGSize(width: 30.0, height: 30.0), rotatedContext: { size, context in
let bounds = CGRect(origin: CGPoint(), size: size)
context.setFillColor(theme.list.itemBlocksBackgroundColor.cgColor)
context.fill(bounds)
context.setBlendMode(.clear)
context.fillEllipse(in: bounds)
context.setBlendMode(.normal)
if bordered {
let lineWidth: CGFloat = 1.0
context.setLineWidth(lineWidth)
context.setStrokeColor(theme.list.disclosureArrowColor.withAlphaComponent(0.4).cgColor)
context.strokeEllipse(in: bounds.insetBy(dx: lineWidth / 2.0, dy: lineWidth / 2.0))
}
})?.stretchableImage(withLeftCapWidth: 15, topCapHeight: 15)
}
class UpdateInfoItem: ListViewItem, ItemListItem {
let theme: PresentationTheme
let appIcon: PresentationAppIcon?
let title: String
let text: String
let entities: [MessageTextEntity]
let sectionId: ItemListSectionId
let style: ItemListStyle
let linkItemAction: ((TextLinkItemActionType, TextLinkItem) -> Void)?
let tag: Any?
let selectable: Bool = false
init(theme: PresentationTheme, appIcon: PresentationAppIcon?, title: String, text: String, entities: [MessageTextEntity], sectionId: ItemListSectionId, style: ItemListStyle, linkItemAction: ((TextLinkItemActionType, TextLinkItem) -> Void)? = nil, tag: Any? = nil) {
self.theme = theme
self.appIcon = appIcon
self.title = title
self.text = text
self.entities = entities
self.sectionId = sectionId
self.style = style
self.linkItemAction = linkItemAction
self.tag = tag
}
func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) {
async {
let node = UpdateInfoItemNode()
let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
node.contentSize = layout.contentSize
node.insets = layout.insets
Queue.mainQueue().async {
completion(node, {
return (nil, { _ in apply() })
})
}
}
}
func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) {
Queue.mainQueue().async {
if let nodeValue = node() as? UpdateInfoItemNode {
let makeLayout = nodeValue.asyncLayout()
async {
let (layout, apply) = makeLayout(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
Queue.mainQueue().async {
completion(layout, { _ in
apply()
})
}
}
}
}
}
}
private let titleFont = Font.bold(17.0)
private let textFont = Font.regular(16.0)
private let textBoldFont = Font.medium(16.0)
private let textItalicFont = Font.italic(16.0)
private let textBoldItalicFont = Font.semiboldItalic(16.0)
private let textFixedFont = Font.regular(16.0)
class UpdateInfoItemNode: ListViewItemNode {
private let backgroundNode: ASDisplayNode
private let topStripeNode: ASDisplayNode
private let bottomStripeNode: ASDisplayNode
private let highlightedBackgroundNode: ASDisplayNode
private var linkHighlightingNode: LinkHighlightingNode?
private let iconNode: ASImageNode
private let overlayNode: ASImageNode
private let titleNode: TextNode
private let textNode: TextNode
private let activateArea: AccessibilityAreaNode
private var item: UpdateInfoItem?
var tag: Any? {
return self.item?.tag
}
init() {
self.backgroundNode = ASDisplayNode()
self.backgroundNode.isLayerBacked = true
self.backgroundNode.backgroundColor = .white
self.topStripeNode = ASDisplayNode()
self.topStripeNode.isLayerBacked = true
self.bottomStripeNode = ASDisplayNode()
self.bottomStripeNode.isLayerBacked = true
self.iconNode = ASImageNode()
self.iconNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: 62.0, height: 62.0))
self.iconNode.isLayerBacked = true
self.overlayNode = ASImageNode()
self.overlayNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: 62.0, height: 62.0))
self.overlayNode.isLayerBacked = true
self.titleNode = TextNode()
self.titleNode.isUserInteractionEnabled = false
self.textNode = TextNode()
self.textNode.isUserInteractionEnabled = false
self.textNode.contentMode = .left
self.textNode.contentsScale = UIScreen.main.scale
self.highlightedBackgroundNode = ASDisplayNode()
self.highlightedBackgroundNode.isLayerBacked = true
self.activateArea = AccessibilityAreaNode()
super.init(layerBacked: false, dynamicBounce: false)
self.addSubnode(self.iconNode)
self.addSubnode(self.overlayNode)
self.addSubnode(self.titleNode)
self.addSubnode(self.textNode)
self.addSubnode(self.activateArea)
}
override func didLoad() {
super.didLoad()
let recognizer = TapLongTapOrDoubleTapGestureRecognizer(target: self, action: #selector(self.tapLongTapOrDoubleTapGesture(_:)))
recognizer.tapActionAtPoint = { [weak self] point in
if let strongSelf = self, strongSelf.linkItemAtPoint(point) != nil {
return .waitForSingleTap
}
return .fail
}
recognizer.highlight = { [weak self] point in
if let strongSelf = self {
strongSelf.updateTouchesAtPoint(point)
}
}
self.view.addGestureRecognizer(recognizer)
}
func asyncLayout() -> (_ item: UpdateInfoItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) {
let makeTitleLayout = TextNode.asyncLayout(self.titleNode)
let makeTextLayout = TextNode.asyncLayout(self.textNode)
let currentItem = self.item
return { item, params, neighbors in
var updatedTheme: PresentationTheme?
var updatedAppIcon: PresentationAppIcon?
if currentItem?.theme !== item.theme {
updatedTheme = item.theme
}
if currentItem?.appIcon != item.appIcon {
updatedAppIcon = item.appIcon
}
let textColor: UIColor = item.theme.list.itemPrimaryTextColor
let inset: CGFloat
let itemBackgroundColor: UIColor
let itemSeparatorColor: UIColor
switch item.style {
case .plain:
itemBackgroundColor = item.theme.list.plainBackgroundColor
itemSeparatorColor = item.theme.list.itemPlainSeparatorColor
inset = 14.0 + params.leftInset
case .blocks:
itemBackgroundColor = item.theme.list.itemBlocksBackgroundColor
itemSeparatorColor = item.theme.list.itemBlocksSeparatorColor
inset = 14.0 + params.rightInset
}
let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.title, font: titleFont, textColor: textColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - params.leftInset - params.rightInset - 88.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
let string = stringWithAppliedEntities(item.text, entities: item.entities, baseColor: textColor, linkColor: item.theme.list.itemAccentColor, baseFont: textFont, linkFont: textFont, boldFont: textBoldFont, italicFont: textItalicFont, boldItalicFont: textBoldItalicFont, fixedFont: textFixedFont)
let (textLayout, textApply) = makeTextLayout(TextNodeLayoutArguments(attributedString: string, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: params.width - params.leftInset - params.rightInset - 28.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
let contentSize: CGSize
let insets: UIEdgeInsets
let separatorHeight = UIScreenPixel
switch item.style {
case .plain:
contentSize = CGSize(width: params.width, height: 88.0 + textLayout.size.height + inset)
insets = itemListNeighborsPlainInsets(neighbors)
case .blocks:
contentSize = CGSize(width: params.width, height: 88.0 + textLayout.size.height + inset)
insets = itemListNeighborsGroupedInsets(neighbors)
}
let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets)
let layoutSize = layout.size
return (layout, { [weak self] in
if let strongSelf = self {
strongSelf.item = item
strongSelf.activateArea.frame = CGRect(origin: CGPoint(x: params.leftInset, y: 0.0), size: CGSize(width: params.width - params.leftInset - params.rightInset, height: layout.contentSize.height))
strongSelf.activateArea.accessibilityLabel = item.text
if let _ = updatedTheme {
strongSelf.topStripeNode.backgroundColor = itemSeparatorColor
strongSelf.bottomStripeNode.backgroundColor = itemSeparatorColor
strongSelf.backgroundNode.backgroundColor = itemBackgroundColor
strongSelf.highlightedBackgroundNode.backgroundColor = item.theme.list.itemHighlightedBackgroundColor
var bordered = true
if let appIcon = item.appIcon {
switch appIcon.name {
case "BlueFilled":
bordered = false
case "BlackFilled":
bordered = false
default:
break
}
}
strongSelf.overlayNode.image = generateBorderImage(theme: item.theme, bordered: bordered)
}
if let appIcon = updatedAppIcon, let image = UIImage(named: appIcon.imageName, in: Bundle.main, compatibleWith: nil) {
strongSelf.iconNode.image = image
}
let _ = titleApply()
let _ = textApply()
switch item.style {
case .plain:
if strongSelf.backgroundNode.supernode != nil {
strongSelf.backgroundNode.removeFromSupernode()
}
if strongSelf.topStripeNode.supernode != nil {
strongSelf.topStripeNode.removeFromSupernode()
}
if strongSelf.bottomStripeNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 0)
}
strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: inset, y: contentSize.height - separatorHeight), size: CGSize(width: params.width - inset, height: separatorHeight))
case .blocks:
if strongSelf.backgroundNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.backgroundNode, at: 0)
}
if strongSelf.topStripeNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.topStripeNode, at: 1)
}
if strongSelf.bottomStripeNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 2)
}
switch neighbors.top {
case .sameSection(false):
strongSelf.topStripeNode.isHidden = true
default:
strongSelf.topStripeNode.isHidden = false
}
let bottomStripeInset: CGFloat
let bottomStripeOffset: CGFloat
switch neighbors.bottom {
case .sameSection(false):
bottomStripeInset = 16.0
bottomStripeOffset = -separatorHeight
default:
bottomStripeInset = 0.0
bottomStripeOffset = 0.0
}
strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight)))
strongSelf.topStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: layoutSize.width, height: separatorHeight))
strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height + bottomStripeOffset), size: CGSize(width: layoutSize.width - bottomStripeInset, height: separatorHeight))
}
let iconFrame = CGRect(origin: CGPoint(x: inset, y: inset), size: CGSize(width: 62.0, height: 62.0))
strongSelf.iconNode.frame = iconFrame
strongSelf.overlayNode.frame = iconFrame
strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: iconFrame.maxX + inset, y: iconFrame.minY + ceil((iconFrame.height - titleLayout.size.height) / 2.0)), size: titleLayout.size)
strongSelf.textNode.frame = CGRect(origin: CGPoint(x: inset, y: iconFrame.maxY + inset), size: textLayout.size)
strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: params.width, height: layout.contentSize.height + UIScreenPixel + UIScreenPixel))
}
})
}
}
override func setHighlighted(_ highlighted: Bool, at point: CGPoint, animated: Bool) {
super.setHighlighted(highlighted, at: point, animated: animated)
if highlighted && self.linkItemAtPoint(point) == nil {
self.highlightedBackgroundNode.alpha = 1.0
if self.highlightedBackgroundNode.supernode == nil {
var anchorNode: ASDisplayNode?
if self.bottomStripeNode.supernode != nil {
anchorNode = self.bottomStripeNode
} else if self.topStripeNode.supernode != nil {
anchorNode = self.topStripeNode
} else if self.backgroundNode.supernode != nil {
anchorNode = self.backgroundNode
}
if let anchorNode = anchorNode {
self.insertSubnode(self.highlightedBackgroundNode, aboveSubnode: anchorNode)
} else {
self.addSubnode(self.highlightedBackgroundNode)
}
}
} else {
if self.highlightedBackgroundNode.supernode != nil {
if animated {
self.highlightedBackgroundNode.layer.animateAlpha(from: self.highlightedBackgroundNode.alpha, to: 0.0, duration: 0.4, completion: { [weak self] completed in
if let strongSelf = self {
if completed {
strongSelf.highlightedBackgroundNode.removeFromSupernode()
}
}
})
self.highlightedBackgroundNode.alpha = 0.0
} else {
self.highlightedBackgroundNode.removeFromSupernode()
}
}
}
}
override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) {
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4)
}
override func animateRemoved(_ currentTimestamp: Double, duration: Double) {
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false)
}
@objc func tapLongTapOrDoubleTapGesture(_ recognizer: TapLongTapOrDoubleTapGestureRecognizer) {
switch recognizer.state {
case .ended:
if let (gesture, location) = recognizer.lastRecognizedGestureAndLocation {
switch gesture {
case .tap, .longTap:
if let item = self.item, let linkItem = self.linkItemAtPoint(location) {
item.linkItemAction?(gesture == .tap ? .tap : .longTap, linkItem)
}
default:
break
}
}
default:
break
}
}
private func linkItemAtPoint(_ point: CGPoint) -> TextLinkItem? {
let textNodeFrame = self.textNode.frame
if let (_, attributes) = self.textNode.attributesAtPoint(CGPoint(x: point.x - textNodeFrame.minX, y: point.y - textNodeFrame.minY)) {
if let url = attributes[NSAttributedStringKey(rawValue: TelegramTextAttributes.URL)] as? String {
return .url(url)
} else if let peerName = attributes[NSAttributedStringKey(rawValue: TelegramTextAttributes.PeerTextMention)] as? String {
return .mention(peerName)
} else if let hashtag = attributes[NSAttributedStringKey(rawValue: TelegramTextAttributes.Hashtag)] as? TelegramHashtag {
return .hashtag(hashtag.peerName, hashtag.hashtag)
} else {
return nil
}
}
return nil
}
private func updateTouchesAtPoint(_ point: CGPoint?) {
if let item = self.item {
var rects: [CGRect]?
if let point = point {
let textNodeFrame = self.textNode.frame
if let (index, attributes) = self.textNode.attributesAtPoint(CGPoint(x: point.x - textNodeFrame.minX, y: point.y - textNodeFrame.minY)) {
let possibleNames: [String] = [
TelegramTextAttributes.URL,
TelegramTextAttributes.PeerMention,
TelegramTextAttributes.PeerTextMention,
TelegramTextAttributes.BotCommand,
TelegramTextAttributes.Hashtag
]
for name in possibleNames {
if let _ = attributes[NSAttributedStringKey(rawValue: name)] {
rects = self.textNode.attributeRects(name: name, at: index)
break
}
}
}
}
if let rects = rects {
let linkHighlightingNode: LinkHighlightingNode
if let current = self.linkHighlightingNode {
linkHighlightingNode = current
} else {
linkHighlightingNode = LinkHighlightingNode(color: item.theme.list.itemAccentColor.withAlphaComponent(0.5))
self.linkHighlightingNode = linkHighlightingNode
self.insertSubnode(linkHighlightingNode, belowSubnode: self.textNode)
}
linkHighlightingNode.frame = self.textNode.frame
linkHighlightingNode.updateRects(rects)
} else if let linkHighlightingNode = self.linkHighlightingNode {
self.linkHighlightingNode = nil
linkHighlightingNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.18, removeOnCompletion: false, completion: { [weak linkHighlightingNode] _ in
linkHighlightingNode?.removeFromSupernode()
})
}
}
}
}
@@ -386,27 +386,27 @@ private enum UserInfoEntry: ItemListNodeEntry {
case let .calls(theme, strings, dateTimeFormat, messages):
return ItemListCallListItem(theme: theme, strings: strings, dateTimeFormat: dateTimeFormat, messages: messages, sectionId: self.section, style: .plain)
case let .about(theme, peer, text, value):
var enabledEntitiyTypes: EnabledEntityTypes = []
var enabledEntityTypes: EnabledEntityTypes = []
if let peer = peer as? TelegramUser, let _ = peer.botInfo {
enabledEntitiyTypes = [.url, .mention, .hashtag]
enabledEntityTypes = [.url, .mention, .hashtag]
}
return ItemListTextWithLabelItem(theme: theme, label: text, text: foldMultipleLineBreaks(value), enabledEntitiyTypes: enabledEntitiyTypes, multiline: true, sectionId: self.section, action: nil, longTapAction: {
return ItemListTextWithLabelItem(theme: theme, label: text, text: foldMultipleLineBreaks(value), enabledEntityTypes: enabledEntityTypes, multiline: true, sectionId: self.section, action: nil, longTapAction: {
arguments.displayAboutContextMenu(value)
}, linkItemAction: { action, itemLink in
arguments.aboutLinkAction(action, itemLink)
}, tag: UserInfoEntryTag.about)
case let .phoneNumber(theme, _, label, value, isMain):
return ItemListTextWithLabelItem(theme: theme, label: label, text: value, textColor: isMain ? .highlighted : .accent, enabledEntitiyTypes: [], multiline: false, sectionId: self.section, action: {
return ItemListTextWithLabelItem(theme: theme, label: label, text: value, textColor: isMain ? .highlighted : .accent, enabledEntityTypes: [], multiline: false, sectionId: self.section, action: {
arguments.openCallMenu(value)
}, longTapAction: {
arguments.displayCopyContextMenu(.phoneNumber, value)
}, tag: UserInfoEntryTag.phoneNumber)
case let .requestPhoneNumber(theme, label, value):
return ItemListTextWithLabelItem(theme: theme, label: label, text: value, textColor: .accent, enabledEntitiyTypes: [], multiline: false, sectionId: self.section, action: {
return ItemListTextWithLabelItem(theme: theme, label: label, text: value, textColor: .accent, enabledEntityTypes: [], multiline: false, sectionId: self.section, action: {
arguments.requestPhoneNumber()
})
case let .userName(theme, text, value):
return ItemListTextWithLabelItem(theme: theme, label: text, text: "@\(value)", textColor: .accent, enabledEntitiyTypes: [], multiline: false, sectionId: self.section, action: {
return ItemListTextWithLabelItem(theme: theme, label: text, text: "@\(value)", textColor: .accent, enabledEntityTypes: [], multiline: false, sectionId: self.section, action: {
arguments.displayUsernameContextMenu("@\(value)")
}, longTapAction: {
arguments.displayCopyContextMenu(.username, "@\(value)")
@@ -1178,113 +1178,114 @@ public func userInfoController(context: AccountContext, peerId: PeerId, mode: Us
let globalNotificationsKey: PostboxViewKey = .preferences(keys: Set<ValueBoxKey>([PreferencesKeys.globalNotifications]))
let signal = combineLatest(context.sharedContext.presentationData, statePromise.get(), peerView.get(), deviceContacts, context.account.postbox.combinedView(keys: [.peerChatState(peerId: peerId), globalNotificationsKey]))
|> map { presentationData, state, view, deviceContacts, combinedView -> (ItemListControllerState, (ItemListNodeState<UserInfoEntry>, UserInfoEntry.ItemGenerationArguments)) in
let peer = peerViewMainPeer(view.0)
var globalNotificationSettings: GlobalNotificationSettings = .defaultSettings
if let preferencesView = combinedView.views[globalNotificationsKey] as? PreferencesView {
if let settings = preferencesView.values[PreferencesKeys.globalNotifications] as? GlobalNotificationSettings {
globalNotificationSettings = settings
}
|> map { presentationData, state, view, deviceContacts, combinedView -> (ItemListControllerState, (ItemListNodeState<UserInfoEntry>, UserInfoEntry.ItemGenerationArguments)) in
let peer = peerViewMainPeer(view.0)
var globalNotificationSettings: GlobalNotificationSettings = .defaultSettings
if let preferencesView = combinedView.views[globalNotificationsKey] as? PreferencesView {
if let settings = preferencesView.values[PreferencesKeys.globalNotifications] as? GlobalNotificationSettings {
globalNotificationSettings = settings
}
if let peer = peer {
let _ = cachedAvatarEntries.modify { value in
if value != nil {
return value
} else {
let promise = Promise<[AvatarGalleryEntry]>()
promise.set(fetchedAvatarGalleryEntries(account: context.account, peer: peer))
return promise
}
}
}
var leftNavigationButton: ItemListNavigationButton?
let rightNavigationButton: ItemListNavigationButton
if let editingState = state.editingState {
leftNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Cancel), style: .regular, enabled: true, action: {
updateState {
$0.withUpdatedEditingState(nil)
}
})
var doneEnabled = true
if let editingName = editingState.editingName, editingName.isEmpty {
doneEnabled = false
}
if state.savingData {
rightNavigationButton = ItemListNavigationButton(content: .none, style: .activity, enabled: doneEnabled, action: {})
}
if let peer = peer {
let _ = cachedAvatarEntries.modify { value in
if value != nil {
return value
} else {
rightNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Done), style: .bold, enabled: doneEnabled, action: {
var updateName: ItemListAvatarAndNameInfoItemName?
updateState { state in
if let editingState = state.editingState, let editingName = editingState.editingName {
if let user = peer {
if ItemListAvatarAndNameInfoItemName(user) != editingName {
updateName = editingName
}
let promise = Promise<[AvatarGalleryEntry]>()
promise.set(fetchedAvatarGalleryEntries(account: context.account, peer: peer))
return promise
}
}
}
var leftNavigationButton: ItemListNavigationButton?
let rightNavigationButton: ItemListNavigationButton
if let editingState = state.editingState {
leftNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Cancel), style: .regular, enabled: true, action: {
updateState {
$0.withUpdatedEditingState(nil)
}
})
var doneEnabled = true
if let editingName = editingState.editingName, editingName.isEmpty {
doneEnabled = false
}
if state.savingData {
rightNavigationButton = ItemListNavigationButton(content: .none, style: .activity, enabled: doneEnabled, action: {})
} else {
rightNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Done), style: .bold, enabled: doneEnabled, action: {
var updateName: ItemListAvatarAndNameInfoItemName?
updateState { state in
if let editingState = state.editingState, let editingName = editingState.editingName {
if let user = peer {
if ItemListAvatarAndNameInfoItemName(user) != editingName {
updateName = editingName
}
}
if updateName != nil {
return state.withUpdatedSavingData(true)
} else {
return state.withUpdatedEditingState(nil)
}
}
if let updateName = updateName, case let .personName(firstName, lastName) = updateName {
updatePeerNameDisposable.set((updateContactName(account: context.account, peerId: peerId, firstName: firstName, lastName: lastName)
|> deliverOnMainQueue).start(error: { _ in
updateState { state in
return state.withUpdatedSavingData(false)
if updateName != nil {
return state.withUpdatedSavingData(true)
} else {
return state.withUpdatedEditingState(nil)
}
}
if let updateName = updateName, case let .personName(firstName, lastName) = updateName {
updatePeerNameDisposable.set((updateContactName(account: context.account, peerId: peerId, firstName: firstName, lastName: lastName)
|> deliverOnMainQueue).start(error: { _ in
updateState { state in
return state.withUpdatedSavingData(false)
}
}, completed: {
updateState { state in
return state.withUpdatedSavingData(false).withUpdatedEditingState(nil)
}
let _ = (getUserPeer(postbox: context.account.postbox, peerId: peerId)
|> mapToSignal { peer, _ -> Signal<Void, NoError> in
guard let peer = peer as? TelegramUser, let phone = peer.phone, !phone.isEmpty else {
return .complete()
}
}, completed: {
updateState { state in
return state.withUpdatedSavingData(false).withUpdatedEditingState(nil)
}
let _ = (getUserPeer(postbox: context.account.postbox, peerId: peerId)
|> mapToSignal { peer, _ -> Signal<Void, NoError> in
guard let peer = peer as? TelegramUser, let phone = peer.phone, !phone.isEmpty else {
return (context.sharedContext.contactDataManager?.basicDataForNormalizedPhoneNumber(DeviceContactNormalizedPhoneNumber(rawValue: formatPhoneNumber(phone))) ?? .single([]))
|> take(1)
|> mapToSignal { records -> Signal<Void, NoError> in
var signals: [Signal<DeviceContactExtendedData?, NoError>] = []
if let contactDataManager = context.sharedContext.contactDataManager {
for (id, basicData) in records {
signals.append(contactDataManager.appendContactData(DeviceContactExtendedData(basicData: DeviceContactBasicData(firstName: firstName, lastName: lastName, phoneNumbers: basicData.phoneNumbers), middleName: "", prefix: "", suffix: "", organization: "", jobTitle: "", department: "", emailAddresses: [], urls: [], addresses: [], birthdayDate: nil, socialProfiles: [], instantMessagingProfiles: []), to: id))
}
}
return combineLatest(signals)
|> mapToSignal { _ -> Signal<Void, NoError> in
return .complete()
}
return (context.sharedContext.contactDataManager?.basicDataForNormalizedPhoneNumber(DeviceContactNormalizedPhoneNumber(rawValue: formatPhoneNumber(phone))) ?? .single([]))
|> take(1)
|> mapToSignal { records -> Signal<Void, NoError> in
var signals: [Signal<DeviceContactExtendedData?, NoError>] = []
if let contactDataManager = context.sharedContext.contactDataManager {
for (id, basicData) in records {
signals.append(contactDataManager.appendContactData(DeviceContactExtendedData(basicData: DeviceContactBasicData(firstName: firstName, lastName: lastName, phoneNumbers: basicData.phoneNumbers), middleName: "", prefix: "", suffix: "", organization: "", jobTitle: "", department: "", emailAddresses: [], urls: [], addresses: [], birthdayDate: nil, socialProfiles: [], instantMessagingProfiles: []), to: id))
}
}
return combineLatest(signals)
|> mapToSignal { _ -> Signal<Void, NoError> in
return .complete()
}
}
}).start()
}))
}
})
}
} else {
rightNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Edit), style: .regular, enabled: true, action: {
if let user = peer {
updateState { state in
return state.withUpdatedEditingState(UserInfoEditingState(editingName: ItemListAvatarAndNameInfoItemName(user)))
}
}
}).start()
}))
}
})
}
let controllerState = ItemListControllerState(theme: presentationData.theme, title: .text(presentationData.strings.UserInfo_Title), leftNavigationButton: leftNavigationButton, rightNavigationButton: rightNavigationButton, backNavigationButton: nil)
let listState = ItemListNodeState(entries: userInfoEntries(account: context.account, presentationData: presentationData, view: view.0, cachedPeerData: view.1, deviceContacts: deviceContacts, mode: mode, state: state, peerChatState: (combinedView.views[.peerChatState(peerId: peerId)] as? PeerChatStateView)?.chatState, globalNotificationSettings: globalNotificationSettings), style: .plain)
return (controllerState, (listState, arguments))
} |> afterDisposed {
actionsDisposable.dispose()
} else {
rightNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Edit), style: .regular, enabled: true, action: {
if let user = peer {
updateState { state in
return state.withUpdatedEditingState(UserInfoEditingState(editingName: ItemListAvatarAndNameInfoItemName(user)))
}
}
})
}
let controllerState = ItemListControllerState(theme: presentationData.theme, title: .text(presentationData.strings.UserInfo_Title), leftNavigationButton: leftNavigationButton, rightNavigationButton: rightNavigationButton, backNavigationButton: nil)
let listState = ItemListNodeState(entries: userInfoEntries(account: context.account, presentationData: presentationData, view: view.0, cachedPeerData: view.1, deviceContacts: deviceContacts, mode: mode, state: state, peerChatState: (combinedView.views[.peerChatState(peerId: peerId)] as? PeerChatStateView)?.chatState, globalNotificationSettings: globalNotificationSettings), style: .plain)
return (controllerState, (listState, arguments))
}
|> afterDisposed {
actionsDisposable.dispose()
}
let controller = ItemListController(context: context, state: signal)
@@ -1449,7 +1450,7 @@ public func userInfoController(context: AccountContext, peerId: PeerId, mode: Us
}
aboutLinkActionImpl = { [weak controller] action, itemLink in
if let controller = controller {
handlePeerInfoAboutTextAction(context: context, peerId: peerId, navigateDisposable: navigateDisposable, controller: controller, action: action, itemLink: itemLink)
handleTextLinkAction(context: context, peerId: peerId, navigateDisposable: navigateDisposable, controller: controller, action: action, itemLink: itemLink)
}
}
displayAboutContextMenuImpl = { [weak controller] text in
@@ -152,6 +152,8 @@
09E4A805223D4A5A0038140F /* OpenSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09E4A804223D4A5A0038140F /* OpenSettings.swift */; };
09E4A807223D4B860038140F /* AccountUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09E4A806223D4B860038140F /* AccountUtils.swift */; };
09EC0DE722C67FB100E7185B /* UpdateInfoController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09EC0DE622C67FB100E7185B /* UpdateInfoController.swift */; };
09EC0DEB22CAFF1400E7185B /* UpdateInfoItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09EC0DEA22CAFF1400E7185B /* UpdateInfoItem.swift */; };
09EC0DED22CB583C00E7185B /* TextLinkHandling.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09EC0DEC22CB583C00E7185B /* TextLinkHandling.swift */; };
09EDAD26220D30980012A50B /* AutodownloadConnectionTypeController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09EDAD25220D30980012A50B /* AutodownloadConnectionTypeController.swift */; };
09EDAD2A220DA6A40012A50B /* VolumeButtons.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09EDAD29220DA6A40012A50B /* VolumeButtons.swift */; };
09EDAD2C2211552F0012A50B /* AutodownloadMediaCategoryController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09EDAD2B2211552F0012A50B /* AutodownloadMediaCategoryController.swift */; };
@@ -1372,6 +1374,8 @@
09E4A804223D4A5A0038140F /* OpenSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenSettings.swift; sourceTree = "<group>"; };
09E4A806223D4B860038140F /* AccountUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountUtils.swift; sourceTree = "<group>"; };
09EC0DE622C67FB100E7185B /* UpdateInfoController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateInfoController.swift; sourceTree = "<group>"; };
09EC0DEA22CAFF1400E7185B /* UpdateInfoItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateInfoItem.swift; sourceTree = "<group>"; };
09EC0DEC22CB583C00E7185B /* TextLinkHandling.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextLinkHandling.swift; sourceTree = "<group>"; };
09EDAD25220D30980012A50B /* AutodownloadConnectionTypeController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutodownloadConnectionTypeController.swift; sourceTree = "<group>"; };
09EDAD29220DA6A40012A50B /* VolumeButtons.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VolumeButtons.swift; sourceTree = "<group>"; };
09EDAD2B2211552F0012A50B /* AutodownloadMediaCategoryController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutodownloadMediaCategoryController.swift; sourceTree = "<group>"; };
@@ -2743,6 +2747,7 @@
D0FC194C201F82A000FEDBB2 /* OpenResolvedUrl.swift */,
D023836F1DDF0462004018B6 /* UrlHandling.swift */,
09E4A804223D4A5A0038140F /* OpenSettings.swift */,
09EC0DEC22CB583C00E7185B /* TextLinkHandling.swift */,
);
name = Routing;
sourceTree = "<group>";
@@ -2777,6 +2782,7 @@
isa = PBXGroup;
children = (
09EC0DE622C67FB100E7185B /* UpdateInfoController.swift */,
09EC0DEA22CAFF1400E7185B /* UpdateInfoItem.swift */,
);
name = Update;
sourceTree = "<group>";
@@ -6037,6 +6043,7 @@
09F664C821EB4A2600AB7E26 /* ThemeGridSearchItem.swift in Sources */,
09A218D9229EE1B600DE6898 /* HorizontalStickerGridItem.swift in Sources */,
D07E413B208A432100FCA8F0 /* ChatListTitleProxyNode.swift in Sources */,
09EC0DED22CB583C00E7185B /* TextLinkHandling.swift in Sources */,
D080B27F1F4C7C6000AA3847 /* InstantPageManagedMediaId.swift in Sources */,
D08984F02114AE0C00918162 /* DataPrivacySettingsController.swift in Sources */,
D087BFAD1F741B9D003FD209 /* ShareContentContainerNode.swift in Sources */,
@@ -6171,6 +6178,7 @@
D0E9BA651F055B4500F079A4 /* BotCheckoutNativeCardEntryController.swift in Sources */,
D02D60B3206C18A600FEFE1E /* SecureIdPlaintextFormControllerNode.swift in Sources */,
D00ACA5A2022897D0045D427 /* ProcessedPeerRestrictionText.swift in Sources */,
09EC0DEB22CAFF1400E7185B /* UpdateInfoItem.swift in Sources */,
D0192D3C210A44D00005FA10 /* DeviceContactData.swift in Sources */,
D0EC6E391EB9F58900EBF1C3 /* ItemListCheckboxItem.swift in Sources */,
D0EC6E3A1EB9F58900EBF1C3 /* ItemListSwitchItem.swift in Sources */,