Support of channel/group ownership transfer

People Nearby initial implementation
Ghost Icons for deleted users
This commit is contained in:
Ilya Laktyushin 2019-06-08 00:10:27 +02:00
parent 339bc3c472
commit bc96107da1
41 changed files with 4650 additions and 3564 deletions

View File

@ -0,0 +1,22 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"filename" : "Ghost@2x.png",
"scale" : "2x"
},
{
"idiom" : "universal",
"filename" : "Ghost@3x.png",
"scale" : "3x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@ -0,0 +1,22 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"filename" : "locationcontacts@2x.png",
"scale" : "2x"
},
{
"idiom" : "universal",
"filename" : "locationcontacts@3x.png",
"scale" : "3x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 619 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 736 B

View File

@ -16,6 +16,11 @@
090E63E62195880F00E3C035 /* ContactAddItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 090E63E52195880F00E3C035 /* ContactAddItem.swift */; };
090E63EE2196FE3A00E3C035 /* OpenAddContact.swift in Sources */ = {isa = PBXBuildFile; fileRef = 090E63ED2196FE3A00E3C035 /* OpenAddContact.swift */; };
090E777922A6A32E00CD99F5 /* ThemeSettingsThemeItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 090E777822A6A32E00CD99F5 /* ThemeSettingsThemeItem.swift */; };
090E778622A9B95A00CD99F5 /* PeopleNearbyController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 090E778522A9B95A00CD99F5 /* PeopleNearbyController.swift */; };
090E778822A9B96100CD99F5 /* PeopleNearbyHeaderItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 090E778722A9B96000CD99F5 /* PeopleNearbyHeaderItem.swift */; };
090E778A22A9F23C00CD99F5 /* ChannelOwnershipTransferController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 090E778922A9F23C00CD99F5 /* ChannelOwnershipTransferController.swift */; };
090E778C22AA842300CD99F5 /* anim_success.json in Resources */ = {isa = PBXBuildFile; fileRef = 090E778B22AA842200CD99F5 /* anim_success.json */; };
090E778E22AA863A00CD99F5 /* PeopleNearbyIconNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 090E778D22AA863A00CD99F5 /* PeopleNearbyIconNode.swift */; };
0910B0ED21FA178C00F8F87D /* WallpaperPreviewMedia.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0910B0EC21FA178C00F8F87D /* WallpaperPreviewMedia.swift */; };
0910B0EF21FA532D00F8F87D /* WallpaperResources.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0910B0EE21FA532D00F8F87D /* WallpaperResources.swift */; };
0910B0F121FB3DE100F8F87D /* WallpaperPatternPanelNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0910B0F021FB3DE100F8F87D /* WallpaperPatternPanelNode.swift */; };
@ -1218,6 +1223,11 @@
090E63E52195880F00E3C035 /* ContactAddItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactAddItem.swift; sourceTree = "<group>"; };
090E63ED2196FE3A00E3C035 /* OpenAddContact.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenAddContact.swift; sourceTree = "<group>"; };
090E777822A6A32E00CD99F5 /* ThemeSettingsThemeItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThemeSettingsThemeItem.swift; sourceTree = "<group>"; };
090E778522A9B95A00CD99F5 /* PeopleNearbyController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PeopleNearbyController.swift; sourceTree = "<group>"; };
090E778722A9B96000CD99F5 /* PeopleNearbyHeaderItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PeopleNearbyHeaderItem.swift; sourceTree = "<group>"; };
090E778922A9F23C00CD99F5 /* ChannelOwnershipTransferController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChannelOwnershipTransferController.swift; sourceTree = "<group>"; };
090E778B22AA842200CD99F5 /* anim_success.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = anim_success.json; sourceTree = "<group>"; };
090E778D22AA863A00CD99F5 /* PeopleNearbyIconNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PeopleNearbyIconNode.swift; sourceTree = "<group>"; };
0910B0EC21FA178C00F8F87D /* WallpaperPreviewMedia.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WallpaperPreviewMedia.swift; sourceTree = "<group>"; };
0910B0EE21FA532D00F8F87D /* WallpaperResources.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WallpaperResources.swift; sourceTree = "<group>"; };
0910B0F021FB3DE100F8F87D /* WallpaperPatternPanelNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WallpaperPatternPanelNode.swift; sourceTree = "<group>"; };
@ -2544,6 +2554,16 @@
name = "Language Suggestion";
sourceTree = "<group>";
};
090E778422A9B94700CD99F5 /* People Nearby */ = {
isa = PBXGroup;
children = (
090E778522A9B95A00CD99F5 /* PeopleNearbyController.swift */,
090E778722A9B96000CD99F5 /* PeopleNearbyHeaderItem.swift */,
090E778D22AA863A00CD99F5 /* PeopleNearbyIconNode.swift */,
);
name = "People Nearby";
sourceTree = "<group>";
};
0919546D229458E900E11046 /* Animated Stickers */ = {
isa = PBXGroup;
children = (
@ -2568,6 +2588,7 @@
09310D13213BC5DE0020033A /* Animations */ = {
isa = PBXGroup;
children = (
090E778B22AA842200CD99F5 /* anim_success.json */,
09FFBCCF227B7F9000C33B4B /* anim_archiveswipe.json */,
094735182277483B00EA2312 /* anim_infotip.json */,
094735072275D72000EA2312 /* anim_archive.json */,
@ -4482,6 +4503,7 @@
D0E1199D229809B6008CAE3A /* ChannelDiscussionGroupActionSheetItem.swift */,
D050A463229C052A0044F11A /* ChannelDiscussionGroupSetupSearchItem.swift */,
D050A465229C06460044F11A /* ChannelDiscussionGroupSearchContainerNode.swift */,
090E778922A9F23C00CD99F5 /* ChannelOwnershipTransferController.swift */,
);
name = "Peer Info";
sourceTree = "<group>";
@ -4669,6 +4691,7 @@
0941A99E210B053300EBE194 /* Open In */,
09F215982263E61400AEDF6D /* Passcode */,
09B4EE5721A82F5900847FA6 /* Permissions */,
090E778422A9B94700CD99F5 /* People Nearby */,
);
name = Controllers;
sourceTree = "<group>";
@ -5284,6 +5307,7 @@
09874E5721078FA100E190B8 /* YoutubeUserScript.js in Resources */,
094735172275D72100EA2312 /* anim_pin.json in Resources */,
094735112275D72100EA2312 /* anim_archive.json in Resources */,
090E778C22AA842300CD99F5 /* anim_success.json in Resources */,
D0EB42051F3143AB00838FE6 /* LegacyComponentsResources.bundle in Resources */,
D0E9BAA21F056F4C00F079A4 /* stp_card_discover@3x.png in Resources */,
D0E9BAB01F056F4C00F079A4 /* stp_card_mastercard@3x.png in Resources */,
@ -5380,6 +5404,7 @@
D0E1199A2297F9C6008CAE3A /* ChannelDiscussionGroupSetupController.swift in Sources */,
D083491C209361DC008CFD52 /* AvatarGalleryItemFooterContentNode.swift in Sources */,
D0EC6CB31EB9F58800EBF1C3 /* shader.c in Sources */,
090E778A22A9F23C00CD99F5 /* ChannelOwnershipTransferController.swift in Sources */,
D06E4C352134AE3C00088087 /* ThemeAutoNightSettingsController.swift in Sources */,
D0F0AAE21EC20EF8005EE2A5 /* CallControllerStatusNode.swift in Sources */,
D0EC6CB41EB9F58800EBF1C3 /* timing.c in Sources */,
@ -5528,6 +5553,7 @@
D0EC6CF31EB9F58800EBF1C3 /* PresentationThemeSettings.swift in Sources */,
D067B4AD211C916300796039 /* TGChannelIntroController.m in Sources */,
D0BE303220601FFC00FBE6D8 /* LocationBroadcastActionSheetItem.swift in Sources */,
090E778E22AA863A00CD99F5 /* PeopleNearbyIconNode.swift in Sources */,
D0EC6CF41EB9F58800EBF1C3 /* ManagedMediaId.swift in Sources */,
09D968A3221F800A00B1458A /* ChatUploadingActivityContentNode.swift in Sources */,
D0CFBB971FD8B0F700B65C0D /* ChatBubbleInstantVideoDecoration.swift in Sources */,
@ -5536,6 +5562,7 @@
D0E9BA521F0559DA00F079A4 /* STPImageLibrary.m in Sources */,
D0EC6CF61EB9F58800EBF1C3 /* ChatContextResultManagedMediaId.swift in Sources */,
D048B33B203C777500038D05 /* RenderedTotalUnreadCount.swift in Sources */,
090E778622A9B95A00CD99F5 /* PeopleNearbyController.swift in Sources */,
D04ECD721FFBF22B00DE9029 /* OpenUrl.swift in Sources */,
D04B4D661EEA993A00711AF6 /* LegacyLocationController.swift in Sources */,
D056CD7A1FF3CC2A00880D28 /* ListMessagePlaybackOverlayNode.swift in Sources */,
@ -6387,6 +6414,7 @@
D0EC6E801EB9F58900EBF1C3 /* ChangePhoneNumberCodeController.swift in Sources */,
D09E637C1F0E7C28003444CD /* SharedMediaPlayer.swift in Sources */,
D0EC6E811EB9F58900EBF1C3 /* NotificationContainerController.swift in Sources */,
090E778822A9B96100CD99F5 /* PeopleNearbyHeaderItem.swift in Sources */,
D0754D271EEE10C800884F6E /* BotCheckoutController.swift in Sources */,
D053DADA201A4C4400993D32 /* ChatTextInputAttributes.swift in Sources */,
0952D1752176DEB500194860 /* NotificationMuteSettingsController.swift in Sources */,

View File

@ -6,6 +6,7 @@ import Display
import TelegramCore
import SwiftSignalKit
private let deletedIcon = UIImage(bundleImageName: "Avatar/DeletedIcon")?.precomposed()
private let savedMessagesIcon = UIImage(bundleImageName: "Avatar/SavedMessagesIcon")?.precomposed()
private let archivedChatsIcon = UIImage(bundleImageName: "Avatar/ArchiveAvatarIcon")?.precomposed()
@ -101,6 +102,7 @@ private enum AvatarNodeIcon: Equatable {
case savedMessagesIcon
case archivedChatsIcon(hiddenByDefault: Bool)
case editAvatarIcon
case deletedIcon
}
public enum AvatarNodeImageOverride: Equatable {
@ -109,6 +111,7 @@ public enum AvatarNodeImageOverride: Equatable {
case savedMessagesIcon
case archivedChatsIcon(hiddenByDefault: Bool)
case editAvatarIcon
case deletedIcon
}
public enum AvatarNodeColorOverride {
@ -291,6 +294,9 @@ public final class AvatarNode: ASDisplayNode {
case .editAvatarIcon:
representation = peer?.smallProfileImage
icon = .editAvatarIcon
case .deletedIcon:
representation = nil
icon = .deletedIcon
}
} else if peer?.restrictionText == nil {
representation = peer?.smallProfileImage
@ -411,7 +417,9 @@ public final class AvatarNode: ASDisplayNode {
let colorsArray: NSArray
var iconColor = UIColor.white
if let parameters = parameters as? AvatarNodeParameters, parameters.icon != .none {
if case .savedMessagesIcon = parameters.icon {
if case .deletedIcon = parameters.icon {
colorsArray = grayscaleColors
} else if case .savedMessagesIcon = parameters.icon {
colorsArray = savedMessagesColors
} else if case .editAvatarIcon = parameters.icon, let theme = parameters.theme {
colorsArray = [theme.list.blocksBackgroundColor.cgColor, theme.list.blocksBackgroundColor.cgColor]
@ -444,7 +452,16 @@ public final class AvatarNode: ASDisplayNode {
context.setBlendMode(.normal)
if let parameters = parameters as? AvatarNodeParameters {
if case .savedMessagesIcon = parameters.icon {
if case .deletedIcon = parameters.icon {
let factor = bounds.size.width / 60.0
context.translateBy(x: bounds.size.width / 2.0, y: bounds.size.height / 2.0)
context.scaleBy(x: factor, y: -factor)
context.translateBy(x: -bounds.size.width / 2.0, y: -bounds.size.height / 2.0)
if let deletedIcon = deletedIcon {
context.draw(deletedIcon.cgImage!, in: CGRect(origin: CGPoint(x: floor((bounds.size.width - deletedIcon.size.width) / 2.0), y: floor((bounds.size.height - deletedIcon.size.height) / 2.0)), size: deletedIcon.size))
}
} else if case .savedMessagesIcon = parameters.icon {
let factor = bounds.size.width / 60.0
context.translateBy(x: bounds.size.width / 2.0, y: bounds.size.height / 2.0)
context.scaleBy(x: factor, y: -factor)

View File

@ -217,7 +217,7 @@ final class CachedEmojiRepresentation: CachedMediaResourceRepresentation {
final class CachedAnimatedStickerRepresentation: CachedMediaResourceRepresentation {
var uniqueId: String {
return "animated-sticker"
return "animated-sticker-v1"
}
func isEqual(to: CachedMediaResourceRepresentation) -> Bool {

View File

@ -6,77 +6,6 @@ import Display
import Postbox
import TelegramCore
private final class CallRatingContentActionNode: HighlightableButtonNode {
private let backgroundNode: ASDisplayNode
let action: TextAlertAction
init(theme: AlertControllerTheme, action: TextAlertAction) {
self.backgroundNode = ASDisplayNode()
self.backgroundNode.isLayerBacked = true
self.backgroundNode.alpha = 0.0
self.action = action
super.init()
self.titleNode.maximumNumberOfLines = 2
self.highligthedChanged = { [weak self] value in
if let strongSelf = self {
if value {
if strongSelf.backgroundNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.backgroundNode, at: 0)
}
strongSelf.backgroundNode.layer.removeAnimation(forKey: "opacity")
strongSelf.backgroundNode.alpha = 1.0
} else if !strongSelf.backgroundNode.alpha.isZero {
strongSelf.backgroundNode.alpha = 0.0
strongSelf.backgroundNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25)
}
}
}
self.updateTheme(theme)
}
func updateTheme(_ theme: AlertControllerTheme) {
self.backgroundNode.backgroundColor = theme.highlightedItemColor
var font = Font.regular(17.0)
var color = theme.accentColor
switch self.action.type {
case .defaultAction, .genericAction:
break
case .destructiveAction:
color = theme.destructiveColor
}
switch self.action.type {
case .defaultAction:
font = Font.semibold(17.0)
case .destructiveAction, .genericAction:
break
}
self.setAttributedTitle(NSAttributedString(string: self.action.title, font: font, textColor: color, paragraphAlignment: .center), for: [])
}
override func didLoad() {
super.didLoad()
self.addTarget(self, action: #selector(self.pressed), forControlEvents: .touchUpInside)
}
@objc func pressed() {
self.action.action()
}
override func layout() {
super.layout()
self.backgroundNode.frame = self.bounds
}
}
private final class CallRatingAlertContentNode: AlertContentNode {
private let strings: PresentationStrings
private let apply: (Int) -> Void
@ -87,7 +16,7 @@ private final class CallRatingAlertContentNode: AlertContentNode {
private let starNodes: [ASButtonNode]
private let actionNodesSeparator: ASDisplayNode
private let actionNodes: [CallRatingContentActionNode]
private let actionNodes: [TextAlertContentActionNode]
private let actionVerticalSeparators: [ASDisplayNode]
private let disposable = MetaDisposable()
@ -114,8 +43,8 @@ private final class CallRatingAlertContentNode: AlertContentNode {
self.actionNodesSeparator = ASDisplayNode()
self.actionNodesSeparator.isLayerBacked = true
self.actionNodes = actions.map { action -> CallRatingContentActionNode in
return CallRatingContentActionNode(theme: theme, action: action)
self.actionNodes = actions.map { action -> TextAlertContentActionNode in
return TextAlertContentActionNode(theme: theme, action: action)
}
var actionVerticalSeparators: [ASDisplayNode] = []

View File

@ -8,139 +8,6 @@ import TelegramCore
private func generateIconImage(theme: AlertControllerTheme) -> UIImage? {
return UIImage(bundleImageName: "Call List/AlertIcon")
// return generateImage(frame.size, contextGenerator: { size, context in
// let bounds = CGRect(origin: CGPoint(), size: size)
// context.clear(bounds)
//
// let relativeFrame = CGRect(x: -frame.minX, y: frame.minY - background.size.height + frame.size.height
// , width: background.size.width, height: background.size.height)
//
// context.beginPath()
// context.addEllipse(in: bounds)
// context.clip()
//
// context.setAlpha(0.8)
// context.draw(background.foregroundImage.cgImage!, in: relativeFrame)
//
// if highlighted {
// context.setFillColor(UIColor(white: 1.0, alpha: 0.65).cgColor)
// context.fillEllipse(in: bounds)
// }
//
// context.setAlpha(1.0)
// context.textMatrix = .identity
//
// let titleFont: UIFont
// let subtitleFont: UIFont
// let titleOffset: CGFloat
// let subtitleOffset: CGFloat
// if size.width > 80.0 {
// titleFont = largeTitleFont
// subtitleFont = largeSubtitleFont
// if subtitle.isEmpty {
// titleOffset = -18.0
// } else {
// titleOffset = -11.0
// }
// subtitleOffset = -54.0
// } else {
// titleFont = regularTitleFont
// subtitleFont = regularSubtitleFont
// if subtitle.isEmpty {
// titleOffset = -17.0
// } else {
// titleOffset = -10.0
// }
// subtitleOffset = -48.0
// }
//
// let titlePath = CGMutablePath()
// titlePath.addRect(bounds.offsetBy(dx: 0.0, dy: titleOffset))
// let titleString = NSAttributedString(string: title, font: titleFont, textColor: .white, paragraphAlignment: .center)
// let titleFramesetter = CTFramesetterCreateWithAttributedString(titleString as CFAttributedString)
// let titleFrame = CTFramesetterCreateFrame(titleFramesetter, CFRangeMake(0, titleString.length), titlePath, nil)
// CTFrameDraw(titleFrame, context)
//
// if !subtitle.isEmpty {
// let subtitlePath = CGMutablePath()
// subtitlePath.addRect(bounds.offsetBy(dx: 0.0, dy: subtitleOffset))
// let subtitleString = NSAttributedString(string: subtitle, font: subtitleFont, textColor: .white, paragraphAlignment: .center)
// let subtitleFramesetter = CTFramesetterCreateWithAttributedString(subtitleString as CFAttributedString)
// let subtitleFrame = CTFramesetterCreateFrame(subtitleFramesetter, CFRangeMake(0, subtitleString.length), subtitlePath, nil)
// CTFrameDraw(subtitleFrame, context)
// }
// })
}
private final class CallSuggestTabContentActionNode: HighlightableButtonNode {
private let backgroundNode: ASDisplayNode
let action: TextAlertAction
init(theme: AlertControllerTheme, action: TextAlertAction) {
self.backgroundNode = ASDisplayNode()
self.backgroundNode.isLayerBacked = true
self.backgroundNode.alpha = 0.0
self.action = action
super.init()
self.titleNode.maximumNumberOfLines = 2
self.highligthedChanged = { [weak self] value in
if let strongSelf = self {
if value {
if strongSelf.backgroundNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.backgroundNode, at: 0)
}
strongSelf.backgroundNode.layer.removeAnimation(forKey: "opacity")
strongSelf.backgroundNode.alpha = 1.0
} else if !strongSelf.backgroundNode.alpha.isZero {
strongSelf.backgroundNode.alpha = 0.0
strongSelf.backgroundNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25)
}
}
}
self.updateTheme(theme)
}
func updateTheme(_ theme: AlertControllerTheme) {
self.backgroundNode.backgroundColor = theme.highlightedItemColor
var font = Font.regular(17.0)
var color = theme.accentColor
switch self.action.type {
case .defaultAction, .genericAction:
break
case .destructiveAction:
color = theme.destructiveColor
}
switch self.action.type {
case .defaultAction:
font = Font.semibold(17.0)
case .destructiveAction, .genericAction:
break
}
self.setAttributedTitle(NSAttributedString(string: self.action.title, font: font, textColor: color, paragraphAlignment: .center), for: [])
}
override func didLoad() {
super.didLoad()
self.addTarget(self, action: #selector(self.pressed), forControlEvents: .touchUpInside)
}
@objc func pressed() {
self.action.action()
}
override func layout() {
super.layout()
self.backgroundNode.frame = self.bounds
}
}
private final class CallSuggestTabAlertContentNode: AlertContentNode {
@ -151,7 +18,7 @@ private final class CallSuggestTabAlertContentNode: AlertContentNode {
private let iconNode: ASImageNode
private let actionNodesSeparator: ASDisplayNode
private let actionNodes: [CallSuggestTabContentActionNode]
private let actionNodes: [TextAlertContentActionNode]
private let actionVerticalSeparators: [ASDisplayNode]
private var validLayout: CGSize?
@ -174,8 +41,8 @@ private final class CallSuggestTabAlertContentNode: AlertContentNode {
self.actionNodesSeparator = ASDisplayNode()
self.actionNodesSeparator.isLayerBacked = true
self.actionNodes = actions.map { action -> CallSuggestTabContentActionNode in
return CallSuggestTabContentActionNode(theme: theme, action: action)
self.actionNodes = actions.map { action -> TextAlertContentActionNode in
return TextAlertContentActionNode(theme: theme, action: action)
}
var actionVerticalSeparators: [ASDisplayNode] = []

View File

@ -8,11 +8,13 @@ import TelegramCore
private final class ChannelAdminControllerArguments {
let account: Account
let toggleRight: (TelegramChatAdminRightsFlags, TelegramChatAdminRightsFlags) -> Void
let transferOwnership: () -> Void
let dismissAdmin: () -> Void
init(account: Account, toggleRight: @escaping (TelegramChatAdminRightsFlags, TelegramChatAdminRightsFlags) -> Void, dismissAdmin: @escaping () -> Void) {
init(account: Account, toggleRight: @escaping (TelegramChatAdminRightsFlags, TelegramChatAdminRightsFlags) -> Void, transferOwnership: @escaping () -> Void, dismissAdmin: @escaping () -> Void) {
self.account = account
self.toggleRight = toggleRight
self.transferOwnership = transferOwnership
self.dismissAdmin = dismissAdmin
}
}
@ -20,6 +22,7 @@ private final class ChannelAdminControllerArguments {
private enum ChannelAdminSection: Int32 {
case info
case rights
case transfer
case dismiss
}
@ -28,6 +31,7 @@ private enum ChannelAdminEntryStableId: Hashable {
case rightsTitle
case right(TelegramChatAdminRightsFlags)
case addAdminsInfo
case transfer
case dismiss
var hashValue: Int {
@ -40,6 +44,8 @@ private enum ChannelAdminEntryStableId: Hashable {
return 2
case .dismiss:
return 3
case .transfer:
return 4
case let .right(flags):
return flags.rawValue.hashValue
}
@ -71,6 +77,12 @@ private enum ChannelAdminEntryStableId: Hashable {
} else {
return false
}
case .transfer:
if case .transfer = rhs {
return true
} else {
return false
}
case .dismiss:
if case .dismiss = rhs {
return true
@ -86,6 +98,7 @@ private enum ChannelAdminEntry: ItemListNodeEntry {
case rightsTitle(PresentationTheme, String)
case rightItem(PresentationTheme, Int, String, TelegramChatAdminRightsFlags, TelegramChatAdminRightsFlags, Bool, Bool)
case addAdminsInfo(PresentationTheme, String)
case transfer(PresentationTheme, String)
case dismiss(PresentationTheme, String)
var section: ItemListSectionId {
@ -94,6 +107,8 @@ private enum ChannelAdminEntry: ItemListNodeEntry {
return ChannelAdminSection.info.rawValue
case .rightsTitle, .rightItem, .addAdminsInfo:
return ChannelAdminSection.rights.rawValue
case .transfer:
return ChannelAdminSection.transfer.rawValue
case .dismiss:
return ChannelAdminSection.dismiss.rawValue
}
@ -109,6 +124,8 @@ private enum ChannelAdminEntry: ItemListNodeEntry {
return .right(right)
case .addAdminsInfo:
return .addAdminsInfo
case .transfer:
return .transfer
case .dismiss:
return .dismiss
}
@ -177,6 +194,12 @@ private enum ChannelAdminEntry: ItemListNodeEntry {
} else {
return false
}
case let .transfer(lhsTheme, lhsText):
if case let .transfer(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
case let .dismiss(lhsTheme, lhsText):
if case let .dismiss(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
@ -218,6 +241,13 @@ private enum ChannelAdminEntry: ItemListNodeEntry {
default:
return true
}
case .transfer:
switch rhs {
case .info, .rightsTitle, .rightItem, .addAdminsInfo, .transfer:
return false
default:
return true
}
case .dismiss:
return false
}
@ -237,8 +267,12 @@ private enum ChannelAdminEntry: ItemListNodeEntry {
})
case let .addAdminsInfo(theme, text):
return ItemListTextItem(theme: theme, text: .plain(text), sectionId: self.section)
case let .transfer(theme, text):
return ItemListActionItem(theme: theme, title: text, kind: .generic, alignment: .center, sectionId: self.section, style: .blocks, action: {
arguments.transferOwnership()
}, tag: nil)
case let .dismiss(theme, text):
return ItemListActionItem(theme: theme, title: text, kind: .destructive, alignment: .natural, sectionId: self.section, style: .blocks, action: {
return ItemListActionItem(theme: theme, title: text, kind: .destructive, alignment: .center, sectionId: self.section, style: .blocks, action: {
arguments.dismissAdmin()
}, tag: nil)
}
@ -422,6 +456,10 @@ private func channelAdminControllerEntries(presentationData: PresentationData, s
entries.append(.addAdminsInfo(presentationData.theme, currentRightsFlags.contains(.canAddAdmins) ? presentationData.strings.Channel_EditAdmin_PermissinAddAdminOn : presentationData.strings.Channel_EditAdmin_PermissinAddAdminOff))
}
if channel.flags.contains(.isCreator) && currentRightsFlags.contains(.canAddAdmins) {
entries.append(.transfer(presentationData.theme, isGroup ? presentationData.strings.Group_EditAdmin_TransferOwnership : presentationData.strings.Channel_EditAdmin_TransferOwnership))
}
if let initialParticipant = initialParticipant, case let .member(participant) = initialParticipant, let adminInfo = participant.adminInfo, !adminInfo.rights.flags.isEmpty {
var canDismiss = false
if channel.flags.contains(.isCreator) {
@ -488,8 +526,11 @@ private func channelAdminControllerEntries(presentationData: PresentationData, s
entries.append(.addAdminsInfo(presentationData.theme, currentRightsFlags.contains(.canAddAdmins) ? presentationData.strings.Channel_EditAdmin_PermissinAddAdminOn : presentationData.strings.Channel_EditAdmin_PermissinAddAdminOff))
}
if group.role == .creator && currentRightsFlags.contains(.canAddAdmins) {
entries.append(.transfer(presentationData.theme, presentationData.strings.Group_EditAdmin_TransferOwnership))
}
if let initialParticipant = initialParticipant, case let .member(participant) = initialParticipant, let adminInfo = participant.adminInfo, !adminInfo.rights.flags.isEmpty {
let canDismiss = true
entries.append(.dismiss(presentationData.theme, presentationData.strings.Channel_Moderator_AccessLevelRevoke))
}
}
@ -497,7 +538,7 @@ private func channelAdminControllerEntries(presentationData: PresentationData, s
return entries
}
public func channelAdminController(context: AccountContext, peerId: PeerId, adminId: PeerId, initialParticipant: ChannelParticipant?, updated: @escaping (TelegramChatAdminRights) -> Void, upgradedToSupergroup: @escaping (PeerId, @escaping () -> Void) -> Void) -> ViewController {
public func channelAdminController(context: AccountContext, peerId: PeerId, adminId: PeerId, initialParticipant: ChannelParticipant?, updated: @escaping (TelegramChatAdminRights) -> Void, upgradedToSupergroup: @escaping (PeerId, @escaping () -> Void) -> Void, transferedOwnership: @escaping (PeerId) -> Void) -> ViewController {
let statePromise = ValuePromise(ChannelAdminControllerState(), ignoreRepeated: true)
let stateValue = Atomic(value: ChannelAdminControllerState())
let updateState: ((ChannelAdminControllerState) -> ChannelAdminControllerState) -> Void = { f in
@ -509,9 +550,18 @@ public func channelAdminController(context: AccountContext, peerId: PeerId, admi
let updateRightsDisposable = MetaDisposable()
actionsDisposable.add(updateRightsDisposable)
let transferOwnershipDisposable = MetaDisposable()
actionsDisposable.add(transferOwnershipDisposable)
var dismissImpl: (() -> Void)?
var presentControllerImpl: ((ViewController, Any?) -> Void)?
let actualPeerId = Atomic<PeerId>(value: peerId)
let upgradedToSupergroupImpl: (PeerId, @escaping () -> Void) -> Void = { peerId, completion in
let _ = actualPeerId.swap(peerId)
upgradedToSupergroup(peerId, completion)
}
let arguments = ChannelAdminControllerArguments(account: context.account, toggleRight: { right, flags in
updateState { current in
var updated = flags
@ -522,6 +572,65 @@ public func channelAdminController(context: AccountContext, peerId: PeerId, admi
}
return current.withUpdatedUpdatedFlags(updated)
}
}, transferOwnership: {
updateState { current in
return current.withUpdatedUpdating(true)
}
let _ = (context.account.postbox.transaction { transaction -> (peer: Peer?, member: Peer?) in
return (peer: transaction.getPeer(peerId), member: transaction.getPeer(adminId))
} |> deliverOnMainQueue).start(next: { peer, member in
guard let peer = peer, let member = member as? TelegramUser else {
return
}
var signal: Signal<Never, ChannelOwnershipTransferError> = .complete()
if let channel = peer as? TelegramChannel {
signal = updateChannelOwnership(postbox: context.account.postbox, network: context.account.network, accountStateManager: context.account.stateManager, channelId: channel.id, memberId: adminId, password: nil)
} else if let _ = peer as? TelegramGroup {
signal = convertGroupToSupergroup(account: context.account, peerId: peerId)
|> map(Optional.init)
|> mapError { _ in ChannelOwnershipTransferError.generic }
|> mapToSignal { upgradedPeerId -> Signal<Never, ChannelOwnershipTransferError> in
guard let upgradedPeerId = upgradedPeerId else {
return .fail(.generic)
}
upgradedToSupergroupImpl(upgradedPeerId, {})
return updateChannelOwnership(postbox: context.account.postbox, network: context.account.network, accountStateManager: context.account.stateManager, channelId: upgradedPeerId, memberId: adminId, password: nil)
}
}
transferOwnershipDisposable.set((signal |> deliverOnMainQueue).start(error: { error in
updateState { current in
return current.withUpdatedUpdating(false)
}
let currentPeerId = actualPeerId.with { $0 }
let channel: Signal<Peer?, NoError>
if currentPeerId == peerId {
channel = .single(peer)
} else {
channel = context.account.postbox.transaction { transaction -> Peer? in
return transaction.getPeer(currentPeerId)
}
}
let _ = (channel |> deliverOnMainQueue).start(next: { channel in
guard let channel = channel as? TelegramChannel else {
return
}
let controller = channelOwnershipTransferController(context: context, channel: channel, member: member, initialError: error, present: { c, a in
presentControllerImpl?(c, a)
}, completion: {
dismissImpl?()
transferedOwnership(member.id)
})
presentControllerImpl?(controller, nil)
})
}))
})
}, dismissAdmin: {
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let actionSheet = ActionSheetController(presentationTheme: presentationData.theme)

View File

@ -199,12 +199,16 @@ private enum ChannelAdminsEntry: ItemListNodeEntry {
let action: (() -> Void)?
switch participant.participant {
case .creator:
peerText = strings.Channel_Management_LabelCreator
peerText = strings.Channel_Management_LabelOwner
action = nil
case let .member(_, _, adminInfo, _):
if let adminInfo = adminInfo {
if let peer = participant.peers[adminInfo.promotedBy] {
if peer.id == participant.peer.id {
peerText = strings.Channel_Management_LabelAdministrator
} else {
peerText = strings.Channel_Management_PromotedBy(peer.displayTitle).0
}
} else {
peerText = ""
}
@ -491,6 +495,18 @@ public func channelAdminsController(context: AccountContext, peerId: PeerId, loa
upgradedToSupergroupImpl?(upgradedPeerId, f)
}
let transferedOwnership: (PeerId) -> Void = { memberId in
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let _ = (context.account.postbox.transaction { transaction -> (channel: Peer?, user: Peer?) in
return (channel: transaction.getPeer(peerId), user: transaction.getPeer(memberId))
} |> deliverOnMainQueue).start(next: { peer, user in
guard let peer = peer, let user = user else {
return
}
presentControllerImpl?(UndoOverlayController(context: context, content: .succeed(text: presentationData.strings.Channel_OwnershipTransfer_TransferCompleted(peer.displayTitle, user.displayTitle).0), elevatedLayout: false, action: { _ in }), nil)
})
}
let peerView = Promise<PeerView>()
peerView.set(context.account.viewTracker.peerView(peerId))
@ -564,7 +580,7 @@ public func channelAdminsController(context: AccountContext, peerId: PeerId, loa
}
}
presentControllerImpl?(channelAdminController(context: context, peerId: peerId, adminId: peer.id, initialParticipant: participant?.participant, updated: { _ in
}, upgradedToSupergroup: upgradedToSupergroup), ViewControllerPresentationArguments(presentationAnimation: .modalSheet))
}, upgradedToSupergroup: upgradedToSupergroup, transferedOwnership: transferedOwnership), ViewControllerPresentationArguments(presentationAnimation: .modalSheet))
})
dismissController = { [weak controller] in
controller?.dismiss()
@ -576,7 +592,7 @@ public func channelAdminsController(context: AccountContext, peerId: PeerId, loa
})
}, openAdmin: { participant in
presentControllerImpl?(channelAdminController(context: context, peerId: peerId, adminId: participant.peerId, initialParticipant: participant, updated: { _ in
}, upgradedToSupergroup: upgradedToSupergroup), ViewControllerPresentationArguments(presentationAnimation: .modalSheet))
}, upgradedToSupergroup: upgradedToSupergroup, transferedOwnership: transferedOwnership), ViewControllerPresentationArguments(presentationAnimation: .modalSheet))
})
let membersAndLoadMoreControl: (Disposable, PeerChannelMemberCategoryControl?)
@ -701,7 +717,7 @@ public func channelAdminsController(context: AccountContext, peerId: PeerId, loa
updateState { state in
return state.withUpdatedSearchingMembers(false)
}
}, upgradedToSupergroup: upgradedToSupergroup), ViewControllerPresentationArguments(presentationAnimation: .modalSheet))
}, upgradedToSupergroup: upgradedToSupergroup, transferedOwnership: transferedOwnership), ViewControllerPresentationArguments(presentationAnimation: .modalSheet))
}
}, present: { c, a in
presentControllerImpl?(c, a)

View File

@ -396,7 +396,7 @@ public func channelMembersController(context: AccountContext, peerId: PeerId) ->
contactsController?.dismiss()
presentControllerImpl?(channelAdminController(context: context, peerId: peerId, adminId: memberId, initialParticipant: nil, updated: { _ in
}, upgradedToSupergroup: { _, f in f () }), ViewControllerPresentationArguments(presentationAnimation: .modalSheet))
}, upgradedToSupergroup: { _, f in f () }, transferedOwnership: { _ in }), ViewControllerPresentationArguments(presentationAnimation: .modalSheet))
})]), nil)
} else {
presentControllerImpl?(textAlertController(context: context, title: nil, text: presentationData.strings.Channel_AddBotErrorHaveRights, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), nil)

View File

@ -332,7 +332,7 @@ final class ChannelMembersSearchContainerNode: SearchDisplayControllerContentNod
return state
}
present(channelAdminController(context: context, peerId: peerId, adminId: participant.peer.id, initialParticipant: participant.participant, updated: { _ in
}, upgradedToSupergroup: { _, f in f() }), ViewControllerPresentationArguments(presentationAnimation: .modalSheet))
}, upgradedToSupergroup: { _, f in f() }, transferedOwnership: { _ in }), ViewControllerPresentationArguments(presentationAnimation: .modalSheet))
}, restrictPeer: { participant in
updateState { state in
var state = state
@ -469,7 +469,7 @@ final class ChannelMembersSearchContainerNode: SearchDisplayControllerContentNod
if case .searchMembers = mode {
switch participant.participant {
case .creator:
label = themeAndStrings.1.Channel_Management_LabelCreator
label = themeAndStrings.1.Channel_Management_LabelOwner
default:
break
}
@ -675,13 +675,13 @@ final class ChannelMembersSearchContainerNode: SearchDisplayControllerContentNod
var enabled = true
if case .banAndPromoteActions = mode {
if case .creator = participant.participant {
label = themeAndStrings.1.Channel_Management_LabelCreator
label = themeAndStrings.1.Channel_Management_LabelOwner
enabled = false
}
} else if case .searchMembers = mode {
switch participant.participant {
case .creator:
label = themeAndStrings.1.Channel_Management_LabelCreator
label = themeAndStrings.1.Channel_Management_LabelOwner
case let .member(member):
if member.adminInfo != nil {
label = themeAndStrings.1.Channel_Management_LabelEditor
@ -712,14 +712,18 @@ final class ChannelMembersSearchContainerNode: SearchDisplayControllerContentNod
case .searchAdmins:
switch participant.participant {
case .creator:
label = themeAndStrings.1.Channel_Management_LabelCreator
label = themeAndStrings.1.Channel_Management_LabelOwner
case let .member(_, _, adminInfo, _):
if let adminInfo = adminInfo {
if let peer = participant.peers[adminInfo.promotedBy] {
if peer.id == participant.peer.id {
label = themeAndStrings.1.Channel_Management_LabelAdministrator
} else {
label = themeAndStrings.1.Channel_Management_PromotedBy(peer.displayTitle).0
}
}
}
}
case .searchBanned:
switch participant.participant {
case let .member(_, _, _, banInfo):
@ -774,7 +778,7 @@ final class ChannelMembersSearchContainerNode: SearchDisplayControllerContentNod
var enabled = true
if case .banAndPromoteActions = mode {
if case .creator = participant.participant {
label = themeAndStrings.1.Channel_Management_LabelCreator
label = themeAndStrings.1.Channel_Management_LabelOwner
enabled = false
}
}
@ -947,13 +951,13 @@ final class ChannelMembersSearchContainerNode: SearchDisplayControllerContentNod
var enabled = true
if case .banAndPromoteActions = mode {
if case .creator = participant.participant {
label = themeAndStrings.1.Channel_Management_LabelCreator
label = themeAndStrings.1.Channel_Management_LabelOwner
enabled = false
}
} else if case .searchMembers = mode {
switch participant.participant {
case .creator:
label = themeAndStrings.1.Channel_Management_LabelCreator
label = themeAndStrings.1.Channel_Management_LabelOwner
case let .member(member):
if member.adminInfo != nil {
label = themeAndStrings.1.Channel_Management_LabelEditor
@ -980,14 +984,18 @@ final class ChannelMembersSearchContainerNode: SearchDisplayControllerContentNod
case .searchAdmins:
switch participant.participant {
case .creator:
label = themeAndStrings.1.Channel_Management_LabelCreator
label = themeAndStrings.1.Channel_Management_LabelOwner
case let .member(_, _, adminInfo, _):
if let adminInfo = adminInfo {
if let peer = participant.peers[adminInfo.promotedBy] {
if peer.id == participant.peer.id {
label = themeAndStrings.1.Channel_Management_LabelAdministrator
} else {
label = themeAndStrings.1.Channel_Management_PromotedBy(peer.displayTitle).0
}
}
}
}
case .searchBanned:
switch participant.participant {
case let .member(_, _, _, banInfo):
@ -1042,7 +1050,7 @@ final class ChannelMembersSearchContainerNode: SearchDisplayControllerContentNod
var enabled = true
if case .banAndPromoteActions = mode {
if case .creator = participant.participant {
label = themeAndStrings.1.Channel_Management_LabelCreator
label = themeAndStrings.1.Channel_Management_LabelOwner
enabled = false
}
}

View File

@ -195,7 +195,7 @@ class ChannelMembersSearchControllerNode: ASDisplayNode {
}
}
if case .creator = participant {
label = strongSelf.presentationData.strings.Channel_Management_LabelCreator
label = strongSelf.presentationData.strings.Channel_Management_LabelOwner
enabled = false
}
}
@ -263,7 +263,7 @@ class ChannelMembersSearchControllerNode: ASDisplayNode {
}
}
if case .creator = participant.participant {
label = strongSelf.presentationData.strings.Channel_Management_LabelCreator
label = strongSelf.presentationData.strings.Channel_Management_LabelOwner
enabled = false
}
}

View File

@ -0,0 +1,481 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import SwiftSignalKit
import Postbox
import TelegramCore
private final class ChannelOwnershipTransferPasswordFieldNode: ASDisplayNode, UITextFieldDelegate {
private var theme: PresentationTheme
private let backgroundNode: ASImageNode
private let textInputNode: TextFieldNode
private let placeholderNode: ASTextNode
var complete: (() -> Void)?
var textChanged: ((String) -> Void)?
private let backgroundInsets = UIEdgeInsets(top: 8.0, left: 22.0, bottom: 15.0, right: 22.0)
private let inputInsets = UIEdgeInsets(top: 5.0, left: 11.0, bottom: 5.0, right: 11.0)
var password: String {
get {
return self.textInputNode.textField.text ?? ""
}
set {
self.textInputNode.textField.text = newValue
self.placeholderNode.isHidden = !newValue.isEmpty
}
}
var placeholder: String = "" {
didSet {
self.placeholderNode.attributedText = NSAttributedString(string: self.placeholder, font: Font.regular(17.0), textColor: self.theme.actionSheet.inputPlaceholderColor)
}
}
init(theme: PresentationTheme, placeholder: String) {
self.theme = theme
self.backgroundNode = ASImageNode()
self.backgroundNode.isLayerBacked = true
self.backgroundNode.displaysAsynchronously = false
self.backgroundNode.displayWithoutProcessing = true
self.backgroundNode.image = generateStretchableFilledCircleImage(diameter: 16.0, color: theme.actionSheet.inputHollowBackgroundColor, strokeColor: theme.actionSheet.inputBorderColor, strokeWidth: UIScreenPixel)
self.textInputNode = TextFieldNode()
self.placeholderNode = ASTextNode()
self.placeholderNode.isUserInteractionEnabled = false
self.placeholderNode.displaysAsynchronously = false
self.placeholderNode.attributedText = NSAttributedString(string: placeholder, font: Font.regular(14.0), textColor: self.theme.actionSheet.inputPlaceholderColor)
super.init()
self.addSubnode(self.backgroundNode)
self.addSubnode(self.textInputNode)
self.addSubnode(self.placeholderNode)
}
override func didLoad() {
super.didLoad()
self.textInputNode.textField.typingAttributes = [NSAttributedStringKey.font.rawValue: Font.regular(14.0), NSAttributedStringKey.foregroundColor.rawValue: self.theme.actionSheet.inputTextColor]
self.textInputNode.textField.font = Font.regular(14.0)
self.textInputNode.textField.textColor = self.theme.list.itemPrimaryTextColor
self.textInputNode.textField.isSecureTextEntry = true
self.textInputNode.textField.returnKeyType = .done
self.textInputNode.textField.keyboardAppearance = theme.chatList.searchBarKeyboardColor.keyboardAppearance
self.textInputNode.clipsToBounds = true
self.textInputNode.textField.delegate = self
self.textInputNode.textField.addTarget(self, action: #selector(self.textFieldTextChanged(_:)), for: .editingChanged)
self.textInputNode.hitTestSlop = UIEdgeInsets(top: -5.0, left: -5.0, bottom: -5.0, right: -5.0)
}
func updateTheme(_ theme: PresentationTheme) {
self.theme = theme
self.backgroundNode.image = generateStretchableFilledCircleImage(diameter: 16.0, color: theme.actionSheet.inputHollowBackgroundColor, strokeColor: theme.actionSheet.inputBorderColor, strokeWidth: UIScreenPixel)
self.textInputNode.textField.keyboardAppearance = theme.chatList.searchBarKeyboardColor.keyboardAppearance
self.textInputNode.textField.textColor = theme.list.itemPrimaryTextColor
self.textInputNode.textField.typingAttributes = [NSAttributedStringKey.font.rawValue: Font.regular(14.0), NSAttributedStringKey.foregroundColor.rawValue: theme.actionSheet.inputTextColor]
self.placeholderNode.attributedText = NSAttributedString(string: self.placeholderNode.attributedText?.string ?? "", font: Font.regular(14.0), textColor: theme.actionSheet.inputPlaceholderColor)
}
func updateLayout(width: CGFloat, transition: ContainedViewLayoutTransition) -> CGFloat {
let backgroundInsets = self.backgroundInsets
let inputInsets = self.inputInsets
let textFieldHeight: CGFloat = 30.0
let panelHeight = textFieldHeight + backgroundInsets.top + backgroundInsets.bottom
let backgroundFrame = CGRect(origin: CGPoint(x: backgroundInsets.left, y: backgroundInsets.top), size: CGSize(width: width - backgroundInsets.left - backgroundInsets.right, height: panelHeight - backgroundInsets.top - backgroundInsets.bottom))
transition.updateFrame(node: self.backgroundNode, frame: backgroundFrame)
let placeholderSize = self.placeholderNode.measure(backgroundFrame.size)
transition.updateFrame(node: self.placeholderNode, frame: CGRect(origin: CGPoint(x: backgroundFrame.minX + inputInsets.left, y: backgroundFrame.minY + floor((backgroundFrame.size.height - placeholderSize.height) / 2.0)), size: placeholderSize))
transition.updateFrame(node: self.textInputNode, frame: CGRect(origin: CGPoint(x: backgroundFrame.minX + inputInsets.left, y: backgroundFrame.minY), size: CGSize(width: backgroundFrame.size.width - inputInsets.left - inputInsets.right, height: backgroundFrame.size.height)))
return panelHeight
}
func activateInput() {
self.textInputNode.becomeFirstResponder()
}
func deactivateInput() {
self.textInputNode.resignFirstResponder()
}
@objc func editableTextNodeDidUpdateText(_ editableTextNode: ASEditableTextNode) {
self.textChanged?(editableTextNode.textView.text)
self.placeholderNode.isHidden = !(editableTextNode.textView.text ?? "").isEmpty
}
@objc func textFieldTextChanged(_ textField: UITextField) {
let text = textField.text ?? ""
self.textChanged?(text)
self.placeholderNode.isHidden = !text.isEmpty
}
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
if string == "\n" {
self.complete?()
return false
}
return true
}
}
private final class ChannelOwnershipTransferAlertContentNode: AlertContentNode {
private let strings: PresentationStrings
private let titleNode: ASTextNode
private let textNode: ASTextNode
let inputFieldNode: ChannelOwnershipTransferPasswordFieldNode
private let actionNodesSeparator: ASDisplayNode
private let actionNodes: [TextAlertContentActionNode]
private let actionVerticalSeparators: [ASDisplayNode]
private let disposable = MetaDisposable()
private var validLayout: CGSize?
private let hapticFeedback = HapticFeedback()
var complete: (() -> Void)? {
didSet {
self.inputFieldNode.complete = self.complete
}
}
override var dismissOnOutsideTap: Bool {
return self.isUserInteractionEnabled
}
init(theme: AlertControllerTheme, ptheme: PresentationTheme, strings: PresentationStrings, actions: [TextAlertAction]) {
self.strings = strings
self.titleNode = ASTextNode()
self.titleNode.maximumNumberOfLines = 2
self.textNode = ASTextNode()
self.textNode.maximumNumberOfLines = 2
self.inputFieldNode = ChannelOwnershipTransferPasswordFieldNode(theme: ptheme, placeholder: strings.Channel_OwnershipTransfer_PasswordPlaceholder)
self.actionNodesSeparator = ASDisplayNode()
self.actionNodesSeparator.isLayerBacked = true
self.actionNodes = actions.map { action -> TextAlertContentActionNode in
return TextAlertContentActionNode(theme: theme, action: action)
}
var actionVerticalSeparators: [ASDisplayNode] = []
if actions.count > 1 {
for _ in 0 ..< actions.count - 1 {
let separatorNode = ASDisplayNode()
separatorNode.isLayerBacked = true
actionVerticalSeparators.append(separatorNode)
}
}
self.actionVerticalSeparators = actionVerticalSeparators
super.init()
self.addSubnode(self.titleNode)
self.addSubnode(self.textNode)
self.addSubnode(self.inputFieldNode)
self.addSubnode(self.actionNodesSeparator)
for actionNode in self.actionNodes {
self.addSubnode(actionNode)
}
self.actionNodes.last?.actionEnabled = false
for separatorNode in self.actionVerticalSeparators {
self.addSubnode(separatorNode)
}
self.inputFieldNode.textChanged = { [weak self] text in
if let strongSelf = self, let lastNode = strongSelf.actionNodes.last {
lastNode.actionEnabled = !text.isEmpty
}
}
self.updateTheme(theme)
}
deinit {
self.disposable.dispose()
}
func dismissInput() {
self.inputFieldNode.deactivateInput()
}
var password: String {
return self.inputFieldNode.password
}
override func updateTheme(_ theme: AlertControllerTheme) {
self.titleNode.attributedText = NSAttributedString(string: self.strings.Channel_OwnershipTransfer_EnterPassword, font: Font.bold(17.0), textColor: theme.primaryColor, paragraphAlignment: .center)
self.textNode.attributedText = NSAttributedString(string: self.strings.Channel_OwnershipTransfer_EnterPasswordText, font: Font.regular(13.0), textColor: theme.primaryColor, paragraphAlignment: .center)
self.actionNodesSeparator.backgroundColor = theme.separatorColor
for actionNode in self.actionNodes {
actionNode.updateTheme(theme)
}
for separatorNode in self.actionVerticalSeparators {
separatorNode.backgroundColor = theme.separatorColor
}
if let size = self.validLayout {
_ = self.updateLayout(size: size, transition: .immediate)
}
}
override func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize {
var size = size
size.width = min(size.width, 270.0)
let measureSize = CGSize(width: size.width - 16.0 * 2.0, height: CGFloat.greatestFiniteMagnitude)
let hadValidLayout = self.validLayout != nil
self.validLayout = size
var origin: CGPoint = CGPoint(x: 0.0, y: 20.0)
let titleSize = self.titleNode.measure(measureSize)
transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - titleSize.width) / 2.0), y: origin.y), size: titleSize))
origin.y += titleSize.height + 4.0
let textSize = self.textNode.measure(measureSize)
transition.updateFrame(node: self.textNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - textSize.width) / 2.0), y: origin.y), size: textSize))
origin.y += textSize.height + 6.0
let actionButtonHeight: CGFloat = 44.0
var minActionsWidth: CGFloat = 0.0
let maxActionWidth: CGFloat = floor(size.width / CGFloat(self.actionNodes.count))
let actionTitleInsets: CGFloat = 8.0
var effectiveActionLayout = TextAlertContentActionLayout.horizontal
for actionNode in self.actionNodes {
let actionTitleSize = actionNode.titleNode.measure(CGSize(width: maxActionWidth, height: actionButtonHeight))
if case .horizontal = effectiveActionLayout, actionTitleSize.height > actionButtonHeight * 0.6667 {
effectiveActionLayout = .vertical
}
switch effectiveActionLayout {
case .horizontal:
minActionsWidth += actionTitleSize.width + actionTitleInsets
case .vertical:
minActionsWidth = max(minActionsWidth, actionTitleSize.width + actionTitleInsets)
}
}
let insets = UIEdgeInsets(top: 18.0, left: 18.0, bottom: 18.0, right: 18.0)
var contentWidth = max(titleSize.width, minActionsWidth)
contentWidth = max(contentWidth, 234.0)
var actionsHeight: CGFloat = 0.0
switch effectiveActionLayout {
case .horizontal:
actionsHeight = actionButtonHeight
case .vertical:
actionsHeight = actionButtonHeight * CGFloat(self.actionNodes.count)
}
let resultWidth = contentWidth + insets.left + insets.right
let inputFieldWidth = resultWidth
let inputFieldHeight = self.inputFieldNode.updateLayout(width: inputFieldWidth, transition: transition)
let inputHeight = inputFieldHeight
transition.updateFrame(node: self.inputFieldNode, frame: CGRect(x: 0.0, y: origin.y, width: resultWidth, height: inputFieldHeight))
transition.updateAlpha(node: self.inputFieldNode, alpha: inputHeight > 0.0 ? 1.0 : 0.0)
let resultSize = CGSize(width: resultWidth, height: titleSize.height + textSize.height + actionsHeight + inputHeight + insets.top + insets.bottom)
transition.updateFrame(node: self.actionNodesSeparator, frame: CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight - UIScreenPixel), size: CGSize(width: resultSize.width, height: UIScreenPixel)))
var actionOffset: CGFloat = 0.0
let actionWidth: CGFloat = floor(resultSize.width / CGFloat(self.actionNodes.count))
var separatorIndex = -1
var nodeIndex = 0
for actionNode in self.actionNodes {
if separatorIndex >= 0 {
let separatorNode = self.actionVerticalSeparators[separatorIndex]
switch effectiveActionLayout {
case .horizontal:
transition.updateFrame(node: separatorNode, frame: CGRect(origin: CGPoint(x: actionOffset - UIScreenPixel, y: resultSize.height - actionsHeight), size: CGSize(width: UIScreenPixel, height: actionsHeight - UIScreenPixel)))
case .vertical:
transition.updateFrame(node: separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight + actionOffset - UIScreenPixel), size: CGSize(width: resultSize.width, height: UIScreenPixel)))
}
}
separatorIndex += 1
let currentActionWidth: CGFloat
switch effectiveActionLayout {
case .horizontal:
if nodeIndex == self.actionNodes.count - 1 {
currentActionWidth = resultSize.width - actionOffset
} else {
currentActionWidth = actionWidth
}
case .vertical:
currentActionWidth = resultSize.width
}
let actionNodeFrame: CGRect
switch effectiveActionLayout {
case .horizontal:
actionNodeFrame = CGRect(origin: CGPoint(x: actionOffset, y: resultSize.height - actionsHeight), size: CGSize(width: currentActionWidth, height: actionButtonHeight))
actionOffset += currentActionWidth
case .vertical:
actionNodeFrame = CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight + actionOffset), size: CGSize(width: currentActionWidth, height: actionButtonHeight))
actionOffset += actionButtonHeight
}
transition.updateFrame(node: actionNode, frame: actionNodeFrame)
nodeIndex += 1
}
if !hadValidLayout {
self.inputFieldNode.activateInput()
}
return resultSize
}
func animateError() {
self.inputFieldNode.layer.addShakeAnimation()
self.hapticFeedback.error()
}
}
private func commitChannelOwnershipTransferController(context: AccountContext, channel: TelegramChannel, member: TelegramUser, completion: @escaping () -> Void) -> ViewController {
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
var dismissImpl: (() -> Void)?
var proceedImpl: (() -> Void)?
let disposable = MetaDisposable()
let contentNode = ChannelOwnershipTransferAlertContentNode(theme: AlertControllerTheme(presentationTheme: presentationData.theme), ptheme: presentationData.theme, strings: presentationData.strings, actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {
dismissImpl?()
}), TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_Done, action: {
proceedImpl?()
})])
let controller = AlertController(theme: AlertControllerTheme(presentationTheme: presentationData.theme), contentNode: contentNode)
let presentationDataDisposable = context.sharedContext.presentationData.start(next: { [weak controller, weak contentNode] presentationData in
controller?.theme = AlertControllerTheme(presentationTheme: presentationData.theme)
contentNode?.inputFieldNode.updateTheme(presentationData.theme)
})
controller.dismissed = {
presentationDataDisposable.dispose()
disposable.dispose()
}
dismissImpl = { [weak controller, weak contentNode] in
contentNode?.dismissInput()
controller?.dismissAnimated()
}
proceedImpl = { [weak contentNode] in
guard let contentNode = contentNode else {
return
}
disposable.set((updateChannelOwnership(postbox: context.account.postbox, network: context.account.network, accountStateManager: context.account.stateManager, channelId: channel.id, memberId: member.id, password: contentNode.password) |> deliverOnMainQueue).start(error: { [weak contentNode] error in
contentNode?.animateError()
}, completed: {
dismissImpl?()
completion()
}))
}
return controller
}
private func confirmChannelOwnershipTransferController(context: AccountContext, channel: TelegramChannel, member: TelegramUser, present: @escaping (ViewController, Any?) -> Void, completion: @escaping () -> Void) -> ViewController {
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let theme = AlertControllerTheme(presentationTheme: presentationData.theme)
var title: String
var text: String
if case .group = channel.info {
title = presentationData.strings.Group_OwnershipTransfer_Title
text = presentationData.strings.Group_OwnershipTransfer_DescriptionInfo(channel.displayTitle, member.displayTitle).0
} else {
title = presentationData.strings.Channel_OwnershipTransfer_Title
text = presentationData.strings.Channel_OwnershipTransfer_DescriptionInfo(channel.displayTitle, member.displayTitle).0
}
let attributedTitle = NSAttributedString(string: title, font: Font.medium(17.0), textColor: theme.primaryColor, paragraphAlignment: .center)
let body = MarkdownAttributeSet(font: Font.regular(13.0), textColor: theme.primaryColor)
let bold = MarkdownAttributeSet(font: Font.semibold(13.0), textColor: theme.primaryColor)
let attributedText = parseMarkdownIntoAttributedString(text, attributes: MarkdownAttributes(body: body, bold: bold, link: body, linkAttribute: { _ in return nil }), textAlignment: .center)
var dismissImpl: (() -> Void)?
let controller = richTextAlertController(context: context, title: attributedTitle, text: attributedText, actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Channel_OwnershipTransfer_ChangeOwner, action: {
dismissImpl?()
present(commitChannelOwnershipTransferController(context: context, channel: channel, member: member, completion: completion), nil)
}), TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_Cancel, action: {
dismissImpl?()
})], actionLayout: .vertical)
dismissImpl = { [weak controller] in
controller?.dismissAnimated()
}
return controller
}
func channelOwnershipTransferController(context: AccountContext, channel: TelegramChannel, member: TelegramUser, initialError: ChannelOwnershipTransferError, present: @escaping (ViewController, Any?) -> Void, completion: @escaping () -> Void) -> ViewController {
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let theme = AlertControllerTheme(presentationTheme: presentationData.theme)
var title: NSAttributedString? = NSAttributedString(string: presentationData.strings.OwnershipTransfer_SecurityCheck, font: Font.medium(17.0), textColor: theme.primaryColor, paragraphAlignment: .center)
var text = presentationData.strings.OwnershipTransfer_SecurityRequirements
var dismissImpl: (() -> Void)?
var actions: [TextAlertAction] = []
switch initialError {
case .requestPassword:
return confirmChannelOwnershipTransferController(context: context, channel: channel, member: member, present: present, completion: completion)
case .twoStepAuthTooFresh, .authSessionTooFresh:
text = text + presentationData.strings.OwnershipTransfer_ComeBackLater
actions = [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {
dismissImpl?()
})]
case .twoStepAuthMissing:
actions = [TextAlertAction(type: .genericAction, title: presentationData.strings.OwnershipTransfer_SetupTwoStepAuth, action: {
let controller = SetupTwoStepVerificationController(context: context, initialState: .automatic, stateUpdated: { update, shouldDismiss, controller in
if shouldDismiss {
controller.dismiss()
}
})
present(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet))
}), TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_Cancel, action: {
dismissImpl?()
})]
default:
title = nil
text = presentationData.strings.Login_UnknownError
actions = [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {
dismissImpl?()
})]
}
let body = MarkdownAttributeSet(font: Font.regular(13.0), textColor: theme.primaryColor)
let bold = MarkdownAttributeSet(font: Font.semibold(13.0), textColor: theme.primaryColor)
let attributedText = parseMarkdownIntoAttributedString(text, attributes: MarkdownAttributes(body: body, bold: bold, link: body, linkAttribute: { _ in return nil }), textAlignment: .center)
let controller = richTextAlertController(context: context, title: title, text: attributedText, actions: actions)
dismissImpl = { [weak controller] in
controller?.dismissAnimated()
}
return controller
}

View File

@ -1449,7 +1449,7 @@ public final class ChatController: TelegramController, KeyShortcutResponder, Gal
if let strongSelf = self {
if let peer = peerViewMainPeer(peerView) {
strongSelf.chatTitleView?.titleContent = .peer(peerView: peerView, onlineMemberCount: onlineMemberCount)
(strongSelf.chatInfoNavigationButton?.buttonItem.customDisplayNode as? ChatAvatarNavigationNode)?.avatarNode.setPeer(account: strongSelf.context.account, theme: strongSelf.presentationData.theme, peer: peer)
(strongSelf.chatInfoNavigationButton?.buttonItem.customDisplayNode as? ChatAvatarNavigationNode)?.avatarNode.setPeer(account: strongSelf.context.account, theme: strongSelf.presentationData.theme, peer: peer, overrideImage: peer.isDeleted ? .deletedIcon : .none)
}
if strongSelf.peerView === peerView {
return

View File

@ -476,6 +476,8 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
var overrideImage: AvatarNodeImageOverride?
if peer.id == item.context.account.peerId {
overrideImage = .savedMessagesIcon
} else if peer.isDeleted {
overrideImage = .deletedIcon
}
self.avatarNode.setPeer(account: item.context.account, theme: item.presentationData.theme, peer: peer, overrideImage: overrideImage, emptyColor: item.presentationData.theme.list.mediaPlaceholderColor, synchronousLoad: synchronousLoads)
}

View File

@ -6,77 +6,6 @@ import Display
import Postbox
import TelegramCore
private final class ChatMessageActionUrlAuthContentActionNode: HighlightableButtonNode {
private let backgroundNode: ASDisplayNode
let action: TextAlertAction
init(theme: AlertControllerTheme, action: TextAlertAction) {
self.backgroundNode = ASDisplayNode()
self.backgroundNode.isLayerBacked = true
self.backgroundNode.alpha = 0.0
self.action = action
super.init()
self.titleNode.maximumNumberOfLines = 2
self.highligthedChanged = { [weak self] value in
if let strongSelf = self {
if value {
if strongSelf.backgroundNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.backgroundNode, at: 0)
}
strongSelf.backgroundNode.layer.removeAnimation(forKey: "opacity")
strongSelf.backgroundNode.alpha = 1.0
} else if !strongSelf.backgroundNode.alpha.isZero {
strongSelf.backgroundNode.alpha = 0.0
strongSelf.backgroundNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25)
}
}
}
self.updateTheme(theme)
}
func updateTheme(_ theme: AlertControllerTheme) {
self.backgroundNode.backgroundColor = theme.highlightedItemColor
var font = Font.regular(17.0)
var color = theme.accentColor
switch self.action.type {
case .defaultAction, .genericAction:
break
case .destructiveAction:
color = theme.destructiveColor
}
switch self.action.type {
case .defaultAction:
font = Font.semibold(17.0)
case .destructiveAction, .genericAction:
break
}
self.setAttributedTitle(NSAttributedString(string: self.action.title, font: font, textColor: color, paragraphAlignment: .center), for: [])
}
override func didLoad() {
super.didLoad()
self.addTarget(self, action: #selector(self.pressed), forControlEvents: .touchUpInside)
}
@objc func pressed() {
self.action.action()
}
override func layout() {
super.layout()
self.backgroundNode.frame = self.bounds
}
}
private let textFont = Font.regular(13.0)
private let boldTextFont = Font.semibold(13.0)
@ -99,7 +28,7 @@ private final class ChatMessageActionUrlAuthAlertContentNode: AlertContentNode {
private let allowWriteLabelNode: ASTextNode
private let actionNodesSeparator: ASDisplayNode
private let actionNodes: [ChatMessageActionUrlAuthContentActionNode]
private let actionNodes: [TextAlertContentActionNode]
private let actionVerticalSeparators: [ASDisplayNode]
private var validLayout: CGSize?
@ -154,8 +83,8 @@ private final class ChatMessageActionUrlAuthAlertContentNode: AlertContentNode {
self.actionNodesSeparator = ASDisplayNode()
self.actionNodesSeparator.isLayerBacked = true
self.actionNodes = actions.map { action -> ChatMessageActionUrlAuthContentActionNode in
return ChatMessageActionUrlAuthContentActionNode(theme: theme, action: action)
self.actionNodes = actions.map { action -> TextAlertContentActionNode in
return TextAlertContentActionNode(theme: theme, action: action)
}
var actionVerticalSeparators: [ASDisplayNode] = []

View File

@ -221,7 +221,7 @@ private enum ChatRecentActionsFilterEntry: ItemListNodeEntry {
let peerText: String
switch participant.participant {
case .creator:
peerText = strings.Channel_Management_LabelCreator
peerText = strings.Channel_Management_LabelOwner
case .member:
peerText = strings.ChatAdmins_AdminLabel.capitalized
}

View File

@ -18,7 +18,6 @@ private final class ChatTextLinkEditInputFieldNode: ASDisplayNode, ASEditableTex
private let backgroundInsets = UIEdgeInsets(top: 8.0, left: 16.0, bottom: 15.0, right: 16.0)
private let inputInsets = UIEdgeInsets(top: 5.0, left: 12.0, bottom: 5.0, right: 12.0)
private let accessoryButtonsWidth: CGFloat = 10.0
var text: String {
get {
@ -81,7 +80,6 @@ private final class ChatTextLinkEditInputFieldNode: ASDisplayNode, ASEditableTex
func updateLayout(width: CGFloat, transition: ContainedViewLayoutTransition) -> CGFloat {
let backgroundInsets = self.backgroundInsets
let inputInsets = self.inputInsets
let accessoryButtonsWidth = self.accessoryButtonsWidth
let textFieldHeight = self.calculateTextFieldMetrics(width: width)
let panelHeight = textFieldHeight + backgroundInsets.top + backgroundInsets.bottom
@ -92,7 +90,7 @@ private final class ChatTextLinkEditInputFieldNode: ASDisplayNode, ASEditableTex
let placeholderSize = self.placeholderNode.measure(backgroundFrame.size)
transition.updateFrame(node: self.placeholderNode, frame: CGRect(origin: CGPoint(x: backgroundFrame.minX + inputInsets.left, y: backgroundFrame.minY + floor((backgroundFrame.size.height - placeholderSize.height) / 2.0)), size: placeholderSize))
transition.updateFrame(node: self.textInputNode, frame: CGRect(origin: CGPoint(x: backgroundFrame.minX + inputInsets.left, y: backgroundFrame.minY), size: CGSize(width: backgroundFrame.size.width - inputInsets.left - inputInsets.right - accessoryButtonsWidth, height: backgroundFrame.size.height)))
transition.updateFrame(node: self.textInputNode, frame: CGRect(origin: CGPoint(x: backgroundFrame.minX + inputInsets.left, y: backgroundFrame.minY), size: CGSize(width: backgroundFrame.size.width - inputInsets.left - inputInsets.right, height: backgroundFrame.size.height)))
return panelHeight
}
@ -122,9 +120,8 @@ private final class ChatTextLinkEditInputFieldNode: ASDisplayNode, ASEditableTex
private func calculateTextFieldMetrics(width: CGFloat) -> CGFloat {
let backgroundInsets = self.backgroundInsets
let inputInsets = self.inputInsets
let accessoryButtonsWidth = self.accessoryButtonsWidth
let unboundTextFieldHeight = max(33.0, ceil(self.textInputNode.measure(CGSize(width: width - backgroundInsets.left - backgroundInsets.right - inputInsets.left - inputInsets.right - accessoryButtonsWidth, height: CGFloat.greatestFiniteMagnitude)).height))
let unboundTextFieldHeight = max(33.0, ceil(self.textInputNode.measure(CGSize(width: width - backgroundInsets.left - backgroundInsets.right - inputInsets.left - inputInsets.right, height: CGFloat.greatestFiniteMagnitude)).height))
return min(61.0, max(33.0, unboundTextFieldHeight))
}
@ -146,91 +143,6 @@ private final class ChatTextLinkEditInputFieldNode: ASDisplayNode, ASEditableTex
}
}
private final class ChatTextLinkEditContentActionNode: HighlightableButtonNode {
private var theme: AlertControllerTheme
let action: TextAlertAction
private let backgroundNode: ASDisplayNode
init(theme: AlertControllerTheme, action: TextAlertAction) {
self.theme = theme
self.action = action
self.backgroundNode = ASDisplayNode()
self.backgroundNode.isLayerBacked = true
self.backgroundNode.alpha = 0.0
super.init()
self.titleNode.maximumNumberOfLines = 2
self.highligthedChanged = { [weak self] value in
if let strongSelf = self {
if value {
if strongSelf.backgroundNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.backgroundNode, at: 0)
}
strongSelf.backgroundNode.layer.removeAnimation(forKey: "opacity")
strongSelf.backgroundNode.alpha = 1.0
} else if !strongSelf.backgroundNode.alpha.isZero {
strongSelf.backgroundNode.alpha = 0.0
strongSelf.backgroundNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25)
}
}
}
self.updateTheme(theme)
}
var actionEnabled: Bool = true {
didSet {
self.isUserInteractionEnabled = self.actionEnabled
self.updateTitle()
}
}
func updateTheme(_ theme: AlertControllerTheme) {
self.theme = theme
self.backgroundNode.backgroundColor = theme.highlightedItemColor
self.updateTitle()
}
private func updateTitle() {
var font = Font.regular(17.0)
var color: UIColor
switch self.action.type {
case .defaultAction, .genericAction:
color = self.actionEnabled ? self.theme.accentColor : self.theme.disabledColor
case .destructiveAction:
color = self.actionEnabled ? self.theme.destructiveColor : self.theme.disabledColor
}
switch self.action.type {
case .defaultAction:
font = Font.semibold(17.0)
case .destructiveAction, .genericAction:
break
}
self.setAttributedTitle(NSAttributedString(string: self.action.title, font: font, textColor: color, paragraphAlignment: .center), for: [])
}
override func didLoad() {
super.didLoad()
self.addTarget(self, action: #selector(self.pressed), forControlEvents: .touchUpInside)
}
@objc func pressed() {
self.action.action()
}
override func layout() {
super.layout()
self.backgroundNode.frame = self.bounds
}
}
private final class ChatTextLinkEditAlertContentNode: AlertContentNode {
private let strings: PresentationStrings
private let text: String
@ -240,7 +152,7 @@ private final class ChatTextLinkEditAlertContentNode: AlertContentNode {
let inputFieldNode: ChatTextLinkEditInputFieldNode
private let actionNodesSeparator: ASDisplayNode
private let actionNodes: [ChatTextLinkEditContentActionNode]
private let actionNodes: [TextAlertContentActionNode]
private let actionVerticalSeparators: [ASDisplayNode]
private let disposable = MetaDisposable()
@ -274,8 +186,8 @@ private final class ChatTextLinkEditAlertContentNode: AlertContentNode {
self.actionNodesSeparator = ASDisplayNode()
self.actionNodesSeparator.isLayerBacked = true
self.actionNodes = actions.map { action -> ChatTextLinkEditContentActionNode in
return ChatTextLinkEditContentActionNode(theme: theme, action: action)
self.actionNodes = actions.map { action -> TextAlertContentActionNode in
return TextAlertContentActionNode(theme: theme, action: action)
}
var actionVerticalSeparators: [ASDisplayNode] = []

View File

@ -44,7 +44,7 @@ extension ActionSheetController {
public extension AlertControllerTheme {
convenience init(presentationTheme: PresentationTheme) {
let actionSheet = presentationTheme.actionSheet
self.init(backgroundColor: actionSheet.opaqueItemBackgroundColor, separatorColor: actionSheet.opaqueItemSeparatorColor, highlightedItemColor: actionSheet.opaqueItemHighlightedBackgroundColor, primaryColor: actionSheet.primaryTextColor, secondaryColor: actionSheet.secondaryTextColor, accentColor: actionSheet.controlAccentColor, destructiveColor: actionSheet.destructiveActionTextColor, disabledColor: actionSheet.disabledActionTextColor)
self.init(backgroundType: actionSheet.backgroundType == .light ? .light : .dark, backgroundColor: actionSheet.itemBackgroundColor, separatorColor: actionSheet.itemHighlightedBackgroundColor, highlightedItemColor: actionSheet.itemHighlightedBackgroundColor, primaryColor: actionSheet.primaryTextColor, secondaryColor: actionSheet.secondaryTextColor, accentColor: actionSheet.controlAccentColor, destructiveColor: actionSheet.destructiveActionTextColor, disabledColor: actionSheet.disabledActionTextColor)
}
}

View File

@ -248,8 +248,8 @@ public class ContactsController: ViewController {
self.contactsNode.openPeopleNearby = { [weak self] in
if let strongSelf = self {
//let controller = peopleNearbyController(context: strongSelf.context)
//(strongSelf.navigationController as? NavigationController)?.pushViewController(controller)
let controller = peopleNearbyController(context: strongSelf.context)
(strongSelf.navigationController as? NavigationController)?.pushViewController(controller)
}
}

View File

@ -31,10 +31,9 @@ final class ContactsControllerNode: ASDisplayNode {
var addNearbyImpl: (() -> Void)?
var inviteImpl: (() -> Void)?
//ContactListAdditionalOption(title: presentationData.strings.Contacts_AddPeopleNearby, icon: .generic(UIImage(bundleImageName: "Contact List/PeopleNearbyIcon")!), action: {
// addNearbyImpl?()
//}),
let options = [ContactListAdditionalOption(title: presentationData.strings.Contacts_InviteFriends, icon: .generic(UIImage(bundleImageName: "Contact List/AddMemberIcon")!), action: {
let options = [ContactListAdditionalOption(title: presentationData.strings.Contacts_AddPeopleNearby, icon: .generic(UIImage(bundleImageName: "Contact List/PeopleNearbyIcon")!), action: {
addNearbyImpl?()
}), ContactListAdditionalOption(title: presentationData.strings.Contacts_InviteFriends, icon: .generic(UIImage(bundleImageName: "Contact List/AddMemberIcon")!), action: {
inviteImpl?()
})]

View File

@ -625,6 +625,8 @@ class ContactsPeerItemNode: ItemListRevealOptionsItemNode {
var overrideImage: AvatarNodeImageOverride?
if peer.id == item.account.peerId, case .generalSearch = item.peerMode {
overrideImage = .savedMessagesIcon
} else if peer.isDeleted {
overrideImage = .deletedIcon
}
strongSelf.avatarNode.setPeer(account: item.account, theme: item.theme, peer: peer, overrideImage: overrideImage, emptyColor: item.theme.list.mediaPlaceholderColor, synchronousLoad: synchronousLoads)
}

View File

@ -408,7 +408,7 @@ private func makeDefaultPresentationTheme(accentColor: UIColor, serviceBackgroun
dimColor: UIColor(white: 0.0, alpha: 0.4),
backgroundType: .light,
opaqueItemBackgroundColor: .white,
itemBackgroundColor: UIColor(white: 1.0, alpha: 0.8),
itemBackgroundColor: UIColor(white: 1.0, alpha: 0.87),
opaqueItemHighlightedBackgroundColor: UIColor(white: 0.9, alpha: 1.0),
itemHighlightedBackgroundColor: UIColor(white: 0.9, alpha: 0.7),
standardActionTextColor: accentColor,

View File

@ -59,7 +59,11 @@ private final class DeleteChatPeerActionSheetItemNode: ActionSheetItemNode {
if chatPeer.id == context.account.peerId {
self.avatarNode.setPeer(account: context.account, theme: (context.sharedContext.currentPresentationData.with { $0 }).theme, peer: peer, overrideImage: .savedMessagesIcon)
} else {
self.avatarNode.setPeer(account: context.account, theme: (context.sharedContext.currentPresentationData.with { $0 }).theme, peer: peer)
var overrideImage: AvatarNodeImageOverride?
if chatPeer.isDeleted {
overrideImage = .deletedIcon
}
self.avatarNode.setPeer(account: context.account, theme: (context.sharedContext.currentPresentationData.with { $0 }).theme, peer: peer, overrideImage: overrideImage)
}
let text: (String, [(Int, NSRange)])

View File

@ -1777,7 +1777,7 @@ public func groupInfoController(context: AccountContext, peerId originalPeerId:
presentControllerImpl?(channelAdminController(context: context, peerId: peerView.peerId, adminId: participant.peer.id, initialParticipant: participant.participant, updated: { _ in
}, upgradedToSupergroup: { upgradedPeerId, f in
upgradedToSupergroupImpl?(upgradedPeerId, f)
}), ViewControllerPresentationArguments(presentationAnimation: .modalSheet))
}, transferedOwnership: { _ in }), ViewControllerPresentationArguments(presentationAnimation: .modalSheet))
})
}, restrictPeer: { participant in
let _ = (peerView.get()

View File

@ -644,6 +644,8 @@ class ItemListAvatarAndNameInfoItemNode: ListViewItemNode, ItemListItemNode, Ite
}
} else if case .editSettings = item.mode {
overrideImage = AvatarNodeImageOverride.editAvatarIcon
} else if peer.isDeleted {
overrideImage = .deletedIcon
}
strongSelf.avatarNode.setPeer(account: item.account, theme: item.theme, peer: peer, overrideImage: overrideImage, emptyColor: ignoreEmpty ? nil : item.theme.list.mediaPlaceholderColor, synchronousLoad: synchronousLoads)

View File

@ -709,7 +709,11 @@ class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNode {
if item.peer.id == item.account.peerId, case .threatSelfAsSaved = item.aliasHandling {
strongSelf.avatarNode.setPeer(account: item.account, theme: item.theme, peer: item.peer, overrideImage: .savedMessagesIcon, emptyColor: item.theme.list.mediaPlaceholderColor, synchronousLoad: synchronousLoad)
} else {
strongSelf.avatarNode.setPeer(account: item.account, theme: item.theme, peer: item.peer, emptyColor: item.theme.list.mediaPlaceholderColor, synchronousLoad: synchronousLoad)
var overrideImage: AvatarNodeImageOverride?
if item.peer.isDeleted {
overrideImage = .deletedIcon
}
strongSelf.avatarNode.setPeer(account: item.account, theme: item.theme, peer: item.peer, overrideImage: overrideImage, emptyColor: item.theme.list.mediaPlaceholderColor, synchronousLoad: synchronousLoad)
}
strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: params.width, height: 50.0 + UIScreenPixel + UIScreenPixel))

View File

@ -0,0 +1,328 @@
import Foundation
import UIKit
import Display
import SwiftSignalKit
import Postbox
import TelegramCore
import MapKit
private struct PeerNearbyEntry {
let peer: Peer
let expires: Int32
let distance: Int32
}
private func arePeersNearbyEqual(_ lhs: PeerNearbyEntry?, _ rhs: PeerNearbyEntry?) -> Bool {
if let lhs = lhs, let rhs = rhs {
return lhs.peer.isEqual(rhs.peer) && lhs.expires == rhs.expires && lhs.distance == rhs.distance
} else {
return (lhs != nil) == (rhs != nil)
}
}
private func arePeerNearbyArraysEqual(_ lhs: [PeerNearbyEntry], _ rhs: [PeerNearbyEntry]) -> Bool {
if lhs.count != rhs.count {
return false
}
for i in 0 ..< lhs.count {
if !lhs[i].peer.isEqual(rhs[i].peer) || lhs[i].expires != rhs[i].expires || lhs[i].distance != rhs[i].distance {
return false
}
}
return true
}
private final class PeopleNearbyControllerArguments {
let context: AccountContext
let openChat: (Peer) -> Void
let openCreateGroup: () -> Void
init(context: AccountContext, openChat: @escaping (Peer) -> Void, openCreateGroup: @escaping () -> Void) {
self.context = context
self.openChat = openChat
self.openCreateGroup = openCreateGroup
}
}
private enum PeopleNearbySection: Int32 {
case header
case users
case groups
case channels
}
private enum PeopleNearbyEntry: ItemListNodeEntry {
case header(PresentationTheme, String)
case usersHeader(PresentationTheme, String)
case empty(PresentationTheme, String)
case user(Int32, PresentationTheme, PresentationStrings, PresentationDateTimeFormat, PresentationPersonNameOrder, PeerNearbyEntry)
case groupsHeader(PresentationTheme, String)
case createGroup(PresentationTheme, String)
case group(Int32, PresentationTheme, PresentationStrings, PresentationDateTimeFormat, PresentationPersonNameOrder, PeerNearbyEntry)
case channelsHeader(PresentationTheme, String)
case channel(Int32, PresentationTheme, PresentationStrings, PresentationDateTimeFormat, PresentationPersonNameOrder, PeerNearbyEntry)
var section: ItemListSectionId {
switch self {
case .header:
return PeopleNearbySection.header.rawValue
case .usersHeader, .empty, .user:
return PeopleNearbySection.users.rawValue
case .groupsHeader, .createGroup, .group:
return PeopleNearbySection.groups.rawValue
case .channelsHeader, .channel:
return PeopleNearbySection.channels.rawValue
}
}
var stableId: Int32 {
switch self {
case .header:
return 0
case .usersHeader:
return 1
case .empty:
return 2
case let .user(index, _, _, _, _, _):
return 3 + index
case .groupsHeader:
return 1000
case .createGroup:
return 1001
case let .group(index, _, _, _, _, _):
return 1002 + index
case .channelsHeader:
return 2000
case let .channel(index, _, _, _, _, _):
return 2001 + index
}
}
static func ==(lhs: PeopleNearbyEntry, rhs: PeopleNearbyEntry) -> Bool {
switch lhs {
case let .header(lhsTheme, lhsText):
if case let .header(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
case let .usersHeader(lhsTheme, lhsText):
if case let .usersHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
case let .empty(lhsTheme, lhsText):
if case let .empty(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
case let .user(lhsIndex, lhsTheme, lhsStrings, lhsDateTimeFormat, lhsDisplayOrder, lhsPeer):
if case let .user(rhsIndex, rhsTheme, rhsStrings, rhsDateTimeFormat, rhsDisplayOrder, rhsPeer) = rhs, lhsIndex == rhsIndex, lhsTheme === rhsTheme, lhsStrings === rhsStrings, lhsDateTimeFormat == rhsDateTimeFormat, lhsDisplayOrder == rhsDisplayOrder, arePeersNearbyEqual(lhsPeer, rhsPeer) {
return true
} else {
return false
}
case let .groupsHeader(lhsTheme, lhsText):
if case let .groupsHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
case let .createGroup(lhsTheme, lhsText):
if case let .createGroup(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
case let .group(lhsIndex, lhsTheme, lhsStrings, lhsDateTimeFormat, lhsDisplayOrder, lhsPeer):
if case let .group(rhsIndex, rhsTheme, rhsStrings, rhsDateTimeFormat, rhsDisplayOrder, rhsPeer) = rhs, lhsIndex == rhsIndex, lhsTheme === rhsTheme, lhsStrings === rhsStrings, lhsDateTimeFormat == rhsDateTimeFormat, lhsDisplayOrder == rhsDisplayOrder, arePeersNearbyEqual(lhsPeer, rhsPeer) {
return true
} else {
return false
}
case let .channelsHeader(lhsTheme, lhsText):
if case let .channelsHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
case let .channel(lhsIndex, lhsTheme, lhsStrings, lhsDateTimeFormat, lhsDisplayOrder, lhsPeer):
if case let .channel(rhsIndex, rhsTheme, rhsStrings, rhsDateTimeFormat, rhsDisplayOrder, rhsPeer) = rhs, lhsIndex == rhsIndex, lhsTheme === rhsTheme, lhsStrings === rhsStrings, lhsDateTimeFormat == rhsDateTimeFormat, lhsDisplayOrder == rhsDisplayOrder, arePeersNearbyEqual(lhsPeer, rhsPeer) {
return true
} else {
return false
}
}
}
static func <(lhs: PeopleNearbyEntry, rhs: PeopleNearbyEntry) -> Bool {
return lhs.stableId < rhs.stableId
}
func item(_ arguments: PeopleNearbyControllerArguments) -> ListViewItem {
switch self {
case let .header(theme, text):
return PeopleNearbyHeaderItem(theme: theme, text: text, sectionId: self.section)
case let .usersHeader(theme, text):
return ItemListSectionHeaderItem(theme: theme, text: text, sectionId: self.section)
case let .empty(theme, text):
return ItemListPlaceholderItem(theme: theme, text: text, sectionId: self.section, style: .blocks)
case let .user(_, theme, strings, dateTimeFormat, nameDisplayOrder, peer):
func distance(_ distance: Int32) -> String {
let formatter = MKDistanceFormatter()
formatter.unitStyle = .abbreviated
return formatter.string(fromDistance: Double(distance))
}
return ItemListPeerItem(theme: theme, strings: strings, dateTimeFormat: dateTimeFormat, nameDisplayOrder: nameDisplayOrder, account: arguments.context.account, peer: peer.peer, aliasHandling: .standard, nameColor: .primary, nameStyle: .distinctBold, presence: nil, text: .text(distance(peer.distance)), label: .none, editing: ItemListPeerItemEditing(editable: false, editing: false, revealed: false), revealOptions: nil, switchValue: nil, enabled: true, selectable: true, sectionId: self.section, action: {
arguments.openChat(peer.peer)
}, setPeerIdWithRevealedOptions: { _, _ in }, removePeer: { _ in }, toggleUpdated: nil, hasTopStripe: false, hasTopGroupInset: false, tag: nil)
case let .groupsHeader(theme, text):
return ItemListSectionHeaderItem(theme: theme, text: text, sectionId: self.section)
case let .createGroup(theme, title):
return ContactListActionItem(theme: theme, title: title, icon: .generic(UIImage(bundleImageName: "Contact List/CreateGroupActionIcon")!), header: nil, action: {
arguments.openCreateGroup()
})
case let .group(_, theme, strings, dateTimeFormat, nameDisplayOrder, peer):
return ItemListPeerItem(theme: theme, strings: strings, dateTimeFormat: dateTimeFormat, nameDisplayOrder: nameDisplayOrder, account: arguments.context.account, peer: peer.peer, aliasHandling: .standard, nameColor: .primary, nameStyle: .distinctBold, presence: nil, text: .text("10 members"), label: .none, editing: ItemListPeerItemEditing(editable: false, editing: false, revealed: false), revealOptions: nil, switchValue: nil, enabled: true, selectable: true, sectionId: self.section, action: {
arguments.openChat(peer.peer)
}, setPeerIdWithRevealedOptions: { _, _ in }, removePeer: { _ in }, toggleUpdated: nil, hasTopStripe: false, hasTopGroupInset: false, tag: nil)
case let .channelsHeader(theme, text):
return ItemListSectionHeaderItem(theme: theme, text: text, sectionId: self.section)
case let .channel(_, theme, strings, dateTimeFormat, nameDisplayOrder, peer):
return ItemListPeerItem(theme: theme, strings: strings, dateTimeFormat: dateTimeFormat, nameDisplayOrder: nameDisplayOrder, account: arguments.context.account, peer: peer.peer, aliasHandling: .standard, nameColor: .primary, nameStyle: .distinctBold, presence: nil, text: .text("10 members"), label: .none, editing: ItemListPeerItemEditing(editable: false, editing: false, revealed: false), revealOptions: nil, switchValue: nil, enabled: true, selectable: true, sectionId: self.section, action: {
arguments.openChat(peer.peer)
}, setPeerIdWithRevealedOptions: { _, _ in }, removePeer: { _ in }, toggleUpdated: nil, hasTopStripe: false, hasTopGroupInset: false, tag: nil)
}
}
}
private struct PeopleNearbyControllerState: Equatable {
static func ==(lhs: PeopleNearbyControllerState, rhs: PeopleNearbyControllerState) -> Bool {
return true
}
}
private struct PeopleNearbyData: Equatable {
let users: [PeerNearbyEntry]
let groups: [PeerNearbyEntry]
let channels: [PeerNearbyEntry]
init(users: [PeerNearbyEntry], groups: [PeerNearbyEntry], channels: [PeerNearbyEntry]) {
self.users = users
self.groups = groups
self.channels = channels
}
static func ==(lhs: PeopleNearbyData, rhs: PeopleNearbyData) -> Bool {
return arePeerNearbyArraysEqual(lhs.users, rhs.users) && arePeerNearbyArraysEqual(lhs.groups, rhs.groups) && arePeerNearbyArraysEqual(lhs.channels, rhs.channels)
}
}
private func peopleNearbyControllerEntries(state: PeopleNearbyControllerState, data: PeopleNearbyData?, presentationData: PresentationData) -> [PeopleNearbyEntry] {
var entries: [PeopleNearbyEntry] = []
entries.append(.header(presentationData.theme, presentationData.strings.PeopleNearby_Description))
entries.append(.usersHeader(presentationData.theme, presentationData.strings.PeopleNearby_Users.uppercased()))
if let data = data, !data.users.isEmpty {
var i: Int32 = 0
for user in data.users {
entries.append(.user(i, presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, presentationData.nameDisplayOrder, user))
i += 1
}
} else {
entries.append(.empty(presentationData.theme, presentationData.strings.PeopleNearby_UsersEmpty))
}
entries.append(.groupsHeader(presentationData.theme, presentationData.strings.PeopleNearby_Groups.uppercased()))
entries.append(.createGroup(presentationData.theme, presentationData.strings.PeopleNearby_CreateGroup))
if let data = data, !data.groups.isEmpty {
var i: Int32 = 0
for group in data.groups {
entries.append(.group(i, presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, presentationData.nameDisplayOrder, group))
i += 1
}
}
if let data = data, !data.channels.isEmpty {
var i: Int32 = 0
for channel in data.channels {
entries.append(.channel(i, presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, presentationData.nameDisplayOrder, channel))
i += 1
}
}
return entries
}
public func peopleNearbyController(context: AccountContext) -> ViewController {
let statePromise = ValuePromise(PeopleNearbyControllerState(), ignoreRepeated: true)
let stateValue = Atomic(value: PeopleNearbyControllerState())
let updateState: ((PeopleNearbyControllerState) -> PeopleNearbyControllerState) -> Void = { f in
statePromise.set(stateValue.modify { f($0) })
}
var presentControllerImpl: ((ViewController, ViewControllerPresentationArguments?) -> Void)?
var navigateToChatImpl: ((Peer) -> Void)?
let actionsDisposable = DisposableSet()
let dataPromise = Promise<PeopleNearbyData?>(nil)
let arguments = PeopleNearbyControllerArguments(context: context, openChat: { peer in
navigateToChatImpl?(peer)
}, openCreateGroup: {
})
let dataSignal: Signal<PeopleNearbyData?, NoError> = currentLocationManagerCoordinate(manager: context.sharedContext.locationManager!, timeout: 5.0)
|> mapToSignal { coordinate -> Signal<PeopleNearbyData?, NoError> in
guard let coordinate = coordinate else {
return .single(nil)
}
return peersNearby(network: context.account.network, accountStateManager: context.account.stateManager, coordinate: (latitude: coordinate.latitude, longitude: coordinate.longitude), radius: 200)
|> mapToSignal { peersNearby -> Signal<PeopleNearbyData?, NoError> in
return context.account.postbox.transaction { transaction -> PeopleNearbyData? in
var result: [PeerNearbyEntry] = []
for peerNearby in peersNearby {
if peerNearby.id != context.account.peerId, let peer = transaction.getPeer(peerNearby.id) {
result.append(PeerNearbyEntry(peer: peer, expires: peerNearby.expires, distance: peerNearby.distance))
}
}
return PeopleNearbyData(users: result, groups: [], channels: [])
}
}
}
dataPromise.set(dataSignal)
let signal = combineLatest(context.sharedContext.presentationData, statePromise.get(), dataPromise.get())
|> deliverOnMainQueue
|> map { presentationData, state, data -> (ItemListControllerState, (ItemListNodeState<PeopleNearbyEntry>, PeopleNearbyEntry.ItemGenerationArguments)) in
let controllerState = ItemListControllerState(theme: presentationData.theme, title: .text(presentationData.strings.PeopleNearby_Title), leftNavigationButton: nil, rightNavigationButton: nil, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: true)
let listState = ItemListNodeState(entries: peopleNearbyControllerEntries(state: state, data: data, presentationData: presentationData), style: .blocks, emptyStateItem: nil, crossfadeState: false, animateChanges: true, userInteractionEnabled: true)
return (controllerState, (listState, arguments))
} |> afterDisposed {
actionsDisposable.dispose()
}
let controller = ItemListController(context: context, state: signal)
navigateToChatImpl = { [weak controller] peer in
if let navigationController = controller?.navigationController as? NavigationController {
navigateToChatController(navigationController: navigationController, context: context, chatLocation: .peer(peer.id))
}
}
presentControllerImpl = { [weak controller] c, p in
if let controller = controller {
controller.present(c, in: .window(.root), with: p)
}
}
return controller
}

View File

@ -0,0 +1,123 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import SwiftSignalKit
class PeopleNearbyHeaderItem: ListViewItem, ItemListItem {
let theme: PresentationTheme
let text: String
let sectionId: ItemListSectionId
init(theme: PresentationTheme, text: String, sectionId: ItemListSectionId) {
self.theme = theme
self.text = text
self.sectionId = sectionId
}
func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) {
async {
let node = PeopleNearbyHeaderItemNode()
let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
node.contentSize = layout.contentSize
node.insets = layout.insets
Queue.mainQueue().async {
completion(node, {
return (nil, { _ in apply() })
})
}
}
}
func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) {
Queue.mainQueue().async {
guard let nodeValue = node() as? PeopleNearbyHeaderItemNode else {
assertionFailure()
return
}
let makeLayout = nodeValue.asyncLayout()
async {
let (layout, apply) = makeLayout(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
Queue.mainQueue().async {
completion(layout, { _ in
apply()
})
}
}
}
}
}
private let titleFont = Font.regular(13.0)
class PeopleNearbyHeaderItemNode: ListViewItemNode {
private let titleNode: TextNode
private var iconNode: PeopleNearbyIconNode?
private var item: PeopleNearbyHeaderItem?
init() {
self.titleNode = TextNode()
self.titleNode.isUserInteractionEnabled = false
self.titleNode.contentMode = .left
self.titleNode.contentsScale = UIScreen.main.scale
super.init(layerBacked: false, dynamicBounce: false)
self.addSubnode(self.titleNode)
}
func asyncLayout() -> (_ item: PeopleNearbyHeaderItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) {
let makeTitleLayout = TextNode.asyncLayout(self.titleNode)
return { item, params, neighbors in
let leftInset: CGFloat = 54.0 + params.leftInset
let topInset: CGFloat = 92.0
let attributedText = NSAttributedString(string: item.text, font: titleFont, textColor: item.theme.list.freeTextColor)
let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: attributedText, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: params.width - params.rightInset - leftInset * 2.0, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets()))
let contentSize: CGSize
contentSize = CGSize(width: params.width, height: topInset + titleLayout.size.height)
let insets = itemListNeighborsGroupedInsets(neighbors)
let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets)
return (layout, { [weak self] in
if let strongSelf = self {
strongSelf.item = item
strongSelf.accessibilityLabel = attributedText.string
let iconNode: PeopleNearbyIconNode
if let node = strongSelf.iconNode {
iconNode = node
iconNode.updateTheme(item.theme)
} else {
iconNode = PeopleNearbyIconNode(theme: item.theme)
strongSelf.iconNode = iconNode
strongSelf.addSubnode(iconNode)
}
let iconSize = CGSize(width: 60.0, height: 60.0)
iconNode.frame = CGRect(origin: CGPoint(x: floor((layout.size.width - iconSize.width) / 2.0), y: 5.0), size: iconSize)
let _ = titleApply()
strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: floor((layout.size.width - titleLayout.size.width) / 2.0), y: topInset), size: titleLayout.size)
}
})
}
}
override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) {
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4)
}
override func animateRemoved(_ currentTimestamp: Double, duration: Double) {
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false)
}
}

View File

@ -0,0 +1,204 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import LegacyComponents
private final class PeopleNearbyIconWavesNodeParams: NSObject {
let color: UIColor
let progress: CGFloat
init(color: UIColor, progress: CGFloat) {
self.color = color
self.progress = progress
super.init()
}
}
private func degToRad(_ degrees: CGFloat) -> CGFloat {
return degrees * CGFloat.pi / 180.0
}
final class PeopleNearbyIconWavesNode: ASDisplayNode {
var color: UIColor {
didSet {
self.setNeedsDisplay()
}
}
private var effectiveProgress: CGFloat = 0.0 {
didSet {
self.setNeedsDisplay()
}
}
init(color: UIColor) {
self.color = color
super.init()
self.isLayerBacked = true
self.isOpaque = false
}
override func willEnterHierarchy() {
super.willEnterHierarchy()
self.pop_removeAnimation(forKey: "indefiniteProgress")
let animation = POPBasicAnimation()
animation.property = (POPAnimatableProperty.property(withName: "progress", initializer: { property in
property?.readBlock = { node, values in
values?.pointee = (node as! PeopleNearbyIconWavesNode).effectiveProgress
}
property?.writeBlock = { node, values in
(node as! PeopleNearbyIconWavesNode).effectiveProgress = values!.pointee
}
property?.threshold = 0.01
}) as! POPAnimatableProperty)
animation.fromValue = CGFloat(0.0) as NSNumber
animation.toValue = CGFloat(1.0) as NSNumber
animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionLinear)
animation.duration = 3.5
animation.repeatForever = true
self.pop_add(animation, forKey: "indefiniteProgress")
}
override func didExitHierarchy() {
super.didExitHierarchy()
self.pop_removeAnimation(forKey: "indefiniteProgress")
}
override func drawParameters(forAsyncLayer layer: _ASDisplayLayer) -> NSObjectProtocol? {
let t = CACurrentMediaTime()
let value: CGFloat = CGFloat(t.truncatingRemainder(dividingBy: 2.0)) / 2.0
return PeopleNearbyIconWavesNodeParams(color: self.color, progress: value)
}
@objc override class func draw(_ bounds: CGRect, withParameters parameters: Any?, isCancelled: () -> Bool, isRasterizing: Bool) {
let context = UIGraphicsGetCurrentContext()!
if !isRasterizing {
context.setBlendMode(.copy)
context.setFillColor(UIColor.clear.cgColor)
context.fill(bounds)
}
if let parameters = parameters as? PeopleNearbyIconWavesNodeParams {
let center = CGPoint(x: bounds.width / 2.0, y: bounds.height / 2.0)
let radius: CGFloat = bounds.width * 0.3333
let range: CGFloat = (bounds.width - radius * 2.0) / 2.0
context.setFillColor(parameters.color.cgColor)
let draw: (CGContext, CGFloat) -> Void = { context, pos in
let path = CGMutablePath()
let pathRadius: CGFloat = bounds.width * 0.3333 + range * pos
path.addEllipse(in: CGRect(x: center.x - pathRadius, y: center.y - pathRadius, width: pathRadius * 2.0, height: pathRadius * 2.0))
let strokedPath = path.copy(strokingWithWidth: 1.0, lineCap: .round, lineJoin: .miter, miterLimit: 10.0)
context.addPath(strokedPath)
context.fillPath()
}
let position = parameters.progress
var alpha = position / 0.5
if alpha > 1.0 {
alpha = 2.0 - alpha
}
context.setAlpha(alpha * 0.7)
draw(context, position)
var progress = parameters.progress + 0.3333
if progress > 1.0 {
progress = progress - 1.0
}
var largerPos = progress
var largerAlpha = largerPos / 0.5
if largerAlpha > 1.0 {
largerAlpha = 2.0 - largerAlpha
}
context.setAlpha(largerAlpha * 0.7)
draw(context, largerPos)
progress = parameters.progress + 0.6666
if progress > 1.0 {
progress = progress - 1.0
}
largerPos = progress
largerAlpha = largerPos / 0.5
if largerAlpha > 1.0 {
largerAlpha = 2.0 - largerAlpha
}
context.setAlpha(largerAlpha * 0.7)
draw(context, largerPos)
}
}
}
private func generateIcon(size: CGSize, color: UIColor, contentColor: UIColor) -> UIImage {
return generateImage(size, rotatedContext: { size, context in
let bounds = CGRect(origin: CGPoint(), size: size)
context.clear(bounds)
context.setFillColor(color.cgColor)
context.fillEllipse(in: bounds)
context.translateBy(x: size.width / 2.0, y: size.height / 2.0)
context.scaleBy(x: size.width / 120.0, y: size.height / 120.0)
context.translateBy(x: -size.width / 2.0, y: -size.height / 2.0)
context.translateBy(x: 0.0, y: 6.0)
context.setFillColor(contentColor.cgColor)
let _ = try? drawSvgPath(context, path: "M27.8628211,52.2347452 L27.8628211,27.1373017 L2.76505663,27.1373017 C1.55217431,27.1373017 0.568938916,26.1540663 0.568938916,24.941184 C0.568938916,24.0832172 1.06857435,23.3038117 1.84819149,22.9456161 L51.2643819,0.241311309 C52.586928,-0.366333451 54.1516568,0.213208572 54.7593016,1.53575465 C55.0801868,2.23416513 55.080181,3.03785964 54.7592857,3.7362655 L32.0544935,53.1516391 C31.548107,54.2537536 30.2441593,54.7366865 29.1420449,54.2302999 C28.3624433,53.8720978 27.8628211,53.0927006 27.8628211,52.2347452 Z ")
})!
}
final class PeopleNearbyIconNode: ASDisplayNode {
private var theme: PresentationTheme
private var iconNode: ASImageNode
private var wavesNode: PeopleNearbyIconWavesNode
init(theme: PresentationTheme) {
self.theme = theme
self.iconNode = ASImageNode()
self.iconNode.isOpaque = false
self.wavesNode = PeopleNearbyIconWavesNode(color: theme.list.itemAccentColor)
super.init()
self.addSubnode(self.iconNode)
self.addSubnode(self.wavesNode)
}
func updateTheme(_ theme: PresentationTheme) {
guard self.theme !== theme else {
return
}
self.theme = theme
self.iconNode.image = generateIcon(size: self.bounds.size, color: self.theme.list.itemAccentColor, contentColor: self.theme.list.itemCheckColors.foregroundColor)
self.wavesNode.color = theme.list.itemAccentColor
}
override func layout() {
super.layout()
if let image = self.iconNode.image, image.size.width == self.bounds.width {
} else {
self.iconNode.image = generateIcon(size: self.bounds.size, color: self.theme.list.itemAccentColor, contentColor: self.theme.list.itemCheckColors.foregroundColor)
}
self.iconNode.frame = self.bounds
self.wavesNode.frame = self.bounds.insetBy(dx: -self.bounds.width * 0.3, dy: -self.bounds.height * 0.3)
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@ -128,6 +128,9 @@ final class SelectablePeerNode: ASDisplayNode {
overrideImage = .savedMessagesIcon
} else {
text = mainPeer.compactDisplayTitle
if mainPeer.isDeleted {
overrideImage = .deletedIcon
}
}
self.textNode.maximumNumberOfLines = UInt(numberOfLines)
self.textNode.attributedText = NSAttributedString(string: text, font: textFont, textColor: self.currentSelected ? self.theme.selectedTextColor : defaultColor, paragraphAlignment: .center)

View File

@ -16,3 +16,28 @@ public func textAlertController(context: AccountContext, title: String?, text: S
return controller
}
public func richTextAlertController(context: AccountContext, title: NSAttributedString?, text: NSAttributedString, actions: [TextAlertAction], actionLayout: TextAlertContentActionLayout = .horizontal) -> AlertController {
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let theme = AlertControllerTheme(presentationTheme: presentationData.theme)
var dismissImpl: (() -> Void)?
let controller = AlertController(theme: theme, contentNode: TextAlertContentNode(theme: theme, title: title, text: text, actions: actions.map { action in
return TextAlertAction(type: action.type, title: action.title, action: {
dismissImpl?()
action.action()
})
}, actionLayout: actionLayout))
dismissImpl = { [weak controller] in
controller?.dismissAnimated()
}
let presentationDataDisposable = context.sharedContext.presentationData.start(next: { [weak controller] presentationData in
controller?.theme = AlertControllerTheme(presentationTheme: presentationData.theme)
})
controller.dismissed = {
presentationDataDisposable.dispose()
}
return controller
}

View File

@ -9,6 +9,7 @@ public enum UndoOverlayContent {
case archivedChat(peerId: PeerId, title: String, text: String, undo: Bool)
case hidArchive(title: String, text: String, undo: Bool)
case revealedArchive(title: String, text: String, undo: Bool)
case succeed(text: String)
}
public final class UndoOverlayController: ViewController {

View File

@ -70,6 +70,7 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode {
self.iconNode?.displaysAsynchronously = false
self.iconNode?.image = UIImage(bundleImageName: "Chat List/ArchivedUndoIcon")
self.iconCheckNode = RadialStatusNode(backgroundNodeColor: .clear)
self.iconCheckNode?.frame = CGRect(x: 0.0, y: 0.0, width: 24.0, height: 24.0)
self.animationNode = nil
} else {
self.iconNode = nil
@ -82,20 +83,32 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode {
self.originalRemainingSeconds = 5
case let .hidArchive(title, text, undo):
self.iconNode = nil
self.animationNode = AnimationNode(animation: "anim_archiveswipe", colors: ["info1.info1.stroke": self.animationBackgroundColor, "info2.info2.Fill": self.animationBackgroundColor], scale: 1.0)
self.iconCheckNode = nil
self.animationNode = AnimationNode(animation: "anim_archiveswipe", colors: ["info1.info1.stroke": self.animationBackgroundColor, "info2.info2.Fill": self.animationBackgroundColor], scale: 1.0)
self.titleNode.attributedText = NSAttributedString(string: title, font: Font.semibold(14.0), textColor: .white)
self.textNode.attributedText = NSAttributedString(string: text, font: Font.regular(14.0), textColor: .white)
displayUndo = undo
self.originalRemainingSeconds = 3
case let .revealedArchive(title, text, undo):
self.iconNode = nil
self.animationNode = AnimationNode(animation: "anim_infotip", colors: ["info1.info1.stroke": self.animationBackgroundColor, "info2.info2.Fill": self.animationBackgroundColor], scale: 1.0)
self.iconCheckNode = nil
self.animationNode = AnimationNode(animation: "anim_infotip", colors: ["info1.info1.stroke": self.animationBackgroundColor, "info2.info2.Fill": self.animationBackgroundColor], scale: 1.0)
self.titleNode.attributedText = NSAttributedString(string: title, font: Font.semibold(14.0), textColor: .white)
self.textNode.attributedText = NSAttributedString(string: text, font: Font.regular(14.0), textColor: .white)
displayUndo = undo
self.originalRemainingSeconds = 3
case let .succeed(text):
self.iconNode = nil
self.iconCheckNode = nil
self.animationNode = AnimationNode(animation: "anim_success", colors: ["info1.info1.stroke": self.animationBackgroundColor, "info2.info2.Fill": self.animationBackgroundColor], scale: 1.0)
let body = MarkdownAttributeSet(font: Font.regular(14.0), textColor: .white)
let bold = MarkdownAttributeSet(font: Font.semibold(14.0), textColor: .white)
let attributedText = parseMarkdownIntoAttributedString(text, attributes: MarkdownAttributes(body: body, bold: bold, link: body, linkAttribute: { _ in return nil }), textAlignment: .natural)
self.textNode.attributedText = attributedText
self.textNode.maximumNumberOfLines = 2
displayUndo = false
self.originalRemainingSeconds = 3
}
self.remainingSeconds = self.originalRemainingSeconds
@ -127,7 +140,7 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode {
case .removedChat:
self.panelWrapperNode.addSubnode(self.timerTextNode)
self.panelWrapperNode.addSubnode(self.statusNode)
case .archivedChat, .hidArchive, .revealedArchive:
case .archivedChat, .hidArchive, .revealedArchive, .succeed:
break
}
self.iconNode.flatMap(self.panelWrapperNode.addSubnode)
@ -266,8 +279,12 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode {
transition.updateFrame(node: iconNode, frame: iconFrame)
if let iconCheckNode = self.iconCheckNode {
let statusSize: CGFloat = 24.0
transition.updateFrame(node: iconCheckNode, frame: CGRect(origin: CGPoint(x: iconFrame.minX + floor((iconFrame.width - statusSize) / 2.0), y: iconFrame.minY + floor((iconFrame.height - statusSize) / 2.0) + 3.0), size: CGSize(width: statusSize, height: statusSize)))
let statusSize: CGFloat = iconCheckNode.frame.width
var offset: CGFloat = 0.0
if statusSize < 30.0 {
offset = 3.0
}
transition.updateFrame(node: iconCheckNode, frame: CGRect(origin: CGPoint(x: iconFrame.minX + floor((iconFrame.width - statusSize) / 2.0), y: iconFrame.minY + floor((iconFrame.height - statusSize) / 2.0) + offset), size: CGSize(width: statusSize, height: statusSize)))
}
}