Low disk space alert
Contacts sort selector Bot button icons tg://msg scheme support Calculate service message color using current wallpaper Various fixes
BIN
Images.xcassets/Chat/Message/BotLink.imageset/BotLink@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
Images.xcassets/Chat/Message/BotLink.imageset/BotLink@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 322 B |
22
Images.xcassets/Chat/Message/BotLink.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,22 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "BotLink@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "BotLink@3x.png",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
BIN
Images.xcassets/Chat/Message/BotLocation.imageset/BotLocation@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
Images.xcassets/Chat/Message/BotLocation.imageset/BotLocation@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 555 B |
22
Images.xcassets/Chat/Message/BotLocation.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,22 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "BotLocation@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "BotLocation@3x.png",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
BIN
Images.xcassets/Chat/Message/BotMessage.imageset/BotMessage@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 1.0 KiB |
BIN
Images.xcassets/Chat/Message/BotMessage.imageset/BotMessage@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 1.9 KiB |
22
Images.xcassets/Chat/Message/BotMessage.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,22 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "BotMessage@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "BotMessage@3x.png",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
BIN
Images.xcassets/Chat/Message/BotPhone.imageset/BotPhone@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
Images.xcassets/Chat/Message/BotPhone.imageset/BotPhone@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
22
Images.xcassets/Chat/Message/BotPhone.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,22 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "BotPhone@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "BotPhone@3x.png",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
BIN
Images.xcassets/Chat/Message/BotShare.imageset/BotShare@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
Images.xcassets/Chat/Message/BotShare.imageset/BotShare@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 1.9 KiB |
22
Images.xcassets/Chat/Message/BotShare.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,22 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "BotShare@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "BotShare@3x.png",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
@ -72,6 +72,7 @@
|
||||
099529AE21D045C400805E13 /* ThemeGridActionNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 099529AD21D045C400805E13 /* ThemeGridActionNode.swift */; };
|
||||
099529B021D2123E00805E13 /* ChatMessageUnsupportedBubbleContentNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 099529AF21D2123E00805E13 /* ChatMessageUnsupportedBubbleContentNode.swift */; };
|
||||
099529B221D24F5800805E13 /* RadialDownloadContentNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 099529B121D24F5800805E13 /* RadialDownloadContentNode.swift */; };
|
||||
099529B421D3E5D800805E13 /* CheckDiskSpace.swift in Sources */ = {isa = PBXBuildFile; fileRef = 099529B321D3E5D800805E13 /* CheckDiskSpace.swift */; };
|
||||
09AE3823214C110900850BFD /* LegacySecureIdScanController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09AE3822214C110800850BFD /* LegacySecureIdScanController.swift */; };
|
||||
09B4EE4721A6D33F00847FA6 /* RecentSessionsEmptyStateItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09B4EE4621A6D33F00847FA6 /* RecentSessionsEmptyStateItem.swift */; };
|
||||
09B4EE4D21A7B73800847FA6 /* PermissionController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09B4EE4C21A7B73800847FA6 /* PermissionController.swift */; };
|
||||
@ -1176,6 +1177,7 @@
|
||||
099529AD21D045C400805E13 /* ThemeGridActionNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThemeGridActionNode.swift; sourceTree = "<group>"; };
|
||||
099529AF21D2123E00805E13 /* ChatMessageUnsupportedBubbleContentNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatMessageUnsupportedBubbleContentNode.swift; sourceTree = "<group>"; };
|
||||
099529B121D24F5800805E13 /* RadialDownloadContentNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RadialDownloadContentNode.swift; sourceTree = "<group>"; };
|
||||
099529B321D3E5D800805E13 /* CheckDiskSpace.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CheckDiskSpace.swift; sourceTree = "<group>"; };
|
||||
09AE3822214C110800850BFD /* LegacySecureIdScanController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LegacySecureIdScanController.swift; sourceTree = "<group>"; };
|
||||
09B4EE4621A6D33F00847FA6 /* RecentSessionsEmptyStateItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecentSessionsEmptyStateItem.swift; sourceTree = "<group>"; };
|
||||
09B4EE4C21A7B73800847FA6 /* PermissionController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PermissionController.swift; sourceTree = "<group>"; };
|
||||
@ -4665,6 +4667,7 @@
|
||||
0902838C2194AEB90067EFBD /* ImageTransparency.swift */,
|
||||
09C9EA32219F79F600E90146 /* ID3Artwork.h */,
|
||||
09C9EA31219F79F500E90146 /* ID3Artwork.m */,
|
||||
099529B321D3E5D800805E13 /* CheckDiskSpace.swift */,
|
||||
);
|
||||
name = Utils;
|
||||
sourceTree = "<group>";
|
||||
@ -5285,6 +5288,7 @@
|
||||
D0E9BA671F055B5500F079A4 /* BotCheckoutNativeCardEntryControllerNode.swift in Sources */,
|
||||
D0EC6D291EB9F58800EBF1C3 /* FetchVideoMediaResource.swift in Sources */,
|
||||
D0AFCC791F4C8D2C000720C6 /* InstantPageSlideshowItem.swift in Sources */,
|
||||
099529B421D3E5D800805E13 /* CheckDiskSpace.swift in Sources */,
|
||||
D04281EF200E3D88009DDE36 /* GroupInfoSearchItem.swift in Sources */,
|
||||
D02660941F34CE5C000E2DC5 /* LegacyLocationVenueIconDataSource.swift in Sources */,
|
||||
D081E104217F57D2003CD921 /* LanguageLinkPreviewController.swift in Sources */,
|
||||
|
||||
@ -273,7 +273,7 @@ class AvatarGalleryController: ViewController {
|
||||
override func loadDisplayNode() {
|
||||
let controllerInteraction = GalleryControllerInteraction(presentController: { [weak self] controller, arguments in
|
||||
if let strongSelf = self {
|
||||
strongSelf.present(controller, in: .window(.root), with: arguments)
|
||||
strongSelf.present(controller, in: .window(.root), with: arguments, blockInteraction: true)
|
||||
}
|
||||
}, dismissController: { [weak self] in
|
||||
self?.dismiss(forceAway: true)
|
||||
|
||||
@ -293,7 +293,7 @@ class CallListCallItemNode: ItemListRevealOptionsItemNode {
|
||||
let editingOffset: CGFloat
|
||||
var editableControlSizeAndApply: (CGSize, () -> ItemListEditableControlNode)?
|
||||
if item.editing {
|
||||
let sizeAndApply = editableControlLayout(56.0, item.theme, false)
|
||||
let sizeAndApply = editableControlLayout(50.0, item.theme, false)
|
||||
editableControlSizeAndApply = sizeAndApply
|
||||
editingOffset = sizeAndApply.0.width
|
||||
} else {
|
||||
@ -420,7 +420,7 @@ class CallListCallItemNode: ItemListRevealOptionsItemNode {
|
||||
|
||||
let (statusLayout, statusApply) = makeStatusLayout(TextNodeLayoutArguments(attributedString: statusAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: max(0.0, params.width - leftInset - rightInset), height: CGFloat.infinity), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||||
|
||||
let nodeLayout = ListViewItemNodeLayout(contentSize: CGSize(width: params.width, height: 56.0), insets: UIEdgeInsets(top: firstWithHeader ? 29.0 : 0.0, left: 0.0, bottom: 0.0, right: 0.0))
|
||||
let nodeLayout = ListViewItemNodeLayout(contentSize: CGSize(width: params.width, height: 50.0), insets: UIEdgeInsets(top: firstWithHeader ? 29.0 : 0.0, left: 0.0, bottom: 0.0, right: 0.0))
|
||||
|
||||
let outgoingIcon = PresentationResourcesCallList.outgoingIcon(item.theme)
|
||||
let infoIcon = PresentationResourcesCallList.infoButton(item.theme)
|
||||
@ -522,13 +522,13 @@ class CallListCallItemNode: ItemListRevealOptionsItemNode {
|
||||
})
|
||||
}
|
||||
|
||||
transition.updateFrame(node: strongSelf.avatarNode, frame: CGRect(origin: CGPoint(x: revealOffset + leftInset - 52.0, y: 8.0), size: CGSize(width: 40.0, height: 40.0)))
|
||||
transition.updateFrame(node: strongSelf.avatarNode, frame: CGRect(origin: CGPoint(x: revealOffset + leftInset - 52.0, y: 5.0), size: CGSize(width: 40.0, height: 40.0)))
|
||||
|
||||
let _ = titleApply()
|
||||
transition.updateFrame(node: strongSelf.titleNode, frame: CGRect(origin: CGPoint(x: revealOffset + leftInset, y: 8.0), size: titleLayout.size))
|
||||
transition.updateFrame(node: strongSelf.titleNode, frame: CGRect(origin: CGPoint(x: revealOffset + leftInset, y: 6.0), size: titleLayout.size))
|
||||
|
||||
let _ = statusApply()
|
||||
transition.updateFrame(node: strongSelf.statusNode, frame: CGRect(origin: CGPoint(x: revealOffset + leftInset, y: 30.0), size: statusLayout.size))
|
||||
transition.updateFrame(node: strongSelf.statusNode, frame: CGRect(origin: CGPoint(x: revealOffset + leftInset, y: 27.0), size: statusLayout.size))
|
||||
|
||||
let _ = dateApply()
|
||||
transition.updateFrame(node: strongSelf.dateNode, frame: CGRect(origin: CGPoint(x: editingOffset + revealOffset + params.width - dateRightInset - dateLayout.size.width, y: floor((nodeLayout.contentSize.height - dateLayout.size.height) / 2.0) + 2.0), size: dateLayout.size))
|
||||
|
||||
@ -803,9 +803,8 @@ public func channelVisibilityController(account: Account, peerId: PeerId, mode:
|
||||
return nil
|
||||
} |> deliverOnMainQueue).start(next: { link in
|
||||
if let link = link {
|
||||
let presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 }
|
||||
let controller = ShareProxyServerActionSheetController(theme: presentationData.theme, strings: presentationData.strings, updatedPresentationData: .complete(), link: link)
|
||||
presentControllerImpl?(controller, nil)
|
||||
let shareController = ShareController(account: account, subject: .url(link))
|
||||
presentControllerImpl?(shareController, nil)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
@ -886,7 +886,7 @@ public final class ChatController: TelegramController, KeyShortcutResponder, Gal
|
||||
}
|
||||
}))
|
||||
}
|
||||
actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [
|
||||
actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [
|
||||
ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, action: { [weak actionSheet] in
|
||||
actionSheet?.dismissAnimated()
|
||||
})
|
||||
@ -2476,6 +2476,11 @@ public final class ChatController: TelegramController, KeyShortcutResponder, Gal
|
||||
guard let strongSelf = self, strongSelf.beginMediaRecordingRequestId == requestId else {
|
||||
return
|
||||
}
|
||||
guard checkAvailableDiskSpace(account: strongSelf.account, present: { [weak self] c, a in
|
||||
self?.present(c, in: .window(.root), with: a)
|
||||
}) else {
|
||||
return
|
||||
}
|
||||
let hasOngoingCall: Signal<Bool, NoError>
|
||||
if let signal = strongSelf.account.telegramApplicationContext.hasOngoingCall {
|
||||
hasOngoingCall = signal
|
||||
@ -4886,7 +4891,7 @@ public final class ChatController: TelegramController, KeyShortcutResponder, Gal
|
||||
}
|
||||
}
|
||||
self.chatDisplayNode.dismissInput()
|
||||
self.present(controller, in: .window(.root))
|
||||
self.present(controller, in: .window(.root), blockInteraction: true)
|
||||
}
|
||||
|
||||
private func openPeer(peerId: PeerId?, navigation: ChatControllerInteractionNavigateToPeer, fromMessage: Message?) {
|
||||
@ -5453,7 +5458,7 @@ public final class ChatController: TelegramController, KeyShortcutResponder, Gal
|
||||
|
||||
switch strongSelf.peekActions {
|
||||
case .standard:
|
||||
if let peer = data.peer {
|
||||
if let peer = data.peer, peer.id != strongSelf.account.peerId {
|
||||
if let _ = data.peer as? TelegramUser {
|
||||
items.append(UIPreviewAction(title: "👍", style: .default, handler: { _, _ in
|
||||
if let strongSelf = self {
|
||||
|
||||
@ -1,9 +1,11 @@
|
||||
import Foundation
|
||||
import TelegramCore
|
||||
import Display
|
||||
import SwiftSignalKit
|
||||
import Postbox
|
||||
|
||||
private var backgroundImageForWallpaper: (TelegramWallpaper, UIImage)?
|
||||
private var serviceBackgroundColorForWallpaper: (TelegramWallpaper, UIColor)?
|
||||
|
||||
func chatControllerBackgroundImage(wallpaper: TelegramWallpaper, postbox: Postbox) -> UIImage? {
|
||||
var backgroundImage: UIImage?
|
||||
@ -33,3 +35,59 @@ func chatControllerBackgroundImage(wallpaper: TelegramWallpaper, postbox: Postbo
|
||||
}
|
||||
return backgroundImage
|
||||
}
|
||||
|
||||
func chatServiceBackgroundColor(wallpaper: TelegramWallpaper, postbox: Postbox) -> Signal<UIColor, NoError> {
|
||||
if wallpaper == serviceBackgroundColorForWallpaper?.0, let color = serviceBackgroundColorForWallpaper?.1 {
|
||||
return .single(color)
|
||||
} else {
|
||||
switch wallpaper {
|
||||
case .builtin, .color:
|
||||
return .single(UIColor(rgb: 0x000000, alpha: 0.3))
|
||||
case let .image(representations):
|
||||
if let largest = largestImageRepresentation(representations) {
|
||||
return Signal<UIColor, NoError> { subscriber in
|
||||
let fetch = postbox.mediaBox.fetchedResource(largest.resource, parameters: nil).start()
|
||||
let data = (postbox.mediaBox.resourceData(largest.resource)
|
||||
|> mapToSignal { data -> Signal<UIColor, NoError> in
|
||||
if data.complete {
|
||||
let image = UIImage(contentsOfFile: data.path)
|
||||
let context = DrawingContext(size: CGSize(width: 1.0, height: 1.0), scale: 1.0, clear: false)
|
||||
context.withFlippedContext({ context in
|
||||
if let cgImage = image?.cgImage {
|
||||
context.draw(cgImage, in: CGRect(x: 0.0, y: 0.0, width: 1.0, height: 1.0))
|
||||
}
|
||||
})
|
||||
var color = context.colorAt(CGPoint())
|
||||
|
||||
var hue: CGFloat = 0.0
|
||||
var saturation: CGFloat = 0.0
|
||||
var brightness: CGFloat = 0.0
|
||||
var alpha: CGFloat = 0.0
|
||||
if color.getHue(&hue, saturation: &saturation, brightness: &brightness, alpha: &alpha) {
|
||||
saturation = min(1.0, saturation + 0.05 + 0.1 * (1.0 - saturation))
|
||||
brightness = max(0.0, brightness * 0.65)
|
||||
alpha = 0.4
|
||||
color = UIColor(hue: hue, saturation: saturation, brightness: brightness, alpha: alpha)
|
||||
}
|
||||
return .single(color)
|
||||
}
|
||||
return .complete()
|
||||
}).start(next: { next in
|
||||
subscriber.putNext(next)
|
||||
}, completed: {
|
||||
subscriber.putCompletion()
|
||||
})
|
||||
return ActionDisposable {
|
||||
fetch.dispose()
|
||||
data.dispose()
|
||||
}
|
||||
}
|
||||
|> afterNext { color in
|
||||
serviceBackgroundColorForWallpaper = (wallpaper, color)
|
||||
}
|
||||
} else {
|
||||
return .single(UIColor(rgb: 0x000000, alpha: 0.3))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -9,10 +9,14 @@ private let titleFont = Font.medium(16.0)
|
||||
private final class ChatMessageActionButtonNode: ASDisplayNode {
|
||||
private let backgroundNode: ASImageNode
|
||||
private var titleNode: TextNode?
|
||||
private var iconNode: ASImageNode?
|
||||
private var buttonView: HighlightTrackingButton?
|
||||
|
||||
private var button: ReplyMarkupButton?
|
||||
var pressed: ((ReplyMarkupButton) -> Void)?
|
||||
var longTapped: ((ReplyMarkupButton) -> Void)?
|
||||
|
||||
var longTapRecognizer: UILongPressGestureRecognizer?
|
||||
|
||||
override init() {
|
||||
self.backgroundNode = ASImageNode()
|
||||
@ -45,6 +49,11 @@ private final class ChatMessageActionButtonNode: ASDisplayNode {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let longTapRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(self.longTapGesture(_:)))
|
||||
longTapRecognizer.minimumPressDuration = 0.3
|
||||
buttonView.addGestureRecognizer(longTapRecognizer)
|
||||
self.longTapRecognizer = longTapRecognizer
|
||||
}
|
||||
|
||||
@objc func buttonPressed() {
|
||||
@ -53,6 +62,12 @@ private final class ChatMessageActionButtonNode: ASDisplayNode {
|
||||
}
|
||||
}
|
||||
|
||||
@objc func longTapGesture(_ recognizer: UILongPressGestureRecognizer) {
|
||||
if let button = self.button, let longTapped = self.longTapped, recognizer.state == .began {
|
||||
longTapped(button)
|
||||
}
|
||||
}
|
||||
|
||||
class func asyncLayout(_ maybeNode: ChatMessageActionButtonNode?) -> (_ account: Account, _ theme: ChatPresentationThemeData, _ strings: PresentationStrings, _ message: Message, _ button: ReplyMarkupButton, _ constrainedWidth: CGFloat, _ position: MessageBubbleActionButtonPosition) -> (minimumWidth: CGFloat, layout: ((CGFloat) -> (CGSize, () -> ChatMessageActionButtonNode))) {
|
||||
let titleLayout = TextNode.asyncLayout(maybeNode?.titleNode)
|
||||
|
||||
@ -83,9 +98,25 @@ private final class ChatMessageActionButtonNode: ASDisplayNode {
|
||||
case .bottomLeft:
|
||||
backgroundImage = incoming ? graphics.chatBubbleActionButtonIncomingBottomLeftImage : graphics.chatBubbleActionButtonOutgoingBottomLeftImage
|
||||
case .bottomRight:
|
||||
backgroundImage = incoming ? graphics.chatBubbleActionButtonIncomingBottomRightImage : graphics.chatBubbleActionButtonOutgoingBottomRightImage
|
||||
backgroundImage = incoming ? graphics.chatBubbleActionButtonIncomingBottomRightImage : graphics.chatBubbleActionButtonOutgoingBottomRightImage
|
||||
case .bottomSingle:
|
||||
backgroundImage = incoming ? graphics.chatBubbleActionButtonIncomingBottomSingleImage : graphics.chatBubbleActionButtonOutgoingBottomSingleImage
|
||||
backgroundImage = incoming ? graphics.chatBubbleActionButtonIncomingBottomSingleImage : graphics.chatBubbleActionButtonOutgoingBottomSingleImage
|
||||
}
|
||||
|
||||
let iconImage: UIImage?
|
||||
switch button.action {
|
||||
case .text:
|
||||
iconImage = incoming ? graphics.chatBubbleActionButtonIncomingMessageIconImage : graphics.chatBubbleActionButtonOutgoingMessageIconImage
|
||||
case .url:
|
||||
iconImage = incoming ? graphics.chatBubbleActionButtonIncomingLinkIconImage : graphics.chatBubbleActionButtonOutgoingLinkIconImage
|
||||
case .requestPhone:
|
||||
iconImage = incoming ? graphics.chatBubbleActionButtonIncomingPhoneIconImage : graphics.chatBubbleActionButtonOutgoingLinkIconImage
|
||||
case .requestMap:
|
||||
iconImage = incoming ? graphics.chatBubbleActionButtonIncomingLocationIconImage : graphics.chatBubbleActionButtonOutgoingLinkIconImage
|
||||
case .switchInline:
|
||||
iconImage = incoming ? graphics.chatBubbleActionButtonIncomingShareIconImage : graphics.chatBubbleActionButtonOutgoingLinkIconImage
|
||||
default:
|
||||
iconImage = nil
|
||||
}
|
||||
|
||||
return (titleSize.size.width + sideInset + sideInset, { width in
|
||||
@ -99,9 +130,29 @@ private final class ChatMessageActionButtonNode: ASDisplayNode {
|
||||
|
||||
node.button = button
|
||||
|
||||
switch button.action {
|
||||
case .url:
|
||||
node.longTapRecognizer?.isEnabled = true
|
||||
default:
|
||||
node.longTapRecognizer?.isEnabled = false
|
||||
}
|
||||
|
||||
node.backgroundNode.image = backgroundImage
|
||||
node.backgroundNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: max(0.0, width), height: 42.0))
|
||||
|
||||
if iconImage != nil {
|
||||
if node.iconNode == nil {
|
||||
let iconNode = ASImageNode()
|
||||
iconNode.contentMode = .center
|
||||
node.iconNode = iconNode
|
||||
node.addSubnode(iconNode)
|
||||
}
|
||||
node.iconNode?.image = iconImage
|
||||
} else if node.iconNode != nil {
|
||||
node.iconNode?.removeFromSupernode()
|
||||
node.iconNode = nil
|
||||
}
|
||||
|
||||
let titleNode = titleApply()
|
||||
if node.titleNode !== titleNode {
|
||||
node.titleNode = titleNode
|
||||
@ -110,7 +161,9 @@ private final class ChatMessageActionButtonNode: ASDisplayNode {
|
||||
}
|
||||
titleNode.frame = CGRect(origin: CGPoint(x: floor((width - titleSize.size.width) / 2.0), y: floor((42.0 - titleSize.size.height) / 2.0) + 1.0), size: titleSize.size)
|
||||
|
||||
|
||||
node.buttonView?.frame = CGRect(origin: CGPoint(), size: CGSize(width: width, height: 42.0))
|
||||
node.iconNode?.frame = CGRect(x: width - 16.0, y: 4.0, width: 12.0, height: 12.0)
|
||||
|
||||
return node
|
||||
})
|
||||
@ -123,7 +176,9 @@ final class ChatMessageActionButtonsNode: ASDisplayNode {
|
||||
private var buttonNodes: [ChatMessageActionButtonNode] = []
|
||||
|
||||
private var buttonPressedWrapper: ((ReplyMarkupButton) -> Void)?
|
||||
private var buttonLongTappedWrapper: ((ReplyMarkupButton) -> Void)?
|
||||
var buttonPressed: ((ReplyMarkupButton) -> Void)?
|
||||
var buttonLongTapped: ((ReplyMarkupButton) -> Void)?
|
||||
|
||||
override init() {
|
||||
super.init()
|
||||
@ -133,6 +188,12 @@ final class ChatMessageActionButtonsNode: ASDisplayNode {
|
||||
buttonPressed(button)
|
||||
}
|
||||
}
|
||||
|
||||
self.buttonLongTappedWrapper = { [weak self] button in
|
||||
if let buttonLongTapped = self?.buttonLongTapped {
|
||||
buttonLongTapped(button)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class func asyncLayout(_ maybeNode: ChatMessageActionButtonsNode?) -> (_ account: Account, _ theme: ChatPresentationThemeData, _ strings: PresentationStrings, _ replyMarkup: ReplyMarkupMessageAttribute, _ message: Message, _ constrainedWidth: CGFloat) -> (minWidth: CGFloat, layout: (CGFloat) -> (CGSize, (_ animated: Bool) -> ChatMessageActionButtonsNode)) {
|
||||
@ -230,6 +291,7 @@ final class ChatMessageActionButtonsNode: ASDisplayNode {
|
||||
if buttonNode.supernode == nil {
|
||||
node.addSubnode(buttonNode)
|
||||
buttonNode.pressed = node.buttonPressedWrapper
|
||||
buttonNode.longTapped = node.buttonLongTappedWrapper
|
||||
}
|
||||
index += 1
|
||||
}
|
||||
|
||||
@ -1424,6 +1424,11 @@ class ChatMessageBubbleItemNode: ChatMessageItemView {
|
||||
strongSelf.performMessageButtonAction(button: button)
|
||||
}
|
||||
}
|
||||
actionButtonsNode.buttonLongTapped = { button in
|
||||
if let strongSelf = self {
|
||||
strongSelf.presentMessageButtonContextMenu(button: button)
|
||||
}
|
||||
}
|
||||
strongSelf.addSubnode(actionButtonsNode)
|
||||
} else {
|
||||
if case let .System(duration) = animation {
|
||||
|
||||
@ -452,6 +452,11 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView {
|
||||
strongSelf.performMessageButtonAction(button: button)
|
||||
}
|
||||
}
|
||||
actionButtonsNode.buttonLongTapped = { button in
|
||||
if let strongSelf = self {
|
||||
strongSelf.presentMessageButtonContextMenu(button: button)
|
||||
}
|
||||
}
|
||||
strongSelf.addSubnode(actionButtonsNode)
|
||||
} else {
|
||||
if case let .System(duration) = animation {
|
||||
|
||||
@ -196,43 +196,54 @@ public class ChatMessageItemView: ListViewItemNode {
|
||||
func performMessageButtonAction(button: ReplyMarkupButton) {
|
||||
if let item = self.item {
|
||||
switch button.action {
|
||||
case .text:
|
||||
item.controllerInteraction.sendMessage(button.title)
|
||||
case let .url(url):
|
||||
item.controllerInteraction.openUrl(url, true, nil)
|
||||
case .requestMap:
|
||||
item.controllerInteraction.shareCurrentLocation()
|
||||
case .requestPhone:
|
||||
item.controllerInteraction.shareAccountContact()
|
||||
case .openWebApp:
|
||||
item.controllerInteraction.requestMessageActionCallback(item.message.id, nil, true)
|
||||
case let .callback(data):
|
||||
item.controllerInteraction.requestMessageActionCallback(item.message.id, data, false)
|
||||
case let .switchInline(samePeer, query):
|
||||
var botPeer: Peer?
|
||||
case .text:
|
||||
item.controllerInteraction.sendMessage(button.title)
|
||||
case let .url(url):
|
||||
item.controllerInteraction.openUrl(url, true, nil)
|
||||
case .requestMap:
|
||||
item.controllerInteraction.shareCurrentLocation()
|
||||
case .requestPhone:
|
||||
item.controllerInteraction.shareAccountContact()
|
||||
case .openWebApp:
|
||||
item.controllerInteraction.requestMessageActionCallback(item.message.id, nil, true)
|
||||
case let .callback(data):
|
||||
item.controllerInteraction.requestMessageActionCallback(item.message.id, data, false)
|
||||
case let .switchInline(samePeer, query):
|
||||
var botPeer: Peer?
|
||||
|
||||
var found = false
|
||||
for attribute in item.message.attributes {
|
||||
if let attribute = attribute as? InlineBotMessageAttribute {
|
||||
if let peerId = attribute.peerId {
|
||||
botPeer = item.message.peers[peerId]
|
||||
found = true
|
||||
var found = false
|
||||
for attribute in item.message.attributes {
|
||||
if let attribute = attribute as? InlineBotMessageAttribute {
|
||||
if let peerId = attribute.peerId {
|
||||
botPeer = item.message.peers[peerId]
|
||||
found = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
botPeer = item.message.author
|
||||
}
|
||||
if !found {
|
||||
botPeer = item.message.author
|
||||
}
|
||||
|
||||
var peerId: PeerId?
|
||||
if samePeer {
|
||||
peerId = item.message.id.peerId
|
||||
}
|
||||
if let botPeer = botPeer, let addressName = botPeer.addressName {
|
||||
item.controllerInteraction.activateSwitchInline(peerId, "@\(addressName) \(query)")
|
||||
}
|
||||
case .payment:
|
||||
item.controllerInteraction.openCheckoutOrReceipt(item.message.id)
|
||||
var peerId: PeerId?
|
||||
if samePeer {
|
||||
peerId = item.message.id.peerId
|
||||
}
|
||||
if let botPeer = botPeer, let addressName = botPeer.addressName {
|
||||
item.controllerInteraction.activateSwitchInline(peerId, "@\(addressName) \(query)")
|
||||
}
|
||||
case .payment:
|
||||
item.controllerInteraction.openCheckoutOrReceipt(item.message.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func presentMessageButtonContextMenu(button: ReplyMarkupButton) {
|
||||
if let item = self.item {
|
||||
switch button.action {
|
||||
case let .url(url):
|
||||
item.controllerInteraction.longTap(.url(url))
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -61,6 +61,14 @@ class ChatMessageMediaBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
}
|
||||
}
|
||||
|
||||
var hasReplyMarkup: Bool = false
|
||||
for attribute in item.message.attributes {
|
||||
if let attribute = attribute as? ReplyMarkupMessageAttribute, attribute.flags.contains(.inline), !attribute.rows.isEmpty {
|
||||
hasReplyMarkup = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
let bubbleInsets: UIEdgeInsets
|
||||
let sizeCalculation: InteractiveMediaNodeSizeCalculation
|
||||
|
||||
@ -91,6 +99,8 @@ class ChatMessageMediaBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
var updatedPosition: ChatMessageBubbleContentPosition = position
|
||||
if forceFullCorners, case .linear = updatedPosition {
|
||||
updatedPosition = .linear(top: .None(.None(.None)), bottom: .None(.None(.None)))
|
||||
} else if hasReplyMarkup, case let .linear(top, _) = updatedPosition {
|
||||
updatedPosition = .linear(top: top, bottom: .Neighbour)
|
||||
}
|
||||
|
||||
let imageCorners = chatMessageBubbleImageContentCorners(relativeContentPosition: updatedPosition, normalRadius: layoutConstants.image.defaultCornerRadius, mergedRadius: layoutConstants.image.mergedCornerRadius, mergedWithAnotherContentRadius: layoutConstants.image.contentMergedCornerRadius)
|
||||
|
||||
@ -431,6 +431,11 @@ class ChatMessageStickerItemNode: ChatMessageItemView {
|
||||
strongSelf.performMessageButtonAction(button: button)
|
||||
}
|
||||
}
|
||||
actionButtonsNode.buttonLongTapped = { button in
|
||||
if let strongSelf = self {
|
||||
strongSelf.presentMessageButtonContextMenu(button: button)
|
||||
}
|
||||
}
|
||||
strongSelf.addSubnode(actionButtonsNode)
|
||||
} else {
|
||||
if case let .System(duration) = animation {
|
||||
|
||||
37
TelegramUI/CheckDiskSpace.swift
Normal file
@ -0,0 +1,37 @@
|
||||
import Foundation
|
||||
import Display
|
||||
import TelegramCore
|
||||
|
||||
func totalDiskSpace() -> Int64 {
|
||||
do {
|
||||
let systemAttributes = try FileManager.default.attributesOfFileSystem(forPath: NSHomeDirectory() as String)
|
||||
return (systemAttributes[FileAttributeKey.systemSize] as? NSNumber)?.int64Value ?? 0
|
||||
} catch {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
func freeDiskSpace() -> Int64 {
|
||||
do {
|
||||
let systemAttributes = try FileManager.default.attributesOfFileSystem(forPath: NSHomeDirectory() as String)
|
||||
return (systemAttributes[FileAttributeKey.systemFreeSize] as? NSNumber)?.int64Value ?? 0
|
||||
} catch {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
func checkAvailableDiskSpace(account: Account, threshold: Int64 = 100 * 1024 * 1024, present: @escaping (ViewController, Any?) -> Void) -> Bool {
|
||||
guard freeDiskSpace() < threshold else {
|
||||
return true
|
||||
}
|
||||
|
||||
let presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 }
|
||||
let controller = textAlertController(account: account, title: nil, text: presentationData.strings.Cache_LowDiskSpaceText, actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.AccessDenied_Settings, action: {
|
||||
let controller = storageUsageController(account: account, isModal: true)
|
||||
present(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet))
|
||||
|
||||
}), TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})])
|
||||
present(controller, nil)
|
||||
|
||||
return false
|
||||
}
|
||||
@ -34,17 +34,17 @@ final class ComposeControllerNode: ASDisplayNode {
|
||||
var openCreateNewSecretChatImpl: (() -> Void)?
|
||||
var openCreateNewChannelImpl: (() -> Void)?
|
||||
|
||||
self.contactListNode = ContactListNode(account: account, presentation: .natural(displaySearch: true, options: [
|
||||
ContactListAdditionalOption(title: self.presentationData.strings.Compose_NewGroup, icon: generateTintedImage(image: UIImage(bundleImageName: "Contact List/CreateGroupActionIcon"), color: presentationData.theme.list.itemAccentColor), action: {
|
||||
self.contactListNode = ContactListNode(account: account, presentation: .single(.natural(displaySearch: true, options: [
|
||||
ContactListAdditionalOption(title: self.presentationData.strings.Compose_NewGroup, icon: .generic(UIImage(bundleImageName: "Contact List/CreateGroupActionIcon")!), action: {
|
||||
openCreateNewGroupImpl?()
|
||||
}),
|
||||
ContactListAdditionalOption(title: self.presentationData.strings.Compose_NewEncryptedChat, icon: generateTintedImage(image: UIImage(bundleImageName: "Contact List/CreateSecretChatActionIcon"), color: presentationData.theme.list.itemAccentColor), action: {
|
||||
ContactListAdditionalOption(title: self.presentationData.strings.Compose_NewEncryptedChat, icon: .generic(UIImage(bundleImageName: "Contact List/CreateSecretChatActionIcon")!), action: {
|
||||
openCreateNewSecretChatImpl?()
|
||||
}),
|
||||
ContactListAdditionalOption(title: self.presentationData.strings.Compose_NewChannel, icon: generateTintedImage(image: UIImage(bundleImageName: "Contact List/CreateChannelActionIcon"), color: presentationData.theme.list.itemAccentColor), action: {
|
||||
ContactListAdditionalOption(title: self.presentationData.strings.Compose_NewChannel, icon: .generic(UIImage(bundleImageName: "Contact List/CreateChannelActionIcon")!), action: {
|
||||
openCreateNewChannelImpl?()
|
||||
})
|
||||
]), displayPermissionPlaceholder: false)
|
||||
])), displayPermissionPlaceholder: false)
|
||||
|
||||
super.init()
|
||||
|
||||
|
||||
@ -189,9 +189,9 @@ class ContactsAddItemNode: ListViewItemNode {
|
||||
|
||||
let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: titleAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: max(0.0, params.width - leftInset - rightInset), height: CGFloat.infinity), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||||
|
||||
let nodeLayout = ListViewItemNodeLayout(contentSize: CGSize(width: params.width, height: 48.0), insets: UIEdgeInsets(top: firstWithHeader ? 29.0 : 0.0, left: 0.0, bottom: 0.0, right: 0.0))
|
||||
let nodeLayout = ListViewItemNodeLayout(contentSize: CGSize(width: params.width, height: 50.0), insets: UIEdgeInsets(top: firstWithHeader ? 29.0 : 0.0, left: 0.0, bottom: 0.0, right: 0.0))
|
||||
|
||||
let titleFrame = CGRect(origin: CGPoint(x: leftInset, y: 13.0), size: titleLayout.size)
|
||||
let titleFrame = CGRect(origin: CGPoint(x: leftInset, y: 14.0), size: titleLayout.size)
|
||||
|
||||
return (nodeLayout, { [weak self] animated in
|
||||
if let strongSelf = self {
|
||||
@ -216,7 +216,7 @@ class ContactsAddItemNode: ListViewItemNode {
|
||||
if let updatedIcon = updatedIcon {
|
||||
strongSelf.iconNode.image = updatedIcon
|
||||
}
|
||||
transition.updateFrame(node: strongSelf.iconNode, frame: CGRect(x: 14.0, y: 4.0, width: 40.0, height: 40.0))
|
||||
transition.updateFrame(node: strongSelf.iconNode, frame: CGRect(x: 14.0, y: 5.0, width: 40.0, height: 40.0))
|
||||
|
||||
let topHighlightInset: CGFloat = (first || !nodeLayout.insets.top.isZero) ? 0.0 : separatorHeight
|
||||
strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: nodeLayout.contentSize.width, height: nodeLayout.contentSize.height))
|
||||
|
||||
@ -3,14 +3,59 @@ import Display
|
||||
import AsyncDisplayKit
|
||||
import SwiftSignalKit
|
||||
|
||||
public enum ContactListActionItemInlineIconPosition {
|
||||
case left
|
||||
case right
|
||||
}
|
||||
|
||||
public enum ContactListActionItemIcon : Equatable {
|
||||
case none
|
||||
case generic(UIImage)
|
||||
case inline(UIImage, ContactListActionItemInlineIconPosition)
|
||||
|
||||
var image: UIImage? {
|
||||
switch self {
|
||||
case .none:
|
||||
return nil
|
||||
case let .generic(image):
|
||||
return image
|
||||
case let .inline(image, _):
|
||||
return image
|
||||
}
|
||||
}
|
||||
|
||||
public static func ==(lhs: ContactListActionItemIcon, rhs: ContactListActionItemIcon) -> Bool {
|
||||
switch lhs {
|
||||
case .none:
|
||||
if case .none = rhs {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .generic(image):
|
||||
if case .generic(image) = rhs {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .inline(image, position):
|
||||
if case .inline(image, position) = rhs {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class ContactListActionItem: ListViewItem {
|
||||
let theme: PresentationTheme
|
||||
let title: String
|
||||
let icon: UIImage?
|
||||
let icon: ContactListActionItemIcon
|
||||
let action: () -> Void
|
||||
let header: ListViewItemHeader?
|
||||
|
||||
init(theme: PresentationTheme, title: String, icon: UIImage?, header: ListViewItemHeader?, action: @escaping () -> Void) {
|
||||
init(theme: PresentationTheme, title: String, icon: ContactListActionItemIcon, header: ListViewItemHeader?, action: @escaping () -> Void) {
|
||||
self.theme = theme
|
||||
self.title = title
|
||||
self.icon = icon
|
||||
@ -152,13 +197,13 @@ class ContactListActionItemNode: ListViewItemNode {
|
||||
}
|
||||
|
||||
var leftInset: CGFloat = 16.0 + params.leftInset
|
||||
if item.icon != nil {
|
||||
if case .generic = item.icon {
|
||||
leftInset += 49.0
|
||||
}
|
||||
|
||||
let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.title, font: titleFont, textColor: item.theme.list.itemAccentColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - 10.0 - leftInset - params.rightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||||
|
||||
let contentSize = CGSize(width: params.width, height: 48.0)
|
||||
let contentSize = CGSize(width: params.width, height: 50.0)
|
||||
let insets = UIEdgeInsets(top: firstWithHeader ? 29.0 : 0.0, left: 0.0, bottom: 0.0, right: 0.0)
|
||||
let separatorHeight = UIScreenPixel
|
||||
|
||||
@ -174,13 +219,13 @@ class ContactListActionItemNode: ListViewItemNode {
|
||||
strongSelf.bottomStripeNode.backgroundColor = item.theme.list.itemPlainSeparatorColor
|
||||
strongSelf.backgroundNode.backgroundColor = item.theme.list.plainBackgroundColor
|
||||
strongSelf.highlightedBackgroundNode.backgroundColor = item.theme.list.itemHighlightedBackgroundColor
|
||||
|
||||
strongSelf.iconNode.image = generateTintedImage(image: item.icon.image, color: item.theme.list.itemAccentColor)
|
||||
}
|
||||
|
||||
let _ = titleApply()
|
||||
|
||||
strongSelf.iconNode.image = item.icon
|
||||
|
||||
if let image = item.icon {
|
||||
if let image = item.icon.image {
|
||||
strongSelf.iconNode.frame = CGRect(origin: CGPoint(x: params.leftInset + floor((leftInset - params.leftInset - image.size.width) / 2.0), y: floor((contentSize.height - image.size.height) / 2.0)), size: image.size)
|
||||
}
|
||||
|
||||
@ -200,7 +245,7 @@ class ContactListActionItemNode: ListViewItemNode {
|
||||
|
||||
strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: leftInset, y: floor((contentSize.height - titleLayout.size.height) / 2.0)), size: titleLayout.size)
|
||||
|
||||
strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: params.width, height: 48.0 + UIScreenPixel + UIScreenPixel))
|
||||
strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: params.width, height: 50.0 + UIScreenPixel + UIScreenPixel))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@ -172,7 +172,7 @@ private enum ContactListNodeEntry: Comparable, Identifiable {
|
||||
interaction.suppressWarning()
|
||||
})
|
||||
case let .permissionEnable(theme, text):
|
||||
return ContactListActionItem(theme: theme, title: text, icon: nil, header: nil, action: {
|
||||
return ContactListActionItem(theme: theme, title: text, icon: .none, header: nil, action: {
|
||||
interaction.authorize()
|
||||
})
|
||||
case let .option(_, option, header, theme, _):
|
||||
@ -457,25 +457,30 @@ private func contactListNodeEntries(accountPeer: Peer?, peers: [ContactListPeer]
|
||||
var indexHeader: unichar = 35
|
||||
switch peer.indexName {
|
||||
case let .title(title, _):
|
||||
if let c = title.uppercased().utf16.first {
|
||||
if let c = title.folding(options: .diacriticInsensitive, locale: .current).uppercased().utf16.first {
|
||||
indexHeader = c
|
||||
}
|
||||
case let .personName(first, last, _, _):
|
||||
switch sortOrder {
|
||||
case .firstLast:
|
||||
if let c = first.uppercased().utf16.first {
|
||||
if let c = first.folding(options: .diacriticInsensitive, locale: .current).uppercased().utf16.first {
|
||||
indexHeader = c
|
||||
} else if let c = last.uppercased().utf16.first {
|
||||
} else if let c = last.folding(options: .diacriticInsensitive, locale: .current).uppercased().utf16.first {
|
||||
indexHeader = c
|
||||
}
|
||||
case .lastFirst:
|
||||
if let c = last.uppercased().utf16.first {
|
||||
if let c = last.folding(options: .diacriticInsensitive, locale: .current).uppercased().utf16.first {
|
||||
indexHeader = c
|
||||
} else if let c = first.uppercased().utf16.first {
|
||||
} else if let c = first.folding(options: .diacriticInsensitive, locale: .current).uppercased().utf16.first {
|
||||
indexHeader = c
|
||||
}
|
||||
}
|
||||
}
|
||||
if let scalar = UnicodeScalar(indexHeader), !NSCharacterSet.uppercaseLetters.contains(scalar) {
|
||||
if let c = "#".utf16.first {
|
||||
indexHeader = c
|
||||
}
|
||||
}
|
||||
let header: ContactListNameIndexHeader
|
||||
if let cached = headerCache[indexHeader] {
|
||||
header = cached
|
||||
@ -590,11 +595,11 @@ private struct ContactsListNodeTransition {
|
||||
|
||||
public struct ContactListAdditionalOption: Equatable {
|
||||
public let title: String
|
||||
public let icon: UIImage?
|
||||
public let icon: ContactListActionItemIcon
|
||||
public let action: () -> Void
|
||||
|
||||
public static func ==(lhs: ContactListAdditionalOption, rhs: ContactListAdditionalOption) -> Bool {
|
||||
return lhs.title == rhs.title && lhs.icon === rhs.icon
|
||||
return lhs.title == rhs.title && lhs.icon == rhs.icon
|
||||
}
|
||||
}
|
||||
|
||||
@ -638,11 +643,11 @@ enum ContactListFilter {
|
||||
|
||||
final class ContactListNode: ASDisplayNode {
|
||||
private let account: Account
|
||||
private let presentation: ContactListPresentation
|
||||
private var presentation: ContactListPresentation?
|
||||
private let filters: [ContactListFilter]
|
||||
|
||||
let listNode: ListView
|
||||
private var indexNode: CollectionIndexNode?
|
||||
private var indexNode: CollectionIndexNode
|
||||
private var indexSections: [String]?
|
||||
|
||||
private var queuedTransitions: [ContactsListNodeTransition] = []
|
||||
@ -696,9 +701,8 @@ final class ContactListNode: ASDisplayNode {
|
||||
private var authorizationNode: PermissionContentNode
|
||||
private let displayPermissionPlaceholder: Bool
|
||||
|
||||
init(account: Account, presentation: ContactListPresentation, filters: [ContactListFilter] = [.excludeSelf], selectionState: ContactListNodeGroupSelectionState? = nil, displayPermissionPlaceholder: Bool = true) {
|
||||
init(account: Account, presentation: Signal<ContactListPresentation, NoError>, filters: [ContactListFilter] = [.excludeSelf], selectionState: ContactListNodeGroupSelectionState? = nil, displayPermissionPlaceholder: Bool = true) {
|
||||
self.account = account
|
||||
self.presentation = presentation
|
||||
self.filters = filters
|
||||
self.displayPermissionPlaceholder = displayPermissionPlaceholder
|
||||
|
||||
@ -707,13 +711,7 @@ final class ContactListNode: ASDisplayNode {
|
||||
self.listNode = ListView()
|
||||
self.listNode.dynamicBounceEnabled = !self.presentationData.disableAnimations
|
||||
|
||||
var generateSections = false
|
||||
if case .natural = presentation {
|
||||
generateSections = true
|
||||
self.indexNode = CollectionIndexNode()
|
||||
} else {
|
||||
self.indexNode = nil
|
||||
}
|
||||
self.indexNode = CollectionIndexNode()
|
||||
|
||||
self.themeAndStringsPromise = Promise((self.presentationData.theme, self.presentationData.strings, self.presentationData.dateTimeFormat, self.presentationData.nameSortOrder, self.presentationData.nameDisplayOrder, self.presentationData.disableAnimations))
|
||||
|
||||
@ -751,15 +749,13 @@ final class ContactListNode: ASDisplayNode {
|
||||
super.init()
|
||||
|
||||
self.backgroundColor = self.presentationData.theme.chatList.backgroundColor
|
||||
if self.indexNode == nil {
|
||||
self.listNode.verticalScrollIndicatorColor = self.presentationData.theme.list.scrollIndicatorColor
|
||||
}
|
||||
//self.listNode.verticalScrollIndicatorColor = self.presentationData.theme.list.scrollIndicatorColor
|
||||
|
||||
self.selectionStateValue = selectionState
|
||||
self.selectionStatePromise.set(.single(selectionState))
|
||||
|
||||
self.addSubnode(self.listNode)
|
||||
self.indexNode.flatMap(self.addSubnode)
|
||||
self.addSubnode(self.indexNode)
|
||||
self.addSubnode(self.authorizationNode)
|
||||
|
||||
let processingQueue = Queue()
|
||||
@ -775,7 +771,7 @@ final class ContactListNode: ASDisplayNode {
|
||||
self?.openPeer?(peer)
|
||||
})
|
||||
|
||||
self.indexNode?.indexSelected = { [weak self] section in
|
||||
self.indexNode.indexSelected = { [weak self] section in
|
||||
guard let strongSelf = self, let entries = previousEntries.with({ $0 }) else {
|
||||
return
|
||||
}
|
||||
@ -811,124 +807,132 @@ final class ContactListNode: ASDisplayNode {
|
||||
let selectionStateSignal = self.selectionStatePromise.get()
|
||||
let transition: Signal<ContactsListNodeTransition, NoError>
|
||||
let themeAndStringsPromise = self.themeAndStringsPromise
|
||||
if case let .search(query, searchChatList, searchDeviceContacts) = presentation {
|
||||
transition = query
|
||||
|> mapToSignal { query in
|
||||
let foundLocalContacts: Signal<([Peer], [PeerId : PeerPresence]), NoError>
|
||||
if searchChatList {
|
||||
let foundChatListPeers = account.postbox.searchPeers(query: query.lowercased(), groupId: nil)
|
||||
foundLocalContacts = foundChatListPeers
|
||||
|> mapToSignal { peers -> Signal<([Peer], [PeerId : PeerPresence]), NoError> in
|
||||
var resultPeers: [Peer] = []
|
||||
for peer in peers {
|
||||
if peer.peerId.namespace != Namespaces.Peer.CloudUser {
|
||||
continue
|
||||
}
|
||||
if let mainPeer = peer.chatMainPeer {
|
||||
resultPeers.append(mainPeer)
|
||||
}
|
||||
}
|
||||
return account.postbox.transaction { transaction -> ([Peer], [PeerId : PeerPresence]) in
|
||||
var resultPresences: [PeerId: PeerPresence] = [:]
|
||||
for peer in resultPeers {
|
||||
if let presence = transaction.getPeerPresence(peerId: peer.id) {
|
||||
resultPresences[peer.id] = presence
|
||||
}
|
||||
}
|
||||
return (resultPeers, resultPresences)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
foundLocalContacts = account.postbox.searchContacts(query: query.lowercased())
|
||||
}
|
||||
let foundRemoteContacts: Signal<([FoundPeer], [FoundPeer]), NoError> = .single(([], []))
|
||||
|> then(
|
||||
searchPeers(account: account, query: query)
|
||||
|> map { ($0.0, $0.1) }
|
||||
|> delay(0.2, queue: Queue.concurrentDefaultQueue())
|
||||
)
|
||||
let foundDeviceContacts: Signal<[DeviceContactStableId: DeviceContactBasicData], NoError>
|
||||
if searchDeviceContacts {
|
||||
foundDeviceContacts = account.telegramApplicationContext.contactDataManager.search(query: query)
|
||||
} else {
|
||||
foundDeviceContacts = .single([:])
|
||||
}
|
||||
|
||||
return combineLatest(foundLocalContacts, foundRemoteContacts, foundDeviceContacts, selectionStateSignal, themeAndStringsPromise.get())
|
||||
|> mapToQueue { localPeersAndStatuses, remotePeers, deviceContacts, selectionState, themeAndStrings -> Signal<ContactsListNodeTransition, NoError> in
|
||||
let signal = deferred { () -> Signal<ContactsListNodeTransition, NoError> in
|
||||
var existingPeerIds = Set<PeerId>()
|
||||
var disabledPeerIds = Set<PeerId>()
|
||||
|
||||
var existingNormalizedPhoneNumbers = Set<DeviceContactNormalizedPhoneNumber>()
|
||||
for filter in filters {
|
||||
switch filter {
|
||||
case .excludeSelf:
|
||||
existingPeerIds.insert(account.peerId)
|
||||
case let .exclude(peerIds):
|
||||
existingPeerIds = existingPeerIds.union(peerIds)
|
||||
case let .disable(peerIds):
|
||||
disabledPeerIds = disabledPeerIds.union(peerIds)
|
||||
}
|
||||
}
|
||||
|
||||
var peers: [ContactListPeer] = []
|
||||
for peer in localPeersAndStatuses.0 {
|
||||
if !existingPeerIds.contains(peer.id) {
|
||||
existingPeerIds.insert(peer.id)
|
||||
peers.append(.peer(peer: peer, isGlobal: false))
|
||||
if searchDeviceContacts, let user = peer as? TelegramUser, let phone = user.phone {
|
||||
existingNormalizedPhoneNumbers.insert(DeviceContactNormalizedPhoneNumber(rawValue: formatPhoneNumber(phone)))
|
||||
}
|
||||
}
|
||||
}
|
||||
for peer in remotePeers.0 {
|
||||
if peer.peer is TelegramUser {
|
||||
if !existingPeerIds.contains(peer.peer.id) {
|
||||
existingPeerIds.insert(peer.peer.id)
|
||||
peers.append(.peer(peer: peer.peer, isGlobal: true))
|
||||
if searchDeviceContacts, let user = peer.peer as? TelegramUser, let phone = user.phone {
|
||||
existingNormalizedPhoneNumbers.insert(DeviceContactNormalizedPhoneNumber(rawValue: formatPhoneNumber(phone)))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for peer in remotePeers.1 {
|
||||
if peer.peer is TelegramUser {
|
||||
if !existingPeerIds.contains(peer.peer.id) {
|
||||
existingPeerIds.insert(peer.peer.id)
|
||||
peers.append(.peer(peer: peer.peer, isGlobal: true))
|
||||
if searchDeviceContacts, let user = peer.peer as? TelegramUser, let phone = user.phone {
|
||||
existingNormalizedPhoneNumbers.insert(DeviceContactNormalizedPhoneNumber(rawValue: formatPhoneNumber(phone)))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
outer: for (stableId, contact) in deviceContacts {
|
||||
inner: for phoneNumber in contact.phoneNumbers {
|
||||
let normalizedNumber = DeviceContactNormalizedPhoneNumber(rawValue: formatPhoneNumber(phoneNumber.value))
|
||||
if existingNormalizedPhoneNumbers.contains(normalizedNumber) {
|
||||
continue outer
|
||||
}
|
||||
}
|
||||
peers.append(.deviceContact(stableId, contact))
|
||||
}
|
||||
|
||||
let entries = contactListNodeEntries(accountPeer: nil, peers: peers, presences: localPeersAndStatuses.1, presentation: presentation, selectionState: selectionState, theme: themeAndStrings.0, strings: themeAndStrings.1, dateTimeFormat: themeAndStrings.2, sortOrder: themeAndStrings.3, displayOrder: themeAndStrings.4, disabledPeerIds: disabledPeerIds, authorizationStatus: .allowed, warningSuppressed: (true, true))
|
||||
let previous = previousEntries.swap(entries)
|
||||
return .single(preparedContactListNodeTransition(account: account, from: previous ?? [], to: entries, interaction: interaction, firstTime: previous == nil, isEmpty: false, generateIndexSections: generateSections, animation: .none))
|
||||
}
|
||||
|
||||
if OSAtomicCompareAndSwap32(1, 0, &firstTime) {
|
||||
return signal |> runOn(Queue.mainQueue())
|
||||
} else {
|
||||
return signal |> runOn(processingQueue)
|
||||
}
|
||||
}
|
||||
transition = presentation
|
||||
|> mapToSignal { presentation in
|
||||
var generateSections = false
|
||||
if case .natural = presentation {
|
||||
generateSections = true
|
||||
}
|
||||
} else {
|
||||
transition = (combineLatest(self.contactPeersViewPromise.get(), selectionStateSignal, themeAndStringsPromise.get(), contactsAuthorization.get(), contactsWarningSuppressed.get())
|
||||
|
||||
if case let .search(query, searchChatList, searchDeviceContacts) = presentation {
|
||||
return query
|
||||
|> mapToSignal { query in
|
||||
let foundLocalContacts: Signal<([Peer], [PeerId : PeerPresence]), NoError>
|
||||
if searchChatList {
|
||||
let foundChatListPeers = account.postbox.searchPeers(query: query.lowercased(), groupId: nil)
|
||||
foundLocalContacts = foundChatListPeers
|
||||
|> mapToSignal { peers -> Signal<([Peer], [PeerId : PeerPresence]), NoError> in
|
||||
var resultPeers: [Peer] = []
|
||||
for peer in peers {
|
||||
if peer.peerId.namespace != Namespaces.Peer.CloudUser {
|
||||
continue
|
||||
}
|
||||
if let mainPeer = peer.chatMainPeer {
|
||||
resultPeers.append(mainPeer)
|
||||
}
|
||||
}
|
||||
return account.postbox.transaction { transaction -> ([Peer], [PeerId : PeerPresence]) in
|
||||
var resultPresences: [PeerId: PeerPresence] = [:]
|
||||
for peer in resultPeers {
|
||||
if let presence = transaction.getPeerPresence(peerId: peer.id) {
|
||||
resultPresences[peer.id] = presence
|
||||
}
|
||||
}
|
||||
return (resultPeers, resultPresences)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
foundLocalContacts = account.postbox.searchContacts(query: query.lowercased())
|
||||
}
|
||||
let foundRemoteContacts: Signal<([FoundPeer], [FoundPeer]), NoError> = .single(([], []))
|
||||
|> then(
|
||||
searchPeers(account: account, query: query)
|
||||
|> map { ($0.0, $0.1) }
|
||||
|> delay(0.2, queue: Queue.concurrentDefaultQueue())
|
||||
)
|
||||
let foundDeviceContacts: Signal<[DeviceContactStableId: DeviceContactBasicData], NoError>
|
||||
if searchDeviceContacts {
|
||||
foundDeviceContacts = account.telegramApplicationContext.contactDataManager.search(query: query)
|
||||
} else {
|
||||
foundDeviceContacts = .single([:])
|
||||
}
|
||||
|
||||
return combineLatest(foundLocalContacts, foundRemoteContacts, foundDeviceContacts, selectionStateSignal, themeAndStringsPromise.get())
|
||||
|> mapToQueue { localPeersAndStatuses, remotePeers, deviceContacts, selectionState, themeAndStrings -> Signal<ContactsListNodeTransition, NoError> in
|
||||
let signal = deferred { () -> Signal<ContactsListNodeTransition, NoError> in
|
||||
var existingPeerIds = Set<PeerId>()
|
||||
var disabledPeerIds = Set<PeerId>()
|
||||
|
||||
var existingNormalizedPhoneNumbers = Set<DeviceContactNormalizedPhoneNumber>()
|
||||
for filter in filters {
|
||||
switch filter {
|
||||
case .excludeSelf:
|
||||
existingPeerIds.insert(account.peerId)
|
||||
case let .exclude(peerIds):
|
||||
existingPeerIds = existingPeerIds.union(peerIds)
|
||||
case let .disable(peerIds):
|
||||
disabledPeerIds = disabledPeerIds.union(peerIds)
|
||||
}
|
||||
}
|
||||
|
||||
var peers: [ContactListPeer] = []
|
||||
for peer in localPeersAndStatuses.0 {
|
||||
if !existingPeerIds.contains(peer.id) {
|
||||
existingPeerIds.insert(peer.id)
|
||||
peers.append(.peer(peer: peer, isGlobal: false))
|
||||
if searchDeviceContacts, let user = peer as? TelegramUser, let phone = user.phone {
|
||||
existingNormalizedPhoneNumbers.insert(DeviceContactNormalizedPhoneNumber(rawValue: formatPhoneNumber(phone)))
|
||||
}
|
||||
}
|
||||
}
|
||||
for peer in remotePeers.0 {
|
||||
if peer.peer is TelegramUser {
|
||||
if !existingPeerIds.contains(peer.peer.id) {
|
||||
existingPeerIds.insert(peer.peer.id)
|
||||
peers.append(.peer(peer: peer.peer, isGlobal: true))
|
||||
if searchDeviceContacts, let user = peer.peer as? TelegramUser, let phone = user.phone {
|
||||
existingNormalizedPhoneNumbers.insert(DeviceContactNormalizedPhoneNumber(rawValue: formatPhoneNumber(phone)))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for peer in remotePeers.1 {
|
||||
if peer.peer is TelegramUser {
|
||||
if !existingPeerIds.contains(peer.peer.id) {
|
||||
existingPeerIds.insert(peer.peer.id)
|
||||
peers.append(.peer(peer: peer.peer, isGlobal: true))
|
||||
if searchDeviceContacts, let user = peer.peer as? TelegramUser, let phone = user.phone {
|
||||
existingNormalizedPhoneNumbers.insert(DeviceContactNormalizedPhoneNumber(rawValue: formatPhoneNumber(phone)))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
outer: for (stableId, contact) in deviceContacts {
|
||||
inner: for phoneNumber in contact.phoneNumbers {
|
||||
let normalizedNumber = DeviceContactNormalizedPhoneNumber(rawValue: formatPhoneNumber(phoneNumber.value))
|
||||
if existingNormalizedPhoneNumbers.contains(normalizedNumber) {
|
||||
continue outer
|
||||
}
|
||||
}
|
||||
peers.append(.deviceContact(stableId, contact))
|
||||
}
|
||||
|
||||
let entries = contactListNodeEntries(accountPeer: nil, peers: peers, presences: localPeersAndStatuses.1, presentation: presentation, selectionState: selectionState, theme: themeAndStrings.0, strings: themeAndStrings.1, dateTimeFormat: themeAndStrings.2, sortOrder: themeAndStrings.3, displayOrder: themeAndStrings.4, disabledPeerIds: disabledPeerIds, authorizationStatus: .allowed, warningSuppressed: (true, true))
|
||||
let previous = previousEntries.swap(entries)
|
||||
return .single(preparedContactListNodeTransition(account: account, from: previous ?? [], to: entries, interaction: interaction, firstTime: previous == nil, isEmpty: false, generateIndexSections: generateSections, animation: .none))
|
||||
}
|
||||
|
||||
if OSAtomicCompareAndSwap32(1, 0, &firstTime) {
|
||||
return signal |> runOn(Queue.mainQueue())
|
||||
} else {
|
||||
return signal |> runOn(processingQueue)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return (combineLatest(self.contactPeersViewPromise.get(), selectionStateSignal, themeAndStringsPromise.get(), contactsAuthorization.get(), contactsWarningSuppressed.get())
|
||||
|> mapToQueue { view, selectionState, themeAndStrings, authorizationStatus, warningSuppressed -> Signal<ContactsListNodeTransition, NoError> in
|
||||
let signal = deferred { () -> Signal<ContactsListNodeTransition, NoError> in
|
||||
var peers = view.peers.map({ ContactListPeer.peer(peer: $0, isGlobal: false) })
|
||||
@ -998,6 +1002,7 @@ final class ContactListNode: ASDisplayNode {
|
||||
}
|
||||
})
|
||||
|> deliverOnMainQueue
|
||||
}
|
||||
}
|
||||
self.disposable.set(transition.start(next: { [weak self] transition in
|
||||
self?.enqueueTransition(transition)
|
||||
@ -1127,7 +1132,7 @@ final class ContactListNode: ASDisplayNode {
|
||||
let updateSizeAndInsets = ListViewUpdateSizeAndInsets(size: layout.size, insets: insets, duration: duration, curve: listViewCurve)
|
||||
|
||||
self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: nil, updateSizeAndInsets: updateSizeAndInsets, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in })
|
||||
if let indexNode = self.indexNode, let indexSections = self.indexSections {
|
||||
if let indexSections = self.indexSections {
|
||||
var insets = layout.insets(options: [.input])
|
||||
if let inputHeight = layout.inputHeight {
|
||||
insets.bottom -= inputHeight
|
||||
@ -1137,7 +1142,7 @@ final class ContactListNode: ASDisplayNode {
|
||||
|
||||
let indexNodeFrame = CGRect(origin: CGPoint(x: layout.size.width - insets.right - 20.0, y: insets.top), size: CGSize(width: 20.0, height: layout.size.height - insets.top - insets.bottom))
|
||||
transition.updateFrame(node: indexNode, frame: indexNodeFrame)
|
||||
indexNode.update(size: indexNodeFrame.size, color: self.presentationData.theme.list.itemAccentColor, sections: indexSections, transition: transition)
|
||||
self.indexNode.update(size: indexNodeFrame.size, color: self.presentationData.theme.list.itemAccentColor, sections: indexSections, transition: transition)
|
||||
}
|
||||
|
||||
self.authorizationNode.updateLayout(size: layout.size, insets: insets, transition: transition)
|
||||
@ -1168,11 +1173,11 @@ final class ContactListNode: ASDisplayNode {
|
||||
} else if transition.animation != .none {
|
||||
if transition.animation == .insertion {
|
||||
options.insert(.AnimateInsertion)
|
||||
} else if case .orderedByPresence = self.presentation {
|
||||
} else if let presentation = self.presentation, case .orderedByPresence = presentation {
|
||||
options.insert(.AnimateCrossfade)
|
||||
}
|
||||
}
|
||||
if let indexNode = self.indexNode, let layout = self.validLayout {
|
||||
if let layout = self.validLayout {
|
||||
self.indexSections = transition.indexSections
|
||||
|
||||
var insets = layout.insets(options: [.input])
|
||||
@ -1184,9 +1189,9 @@ final class ContactListNode: ASDisplayNode {
|
||||
}
|
||||
|
||||
let indexNodeFrame = CGRect(origin: CGPoint(x: layout.size.width - insets.right - 20.0, y: insets.top), size: CGSize(width: 20.0, height: layout.size.height - insets.top - insets.bottom))
|
||||
indexNode.frame = indexNodeFrame
|
||||
self.indexNode.frame = indexNodeFrame
|
||||
|
||||
indexNode.update(size: CGSize(width: 20.0, height: layout.size.height - insets.top - insets.bottom), color: self.presentationData.theme.list.itemAccentColor, sections: transition.indexSections, transition: .immediate)
|
||||
self.indexNode.update(size: CGSize(width: 20.0, height: layout.size.height - insets.top - insets.bottom), color: self.presentationData.theme.list.itemAccentColor, sections: transition.indexSections, transition: .animated(duration: 0.2, curve: .easeInOut))
|
||||
}
|
||||
self.listNode.transaction(deleteIndices: transition.deletions, insertIndicesAndItems: transition.insertions, updateIndicesAndItems: transition.updates, options: options, updateOpaqueState: nil, completion: { [weak self] _ in
|
||||
if let strongSelf = self {
|
||||
|
||||
@ -56,7 +56,7 @@ final class ContactMultiselectionControllerNode: ASDisplayNode {
|
||||
placeholder = self.presentationData.strings.Compose_TokenListPlaceholder
|
||||
}
|
||||
|
||||
self.contactListNode = ContactListNode(account: account, presentation: .natural(displaySearch: false, options: options), filters: filters, selectionState: ContactListNodeGroupSelectionState())
|
||||
self.contactListNode = ContactListNode(account: account, presentation: .single(.natural(displaySearch: false, options: options)), filters: filters, selectionState: ContactListNodeGroupSelectionState())
|
||||
self.tokenListNode = EditableTokenListNode(theme: EditableTokenListNodeTheme(backgroundColor: self.presentationData.theme.rootController.navigationBar.backgroundColor, separatorColor: self.presentationData.theme.rootController.navigationBar.separatorColor, placeholderTextColor: self.presentationData.theme.list.itemPlaceholderTextColor, primaryTextColor: self.presentationData.theme.list.itemPrimaryTextColor, selectedTextColor: self.presentationData.theme.list.itemAccentColor, keyboardColor: self.presentationData.theme.chatList.searchBarKeyboardColor), placeholder: placeholder)
|
||||
|
||||
super.init()
|
||||
@ -99,7 +99,7 @@ final class ContactMultiselectionControllerNode: ASDisplayNode {
|
||||
if case let .peerSelection(value) = mode {
|
||||
searchChatList = value
|
||||
}
|
||||
let searchResultsNode = ContactListNode(account: account, presentation: .search(signal: searchText.get(), searchChatList: searchChatList, searchDeviceContacts: false), filters: filters, selectionState: selectionState)
|
||||
let searchResultsNode = ContactListNode(account: account, presentation: .single(.search(signal: searchText.get(), searchChatList: searchChatList, searchDeviceContacts: false)), filters: filters, selectionState: selectionState)
|
||||
searchResultsNode.openPeer = { peer in
|
||||
self?.tokenListNode.setText("")
|
||||
self?.openPeer?(peer)
|
||||
|
||||
@ -39,7 +39,7 @@ final class ContactSelectionControllerNode: ASDisplayNode {
|
||||
self.presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 }
|
||||
self.displayDeviceContacts = displayDeviceContacts
|
||||
|
||||
self.contactListNode = ContactListNode(account: account, presentation: .natural(displaySearch: true, options: options))
|
||||
self.contactListNode = ContactListNode(account: account, presentation: .single(.natural(displaySearch: true, options: options)))
|
||||
|
||||
self.dimNode = ASDisplayNode()
|
||||
|
||||
|
||||
@ -2,27 +2,36 @@ import Foundation
|
||||
import Postbox
|
||||
import SwiftSignalKit
|
||||
|
||||
public enum ContactsSortOrder: Int32 {
|
||||
case presence
|
||||
case natural
|
||||
}
|
||||
|
||||
public struct ContactSynchronizationSettings: Equatable, PreferencesEntry {
|
||||
public var synchronizeDeviceContacts: Bool
|
||||
public var nameDisplayOrder: PresentationPersonNameOrder
|
||||
public var sortOrder: ContactsSortOrder
|
||||
|
||||
public static var defaultSettings: ContactSynchronizationSettings {
|
||||
return ContactSynchronizationSettings(synchronizeDeviceContacts: true, nameDisplayOrder: .firstLast)
|
||||
return ContactSynchronizationSettings(synchronizeDeviceContacts: true, nameDisplayOrder: .firstLast, sortOrder: .presence)
|
||||
}
|
||||
|
||||
public init(synchronizeDeviceContacts: Bool, nameDisplayOrder: PresentationPersonNameOrder) {
|
||||
public init(synchronizeDeviceContacts: Bool, nameDisplayOrder: PresentationPersonNameOrder, sortOrder: ContactsSortOrder) {
|
||||
self.synchronizeDeviceContacts = synchronizeDeviceContacts
|
||||
self.nameDisplayOrder = nameDisplayOrder
|
||||
self.sortOrder = sortOrder
|
||||
}
|
||||
|
||||
public init(decoder: PostboxDecoder) {
|
||||
self.synchronizeDeviceContacts = decoder.decodeInt32ForKey("synchronizeDeviceContacts", orElse: 0) != 0
|
||||
self.nameDisplayOrder = PresentationPersonNameOrder(rawValue: decoder.decodeInt32ForKey("nameDisplayOrder", orElse: 0)) ?? .firstLast
|
||||
self.sortOrder = ContactsSortOrder(rawValue: decoder.decodeInt32ForKey("sortOrder", orElse: 0)) ?? .presence
|
||||
}
|
||||
|
||||
public func encode(_ encoder: PostboxEncoder) {
|
||||
encoder.encodeInt32(self.synchronizeDeviceContacts ? 1 : 0, forKey: "synchronizeDeviceContacts")
|
||||
encoder.encodeInt32(self.nameDisplayOrder.rawValue, forKey: "synchronizeDeviceContacts")
|
||||
encoder.encodeInt32(self.nameDisplayOrder.rawValue, forKey: "nameDisplayOrder")
|
||||
encoder.encodeInt32(self.sortOrder.rawValue, forKey: "sortOrder")
|
||||
}
|
||||
|
||||
public func isEqual(to: PreferencesEntry) -> Bool {
|
||||
|
||||
@ -22,6 +22,7 @@ public class ContactsController: ViewController {
|
||||
private var presentationData: PresentationData
|
||||
private var presentationDataDisposable: Disposable?
|
||||
private var authorizationDisposable: Disposable?
|
||||
private let sortOrderPromise = Promise<ContactsSortOrder>()
|
||||
|
||||
public init(account: Account) {
|
||||
self.account = account
|
||||
@ -46,6 +47,7 @@ public class ContactsController: ViewController {
|
||||
self.tabBarItem.selectedImage = icon
|
||||
|
||||
self.navigationItem.backBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Back, style: .plain, target: nil, action: nil)
|
||||
self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: "Sort", style: .plain, target: self, action: #selector(self.sortPressed))
|
||||
self.navigationItem.rightBarButtonItem = UIBarButtonItem(image: PresentationResourcesRootController.navigationAddIcon(self.presentationData.theme), style: .plain, target: self, action: #selector(self.addPressed))
|
||||
|
||||
self.scrollToTop = { [weak self] in
|
||||
@ -68,27 +70,37 @@ public class ContactsController: ViewController {
|
||||
}
|
||||
})
|
||||
|
||||
let preferencesKey = PostboxViewKey.preferences(keys: Set([ApplicationSpecificPreferencesKeys.contactSynchronizationSettings]))
|
||||
if #available(iOSApplicationExtension 10.0, *) {
|
||||
let warningKey = PostboxViewKey.noticeEntry(ApplicationSpecificNotice.contactsPermissionWarningKey())
|
||||
let preferencesKey = PostboxViewKey.preferences(keys: Set([ApplicationSpecificPreferencesKeys.contactSynchronizationSettings]))
|
||||
self.authorizationDisposable = (combineLatest(DeviceAccess.authorizationStatus(account: account, subject: .contacts), account.postbox.combinedView(keys: [warningKey, preferencesKey])
|
||||
|> map { combined -> Bool in
|
||||
let synchronizeDeviceContacts: Bool = ((combined.views[preferencesKey] as? PreferencesView)?.values[ApplicationSpecificPreferencesKeys.contactSynchronizationSettings] as? ContactSynchronizationSettings)?.synchronizeDeviceContacts ?? true
|
||||
|> map { combined -> (Bool, ContactsSortOrder) in
|
||||
let settings = ((combined.views[preferencesKey] as? PreferencesView)?.values[ApplicationSpecificPreferencesKeys.contactSynchronizationSettings] as? ContactSynchronizationSettings)
|
||||
let synchronizeDeviceContacts: Bool = settings?.synchronizeDeviceContacts ?? true
|
||||
let sortOrder: ContactsSortOrder = settings?.sortOrder ?? .presence
|
||||
if !synchronizeDeviceContacts {
|
||||
return true
|
||||
return (true, sortOrder)
|
||||
}
|
||||
let timestamp = (combined.views[warningKey] as? NoticeEntryView)?.value.flatMap({ ApplicationSpecificNotice.getTimestampValue($0) })
|
||||
if let timestamp = timestamp, timestamp > 0 {
|
||||
return true
|
||||
return (true, sortOrder)
|
||||
} else {
|
||||
return false
|
||||
return (false, sortOrder)
|
||||
}
|
||||
})
|
||||
|> deliverOnMainQueue).start(next: { [weak self] status, suppressed in
|
||||
|> deliverOnMainQueue).start(next: { [weak self] status, suppressedAndSortOrder in
|
||||
if let strongSelf = self {
|
||||
let (suppressed, sortOrder) = suppressedAndSortOrder
|
||||
strongSelf.tabBarItem.badgeValue = status != .allowed && !suppressed ? "!" : nil
|
||||
strongSelf.sortOrderPromise.set(.single(sortOrder))
|
||||
}
|
||||
})
|
||||
} else {
|
||||
self.sortOrderPromise.set(account.postbox.combinedView(keys: [preferencesKey])
|
||||
|> map { combined -> ContactsSortOrder in
|
||||
let settings = ((combined.views[preferencesKey] as? PreferencesView)?.values[ApplicationSpecificPreferencesKeys.contactSynchronizationSettings] as? ContactSynchronizationSettings)
|
||||
return settings?.sortOrder ?? .presence
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@ -113,7 +125,7 @@ public class ContactsController: ViewController {
|
||||
}
|
||||
|
||||
override public func loadDisplayNode() {
|
||||
self.displayNode = ContactsControllerNode(account: self.account, present: { [weak self] c, a in
|
||||
self.displayNode = ContactsControllerNode(account: self.account, sortOrder: sortOrderPromise.get() |> distinctUntilChanged, present: { [weak self] c, a in
|
||||
self?.present(c, in: .window(.root), with: a)
|
||||
})
|
||||
self._ready.set(self.contactsNode.contactListNode.ready)
|
||||
@ -226,6 +238,40 @@ public class ContactsController: ViewController {
|
||||
}
|
||||
}
|
||||
|
||||
func updateSortOrder(_ sortOrder: ContactsSortOrder) {
|
||||
self.sortOrderPromise.set(.single(sortOrder))
|
||||
let _ = updateContactSettingsInteractively(postbox: self.account.postbox) { current -> ContactSynchronizationSettings in
|
||||
var updated = current
|
||||
updated.sortOrder = sortOrder
|
||||
return updated
|
||||
}.start()
|
||||
}
|
||||
|
||||
@objc func sortPressed() {
|
||||
let actionSheet = ActionSheetController(presentationTheme: self.presentationData.theme)
|
||||
|
||||
var items: [ActionSheetItem] = []
|
||||
items.append(ActionSheetTextItem(title: self.presentationData.strings.Contacts_SortBy))
|
||||
items.append(ActionSheetButtonItem(title: self.presentationData.strings.Contacts_SortByName, color: .accent, action: { [weak self, weak actionSheet] in
|
||||
actionSheet?.dismissAnimated()
|
||||
if let strongSelf = self {
|
||||
strongSelf.updateSortOrder(.natural)
|
||||
}
|
||||
}))
|
||||
items.append(ActionSheetButtonItem(title: self.presentationData.strings.Contacts_SortByPresence, color: .accent, action: { [weak self, weak actionSheet] in
|
||||
actionSheet?.dismissAnimated()
|
||||
if let strongSelf = self {
|
||||
strongSelf.updateSortOrder(.presence)
|
||||
}
|
||||
}))
|
||||
actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [
|
||||
ActionSheetButtonItem(title: self.presentationData.strings.Common_Cancel, color: .accent, action: { [weak actionSheet] in
|
||||
actionSheet?.dismissAnimated()
|
||||
})
|
||||
])])
|
||||
self.present(actionSheet, in: .window(.root))
|
||||
}
|
||||
|
||||
@objc func addPressed() {
|
||||
let _ = (DeviceAccess.authorizationStatus(account: self.account, subject: .contacts)
|
||||
|> take(1)
|
||||
|
||||
@ -22,15 +22,27 @@ final class ContactsControllerNode: ASDisplayNode {
|
||||
private var presentationData: PresentationData
|
||||
private var presentationDataDisposable: Disposable?
|
||||
|
||||
init(account: Account, present: @escaping (ViewController, Any?) -> Void) {
|
||||
init(account: Account, sortOrder: Signal<ContactsSortOrder, NoError>, present: @escaping (ViewController, Any?) -> Void) {
|
||||
self.account = account
|
||||
|
||||
self.presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 }
|
||||
|
||||
var inviteImpl: (() -> Void)?
|
||||
self.contactListNode = ContactListNode(account: account, presentation: .orderedByPresence(options: [ContactListAdditionalOption(title: presentationData.strings.Contacts_InviteFriends, icon: generateTintedImage(image: UIImage(bundleImageName: "Contact List/AddMemberIcon"), color: self.presentationData.theme.list.itemAccentColor), action: {
|
||||
let options = [ContactListAdditionalOption(title: presentationData.strings.Contacts_InviteFriends, icon: .generic(UIImage(bundleImageName: "Contact List/AddMemberIcon")!), action: {
|
||||
inviteImpl?()
|
||||
})]))
|
||||
})]
|
||||
|
||||
let presentation = sortOrder
|
||||
|> map { sortOrder -> ContactListPresentation in
|
||||
switch sortOrder {
|
||||
case .presence:
|
||||
return .orderedByPresence(options: options)
|
||||
case .natural:
|
||||
return .natural(displaySearch: true, options: options)
|
||||
}
|
||||
}
|
||||
|
||||
self.contactListNode = ContactListNode(account: account, presentation: presentation)
|
||||
|
||||
super.init()
|
||||
|
||||
|
||||
@ -478,31 +478,31 @@ class ContactsPeerItemNode: ItemListRevealOptionsItemNode {
|
||||
}
|
||||
|
||||
switch item.status {
|
||||
case .none:
|
||||
break
|
||||
case let .presence(presence, dateTimeFormat):
|
||||
let presence = (presence as? TelegramUserPresence) ?? TelegramUserPresence(status: .none, lastActivity: 0)
|
||||
userPresence = presence
|
||||
let timestamp = CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970
|
||||
let (string, activity) = stringAndActivityForUserPresence(strings: item.strings, dateTimeFormat: dateTimeFormat, presence: presence, relativeTo: Int32(timestamp))
|
||||
statusAttributedString = NSAttributedString(string: string, font: statusFont, textColor: activity ? item.theme.list.itemAccentColor : item.theme.list.itemSecondaryTextColor)
|
||||
case let .addressName(suffix):
|
||||
if let addressName = peer.addressName {
|
||||
let addressNameString = NSAttributedString(string: "@" + addressName, font: statusFont, textColor: item.theme.list.itemAccentColor)
|
||||
if !suffix.isEmpty {
|
||||
let suffixString = NSAttributedString(string: suffix, font: statusFont, textColor: item.theme.list.itemSecondaryTextColor)
|
||||
let finalString = NSMutableAttributedString()
|
||||
finalString.append(addressNameString)
|
||||
finalString.append(suffixString)
|
||||
statusAttributedString = finalString
|
||||
} else {
|
||||
statusAttributedString = addressNameString
|
||||
case .none:
|
||||
break
|
||||
case let .presence(presence, dateTimeFormat):
|
||||
let presence = (presence as? TelegramUserPresence) ?? TelegramUserPresence(status: .none, lastActivity: 0)
|
||||
userPresence = presence
|
||||
let timestamp = CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970
|
||||
let (string, activity) = stringAndActivityForUserPresence(strings: item.strings, dateTimeFormat: dateTimeFormat, presence: presence, relativeTo: Int32(timestamp))
|
||||
statusAttributedString = NSAttributedString(string: string, font: statusFont, textColor: activity ? item.theme.list.itemAccentColor : item.theme.list.itemSecondaryTextColor)
|
||||
case let .addressName(suffix):
|
||||
if let addressName = peer.addressName {
|
||||
let addressNameString = NSAttributedString(string: "@" + addressName, font: statusFont, textColor: item.theme.list.itemAccentColor)
|
||||
if !suffix.isEmpty {
|
||||
let suffixString = NSAttributedString(string: suffix, font: statusFont, textColor: item.theme.list.itemSecondaryTextColor)
|
||||
let finalString = NSMutableAttributedString()
|
||||
finalString.append(addressNameString)
|
||||
finalString.append(suffixString)
|
||||
statusAttributedString = finalString
|
||||
} else {
|
||||
statusAttributedString = addressNameString
|
||||
}
|
||||
} else if !suffix.isEmpty {
|
||||
statusAttributedString = NSAttributedString(string: suffix, font: statusFont, textColor: item.theme.list.itemSecondaryTextColor)
|
||||
}
|
||||
} else if !suffix.isEmpty {
|
||||
statusAttributedString = NSAttributedString(string: suffix, font: statusFont, textColor: item.theme.list.itemSecondaryTextColor)
|
||||
}
|
||||
case let .custom(text):
|
||||
statusAttributedString = NSAttributedString(string: text, font: statusFont, textColor: item.theme.list.itemSecondaryTextColor)
|
||||
case let .custom(text):
|
||||
statusAttributedString = NSAttributedString(string: text, font: statusFont, textColor: item.theme.list.itemSecondaryTextColor)
|
||||
}
|
||||
}
|
||||
case let .deviceContact(_, contact):
|
||||
@ -562,13 +562,13 @@ class ContactsPeerItemNode: ItemListRevealOptionsItemNode {
|
||||
|
||||
let (statusLayout, statusApply) = makeStatusLayout(TextNodeLayoutArguments(attributedString: statusAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: max(0.0, params.width - leftInset - rightInset - badgeSize), height: CGFloat.infinity), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||||
|
||||
let nodeLayout = ListViewItemNodeLayout(contentSize: CGSize(width: params.width, height: 48.0), insets: UIEdgeInsets(top: firstWithHeader ? 29.0 : 0.0, left: 0.0, bottom: 0.0, right: 0.0))
|
||||
let nodeLayout = ListViewItemNodeLayout(contentSize: CGSize(width: params.width, height: 50.0), insets: UIEdgeInsets(top: firstWithHeader ? 29.0 : 0.0, left: 0.0, bottom: 0.0, right: 0.0))
|
||||
|
||||
let titleFrame: CGRect
|
||||
if statusAttributedString != nil {
|
||||
titleFrame = CGRect(origin: CGPoint(x: leftInset, y: 4.0), size: titleLayout.size)
|
||||
titleFrame = CGRect(origin: CGPoint(x: leftInset, y: 6.0), size: titleLayout.size)
|
||||
} else {
|
||||
titleFrame = CGRect(origin: CGPoint(x: leftInset, y: 13.0), size: titleLayout.size)
|
||||
titleFrame = CGRect(origin: CGPoint(x: leftInset, y: 14.0), size: titleLayout.size)
|
||||
}
|
||||
|
||||
let peerRevealOptions: [ItemListRevealOption]
|
||||
@ -641,7 +641,7 @@ class ContactsPeerItemNode: ItemListRevealOptionsItemNode {
|
||||
strongSelf.highlightedBackgroundNode.backgroundColor = item.theme.list.itemHighlightedBackgroundColor
|
||||
}
|
||||
|
||||
transition.updateFrame(node: strongSelf.avatarNode, frame: CGRect(origin: CGPoint(x: revealOffset + leftInset - 51.0, y: 4.0), size: CGSize(width: 40.0, height: 40.0)))
|
||||
transition.updateFrame(node: strongSelf.avatarNode, frame: CGRect(origin: CGPoint(x: revealOffset + leftInset - 50.0, y: 5.0), size: CGSize(width: 40.0, height: 40.0)))
|
||||
|
||||
let _ = titleApply()
|
||||
transition.updateFrame(node: strongSelf.titleNode, frame: titleFrame.offsetBy(dx: revealOffset, dy: 0.0))
|
||||
@ -650,7 +650,7 @@ class ContactsPeerItemNode: ItemListRevealOptionsItemNode {
|
||||
strongSelf.statusNode.alpha = item.enabled ? 1.0 : 1.0
|
||||
|
||||
let _ = statusApply()
|
||||
let statusFrame = CGRect(origin: CGPoint(x: revealOffset + leftInset, y: 25.0), size: statusLayout.size)
|
||||
let statusFrame = CGRect(origin: CGPoint(x: revealOffset + leftInset, y: 27.0), size: statusLayout.size)
|
||||
let previousStatusFrame = strongSelf.statusNode.frame
|
||||
|
||||
strongSelf.statusNode.frame = statusFrame
|
||||
@ -776,7 +776,7 @@ class ContactsPeerItemNode: ItemListRevealOptionsItemNode {
|
||||
}
|
||||
|
||||
var avatarFrame = self.avatarNode.frame
|
||||
avatarFrame.origin.x = offset + leftInset - 51.0
|
||||
avatarFrame.origin.x = offset + leftInset - 50.0
|
||||
transition.updateFrame(node: self.avatarNode, frame: avatarFrame)
|
||||
|
||||
var titleFrame = self.titleNode.frame
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
|
||||
private func makeDefaultPresentationTheme(accentColor: UIColor, day: Bool) -> PresentationTheme {
|
||||
private func makeDefaultPresentationTheme(accentColor: UIColor, serviceBackgroundColor: UIColor, day: Bool) -> PresentationTheme {
|
||||
let destructiveColor: UIColor = UIColor(rgb: 0xff3b30)
|
||||
let constructiveColor: UIColor = UIColor(rgb: 0x4cd964)
|
||||
let secretColor: UIColor = UIColor(rgb: 0x00B12C)
|
||||
@ -201,15 +201,15 @@ private func makeDefaultPresentationTheme(accentColor: UIColor, day: Bool) -> Pr
|
||||
outgoingFileDescriptionColor: UIColor(rgb: 0x6fb26a),
|
||||
incomingFileDurationColor: UIColor(rgb: 0x525252, alpha: 0.6),
|
||||
outgoingFileDurationColor: UIColor(rgb: 0x008c09, alpha: 0.8),
|
||||
shareButtonFillColor: PresentationThemeVariableColor(withWallpaper: UIColor(rgb: 0x000000, alpha: 0.3), withoutWallpaper: UIColor(rgb: 0x748391, alpha: 0.45)),
|
||||
shareButtonFillColor: PresentationThemeVariableColor(withWallpaper: serviceBackgroundColor, withoutWallpaper: UIColor(rgb: 0x748391, alpha: 0.45)),
|
||||
shareButtonStrokeColor: .clear,
|
||||
shareButtonForegroundColor: .white,
|
||||
mediaOverlayControlBackgroundColor: UIColor(white: 0.0, alpha: 0.6),
|
||||
mediaOverlayControlForegroundColor: UIColor(white: 1.0, alpha: 1.0),
|
||||
actionButtonsIncomingFillColor: PresentationThemeVariableColor(withWallpaper: UIColor(rgb: 0x000000, alpha: 0.25), withoutWallpaper: UIColor(rgb: 0x596E89, alpha: 0.35)),
|
||||
actionButtonsIncomingFillColor: PresentationThemeVariableColor(withWallpaper: serviceBackgroundColor, withoutWallpaper: UIColor(rgb: 0x596E89, alpha: 0.35)),
|
||||
actionButtonsIncomingStrokeColor: .clear,
|
||||
actionButtonsIncomingTextColor: .white,
|
||||
actionButtonsOutgoingFillColor: PresentationThemeVariableColor(withWallpaper: UIColor(rgb: 0x000000, alpha: 0.25), withoutWallpaper: UIColor(rgb: 0x596E89, alpha: 0.35)),
|
||||
actionButtonsOutgoingFillColor: PresentationThemeVariableColor(withWallpaper: serviceBackgroundColor, withoutWallpaper: UIColor(rgb: 0x596E89, alpha: 0.35)),
|
||||
actionButtonsOutgoingStrokeColor: .clear,
|
||||
actionButtonsOutgoingTextColor: .white,
|
||||
selectionControlBorderColor: UIColor(rgb: 0xC7C7CC),
|
||||
@ -263,11 +263,11 @@ private func makeDefaultPresentationTheme(accentColor: UIColor, day: Bool) -> Pr
|
||||
mediaOverlayControlBackgroundColor: UIColor(rgb: 0x000000, alpha: 0.6),
|
||||
mediaOverlayControlForegroundColor: UIColor(rgb: 0xffffff, alpha: 1.0),
|
||||
actionButtonsIncomingFillColor: PresentationThemeVariableColor(withWallpaper: UIColor(rgb: 0xffffff, alpha: 0.8), withoutWallpaper: UIColor(rgb: 0xffffff, alpha: 0.8)),
|
||||
actionButtonsIncomingStrokeColor: UIColor(rgb: 0x3996ee),
|
||||
actionButtonsIncomingTextColor: UIColor(rgb: 0x3996ee),
|
||||
actionButtonsIncomingStrokeColor: accentColor.withMultipliedBrightnessBy(1.2),
|
||||
actionButtonsIncomingTextColor: accentColor.withMultipliedBrightnessBy(1.2),
|
||||
actionButtonsOutgoingFillColor: PresentationThemeVariableColor(withWallpaper: UIColor(rgb: 0xffffff, alpha: 0.8), withoutWallpaper: UIColor(rgb: 0xffffff, alpha: 0.8)),
|
||||
actionButtonsOutgoingStrokeColor: UIColor(rgb: 0x3996ee),
|
||||
actionButtonsOutgoingTextColor: UIColor(rgb: 0x3996ee),
|
||||
actionButtonsOutgoingStrokeColor: accentColor.withMultipliedBrightnessBy(1.2),
|
||||
actionButtonsOutgoingTextColor: accentColor.withMultipliedBrightnessBy(1.2),
|
||||
selectionControlBorderColor: UIColor(rgb: 0xC7C7CC),
|
||||
selectionControlFillColor: accentColor,
|
||||
selectionControlForegroundColor: .white,
|
||||
@ -281,7 +281,7 @@ private func makeDefaultPresentationTheme(accentColor: UIColor, day: Bool) -> Pr
|
||||
)
|
||||
|
||||
let serviceMessage = PresentationThemeServiceMessage(
|
||||
components: PresentationThemeServiceMessageColor(withDefaultWallpaper: PresentationThemeServiceMessageColorComponents(fill: UIColor(rgb: 0x748391, alpha: 0.45), primaryText: .white, linkHighlight: UIColor(rgb: 0x748391, alpha: 0.25), dateFillStatic: UIColor(rgb: 0x748391, alpha: 0.45), dateFillFloating: UIColor(rgb: 0x939fab, alpha: 0.5)), withCustomWallpaper: PresentationThemeServiceMessageColorComponents(fill: UIColor(rgb: 0x000000, alpha: 0.25), primaryText: .white, linkHighlight: UIColor(rgb: 0x748391, alpha: 0.25), dateFillStatic: UIColor(rgb: 0x000000, alpha: 0.25), dateFillFloating: UIColor(rgb: 0x000000, alpha: 0.2))),
|
||||
components: PresentationThemeServiceMessageColor(withDefaultWallpaper: PresentationThemeServiceMessageColorComponents(fill: UIColor(rgb: 0x748391, alpha: 0.45), primaryText: .white, linkHighlight: UIColor(rgb: 0x748391, alpha: 0.25), dateFillStatic: UIColor(rgb: 0x748391, alpha: 0.45), dateFillFloating: UIColor(rgb: 0x939fab, alpha: 0.5)), withCustomWallpaper: PresentationThemeServiceMessageColorComponents(fill: serviceBackgroundColor, primaryText: .white, linkHighlight: UIColor(rgb: 0x748391, alpha: 0.25), dateFillStatic: serviceBackgroundColor, dateFillFloating: serviceBackgroundColor.withAlphaComponent(serviceBackgroundColor.alpha * 0.6667))),
|
||||
unreadBarFillColor: UIColor(white: 1.0, alpha: 0.9),
|
||||
unreadBarStrokeColor: UIColor(white: 0.0, alpha: 0.2),
|
||||
unreadBarTextColor: UIColor(rgb: 0x86868d),
|
||||
@ -425,10 +425,14 @@ private func makeDefaultPresentationTheme(accentColor: UIColor, day: Bool) -> Pr
|
||||
)
|
||||
}
|
||||
|
||||
public let defaultPresentationTheme = makeDefaultPresentationTheme(accentColor: UIColor(rgb: 0x007ee5), day: false)
|
||||
public let defaultPresentationTheme = makeDefaultPresentationTheme(accentColor: UIColor(rgb: 0x007ee5), serviceBackgroundColor: UIColor(rgb: 0x000000, alpha: 0.3), day: false)
|
||||
|
||||
let defaultDayAccentColor: Int32 = 0x007ee5
|
||||
|
||||
func makeDefaultPresentationTheme(serviceBackgroundColor: UIColor?) -> PresentationTheme {
|
||||
return makeDefaultPresentationTheme(accentColor: UIColor(rgb: 0x007ee5), serviceBackgroundColor: serviceBackgroundColor ?? .black, day: false)
|
||||
}
|
||||
|
||||
func makeDefaultDayPresentationTheme(accentColor: Int32?) -> PresentationTheme {
|
||||
let color: UIColor
|
||||
if let accentColor = accentColor {
|
||||
@ -436,5 +440,5 @@ func makeDefaultDayPresentationTheme(accentColor: Int32?) -> PresentationTheme {
|
||||
} else {
|
||||
color = UIColor(rgb: UInt32(bitPattern: defaultDayAccentColor))
|
||||
}
|
||||
return makeDefaultPresentationTheme(accentColor: color, day: true)
|
||||
return makeDefaultPresentationTheme(accentColor: color, serviceBackgroundColor: UIColor(rgb: 0x000000, alpha: 0.3), day: true)
|
||||
}
|
||||
|
||||
@ -732,7 +732,7 @@ class GalleryController: ViewController {
|
||||
override func loadDisplayNode() {
|
||||
let controllerInteraction = GalleryControllerInteraction(presentController: { [weak self] controller, arguments in
|
||||
if let strongSelf = self {
|
||||
strongSelf.present(controller, in: .window(.root), with: arguments)
|
||||
strongSelf.present(controller, in: .window(.root), with: arguments, blockInteraction: true)
|
||||
}
|
||||
}, dismissController: { [weak self] in
|
||||
self?.dismiss(forceAway: true)
|
||||
|
||||
@ -1390,7 +1390,7 @@ public func groupInfoController(account: Account, peerId: PeerId) -> ViewControl
|
||||
}
|
||||
|
||||
if canCreateInviteLink {
|
||||
options.append(ContactListAdditionalOption(title: presentationData.strings.GroupInfo_InviteByLink, icon: generateTintedImage(image: UIImage(bundleImageName: "Contact List/LinkActionIcon"), color: presentationData.theme.list.itemAccentColor), action: {
|
||||
options.append(ContactListAdditionalOption(title: presentationData.strings.GroupInfo_InviteByLink, icon: .generic(UIImage(bundleImageName: "Contact List/LinkActionIcon")!), action: {
|
||||
inviteByLinkImpl?()
|
||||
}))
|
||||
}
|
||||
|
||||
@ -1199,7 +1199,7 @@ final class InstantPageControllerNode: ASDisplayNode, UIScrollViewDelegate {
|
||||
}
|
||||
|
||||
if let map = media.media as? TelegramMediaMap {
|
||||
let controller = legacyLocationController(message: nil, mapMedia: map, account: self.account, modal: false, openPeer: { _ in }, sendLiveLocation: { _, _ in }, stopLiveLocation: { }, openUrl: { _ in })
|
||||
let controller = legacyLocationController(message: nil, mapMedia: map, account: self.account, isModal: false, openPeer: { _ in }, sendLiveLocation: { _, _ in }, stopLiveLocation: { }, openUrl: { _ in })
|
||||
self.pushController(controller)
|
||||
return
|
||||
}
|
||||
|
||||
@ -276,7 +276,7 @@ class InstantPageGalleryController: ViewController {
|
||||
override func loadDisplayNode() {
|
||||
let controllerInteraction = GalleryControllerInteraction(presentController: { [weak self] controller, arguments in
|
||||
if let strongSelf = self {
|
||||
strongSelf.present(controller, in: .window(.root), with: arguments)
|
||||
strongSelf.present(controller, in: .window(.root), with: arguments, blockInteraction: true)
|
||||
}
|
||||
}, dismissController: { [weak self] in
|
||||
self?.dismiss(forceAway: true)
|
||||
|
||||
@ -225,7 +225,7 @@ private func inviteContactsEntries(accountPeer: Peer?, sortedContacts: [(DeviceC
|
||||
|
||||
entries.append(.search(theme, strings))
|
||||
|
||||
entries.append(.option(0, ContactListAdditionalOption(title: strings.Contacts_ShareTelegram, icon: generateTintedImage(image: UIImage(bundleImageName: "Contact List/InviteActionIcon"), color: theme.list.itemAccentColor), action: {
|
||||
entries.append(.option(0, ContactListAdditionalOption(title: strings.Contacts_ShareTelegram, icon: .generic(UIImage(bundleImageName: "Contact List/InviteActionIcon")!), action: {
|
||||
interaction.shareTelegram()
|
||||
}), theme, strings))
|
||||
|
||||
|
||||
@ -381,7 +381,7 @@ class ItemListPeerItemNode: ItemListRevealOptionsItemNode {
|
||||
|
||||
let editingOffset: CGFloat
|
||||
if item.editing.editing {
|
||||
let sizeAndApply = editableControlLayout(48.0, item.theme, false)
|
||||
let sizeAndApply = editableControlLayout(50.0, item.theme, false)
|
||||
editableControlSizeAndApply = sizeAndApply
|
||||
editingOffset = sizeAndApply.0.width
|
||||
} else {
|
||||
@ -425,7 +425,7 @@ class ItemListPeerItemNode: ItemListRevealOptionsItemNode {
|
||||
break
|
||||
}
|
||||
}
|
||||
let contentSize = CGSize(width: params.width, height: 48.0)
|
||||
let contentSize = CGSize(width: params.width, height: 50.0)
|
||||
let separatorHeight = UIScreenPixel
|
||||
|
||||
let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets)
|
||||
@ -544,8 +544,8 @@ class ItemListPeerItemNode: ItemListRevealOptionsItemNode {
|
||||
transition.updateFrame(node: strongSelf.topStripeNode, frame: CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: layoutSize.width, height: separatorHeight)))
|
||||
transition.updateFrame(node: strongSelf.bottomStripeNode, frame: CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height + bottomStripeOffset), size: CGSize(width: layoutSize.width - bottomStripeInset, height: separatorHeight)))
|
||||
|
||||
transition.updateFrame(node: strongSelf.titleNode, frame: CGRect(origin: CGPoint(x: leftInset + revealOffset + editingOffset, y: statusAttributedString == nil ? 13.0 : 5.0), size: titleLayout.size))
|
||||
transition.updateFrame(node: strongSelf.statusNode, frame: CGRect(origin: CGPoint(x: leftInset + revealOffset + editingOffset, y: 25.0), size: statusLayout.size))
|
||||
transition.updateFrame(node: strongSelf.titleNode, frame: CGRect(origin: CGPoint(x: leftInset + revealOffset + editingOffset, y: statusAttributedString == nil ? 14.0 : 6.0), size: titleLayout.size))
|
||||
transition.updateFrame(node: strongSelf.statusNode, frame: CGRect(origin: CGPoint(x: leftInset + revealOffset + editingOffset, y: 27.0), size: statusLayout.size))
|
||||
|
||||
if let currentSwitchNode = currentSwitchNode {
|
||||
if currentSwitchNode !== strongSelf.switchNode {
|
||||
@ -608,7 +608,7 @@ class ItemListPeerItemNode: ItemListRevealOptionsItemNode {
|
||||
|
||||
transition.updateFrame(node: strongSelf.labelNode, frame: CGRect(origin: CGPoint(x: revealOffset + params.width - labelLayout.size.width - rightLabelInset - rightInset, y: floor((contentSize.height - labelLayout.size.height) / 2.0)), size: labelLayout.size))
|
||||
|
||||
transition.updateFrame(node: strongSelf.avatarNode, frame: CGRect(origin: CGPoint(x: params.leftInset + revealOffset + editingOffset + 12.0, y: 4.0), size: CGSize(width: 40.0, height: 40.0)))
|
||||
transition.updateFrame(node: strongSelf.avatarNode, frame: CGRect(origin: CGPoint(x: params.leftInset + revealOffset + editingOffset + 15.0, y: 5.0), size: CGSize(width: 40.0, height: 40.0)))
|
||||
|
||||
if item.peer.id == item.account.peerId, case .threatSelfAsSaved = item.aliasHandling {
|
||||
strongSelf.avatarNode.setPeer(account: item.account, peer: item.peer, overrideImage: .savedMessagesIcon, emptyColor: item.theme.list.mediaPlaceholderColor)
|
||||
@ -616,7 +616,7 @@ class ItemListPeerItemNode: ItemListRevealOptionsItemNode {
|
||||
strongSelf.avatarNode.setPeer(account: item.account, peer: item.peer, emptyColor: item.theme.list.mediaPlaceholderColor)
|
||||
}
|
||||
|
||||
strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: params.width, height: 48.0 + UIScreenPixel + UIScreenPixel))
|
||||
strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: params.width, height: 50.0 + UIScreenPixel + UIScreenPixel))
|
||||
|
||||
if let presence = item.presence as? TelegramUserPresence {
|
||||
strongSelf.peerPresenceManager?.reset(presence: presence)
|
||||
@ -711,7 +711,7 @@ class ItemListPeerItemNode: ItemListRevealOptionsItemNode {
|
||||
|
||||
transition.updateFrame(node: self.labelNode, frame: CGRect(origin: CGPoint(x: revealOffset + params.width - self.labelNode.bounds.size.width - rightLabelInset, y: self.labelNode.frame.minY), size: self.labelNode.bounds.size))
|
||||
|
||||
transition.updateFrame(node: self.avatarNode, frame: CGRect(origin: CGPoint(x: revealOffset + editingOffset + params.leftInset + 12.0, y: self.avatarNode.frame.minY), size: CGSize(width: 40.0, height: 40.0)))
|
||||
transition.updateFrame(node: self.avatarNode, frame: CGRect(origin: CGPoint(x: revealOffset + editingOffset + params.leftInset + 15.0, y: self.avatarNode.frame.minY), size: CGSize(width: 40.0, height: 40.0)))
|
||||
}
|
||||
|
||||
override func revealOptionsInteractivelyOpened() {
|
||||
|
||||
@ -120,7 +120,7 @@ func legacyLocationPalette(from theme: PresentationTheme) -> TGLocationPallete {
|
||||
return TGLocationPallete(backgroundColor: listTheme.plainBackgroundColor, selectionColor: listTheme.itemHighlightedBackgroundColor, separatorColor: listTheme.itemPlainSeparatorColor, textColor: listTheme.itemPrimaryTextColor, secondaryTextColor: listTheme.itemSecondaryTextColor, accentColor: listTheme.itemAccentColor, destructiveColor: listTheme.itemDestructiveColor, locationColor: UIColor(rgb: 0x008df2), liveLocationColor: UIColor(rgb: 0xff6464), iconColor: searchTheme.backgroundColor, sectionHeaderBackgroundColor: theme.chatList.sectionHeaderFillColor, sectionHeaderTextColor: theme.chatList.sectionHeaderTextColor, searchBarPallete: TGSearchBarPallete(dark: theme.overallDarkAppearance, backgroundColor: searchTheme.backgroundColor, highContrastBackgroundColor: searchTheme.backgroundColor, textColor: searchTheme.inputTextColor, placeholderColor: searchTheme.inputPlaceholderTextColor, clearIcon: generateClearIcon(color: theme.rootController.activeNavigationSearchBar.inputClearButtonColor), barBackgroundColor: searchTheme.backgroundColor, barSeparatorColor: searchTheme.separatorColor, plainBackgroundColor: searchTheme.backgroundColor, accentColor: searchTheme.accentColor, accentContrastColor: searchTheme.backgroundColor, menuBackgroundColor: searchTheme.backgroundColor, segmentedControlBackgroundImage: nil, segmentedControlSelectedImage: nil, segmentedControlHighlightedImage: nil, segmentedControlDividerImage: nil), avatarPlaceholder: nil)
|
||||
}
|
||||
|
||||
func legacyLocationController(message: Message?, mapMedia: TelegramMediaMap, account: Account, modal: Bool, openPeer: @escaping (Peer) -> Void, sendLiveLocation: @escaping (CLLocationCoordinate2D, Int32) -> Void, stopLiveLocation: @escaping () -> Void, openUrl: @escaping (String) -> Void) -> ViewController {
|
||||
func legacyLocationController(message: Message?, mapMedia: TelegramMediaMap, account: Account, isModal: Bool, openPeer: @escaping (Peer) -> Void, sendLiveLocation: @escaping (CLLocationCoordinate2D, Int32) -> Void, stopLiveLocation: @escaping () -> Void, openUrl: @escaping (String) -> Void) -> ViewController {
|
||||
let legacyLocation = TGLocationMediaAttachment()
|
||||
legacyLocation.latitude = mapMedia.latitude
|
||||
legacyLocation.longitude = mapMedia.longitude
|
||||
@ -130,7 +130,7 @@ func legacyLocationController(message: Message?, mapMedia: TelegramMediaMap, acc
|
||||
|
||||
let presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 }
|
||||
|
||||
let legacyController = LegacyController(presentation: modal ? .modal(animateIn: true) : .navigation, theme: presentationData.theme, strings: presentationData.strings)
|
||||
let legacyController = LegacyController(presentation: isModal ? .modal(animateIn: true) : .navigation, theme: presentationData.theme, strings: presentationData.strings)
|
||||
let controller: TGLocationViewController
|
||||
|
||||
if let message = message {
|
||||
@ -227,7 +227,7 @@ func legacyLocationController(message: Message?, mapMedia: TelegramMediaMap, acc
|
||||
let theme = (account.telegramApplicationContext.currentPresentationData.with { $0 }).theme
|
||||
controller.pallete = legacyLocationPalette(from: theme)
|
||||
|
||||
controller.modalMode = modal
|
||||
controller.modalMode = isModal
|
||||
controller.presentActionsMenu = { [weak legacyController] legacyLocation, directions in
|
||||
if let strongLegacyController = legacyController, let location = legacyLocation {
|
||||
let map = telegramMap(for: location)
|
||||
@ -249,7 +249,7 @@ func legacyLocationController(message: Message?, mapMedia: TelegramMediaMap, acc
|
||||
}
|
||||
}
|
||||
|
||||
if modal {
|
||||
if isModal {
|
||||
let navigationController = TGNavigationController(controllers: [controller])!
|
||||
legacyController.bind(controller: navigationController)
|
||||
controller.navigation_setDismiss({ [weak legacyController] in
|
||||
|
||||
@ -199,7 +199,7 @@ func openChatMessage(account: Account, message: Message, standalone: Bool, rever
|
||||
case let .map(mapMedia):
|
||||
dismissInput()
|
||||
|
||||
let controller = legacyLocationController(message: message, mapMedia: mapMedia, account: account, modal: modal, openPeer: { peer in
|
||||
let controller = legacyLocationController(message: message, mapMedia: mapMedia, account: account, isModal: modal, openPeer: { peer in
|
||||
openPeer(peer, .info)
|
||||
}, sendLiveLocation: { coordinate, period in
|
||||
let outMessage: EnqueueMessage = .message(text: "", attributes: [], mediaReference: .standalone(media: TelegramMediaMap(latitude: coordinate.latitude, longitude: coordinate.longitude, geoPlace: nil, venue: nil, liveBroadcastingTimeout: period)), replyToMessageId: nil, localGroupingKey: nil)
|
||||
|
||||
@ -80,7 +80,6 @@ func openResolvedUrl(_ resolvedUrl: ResolvedUrl, account: Account, context: Open
|
||||
dismissInput()
|
||||
present(LanguageLinkPreviewController(account: account, identifier: identifier), nil)
|
||||
case let .proxy(host, port, username, password, secret):
|
||||
let presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 }
|
||||
let server: ProxyServerSettings
|
||||
if let secret = secret {
|
||||
server = ProxyServerSettings(host: host, port: abs(port), connection: .mtp(secret: secret))
|
||||
@ -128,23 +127,26 @@ func openResolvedUrl(_ resolvedUrl: ResolvedUrl, account: Account, context: Open
|
||||
present(standardTextAlertController(theme: AlertControllerTheme(presentationTheme: presentationData.theme), title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), nil)
|
||||
})
|
||||
dismissInput()
|
||||
case let .share(url, text):
|
||||
let controller = PeerSelectionController(account: account)
|
||||
controller.peerSelected = { [weak controller] peerId in
|
||||
if let strongController = controller {
|
||||
strongController.dismiss()
|
||||
|
||||
let textInputState: ChatTextInputState
|
||||
if let text = text, !text.isEmpty {
|
||||
case let .share(url, text, to):
|
||||
let continueWithPeer: (PeerId) -> Void = { peerId in
|
||||
let textInputState: ChatTextInputState?
|
||||
if let text = text, !text.isEmpty {
|
||||
if let url = url, !url.isEmpty {
|
||||
let urlString = NSMutableAttributedString(string: "\(url)\n")
|
||||
let textString = NSAttributedString(string: "\(text)")
|
||||
let selectionRange: Range<Int> = urlString.length ..< (urlString.length + textString.length)
|
||||
urlString.append(textString)
|
||||
textInputState = ChatTextInputState(inputText: urlString, selectionRange: selectionRange)
|
||||
} else {
|
||||
textInputState = ChatTextInputState(inputText: NSAttributedString(string: "\(url)"))
|
||||
textInputState = ChatTextInputState(inputText: NSAttributedString(string: "\(text)"))
|
||||
}
|
||||
} else if let url = url, !url.isEmpty {
|
||||
textInputState = ChatTextInputState(inputText: NSAttributedString(string: "\(url)"))
|
||||
} else {
|
||||
textInputState = nil
|
||||
}
|
||||
|
||||
if let textInputState = textInputState {
|
||||
let _ = (account.postbox.transaction({ transaction -> Void in
|
||||
transaction.updatePeerChatInterfaceState(peerId, update: { currentState in
|
||||
if let currentState = currentState as? ChatInterfaceState {
|
||||
@ -159,9 +161,21 @@ func openResolvedUrl(_ resolvedUrl: ResolvedUrl, account: Account, context: Open
|
||||
})
|
||||
}
|
||||
}
|
||||
if let navigationController = navigationController {
|
||||
account.telegramApplicationContext.applicationBindings.dismissNativeController()
|
||||
(navigationController.viewControllers.last as? ViewController)?.present(controller, in: .window(.root), with: ViewControllerPresentationArguments(presentationAnimation: ViewControllerPresentationAnimation.modalSheet))
|
||||
|
||||
if let to = to {
|
||||
|
||||
} else {
|
||||
let controller = PeerSelectionController(account: account)
|
||||
controller.peerSelected = { [weak controller] peerId in
|
||||
if let strongController = controller {
|
||||
strongController.dismiss()
|
||||
continueWithPeer(peerId)
|
||||
}
|
||||
}
|
||||
if let navigationController = navigationController {
|
||||
account.telegramApplicationContext.applicationBindings.dismissNativeController()
|
||||
(navigationController.viewControllers.last as? ViewController)?.present(controller, in: .window(.root), with: ViewControllerPresentationArguments(presentationAnimation: ViewControllerPresentationAnimation.modalSheet))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -218,48 +218,50 @@ public func openExternalUrl(account: Account, context: OpenURLContext = .generic
|
||||
}
|
||||
|
||||
let continueHandling: () -> Void = {
|
||||
let handleRevolvedUrl: (ResolvedUrl) -> Void = { resolved in
|
||||
if case let .externalUrl(value) = resolved {
|
||||
applicationContext.applicationBindings.openUrl(value)
|
||||
} else {
|
||||
openResolvedUrl(resolved, account: account, navigationController: navigationController, openPeer: { peerId, navigation in
|
||||
switch navigation {
|
||||
case .info:
|
||||
let _ = (account.postbox.loadedPeerWithId(peerId)
|
||||
|> deliverOnMainQueue).start(next: { peer in
|
||||
if let infoController = peerInfoController(account: account, peer: peer) {
|
||||
if let navigationController = navigationController {
|
||||
navigationController.view.window?.rootViewController?.dismiss(animated: true, completion: nil)
|
||||
}
|
||||
navigationController?.pushViewController(infoController)
|
||||
}
|
||||
})
|
||||
case let .chat(_, messageId):
|
||||
if let navigationController = navigationController {
|
||||
navigationController.view.window?.rootViewController?.dismiss(animated: true, completion: nil)
|
||||
navigateToChatController(navigationController: navigationController, account: account, chatLocation: .peer(peerId), messageId: messageId)
|
||||
}
|
||||
case let .withBotStartPayload(payload):
|
||||
if let navigationController = navigationController {
|
||||
navigationController.view.window?.rootViewController?.dismiss(animated: true, completion: nil)
|
||||
navigateToChatController(navigationController: navigationController, account: account, chatLocation: .peer(peerId), botStart: payload)
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
}, present: { c, a in
|
||||
account.telegramApplicationContext.applicationBindings.dismissNativeController()
|
||||
|
||||
c.presentationArguments = a
|
||||
|
||||
account.telegramApplicationContext.applicationBindings.getWindowHost()?.present(c, on: .root, blockInteraction: false, completion: {})
|
||||
}, dismissInput: {
|
||||
dismissInput()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
let handleInternalUrl: (String) -> Void = { url in
|
||||
let _ = (resolveUrl(account: account, url: url)
|
||||
|> deliverOnMainQueue).start(next: { resolved in
|
||||
if case let .externalUrl(value) = resolved {
|
||||
applicationContext.applicationBindings.openUrl(value)
|
||||
} else {
|
||||
openResolvedUrl(resolved, account: account, navigationController: navigationController, openPeer: { peerId, navigation in
|
||||
switch navigation {
|
||||
case .info:
|
||||
let _ = (account.postbox.loadedPeerWithId(peerId)
|
||||
|> deliverOnMainQueue).start(next: { peer in
|
||||
if let infoController = peerInfoController(account: account, peer: peer) {
|
||||
if let navigationController = navigationController {
|
||||
navigationController.view.window?.rootViewController?.dismiss(animated: true, completion: nil)
|
||||
}
|
||||
navigationController?.pushViewController(infoController)
|
||||
}
|
||||
})
|
||||
case let .chat(_, messageId):
|
||||
if let navigationController = navigationController {
|
||||
navigationController.view.window?.rootViewController?.dismiss(animated: true, completion: nil)
|
||||
navigateToChatController(navigationController: navigationController, account: account, chatLocation: .peer(peerId), messageId: messageId)
|
||||
}
|
||||
case let .withBotStartPayload(payload):
|
||||
if let navigationController = navigationController {
|
||||
navigationController.view.window?.rootViewController?.dismiss(animated: true, completion: nil)
|
||||
navigateToChatController(navigationController: navigationController, account: account, chatLocation: .peer(peerId), botStart: payload)
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
}, present: { c, a in
|
||||
account.telegramApplicationContext.applicationBindings.dismissNativeController()
|
||||
|
||||
c.presentationArguments = a
|
||||
|
||||
account.telegramApplicationContext.applicationBindings.getWindowHost()?.present(c, on: .root, blockInteraction: false, completion: {})
|
||||
}, dismissInput: {
|
||||
dismissInput()
|
||||
})
|
||||
}
|
||||
})
|
||||
|> deliverOnMainQueue).start(next: handleRevolvedUrl)
|
||||
}
|
||||
|
||||
if let scheme = parsedUrl.scheme, (scheme == "tg" || scheme == applicationContext.applicationBindings.appSpecificScheme), let query = parsedUrl.query {
|
||||
@ -329,6 +331,26 @@ public func openExternalUrl(account: Account, context: OpenURLContext = .generic
|
||||
convertedUrl = "https://t.me/setlanguage/\(lang)"
|
||||
}
|
||||
}
|
||||
} else if parsedUrl.host == "msg" {
|
||||
if let components = URLComponents(string: "/?" + query) {
|
||||
var sharePhoneNumber: String?
|
||||
var shareText: String?
|
||||
if let queryItems = components.queryItems {
|
||||
for queryItem in queryItems {
|
||||
if let value = queryItem.value {
|
||||
if queryItem.name == "to" {
|
||||
sharePhoneNumber = value
|
||||
} else if queryItem.name == "text" {
|
||||
shareText = value
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if sharePhoneNumber != nil || shareText != nil {
|
||||
handleRevolvedUrl(.share(url: nil, text: shareText, to: sharePhoneNumber))
|
||||
return
|
||||
}
|
||||
}
|
||||
} else if parsedUrl.host == "msg_url" {
|
||||
if let components = URLComponents(string: "/?" + query) {
|
||||
var shareUrl: String?
|
||||
|
||||
@ -336,7 +336,7 @@ final class PeerSelectionControllerNode: ASDisplayNode {
|
||||
self.recursivelyEnsureDisplaySynchronously(true)
|
||||
contactListNode.enableUpdates = true
|
||||
} else {
|
||||
let contactListNode = ContactListNode(account: account, presentation: .natural(displaySearch: true, options: []))
|
||||
let contactListNode = ContactListNode(account: account, presentation: .single(.natural(displaySearch: true, options: [])))
|
||||
self.contactListNode = contactListNode
|
||||
contactListNode.enableUpdates = true
|
||||
contactListNode.activateSearch = { [weak self] in
|
||||
|
||||
@ -358,71 +358,75 @@ public func updatedPresentationData(postbox: Postbox, applicationBindings: Teleg
|
||||
|
||||
let contactSettings: ContactSynchronizationSettings = (view.views[preferencesKey] as! PreferencesView).values[ApplicationSpecificPreferencesKeys.contactSynchronizationSettings] as? ContactSynchronizationSettings ?? ContactSynchronizationSettings.defaultSettings
|
||||
|
||||
return applicationBindings.applicationInForeground
|
||||
|> mapToSignal({ inForeground -> Signal<PresentationData, NoError> in
|
||||
if inForeground {
|
||||
return automaticThemeShouldSwitch(themeSettings.automaticThemeSwitchSetting, currentTheme: themeSettings.theme)
|
||||
|> distinctUntilChanged
|
||||
|> map { shouldSwitch in
|
||||
let themeValue: PresentationTheme
|
||||
let effectiveTheme: PresentationThemeReference
|
||||
var effectiveChatWallpaper: TelegramWallpaper = themeSettings.chatWallpaper
|
||||
if shouldSwitch {
|
||||
effectiveTheme = .builtin(themeSettings.automaticThemeSwitchSetting.theme)
|
||||
switch effectiveChatWallpaper {
|
||||
case .builtin, .color:
|
||||
switch themeSettings.automaticThemeSwitchSetting.theme {
|
||||
case .nightAccent:
|
||||
effectiveChatWallpaper = .color(0x18222d)
|
||||
case .nightGrayscale:
|
||||
effectiveChatWallpaper = .color(0x000000)
|
||||
default:
|
||||
break
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
} else {
|
||||
effectiveTheme = themeSettings.theme
|
||||
}
|
||||
switch effectiveTheme {
|
||||
case let .builtin(reference):
|
||||
switch reference {
|
||||
case .dayClassic:
|
||||
themeValue = defaultPresentationTheme
|
||||
case .nightGrayscale:
|
||||
themeValue = defaultDarkPresentationTheme
|
||||
case .nightAccent:
|
||||
themeValue = defaultDarkAccentPresentationTheme
|
||||
case .day:
|
||||
themeValue = makeDefaultDayPresentationTheme(accentColor: themeSettings.themeAccentColor ?? defaultDayAccentColor)
|
||||
return (.single(UIColor(rgb: 0x000000, alpha: 0.3))
|
||||
|> then(chatServiceBackgroundColor(wallpaper: themeSettings.chatWallpaper, postbox: postbox)))
|
||||
|> mapToSignal { serviceBackgroundColor in
|
||||
return applicationBindings.applicationInForeground
|
||||
|> mapToSignal({ inForeground -> Signal<PresentationData, NoError> in
|
||||
if inForeground {
|
||||
return automaticThemeShouldSwitch(themeSettings.automaticThemeSwitchSetting, currentTheme: themeSettings.theme)
|
||||
|> distinctUntilChanged
|
||||
|> map { shouldSwitch in
|
||||
let themeValue: PresentationTheme
|
||||
let effectiveTheme: PresentationThemeReference
|
||||
var effectiveChatWallpaper: TelegramWallpaper = themeSettings.chatWallpaper
|
||||
if shouldSwitch {
|
||||
effectiveTheme = .builtin(themeSettings.automaticThemeSwitchSetting.theme)
|
||||
switch effectiveChatWallpaper {
|
||||
case .builtin, .color:
|
||||
switch themeSettings.automaticThemeSwitchSetting.theme {
|
||||
case .nightAccent:
|
||||
effectiveChatWallpaper = .color(0x18222d)
|
||||
case .nightGrayscale:
|
||||
effectiveChatWallpaper = .color(0x000000)
|
||||
default:
|
||||
break
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
} else {
|
||||
effectiveTheme = themeSettings.theme
|
||||
}
|
||||
switch effectiveTheme {
|
||||
case let .builtin(reference):
|
||||
switch reference {
|
||||
case .dayClassic:
|
||||
themeValue = makeDefaultPresentationTheme(serviceBackgroundColor: serviceBackgroundColor)
|
||||
case .nightGrayscale:
|
||||
themeValue = defaultDarkPresentationTheme
|
||||
case .nightAccent:
|
||||
themeValue = defaultDarkAccentPresentationTheme
|
||||
case .day:
|
||||
themeValue = makeDefaultDayPresentationTheme(accentColor: themeSettings.themeAccentColor ?? defaultDayAccentColor)
|
||||
}
|
||||
}
|
||||
|
||||
let localizationSettings: LocalizationSettings?
|
||||
if let current = (view.views[preferencesKey] as! PreferencesView).values[PreferencesKeys.localizationSettings] as? LocalizationSettings {
|
||||
localizationSettings = current
|
||||
} else {
|
||||
localizationSettings = nil
|
||||
}
|
||||
|
||||
let stringsValue: PresentationStrings
|
||||
if let localizationSettings = localizationSettings {
|
||||
stringsValue = PresentationStrings(primaryComponent: PresentationStringsComponent(languageCode: localizationSettings.primaryComponent.languageCode, localizedName: localizationSettings.primaryComponent.localizedName, pluralizationRulesCode: localizationSettings.primaryComponent.customPluralizationCode, dict: dictFromLocalization(localizationSettings.primaryComponent.localization)), secondaryComponent: localizationSettings.secondaryComponent.flatMap({ PresentationStringsComponent(languageCode: $0.languageCode, localizedName: $0.localizedName, pluralizationRulesCode: $0.customPluralizationCode, dict: dictFromLocalization($0.localization)) }))
|
||||
} else {
|
||||
stringsValue = defaultPresentationStrings
|
||||
}
|
||||
|
||||
let dateTimeFormat = currentDateTimeFormat()
|
||||
let nameDisplayOrder = contactSettings.nameDisplayOrder
|
||||
let nameSortOrder = currentPersonNameSortOrder()
|
||||
|
||||
return PresentationData(strings: stringsValue, theme: themeValue, chatWallpaper: effectiveChatWallpaper, volumeControlStatusBarIcons: volumeControlStatusBarIcons(), fontSize: themeSettings.fontSize, dateTimeFormat: dateTimeFormat, nameDisplayOrder: nameDisplayOrder, nameSortOrder: nameSortOrder, disableAnimations: themeSettings.disableAnimations)
|
||||
}
|
||||
|
||||
let localizationSettings: LocalizationSettings?
|
||||
if let current = (view.views[preferencesKey] as! PreferencesView).values[PreferencesKeys.localizationSettings] as? LocalizationSettings {
|
||||
localizationSettings = current
|
||||
} else {
|
||||
localizationSettings = nil
|
||||
}
|
||||
|
||||
let stringsValue: PresentationStrings
|
||||
if let localizationSettings = localizationSettings {
|
||||
stringsValue = PresentationStrings(primaryComponent: PresentationStringsComponent(languageCode: localizationSettings.primaryComponent.languageCode, localizedName: localizationSettings.primaryComponent.localizedName, pluralizationRulesCode: localizationSettings.primaryComponent.customPluralizationCode, dict: dictFromLocalization(localizationSettings.primaryComponent.localization)), secondaryComponent: localizationSettings.secondaryComponent.flatMap({ PresentationStringsComponent(languageCode: $0.languageCode, localizedName: $0.localizedName, pluralizationRulesCode: $0.customPluralizationCode, dict: dictFromLocalization($0.localization)) }))
|
||||
} else {
|
||||
stringsValue = defaultPresentationStrings
|
||||
}
|
||||
|
||||
let dateTimeFormat = currentDateTimeFormat()
|
||||
let nameDisplayOrder = contactSettings.nameDisplayOrder
|
||||
let nameSortOrder = currentPersonNameSortOrder()
|
||||
|
||||
return PresentationData(strings: stringsValue, theme: themeValue, chatWallpaper: effectiveChatWallpaper, volumeControlStatusBarIcons: volumeControlStatusBarIcons(), fontSize: themeSettings.fontSize, dateTimeFormat: dateTimeFormat, nameDisplayOrder: nameDisplayOrder, nameSortOrder: nameSortOrder, disableAnimations: themeSettings.disableAnimations)
|
||||
} else {
|
||||
return .complete()
|
||||
}
|
||||
} else {
|
||||
return .complete()
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -222,6 +222,18 @@ public final class PrincipalThemeAdditionalGraphics {
|
||||
public let chatBubbleActionButtonOutgoingBottomRightImage: UIImage
|
||||
public let chatBubbleActionButtonOutgoingBottomSingleImage: UIImage
|
||||
|
||||
public let chatBubbleActionButtonIncomingMessageIconImage: UIImage
|
||||
public let chatBubbleActionButtonIncomingLinkIconImage: UIImage
|
||||
public let chatBubbleActionButtonIncomingShareIconImage: UIImage
|
||||
public let chatBubbleActionButtonIncomingPhoneIconImage: UIImage
|
||||
public let chatBubbleActionButtonIncomingLocationIconImage: UIImage
|
||||
|
||||
public let chatBubbleActionButtonOutgoingMessageIconImage: UIImage
|
||||
public let chatBubbleActionButtonOutgoingLinkIconImage: UIImage
|
||||
public let chatBubbleActionButtonOutgoingShareIconImage: UIImage
|
||||
public let chatBubbleActionButtonOutgoingPhoneIconImage: UIImage
|
||||
public let chatBubbleActionButtonOutgoingLocationIconImage: UIImage
|
||||
|
||||
init(_ theme: PresentationThemeChat, wallpaper: TelegramWallpaper) {
|
||||
let serviceColor = serviceMessageColorComponents(chatTheme: theme, wallpaper: wallpaper)
|
||||
self.chatServiceBubbleFillImage = generateImage(CGSize(width: 20.0, height: 20.0), contextGenerator: { size, context -> Void in
|
||||
@ -243,5 +255,15 @@ public final class PrincipalThemeAdditionalGraphics {
|
||||
self.chatBubbleActionButtonOutgoingBottomLeftImage = messageBubbleActionButtonImage(color: bubbleVariableColor(variableColor: theme.bubble.actionButtonsOutgoingFillColor, wallpaper: wallpaper), strokeColor: theme.bubble.actionButtonsOutgoingStrokeColor, position: .bottomLeft)
|
||||
self.chatBubbleActionButtonOutgoingBottomRightImage = messageBubbleActionButtonImage(color: bubbleVariableColor(variableColor: theme.bubble.actionButtonsOutgoingFillColor, wallpaper: wallpaper), strokeColor: theme.bubble.actionButtonsOutgoingStrokeColor, position: .bottomRight)
|
||||
self.chatBubbleActionButtonOutgoingBottomSingleImage = messageBubbleActionButtonImage(color: bubbleVariableColor(variableColor: theme.bubble.actionButtonsOutgoingFillColor, wallpaper: wallpaper), strokeColor: theme.bubble.actionButtonsOutgoingStrokeColor, position: .bottomSingle)
|
||||
self.chatBubbleActionButtonIncomingMessageIconImage = generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/BotMessage"), color: theme.bubble.actionButtonsIncomingTextColor)!
|
||||
self.chatBubbleActionButtonIncomingLinkIconImage = generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/BotLink"), color: theme.bubble.actionButtonsIncomingTextColor)!
|
||||
self.chatBubbleActionButtonIncomingShareIconImage = generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/BotShare"), color: theme.bubble.actionButtonsIncomingTextColor)!
|
||||
self.chatBubbleActionButtonIncomingPhoneIconImage = generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/BotPhone"), color: theme.bubble.actionButtonsIncomingTextColor)!
|
||||
self.chatBubbleActionButtonIncomingLocationIconImage = generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/BotLocation"), color: theme.bubble.actionButtonsIncomingTextColor)!
|
||||
self.chatBubbleActionButtonOutgoingMessageIconImage = generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/BotMessage"), color: theme.bubble.actionButtonsOutgoingTextColor)!
|
||||
self.chatBubbleActionButtonOutgoingLinkIconImage = generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/BotLink"), color: theme.bubble.actionButtonsOutgoingTextColor)!
|
||||
self.chatBubbleActionButtonOutgoingShareIconImage = generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/BotShare"), color: theme.bubble.actionButtonsOutgoingTextColor)!
|
||||
self.chatBubbleActionButtonOutgoingPhoneIconImage = generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/BotPhone"), color: theme.bubble.actionButtonsOutgoingTextColor)!
|
||||
self.chatBubbleActionButtonOutgoingLocationIconImage = generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/BotLocation"), color: theme.bubble.actionButtonsOutgoingTextColor)!
|
||||
}
|
||||
}
|
||||
|
||||
@ -4,14 +4,21 @@ import AsyncDisplayKit
|
||||
import LegacyComponents
|
||||
import SwiftSignalKit
|
||||
|
||||
private extension CAShapeLayer {
|
||||
func animateStrokeStart(from: CGFloat, to: CGFloat, duration: Double, delay: Double = 0.0, timingFunction: String = kCAMediaTimingFunctionEaseInEaseOut, removeOnCompletion: Bool = true, completion: ((Bool) -> ())? = nil) {
|
||||
self.animate(from: NSNumber(value: Float(from)), to: NSNumber(value: Float(to)), keyPath: "strokeStart", timingFunction: timingFunction, duration: duration, delay: delay, removeOnCompletion: removeOnCompletion, completion: completion)
|
||||
}
|
||||
|
||||
func animateStrokeEnd(from: CGFloat, to: CGFloat, duration: Double, delay: Double = 0.0, timingFunction: String = kCAMediaTimingFunctionEaseInEaseOut, removeOnCompletion: Bool = true, completion: ((Bool) -> ())? = nil) {
|
||||
self.animate(from: NSNumber(value: Float(from)), to: NSNumber(value: Float(to)), keyPath: "strokeEnd", timingFunction: timingFunction, duration: duration, delay: delay, removeOnCompletion: removeOnCompletion, completion: completion)
|
||||
}
|
||||
}
|
||||
|
||||
final class RadialDownloadContentNode: RadialStatusContentNode {
|
||||
var color: UIColor {
|
||||
didSet {
|
||||
self.leftLine.fillColor = UIColor.clear.cgColor
|
||||
self.leftLine.strokeColor = self.color.cgColor
|
||||
self.rightLine.fillColor = UIColor.clear.cgColor
|
||||
self.rightLine.strokeColor = self.color.cgColor
|
||||
self.arrowBody.fillColor = UIColor.clear.cgColor
|
||||
self.arrowBody.strokeColor = self.color.cgColor
|
||||
self.setNeedsDisplay()
|
||||
}
|
||||
@ -69,7 +76,7 @@ final class RadialDownloadContentNode: RadialStatusContentNode {
|
||||
}
|
||||
}
|
||||
|
||||
private func svgPath(_ path: StaticString, scale: CGFloat = 1.0, offset: CGPoint = CGPoint()) throws -> UIBezierPath {
|
||||
private func svgPath(_ path: StaticString, scale: CGPoint = CGPoint(x: 1.0, y: 1.0), offset: CGPoint = CGPoint()) throws -> UIBezierPath {
|
||||
var index: UnsafePointer<UInt8> = path.utf8Start
|
||||
let end = path.utf8Start.advanced(by: path.utf8CodeUnitCount)
|
||||
let path = UIBezierPath()
|
||||
@ -78,22 +85,22 @@ final class RadialDownloadContentNode: RadialStatusContentNode {
|
||||
index = index.successor()
|
||||
|
||||
if c == 77 { // M
|
||||
let x = try readCGFloat(&index, end: end, separator: 44) * scale + offset.x
|
||||
let y = try readCGFloat(&index, end: end, separator: 32) * scale + offset.y
|
||||
let x = try readCGFloat(&index, end: end, separator: 44) * scale.x + offset.x
|
||||
let y = try readCGFloat(&index, end: end, separator: 32) * scale.y + offset.y
|
||||
|
||||
path.move(to: CGPoint(x: x, y: y))
|
||||
} else if c == 76 { // L
|
||||
let x = try readCGFloat(&index, end: end, separator: 44) * scale + offset.x
|
||||
let y = try readCGFloat(&index, end: end, separator: 32) * scale + offset.y
|
||||
let x = try readCGFloat(&index, end: end, separator: 44) * scale.x + offset.x
|
||||
let y = try readCGFloat(&index, end: end, separator: 32) * scale.y + offset.y
|
||||
|
||||
path.addLine(to: CGPoint(x: x, y: y))
|
||||
} else if c == 67 { // C
|
||||
let x1 = try readCGFloat(&index, end: end, separator: 44) * scale + offset.x
|
||||
let y1 = try readCGFloat(&index, end: end, separator: 32) * scale + offset.y
|
||||
let x2 = try readCGFloat(&index, end: end, separator: 44) * scale + offset.x
|
||||
let y2 = try readCGFloat(&index, end: end, separator: 32) * scale + offset.y
|
||||
let x = try readCGFloat(&index, end: end, separator: 44) * scale + offset.x
|
||||
let y = try readCGFloat(&index, end: end, separator: 32) * scale + offset.y
|
||||
let x1 = try readCGFloat(&index, end: end, separator: 44) * scale.x + offset.x
|
||||
let y1 = try readCGFloat(&index, end: end, separator: 32) * scale.y + offset.y
|
||||
let x2 = try readCGFloat(&index, end: end, separator: 44) * scale.x + offset.x
|
||||
let y2 = try readCGFloat(&index, end: end, separator: 32) * scale.y + offset.y
|
||||
let x = try readCGFloat(&index, end: end, separator: 44) * scale.x + offset.x
|
||||
let y = try readCGFloat(&index, end: end, separator: 32) * scale.y + offset.y
|
||||
path.addCurve(to: CGPoint(x: x, y: y), controlPoint1: CGPoint(x: x1, y: y1), controlPoint2: CGPoint(x: x2, y: y2))
|
||||
} else if c == 32 { // space
|
||||
continue
|
||||
@ -107,6 +114,7 @@ final class RadialDownloadContentNode: RadialStatusContentNode {
|
||||
|
||||
let bounds = self.bounds
|
||||
let diameter = min(bounds.size.width, bounds.size.height)
|
||||
let factor = diameter / 50.0
|
||||
|
||||
var lineWidth: CGFloat = 2.0
|
||||
if diameter < 24.0 {
|
||||
@ -117,20 +125,10 @@ final class RadialDownloadContentNode: RadialStatusContentNode {
|
||||
self.rightLine.lineWidth = lineWidth
|
||||
self.arrowBody.lineWidth = lineWidth
|
||||
|
||||
let factor = diameter / 50.0
|
||||
|
||||
let arrowHeadSize: CGFloat = 15.0 * factor
|
||||
let arrowLength: CGFloat = 18.0 * factor
|
||||
let arrowHeadOffset: CGFloat = 1.0 * factor
|
||||
|
||||
var bodyPath = UIBezierPath()
|
||||
if let path = try? svgPath("M1.20125335,62.2095675 C1.78718228,62.9863141 2.3877868,63.7395876 3.00158591,64.4690754 C22.1087455,87.1775489 54.0019347,86.8368674 54.0066002,54.0178571 L54.0066002,0.625 ", scale: 0.333333 * factor, offset: CGPoint(x: 7.0 * factor, y: (17.0 - UIScreenPixel) * factor)) {
|
||||
bodyPath = path
|
||||
}
|
||||
|
||||
self.arrowBody.path = bodyPath.cgPath
|
||||
self.arrowBody.strokeStart = 0.62
|
||||
|
||||
let leftPath = UIBezierPath()
|
||||
leftPath.move(to: CGPoint(x: diameter / 2.0, y: diameter / 2.0 + arrowLength / 2.0 + arrowHeadOffset))
|
||||
leftPath.addLine(to: CGPoint(x: diameter / 2.0 - arrowHeadSize / 2.0, y: diameter / 2.0 + arrowLength / 2.0 - arrowHeadSize / 2.0 + arrowHeadOffset))
|
||||
@ -145,6 +143,18 @@ final class RadialDownloadContentNode: RadialStatusContentNode {
|
||||
private let duration: Double = 0.2
|
||||
|
||||
override func prepareAnimateOut(completion: @escaping () -> Void) {
|
||||
let bounds = self.bounds
|
||||
let diameter = min(bounds.size.width, bounds.size.height)
|
||||
let factor = diameter / 50.0
|
||||
|
||||
var bodyPath = UIBezierPath()
|
||||
if let path = try? svgPath("M1.20125335,62.2095675 C1.78718228,62.9863141 2.3877868,63.7395876 3.00158591,64.4690754 C22.1087455,87.1775489 54.0019347,86.8368674 54.0066002,54.0178571 L54.0066002,0.625 ", scale: CGPoint(x: 0.333333 * factor, y: 0.333333 * factor), offset: CGPoint(x: 7.0 * factor, y: (17.0 - UIScreenPixel) * factor)) {
|
||||
bodyPath = path
|
||||
}
|
||||
|
||||
self.arrowBody.path = bodyPath.cgPath
|
||||
self.arrowBody.strokeStart = 0.62
|
||||
|
||||
self.leftLine.animateStrokeEnd(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false)
|
||||
self.rightLine.animateStrokeEnd(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false)
|
||||
|
||||
@ -162,16 +172,23 @@ final class RadialDownloadContentNode: RadialStatusContentNode {
|
||||
self.arrowBody.animateAlpha(from: 1.0, to: 0.0, duration: 0.1, delay: 0.4, removeOnCompletion: false)
|
||||
}
|
||||
|
||||
override func prepareAnimateIn(from: RadialStatusNodeState?) {
|
||||
let bounds = self.bounds
|
||||
let diameter = min(bounds.size.width, bounds.size.height)
|
||||
let factor = diameter / 50.0
|
||||
|
||||
var bodyPath = UIBezierPath()
|
||||
if let path = try? svgPath("M1.20125335,62.2095675 C1.78718228,62.9863141 2.3877868,63.7395876 3.00158591,64.4690754 C22.1087455,87.1775489 54.0019347,86.8368674 54.0066002,54.0178571 L54.0066002,0.625 ", scale: CGPoint(x: -0.333333 * factor, y: 0.333333 * factor), offset: CGPoint(x: 43.0 * factor, y: (17.0 - UIScreenPixel) * factor)) {
|
||||
bodyPath = path
|
||||
}
|
||||
|
||||
self.arrowBody.path = bodyPath.cgPath
|
||||
self.arrowBody.strokeStart = 0.62
|
||||
}
|
||||
|
||||
override func animateIn(from: RadialStatusNodeState) {
|
||||
if case .progress = from {
|
||||
var transform = CATransform3DMakeScale(-1.0, 1.0, 1.0)
|
||||
transform = CATransform3DTranslate(transform, -50.0, 0.0, 0.0)
|
||||
self.arrowBody.transform = transform
|
||||
self.arrowBody.animateStrokeStart(from: 0.0, to: 0.62, duration: 0.5, removeOnCompletion: false, completion: { [weak self] _ in
|
||||
UIView.performWithoutAnimation {
|
||||
self?.arrowBody.transform = CATransform3DIdentity
|
||||
}
|
||||
})
|
||||
self.arrowBody.animateStrokeStart(from: 0.0, to: 0.62, duration: 0.5, removeOnCompletion: false, completion: nil)
|
||||
self.arrowBody.animateStrokeEnd(from: 0.0, to: 1.0, duration: 0.5, removeOnCompletion: false, completion: nil)
|
||||
|
||||
self.leftLine.animateStrokeEnd(from: 0.0, to: 1.0, duration: 0.2, delay: 0.3, removeOnCompletion: false)
|
||||
@ -185,13 +202,3 @@ final class RadialDownloadContentNode: RadialStatusContentNode {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension CAShapeLayer {
|
||||
func animateStrokeStart(from: CGFloat, to: CGFloat, duration: Double, delay: Double = 0.0, timingFunction: String = kCAMediaTimingFunctionEaseInEaseOut, removeOnCompletion: Bool = true, completion: ((Bool) -> ())? = nil) {
|
||||
self.animate(from: NSNumber(value: Float(from)), to: NSNumber(value: Float(to)), keyPath: "strokeStart", timingFunction: timingFunction, duration: duration, delay: delay, removeOnCompletion: removeOnCompletion, completion: completion)
|
||||
}
|
||||
|
||||
func animateStrokeEnd(from: CGFloat, to: CGFloat, duration: Double, delay: Double = 0.0, timingFunction: String = kCAMediaTimingFunctionEaseInEaseOut, removeOnCompletion: Bool = true, completion: ((Bool) -> ())? = nil) {
|
||||
self.animate(from: NSNumber(value: Float(from)), to: NSNumber(value: Float(to)), keyPath: "strokeEnd", timingFunction: timingFunction, duration: duration, delay: delay, removeOnCompletion: removeOnCompletion, completion: completion)
|
||||
}
|
||||
}
|
||||
|
||||
@ -301,7 +301,7 @@ final class RadialProgressContentNode: RadialStatusContentNode {
|
||||
self.cancelNode.layer.animateRotation(from: 0.0, to: CGFloat.pi / 3.0, duration: duration)
|
||||
}
|
||||
|
||||
override func prepareAnimateIn() {
|
||||
override func prepareAnimateIn(from: RadialStatusNodeState?) {
|
||||
self.ready = true
|
||||
self.spinnerNode.progress = self.progress
|
||||
}
|
||||
|
||||
@ -20,7 +20,7 @@ class RadialStatusContentNode: ASDisplayNode {
|
||||
self.layer.animateScale(from: 1.0, to: 0.2, duration: duration, removeOnCompletion: false)
|
||||
}
|
||||
|
||||
func prepareAnimateIn() {
|
||||
func prepareAnimateIn(from: RadialStatusNodeState?) {
|
||||
}
|
||||
|
||||
func animateIn(from: RadialStatusNodeState) {
|
||||
|
||||
@ -127,7 +127,7 @@ public enum RadialStatusNodeState: Equatable {
|
||||
public final class RadialStatusNode: ASControlNode {
|
||||
var backgroundNodeColor: UIColor {
|
||||
didSet {
|
||||
self.transitionToBackgroundColor(state.backgroundColor(color: self.backgroundNodeColor), animated: false, completion: {})
|
||||
self.transitionToBackgroundColor(state.backgroundColor(color: self.backgroundNodeColor), previousContentNode: nil, animated: false, completion: {})
|
||||
}
|
||||
}
|
||||
|
||||
@ -152,7 +152,7 @@ public final class RadialStatusNode: ASControlNode {
|
||||
if contentNode !== self.contentNode {
|
||||
self.transitionToContentNode(contentNode, state: state, fromState: fromState, backgroundColor: state.backgroundColor(color: self.backgroundNodeColor), animated: animated, completion: completion)
|
||||
} else {
|
||||
self.transitionToBackgroundColor(state.backgroundColor(color: self.backgroundNodeColor), animated: animated, completion: completion)
|
||||
self.transitionToBackgroundColor(state.backgroundColor(color: self.backgroundNodeColor), previousContentNode: nil, animated: animated, completion: completion)
|
||||
}
|
||||
} else {
|
||||
completion()
|
||||
@ -177,13 +177,13 @@ public final class RadialStatusNode: ASControlNode {
|
||||
if let contentNode = strongSelf.contentNode {
|
||||
strongSelf.addSubnode(contentNode)
|
||||
contentNode.frame = strongSelf.bounds
|
||||
contentNode.prepareAnimateIn()
|
||||
contentNode.prepareAnimateIn(from: fromState)
|
||||
if strongSelf.isNodeLoaded {
|
||||
contentNode.layout()
|
||||
contentNode.animateIn(from: fromState)
|
||||
}
|
||||
}
|
||||
strongSelf.transitionToBackgroundColor(backgroundColor, animated: animated, completion: completion)
|
||||
strongSelf.transitionToBackgroundColor(backgroundColor, previousContentNode: previousContentNode, animated: animated, completion: completion)
|
||||
})
|
||||
} else {
|
||||
previousContentNode.removeFromSupernode()
|
||||
@ -195,7 +195,7 @@ public final class RadialStatusNode: ASControlNode {
|
||||
contentNode.layout()
|
||||
}
|
||||
}
|
||||
strongSelf.transitionToBackgroundColor(backgroundColor, animated: animated, completion: completion)
|
||||
strongSelf.transitionToBackgroundColor(backgroundColor, previousContentNode: nil, animated: animated, completion: completion)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -203,14 +203,14 @@ public final class RadialStatusNode: ASControlNode {
|
||||
self.contentNode = node
|
||||
if let contentNode = self.contentNode {
|
||||
contentNode.frame = self.bounds
|
||||
contentNode.prepareAnimateIn()
|
||||
contentNode.prepareAnimateIn(from: nil)
|
||||
self.addSubnode(contentNode)
|
||||
}
|
||||
self.transitionToBackgroundColor(backgroundColor, animated: animated, completion: completion)
|
||||
self.transitionToBackgroundColor(backgroundColor, previousContentNode: nil, animated: animated, completion: completion)
|
||||
}
|
||||
}
|
||||
|
||||
private func transitionToBackgroundColor(_ color: UIColor?, animated: Bool, completion: @escaping () -> Void) {
|
||||
private func transitionToBackgroundColor(_ color: UIColor?, previousContentNode: RadialStatusContentNode?, animated: Bool, completion: @escaping () -> Void) {
|
||||
let currentColor = self.backgroundNode?.color
|
||||
|
||||
var updated = false
|
||||
@ -235,7 +235,9 @@ public final class RadialStatusNode: ASControlNode {
|
||||
} else if let backgroundNode = self.backgroundNode {
|
||||
self.backgroundNode = nil
|
||||
if animated {
|
||||
backgroundNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false, completion: { [weak backgroundNode] _ in
|
||||
backgroundNode.layer.animateScale(from: 1.0, to: 0.01, duration: 0.2, removeOnCompletion: false)
|
||||
previousContentNode?.layer.animateScale(from: 1.0, to: 0.01, duration: 0.2, removeOnCompletion: false)
|
||||
backgroundNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak backgroundNode] _ in
|
||||
backgroundNode?.removeFromSupernode()
|
||||
completion()
|
||||
})
|
||||
|
||||
@ -208,7 +208,7 @@ public final class SecretMediaPreviewController: ViewController {
|
||||
public override func loadDisplayNode() {
|
||||
let controllerInteraction = GalleryControllerInteraction(presentController: { [weak self] controller, arguments in
|
||||
if let strongSelf = self {
|
||||
strongSelf.present(controller, in: .window(.root), with: arguments)
|
||||
strongSelf.present(controller, in: .window(.root), with: arguments, blockInteraction: true)
|
||||
}
|
||||
}, dismissController: { [weak self] in
|
||||
self?.dismiss(forceAway: true)
|
||||
|
||||
@ -170,7 +170,7 @@ class SecureIdDocumentGalleryController: ViewController {
|
||||
override func loadDisplayNode() {
|
||||
let controllerInteraction = GalleryControllerInteraction(presentController: { [weak self] controller, arguments in
|
||||
if let strongSelf = self {
|
||||
strongSelf.present(controller, in: .window(.root), with: arguments)
|
||||
strongSelf.present(controller, in: .window(.root), with: arguments, blockInteraction: true)
|
||||
}
|
||||
}, dismissController: { [weak self] in
|
||||
self?.dismiss(forceAway: true)
|
||||
|
||||
@ -251,7 +251,7 @@ private func stringForCategory(strings: PresentationStrings, category: PeerCache
|
||||
}
|
||||
}
|
||||
|
||||
func storageUsageController(account: Account) -> ViewController {
|
||||
func storageUsageController(account: Account, isModal: Bool = false) -> ViewController {
|
||||
let cacheSettingsPromise = Promise<CacheStorageSettings>()
|
||||
cacheSettingsPromise.set(account.postbox.preferencesView(keys: [PreferencesKeys.cacheStorageSettings])
|
||||
|> map { view -> CacheStorageSettings in
|
||||
@ -672,10 +672,15 @@ func storageUsageController(account: Account) -> ViewController {
|
||||
})
|
||||
})
|
||||
|
||||
var dismissImpl: (() -> Void)?
|
||||
|
||||
let signal = combineLatest((account.applicationContext as! TelegramApplicationContext).presentationData, cacheSettingsPromise.get(), statsPromise.get()) |> deliverOnMainQueue
|
||||
|> map { presentationData, cacheSettings, cacheStats -> (ItemListControllerState, (ItemListNodeState<StorageUsageEntry>, StorageUsageEntry.ItemGenerationArguments)) in
|
||||
let leftNavigationButton = isModal ? ItemListNavigationButton(content: .text(presentationData.strings.Common_Cancel), style: .regular, enabled: true, action: {
|
||||
dismissImpl?()
|
||||
}) : nil
|
||||
|
||||
let controllerState = ItemListControllerState(theme: presentationData.theme, title: .text(presentationData.strings.Cache_Title), leftNavigationButton: nil, rightNavigationButton: nil, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: false)
|
||||
let controllerState = ItemListControllerState(theme: presentationData.theme, title: .text(presentationData.strings.Cache_Title), leftNavigationButton: leftNavigationButton, rightNavigationButton: nil, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: false)
|
||||
let listState = ItemListNodeState(entries: storageUsageControllerEntries(presentationData: presentationData, cacheSettings: cacheSettings, cacheStats: cacheStats), style: .blocks, emptyStateItem: nil, animateChanges: false)
|
||||
|
||||
return (controllerState, (listState, arguments))
|
||||
@ -687,6 +692,8 @@ func storageUsageController(account: Account) -> ViewController {
|
||||
presentControllerImpl = { [weak controller] c in
|
||||
controller?.present(c, in: .window(.root), with: ViewControllerPresentationArguments(presentationAnimation: .modalSheet))
|
||||
}
|
||||
|
||||
dismissImpl = { [weak controller] in
|
||||
controller?.dismiss()
|
||||
}
|
||||
return controller
|
||||
}
|
||||
|
||||
@ -71,7 +71,7 @@ class ThemeGalleryController: ViewController {
|
||||
|
||||
super.init(navigationBarPresentationData: NavigationBarPresentationData(presentationData: presentationData))
|
||||
|
||||
self.title = "Chat Preview"
|
||||
self.title = self.presentationData.strings.Wallpaper_Title
|
||||
self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBar.style.style
|
||||
|
||||
let initialEntries: [ThemeGalleryEntry] = wallpapers.map { ThemeGalleryEntry.wallpaper($0) }
|
||||
@ -152,7 +152,7 @@ class ThemeGalleryController: ViewController {
|
||||
override func loadDisplayNode() {
|
||||
let controllerInteraction = GalleryControllerInteraction(presentController: { [weak self] controller, arguments in
|
||||
if let strongSelf = self {
|
||||
strongSelf.present(controller, in: .window(.root), with: arguments)
|
||||
strongSelf.present(controller, in: .window(.root), with: arguments, blockInteraction: true)
|
||||
}
|
||||
}, dismissController: { [weak self] in
|
||||
self?.dismiss(forceAway: true)
|
||||
|
||||
@ -33,12 +33,8 @@ final class ThemeGridController: ViewController {
|
||||
|
||||
super.init(navigationBarPresentationData: NavigationBarPresentationData(presentationData: self.presentationData))
|
||||
|
||||
switch mode {
|
||||
case .wallpapers:
|
||||
self.title = self.presentationData.strings.Wallpaper_Title
|
||||
case .solidColors:
|
||||
self.title = "Solid Colors"
|
||||
}
|
||||
self.title = self.presentationData.strings.Wallpaper_Title
|
||||
|
||||
self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBar.style.style
|
||||
|
||||
self.scrollToTop = { [weak self] in
|
||||
@ -69,12 +65,7 @@ final class ThemeGridController: ViewController {
|
||||
}
|
||||
|
||||
private func updateThemeAndStrings() {
|
||||
switch mode {
|
||||
case .wallpapers:
|
||||
self.title = self.presentationData.strings.Wallpaper_Title
|
||||
case .solidColors:
|
||||
self.title = "Solid Colors"
|
||||
}
|
||||
self.title = self.presentationData.strings.Wallpaper_Title
|
||||
|
||||
self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBar.style.style
|
||||
self.navigationBar?.updatePresentationData(NavigationBarPresentationData(presentationData: self.presentationData))
|
||||
|
||||
@ -19,7 +19,7 @@ enum ParsedInternalUrl {
|
||||
case internalInstantView(url: String)
|
||||
case confirmationCode(Int)
|
||||
case cancelAccountReset(phone: String, hash: String)
|
||||
case share(url: String, text: String?)
|
||||
case share(url: String?, text: String?, to: String?)
|
||||
}
|
||||
|
||||
private enum ParsedUrl {
|
||||
@ -40,7 +40,7 @@ enum ResolvedUrl {
|
||||
case localization(String)
|
||||
case confirmationCode(Int)
|
||||
case cancelAccountReset(phone: String, hash: String)
|
||||
case share(url: String, text: String?)
|
||||
case share(url: String?, text: String?, to: String?)
|
||||
}
|
||||
|
||||
func parseInternalUrl(query: String) -> ParsedInternalUrl? {
|
||||
@ -163,7 +163,7 @@ func parseInternalUrl(query: String) -> ParsedInternalUrl? {
|
||||
}
|
||||
|
||||
if let url = url {
|
||||
return .share(url: url, text: text)
|
||||
return .share(url: url, text: text, to: nil)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
@ -231,8 +231,8 @@ private func resolveInternalUrl(account: Account, url: ParsedInternalUrl) -> Sig
|
||||
return .single(.confirmationCode(code))
|
||||
case let .cancelAccountReset(phone, hash):
|
||||
return .single(.cancelAccountReset(phone: phone, hash: hash))
|
||||
case let .share(url, text):
|
||||
return .single(.share(url: url, text: text))
|
||||
case let .share(url, text, to):
|
||||
return .single(.share(url: url, text: text, to: to))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -213,7 +213,7 @@ class WebSearchGalleryController: ViewController {
|
||||
override func loadDisplayNode() {
|
||||
let controllerInteraction = GalleryControllerInteraction(presentController: { [weak self] controller, arguments in
|
||||
if let strongSelf = self {
|
||||
strongSelf.present(controller, in: .window(.root), with: arguments)
|
||||
strongSelf.present(controller, in: .window(.root), with: arguments, blockInteraction: true)
|
||||
}
|
||||
}, dismissController: { [weak self] in
|
||||
self?.dismiss(forceAway: true)
|
||||
|
||||