no message

This commit is contained in:
Peter 2017-06-06 13:13:26 +03:00
parent d55e3da7b3
commit b8230a4fdb
397 changed files with 28757 additions and 6756 deletions

3
.gitmodules vendored Normal file
View File

@ -0,0 +1,3 @@
[submodule "submodules/libtgvoip"]
path = submodules/libtgvoip
url = https://bitbucket.org/grishka/libtgvoip.git

View File

@ -0,0 +1,9 @@
{
"info" : {
"version" : 1,
"author" : "xcode"
},
"properties" : {
"provides-namespace" : true
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 598 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 932 B

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 556 B

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 618 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1022 B

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 503 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 782 B

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 535 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 805 B

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 545 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 844 B

View File

@ -0,0 +1,22 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"filename" : "CallPhoneIcon@2x.png",
"scale" : "2x"
},
{
"idiom" : "universal",
"filename" : "CallPhoneIcon@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" : "CallSpeakerIcon@2x.png",
"scale" : "2x"
},
{
"idiom" : "universal",
"filename" : "CallSpeakerIcon@3x.png",
"scale" : "3x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

View File

@ -0,0 +1,9 @@
{
"info" : {
"version" : 1,
"author" : "xcode"
},
"properties" : {
"provides-namespace" : true
}
}

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1019 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 747 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1019 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -0,0 +1,9 @@
{
"info" : {
"version" : 1,
"author" : "xcode"
},
"properties" : {
"provides-namespace" : true
}
}

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 510 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@ -0,0 +1,9 @@
{
"info" : {
"version" : 1,
"author" : "xcode"
},
"properties" : {
"provides-namespace" : true
}
}

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 251 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 320 B

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 230 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 319 B

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 162 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 174 B

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 162 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 153 B

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 593 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 874 B

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "0820"
LastUpgradeVersion = "0900"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
@ -14,7 +14,7 @@
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "D0FC407E1D5B8E7400261D9D"
BlueprintIdentifier = "D0EC6CA41EB9F4CC00EBF1C3"
BuildableName = "TelegramUI.framework"
BlueprintName = "TelegramUI"
ReferencedContainer = "container:TelegramUI.xcodeproj">
@ -45,7 +45,7 @@
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "D0FC407E1D5B8E7400261D9D"
BlueprintIdentifier = "D0EC6CA41EB9F4CC00EBF1C3"
BuildableName = "TelegramUI.framework"
BlueprintName = "TelegramUI"
ReferencedContainer = "container:TelegramUI.xcodeproj">
@ -63,7 +63,7 @@
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "D0FC407E1D5B8E7400261D9D"
BlueprintIdentifier = "D0EC6CA41EB9F4CC00EBF1C3"
BuildableName = "TelegramUI.framework"
BlueprintName = "TelegramUI"
ReferencedContainer = "container:TelegramUI.xcodeproj">

View File

@ -12,6 +12,11 @@
</dict>
<key>SuppressBuildableAutocreation</key>
<dict>
<key>D0EC6CA41EB9F4CC00EBF1C3</key>
<dict>
<key>primary</key>
<true/>
</dict>
<key>D0FC407E1D5B8E7400261D9D</key>
<dict>
<key>primary</key>

View File

@ -4,4 +4,7 @@ import AsyncDisplayKit
class AccessoryPanelNode: ASDisplayNode {
var dismiss: (() -> Void)?
var interfaceInteraction: ChatPanelInterfaceInteraction?
func updateThemeAndStrings(theme: PresentationTheme, strings: PresentationStrings) {
}
}

View File

@ -0,0 +1,54 @@
import Foundation
import AsyncDisplayKit
final class ActivityIndicator: ASDisplayNode {
private let indicatorNode: ASImageNode
init(theme: PresentationTheme) {
self.indicatorNode = ASImageNode()
self.indicatorNode.isLayerBacked = true
self.indicatorNode.displayWithoutProcessing = true
self.indicatorNode.displaysAsynchronously = false
self.indicatorNode.image = PresentationResourcesRootController.navigationIndefiniteActivityImage(theme)
super.init()
self.isLayerBacked = true
self.addSubnode(self.indicatorNode)
}
override func willEnterHierarchy() {
super.willEnterHierarchy()
let basicAnimation = CABasicAnimation(keyPath: "transform.rotation.z")
basicAnimation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
basicAnimation.duration = 0.5
basicAnimation.fromValue = NSNumber(value: Float(0.0))
basicAnimation.toValue = NSNumber(value: Float.pi * 2.0)
basicAnimation.repeatCount = Float.infinity
basicAnimation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionLinear)
self.indicatorNode.layer.add(basicAnimation, forKey: "progressRotation")
}
override func didExitHierarchy() {
super.didExitHierarchy()
self.indicatorNode.layer.removeAnimation(forKey: "progressRotation")
}
override func calculateSizeThatFits(_ constrainedSize: CGSize) -> CGSize {
return CGSize(width: 22.0, height: 22.0)
}
override func layout() {
super.layout()
let size = self.bounds.size
let indicatorSize = CGSize(width: 22.0, height: 22.0)
self.indicatorNode.frame = CGRect(origin: CGPoint(x: floor((size.width - indicatorSize.width) / 2.0), y: floor((size.height - indicatorSize.height) / 2.0)), size: indicatorSize)
}
}

View File

@ -0,0 +1,28 @@
import Foundation
func addAttributesToStringWithRanges(_ stringWithRanges: (String, [(Int, NSRange)]), body: MarkdownAttributeSet, argumentAttributes: [Int: MarkdownAttributeSet], textAlignment: NSTextAlignment = .natural) -> NSAttributedString {
let result = NSMutableAttributedString()
var bodyAttributes: [String: Any] = [NSFontAttributeName: body.font, NSForegroundColorAttributeName: body.textColor, NSParagraphStyleAttributeName: paragraphStyleWithAlignment(textAlignment)]
if !body.additionalAttributes.isEmpty {
for (key, value) in body.additionalAttributes {
bodyAttributes[key] = value
}
}
result.append(NSAttributedString(string: stringWithRanges.0, attributes: bodyAttributes))
for (index, range) in stringWithRanges.1 {
if let attributes = argumentAttributes[index] {
var argumentAttributes: [String: Any] = [NSFontAttributeName: attributes.font, NSForegroundColorAttributeName: attributes.textColor, NSParagraphStyleAttributeName: paragraphStyleWithAlignment(textAlignment)]
if !attributes.additionalAttributes.isEmpty {
for (key, value) in attributes.additionalAttributes {
argumentAttributes[key] = value
}
}
result.addAttributes(argumentAttributes, range: range)
}
}
return result
}

View File

@ -1,7 +0,0 @@
import Foundation
import Display
import AsyncDisplayKit
class AlertController {
}

View File

@ -56,7 +56,7 @@ private enum ArchivedStickerPacksEntryId: Hashable {
private enum ArchivedStickerPacksEntry: ItemListNodeEntry {
case info(String)
case pack(Int32, StickerPackCollectionInfo, StickerPackItem?, Int32, Bool, ItemListStickerPackItemEditing)
case pack(Int32, PresentationTheme, StickerPackCollectionInfo, StickerPackItem?, String, Bool, ItemListStickerPackItemEditing)
var section: ItemListSectionId {
switch self {
@ -69,7 +69,7 @@ private enum ArchivedStickerPacksEntry: ItemListNodeEntry {
switch self {
case .info:
return .index(0)
case let .pack(_, info, _, _, _, _):
case let .pack(_, _, info, _, _, _, _):
return .pack(info.id)
}
}
@ -82,11 +82,14 @@ private enum ArchivedStickerPacksEntry: ItemListNodeEntry {
} else {
return false
}
case let .pack(lhsIndex, lhsInfo, lhsTopItem, lhsCount, lhsEnabled, lhsEditing):
if case let .pack(rhsIndex, rhsInfo, rhsTopItem, rhsCount, rhsEnabled, rhsEditing) = rhs {
case let .pack(lhsIndex, lhsTheme, lhsInfo, lhsTopItem, lhsCount, lhsEnabled, lhsEditing):
if case let .pack(rhsIndex, rhsTheme, rhsInfo, rhsTopItem, rhsCount, rhsEnabled, rhsEditing) = rhs {
if lhsIndex != rhsIndex {
return false
}
if lhsTheme !== rhsTheme {
return false
}
if lhsInfo != rhsInfo {
return false
}
@ -118,9 +121,9 @@ private enum ArchivedStickerPacksEntry: ItemListNodeEntry {
default:
return true
}
case let .pack(lhsIndex, _, _, _, _, _):
case let .pack(lhsIndex, _, _, _, _, _, _):
switch rhs {
case let .pack(rhsIndex, _, _, _, _, _):
case let .pack(rhsIndex, _, _, _, _, _, _):
return lhsIndex < rhsIndex
default:
return false
@ -132,8 +135,8 @@ private enum ArchivedStickerPacksEntry: ItemListNodeEntry {
switch self {
case let .info(text):
return ItemListTextItem(text: .plain(text), sectionId: self.section)
case let .pack(_, info, topItem, count, enabled, editing):
return ItemListStickerPackItem(account: arguments.account, packInfo: info, itemCount: count, topItem: topItem, unread: false, control: .none, editing: editing, enabled: enabled, sectionId: self.section, action: { _ in
case let .pack(_, theme, info, topItem, count, enabled, editing):
return ItemListStickerPackItem(theme: theme, account: arguments.account, packInfo: info, itemCount: count, topItem: topItem, unread: false, control: .none, editing: editing, enabled: enabled, sectionId: self.section, action: { _ in
arguments.openStickerPack(info)
}, setPackIdWithRevealedOptions: { current, previous in
arguments.setPackIdWithRevealedOptions(current, previous)
@ -189,11 +192,19 @@ private struct ArchivedStickerPacksControllerState: Equatable {
}
}
private func archivedStickerPacksControllerEntries(state: ArchivedStickerPacksControllerState, packs: [ArchivedStickerPackItem]?, installedView: CombinedView) -> [ArchivedStickerPacksEntry] {
private func stringForStickerCount(_ count: Int32) -> String {
if count == 1 {
return "1 sticker"
} else {
return "\(count) stickers"
}
}
private func archivedStickerPacksControllerEntries(presentationData: PresentationData, state: ArchivedStickerPacksControllerState, packs: [ArchivedStickerPackItem]?, installedView: CombinedView) -> [ArchivedStickerPacksEntry] {
var entries: [ArchivedStickerPacksEntry] = []
if let packs = packs {
entries.append(.info("You can have up to 200 sticker sets installed.\nUnused stickers are archived when you add more.\n\n"))
entries.append(.info(presentationData.strings.StickerPacksSettings_ArchivedPacks_Info + "\n\n"))
var installedIds = Set<ItemCollectionId>()
if let view = installedView.views[.itemCollectionIds(namespaces: [Namespaces.ItemCollection.CloudStickerPacks])] as? ItemCollectionIdsView, let ids = view.idsByNamespace[Namespaces.ItemCollection.CloudStickerPacks] {
@ -203,7 +214,7 @@ private func archivedStickerPacksControllerEntries(state: ArchivedStickerPacksCo
var index: Int32 = 0
for item in packs {
if !installedIds.contains(item.info.id) {
entries.append(.pack(index, item.info, item.topItems.first, item.info.count, !state.removingPackIds.contains(item.info.id), ItemListStickerPackItemEditing(editable: true, editing: state.editing, revealed: state.packIdWithRevealedOptions == item.info.id)))
entries.append(.pack(index, presentationData.theme, item.info, item.topItems.first, stringForStickerCount(item.info.count), !state.removingPackIds.contains(item.info.id), ItemListStickerPackItemEditing(editable: true, editing: state.editing, revealed: state.packIdWithRevealedOptions == item.info.id)))
index += 1
}
}
@ -286,18 +297,19 @@ public func archivedStickerPacksController(account: Account) -> ViewController {
var previousPackCount: Int?
let signal = combineLatest(statePromise.get() |> deliverOnMainQueue, stickerPacks.get() |> deliverOnMainQueue, installedStickerPacks.get() |> deliverOnMainQueue)
|> map { state, packs, installedView -> (ItemListControllerState, (ItemListNodeState<ArchivedStickerPacksEntry>, ArchivedStickerPacksEntry.ItemGenerationArguments)) in
let signal = combineLatest((account.applicationContext as! TelegramApplicationContext).presentationData, statePromise.get() |> deliverOnMainQueue, stickerPacks.get() |> deliverOnMainQueue, installedStickerPacks.get() |> deliverOnMainQueue)
|> deliverOnMainQueue
|> map { presentationData, state, packs, installedView -> (ItemListControllerState, (ItemListNodeState<ArchivedStickerPacksEntry>, ArchivedStickerPacksEntry.ItemGenerationArguments)) in
var rightNavigationButton: ItemListNavigationButton?
if let packs = packs, packs.count != 0 {
if state.editing {
rightNavigationButton = ItemListNavigationButton(title: "Done", style: .bold, enabled: true, action: {
rightNavigationButton = ItemListNavigationButton(title: presentationData.strings.Common_Done, style: .bold, enabled: true, action: {
updateState {
$0.withUpdatedEditing(false)
}
})
} else {
rightNavigationButton = ItemListNavigationButton(title: "Edit", style: .regular, enabled: true, action: {
rightNavigationButton = ItemListNavigationButton(title: presentationData.strings.Common_Edit, style: .regular, enabled: true, action: {
updateState {
$0.withUpdatedEditing(true)
}
@ -313,16 +325,15 @@ public func archivedStickerPacksController(account: Account) -> ViewController {
emptyStateItem = ItemListLoadingIndicatorEmptyStateItem()
}
let controllerState = ItemListControllerState(title: .text("Archived Stickers"), leftNavigationButton: nil, rightNavigationButton: rightNavigationButton, animateChanges: true)
let controllerState = ItemListControllerState(theme: presentationData.theme, title: .text(presentationData.strings.StickerPacksSettings_ArchivedPacks), leftNavigationButton: nil, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: true)
let listState = ItemListNodeState(entries: archivedStickerPacksControllerEntries(state: state, packs: packs, installedView: installedView), style: .blocks, emptyStateItem: emptyStateItem, animateChanges: previous != nil && packs != nil && (previous! != 0 && previous! >= packs!.count - 10))
let listState = ItemListNodeState(entries: archivedStickerPacksControllerEntries(presentationData: presentationData, state: state, packs: packs, installedView: installedView), style: .blocks, emptyStateItem: emptyStateItem, animateChanges: previous != nil && packs != nil && (previous! != 0 && previous! >= packs!.count - 10))
return (controllerState, (listState, arguments))
} |> afterDisposed {
actionsDisposable.dispose()
}
let controller = ItemListController(signal)
controller.navigationItem.backBarButtonItem = UIBarButtonItem(title: "Back", style: .plain, target: nil, action: nil)
let controller = ItemListController(account: account, state: signal)
presentControllerImpl = { [weak controller] c, p in
if let controller = controller {
controller.present(c, in: .window, with: p)

View File

@ -27,12 +27,8 @@ final class AuthorizationSequenceCodeEntryController: ViewController {
}
}
override init(navigationBar: NavigationBar = NavigationBar()) {
super.init(navigationBar: navigationBar)
self.navigationBar.backgroundColor = nil
self.navigationBar.isOpaque = false
self.navigationBar.stripeColor = UIColor.clear
init() {
super.init(navigationBarTheme: AuthorizationSequenceController.navigationBarTheme)
self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: "Next", style: .done, target: self, action: #selector(self.nextPressed))
}

View File

@ -41,7 +41,7 @@ func authorizationNextOptionText(_ type: AuthorizationCodeNextType?, timeout: In
}
}
} else {
return NSAttributedString(string: "Haven't received the code?", font: Font.regular(16.0), textColor: UIColor(0x007ee5), paragraphAlignment: .center)
return NSAttributedString(string: "Haven't received the code?", font: Font.regular(16.0), textColor: UIColor(rgb: 0x007ee5), paragraphAlignment: .center)
}
}
@ -84,11 +84,11 @@ final class AuthorizationSequenceCodeEntryControllerNode: ASDisplayNode, UITextF
override init() {
self.navigationBackgroundNode = ASDisplayNode()
self.navigationBackgroundNode.isLayerBacked = true
self.navigationBackgroundNode.backgroundColor = UIColor(0xefefef)
self.navigationBackgroundNode.backgroundColor = UIColor(rgb: 0xefefef)
self.stripeNode = ASDisplayNode()
self.stripeNode.isLayerBacked = true
self.stripeNode.backgroundColor = UIColor(0xbcbbc1)
self.stripeNode.backgroundColor = UIColor(rgb: 0xbcbbc1)
self.titleNode = ASTextNode()
self.titleNode.isLayerBacked = true
@ -105,7 +105,7 @@ final class AuthorizationSequenceCodeEntryControllerNode: ASDisplayNode, UITextF
self.codeSeparatorNode = ASDisplayNode()
self.codeSeparatorNode.isLayerBacked = true
self.codeSeparatorNode.backgroundColor = UIColor(0xbcbbc1)
self.codeSeparatorNode.backgroundColor = UIColor(rgb: 0xbcbbc1)
self.codeField = TextFieldNode()
self.codeField.textField.font = Font.regular(24.0)
@ -129,7 +129,7 @@ final class AuthorizationSequenceCodeEntryControllerNode: ASDisplayNode, UITextF
self.codeField.textField.addTarget(self, action: #selector(self.codeFieldTextChanged(_:)), for: .editingChanged)
self.codeField.textField.attributedPlaceholder = NSAttributedString(string: "Code", font: Font.regular(24.0), textColor: UIColor(0xbcbcc3))
self.codeField.textField.attributedPlaceholder = NSAttributedString(string: "Code", font: Font.regular(24.0), textColor: UIColor(rgb: 0xbcbcc3))
}
deinit {

View File

@ -7,6 +7,8 @@ import SwiftSignalKit
import MtProtoKitDynamic
public final class AuthorizationSequenceController: NavigationController {
static let navigationBarTheme = NavigationBarTheme(buttonColor: UIColor(rgb: 0x007ee5), primaryTextColor: .black, backgroundColor: .clear, separatorColor: .clear)
private var account: UnauthorizedAccount
private var stateDisposable: Disposable?

View File

@ -440,13 +440,11 @@ final class AuthorizationSequenceCountrySelectionController: ViewController {
var completeWithCountryCode: ((Int) -> Void)?
override init(navigationBar: NavigationBar = NavigationBar()) {
init() {
self.innerController = InnerCountrySelectionController()
self.innerNavigationController = UINavigationController(rootViewController: self.innerController)
super.init(navigationBar: navigationBar)
self.navigationBar.isHidden = true
super.init(navigationBarTheme: nil)
self.innerController.dismiss = { [weak self] in
self?.cancelPressed()

View File

@ -24,12 +24,8 @@ final class AuthorizationSequencePasswordEntryController: ViewController {
}
}
override init(navigationBar: NavigationBar = NavigationBar()) {
super.init(navigationBar: navigationBar)
self.navigationBar.backgroundColor = nil
self.navigationBar.isOpaque = false
self.navigationBar.stripeColor = UIColor.clear
init() {
super.init(navigationBarTheme: AuthorizationSequenceController.navigationBarTheme)
self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: "Next", style: .done, target: self, action: #selector(self.nextPressed))
}

View File

@ -30,11 +30,11 @@ final class AuthorizationSequencePasswordEntryControllerNode: ASDisplayNode, UIT
override init() {
self.navigationBackgroundNode = ASDisplayNode()
self.navigationBackgroundNode.isLayerBacked = true
self.navigationBackgroundNode.backgroundColor = UIColor(0xefefef)
self.navigationBackgroundNode.backgroundColor = UIColor(rgb: 0xefefef)
self.stripeNode = ASDisplayNode()
self.stripeNode.isLayerBacked = true
self.stripeNode.backgroundColor = UIColor(0xbcbbc1)
self.stripeNode.backgroundColor = UIColor(rgb: 0xbcbbc1)
self.titleNode = ASTextNode()
self.titleNode.isLayerBacked = true
@ -49,11 +49,11 @@ final class AuthorizationSequencePasswordEntryControllerNode: ASDisplayNode, UIT
self.nextOptionNode = ASTextNode()
self.nextOptionNode.isLayerBacked = true
self.nextOptionNode.displaysAsynchronously = false
self.nextOptionNode.attributedText = NSAttributedString(string: "Forgot password?", font: Font.regular(16.0), textColor: UIColor(0x007ee5), paragraphAlignment: .center)
self.nextOptionNode.attributedText = NSAttributedString(string: "Forgot password?", font: Font.regular(16.0), textColor: UIColor(rgb: 0x007ee5), paragraphAlignment: .center)
self.codeSeparatorNode = ASDisplayNode()
self.codeSeparatorNode.isLayerBacked = true
self.codeSeparatorNode.backgroundColor = UIColor(0xbcbbc1)
self.codeSeparatorNode.backgroundColor = UIColor(rgb: 0xbcbbc1)
self.codeField = TextFieldNode()
self.codeField.textField.font = Font.regular(20.0)
@ -79,7 +79,7 @@ final class AuthorizationSequencePasswordEntryControllerNode: ASDisplayNode, UIT
}
func updateData(hint: String) {
self.codeField.textField.attributedPlaceholder = NSAttributedString(string: hint, font: Font.regular(20.0), textColor: UIColor(0xbcbcc3))
self.codeField.textField.attributedPlaceholder = NSAttributedString(string: hint, font: Font.regular(20.0), textColor: UIColor(rgb: 0xbcbcc3))
}
func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) {

View File

@ -24,12 +24,8 @@ final class AuthorizationSequencePhoneEntryController: ViewController {
private let hapticFeedback = HapticFeedback()
override init(navigationBar: NavigationBar = NavigationBar()) {
super.init(navigationBar: navigationBar)
self.navigationBar.backgroundColor = nil
self.navigationBar.isOpaque = false
self.navigationBar.stripeColor = UIColor.clear
init() {
super.init(navigationBarTheme: AuthorizationSequenceController.navigationBarTheme)
self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: "Next", style: .done, target: self, action: #selector(self.nextPressed))
}

View File

@ -241,7 +241,7 @@ private let countryButtonBackground = generateImage(CGSize(width: 61.0, height:
let arrowSize: CGFloat = 10.0
let lineWidth = UIScreenPixel
context.clear(CGRect(origin: CGPoint(), size: size))
context.setStrokeColor(UIColor(0xbcbbc1).cgColor)
context.setStrokeColor(UIColor(rgb: 0xbcbbc1).cgColor)
context.setLineWidth(lineWidth)
context.move(to: CGPoint(x: size.width, y: size.height - arrowSize - lineWidth / 2.0))
context.addLine(to: CGPoint(x: size.width - 1.0, y: size.height - arrowSize - lineWidth / 2.0))
@ -254,7 +254,7 @@ private let countryButtonBackground = generateImage(CGSize(width: 61.0, height:
private let countryButtonHighlightedBackground = generateImage(CGSize(width: 60.0, height: 67.0), rotatedContext: { size, context in
let arrowSize: CGFloat = 10.0
context.clear(CGRect(origin: CGPoint(), size: size))
context.setFillColor(UIColor(0xbcbbc1).cgColor)
context.setFillColor(UIColor(rgb: 0xbcbbc1).cgColor)
context.fill(CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: size.height - arrowSize)))
context.move(to: CGPoint(x: size.width, y: size.height - arrowSize))
context.addLine(to: CGPoint(x: size.width - 1.0, y: size.height - arrowSize))
@ -268,7 +268,7 @@ private let phoneInputBackground = generateImage(CGSize(width: 85.0, height: 57.
let arrowSize: CGFloat = 10.0
let lineWidth = UIScreenPixel
context.clear(CGRect(origin: CGPoint(), size: size))
context.setStrokeColor(UIColor(0xbcbbc1).cgColor)
context.setStrokeColor(UIColor(rgb: 0xbcbbc1).cgColor)
context.setLineWidth(lineWidth)
context.move(to: CGPoint(x: 15.0, y: size.height - lineWidth / 2.0))
context.addLine(to: CGPoint(x: size.width, y: size.height - lineWidth / 2.0))
@ -313,11 +313,11 @@ final class AuthorizationSequencePhoneEntryControllerNode: ASDisplayNode {
override init() {
self.navigationBackgroundNode = ASDisplayNode()
self.navigationBackgroundNode.isLayerBacked = true
self.navigationBackgroundNode.backgroundColor = UIColor(0xefefef)
self.navigationBackgroundNode.backgroundColor = UIColor(rgb: 0xefefef)
self.stripeNode = ASDisplayNode()
self.stripeNode.isLayerBacked = true
self.stripeNode.backgroundColor = UIColor(0xbcbbc1)
self.stripeNode.backgroundColor = UIColor(rgb: 0xbcbbc1)
self.titleNode = ASTextNode()
self.titleNode.isLayerBacked = true
@ -327,14 +327,14 @@ final class AuthorizationSequencePhoneEntryControllerNode: ASDisplayNode {
self.noticeNode = ASTextNode()
self.noticeNode.isLayerBacked = true
self.noticeNode.displaysAsynchronously = false
self.noticeNode.attributedText = NSAttributedString(string: "Please confirm your country code and enter your phone number.", font: Font.regular(16.0), textColor: UIColor(0x878787), paragraphAlignment: .center)
self.noticeNode.attributedText = NSAttributedString(string: "Please confirm your country code and enter your phone number.", font: Font.regular(16.0), textColor: UIColor(rgb: 0x878787), paragraphAlignment: .center)
self.termsOfServiceNode = ASTextNode()
self.termsOfServiceNode.isLayerBacked = true
self.termsOfServiceNode.displaysAsynchronously = false
let termsString = NSMutableAttributedString()
termsString.append(NSAttributedString(string: "By signing up,\nyou agree to the ", font: Font.regular(16.0), textColor: UIColor.black))
termsString.append(NSAttributedString(string: "Terms of Service", font: Font.regular(16.0), textColor: UIColor(0x007ee5)))
termsString.append(NSAttributedString(string: "Terms of Service", font: Font.regular(16.0), textColor: UIColor(rgb: 0x007ee5)))
termsString.append(NSAttributedString(string: ".", font: Font.regular(16.0), textColor: UIColor.black))
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.alignment = .center
@ -371,7 +371,7 @@ final class AuthorizationSequencePhoneEntryControllerNode: ASDisplayNode {
self.countryButton.contentEdgeInsets = UIEdgeInsets(top: 0.0, left: 15.0, bottom: 10.0, right: 0.0)
self.countryButton.contentHorizontalAlignment = .left
self.phoneInputNode.numberField.textField.attributedPlaceholder = NSAttributedString(string: "Your phone number", font: Font.regular(20.0), textColor: UIColor(0xbcbcc3))
self.phoneInputNode.numberField.textField.attributedPlaceholder = NSAttributedString(string: "Your phone number", font: Font.regular(20.0), textColor: UIColor(rgb: 0xbcbcc3))
self.countryButton.addTarget(self, action: #selector(self.countryPressed), forControlEvents: .touchUpInside)

View File

@ -24,12 +24,8 @@ final class AuthorizationSequenceSignUpController: ViewController {
}
}
override init(navigationBar: NavigationBar = NavigationBar()) {
super.init(navigationBar: navigationBar)
self.navigationBar.backgroundColor = nil
self.navigationBar.isOpaque = false
self.navigationBar.stripeColor = UIColor.clear
init() {
super.init(navigationBarTheme: AuthorizationSequenceController.navigationBarTheme)
self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: "Next", style: .done, target: self, action: #selector(self.nextPressed))
}

View File

@ -33,11 +33,11 @@ final class AuthorizationSequenceSignUpControllerNode: ASDisplayNode, UITextFiel
override init() {
self.navigationBackgroundNode = ASDisplayNode()
self.navigationBackgroundNode.isLayerBacked = true
self.navigationBackgroundNode.backgroundColor = UIColor(0xefefef)
self.navigationBackgroundNode.backgroundColor = UIColor(rgb: 0xefefef)
self.stripeNode = ASDisplayNode()
self.stripeNode.isLayerBacked = true
self.stripeNode.backgroundColor = UIColor(0xbcbbc1)
self.stripeNode.backgroundColor = UIColor(rgb: 0xbcbbc1)
self.titleNode = ASTextNode()
self.titleNode.isLayerBacked = true
@ -47,31 +47,31 @@ final class AuthorizationSequenceSignUpControllerNode: ASDisplayNode, UITextFiel
self.currentOptionNode = ASTextNode()
self.currentOptionNode.isLayerBacked = true
self.currentOptionNode.displaysAsynchronously = false
self.currentOptionNode.attributedText = NSAttributedString(string: "Enter your name and add a profile picture", font: Font.regular(16.0), textColor: UIColor(0x878787), paragraphAlignment: .center)
self.currentOptionNode.attributedText = NSAttributedString(string: "Enter your name and add a profile picture", font: Font.regular(16.0), textColor: UIColor(rgb: 0x878787), paragraphAlignment: .center)
self.firstSeparatorNode = ASDisplayNode()
self.firstSeparatorNode.isLayerBacked = true
self.firstSeparatorNode.backgroundColor = UIColor(0xbcbbc1)
self.firstSeparatorNode.backgroundColor = UIColor(rgb: 0xbcbbc1)
self.lastSeparatorNode = ASDisplayNode()
self.lastSeparatorNode.isLayerBacked = true
self.lastSeparatorNode.backgroundColor = UIColor(0xbcbbc1)
self.lastSeparatorNode.backgroundColor = UIColor(rgb: 0xbcbbc1)
self.firstNameField = TextFieldNode()
self.firstNameField.textField.font = Font.regular(20.0)
self.firstNameField.textField.textAlignment = .natural
self.firstNameField.textField.returnKeyType = .next
self.firstNameField.textField.attributedPlaceholder = NSAttributedString(string: "First name", font: self.firstNameField.textField.font, textColor: UIColor(0xbcbcc3))
self.firstNameField.textField.attributedPlaceholder = NSAttributedString(string: "First name", font: self.firstNameField.textField.font, textColor: UIColor(rgb: 0xbcbcc3))
self.lastNameField = TextFieldNode()
self.lastNameField.textField.font = Font.regular(20.0)
self.lastNameField.textField.textAlignment = .natural
self.lastNameField.textField.returnKeyType = .done
self.lastNameField.textField.attributedPlaceholder = NSAttributedString(string: "Last name", font: self.lastNameField.textField.font, textColor: UIColor(0xbcbcc3))
self.lastNameField.textField.attributedPlaceholder = NSAttributedString(string: "Last name", font: self.lastNameField.textField.font, textColor: UIColor(rgb: 0xbcbcc3))
self.addPhotoButton = HighlightableButtonNode()
self.addPhotoButton.setAttributedTitle(NSAttributedString(string: "add\nphoto", font: Font.regular(16.0), textColor: UIColor(0xbcbcc3), paragraphAlignment: .center), for: .normal)
self.addPhotoButton.setBackgroundImage(generateCircleImage(diameter: 110.0, lineWidth: 1.0, color: UIColor(0xbcbcc3)), for: .normal)
self.addPhotoButton.setAttributedTitle(NSAttributedString(string: "add\nphoto", font: Font.regular(16.0), textColor: UIColor(rgb: 0xbcbcc3), paragraphAlignment: .center), for: .normal)
self.addPhotoButton.setBackgroundImage(generateCircleImage(diameter: 110.0, lineWidth: 1.0, color: UIColor(rgb: 0xbcbcc3)), for: .normal)
super.init(viewBlock: {
return UITracingLayerView()
@ -96,8 +96,8 @@ final class AuthorizationSequenceSignUpControllerNode: ASDisplayNode, UITextFiel
}
func updateData(firstName: String, lastName: String) {
self.firstNameField.textField.attributedPlaceholder = NSAttributedString(string: firstName, font: Font.regular(20.0), textColor: UIColor(0xbcbcc3))
self.lastNameField.textField.attributedPlaceholder = NSAttributedString(string: lastName, font: Font.regular(20.0), textColor: UIColor(0xbcbcc3))
self.firstNameField.textField.attributedPlaceholder = NSAttributedString(string: firstName, font: Font.regular(20.0), textColor: UIColor(rgb: 0xbcbcc3))
self.lastNameField.textField.attributedPlaceholder = NSAttributedString(string: lastName, font: Font.regular(20.0), textColor: UIColor(rgb: 0xbcbcc3))
}
func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) {

View File

@ -13,10 +13,9 @@ final class AuthorizationSequenceSplashController: ViewController {
var nextPressed: (() -> Void)?
override init(navigationBar: NavigationBar = NavigationBar()) {
super.init(navigationBar: navigationBar)
init() {
super.init(navigationBarTheme: nil)
self.navigationBar.isHidden = true
self.controller.startMessaging = { [weak self] in
self?.nextPressed?()
}

View File

@ -12,8 +12,8 @@ public struct AutomaticMediaDownloadCategoryPeers: Coding, Equatable {
}
public init(decoder: Decoder) {
self.privateChats = (decoder.decodeInt32ForKey("p") as Int32) != 0
self.groupsAndChannels = (decoder.decodeInt32ForKey("g") as Int32) != 0
self.privateChats = decoder.decodeInt32ForKey("p", orElse: 0) != 0
self.groupsAndChannels = decoder.decodeInt32ForKey("g", orElse: 0) != 0
}
public func encode(_ encoder: Encoder) {
@ -115,7 +115,7 @@ public struct AutomaticMediaDownloadSettings: PreferencesEntry, Equatable {
public init(decoder: Decoder) {
self.categories = decoder.decodeObjectForKey("c", decoder: { AutomaticMediaDownloadCategories(decoder: $0) }) as! AutomaticMediaDownloadCategories
self.saveIncomingPhotos = (decoder.decodeInt32ForKey("siph") as Int32) != 0
self.saveIncomingPhotos = decoder.decodeInt32ForKey("siph", orElse: 0) != 0
}
public func encode(_ encoder: Encoder) {

View File

@ -80,12 +80,7 @@ class AvatarGalleryController: ViewController {
self.account = account
self.replaceRootController = replaceRootController
super.init()
self.navigationBar.backgroundColor = UIColor(white: 0.0, alpha: 0.6)
self.navigationBar.stripeColor = UIColor.clear
self.navigationBar.foregroundColor = UIColor.white
self.navigationBar.accentColor = UIColor.white
super.init(navigationBarTheme: GalleryController.darkNavigationTheme)
self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: "Done", style: .done, target: self, action: #selector(self.donePressed))
@ -147,29 +142,6 @@ class AvatarGalleryController: ViewController {
$0.withUpdatedFooterContentNode(footerContentNode)
}, transition: .immediate)
}))
self.centralItemAttributesDisposable.add(self.centralItemNavigationStyle.get().start(next: { [weak self] style in
if let strongSelf = self {
switch style {
case .dark:
strongSelf.statusBar.statusBarStyle = .White
strongSelf.navigationBar.backgroundColor = UIColor(white: 0.0, alpha: 0.5)
strongSelf.navigationBar.stripeColor = UIColor.clear
strongSelf.navigationBar.foregroundColor = UIColor.white
strongSelf.navigationBar.accentColor = UIColor.white
strongSelf.galleryNode.backgroundNode.backgroundColor = UIColor.black
strongSelf.galleryNode.isBackgroundExtendedOverNavigationBar = true
case .light:
strongSelf.statusBar.statusBarStyle = .Black
strongSelf.navigationBar.backgroundColor = UIColor(red: 0.968626451, green: 0.968626451, blue: 0.968626451, alpha: 1.0)
strongSelf.navigationBar.foregroundColor = UIColor.black
strongSelf.navigationBar.accentColor = UIColor(0x007ee5)
strongSelf.navigationBar.stripeColor = UIColor(red: 0.6953125, green: 0.6953125, blue: 0.6953125, alpha: 1.0)
strongSelf.galleryNode.backgroundNode.backgroundColor = UIColor(0xbdbdc2)
strongSelf.galleryNode.isBackgroundExtendedOverNavigationBar = false
}
}
}))
}
required init(coder aDecoder: NSCoder) {

View File

@ -23,16 +23,16 @@ private class AvatarNodeParameters: NSObject {
}
private let gradientColors: [NSArray] = [
[UIColor(0xff516a).cgColor, UIColor(0xff885e).cgColor],
[UIColor(0xffa85c).cgColor, UIColor(0xffcd6a).cgColor],
[UIColor(0x54cb68).cgColor, UIColor(0xa0de7e).cgColor],
[UIColor(0x2a9ef1).cgColor, UIColor(0x72d5fd).cgColor],
[UIColor(0x665fff).cgColor, UIColor(0x82b1ff).cgColor],
[UIColor(0xd669ed).cgColor, UIColor(0xe0a2f3).cgColor]
[UIColor(rgb: 0xff516a).cgColor, UIColor(rgb: 0xff885e).cgColor],
[UIColor(rgb: 0xffa85c).cgColor, UIColor(rgb: 0xffcd6a).cgColor],
[UIColor(rgb: 0x54cb68).cgColor, UIColor(rgb: 0xa0de7e).cgColor],
[UIColor(rgb: 0x2a9ef1).cgColor, UIColor(rgb: 0x72d5fd).cgColor],
[UIColor(rgb: 0x665fff).cgColor, UIColor(rgb: 0x82b1ff).cgColor],
[UIColor(rgb: 0xd669ed).cgColor, UIColor(rgb: 0xe0a2f3).cgColor]
]
private let grayscaleColors: NSArray = [
UIColor(0xefefef).cgColor, UIColor(0xeeeeee).cgColor
UIColor(rgb: 0xefefef).cgColor, UIColor(rgb: 0xeeeeee).cgColor
]
private enum AvatarNodeState: Equatable {

View File

@ -44,7 +44,7 @@ private enum BlockedPeersEntryStableId: Hashable {
}
private enum BlockedPeersEntry: ItemListNodeEntry {
case peerItem(Int32, Peer, ItemListPeerItemEditing, Bool)
case peerItem(Int32, PresentationTheme, PresentationStrings, Peer, ItemListPeerItemEditing, Bool)
var section: ItemListSectionId {
switch self {
@ -55,18 +55,24 @@ private enum BlockedPeersEntry: ItemListNodeEntry {
var stableId: BlockedPeersEntryStableId {
switch self {
case let .peerItem(_, peer, _, _):
case let .peerItem(_, _, _, peer, _, _):
return .peer(peer.id)
}
}
static func ==(lhs: BlockedPeersEntry, rhs: BlockedPeersEntry) -> Bool {
switch lhs {
case let .peerItem(lhsIndex, lhsPeer, lhsEditing, lhsEnabled):
if case let .peerItem(rhsIndex, rhsPeer, rhsEditing, rhsEnabled) = rhs {
case let .peerItem(lhsIndex, lhsTheme, lhsStrings, lhsPeer, lhsEditing, lhsEnabled):
if case let .peerItem(rhsIndex, rhsTheme, rhsStrings, rhsPeer, rhsEditing, rhsEnabled) = rhs {
if lhsIndex != rhsIndex {
return false
}
if lhsTheme !== rhsTheme {
return false
}
if lhsStrings !== rhsStrings {
return false
}
if !lhsPeer.isEqual(rhsPeer) {
return false
}
@ -85,9 +91,9 @@ private enum BlockedPeersEntry: ItemListNodeEntry {
static func <(lhs: BlockedPeersEntry, rhs: BlockedPeersEntry) -> Bool {
switch lhs {
case let .peerItem(index, _, _, _):
case let .peerItem(index, _, _, _, _, _):
switch rhs {
case let .peerItem(rhsIndex, _, _, _):
case let .peerItem(rhsIndex, _, _, _, _, _):
return index < rhsIndex
}
}
@ -95,8 +101,8 @@ private enum BlockedPeersEntry: ItemListNodeEntry {
func item(_ arguments: BlockedPeersControllerArguments) -> ListViewItem {
switch self {
case let .peerItem(_, peer, editing, enabled):
return ItemListPeerItem(account: arguments.account, peer: peer, presence: nil, text: .none, label: .none, editing: editing, switchValue: nil, enabled: enabled, sectionId: self.section, action: nil, setPeerIdWithRevealedOptions: { previousId, id in
case let .peerItem(_, theme, strings, peer, editing, enabled):
return ItemListPeerItem(theme: theme, strings: strings, account: arguments.account, peer: peer, presence: nil, text: .none, label: .none, editing: editing, switchValue: nil, enabled: enabled, sectionId: self.section, action: nil, setPeerIdWithRevealedOptions: { previousId, id in
arguments.setPeerIdWithRevealedOptions(previousId, id)
}, removePeer: { peerId in
arguments.removePeer(peerId)
@ -149,13 +155,13 @@ private struct BlockedPeersControllerState: Equatable {
}
}
private func blockedPeersControllerEntries(state: BlockedPeersControllerState, peers: [Peer]?) -> [BlockedPeersEntry] {
private func blockedPeersControllerEntries(presentationData: PresentationData, state: BlockedPeersControllerState, peers: [Peer]?) -> [BlockedPeersEntry] {
var entries: [BlockedPeersEntry] = []
if let peers = peers {
var index: Int32 = 0
for peer in peers {
entries.append(.peerItem(index, peer, ItemListPeerItemEditing(editable: true, editing: state.editing, revealed: peer.id == state.peerIdWithRevealedOptions), state.removingPeerId != peer.id))
entries.append(.peerItem(index, presentationData.theme, presentationData.strings, peer, ItemListPeerItemEditing(editable: true, editing: state.editing, revealed: peer.id == state.peerIdWithRevealedOptions), state.removingPeerId != peer.id))
index += 1
}
}
@ -229,9 +235,9 @@ public func blockedPeersController(account: Account) -> ViewController {
var previousPeers: [Peer]?
let signal = combineLatest(statePromise.get(), peersPromise.get())
let signal = combineLatest((account.applicationContext as! TelegramApplicationContext).presentationData, statePromise.get(), peersPromise.get())
|> deliverOnMainQueue
|> map { state, peers -> (ItemListControllerState, (ItemListNodeState<BlockedPeersEntry>, BlockedPeersEntry.ItemGenerationArguments)) in
|> map { presentationData, state, peers -> (ItemListControllerState, (ItemListNodeState<BlockedPeersEntry>, BlockedPeersEntry.ItemGenerationArguments)) in
var rightNavigationButton: ItemListNavigationButton?
if let peers = peers, !peers.isEmpty {
if state.editing {
@ -261,15 +267,15 @@ public func blockedPeersController(account: Account) -> ViewController {
let previous = previousPeers
previousPeers = peers
let controllerState = ItemListControllerState(title: .text("Blocked Users"), leftNavigationButton: nil, rightNavigationButton: rightNavigationButton, animateChanges: true)
let listState = ItemListNodeState(entries: blockedPeersControllerEntries(state: state, peers: peers), style: .blocks, emptyStateItem: emptyStateItem, animateChanges: previous != nil && peers != nil && previous!.count >= peers!.count)
let controllerState = ItemListControllerState(theme: presentationData.theme, title: .text("Blocked Users"), leftNavigationButton: nil, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: "Back"), animateChanges: true)
let listState = ItemListNodeState(entries: blockedPeersControllerEntries(presentationData: presentationData, state: state, peers: peers), style: .blocks, emptyStateItem: emptyStateItem, animateChanges: previous != nil && peers != nil && previous!.count >= peers!.count)
return (controllerState, (listState, arguments))
} |> afterDisposed {
actionsDisposable.dispose()
}
let controller = ItemListController(signal)
let controller = ItemListController(account: account, state: signal)
presentControllerImpl = { [weak controller] c, p in
if let controller = controller {
controller.present(c, in: .window, with: p)

View File

@ -0,0 +1,160 @@
import Foundation
import Display
import AsyncDisplayKit
import Postbox
import TelegramCore
import SwiftSignalKit
public final class CallController: ViewController {
private var controllerNode: CallControllerNode {
return self.displayNode as! CallControllerNode
}
private let _ready = Promise<Bool>(false)
override public var ready: Promise<Bool> {
return self._ready
}
private let account: Account
public let call: PresentationCall
private var presentationData: PresentationData
private var animatedAppearance = false
private var peer: Peer?
private var peerDisposable: Disposable?
private var disposable: Disposable?
private var callMutedDisposable: Disposable?
private var isMuted = false
private var speakerModeDisposable: Disposable?
private var speakerMode = false
public init(account: Account, call: PresentationCall) {
self.account = account
self.call = call
self.presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 }
super.init(navigationBarTheme: nil)
self.statusBar.statusBarStyle = .White
self.statusBar.ignoreInCall = true
self.supportedOrientations = .portrait
self.disposable = (call.state |> deliverOnMainQueue).start(next: { [weak self] callState in
self?.callStateUpdated(callState)
})
self.callMutedDisposable = (call.isMuted |> deliverOnMainQueue).start(next: { [weak self] value in
if let strongSelf = self {
strongSelf.isMuted = value
if strongSelf.isNodeLoaded {
strongSelf.controllerNode.isMuted = value
}
}
})
self.speakerModeDisposable = (call.speakerMode |> deliverOnMainQueue).start(next: { [weak self] value in
if let strongSelf = self {
strongSelf.speakerMode = value
if strongSelf.isNodeLoaded {
strongSelf.controllerNode.speakerMode = value
}
}
})
}
required public init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
self.peerDisposable?.dispose()
self.disposable?.dispose()
self.callMutedDisposable?.dispose()
self.speakerModeDisposable?.dispose()
}
private func callStateUpdated(_ callState: PresentationCallState) {
if self.isNodeLoaded {
self.controllerNode.updateCallState(callState)
}
}
override public func loadDisplayNode() {
self.displayNode = CallControllerNode(account: self.account, presentationData: self.presentationData, statusBar: self.statusBar)
self.displayNodeDidLoad()
self.controllerNode.toggleMute = { [weak self] in
self?.call.toggleIsMuted()
}
self.controllerNode.toggleSpeaker = { [weak self] in
self?.call.toggleSpeaker()
}
self.controllerNode.acceptCall = { [weak self] in
let _ = self?.call.answer()
}
self.controllerNode.endCall = { [weak self] in
let _ = self?.call.hangUp()
}
self.controllerNode.back = { [weak self] in
let _ = self?.dismiss()
}
self.controllerNode.disissedInteractively = { [weak self] in
self?.animatedAppearance = false
self?.presentingViewController?.dismiss(animated: false, completion: nil)
}
self.peerDisposable = (account.postbox.peerView(id: self.call.peerId)
|> deliverOnMainQueue).start(next: { [weak self] view in
if let strongSelf = self {
if let peer = view.peers[view.peerId] {
strongSelf.peer = peer
strongSelf.controllerNode.updatePeer(peer: peer)
strongSelf._ready.set(.single(true))
}
}
})
self.controllerNode.isMuted = self.isMuted
self.controllerNode.speakerMode = self.speakerMode
}
override public func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
if !self.animatedAppearance {
self.animatedAppearance = true
self.controllerNode.animateIn()
}
}
override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
super.containerLayoutUpdated(layout, transition: transition)
self.controllerNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationHeight, transition: transition)
}
override open func dismiss(completion: (() -> Void)? = nil) {
self.controllerNode.animateOut(completion: { [weak self] in
self?.animatedAppearance = false
self?.presentingViewController?.dismiss(animated: false, completion: nil)
completion?()
})
}
@objc func backPressed() {
self.dismiss()
}
}

View File

@ -0,0 +1,195 @@
import Foundation
import Display
import AsyncDisplayKit
import SwiftSignalKit
enum CallControllerButtonType {
case mute
case end
case accept
case speaker
case bluetooth
}
private let buttonSize = CGSize(width: 75.0, height: 75.0)
private func generateEmptyButtonImage(icon: UIImage?, strokeColor: UIColor?, fillColor: UIColor, knockout: Bool = false, angle: CGFloat = 0.0) -> UIImage? {
return generateImage(buttonSize, contextGenerator: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
context.setBlendMode(.copy)
if let strokeColor = strokeColor {
context.setFillColor(strokeColor.cgColor)
context.fillEllipse(in: CGRect(origin: CGPoint(), size: size))
context.setFillColor(fillColor.cgColor)
context.fillEllipse(in: CGRect(origin: CGPoint(x: 1.5, y: 1.5), size: CGSize(width: size.width - 3.0, height: size.height - 3.0)))
} else {
context.setFillColor(fillColor.cgColor)
context.fillEllipse(in: CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: size.height)))
}
if let icon = icon {
if !angle.isZero {
context.translateBy(x: size.width / 2.0, y: size.height / 2.0)
context.rotate(by: angle)
context.translateBy(x: -size.width / 2.0, y: -size.height / 2.0)
}
let imageSize = icon.size
let imageRect = CGRect(origin: CGPoint(x: floor((size.width - imageSize.width) / 2.0), y: floor((size.width - imageSize.height) / 2.0)), size: imageSize)
if knockout {
context.setBlendMode(.copy)
context.clip(to: imageRect, mask: icon.cgImage!)
context.setFillColor(UIColor.clear.cgColor)
context.fill(imageRect)
} else {
context.setBlendMode(.normal)
context.draw(icon.cgImage!, in: imageRect)
}
}
})
}
private func generateFilledButtonImage(color: UIColor, icon: UIImage?, angle: CGFloat = 0.0) -> UIImage? {
return generateImage(buttonSize, contextGenerator: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
context.setBlendMode(.normal)
context.setFillColor(color.cgColor)
context.fillEllipse(in: CGRect(origin: CGPoint(), size: size))
if let icon = icon {
if !angle.isZero {
context.translateBy(x: size.width / 2.0, y: size.height / 2.0)
context.rotate(by: angle)
context.translateBy(x: -size.width / 2.0, y: -size.height / 2.0)
}
context.draw(icon.cgImage!, in: CGRect(origin: CGPoint(x: floor((size.width - icon.size.width) / 2.0), y: floor((size.height - icon.size.height) / 2.0)), size: icon.size))
}
})
}
private let emptyStroke = UIColor(white: 1.0, alpha: 0.8)
private let emptyHighlightedFill = UIColor(white: 1.0, alpha: 0.3)
private let invertedFill = UIColor(white: 1.0, alpha: 1.0)
private let labelFont = Font.regular(14.5)
final class CallControllerButtonNode: HighlightTrackingButtonNode {
private let regularImage: UIImage?
private let highlightedImage: UIImage?
private let filledImage: UIImage?
private let backgroundNode: ASImageNode
private let labelNode: ASTextNode?
init(type: CallControllerButtonType, label: String?) {
self.backgroundNode = ASImageNode()
self.backgroundNode.isLayerBacked = true
self.backgroundNode.displayWithoutProcessing = false
self.backgroundNode.displaysAsynchronously = false
if let label = label {
let labelNode = ASTextNode()
labelNode.attributedText = NSAttributedString(string: label, font: labelFont, textColor: .white)
self.labelNode = labelNode
} else {
self.labelNode = nil
}
var regularImage: UIImage?
var highlightedImage: UIImage?
var filledImage: UIImage?
switch type {
case .mute:
regularImage = generateEmptyButtonImage(icon: UIImage(bundleImageName: "Call/CallMuteButton"), strokeColor: emptyStroke, fillColor: .clear)
highlightedImage = generateEmptyButtonImage(icon: UIImage(bundleImageName: "Call/CallMuteButton"), strokeColor: emptyStroke, fillColor: emptyHighlightedFill)
filledImage = generateEmptyButtonImage(icon: UIImage(bundleImageName: "Call/CallMuteButton"), strokeColor: nil, fillColor: invertedFill, knockout: true)
case .accept:
regularImage = generateFilledButtonImage(color: UIColor(rgb: 0x74db58), icon: UIImage(bundleImageName: "Call/CallPhoneButton"), angle: CGFloat.pi * 3.0 / 4.0)
highlightedImage = generateFilledButtonImage(color: UIColor(rgb: 0x74db58), icon: UIImage(bundleImageName: "Call/CallPhoneButton"), angle: CGFloat.pi * 3.0 / 4.0)
case .end:
regularImage = generateFilledButtonImage(color: UIColor(rgb: 0xd92326), icon: UIImage(bundleImageName: "Call/CallPhoneButton"))
highlightedImage = generateFilledButtonImage(color: UIColor(rgb: 0xd92326), icon: UIImage(bundleImageName: "Call/CallPhoneButton"))
case .speaker:
regularImage = generateEmptyButtonImage(icon: UIImage(bundleImageName: "Call/CallSpeakerButton"), strokeColor: emptyStroke, fillColor: .clear)
highlightedImage = generateEmptyButtonImage(icon: UIImage(bundleImageName: "Call/CallSpeakerButton"), strokeColor: emptyStroke, fillColor: emptyHighlightedFill)
filledImage = generateEmptyButtonImage(icon: UIImage(bundleImageName: "Call/CallSpeakerButton"), strokeColor: nil, fillColor: invertedFill, knockout: true)
case .bluetooth:
regularImage = generateEmptyButtonImage(icon: UIImage(bundleImageName: "Call/CallBluetoothButton"), strokeColor: emptyStroke, fillColor: .clear)
highlightedImage = generateEmptyButtonImage(icon: UIImage(bundleImageName: "Call/CallBluetoothButton"), strokeColor: emptyStroke, fillColor: emptyHighlightedFill)
filledImage = generateEmptyButtonImage(icon: UIImage(bundleImageName: "Call/CallBluetoothButton"), strokeColor: nil, fillColor: invertedFill, knockout: true)
}
self.regularImage = regularImage
self.highlightedImage = highlightedImage
self.filledImage = filledImage
super.init()
self.addSubnode(self.backgroundNode)
if let labelNode = self.labelNode {
self.addSubnode(labelNode)
}
self.backgroundNode.image = regularImage
self.currentImage = regularImage
self.highligthedChanged = { [weak self] highlighted in
if let strongSelf = self {
strongSelf.internalHighlighted = highlighted
strongSelf.updateState(highlighted: highlighted, selected: strongSelf.isSelected)
}
}
}
private var internalHighlighted = false
override var isSelected: Bool {
didSet {
self.updateState(highlighted: self.internalHighlighted, selected: self.isSelected)
}
}
private var currentImage: UIImage?
private func updateState(highlighted: Bool, selected: Bool) {
let image: UIImage?
if selected {
image = self.filledImage
} else if highlighted {
image = self.highlightedImage
} else {
image = self.regularImage
}
if self.currentImage !== image {
let currentContents = self.backgroundNode.layer.contents
self.backgroundNode.layer.removeAnimation(forKey: "contents")
if let currentContents = currentContents, let image = image {
self.backgroundNode.image = image
self.backgroundNode.layer.animate(from: currentContents as AnyObject, to: image.cgImage!, keyPath: "contents", timingFunction: kCAMediaTimingFunctionEaseInEaseOut, duration: image === self.currentImage || image === self.filledImage ? 0.25 : 0.15)
} else {
self.backgroundNode.image = image
}
self.currentImage = image
}
}
func animateRollTransition() {
self.backgroundNode.layer.animate(from: 0.0 as NSNumber, to: (-CGFloat.pi * 5 / 4) as NSNumber, keyPath: "transform.rotation.z", timingFunction: kCAMediaTimingFunctionSpring, duration: 0.3, removeOnCompletion: false)
self.labelNode?.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false)
}
override func layout() {
super.layout()
let size = self.bounds.size
self.backgroundNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: size.width))
if let labelNode = self.labelNode {
let labelSize = labelNode.measure(CGSize(width: 200.0, height: 100.0))
labelNode.frame = CGRect(origin: CGPoint(x: floor((size.width - labelSize.width) / 2.0), y: 81.0), size: labelSize)
}
}
}

View File

@ -0,0 +1,196 @@
import Foundation
import Display
import AsyncDisplayKit
import SwiftSignalKit
enum CallControllerButtonsSpeakerMode {
case bluetooth
case speaker
}
enum CallControllerButtonsMode: Equatable {
case active(CallControllerButtonsSpeakerMode)
case incoming
static func ==(lhs: CallControllerButtonsMode, rhs: CallControllerButtonsMode) -> Bool {
switch lhs {
case let .active(mode):
if case .active(mode) = rhs {
return true
} else {
return false
}
case .incoming:
if case .incoming = rhs {
return true
} else {
return false
}
}
}
}
final class CallControllerButtonsNode: ASDisplayNode {
private let acceptButton: CallControllerButtonNode
private let declineButton: CallControllerButtonNode
private let muteButton: CallControllerButtonNode
private let endButton: CallControllerButtonNode
private let speakerButton: CallControllerButtonNode
private var mode: CallControllerButtonsMode?
private var validLayout: CGFloat?
var isMuted = false {
didSet {
self.muteButton.isSelected = self.isMuted
}
}
var speakerMode = false {
didSet {
self.speakerButton.isSelected = self.speakerMode
}
}
var accept: (() -> Void)?
var mute: (() -> Void)?
var end: (() -> Void)?
var speaker: (() -> Void)?
init(strings: PresentationStrings) {
self.acceptButton = CallControllerButtonNode(type: .accept, label: strings.Call_Accept)
self.acceptButton.alpha = 0.0
self.declineButton = CallControllerButtonNode(type: .end, label: strings.Call_Decline)
self.declineButton.alpha = 0.0
self.muteButton = CallControllerButtonNode(type: .mute, label: nil)
self.muteButton.alpha = 0.0
self.endButton = CallControllerButtonNode(type: .end, label: nil)
self.endButton.alpha = 0.0
self.speakerButton = CallControllerButtonNode(type: .speaker, label: nil)
self.speakerButton.alpha = 0.0
super.init()
self.addSubnode(self.acceptButton)
self.addSubnode(self.declineButton)
self.addSubnode(self.muteButton)
self.addSubnode(self.endButton)
self.addSubnode(self.speakerButton)
self.acceptButton.addTarget(self, action: #selector(self.buttonPressed(_:)), forControlEvents: .touchUpInside)
self.declineButton.addTarget(self, action: #selector(self.buttonPressed(_:)), forControlEvents: .touchUpInside)
self.muteButton.addTarget(self, action: #selector(self.buttonPressed(_:)), forControlEvents: .touchUpInside)
self.endButton.addTarget(self, action: #selector(self.buttonPressed(_:)), forControlEvents: .touchUpInside)
self.speakerButton.addTarget(self, action: #selector(self.buttonPressed(_:)), forControlEvents: .touchUpInside)
}
func updateLayout(constrainedWidth: CGFloat, transition: ContainedViewLayoutTransition) {
let previousLayout = self.validLayout
self.validLayout = constrainedWidth
if let mode = self.mode, previousLayout != self.validLayout {
self.updateButtonsLayout(mode: mode, width: constrainedWidth, animated: false)
}
}
func updateMode(_ mode: CallControllerButtonsMode) {
if self.mode != mode {
let previousMode = self.mode
self.mode = mode
if let validLayout = self.validLayout {
self.updateButtonsLayout(mode: mode, width: validLayout, animated: previousMode != nil)
}
}
}
private func updateButtonsLayout(mode: CallControllerButtonsMode, width: CGFloat, animated: Bool) {
let transition: ContainedViewLayoutTransition
if animated {
transition = .animated(duration: 0.3, curve: .spring)
} else {
transition = .immediate
}
let threeButtonSpacing: CGFloat = 28.0
let twoButtonSpacing: CGFloat = 105.0
let buttonSize = CGSize(width: 75.0, height: 75.0)
let threeButtonsWidth = 3.0 * buttonSize.width + max(0.0, 3.0 - 1.0) * threeButtonSpacing
let twoButtonsWidth = 2.0 * buttonSize.width + max(0.0, 2.0 - 1.0) * twoButtonSpacing
var origin = CGPoint(x: floor((width - threeButtonsWidth) / 2.0), y: 0.0)
for button in [self.muteButton, self.endButton, self.speakerButton] {
transition.updateFrame(node: button, frame: CGRect(origin: origin, size: buttonSize))
origin.x += buttonSize.width + threeButtonSpacing
}
origin = CGPoint(x: floor((width - twoButtonsWidth) / 2.0), y: 0.0)
for button in [self.declineButton, self.acceptButton] {
transition.updateFrame(node: button, frame: CGRect(origin: origin, size: buttonSize))
origin.x += buttonSize.width + twoButtonSpacing
}
switch mode {
case .incoming:
for button in [self.declineButton, self.acceptButton] {
button.alpha = 1.0
}
for button in [self.muteButton, self.endButton, self.speakerButton] {
button.alpha = 0.0
}
case .active:
for button in [self.muteButton, self.speakerButton] {
if animated && button.alpha.isZero {
button.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
}
button.alpha = 1.0
}
var animatingAcceptButton = false
if self.endButton.alpha.isZero {
if animated {
if !self.acceptButton.alpha.isZero {
animatingAcceptButton = true
self.endButton.layer.animatePosition(from: self.acceptButton.position, to: self.endButton.position, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring)
self.acceptButton.animateRollTransition()
self.endButton.layer.animate(from: (CGFloat.pi * 5 / 4) as NSNumber, to: 0.0 as NSNumber, keyPath: "transform.rotation.z", timingFunction: kCAMediaTimingFunctionSpring, duration: 0.3)
self.acceptButton.layer.animatePosition(from: self.acceptButton.position, to: self.endButton.position, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, completion: { [weak self] _ in
if let strongSelf = self {
strongSelf.acceptButton.alpha = 0.0
strongSelf.acceptButton.layer.removeAnimation(forKey: "position")
strongSelf.acceptButton.layer.removeAnimation(forKey: "transform.rotation.z")
}
})
}
self.endButton.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
}
self.endButton.alpha = 1.0
}
if !self.declineButton.alpha.isZero {
if animated {
self.declineButton.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2)
}
self.declineButton.alpha = 0.0
}
if self.acceptButton.alpha.isZero && !animatingAcceptButton {
self.acceptButton.alpha = 0.0
}
}
}
@objc func buttonPressed(_ button: CallControllerButtonNode) {
if button === self.muteButton {
self.mute?()
} else if button === self.endButton || button === self.declineButton {
self.end?()
} else if button === self.speakerButton {
self.speaker?()
} else if button === self.acceptButton {
self.accept?()
}
}
}

View File

@ -0,0 +1,107 @@
import Foundation
import Display
import AsyncDisplayKit
import SwiftSignalKit
import TelegramLegacyComponents
private let emojiFont = Font.regular(28.0)
private let textFont = Font.regular(15.0)
final class CallControllerKeyPreviewNode: ASDisplayNode {
private let keyTextNode: ASTextNode
private let infoTextNode: ASTextNode
private let effectView: UIVisualEffectView
private let dismiss: () -> Void
init(keyText: String, infoText: String, dismiss: @escaping () -> Void) {
self.keyTextNode = ASTextNode()
self.keyTextNode.displaysAsynchronously = false
self.infoTextNode = ASTextNode()
self.infoTextNode.displaysAsynchronously = false
self.dismiss = dismiss
self.effectView = UIVisualEffectView()
if #available(iOS 9.0, *) {
} else {
self.effectView.effect = UIBlurEffect(style: .dark)
self.effectView.alpha = 0.0
}
super.init()
self.keyTextNode.attributedText = NSAttributedString(string: keyText, attributes: [NSFontAttributeName: Font.regular(58.0), NSKernAttributeName: 9.0 as NSNumber])
self.infoTextNode.attributedText = NSAttributedString(string: infoText, font: Font.regular(14.0), textColor: UIColor.white, paragraphAlignment: .center)
self.view.addSubview(self.effectView)
self.addSubnode(self.keyTextNode)
self.addSubnode(self.infoTextNode)
}
override func didLoad() {
super.didLoad()
self.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapResture(_:))))
}
func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) {
self.effectView.frame = CGRect(origin: CGPoint(), size: size)
let keyTextSize = self.keyTextNode.measure(CGSize(width: 300.0, height: 300.0))
transition.updateFrame(node: self.keyTextNode, frame: CGRect(origin: CGPoint(x: floor((size.width - keyTextSize.width) / 2) + 6.0, y: floor((size.height - keyTextSize.height) / 2) - 50.0), size: keyTextSize))
let infoTextSize = self.infoTextNode.measure(CGSize(width: size.width - 20.0, height: CGFloat.greatestFiniteMagnitude))
transition.updateFrame(node: self.infoTextNode, frame: CGRect(origin: CGPoint(x: floor((size.width - infoTextSize.width) / 2.0), y: floor((size.height - infoTextSize.height) / 2.0) + 30.0), size: infoTextSize))
}
func animateIn(from rect: CGRect, fromNode: ASDisplayNode) {
self.keyTextNode.layer.animatePosition(from: CGPoint(x: rect.midX, y: rect.midY), to: self.keyTextNode.layer.position, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring)
if let transitionView = fromNode.view.snapshotView(afterScreenUpdates: false) {
self.view.addSubview(transitionView)
transitionView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false)
transitionView.layer.animatePosition(from: CGPoint(x: rect.midX, y: rect.midY), to: self.keyTextNode.layer.position, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, completion: { [weak transitionView] _ in
transitionView?.removeFromSuperview()
})
transitionView.layer.animateScale(from: 1.0, to: self.keyTextNode.frame.size.width / rect.size.width, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false)
}
self.keyTextNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15)
self.keyTextNode.layer.animateScale(from: rect.size.width / self.keyTextNode.frame.size.width, to: 1.0, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring)
self.infoTextNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
UIView.animate(withDuration: 0.3, animations: {
if #available(iOS 9.0, *) {
self.effectView.effect = TGBlurEffect.call()!
} else {
self.effectView.alpha = 1.0
}
})
}
func animateOut(to rect: CGRect, toNode: ASDisplayNode, completion: @escaping () -> Void) {
self.keyTextNode.layer.animatePosition(from: self.keyTextNode.layer.position, to: CGPoint(x: rect.midX, y: rect.midY), duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, completion: { _ in
completion()
})
self.keyTextNode.layer.animateScale(from: 1.0, to: rect.size.width / self.keyTextNode.frame.size.width, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false)
self.infoTextNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false)
UIView.animate(withDuration: 0.3, animations: {
if #available(iOS 9.0, *) {
self.effectView.effect = nil
} else {
self.effectView.alpha = 0.0
}
})
}
@objc func tapResture(_ recognizer: UITapGestureRecognizer) {
if case .ended = recognizer.state {
self.dismiss()
}
}
}

View File

@ -0,0 +1,369 @@
import Foundation
import Display
import AsyncDisplayKit
import Postbox
import TelegramCore
import SwiftSignalKit
private func generateBackArrowImage(color: UIColor) -> UIImage? {
return generateImage(CGSize(width: 13.0, height: 22.0), contextGenerator: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
context.setFillColor(color.cgColor)
let _ = try? drawSvgPath(context, path: "M10.6569398,0.0 L0.0,11 L10.6569398,22 L13,19.1782395 L5.07681762,11 L13,2.82176047 Z ")
})
}
final class CallControllerNode: ASDisplayNode {
private let account: Account
private let statusBar: StatusBar
private var presentationData: PresentationData
private var peer: Peer?
private let containerNode: ASDisplayNode
private let imageNode: TransformImageNode
private let dimNode: ASDisplayNode
private let backButtonArrowNode: ASImageNode
private let backButtonNode: HighlightableButtonNode
private let statusNode: CallControllerStatusNode
private let buttonsNode: CallControllerButtonsNode
private var keyPreviewNode: CallControllerKeyPreviewNode?
private var keyTextData: (Data, String)?
private let keyButtonNode: HighlightableButtonNode
private var validLayout: (ContainerViewLayout, CGFloat)?
var isMuted: Bool = false {
didSet {
self.buttonsNode.isMuted = self.isMuted
}
}
var speakerMode: Bool = false {
didSet {
self.buttonsNode.speakerMode = self.speakerMode
}
}
var toggleMute: (() -> Void)?
var toggleSpeaker: (() -> Void)?
var acceptCall: (() -> Void)?
var endCall: (() -> Void)?
var back: (() -> Void)?
var disissedInteractively: (() -> Void)?
init(account: Account, presentationData: PresentationData, statusBar: StatusBar) {
self.account = account
self.presentationData = presentationData
self.statusBar = statusBar
self.containerNode = ASDisplayNode()
self.imageNode = TransformImageNode()
self.dimNode = ASDisplayNode()
self.dimNode.isLayerBacked = true
self.dimNode.backgroundColor = UIColor(white: 0.0, alpha: 0.4)
self.backButtonArrowNode = ASImageNode()
self.backButtonArrowNode.displayWithoutProcessing = true
self.backButtonArrowNode.displaysAsynchronously = false
self.backButtonArrowNode.image = generateBackArrowImage(color: .white)
self.backButtonNode = HighlightableButtonNode()
self.statusNode = CallControllerStatusNode()
self.buttonsNode = CallControllerButtonsNode(strings: self.presentationData.strings)
self.keyButtonNode = HighlightableButtonNode()
super.init(viewBlock: {
return UITracingLayerView()
}, didLoad: nil)
self.containerNode.backgroundColor = .black
self.addSubnode(self.containerNode)
self.backButtonNode.setTitle(presentationData.strings.Common_Back, with: Font.regular(17.0), with: .white, for: [])
self.backButtonNode.hitTestSlop = UIEdgeInsets(top: -8.0, left: -20.0, bottom: -8.0, right: -8.0)
self.backButtonNode.highligthedChanged = { [weak self] highlighted in
if let strongSelf = self {
if highlighted {
strongSelf.backButtonNode.layer.removeAnimation(forKey: "opacity")
strongSelf.backButtonArrowNode.layer.removeAnimation(forKey: "opacity")
strongSelf.backButtonNode.alpha = 0.4
strongSelf.backButtonArrowNode.alpha = 0.4
} else {
strongSelf.backButtonNode.alpha = 1.0
strongSelf.backButtonArrowNode.alpha = 1.0
strongSelf.backButtonNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2)
strongSelf.backButtonArrowNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2)
}
}
}
self.containerNode.addSubnode(self.imageNode)
self.containerNode.addSubnode(self.dimNode)
self.containerNode.addSubnode(self.statusNode)
self.containerNode.addSubnode(self.buttonsNode)
self.containerNode.addSubnode(self.keyButtonNode)
self.containerNode.addSubnode(self.backButtonArrowNode)
self.containerNode.addSubnode(self.backButtonNode)
self.buttonsNode.mute = { [weak self] in
self?.toggleMute?()
}
self.buttonsNode.speaker = { [weak self] in
self?.toggleSpeaker?()
}
self.buttonsNode.end = { [weak self] in
self?.endCall?()
}
self.buttonsNode.accept = { [weak self] in
self?.acceptCall?()
}
self.keyButtonNode.addTarget(self, action: #selector(self.keyPressed), forControlEvents: .touchUpInside)
self.backButtonNode.addTarget(self, action: #selector(self.backPressed), forControlEvents: .touchUpInside)
}
override func didLoad() {
super.didLoad()
self.view.addGestureRecognizer(UIPanGestureRecognizer(target: self, action: #selector(self.panGesture(_:))))
}
func updatePeer(peer: Peer) {
if !arePeersEqual(self.peer, peer) {
self.peer = peer
self.imageNode.setSignal(account: self.account, signal: chatAvatarGalleryPhoto(account: self.account, representations: peer.profileImageRepresentations, autoFetchFullSize: true))
self.statusNode.title = peer.displayTitle
if let (layout, navigationBarHeight) = self.validLayout {
self.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: .immediate)
}
}
}
func updateCallState(_ callState: PresentationCallState) {
let statusValue: CallControllerStatusValue
switch callState {
case .waiting, .connecting:
statusValue = .text(self.presentationData.strings.Call_StatusConnecting)
case let .requesting(ringing):
if ringing {
statusValue = .text(self.presentationData.strings.Call_StatusRinging)
} else {
statusValue = .text(self.presentationData.strings.Call_StatusRequesting)
}
case .terminating, .terminated:
statusValue = .text(self.presentationData.strings.Call_StatusEnded)
case .ringing:
statusValue = .text(self.presentationData.strings.Call_StatusIncoming)
case let .active(timestamp, keyVisualHash):
let strings = self.presentationData.strings
statusValue = .timer({ value in
return strings.Call_StatusOngoing(value).0
}, timestamp)
if self.keyTextData?.0 != keyVisualHash {
let text = stringForEmojiHashOfData(keyVisualHash, 4)!
self.keyTextData = (keyVisualHash, text)
self.keyButtonNode.setAttributedTitle(NSAttributedString(string: text, attributes: [NSFontAttributeName: Font.regular(22.0), NSKernAttributeName: 2.5 as NSNumber]), for: [])
let keyTextSize = self.keyButtonNode.measure(CGSize(width: 200.0, height: 200.0))
self.keyButtonNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
self.keyButtonNode.frame = CGRect(origin: self.keyButtonNode.frame.origin, size: keyTextSize)
if let (layout, navigationBarHeight) = self.validLayout {
self.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: .immediate)
}
}
}
switch callState {
case .terminated, .terminating:
if !self.statusNode.alpha.isEqual(to: 0.5) {
self.statusNode.alpha = 0.5
self.buttonsNode.alpha = 0.5
self.keyButtonNode.alpha = 0.5
self.backButtonArrowNode.alpha = 0.5
self.backButtonNode.alpha = 0.5
self.statusNode.layer.animateAlpha(from: 1.0, to: 0.5, duration: 0.25)
self.buttonsNode.layer.animateAlpha(from: 1.0, to: 0.5, duration: 0.25)
self.keyButtonNode.layer.animateAlpha(from: 1.0, to: 0.5, duration: 0.25)
}
default:
if !self.statusNode.alpha.isEqual(to: 1.0) {
self.statusNode.alpha = 1.0
self.buttonsNode.alpha = 1.0
self.keyButtonNode.alpha = 1.0
self.backButtonArrowNode.alpha = 1.0
self.backButtonNode.alpha = 1.0
}
}
self.statusNode.status = statusValue
switch callState {
case .ringing:
self.buttonsNode.updateMode(.incoming)
default:
self.buttonsNode.updateMode(.active(.speaker))
}
}
func animateIn() {
var bounds = self.bounds
bounds.origin = CGPoint()
self.bounds = bounds
self.layer.removeAnimation(forKey: "bounds")
self.statusBar.layer.removeAnimation(forKey: "opacity")
self.containerNode.layer.removeAnimation(forKey: "opacity")
self.containerNode.layer.removeAnimation(forKey: "scale")
self.statusBar.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
self.containerNode.layer.animateScale(from: 1.04, to: 1.0, duration: 0.3)
self.containerNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
}
func animateOut(completion: @escaping () -> Void) {
self.statusBar.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false)
self.containerNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false)
self.containerNode.layer.animateScale(from: 1.0, to: 1.04, duration: 0.3, removeOnCompletion: false, completion: { _ in
completion()
})
}
func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) {
self.validLayout = (layout, navigationBarHeight)
transition.updateFrame(node: self.containerNode, frame: CGRect(origin: CGPoint(), size: layout.size))
transition.updateFrame(node: self.dimNode, frame: CGRect(origin: CGPoint(), size: layout.size))
if let keyPreviewNode = self.keyPreviewNode {
transition.updateFrame(node: keyPreviewNode, frame: CGRect(origin: CGPoint(), size: layout.size))
keyPreviewNode.updateLayout(size: layout.size, transition: .immediate)
}
transition.updateFrame(node: self.imageNode, frame: CGRect(origin: CGPoint(), size: layout.size))
let arguments = TransformImageArguments(corners: ImageCorners(), imageSize: CGSize(width: 640.0, height: 640.0).aspectFilled(layout.size), boundingSize: layout.size, intrinsicInsets: UIEdgeInsets())
let apply = self.imageNode.asyncLayout()(arguments)
apply()
let backSize = self.backButtonNode.measure(CGSize(width: 320.0, height: 100.0))
if let image = self.backButtonArrowNode.image {
transition.updateFrame(node: self.backButtonArrowNode, frame: CGRect(origin: CGPoint(x: 10.0, y: 31.0), size: image.size))
}
transition.updateFrame(node: self.backButtonNode, frame: CGRect(origin: CGPoint(x: 29.0, y: 31.0), size: backSize))
let statusOffset: CGFloat
if layout.metrics.widthClass == .regular && layout.metrics.heightClass == .regular {
if layout.size.height.isEqual(to: 1366.0) {
statusOffset = 160.0
} else {
statusOffset = 120.0
}
} else {
if layout.size.height.isEqual(to: 736.0) {
statusOffset = 80.0
} else if layout.size.width.isEqual(to: 320.0) {
statusOffset = 60.0
} else {
statusOffset = 64.0
}
}
let buttonsHeight: CGFloat = 75.0
let buttonsOffset: CGFloat
if layout.size.width.isEqual(to: 320.0) {
if layout.size.height.isEqual(to: 480.0) {
buttonsOffset = 53.0
} else {
buttonsOffset = 63.0
}
} else {
buttonsOffset = 83.0
}
let statusHeight = self.statusNode.updateLayout(constrainedWidth: layout.size.width, transition: transition)
transition.updateFrame(node: self.statusNode, frame: CGRect(origin: CGPoint(x: 0.0, y: statusOffset), size: CGSize(width: layout.size.width, height: statusHeight)))
self.buttonsNode.updateLayout(constrainedWidth: layout.size.width, transition: transition)
transition.updateFrame(node: self.buttonsNode, frame: CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - (buttonsOffset - 40.0) - buttonsHeight), size: CGSize(width: layout.size.width, height: buttonsHeight)))
let keyTextSize = self.keyButtonNode.frame.size
transition.updateFrame(node: self.keyButtonNode, frame: CGRect(origin: CGPoint(x: layout.size.width - keyTextSize.width - 8.0, y: 28.0), size: keyTextSize))
}
@objc func keyPressed() {
if self.keyPreviewNode == nil, let keyText = self.keyTextData?.1, let peer = self.peer {
let keyPreviewNode = CallControllerKeyPreviewNode(keyText: keyText, infoText: self.presentationData.strings.Call_EmojiDescription(peer.compactDisplayTitle).0, dismiss: { [weak self] in
if let _ = self?.keyPreviewNode {
self?.backPressed()
}
})
self.containerNode.insertSubnode(keyPreviewNode, aboveSubnode: self.dimNode)
self.keyPreviewNode = keyPreviewNode
if let (validLayout, _) = self.validLayout {
keyPreviewNode.updateLayout(size: validLayout.size, transition: .immediate)
self.keyButtonNode.isHidden = true
keyPreviewNode.animateIn(from: self.keyButtonNode.frame, fromNode: self.keyButtonNode)
}
}
}
@objc func backPressed() {
if let keyPreviewNode = self.keyPreviewNode {
self.keyPreviewNode = nil
keyPreviewNode.animateOut(to: self.keyButtonNode.frame, toNode: self.keyButtonNode, completion: { [weak self, weak keyPreviewNode] in
self?.keyButtonNode.isHidden = false
keyPreviewNode?.removeFromSupernode()
})
} else {
self.back?()
}
}
@objc func panGesture(_ recognizer: UIPanGestureRecognizer) {
switch recognizer.state {
case .changed:
let offset = recognizer.translation(in: self.view).y
var bounds = self.bounds
bounds.origin.y = -offset
self.bounds = bounds
case .ended:
let velocity = recognizer.velocity(in: self.view).y
if abs(velocity) < 100.0 {
var bounds = self.bounds
let previous = bounds
bounds.origin = CGPoint()
self.bounds = bounds
self.layer.animateBounds(from: previous, to: bounds, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring)
} else {
var bounds = self.bounds
let previous = bounds
bounds.origin = CGPoint(x: 0.0, y: velocity > 0.0 ? -bounds.height: bounds.height)
self.bounds = bounds
self.layer.animateBounds(from: previous, to: bounds, duration: 0.15, timingFunction: kCAMediaTimingFunctionEaseOut, completion: { [weak self] _ in
self?.disissedInteractively?()
})
}
case .cancelled:
var bounds = self.bounds
let previous = bounds
bounds.origin = CGPoint()
self.bounds = bounds
self.layer.animateBounds(from: previous, to: bounds, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring)
default:
break
}
}
}

View File

@ -0,0 +1,130 @@
import Foundation
import Display
import AsyncDisplayKit
import SwiftSignalKit
private let compactNameFont = Font.light(28.0)
private let regularNameFont = Font.light(36.0)
private let compactStatusFont = Font.regular(18.0)
private let regularStatusFont = Font.regular(18.0)
enum CallControllerStatusValue: Equatable {
case text(String)
case timer((String) -> String, Double)
static func ==(lhs: CallControllerStatusValue, rhs: CallControllerStatusValue) -> Bool {
switch lhs {
case let .text(text):
if case .text(text) = rhs {
return true
} else {
return false
}
case let .timer(_, referenceTime):
if case .timer(_, referenceTime) = rhs {
return true
} else {
return false
}
}
}
}
final class CallControllerStatusNode: ASDisplayNode {
private let titleNode: TextNode
private let statusNode: TextNode
private let statusMeasureNode: TextNode
var title: String = ""
var status: CallControllerStatusValue = .text("") {
didSet {
if self.status != oldValue {
self.statusTimer?.invalidate()
if case .timer = self.status {
self.statusTimer = SwiftSignalKit.Timer(timeout: 0.5, repeat: true, completion: { [weak self] in
if let strongSelf = self, let validLayoutWidth = strongSelf.validLayoutWidth {
let _ = strongSelf.updateLayout(constrainedWidth: validLayoutWidth, transition: .immediate)
}
}, queue: Queue.mainQueue())
self.statusTimer?.start()
} else {
if let validLayoutWidth = self.validLayoutWidth {
let _ = self.updateLayout(constrainedWidth: validLayoutWidth, transition: .immediate)
}
}
}
}
}
private var statusTimer: SwiftSignalKit.Timer?
private var validLayoutWidth: CGFloat?
override init() {
self.titleNode = TextNode()
self.statusNode = TextNode()
self.statusNode.displaysAsynchronously = false
self.statusMeasureNode = TextNode()
super.init()
self.isUserInteractionEnabled = false
self.addSubnode(self.titleNode)
self.addSubnode(self.statusNode)
}
deinit {
self.statusTimer?.invalidate()
}
func updateLayout(constrainedWidth: CGFloat, transition: ContainedViewLayoutTransition) -> CGFloat {
self.validLayoutWidth = constrainedWidth
let nameFont: UIFont
let statusFont: UIFont
if constrainedWidth < 330.0 {
nameFont = compactNameFont
statusFont = compactStatusFont
} else {
nameFont = regularNameFont
statusFont = regularStatusFont
}
let statusText: String
let statusMeasureText: String
switch self.status {
case let .text(text):
statusText = text
statusMeasureText = text
case let .timer(format, referenceTime):
let duration = Int32(CFAbsoluteTimeGetCurrent() - referenceTime)
let durationString: String
let measureDurationString: String
if duration > 60 * 60 {
durationString = String(format: "%02d:%02d:%02d", arguments: [duration / 3600, (duration / 60) % 60, duration % 60])
measureDurationString = "00:00:00"
} else {
durationString = String(format: "%02d:%02d", arguments: [(duration / 60) % 60, duration % 60])
measureDurationString = "00:00"
}
statusText = format(durationString)
statusMeasureText = format(measureDurationString)
}
let spacing: CGFloat = 4.0
let (titleLayout, titleApply) = TextNode.asyncLayout(self.titleNode)(NSAttributedString(string: self.title, font: nameFont, textColor: .white), nil, 1, .end, CGSize(width: constrainedWidth, height: CGFloat.greatestFiniteMagnitude), .natural, nil, UIEdgeInsets(top: 2.0, left: 2.0, bottom: 2.0, right: 2.0))
let (statusMeasureLayout, statusMeasureApply) = TextNode.asyncLayout(self.statusMeasureNode)(NSAttributedString(string: statusMeasureText, font: statusFont, textColor: .white), nil, 1, .end, CGSize(width: constrainedWidth, height: CGFloat.greatestFiniteMagnitude), .natural, nil, UIEdgeInsets(top: 2.0, left: 2.0, bottom: 2.0, right: 2.0))
let (statusLayout, statusApply) = TextNode.asyncLayout(self.statusNode)(NSAttributedString(string: statusText, font: statusFont, textColor: .white), nil, 1, .end, CGSize(width: constrainedWidth, height: CGFloat.greatestFiniteMagnitude), .natural, nil, UIEdgeInsets(top: 2.0, left: 2.0, bottom: 2.0, right: 2.0))
let _ = titleApply()
let _ = statusApply()
let _ = statusMeasureApply()
self.titleNode.frame = CGRect(origin: CGPoint(x: floor((constrainedWidth - titleLayout.size.width) / 2.0), y: 0.0), size: titleLayout.size)
self.statusNode.frame = CGRect(origin: CGPoint(x: floor((constrainedWidth - statusMeasureLayout.size.width) / 2.0), y: titleLayout.size.height + spacing), size: statusLayout.size)
return titleLayout.size.height + spacing + statusLayout.size.height
}
}

View File

@ -0,0 +1,229 @@
import Foundation
import CallKit
import AVFoundation
import Postbox
import SwiftSignalKit
final class CallKitIntegration {
private let providerDelegate: AnyObject
private let audioSessionActivePromise = ValuePromise<Bool>(false, ignoreRepeated: true)
var audioSessionActive: Signal<Bool, NoError> {
return self.audioSessionActivePromise.get()
}
init?(startCall: @escaping (UUID, String) -> Signal<Bool, NoError>, answerCall: @escaping (UUID) -> Void, endCall: @escaping (UUID) -> Signal<Bool, NoError>, audioSessionActivationChanged: @escaping (Bool) -> Void) {
#if (arch(i386) || arch(x86_64)) && os(iOS)
return nil
#else
if #available(iOSApplicationExtension 10.0, *) {
self.providerDelegate = CallKitProviderDelegate(audioSessionActivePromise: self.audioSessionActivePromise, startCall: startCall, answerCall: answerCall, endCall: endCall, audioSessionActivationChanged: audioSessionActivationChanged)
} else {
return nil
}
#endif
}
func startCall(peerId: PeerId, displayTitle: String) {
if #available(iOSApplicationExtension 10.0, *) {
(self.providerDelegate as! CallKitProviderDelegate).startCall(peerId: peerId, displayTitle: displayTitle)
}
}
func answerCall(uuid: UUID) {
if #available(iOSApplicationExtension 10.0, *) {
(self.providerDelegate as! CallKitProviderDelegate).answerCall(uuid: uuid)
}
}
func dropCall(uuid: UUID) {
if #available(iOSApplicationExtension 10.0, *) {
(self.providerDelegate as! CallKitProviderDelegate).dropCall(uuid: uuid)
}
}
func reportIncomingCall(uuid: UUID, handle: String, displayTitle: String, completion: ((NSError?) -> Void)?) {
if #available(iOSApplicationExtension 10.0, *) {
(self.providerDelegate as! CallKitProviderDelegate).reportIncomingCall(uuid: uuid, handle: handle, displayTitle: displayTitle, completion: completion)
}
}
func reportOutgoingCallConnected(uuid: UUID, at date: Date) {
if #available(iOSApplicationExtension 10.0, *) {
(self.providerDelegate as! CallKitProviderDelegate).reportOutgoingCallConnected(uuid: uuid, at: date)
}
}
}
@available(iOSApplicationExtension 10.0, *)
class CallKitProviderDelegate: NSObject, CXProviderDelegate {
private let provider: CXProvider
private let callController = CXCallController()
private let startCall: (UUID, String) -> Signal<Bool, NoError>
private let answerCall: (UUID) -> Void
private let endCall: (UUID) -> Signal<Bool, NoError>
private let audioSessionActivationChanged: (Bool) -> Void
private let disposableSet = DisposableSet()
fileprivate let audioSessionActivePromise: ValuePromise<Bool>
init(audioSessionActivePromise: ValuePromise<Bool>, startCall: @escaping (UUID, String) -> Signal<Bool, NoError>, answerCall: @escaping (UUID) -> Void, endCall: @escaping (UUID) -> Signal<Bool, NoError>, audioSessionActivationChanged: @escaping (Bool) -> Void) {
self.audioSessionActivePromise = audioSessionActivePromise
self.startCall = startCall
self.answerCall = answerCall
self.endCall = endCall
self.audioSessionActivationChanged = audioSessionActivationChanged
self.provider = CXProvider(configuration: CallKitProviderDelegate.providerConfiguration)
super.init()
self.provider.setDelegate(self, queue: nil)
}
static var providerConfiguration: CXProviderConfiguration {
let providerConfiguration = CXProviderConfiguration(localizedName: "Telegram")
providerConfiguration.supportsVideo = false
providerConfiguration.maximumCallsPerCallGroup = 1
providerConfiguration.maximumCallGroups = 1
providerConfiguration.supportedHandleTypes = [.phoneNumber, .generic]
if let image = UIImage(bundleImageName: "Call/CallKitLogo") {
providerConfiguration.iconTemplateImageData = UIImagePNGRepresentation(image)
}
return providerConfiguration
}
private func requestTransaction(_ transaction: CXTransaction, completion: ((Bool) -> Void)? = nil) {
self.callController.request(transaction) { error in
if let error = error {
print("Error requesting transaction: \(error)")
}
completion?(error == nil)
}
}
func endCall(uuid: UUID) {
let endCallAction = CXEndCallAction(call: uuid)
let transaction = CXTransaction(action: endCallAction)
self.requestTransaction(transaction)
}
func dropCall(uuid: UUID) {
self.provider.reportCall(with: uuid, endedAt: nil, reason: CXCallEndedReason.remoteEnded)
}
func answerCall(uuid: UUID) {
}
func startCall(peerId: PeerId, displayTitle: String) {
let uuid = UUID()
let handle = CXHandle(type: .generic, value: "\(peerId.id)")
let startCallAction = CXStartCallAction(call: uuid, handle: handle)
startCallAction.contactIdentifier = displayTitle
startCallAction.isVideo = false
let transaction = CXTransaction(action: startCallAction)
self.requestTransaction(transaction, completion: { _ in
let update = CXCallUpdate()
update.remoteHandle = handle
update.localizedCallerName = displayTitle
update.supportsHolding = false
update.supportsGrouping = false
update.supportsUngrouping = false
update.supportsDTMF = false
self.provider.reportCall(with: uuid, updated: update)
})
}
func reportIncomingCall(uuid: UUID, handle: String, displayTitle: String, completion: ((NSError?) -> Void)?) {
let update = CXCallUpdate()
update.remoteHandle = CXHandle(type: .generic, value: handle)
update.localizedCallerName = displayTitle
update.supportsHolding = false
update.supportsGrouping = false
update.supportsUngrouping = false
update.supportsDTMF = false
self.provider.reportNewIncomingCall(with: uuid, update: update, completion: { error in
completion?(error as NSError?)
})
}
func reportOutgoingCallConnecting(uuid: UUID, at date: Date) {
self.provider.reportOutgoingCall(with: uuid, startedConnectingAt: date)
}
func reportOutgoingCallConnected(uuid: UUID, at date: Date) {
self.provider.reportOutgoingCall(with: uuid, connectedAt: date)
}
func providerDidReset(_ provider: CXProvider) {
/*stopAudio()
for call in callManager.calls {
call.end()
}
callManager.removeAllCalls()*/
}
func provider(_ provider: CXProvider, perform action: CXStartCallAction) {
let disposable = MetaDisposable()
self.disposableSet.add(disposable)
disposable.set((self.startCall(action.callUUID, action.handle.value)
|> deliverOnMainQueue
|> afterDisposed { [weak self, weak disposable] in
if let strongSelf = self, let disposable = disposable {
strongSelf.disposableSet.remove(disposable)
}
}).start(next: { result in
if result {
action.fulfill()
} else {
action.fail()
}
}))
}
func provider(_ provider: CXProvider, perform action: CXAnswerCallAction) {
self.answerCall(action.callUUID)
action.fulfill()
}
func provider(_ provider: CXProvider, perform action: CXEndCallAction) {
let disposable = MetaDisposable()
self.disposableSet.add(disposable)
disposable.set((self.endCall(action.callUUID)
|> deliverOnMainQueue
|> afterDisposed { [weak self, weak disposable] in
if let strongSelf = self, let disposable = disposable {
strongSelf.disposableSet.remove(disposable)
}
}).start(next: { result in
if result {
action.fulfill(withDateEnded: Date())
} else {
action.fail()
}
}))
}
func provider(_ provider: CXProvider, didActivate audioSession: AVAudioSession) {
self.audioSessionActivationChanged(true)
self.audioSessionActivePromise.set(true)
}
func provider(_ provider: CXProvider, didDeactivate audioSession: AVAudioSession) {
self.audioSessionActivationChanged(false)
self.audioSessionActivePromise.set(false)
}
}

View File

@ -0,0 +1,536 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Postbox
import Display
import SwiftSignalKit
import TelegramCore
private let titleFont = Font.regular(17.0)
private let statusFont = Font.regular(14.0)
private let dateFont = Font.regular(15.0)
private func callDurationString(strings: PresentationStrings, duration: Int32) -> String {
if duration < 60 {
return strings.Call_ShortSeconds(duration)
} else {
return strings.Call_ShortMinutes(duration / 60)
}
}
class CallListCallItem: ListViewItem {
let theme: PresentationTheme
let strings: PresentationStrings
let account: Account
let topMessage: Message
let messages: [Message]
let editing: Bool
let revealed: Bool
let interaction: CallListNodeInteraction
let selectable: Bool = true
let headerAccessoryItem: ListViewAccessoryItem?
let header: ListViewItemHeader?
init(theme: PresentationTheme, strings: PresentationStrings, account: Account, topMessage: Message, messages: [Message], editing: Bool, revealed: Bool, interaction: CallListNodeInteraction) {
self.theme = theme
self.strings = strings
self.account = account
self.topMessage = topMessage
self.messages = messages
self.editing = editing
self.revealed = revealed
self.interaction = interaction
self.headerAccessoryItem = nil
self.header = nil
}
func nodeConfiguredForWidth(async: @escaping (@escaping () -> Void) -> Void, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, () -> Void)) -> Void) {
async {
let node = CallListCallItemNode()
let makeLayout = node.asyncLayout()
let (first, last, firstWithHeader) = CallListCallItem.mergeType(item: self, previousItem: previousItem, nextItem: nextItem)
let (nodeLayout, nodeApply) = makeLayout(self, width, first, last, firstWithHeader)
node.contentSize = nodeLayout.contentSize
node.insets = nodeLayout.insets
completion(node, {
return (nil, {
nodeApply().1(false)
})
})
}
}
func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: ListViewItemNode, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping () -> Void) -> Void) {
if let node = node as? CallListCallItemNode {
Queue.mainQueue().async {
let layout = node.asyncLayout()
async {
let (first, last, firstWithHeader) = CallListCallItem.mergeType(item: self, previousItem: previousItem, nextItem: nextItem)
let (nodeLayout, apply) = layout(self, width, first, last, firstWithHeader)
var animated = true
if case .None = animation {
animated = false
}
Queue.mainQueue().async {
completion(nodeLayout, {
apply().1(animated)
})
}
}
}
}
}
func selected(listView: ListView) {
listView.clearHighlightAnimated(true)
self.interaction.call(self.topMessage.id.peerId)
}
static func mergeType(item: CallListCallItem, previousItem: ListViewItem?, nextItem: ListViewItem?) -> (first: Bool, last: Bool, firstWithHeader: Bool) {
var first = false
var last = false
var firstWithHeader = false
if let previousItem = previousItem {
if let header = item.header {
if let previousItem = previousItem as? CallListCallItem {
firstWithHeader = header.id != previousItem.header?.id
} else {
firstWithHeader = true
}
}
} else {
first = true
firstWithHeader = item.header != nil
}
if let nextItem = nextItem {
if let header = item.header {
if let nextItem = nextItem as? CallListCallItem {
last = header.id != nextItem.header?.id
} else {
last = true
}
}
} else {
last = true
}
return (first, last, firstWithHeader)
}
}
private let separatorHeight = 1.0 / UIScreen.main.scale
class CallListCallItemNode: ItemListRevealOptionsItemNode {
private let backgroundNode: ASDisplayNode
private let separatorNode: ASDisplayNode
private let highlightedBackgroundNode: ASDisplayNode
private let avatarNode: AvatarNode
private let titleNode: TextNode
private let statusNode: TextNode
private let dateNode: TextNode
private let typeIconNode: ASImageNode
private let infoButtonNode: HighlightableButtonNode
var editableControlNode: ItemListEditableControlNode?
private var avatarState: (Account, Peer?)?
private var layoutParams: (CallListCallItem, CGFloat, Bool, Bool, Bool)?
required init() {
self.backgroundNode = ASDisplayNode()
self.backgroundNode.isLayerBacked = true
self.separatorNode = ASDisplayNode()
self.separatorNode.isLayerBacked = true
self.highlightedBackgroundNode = ASDisplayNode()
self.highlightedBackgroundNode.isLayerBacked = true
self.avatarNode = AvatarNode(font: Font.regular(15.0))
self.avatarNode.isLayerBacked = true
self.titleNode = TextNode()
self.statusNode = TextNode()
self.dateNode = TextNode()
self.typeIconNode = ASImageNode()
self.typeIconNode.isLayerBacked = true
self.typeIconNode.displayWithoutProcessing = true
self.typeIconNode.displaysAsynchronously = false
self.infoButtonNode = HighlightableButtonNode()
self.infoButtonNode.hitTestSlop = UIEdgeInsets(top: 4.0, left: 4.0, bottom: 4.0, right: 4.0)
super.init(layerBacked: false, dynamicBounce: false, rotated: false, seeThrough: false)
self.addSubnode(self.backgroundNode)
self.addSubnode(self.separatorNode)
self.addSubnode(self.avatarNode)
self.addSubnode(self.typeIconNode)
self.addSubnode(self.titleNode)
self.addSubnode(self.statusNode)
self.addSubnode(self.dateNode)
self.addSubnode(self.infoButtonNode)
self.infoButtonNode.addTarget(self, action: #selector(self.infoPressed), forControlEvents: .touchUpInside)
}
override func layoutForWidth(_ width: CGFloat, item: ListViewItem, previousItem: ListViewItem?, nextItem: ListViewItem?) {
if let (item, _, _, _, _) = self.layoutParams {
let (first, last, firstWithHeader) = CallListCallItem.mergeType(item: item, previousItem: previousItem, nextItem: nextItem)
self.layoutParams = (item, width, first, last, firstWithHeader)
let makeLayout = self.asyncLayout()
let (nodeLayout, nodeApply) = makeLayout(item, width, first, last, firstWithHeader)
self.contentSize = nodeLayout.contentSize
self.insets = nodeLayout.insets
let _ = nodeApply()
}
}
override func setHighlighted(_ highlighted: Bool, animated: Bool) {
super.setHighlighted(highlighted, animated: animated)
if highlighted {
self.highlightedBackgroundNode.alpha = 1.0
if self.highlightedBackgroundNode.supernode == nil {
self.insertSubnode(self.highlightedBackgroundNode, aboveSubnode: self.separatorNode)
}
} else {
if self.highlightedBackgroundNode.supernode != nil {
if animated {
self.highlightedBackgroundNode.layer.animateAlpha(from: self.highlightedBackgroundNode.alpha, to: 0.0, duration: 0.4, completion: { [weak self] completed in
if let strongSelf = self {
if completed {
strongSelf.highlightedBackgroundNode.removeFromSupernode()
}
}
})
self.highlightedBackgroundNode.alpha = 0.0
} else {
self.highlightedBackgroundNode.removeFromSupernode()
}
}
}
}
func asyncLayout() -> (_ item: CallListCallItem, _ width: CGFloat, _ first: Bool, _ last: Bool, _ firstWithHeader: Bool) -> (ListViewItemNodeLayout, () -> (Signal<Void, NoError>?, (Bool) -> Void)) {
let makeTitleLayout = TextNode.asyncLayout(self.titleNode)
let makeStatusLayout = TextNode.asyncLayout(self.statusNode)
let makeDateLayout = TextNode.asyncLayout(self.dateNode)
let editableControlLayout = ItemListEditableControlNode.asyncLayout(self.editableControlNode)
let currentItem = self.layoutParams?.0
return { [weak self] item, width, first, last, firstWithHeader in
var updatedTheme: PresentationTheme?
var updatedInfoIcon = false
if currentItem?.theme !== item.theme {
updatedTheme = item.theme
updatedInfoIcon = true
}
let editingOffset: CGFloat
var editableControlSizeAndApply: (CGSize, () -> ItemListEditableControlNode)?
if item.editing {
let sizeAndApply = editableControlLayout(56.0)
editableControlSizeAndApply = sizeAndApply
editingOffset = sizeAndApply.0.width
} else {
editingOffset = 0.0
}
var leftInset: CGFloat = 86.0
let rightInset: CGFloat = 13.0
var infoIconRightInset: CGFloat = rightInset
var dateRightInset: CGFloat = 43.0
if item.editing {
leftInset += editingOffset
dateRightInset += 5.0
infoIconRightInset -= 36.0
}
var titleAttributedString: NSAttributedString?
var statusAttributedString: NSAttributedString?
var titleColor = item.theme.list.itemPrimaryTextColor
var hasMissed = false
var hasIncoming = false
var hasOutgoing = false
var hadDuration = false
var callDuration: Int32?
for message in item.messages {
inner: for media in message.media {
if let action = media as? TelegramMediaAction {
if case let .phoneCall(_, discardReason, duration) = action.action {
if message.flags.contains(.Incoming) {
hasIncoming = true
if let discardReason = discardReason, case .missed = discardReason {
titleColor = item.theme.list.itemDestructiveColor
hasMissed = true
}
} else {
hasOutgoing = true
}
if callDuration == nil && !hadDuration {
hadDuration = true
callDuration = duration
} else {
callDuration = nil
}
}
break inner
}
}
}
if let peer = item.topMessage.peers[item.topMessage.id.peerId] {
if let user = peer as? TelegramUser {
if let firstName = user.firstName, let lastName = user.lastName, !firstName.isEmpty, !lastName.isEmpty {
let string = NSMutableAttributedString()
string.append(NSAttributedString(string: firstName, font: titleFont, textColor: titleColor))
string.append(NSAttributedString(string: " ", font: titleFont, textColor: titleColor))
string.append(NSAttributedString(string: lastName, font: titleFont, textColor: titleColor))
if item.messages.count > 1 {
string.append(NSAttributedString(string: " (\(item.messages.count))", font: titleFont, textColor: titleColor))
}
titleAttributedString = string
} else if let firstName = user.firstName, !firstName.isEmpty {
titleAttributedString = NSAttributedString(string: firstName, font: titleFont, textColor: titleColor)
} else if let lastName = user.lastName, !lastName.isEmpty {
titleAttributedString = NSAttributedString(string: lastName, font: titleFont, textColor: titleColor)
} else {
titleAttributedString = NSAttributedString(string: item.strings.User_DeletedAccount, font: titleFont, textColor: titleColor)
}
} else if let group = peer as? TelegramGroup {
titleAttributedString = NSAttributedString(string: group.title, font: titleFont, textColor: titleColor)
} else if let channel = peer as? TelegramChannel {
titleAttributedString = NSAttributedString(string: channel.title, font: titleFont, textColor: titleColor)
}
if hasMissed {
statusAttributedString = NSAttributedString(string: item.strings.Calls_Missed, font: statusFont, textColor: item.theme.list.itemSecondaryTextColor)
} else if hasIncoming && hasOutgoing {
statusAttributedString = NSAttributedString(string: item.strings.Notification_CallOutgoingShort + ", " + item.strings.Notification_CallIncomingShort, font: statusFont, textColor: item.theme.list.itemSecondaryTextColor)
} else if hasIncoming {
if let callDuration = callDuration, callDuration != 0 {
statusAttributedString = NSAttributedString(string: item.strings.Notification_CallTimeFormat(item.strings.Notification_CallIncomingShort, callDurationString(strings: item.strings, duration: callDuration)).0, font: statusFont, textColor: item.theme.list.itemSecondaryTextColor)
} else {
statusAttributedString = NSAttributedString(string: item.strings.Notification_CallIncomingShort, font: statusFont, textColor: item.theme.list.itemSecondaryTextColor)
}
} else {
if let callDuration = callDuration, callDuration != 0 {
statusAttributedString = NSAttributedString(string: item.strings.Notification_CallTimeFormat(item.strings.Notification_CallOutgoingShort, callDurationString(strings: item.strings, duration: callDuration)).0, font: statusFont, textColor: item.theme.list.itemSecondaryTextColor)
} else {
statusAttributedString = NSAttributedString(string: item.strings.Notification_CallOutgoingShort, font: statusFont, textColor: item.theme.list.itemSecondaryTextColor)
}
}
}
var t = Int(item.topMessage.timestamp)
var timeinfo = tm()
localtime_r(&t, &timeinfo)
let timestamp = Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970)
let dateText = stringForRelativeTimestamp(strings: item.strings, relativeTimestamp: item.topMessage.timestamp, relativeTo: timestamp)
let (titleLayout, titleApply) = makeTitleLayout(titleAttributedString, nil, 1, .end, CGSize(width: max(0.0, width - leftInset - rightInset), height: CGFloat.infinity), .natural, nil, UIEdgeInsets())
let (statusLayout, statusApply) = makeStatusLayout(statusAttributedString, nil, 1, .end, CGSize(width: max(0.0, width - leftInset - rightInset), height: CGFloat.infinity), .natural, nil, UIEdgeInsets())
let (dateLayout, dateApply) = makeDateLayout(NSAttributedString(string: dateText, font: dateFont, textColor: item.theme.list.itemSecondaryTextColor), nil, 1, .end, CGSize(width: max(0.0, width - leftInset - rightInset), height: CGFloat.infinity), .natural, nil, UIEdgeInsets())
let nodeLayout = ListViewItemNodeLayout(contentSize: CGSize(width: width, height: 56.0), insets: UIEdgeInsets(top: firstWithHeader ? 29.0 : 0.0, left: 0.0, bottom: 0.0, right: 0.0))
let outgoingIcon = PresentationResourcesCallList.outgoingIcon(item.theme)
let infoIcon = PresentationResourcesCallList.infoButton(item.theme)
return (nodeLayout, { [weak self] in
if let strongSelf = self {
if let peer = item.topMessage.peers[item.topMessage.id.peerId] {
strongSelf.avatarNode.setPeer(account: item.account, peer: peer)
}
return (strongSelf.avatarNode.ready, { [weak strongSelf] animated in
if let strongSelf = strongSelf {
strongSelf.layoutParams = (item, width, first, last, firstWithHeader)
let revealOffset = strongSelf.revealOffset
let transition: ContainedViewLayoutTransition
if animated {
transition = ContainedViewLayoutTransition.animated(duration: 0.4, curve: .spring)
} else {
transition = .immediate
}
if let _ = updatedTheme {
strongSelf.separatorNode.backgroundColor = item.theme.list.itemSeparatorColor
strongSelf.backgroundNode.backgroundColor = item.theme.list.itemBackgroundColor
strongSelf.highlightedBackgroundNode.backgroundColor = item.theme.list.itemHighlightedBackgroundColor
}
if let editableControlSizeAndApply = editableControlSizeAndApply {
if strongSelf.editableControlNode == nil {
let editableControlNode = editableControlSizeAndApply.1()
editableControlNode.tapped = {
if let strongSelf = self {
strongSelf.setRevealOptionsOpened(true, animated: true)
strongSelf.revealOptionsInteractivelyOpened()
}
}
strongSelf.editableControlNode = editableControlNode
strongSelf.addSubnode(editableControlNode)
let editableControlFrame = CGRect(origin: CGPoint(x: revealOffset, y: 0.0), size: editableControlSizeAndApply.0)
editableControlNode.frame = editableControlFrame
transition.animatePosition(node: editableControlNode, from: CGPoint(x: editableControlFrame.midX - editableControlFrame.size.width, y: editableControlFrame.midY))
editableControlNode.alpha = 0.0
transition.updateAlpha(node: editableControlNode, alpha: 1.0)
}
} else if let editableControlNode = strongSelf.editableControlNode {
var editableControlFrame = editableControlNode.frame
editableControlFrame.origin.x = -editableControlFrame.size.width
strongSelf.editableControlNode = nil
transition.updateAlpha(node: editableControlNode, alpha: 0.0)
transition.updateFrame(node: editableControlNode, frame: editableControlFrame, completion: { [weak editableControlNode] _ in
editableControlNode?.removeFromSupernode()
})
}
transition.updateFrame(node: strongSelf.avatarNode, frame: CGRect(origin: CGPoint(x: revealOffset + leftInset - 52.0, y: 8.0), size: CGSize(width: 40.0, height: 40.0)))
let _ = titleApply()
transition.updateFrame(node: strongSelf.titleNode, frame: CGRect(origin: CGPoint(x: revealOffset + leftInset, y: 8.0), size: titleLayout.size))
let _ = statusApply()
transition.updateFrame(node: strongSelf.statusNode, frame: CGRect(origin: CGPoint(x: revealOffset + leftInset, y: 30.0), size: statusLayout.size))
let _ = dateApply()
transition.updateFrame(node: strongSelf.dateNode, frame: CGRect(origin: CGPoint(x: editingOffset + revealOffset + width - dateRightInset - dateLayout.size.width, y: floor((nodeLayout.contentSize.height - dateLayout.size.height) / 2.0) + 2.0), size: dateLayout.size))
if let outgoingIcon = outgoingIcon {
if strongSelf.typeIconNode.image !== outgoingIcon {
strongSelf.typeIconNode.image = outgoingIcon
}
transition.updateFrame(node: strongSelf.typeIconNode, frame: CGRect(origin: CGPoint(x: revealOffset + leftInset - 76.0, y: floor((nodeLayout.contentSize.height - outgoingIcon.size.height) / 2.0)), size: outgoingIcon.size))
}
strongSelf.typeIconNode.isHidden = !hasOutgoing
if let infoIcon = infoIcon {
if updatedInfoIcon {
strongSelf.infoButtonNode.setImage(infoIcon, for: [])
}
transition.updateFrame(node: strongSelf.infoButtonNode, frame: CGRect(origin: CGPoint(x: revealOffset + width - infoIconRightInset - infoIcon.size.width, y: floor((nodeLayout.contentSize.height - infoIcon.size.height) / 2.0)), size: infoIcon.size))
}
transition.updateAlpha(node: strongSelf.infoButtonNode, alpha: item.editing ? 0.0 : 1.0)
let topHighlightInset: CGFloat = (first || !nodeLayout.insets.top.isZero) ? 0.0 : separatorHeight
strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: nodeLayout.contentSize.width, height: nodeLayout.contentSize.height))
strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -nodeLayout.insets.top - topHighlightInset), size: CGSize(width: nodeLayout.size.width, height: nodeLayout.size.height + topHighlightInset))
transition.updateFrame(node: strongSelf.separatorNode, frame: CGRect(origin: CGPoint(x: leftInset, y: nodeLayout.contentSize.height - separatorHeight), size: CGSize(width: max(0.0, nodeLayout.size.width - 65.0), height: separatorHeight)))
strongSelf.separatorNode.isHidden = last
strongSelf.setRevealOptions([ItemListRevealOption(key: 0, title: item.strings.Common_Delete, icon: nil, color: UIColor(rgb: 0xff3824))])
}
})
} else {
return (nil, { _ in })
}
})
}
}
override func layoutHeaderAccessoryItemNode(_ accessoryItemNode: ListViewAccessoryItemNode) {
let bounds = self.bounds
accessoryItemNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -29.0), size: CGSize(width: bounds.size.width, height: 29.0))
}
override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) {
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration * 0.5)
}
override func animateRemoved(_ currentTimestamp: Double, duration: Double) {
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration * 0.3, removeOnCompletion: false)
}
override public func header() -> ListViewItemHeader? {
if let (item, _, _, _, _) = self.layoutParams {
return item.header
} else {
return nil
}
}
@objc func infoPressed() {
if let item = self.layoutParams?.0 {
item.interaction.openInfo(item.topMessage.id.peerId)
}
}
override func revealOptionsInteractivelyOpened() {
if let item = self.layoutParams?.0 {
item.interaction.setMessageIdWithRevealedOptions(item.topMessage.id, nil)
}
}
override func revealOptionsInteractivelyClosed() {
if let item = self.layoutParams?.0 {
item.interaction.setMessageIdWithRevealedOptions(nil, item.topMessage.id)
}
}
override func updateRevealOffset(offset: CGFloat, transition: ContainedViewLayoutTransition) {
super.updateRevealOffset(offset: offset, transition: transition)
if let item = self.layoutParams?.0 {
let revealOffset = offset
let editingOffset: CGFloat
if let editableControlNode = self.editableControlNode {
editingOffset = editableControlNode.bounds.size.width
var editableControlFrame = editableControlNode.frame
editableControlFrame.origin.x = offset
transition.updateFrame(node: editableControlNode, frame: editableControlFrame)
} else {
editingOffset = 0.0
}
let leftInset: CGFloat = 86.0 + editingOffset
let rightInset: CGFloat = 13.0
var infoIconRightInset: CGFloat = rightInset
var dateRightInset: CGFloat = 43.0
if item.editing {
dateRightInset += 5.0
infoIconRightInset -= 36.0
}
transition.updateFrame(node: self.avatarNode, frame: CGRect(origin: CGPoint(x: revealOffset + leftInset - 52.0, y: 8.0), size: CGSize(width: 40.0, height: 40.0)))
transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(x: revealOffset + leftInset, y: 8.0), size: self.titleNode.bounds.size))
transition.updateFrame(node: self.statusNode, frame: CGRect(origin: CGPoint(x: revealOffset + leftInset, y: 30.0), size: self.statusNode.bounds.size))
transition.updateFrame(node: self.dateNode, frame: CGRect(origin: CGPoint(x: editingOffset + revealOffset + self.bounds.size.width - dateRightInset - self.dateNode.bounds.size.width, y: self.dateNode.frame.minY), size: self.dateNode.bounds.size))
transition.updateFrame(node: self.typeIconNode, frame: CGRect(origin: CGPoint(x: revealOffset + leftInset - 76.0, y: self.typeIconNode.frame.minY), size: self.typeIconNode.bounds.size))
transition.updateFrame(node: self.infoButtonNode, frame: CGRect(origin: CGPoint(x: revealOffset + self.bounds.size.width - infoIconRightInset - self.infoButtonNode.bounds.width, y: self.infoButtonNode.frame.minY), size: self.infoButtonNode.bounds.size))
}
}
override func revealOptionSelected(_ option: ItemListRevealOption) {
self.setRevealOptionsOpened(false, animated: true)
self.revealOptionsInteractivelyClosed()
if let item = self.layoutParams?.0 {
item.interaction.delete(item.messages.map { $0.id })
}
}
}

View File

@ -0,0 +1,196 @@
import Foundation
import Display
import AsyncDisplayKit
import Postbox
import TelegramCore
import SwiftSignalKit
public final class CallListController: ViewController {
private var controllerNode: CallListControllerNode {
return self.displayNode as! CallListControllerNode
}
private let _ready = Promise<Bool>(false)
override public var ready: Promise<Bool> {
return self._ready
}
private let account: Account
private var presentationData: PresentationData
private var presentationDataDisposable: Disposable?
private let segmentedTitleView: ItemListControllerSegmentedTitleView
private let createActionDisposable = MetaDisposable()
public init(account: Account) {
self.account = account
self.presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 }
self.segmentedTitleView = ItemListControllerSegmentedTitleView(segments: [self.presentationData.strings.Calls_All, self.presentationData.strings.Calls_Missed], index: 0, color: self.presentationData.theme.rootController.navigationBar.accentTextColor)
super.init(navigationBarTheme: NavigationBarTheme(rootControllerTheme: self.presentationData.theme))
self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBar.style.style
self.navigationItem.titleView = self.segmentedTitleView
self.navigationItem.rightBarButtonItem = UIBarButtonItem(image: PresentationResourcesRootController.navigationCallIcon(self.presentationData.theme), style: .plain, target: self, action: #selector(self.callPressed))
self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Edit, style: .plain, target: self, action: #selector(self.editPressed))
self.tabBarItem.title = self.presentationData.strings.Calls_TabTitle
self.tabBarItem.image = UIImage(bundleImageName: "Chat List/Tabs/IconCalls")
self.tabBarItem.selectedImage = UIImage(bundleImageName: "Chat List/Tabs/IconCallsSelected")
self.segmentedTitleView.indexUpdated = { [weak self] index in
if let strongSelf = self {
strongSelf.controllerNode.updateType(index == 0 ? .all : .missed)
}
}
self.presentationDataDisposable = (account.telegramApplicationContext.presentationData
|> deliverOnMainQueue).start(next: { [weak self] presentationData in
if let strongSelf = self {
let previousTheme = strongSelf.presentationData.theme
let previousStrings = strongSelf.presentationData.strings
strongSelf.presentationData = presentationData
if previousTheme !== presentationData.theme || previousStrings !== presentationData.strings {
strongSelf.updateThemeAndStrings()
}
}
})
self.scrollToTop = { [weak self] in
self?.controllerNode.scrollToLatest()
}
}
required public init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
self.createActionDisposable.dispose()
self.presentationDataDisposable?.dispose()
}
private func updateThemeAndStrings() {
self.segmentedTitleView.segments = [self.presentationData.strings.Calls_All, self.presentationData.strings.Calls_Missed]
self.segmentedTitleView.color = self.presentationData.theme.rootController.navigationBar.accentTextColor
self.tabBarItem.title = self.presentationData.strings.Calls_TabTitle
self.navigationItem.rightBarButtonItem = UIBarButtonItem(image: PresentationResourcesRootController.navigationCallIcon(self.presentationData.theme), style: .plain, target: self, action: #selector(self.callPressed))
self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Edit, style: .plain, target: self, action: #selector(self.editPressed))
self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBar.style.style
self.navigationBar?.updateTheme(NavigationBarTheme(rootControllerTheme: self.presentationData.theme))
if self.isNodeLoaded {
self.controllerNode.updateThemeAndStrings(theme: self.presentationData.theme, strings: self.presentationData.strings)
}
}
override public func loadDisplayNode() {
self.displayNode = CallListControllerNode(account: self.account, presentationData: self.presentationData, call: { [weak self] peerId in
if let strongSelf = self {
strongSelf.call(peerId)
}
}, openInfo: { [weak self] peerId in
if let strongSelf = self {
let _ = (strongSelf.account.postbox.loadedPeerWithId(peerId)
|> take(1)
|> deliverOnMainQueue).start(next: { peer in
if let strongSelf = self {
if let infoController = peerInfoController(account: strongSelf.account, peer: peer) {
(strongSelf.navigationController as? NavigationController)?.pushViewController(infoController)
}
}
})
}
})
self._ready.set(self.controllerNode.ready)
self.displayNodeDidLoad()
}
override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
super.containerLayoutUpdated(layout, transition: transition)
self.controllerNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationHeight, transition: transition)
}
@objc func callPressed() {
let controller = ContactSelectionController(account: self.account, title: { $0.Calls_NewCall })
self.createActionDisposable.set((controller.result
|> take(1)
|> deliverOnMainQueue).start(next: { [weak controller, weak self] peerId in
controller?.dismissSearch()
if let strongSelf = self, let peerId = peerId {
strongSelf.call(peerId, began: {
if let strongSelf = self {
if let hasOngoingCall = strongSelf.account.telegramApplicationContext.hasOngoingCall {
let _ = (hasOngoingCall
|> filter { $0 }
|> timeout(1.0, queue: Queue.mainQueue(), alternate: .single(true))
|> delay(0.5, queue: Queue.mainQueue())
|> deliverOnMainQueue).start(next: { _ in
if let strongSelf = self, let controller = controller, let navigationController = controller.navigationController as? NavigationController {
if navigationController.viewControllers.last === controller {
let _ = navigationController.popViewController(animated: true)
}
}
})
}
}
})
}
}))
(self.navigationController as? NavigationController)?.pushViewController(controller)
}
@objc func editPressed() {
self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Done, style: .done, target: self, action: #selector(self.donePressed))
self.controllerNode.updateState { state in
return state.withUpdatedEditing(true)
}
}
@objc func donePressed() {
self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Edit, style: .plain, target: self, action: #selector(self.editPressed))
self.controllerNode.updateState { state in
return state.withUpdatedEditing(false)
}
}
private func call(_ peerId: PeerId, began: (() -> Void)? = nil) {
let callResult = self.account.telegramApplicationContext.callManager?.requestCall(peerId: peerId, endCurrentIfAny: false)
if let callResult = callResult {
if case let .alreadyInProgress(currentPeerId) = callResult {
if currentPeerId == peerId {
began?()
self.account.telegramApplicationContext.navigateToCurrentCall?()
} else {
let presentationData = self.presentationData
let _ = (self.account.postbox.modify { modifier -> (Peer?, Peer?) in
return (modifier.getPeer(peerId), modifier.getPeer(currentPeerId))
} |> deliverOnMainQueue).start(next: { [weak self] peer, current in
if let strongSelf = self, let peer = peer, let current = current {
strongSelf.present(standardTextAlertController(title: presentationData.strings.Call_CallInProgressTitle, text: presentationData.strings.Call_CallInProgressMessage(current.compactDisplayTitle, peer.compactDisplayTitle).0, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .genericAction, title: presentationData.strings.Common_OK, action: {
if let strongSelf = self {
let _ = strongSelf.account.telegramApplicationContext.callManager?.requestCall(peerId: peerId, endCurrentIfAny: true)
began?()
}
})]), in: .window)
}
})
}
} else {
began?()
}
}
}
}

View File

@ -0,0 +1,417 @@
import Foundation
import AsyncDisplayKit
import Display
import Postbox
import TelegramCore
import SwiftSignalKit
private struct CallListNodeListViewTransition {
let callListView: CallListNodeView
let deleteItems: [ListViewDeleteItem]
let insertItems: [ListViewInsertItem]
let updateItems: [ListViewUpdateItem]
let options: ListViewDeleteAndInsertOptions
let scrollToItem: ListViewScrollToItem?
let stationaryItemRange: (Int, Int)?
}
private extension CallListViewEntry {
var lowestIndex: MessageIndex {
switch self {
case let .hole(index):
return index
case let .message(_, messages):
var lowest = MessageIndex(messages[0])
for i in 1 ..< messages.count {
let index = MessageIndex(messages[i])
if index < lowest {
lowest = index
}
}
return lowest
}
}
var highestIndex: MessageIndex {
switch self {
case let .hole(index):
return index
case let .message(_, messages):
var highest = MessageIndex(messages[0])
for i in 1 ..< messages.count {
let index = MessageIndex(messages[i])
if index > highest {
highest = index
}
}
return highest
}
}
}
final class CallListNodeInteraction {
let setMessageIdWithRevealedOptions: (MessageId?, MessageId?) -> Void
let call: (PeerId) -> Void
let openInfo: (PeerId) -> Void
let delete: ([MessageId]) -> Void
init(setMessageIdWithRevealedOptions: @escaping (MessageId?, MessageId?) -> Void, call: @escaping (PeerId) -> Void, openInfo: @escaping (PeerId) -> Void, delete: @escaping ([MessageId]) -> Void) {
self.setMessageIdWithRevealedOptions = setMessageIdWithRevealedOptions
self.call = call
self.openInfo = openInfo
self.delete = delete
}
}
struct CallListNodeState: Equatable {
let theme: PresentationTheme
let strings: PresentationStrings
let editing: Bool
let messageIdWithRevealedOptions: MessageId?
func withUpdatedPresentationData(theme: PresentationTheme, strings: PresentationStrings) -> CallListNodeState {
return CallListNodeState(theme: theme, strings: strings, editing: self.editing, messageIdWithRevealedOptions: self.messageIdWithRevealedOptions)
}
func withUpdatedEditing(_ editing: Bool) -> CallListNodeState {
return CallListNodeState(theme: self.theme, strings: self.strings, editing: editing, messageIdWithRevealedOptions: self.messageIdWithRevealedOptions)
}
func withUpdatedMessageIdWithRevealedOptions(_ messageIdWithRevealedOptions: MessageId?) -> CallListNodeState {
return CallListNodeState(theme: self.theme, strings: self.strings, editing: self.editing, messageIdWithRevealedOptions: messageIdWithRevealedOptions)
}
static func ==(lhs: CallListNodeState, rhs: CallListNodeState) -> Bool {
if lhs.theme !== rhs.theme {
return false
}
if lhs.strings !== rhs.strings {
return false
}
if lhs.editing != rhs.editing {
return false
}
if lhs.messageIdWithRevealedOptions != rhs.messageIdWithRevealedOptions {
return false
}
return true
}
}
private func mappedInsertEntries(account: Account, nodeInteraction: CallListNodeInteraction, entries: [CallListNodeViewTransitionInsertEntry]) -> [ListViewInsertItem] {
return entries.map { entry -> ListViewInsertItem in
switch entry.entry {
case let .messageEntry(topMessage, messages, theme, strings, editing, hasActiveRevealControls):
return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: CallListCallItem(theme: theme, strings: strings, account: account, topMessage: topMessage, messages: messages, editing: editing, revealed: hasActiveRevealControls, interaction: nodeInteraction), directionHint: entry.directionHint)
case let .holeEntry(theme):
return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListHoleItem(), directionHint: entry.directionHint)
}
}
}
private func mappedUpdateEntries(account: Account, nodeInteraction: CallListNodeInteraction, entries: [CallListNodeViewTransitionUpdateEntry]) -> [ListViewUpdateItem] {
return entries.map { entry -> ListViewUpdateItem in
switch entry.entry {
case let .messageEntry(topMessage, messages, theme, strings, editing, hasActiveRevealControls):
return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: CallListCallItem(theme: theme, strings: strings, account: account, topMessage: topMessage, messages: messages, editing: editing, revealed: hasActiveRevealControls, interaction: nodeInteraction), directionHint: entry.directionHint)
case let .holeEntry(theme):
return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListHoleItem(), directionHint: entry.directionHint)
}
}
}
private func mappedCallListNodeViewListTransition(account: Account, nodeInteraction: CallListNodeInteraction, transition: CallListNodeViewTransition) -> CallListNodeListViewTransition {
return CallListNodeListViewTransition(callListView: transition.callListView, deleteItems: transition.deleteItems, insertItems: mappedInsertEntries(account: account, nodeInteraction: nodeInteraction, entries: transition.insertEntries), updateItems: mappedUpdateEntries(account: account, nodeInteraction: nodeInteraction, entries: transition.updateEntries), options: transition.options, scrollToItem: transition.scrollToItem, stationaryItemRange: transition.stationaryItemRange)
}
private final class CallListOpaqueTransactionState {
let callListView: CallListNodeView
init(callListView: CallListNodeView) {
self.callListView = callListView
}
}
final class CallListControllerNode: ASDisplayNode {
private let account: Account
private var presentationData: PresentationData
private var containerLayout: (ContainerViewLayout, CGFloat)?
private let _ready = ValuePromise<Bool>()
private var didSetReady = false
var ready: Signal<Bool, NoError> {
return _ready.get()
}
var peerSelected: ((PeerId) -> Void)?
var activateSearch: (() -> Void)?
var deletePeerChat: ((PeerId) -> Void)?
private let viewProcessingQueue = Queue()
private var callListView: CallListNodeView?
private var dequeuedInitialTransitionOnLayout = false
private var enqueuedTransition: (CallListNodeListViewTransition, () -> Void)?
private var currentState: CallListNodeState
private let statePromise: ValuePromise<CallListNodeState>
private var currentLocationAndType = CallListNodeLocationAndType(location: .initial(count: 50), type: .all)
private let callListLocationAndType = ValuePromise<CallListNodeLocationAndType>()
private let callListDisposable = MetaDisposable()
private let listNode: ListView
private let call: (PeerId) -> Void
private let openInfo: (PeerId) -> Void
init(account: Account, presentationData: PresentationData, call: @escaping (PeerId) -> Void, openInfo: @escaping (PeerId) -> Void) {
self.account = account
self.presentationData = presentationData
self.call = call
self.openInfo = openInfo
self.currentState = CallListNodeState(theme: presentationData.theme, strings: presentationData.strings, editing: false, messageIdWithRevealedOptions: nil)
self.statePromise = ValuePromise(self.currentState, ignoreRepeated: true)
self.listNode = ListView()
super.init(viewBlock: {
return UITracingLayerView()
}, didLoad: nil)
self.addSubnode(self.listNode)
self.backgroundColor = presentationData.theme.chatList.backgroundColor
let nodeInteraction = CallListNodeInteraction(setMessageIdWithRevealedOptions: { [weak self] messageId, fromMessageId in
if let strongSelf = self {
strongSelf.updateState { state in
if (messageId == nil && fromMessageId == state.messageIdWithRevealedOptions) || (messageId != nil && fromMessageId == nil) {
return state.withUpdatedMessageIdWithRevealedOptions(messageId)
} else {
return state
}
}
}
}, call: { [weak self] peerId in
self?.call(peerId)
}, openInfo: { [weak self] peerId in
self?.openInfo(peerId)
}, delete: { [weak self] messageIds in
if let strongSelf = self {
let _ = deleteMessagesInteractively(postbox: strongSelf.account.postbox, messageIds: messageIds, type: .forLocalPeer).start()
}
})
let viewProcessingQueue = self.viewProcessingQueue
let callListViewUpdate = self.callListLocationAndType.get()
|> distinctUntilChanged
|> mapToSignal { locationAndType in
return callListViewForLocationAndType(locationAndType: locationAndType, account: account)
}
let previousView = Atomic<CallListNodeView?>(value: nil)
let callListNodeViewTransition = combineLatest(callListViewUpdate, self.statePromise.get()) |> mapToQueue { (update, state) -> Signal<CallListNodeListViewTransition, NoError> in
let processedView = CallListNodeView(originalView: update.view, filteredEntries: callListNodeEntriesForView(update.view, state: state))
let previous = previousView.swap(processedView)
let reason: CallListNodeViewTransitionReason
var prepareOnMainQueue = false
var previousWasEmptyOrSingleHole = false
if let previous = previous {
if previous.filteredEntries.count == 1 {
if case .holeEntry = previous.filteredEntries[0] {
previousWasEmptyOrSingleHole = true
}
}
} else {
previousWasEmptyOrSingleHole = true
}
if previousWasEmptyOrSingleHole {
reason = .initial
if previous == nil {
prepareOnMainQueue = true
}
} else {
switch update.type {
case .InitialUnread:
reason = .initial
prepareOnMainQueue = true
case .Generic:
reason = .interactiveChanges
case .UpdateVisible:
reason = .reload
case .FillHole:
reason = .reload
}
}
return preparedCallListNodeViewTransition(from: previous, to: processedView, reason: reason, account: account, scrollPosition: update.scrollPosition)
|> map({ mappedCallListNodeViewListTransition(account: account, nodeInteraction: nodeInteraction, transition: $0) })
|> runOn(prepareOnMainQueue ? Queue.mainQueue() : viewProcessingQueue)
}
let appliedTransition = callListNodeViewTransition |> deliverOnMainQueue |> mapToQueue { [weak self] transition -> Signal<Void, NoError> in
if let strongSelf = self {
return strongSelf.enqueueTransition(transition)
}
return .complete()
}
self.listNode.displayedItemRangeChanged = { [weak self] range, transactionOpaqueState in
if let strongSelf = self, let range = range.loadedRange, let view = (transactionOpaqueState as? CallListOpaqueTransactionState)?.callListView.originalView {
var location: CallListNodeLocation?
if range.firstIndex < 5 && view.later != nil {
location = .navigation(index: view.entries[view.entries.count - 1].highestIndex)
} else if range.firstIndex >= 5 && range.lastIndex >= view.entries.count - 5 && view.earlier != nil {
location = .navigation(index: view.entries[0].lowestIndex)
}
if let location = location, location != strongSelf.currentLocationAndType.location {
strongSelf.currentLocationAndType = CallListNodeLocationAndType(location: location, type: strongSelf.currentLocationAndType.type)
strongSelf.callListLocationAndType.set(strongSelf.currentLocationAndType)
}
}
}
self.callListDisposable.set(appliedTransition.start())
self.callListLocationAndType.set(self.currentLocationAndType)
}
deinit {
self.callListDisposable.dispose()
}
func updateThemeAndStrings(theme: PresentationTheme, strings: PresentationStrings) {
if theme !== self.currentState.theme || strings !== self.currentState.strings {
self.backgroundColor = theme.chatList.backgroundColor
self.updateState {
return $0.withUpdatedPresentationData(theme: theme, strings: strings)
}
}
}
func updateState(_ f: (CallListNodeState) -> CallListNodeState) {
let state = f(self.currentState)
if state != self.currentState {
self.currentState = state
self.statePromise.set(state)
}
}
func updateType(_ type: CallListViewType) {
if type != self.currentLocationAndType.type {
if let view = self.callListView?.originalView, !view.entries.isEmpty {
self.currentLocationAndType = CallListNodeLocationAndType(location: .changeType(index: view.entries[view.entries.count - 1].highestIndex), type: type)
self.callListLocationAndType.set(self.currentLocationAndType)
}
}
}
private func enqueueTransition(_ transition: CallListNodeListViewTransition) -> Signal<Void, NoError> {
return Signal { [weak self] subscriber in
if let strongSelf = self {
if let _ = strongSelf.enqueuedTransition {
preconditionFailure()
}
strongSelf.enqueuedTransition = (transition, {
subscriber.putCompletion()
})
if strongSelf.isNodeLoaded {
strongSelf.dequeueTransition()
} else {
if !strongSelf.didSetReady {
strongSelf.didSetReady = true
strongSelf._ready.set(true)
}
}
} else {
subscriber.putCompletion()
}
return EmptyDisposable
} |> runOn(Queue.mainQueue())
}
private func dequeueTransition() {
if let (transition, completion) = self.enqueuedTransition {
self.enqueuedTransition = nil
let completion: (ListViewDisplayedItemRange) -> Void = { [weak self] visibleRange in
if let strongSelf = self {
strongSelf.callListView = transition.callListView
if !strongSelf.didSetReady {
strongSelf.didSetReady = true
strongSelf._ready.set(true)
}
completion()
}
}
self.listNode.transaction(deleteIndices: transition.deleteItems, insertIndicesAndItems: transition.insertItems, updateIndicesAndItems: transition.updateItems, options: transition.options, scrollToItem: transition.scrollToItem, stationaryItemRange: transition.stationaryItemRange, updateOpaqueState: CallListOpaqueTransactionState(callListView: transition.callListView), completion: completion)
}
}
func scrollToLatest() {
if let view = self.callListView?.originalView, view.later == nil {
self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous], scrollToItem: ListViewScrollToItem(index: 0, position: .Top, animated: true, curve: .Default, directionHint: .Up), updateSizeAndInsets: nil, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in })
} else {
let location: CallListNodeLocation = .scroll(index: MessageIndex.absoluteUpperBound(), sourceIndex: MessageIndex.absoluteLowerBound(), scrollPosition: .Top, animated: true)
self.currentLocationAndType = CallListNodeLocationAndType(location: location, type: self.currentLocationAndType.type)
self.callListLocationAndType.set(self.currentLocationAndType)
}
}
func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) {
self.containerLayout = (layout, navigationBarHeight)
var insets = layout.insets(options: [.input])
insets.top += max(navigationBarHeight, layout.insets(options: [.statusBar]).top)
self.listNode.bounds = CGRect(x: 0.0, y: 0.0, width: layout.size.width, height: layout.size.height)
self.listNode.position = CGPoint(x: layout.size.width / 2.0, y: layout.size.height / 2.0)
var duration: Double = 0.0
var curve: UInt = 0
switch transition {
case .immediate:
break
case let .animated(animationDuration, animationCurve):
duration = animationDuration
switch animationCurve {
case .easeInOut:
break
case .spring:
curve = 7
}
}
let listViewCurve: ListViewAnimationCurve
if curve == 7 {
listViewCurve = .Spring(duration: duration)
} else {
listViewCurve = .Default
}
let updateSizeAndInsets = ListViewUpdateSizeAndInsets(size: layout.size, insets: insets, duration: duration, curve: listViewCurve)
self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: nil, updateSizeAndInsets: updateSizeAndInsets, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in })
if !self.dequeuedInitialTransitionOnLayout {
self.dequeuedInitialTransitionOnLayout = true
self.dequeueTransition()
}
}
}

View File

@ -0,0 +1,128 @@
import Foundation
import Postbox
import TelegramCore
enum CallListNodeEntryId: Hashable {
case hole(MessageIndex)
case message(MessageIndex)
var hashValue: Int {
switch self {
case let .hole(index):
return index.hashValue
case let .message(index):
return index.hashValue
}
}
static func <(lhs: CallListNodeEntryId, rhs: CallListNodeEntryId) -> Bool {
return lhs.hashValue < rhs.hashValue
}
static func ==(lhs: CallListNodeEntryId, rhs: CallListNodeEntryId) -> Bool {
switch lhs {
case let .hole(index):
if case .hole(index) = rhs {
return true
} else {
return false
}
case let .message(index):
if case .message(index) = rhs {
return true
} else {
return false
}
}
}
}
private func areMessagesEqual(_ lhsMessage: Message, _ rhsMessage: Message) -> Bool {
if lhsMessage.stableVersion != rhsMessage.stableVersion {
return false
}
if lhsMessage.id != rhsMessage.id || lhsMessage.flags != rhsMessage.flags {
return false
}
return true
}
enum CallListNodeEntry: Comparable, Identifiable {
case messageEntry(topMessage: Message, messages: [Message], theme: PresentationTheme, strings: PresentationStrings, editing: Bool, hasActiveRevealControls: Bool)
case holeEntry(index: MessageIndex, theme: PresentationTheme)
var index: MessageIndex {
switch self {
case let .messageEntry(message, _, _, _, _, _):
return MessageIndex(message)
case let .holeEntry(index, _):
return index
}
}
var stableId: CallListNodeEntryId {
switch self {
case let .messageEntry(message, _, _, _, _, _):
return .message(MessageIndex(message))
case let .holeEntry(index, _):
return .hole(index)
}
}
static func <(lhs: CallListNodeEntry, rhs: CallListNodeEntry) -> Bool {
return lhs.index < rhs.index
}
static func ==(lhs: CallListNodeEntry, rhs: CallListNodeEntry) -> Bool {
switch lhs {
case let .messageEntry(lhsMessage, lhsMessages, lhsTheme, lhsStrings, lhsEditing, lhsHasRevealControls):
if case let .messageEntry(rhsMessage, rhsMessages, rhsTheme, rhsStrings, rhsEditing, rhsHasRevealControls) = rhs {
if lhsTheme !== rhsTheme {
return false
}
if lhsStrings !== rhsStrings {
return false
}
if lhsEditing != rhsEditing {
return false
}
if lhsHasRevealControls != rhsHasRevealControls {
return false
}
if !areMessagesEqual(lhsMessage, rhsMessage) {
return false
}
if lhsMessages.count != rhsMessages.count {
return false
}
for i in 0 ..< lhsMessages.count {
if !areMessagesEqual(lhsMessages[i], rhsMessages[i]) {
return false
}
}
return true
} else {
return false
}
case let .holeEntry(lhsIndex, lhsTheme):
if case let .holeEntry(rhsIndex, rhsTheme) = rhs, lhsIndex == rhsIndex, lhsTheme === rhsTheme {
return true
} else {
return false
}
}
}
}
func callListNodeEntriesForView(_ view: CallListView, state: CallListNodeState) -> [CallListNodeEntry] {
var result: [CallListNodeEntry] = []
for entry in view.entries {
switch entry {
case let .message(topMessage, messages):
result.append(.messageEntry(topMessage: topMessage, messages: messages, theme: state.theme, strings: state.strings, editing: state.editing, hasActiveRevealControls: state.messageIdWithRevealedOptions == topMessage.id))
case let .hole(index):
result.append(.holeEntry(index: index, theme: state.theme))
}
}
return result
}

View File

@ -0,0 +1,83 @@
import Foundation
import Postbox
import TelegramCore
import SwiftSignalKit
import Display
enum CallListNodeLocation: Equatable {
case initial(count: Int)
case changeType(index: MessageIndex)
case navigation(index: MessageIndex)
case scroll(index: MessageIndex, sourceIndex: MessageIndex, scrollPosition: ListViewScrollPosition, animated: Bool)
static func ==(lhs: CallListNodeLocation, rhs: CallListNodeLocation) -> Bool {
switch lhs {
case let .navigation(index):
switch rhs {
case .navigation(index):
return true
default:
return false
}
default:
return false
}
}
}
struct CallListNodeLocationAndType: Equatable {
let location: CallListNodeLocation
let type: CallListViewType
static func ==(lhs: CallListNodeLocationAndType, rhs: CallListNodeLocationAndType) -> Bool {
return lhs.location == rhs.location && lhs.type == rhs.type
}
}
struct CallListNodeViewUpdate {
let view: CallListView
let type: ViewUpdateType
let scrollPosition: CallListNodeViewScrollPosition?
}
func callListViewForLocationAndType(locationAndType: CallListNodeLocationAndType, account: Account) -> Signal<CallListNodeViewUpdate, NoError> {
switch locationAndType.location {
case let .initial(count):
return account.viewTracker.callListView(type: locationAndType.type, index: MessageIndex.absoluteUpperBound(), count: count) |> map { view -> CallListNodeViewUpdate in
return CallListNodeViewUpdate(view: view, type: .Generic, scrollPosition: nil)
}
case let .changeType(index):
return account.viewTracker.callListView(type: locationAndType.type, index: index, count: 120) |> map { view -> CallListNodeViewUpdate in
let genericType: ViewUpdateType
genericType = .Generic
return CallListNodeViewUpdate(view: view, type: genericType, scrollPosition: nil)
}
case let .navigation(index):
var first = true
return account.viewTracker.callListView(type: locationAndType.type, index: index, count: 120) |> map { view -> CallListNodeViewUpdate in
let genericType: ViewUpdateType
if first {
first = false
genericType = .UpdateVisible
} else {
genericType = .Generic
}
return CallListNodeViewUpdate(view: view, type: genericType, scrollPosition: nil)
}
case let .scroll(index, sourceIndex, scrollPosition, animated):
let directionHint: ListViewScrollToItemDirectionHint = sourceIndex > index ? .Down : .Up
let callScrollPosition: CallListNodeViewScrollPosition = .index(index: index, position: scrollPosition, directionHint: directionHint, animated: animated)
var first = true
return account.viewTracker.callListView(type: locationAndType.type, index: index, count: 120) |> map { view -> CallListNodeViewUpdate in
let genericType: ViewUpdateType
let scrollPosition: CallListNodeViewScrollPosition? = first ? callScrollPosition : nil
if first {
first = false
genericType = .UpdateVisible
} else {
genericType = .Generic
}
return CallListNodeViewUpdate(view: view, type: genericType, scrollPosition: scrollPosition)
}
}
}

View File

@ -0,0 +1,170 @@
import Foundation
import Postbox
import TelegramCore
import SwiftSignalKit
import Display
struct CallListNodeView {
let originalView: CallListView
let filteredEntries: [CallListNodeEntry]
}
enum CallListNodeViewTransitionReason {
case initial
case interactiveChanges
case holeChanges(filledHoleDirections: [MessageIndex: HoleFillDirection], removeHoleDirections: [MessageIndex: HoleFillDirection])
case reload
}
struct CallListNodeViewTransitionInsertEntry {
let index: Int
let previousIndex: Int?
let entry: CallListNodeEntry
let directionHint: ListViewItemOperationDirectionHint?
}
struct CallListNodeViewTransitionUpdateEntry {
let index: Int
let previousIndex: Int
let entry: CallListNodeEntry
let directionHint: ListViewItemOperationDirectionHint?
}
struct CallListNodeViewTransition {
let callListView: CallListNodeView
let deleteItems: [ListViewDeleteItem]
let insertEntries: [CallListNodeViewTransitionInsertEntry]
let updateEntries: [CallListNodeViewTransitionUpdateEntry]
let options: ListViewDeleteAndInsertOptions
let scrollToItem: ListViewScrollToItem?
let stationaryItemRange: (Int, Int)?
}
enum CallListNodeViewScrollPosition {
case index(index: MessageIndex, position: ListViewScrollPosition, directionHint: ListViewScrollToItemDirectionHint, animated: Bool)
}
func preparedCallListNodeViewTransition(from fromView: CallListNodeView?, to toView: CallListNodeView, reason: CallListNodeViewTransitionReason, account: Account, scrollPosition: CallListNodeViewScrollPosition?) -> Signal<CallListNodeViewTransition, NoError> {
return Signal { subscriber in
let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromView?.filteredEntries ?? [], rightList: toView.filteredEntries)
var adjustedDeleteIndices: [ListViewDeleteItem] = []
let previousCount: Int
if let fromView = fromView {
previousCount = fromView.filteredEntries.count
} else {
previousCount = 0;
}
for index in deleteIndices {
adjustedDeleteIndices.append(ListViewDeleteItem(index: previousCount - 1 - index, directionHint: nil))
}
var adjustedIndicesAndItems: [CallListNodeViewTransitionInsertEntry] = []
var adjustedUpdateItems: [CallListNodeViewTransitionUpdateEntry] = []
let updatedCount = toView.filteredEntries.count
var options: ListViewDeleteAndInsertOptions = []
var maxAnimatedInsertionIndex = -1
var stationaryItemRange: (Int, Int)?
var scrollToItem: ListViewScrollToItem?
switch reason {
case .initial:
let _ = options.insert(.LowLatency)
let _ = options.insert(.Synchronous)
case .interactiveChanges:
let _ = options.insert(.AnimateAlpha)
let _ = options.insert(.AnimateInsertion)
for (index, _, _) in indicesAndItems.sorted(by: { $0.0 > $1.0 }) {
let adjustedIndex = updatedCount - 1 - index
if adjustedIndex == maxAnimatedInsertionIndex + 1 {
maxAnimatedInsertionIndex += 1
}
}
case .reload:
break
case let .holeChanges(filledHoleDirections, removeHoleDirections):
if let (_, removeDirection) = removeHoleDirections.first {
switch removeDirection {
case .LowerToUpper:
var holeIndex: MessageIndex?
for (index, _) in filledHoleDirections {
if holeIndex == nil || index < holeIndex! {
holeIndex = index
}
}
if let holeIndex = holeIndex {
for i in 0 ..< toView.filteredEntries.count {
if toView.filteredEntries[i].index >= holeIndex {
let index = toView.filteredEntries.count - 1 - (i - 1)
stationaryItemRange = (index, Int.max)
break
}
}
}
case .UpperToLower:
break
case .AroundIndex:
break
}
}
}
for (index, entry, previousIndex) in indicesAndItems {
let adjustedIndex = updatedCount - 1 - index
let adjustedPrevousIndex: Int?
if let previousIndex = previousIndex {
adjustedPrevousIndex = previousCount - 1 - previousIndex
} else {
adjustedPrevousIndex = nil
}
var directionHint: ListViewItemOperationDirectionHint?
if maxAnimatedInsertionIndex >= 0 && adjustedIndex <= maxAnimatedInsertionIndex {
directionHint = .Down
}
adjustedIndicesAndItems.append(CallListNodeViewTransitionInsertEntry(index: adjustedIndex, previousIndex: adjustedPrevousIndex, entry: entry, directionHint: directionHint))
}
for (index, entry, previousIndex) in updateIndices {
let adjustedIndex = updatedCount - 1 - index
let adjustedPreviousIndex = previousCount - 1 - previousIndex
let directionHint: ListViewItemOperationDirectionHint? = nil
adjustedUpdateItems.append(CallListNodeViewTransitionUpdateEntry(index: adjustedIndex, previousIndex: adjustedPreviousIndex, entry: entry, directionHint: directionHint))
}
if let scrollPosition = scrollPosition {
switch scrollPosition {
case let .index(scrollIndex, position, directionHint, animated):
var index = toView.filteredEntries.count - 1
for entry in toView.filteredEntries {
if entry.index >= scrollIndex {
scrollToItem = ListViewScrollToItem(index: index, position: position, animated: animated, curve: .Default, directionHint: directionHint)
break
}
index -= 1
}
if scrollToItem == nil {
var index = 0
for entry in toView.filteredEntries.reversed() {
if entry.index < scrollIndex {
scrollToItem = ListViewScrollToItem(index: index, position: position, animated: animated, curve: .Default, directionHint: directionHint)
break
}
index += 1
}
}
}
}
subscriber.putNext(CallListNodeViewTransition(callListView: toView, deleteItems: adjustedDeleteIndices, insertEntries: adjustedIndicesAndItems, updateEntries: adjustedUpdateItems, options: options, scrollToItem: scrollToItem, stationaryItemRange: stationaryItemRange))
subscriber.putCompletion()
return EmptyDisposable
}
}

View File

@ -38,8 +38,8 @@ private enum ChangePhoneNumberCodeTag: ItemListItemTag {
}
private enum ChangePhoneNumberCodeEntry: ItemListNodeEntry {
case codeEntry(String)
case codeInfo(String)
case codeEntry(PresentationTheme, String)
case codeInfo(PresentationTheme, String)
var section: ItemListSectionId {
return ChangePhoneNumberCodeSection.code.rawValue
@ -56,14 +56,14 @@ private enum ChangePhoneNumberCodeEntry: ItemListNodeEntry {
static func ==(lhs: ChangePhoneNumberCodeEntry, rhs: ChangePhoneNumberCodeEntry) -> Bool {
switch lhs {
case let .codeEntry(text):
if case .codeEntry(text) = rhs {
case let .codeEntry(lhsTheme, lhsText):
if case let .codeEntry(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
case let .codeInfo(text):
if case .codeInfo(text) = rhs {
case let .codeInfo(lhsTheme, lhsText):
if case let .codeInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
@ -77,14 +77,14 @@ private enum ChangePhoneNumberCodeEntry: ItemListNodeEntry {
func item(_ arguments: ChangePhoneNumberCodeControllerArguments) -> ListViewItem {
switch self {
case let .codeEntry(text):
return ItemListSingleLineInputItem(title: NSAttributedString(string: "Code", textColor: .black), text: text, placeholder: "", type: .number, spacing: 10.0, tag: ChangePhoneNumberCodeTag.input, sectionId: self.section, textUpdated: { updatedText in
case let .codeEntry(theme, text):
return ItemListSingleLineInputItem(theme: theme, title: NSAttributedString(string: "Code", textColor: .black), text: text, placeholder: "", type: .number, spacing: 10.0, tag: ChangePhoneNumberCodeTag.input, sectionId: self.section, textUpdated: { updatedText in
arguments.updateEntryText(updatedText)
}, action: {
arguments.next()
})
case let .codeInfo(text):
return ItemListTextItem(text: .plain(text), sectionId: self.section)
case let .codeInfo(theme, text):
return ItemListTextItem(theme: theme, text: .plain(text), sectionId: self.section)
}
}
}
@ -126,15 +126,15 @@ private struct ChangePhoneNumberCodeControllerState: Equatable {
}
}
private func changePhoneNumberCodeControllerEntries(state: ChangePhoneNumberCodeControllerState, codeData: ChangeAccountPhoneNumberData, timeout: Int32?) -> [ChangePhoneNumberCodeEntry] {
private func changePhoneNumberCodeControllerEntries(presentationData: PresentationData, state: ChangePhoneNumberCodeControllerState, codeData: ChangeAccountPhoneNumberData, timeout: Int32?) -> [ChangePhoneNumberCodeEntry] {
var entries: [ChangePhoneNumberCodeEntry] = []
entries.append(.codeEntry(state.codeText))
entries.append(.codeEntry(presentationData.theme, state.codeText))
var text = authorizationCurrentOptionText(codeData.type).string
if let nextType = codeData.nextType {
text += "\n\n" + authorizationNextOptionText(nextType, timeout: timeout).string
}
entries.append(.codeInfo(text))
entries.append(.codeInfo(presentationData.theme, text))
return entries
}
@ -260,8 +260,9 @@ func changePhoneNumberCodeController(account: Account, phoneNumber: String, code
checkCode()
})
let signal = combineLatest(statePromise.get() |> deliverOnMainQueue, currentDataPromise.get() |> deliverOnMainQueue, timeout.get() |> deliverOnMainQueue)
|> map { state, data, timeout -> (ItemListControllerState, (ItemListNodeState<ChangePhoneNumberCodeEntry>, ChangePhoneNumberCodeEntry.ItemGenerationArguments)) in
let signal = combineLatest((account.applicationContext as! TelegramApplicationContext).presentationData, statePromise.get() |> deliverOnMainQueue, currentDataPromise.get() |> deliverOnMainQueue, timeout.get() |> deliverOnMainQueue)
|> deliverOnMainQueue
|> map { presentationData, state, data, timeout -> (ItemListControllerState, (ItemListNodeState<ChangePhoneNumberCodeEntry>, ChangePhoneNumberCodeEntry.ItemGenerationArguments)) in
var rightNavigationButton: ItemListNavigationButton?
if state.checking {
rightNavigationButton = ItemListNavigationButton(title: "", style: .activity, enabled: true, action: {})
@ -270,21 +271,20 @@ func changePhoneNumberCodeController(account: Account, phoneNumber: String, code
if state.codeText.isEmpty {
nextEnabled = false
}
rightNavigationButton = ItemListNavigationButton(title: "Next", style: .bold, enabled: nextEnabled, action: {
rightNavigationButton = ItemListNavigationButton(title: presentationData.strings.Common_Next, style: .bold, enabled: nextEnabled, action: {
checkCode()
})
}
let controllerState = ItemListControllerState(title: .text(formatPhoneNumber(phoneNumber)), leftNavigationButton: nil, rightNavigationButton: rightNavigationButton, animateChanges: false)
let listState = ItemListNodeState(entries: changePhoneNumberCodeControllerEntries(state: state, codeData: data, timeout: timeout), style: .blocks, focusItemTag: ChangePhoneNumberCodeTag.input, emptyStateItem: nil, animateChanges: false)
let controllerState = ItemListControllerState(theme: presentationData.theme, title: .text(formatPhoneNumber(phoneNumber)), leftNavigationButton: nil, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: false)
let listState = ItemListNodeState(entries: changePhoneNumberCodeControllerEntries(presentationData: presentationData, state: state, codeData: data, timeout: timeout), style: .blocks, focusItemTag: ChangePhoneNumberCodeTag.input, emptyStateItem: nil, animateChanges: false)
return (controllerState, (listState, arguments))
} |> afterDisposed {
actionsDisposable.dispose()
}
let controller = ItemListController(signal)
controller.navigationItem.backBarButtonItem = UIBarButtonItem(title: "Back", style: .plain, target: nil, action: nil)
let controller = ItemListController(account: account, state: signal)
presentControllerImpl = { [weak controller] c, p in
if let controller = controller {

View File

@ -29,14 +29,20 @@ final class ChangePhoneNumberController: ViewController {
private let hapticFeedback = HapticFeedback()
private var presentationData: PresentationData
init(account: Account) {
self.account = account
super.init(navigationBar: NavigationBar())
self.presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 }
self.title = "Change Number"
self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: "Next", style: .done, target: self, action: #selector(self.nextPressed))
self.navigationItem.backBarButtonItem = UIBarButtonItem(title: "Back", style: .plain, target: nil, action: nil)
super.init(navigationBarTheme: NavigationBarTheme(rootControllerTheme: self.presentationData.theme))
self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBar.style.style
self.title = self.presentationData.strings.ChangePhoneNumberNumber_Title
self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Next, style: .done, target: self, action: #selector(self.nextPressed))
self.navigationItem.backBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Back, style: .plain, target: nil, action: nil)
}
required init(coder aDecoder: NSCoder) {
@ -57,7 +63,7 @@ final class ChangePhoneNumberController: ViewController {
}
override public func loadDisplayNode() {
self.displayNode = ChangePhoneNumberControllerNode()
self.displayNode = ChangePhoneNumberControllerNode(presentationData: self.presentationData)
self.displayNodeDidLoad()
self.controllerNode.selectCountryCode = { [weak self] in
if let strongSelf = self {

View File

@ -17,7 +17,7 @@ private let countryButtonBackground = generateImage(CGSize(width: 45.0, height:
context.closePath()
context.fillPath()
context.setStrokeColor(UIColor(0xbcbbc1).cgColor)
context.setStrokeColor(UIColor(rgb: 0xbcbbc1).cgColor)
context.setLineWidth(lineWidth)
context.move(to: CGPoint(x: size.width, y: size.height - arrowSize - lineWidth / 2.0))
@ -35,7 +35,7 @@ private let countryButtonBackground = generateImage(CGSize(width: 45.0, height:
private let countryButtonHighlightedBackground = generateImage(CGSize(width: 45.0, height: 44.0 + 6.0), rotatedContext: { size, context in
let arrowSize: CGFloat = 6.0
context.clear(CGRect(origin: CGPoint(), size: size))
context.setFillColor(UIColor(0xbcbbc1).cgColor)
context.setFillColor(UIColor(rgb: 0xbcbbc1).cgColor)
context.fill(CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: size.height - arrowSize)))
context.move(to: CGPoint(x: size.width, y: size.height - arrowSize))
context.addLine(to: CGPoint(x: size.width - 1.0, y: size.height - arrowSize))
@ -50,7 +50,7 @@ private let phoneInputBackground = generateImage(CGSize(width: 60.0, height: 44.
context.clear(CGRect(origin: CGPoint(), size: size))
context.setFillColor(UIColor.white.cgColor)
context.fill(CGRect(origin: CGPoint(), size: size))
context.setStrokeColor(UIColor(0xbcbbc1).cgColor)
context.setStrokeColor(UIColor(rgb: 0xbcbbc1).cgColor)
context.setLineWidth(lineWidth)
context.move(to: CGPoint(x: 0.0, y: size.height - lineWidth / 2.0))
context.addLine(to: CGPoint(x: size.width, y: size.height - lineWidth / 2.0))
@ -89,16 +89,20 @@ final class ChangePhoneNumberControllerNode: ASDisplayNode {
}
}
override init() {
var presentationData: PresentationData
init(presentationData: PresentationData) {
self.presentationData = presentationData
self.titleNode = ASTextNode()
self.titleNode.isLayerBacked = true
self.titleNode.displaysAsynchronously = false
self.titleNode.attributedText = NSAttributedString(string: "NEW NUMBER", font: Font.regular(14.0), textColor: UIColor(0x6d6d72))
self.titleNode.attributedText = NSAttributedString(string: self.presentationData.strings.ChangePhoneNumberNumber_NewNumber, font: Font.regular(14.0), textColor: self.presentationData.theme.list.sectionHeaderTextColor)
self.noticeNode = ASTextNode()
self.noticeNode.isLayerBacked = true
self.noticeNode.displaysAsynchronously = false
self.noticeNode.attributedText = NSAttributedString(string: "We will send an SMS with a confirmation code to your new number.", font: Font.regular(14.0), textColor: UIColor(0x6d6d72))
self.noticeNode.attributedText = NSAttributedString(string: self.presentationData.strings.ChangePhoneNumberNumber_Help, font: Font.regular(14.0), textColor: self.presentationData.theme.list.freeTextColor)
self.countryButton = ASButtonNode()
self.countryButton.setBackgroundImage(countryButtonBackground, for: [])
@ -116,7 +120,7 @@ final class ChangePhoneNumberControllerNode: ASDisplayNode {
return UITracingLayerView()
}, didLoad: nil)
self.backgroundColor = UIColor(0xefefef)
self.backgroundColor = self.presentationData.theme.list.blocksBackgroundColor
self.addSubnode(self.titleNode)
self.addSubnode(self.noticeNode)
@ -127,7 +131,7 @@ final class ChangePhoneNumberControllerNode: ASDisplayNode {
self.countryButton.contentEdgeInsets = UIEdgeInsets(top: 0.0, left: 15.0, bottom: 4.0, right: 0.0)
self.countryButton.contentHorizontalAlignment = .left
self.phoneInputNode.numberField.textField.attributedPlaceholder = NSAttributedString(string: "Your phone number", font: Font.regular(17.0), textColor: UIColor(0xbcbcc3))
self.phoneInputNode.numberField.textField.attributedPlaceholder = NSAttributedString(string: self.presentationData.strings.Login_PhonePlaceholder, font: Font.regular(17.0), textColor: self.presentationData.theme.list.itemPlaceholderTextColor)
self.countryButton.addTarget(self, action: #selector(self.countryPressed), forControlEvents: .touchUpInside)
@ -136,7 +140,7 @@ final class ChangePhoneNumberControllerNode: ASDisplayNode {
if let code = Int(code), let countryName = countryCodeToName[code] {
strongSelf.countryButton.setTitle(countryName, with: Font.regular(17.0), with: .black, for: [])
} else {
strongSelf.countryButton.setTitle("Select Country", with: Font.regular(17.0), with: .black, for: [])
strongSelf.countryButton.setTitle(strongSelf.presentationData.strings.Login_CountryCode, with: Font.regular(17.0), with: .black, for: [])
}
}
}

View File

@ -4,6 +4,8 @@ import AsyncDisplayKit
import TelegramCore
private final class ChangePhoneNumberIntroControllerNode: ASDisplayNode {
var presentationData: PresentationData
let iconNode: ASImageNode
let labelNode: ASTextNode
let buttonNode: HighlightableButtonNode
@ -11,7 +13,9 @@ private final class ChangePhoneNumberIntroControllerNode: ASDisplayNode {
var dismiss: (() -> Void)?
var action: (() -> Void)?
override init() {
init(presentationData: PresentationData) {
self.presentationData = presentationData
self.iconNode = ASImageNode()
self.labelNode = ASTextNode()
self.buttonNode = HighlightableButtonNode()
@ -20,16 +24,17 @@ private final class ChangePhoneNumberIntroControllerNode: ASDisplayNode {
return UITracingLayerView()
}, didLoad: nil)
self.iconNode.image = UIImage(bundleImageName: "Settings/ChangePhoneIntroIcon")?.precomposed()
self.labelNode.attributedText = NSAttributedString(string: "You can change your Telegram number here. Your account and all your cloud data — messages, media, contacts, etc. will be moved to the new number.\n\nImportant: all your Telegram contacts will get your new number added to their address book, provided they had your old number and you haven't blocked them in Telegram.", font: Font.regular(14.0), textColor: UIColor(0x6d6d72), paragraphAlignment: .center)
self.buttonNode.setTitle("Change Number", with: Font.regular(19.0), with: UIColor(0x007ee5), for: .normal)
self.iconNode.image = generateTintedImage(image: UIImage(bundleImageName: "Settings/ChangePhoneIntroIcon"), color: presentationData.theme.list.freeTextColor)
let textColor = self.presentationData.theme.list.freeTextColor
self.labelNode.attributedText = parseMarkdownIntoAttributedString(self.presentationData.strings.PhoneNumberHelp_Help, attributes: MarkdownAttributes(body: MarkdownAttributeSet(font: Font.regular(14.0), textColor: textColor), bold: MarkdownAttributeSet(font: Font.semibold(14.0), textColor: textColor), link: MarkdownAttributeSet(font: Font.regular(14.0), textColor: textColor), linkAttribute: { _ in return nil }), textAlignment: .center)
self.buttonNode.setTitle(self.presentationData.strings.PhoneNumberHelp_ChangeNumber, with: Font.regular(19.0), with: self.presentationData.theme.list.itemAccentColor, for: .normal)
self.buttonNode.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: .touchUpInside)
self.addSubnode(self.iconNode)
self.addSubnode(self.labelNode)
self.addSubnode(self.buttonNode)
self.backgroundColor = UIColor(0xefeff4)
self.backgroundColor = self.presentationData.theme.list.blocksBackgroundColor
}
func animateIn() {
@ -72,13 +77,19 @@ final class ChangePhoneNumberIntroController: ViewController {
private let account: Account
private var didPlayPresentationAnimation = false
private var presentationData: PresentationData
init(account: Account, phoneNumber: String) {
self.account = account
super.init(navigationBar: NavigationBar())
self.presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 }
super.init(navigationBarTheme: NavigationBarTheme(rootControllerTheme: self.presentationData.theme))
self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBar.style.style
self.title = phoneNumber
self.navigationItem.backBarButtonItem = UIBarButtonItem(title: "Back", style: .plain, target: nil, action: nil)
self.navigationItem.backBarButtonItem = UIBarButtonItem(title: presentationData.strings.Common_Back, style: .plain, target: nil, action: nil)
//self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: "Cancel", style: .plain, target: self, action: #selector(self.cancelPressed))
}
@ -87,7 +98,7 @@ final class ChangePhoneNumberIntroController: ViewController {
}
override func loadDisplayNode() {
self.displayNode = ChangePhoneNumberIntroControllerNode()
self.displayNode = ChangePhoneNumberIntroControllerNode(presentationData: self.presentationData)
(self.displayNode as! ChangePhoneNumberIntroControllerNode).dismiss = { [weak self] in
self?.presentingViewController?.dismiss(animated: false, completion: nil)
}
@ -117,7 +128,7 @@ final class ChangePhoneNumberIntroController: ViewController {
}
func proceed() {
self.present(standardTextAlertController(title: nil, text: "All your Telegram contacts will get your new number added to their address book, provided they had your old number and you haven't blocked them in Telegram.", actions: [TextAlertAction(type: .defaultAction, title: "Cancel", action: {}), TextAlertAction(type: .genericAction, title: "OK", action: { [weak self] in
self.present(standardTextAlertController(title: nil, text: self.presentationData.strings.PhoneNumberHelp_Alert, actions: [TextAlertAction(type: .defaultAction, title: self.presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .genericAction, title: self.presentationData.strings.Common_OK, action: { [weak self] in
if let strongSelf = self {
(strongSelf.navigationController as? NavigationController)?.replaceTopController(ChangePhoneNumberController(account: strongSelf.account), animated: true)
}

View File

@ -0,0 +1,478 @@
import Foundation
import Display
import SwiftSignalKit
import Postbox
import TelegramCore
private final class ChannelAdminControllerArguments {
let account: Account
let toggleRight: (TelegramChannelAdminRightsFlags, TelegramChannelAdminRightsFlags) -> Void
let dismissAdmin: () -> Void
init(account: Account, toggleRight: @escaping (TelegramChannelAdminRightsFlags, TelegramChannelAdminRightsFlags) -> Void, dismissAdmin: @escaping () -> Void) {
self.account = account
self.toggleRight = toggleRight
self.dismissAdmin = dismissAdmin
}
}
private enum ChannelAdminSection: Int32 {
case info
case rights
case dismiss
}
private enum ChannelAdminEntryStableId: Hashable {
case info
case right(TelegramChannelAdminRightsFlags)
case dismiss
var hashValue: Int {
switch self {
case .info:
return 0
case .dismiss:
return 1
case let .right(flags):
return flags.rawValue.hashValue
}
}
static func ==(lhs: ChannelAdminEntryStableId, rhs: ChannelAdminEntryStableId) -> Bool {
switch lhs {
case .info:
if case .info = rhs {
return true
} else {
return false
}
case let right(flags):
if case .right(flags) = rhs {
return true
} else {
return false
}
case .dismiss:
if case .dismiss = rhs {
return true
} else {
return false
}
}
}
}
private enum ChannelAdminEntry: ItemListNodeEntry {
case info(PresentationTheme, PresentationStrings, Peer, TelegramUserPresence?)
case rightItem(PresentationTheme, Int, String, TelegramChannelAdminRightsFlags, TelegramChannelAdminRightsFlags, Bool, Bool)
case dismiss(PresentationTheme, String)
var section: ItemListSectionId {
switch self {
case .info:
return ChannelAdminSection.info.rawValue
case .rightItem:
return ChannelAdminSection.rights.rawValue
case .dismiss:
return ChannelAdminSection.dismiss.rawValue
}
}
var stableId: ChannelAdminEntryStableId {
switch self {
case .info:
return .info
case let .rightItem(_, _, _, right, _, _, _):
return .right(right)
case .dismiss:
return .dismiss
}
}
static func ==(lhs: ChannelAdminEntry, rhs: ChannelAdminEntry) -> Bool {
switch lhs {
case let .info(lhsTheme, lhsStrings, lhsPeer, lhsPresence):
if case let .info(rhsTheme, rhsStrings, rhsPeer, rhsPresence) = rhs {
if lhsTheme !== rhsTheme {
return false
}
if lhsStrings !== rhsStrings {
return false
}
if !arePeersEqual(lhsPeer, rhsPeer) {
return false
}
if lhsPresence != rhsPresence {
return false
}
return true
} else {
return false
}
case let .rightItem(lhsTheme, lhsIndex, lhsText, lhsRight, lhsFlags, lhsValue, lhsEnabled):
if case let .rightItem(rhsTheme, rhsIndex, rhsText, rhsRight, rhsFlags, rhsValue, rhsEnabled) = rhs {
if lhsTheme !== rhsTheme {
return false
}
if lhsIndex != rhsIndex {
return false
}
if lhsText != rhsText {
return false
}
if lhsRight != rhsRight {
return false
}
if lhsFlags != rhsFlags {
return false
}
if lhsValue != rhsValue {
return false
}
if lhsEnabled != rhsEnabled {
return false
}
return true
} else {
return false
}
case let .dismiss(lhsTheme, lhsText):
if case let .dismiss(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
}
}
static func <(lhs: ChannelAdminEntry, rhs: ChannelAdminEntry) -> Bool {
switch lhs {
case .info:
switch rhs {
case .info:
return false
default:
return true
}
case let .rightItem(_, lhsIndex, _, _, _, _, _):
switch rhs {
case .info:
return false
case let .rightItem(_, rhsIndex, _, _, _, _, _):
return lhsIndex < rhsIndex
default:
return true
}
case .dismiss:
return false
}
}
func item(_ arguments: ChannelAdminControllerArguments) -> ListViewItem {
switch self {
case let .info(theme, strings, peer, presence):
return ItemListAvatarAndNameInfoItem(account: arguments.account, theme: theme, strings: strings, peer: peer, presence: presence, cachedData: nil, state: ItemListAvatarAndNameInfoItemState(), sectionId: self.section, style: .blocks(withTopInset: true), editingNameUpdated: { _ in
}, avatarTapped: {
})
case let .rightItem(theme, _, text, right, flags, value, enabled):
return ItemListSwitchItem(theme: theme, title: text, value: value, enabled: enabled, sectionId: self.section, style: .blocks, updated: { _ in
arguments.toggleRight(right, flags)
})
case let .dismiss(theme, text):
return ItemListActionItem(theme: theme, title: text, kind: .destructive, alignment: .natural, sectionId: self.section, style: .blocks, action: {
arguments.dismissAdmin()
}, tag: nil)
}
}
}
private struct ChannelAdminControllerState: Equatable {
let updatedFlags: TelegramChannelAdminRightsFlags?
let updating: Bool
init(updatedFlags: TelegramChannelAdminRightsFlags? = nil, updating: Bool = false) {
self.updatedFlags = updatedFlags
self.updating = updating
}
static func ==(lhs: ChannelAdminControllerState, rhs: ChannelAdminControllerState) -> Bool {
if lhs.updatedFlags != rhs.updatedFlags {
return false
}
if lhs.updating != rhs.updating {
return false
}
return true
}
func withUpdatedUpdatedFlags(_ updatedFlags: TelegramChannelAdminRightsFlags?) -> ChannelAdminControllerState {
return ChannelAdminControllerState(updatedFlags: updatedFlags, updating: self.updating)
}
func withUpdatedUpdating(_ updating: Bool) -> ChannelAdminControllerState {
return ChannelAdminControllerState(updatedFlags: self.updatedFlags, updating: updating)
}
}
private func stringForRight(strings: PresentationStrings, right: TelegramChannelAdminRightsFlags, isGroup: Bool) -> String {
if right.contains(.canChangeInfo) {
return isGroup ? strings.Group_EditAdmin_PermissionChangeInfo : strings.Channel_EditAdmin_PermissionChangeInfo
} else if right.contains(.canPostMessages) {
return strings.Channel_EditAdmin_PermissionPostMessages
} else if right.contains(.canEditMessages) {
return strings.Channel_EditAdmin_PermissionEditMessages
} else if right.contains(.canDeleteMessages) {
return strings.Channel_EditAdmin_PermissionDeleteMessages
} else if right.contains(.canBanUsers) {
return strings.Channel_EditAdmin_PermissionBanUsers
} else if right.contains(.canInviteUsers) {
return strings.Channel_EditAdmin_PermissionInviteUsers
} else if right.contains(.canChangeInviteLink) {
return strings.Channel_EditAdmin_PermissionChangeInviteLink
} else if right.contains(.canPinMessages) {
return strings.Channel_EditAdmin_PermissionPinMessages
} else if right.contains(.canAddAdmins) {
return strings.Channel_EditAdmin_PermissionAddAdmins
} else {
return ""
}
}
private func rightDependencies(_ right: TelegramChannelAdminRightsFlags) -> [TelegramChannelAdminRightsFlags] {
if right.contains(.canChangeInfo) {
return []
} else if right.contains(.canPostMessages) {
return []
} else if right.contains(.canEditMessages) {
return []
} else if right.contains(.canDeleteMessages) {
return []
} else if right.contains(.canBanUsers) {
return []
} else if right.contains(.canInviteUsers) {
return []
} else if right.contains(.canChangeInviteLink) {
return [.canInviteUsers]
} else if right.contains(.canPinMessages) {
return []
} else if right.contains(.canAddAdmins) {
return []
} else {
return []
}
}
private func canEditAdminRights(accountPeerId: PeerId, channelView: PeerView, initialParticipant: ChannelParticipant?) -> Bool {
if let channel = channelView.peers[channelView.peerId] as? TelegramChannel {
if channel.flags.contains(.isCreator) {
return true
} else if let initialParticipant = initialParticipant {
switch initialParticipant {
case .creator:
return false
case let .member(_, _, adminInfo, _):
if let adminInfo = adminInfo {
return adminInfo.canBeEditedByAccountPeer || adminInfo.promotedBy == accountPeerId
} else {
return false
}
}
} else {
return channel.hasAdminRights(.canAddAdmins)
}
} else {
return false
}
}
private func channelAdminControllerEntries(presentationData: PresentationData, state: ChannelAdminControllerState, accountPeerId: PeerId, channelView: PeerView, adminView: PeerView, initialParticipant: ChannelParticipant?) -> [ChannelAdminEntry] {
var entries: [ChannelAdminEntry] = []
if let channel = channelView.peers[channelView.peerId] as? TelegramChannel, let admin = adminView.peers[adminView.peerId] {
entries.append(.info(presentationData.theme, presentationData.strings, admin, adminView.peerPresences[admin.id] as? TelegramUserPresence))
let isGroup: Bool
let maskRightsFlags: TelegramChannelAdminRightsFlags
let rightsOrder: [TelegramChannelAdminRightsFlags]
switch channel.info {
case .broadcast:
isGroup = false
maskRightsFlags = .broadcastSpecific
rightsOrder = [
.canChangeInfo,
.canPostMessages,
.canEditMessages,
.canDeleteMessages,
.canAddAdmins
]
case .group:
isGroup = true
maskRightsFlags = .groupSpecific
rightsOrder = [
.canChangeInfo,
.canDeleteMessages,
.canBanUsers,
.canInviteUsers,
.canChangeInviteLink,
.canPinMessages,
.canAddAdmins
]
}
if canEditAdminRights(accountPeerId: accountPeerId, channelView: channelView, initialParticipant: initialParticipant) {
let accountUserRightsFlags: TelegramChannelAdminRightsFlags
if channel.flags.contains(.isCreator) {
accountUserRightsFlags = maskRightsFlags
} else if let adminRights = channel.adminRights {
accountUserRightsFlags = maskRightsFlags.intersection(adminRights.flags)
} else {
accountUserRightsFlags = []
}
let currentRightsFlags: TelegramChannelAdminRightsFlags
if let updatedFlags = state.updatedFlags {
currentRightsFlags = updatedFlags
} else if let initialParticipant = initialParticipant, case let .member(_, _, maybeAdminRights, _) = initialParticipant, let adminRights = maybeAdminRights {
currentRightsFlags = adminRights.rights.flags
} else {
currentRightsFlags = accountUserRightsFlags.subtracting(.canAddAdmins)
}
var index = 0
for right in rightsOrder {
if accountUserRightsFlags.contains(right) {
entries.append(.rightItem(presentationData.theme, index, stringForRight(strings: presentationData.strings, right: right, isGroup: isGroup), right, currentRightsFlags, currentRightsFlags.contains(right), !state.updating))
index += 1
}
}
if let initialParticipant = initialParticipant {
var canDismiss = false
if channel.flags.contains(.isCreator) {
canDismiss = true
} else {
switch initialParticipant {
case .creator:
break
case let .member(_, _, adminInfo, _):
if let adminInfo = adminInfo {
if adminInfo.promotedBy == accountPeerId || adminInfo.canBeEditedByAccountPeer {
canDismiss = true
}
}
}
}
if canDismiss {
entries.append(.dismiss(presentationData.theme, presentationData.strings.Channel_Moderator_AccessLevelRevoke))
}
}
} else if let initialParticipant = initialParticipant, case let .member(_, _, maybeAdminInfo, _) = initialParticipant, let adminInfo = maybeAdminInfo {
var index = 0
for right in rightsOrder {
entries.append(.rightItem(presentationData.theme, index, stringForRight(strings: presentationData.strings, right: right, isGroup: isGroup), right, adminInfo.rights.flags, adminInfo.rights.flags.contains(right), false))
index += 1
}
}
}
return entries
}
public func channelAdminController(account: Account, peerId: PeerId, adminId: PeerId, initialParticipant: ChannelParticipant?, updated: @escaping (TelegramChannelAdminRights) -> Void) -> ViewController {
let statePromise = ValuePromise(ChannelAdminControllerState(), ignoreRepeated: true)
let stateValue = Atomic(value: ChannelAdminControllerState())
let updateState: ((ChannelAdminControllerState) -> ChannelAdminControllerState) -> Void = { f in
statePromise.set(stateValue.modify { f($0) })
}
let actionsDisposable = DisposableSet()
let updateRightsDisposable = MetaDisposable()
actionsDisposable.add(updateRightsDisposable)
var dismissImpl: (() -> Void)?
let arguments = ChannelAdminControllerArguments(account: account, toggleRight: { right, flags in
updateState { current in
var updated = flags
if flags.contains(right) {
updated.remove(right)
} else {
updated.insert(right)
}
return current.withUpdatedUpdatedFlags(updated)
}
}, dismissAdmin: {
updateState { current in
return current.withUpdatedUpdating(true)
}
updateRightsDisposable.set((updatePeerAdminRights(account: account, peerId: peerId, adminId: adminId, rights: TelegramChannelAdminRights(flags: [])) |> deliverOnMainQueue).start(error: { _ in
}, completed: {
updated(TelegramChannelAdminRights(flags: []))
dismissImpl?()
}))
})
let combinedView = account.postbox.combinedView(keys: [.peer(peerId: peerId), .peer(peerId: adminId)])
let signal = combineLatest((account.applicationContext as! TelegramApplicationContext).presentationData, statePromise.get(), combinedView)
|> deliverOnMainQueue
|> map { presentationData, state, combinedView -> (ItemListControllerState, (ItemListNodeState<ChannelAdminEntry>, ChannelAdminEntry.ItemGenerationArguments)) in
let channelView = combinedView.views[.peer(peerId: peerId)] as! PeerView
let adminView = combinedView.views[.peer(peerId: adminId)] as! PeerView
let canEdit = canEditAdminRights(accountPeerId: account.peerId, channelView: channelView, initialParticipant: initialParticipant)
let leftNavigationButton: ItemListNavigationButton
if canEdit {
leftNavigationButton = ItemListNavigationButton(title: presentationData.strings.Common_Cancel, style: .regular, enabled: true, action: {
dismissImpl?()
})
} else {
leftNavigationButton = ItemListNavigationButton(title: presentationData.strings.Common_Done, style: .bold, enabled: true, action: {
dismissImpl?()
})
}
var rightNavigationButton: ItemListNavigationButton?
if state.updating {
rightNavigationButton = ItemListNavigationButton(title: "", style: .activity, enabled: true, action: {})
} else if canEdit {
rightNavigationButton = ItemListNavigationButton(title: presentationData.strings.Common_Done, style: .bold, enabled: true, action: {
var updateFlags: TelegramChannelAdminRightsFlags?
updateState { current in
updateFlags = current.updatedFlags
if let _ = updateFlags {
return current.withUpdatedUpdating(true)
} else {
return current
}
}
if let updateFlags = updateFlags {
updateRightsDisposable.set((updatePeerAdminRights(account: account, peerId: peerId, adminId: adminId, rights: TelegramChannelAdminRights(flags: updateFlags)) |> deliverOnMainQueue).start(error: { _ in
}, completed: {
updated(TelegramChannelAdminRights(flags: updateFlags))
dismissImpl?()
}))
}
})
}
let controllerState = ItemListControllerState(theme: presentationData.theme, title: .text(presentationData.strings.Channel_Management_LabelEditor), leftNavigationButton: leftNavigationButton, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: false)
let listState = ItemListNodeState(entries: channelAdminControllerEntries(presentationData: presentationData, state: state, accountPeerId: account.peerId, channelView: channelView, adminView: adminView, initialParticipant: initialParticipant), style: .blocks, emptyStateItem: nil, animateChanges: true)
return (controllerState, (listState, arguments))
} |> afterDisposed {
actionsDisposable.dispose()
}
let controller = ItemListController(account: account, state: signal)
dismissImpl = { [weak controller] in
controller?.dismiss()
}
return controller
}

Some files were not shown because too many files have changed in this diff Show More