Invite Links Improvements

This commit is contained in:
Ilya Laktyushin 2021-01-18 11:18:12 +03:00
parent d5a25602ce
commit ebe05a1e8a
19 changed files with 4067 additions and 3520 deletions

View File

@ -5842,6 +5842,7 @@ Sorry for the inconvenience.";
"InviteLink.Expired" = "expired"; "InviteLink.Expired" = "expired";
"InviteLink.UsageLimitReached" = "limit reached"; "InviteLink.UsageLimitReached" = "limit reached";
"InviteLink.Revoked" = "revoked"; "InviteLink.Revoked" = "revoked";
"InviteLink.TapToCopy" = "tap to copy";
"InviteLink.AdditionalLinks" = "Additional Links"; "InviteLink.AdditionalLinks" = "Additional Links";
"InviteLink.Create" = "Create a New Link"; "InviteLink.Create" = "Create a New Link";
@ -5882,9 +5883,11 @@ Sorry for the inconvenience.";
"InviteLink.InviteLink" = "Invite Link"; "InviteLink.InviteLink" = "Invite Link";
"InviteLink.CreatedBy" = "Link Created By"; "InviteLink.CreatedBy" = "Link Created By";
"InviteLink.DeleteAllRevokedLinksAlert.Text" = "This will delete all revoked links"; "InviteLink.DeleteAllRevokedLinksAlert.Text" = "This will delete all revoked links.";
"InviteLink.DeleteAllRevokedLinksAlert.Action" = "Delete"; "InviteLink.DeleteAllRevokedLinksAlert.Action" = "Delete";
"InviteLink.ExpiresIn" = "expires in %@";
"Conversation.ChecksTooltip.Delivered" = "Delivered"; "Conversation.ChecksTooltip.Delivered" = "Delivered";
"Conversation.ChecksTooltip.Read" = "Read"; "Conversation.ChecksTooltip.Read" = "Read";

View File

@ -45,25 +45,39 @@ private struct InviteLinkInviteTransaction {
} }
private enum InviteLinkInviteEntryId: Hashable { private enum InviteLinkInviteEntryId: Hashable {
case header
case mainLink case mainLink
case links(Int32) case links(Int32)
case manage
} }
private enum InviteLinkInviteEntry: Comparable, Identifiable { private enum InviteLinkInviteEntry: Comparable, Identifiable {
case header(PresentationTheme, String, String)
case mainLink(PresentationTheme, ExportedInvitation) case mainLink(PresentationTheme, ExportedInvitation)
case links(Int32, PresentationTheme, [ExportedInvitation]) case links(Int32, PresentationTheme, [ExportedInvitation])
case manage(PresentationTheme, String)
var stableId: InviteLinkInviteEntryId { var stableId: InviteLinkInviteEntryId {
switch self { switch self {
case .header:
return .header
case .mainLink: case .mainLink:
return .mainLink return .mainLink
case let .links(index, _, _): case let .links(index, _, _):
return .links(index) return .links(index)
case .manage:
return .manage
} }
} }
static func ==(lhs: InviteLinkInviteEntry, rhs: InviteLinkInviteEntry) -> Bool { static func ==(lhs: InviteLinkInviteEntry, rhs: InviteLinkInviteEntry) -> Bool {
switch lhs { switch lhs {
case let .header(lhsTheme, lhsTitle, lhsText):
if case let .header(rhsTheme, rhsTitle, rhsText) = rhs, lhsTheme === rhsTheme, lhsTitle == rhsTitle, lhsText == rhsText {
return true
} else {
return false
}
case let .mainLink(lhsTheme, lhsInvitation): case let .mainLink(lhsTheme, lhsInvitation):
if case let .mainLink(rhsTheme, rhsInvitation) = rhs, lhsTheme === rhsTheme, lhsInvitation == rhsInvitation { if case let .mainLink(rhsTheme, rhsInvitation) = rhs, lhsTheme === rhsTheme, lhsInvitation == rhsInvitation {
return true return true
@ -76,43 +90,71 @@ private enum InviteLinkInviteEntry: Comparable, Identifiable {
} else { } else {
return false return false
} }
case let .manage(lhsTheme, lhsText):
if case let .manage(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
} }
} }
static func <(lhs: InviteLinkInviteEntry, rhs: InviteLinkInviteEntry) -> Bool { static func <(lhs: InviteLinkInviteEntry, rhs: InviteLinkInviteEntry) -> Bool {
switch lhs { switch lhs {
case .header:
switch rhs {
case .header:
return false
case .mainLink, .links, .manage:
return true
}
case .mainLink: case .mainLink:
switch rhs { switch rhs {
case .mainLink: case .header, .mainLink:
return false return false
case .links: case .links, .manage:
return true return true
} }
case let .links(lhsIndex, _, _): case let .links(lhsIndex, _, _):
switch rhs { switch rhs {
case .mainLink: case .header, .mainLink:
return false return false
case let .links(rhsIndex, _, _): case let .links(rhsIndex, _, _):
return lhsIndex < rhsIndex return lhsIndex < rhsIndex
case .manage:
return true
}
case .manage:
switch rhs {
case .header, .mainLink, .links:
return false
case .manage:
return true
} }
} }
} }
func item(account: Account, presentationData: PresentationData, interaction: InviteLinkInviteInteraction) -> ListViewItem { func item(account: Account, presentationData: PresentationData, interaction: InviteLinkInviteInteraction) -> ListViewItem {
switch self { switch self {
case let .header(theme, title, text):
return InviteLinkInviteHeaderItem(theme: theme, title: title, text: text)
case let .mainLink(_, invite): case let .mainLink(_, invite):
return ItemListPermanentInviteLinkItem(context: interaction.context, presentationData: ItemListPresentationData(presentationData), invite: invite, peers: [], buttonColor: nil, sectionId: 0, style: .plain, shareAction: { return ItemListPermanentInviteLinkItem(context: interaction.context, presentationData: ItemListPresentationData(presentationData), invite: invite, peers: [], displayButton: true, displayImporters: false, buttonColor: nil, sectionId: 0, style: .plain, shareAction: {
interaction.shareLink(invite) interaction.shareLink(invite)
}, contextAction: { node in }, contextAction: { node in
interaction.mainLinkContextAction(invite, node, nil) interaction.mainLinkContextAction(invite, node, nil)
}, viewAction: { }, viewAction: {
}) })
case let .links(_, _, invites): case let .links(_, _, invites):
return ItemListInviteLinkGridItem(presentationData: ItemListPresentationData(presentationData), invites: invites, sectionId: 0, style: .plain, tapAction: { invite in return ItemListInviteLinkGridItem(presentationData: ItemListPresentationData(presentationData), invites: invites, share: true, sectionId: 0, style: .plain, tapAction: { invite in
interaction.copyLink(invite) interaction.copyLink(invite)
}, contextAction: { invite, _ in }, contextAction: { invite, _ in
interaction.shareLink(invite) interaction.shareLink(invite)
}) })
case let .manage(theme, text):
return InviteLinkInviteManageItem(theme: theme, text: text, action: {
interaction.manageLinks()
})
} }
} }
} }
@ -140,7 +182,6 @@ public final class InviteLinkInviteController: ViewController {
private var presentationDataDisposable: Disposable? private var presentationDataDisposable: Disposable?
public init(context: AccountContext, peerId: PeerId) { public init(context: AccountContext, peerId: PeerId) {
fatalError()
self.context = context self.context = context
self.peerId = peerId self.peerId = peerId
@ -196,7 +237,7 @@ public final class InviteLinkInviteController: ViewController {
self.controllerNode.animateOut(completion: { [weak self] in self.controllerNode.animateOut(completion: { [weak self] in
completion?() completion?()
self?.dismiss(animated: false) self?.presentingViewController?.dismiss(animated: false, completion: nil)
}) })
} }
} }
@ -360,12 +401,15 @@ public final class InviteLinkInviteController: ViewController {
if let strongSelf = self { if let strongSelf = self {
var entries: [InviteLinkInviteEntry] = [] var entries: [InviteLinkInviteEntry] = []
entries.append(.header(presentationData.theme, presentationData.strings.InviteLink_InviteLink, presentationData.strings.InviteLink_CreatePrivateLinkHelp))
if let cachedData = view.cachedData as? CachedGroupData, let invite = cachedData.exportedInvitation { if let cachedData = view.cachedData as? CachedGroupData, let invite = cachedData.exportedInvitation {
entries.append(.mainLink(presentationData.theme, invite)) entries.append(.mainLink(presentationData.theme, invite))
} else if let cachedData = view.cachedData as? CachedChannelData, let invite = cachedData.exportedInvitation { } else if let cachedData = view.cachedData as? CachedChannelData, let invite = cachedData.exportedInvitation {
entries.append(.mainLink(presentationData.theme, invite)) entries.append(.mainLink(presentationData.theme, invite))
} }
entries.append(.manage(presentationData.theme, presentationData.strings.InviteLink_Manage))
let previousEntries = previousEntries.swap(entries) let previousEntries = previousEntries.swap(entries)
@ -507,7 +551,7 @@ public final class InviteLinkInviteController: ViewController {
insets.bottom = layout.intrinsicInsets.bottom insets.bottom = layout.intrinsicInsets.bottom
let headerHeight: CGFloat = 54.0 let headerHeight: CGFloat = 54.0
let visibleItemsHeight: CGFloat = 147.0 + floor(52.0 * 3.5) let visibleItemsHeight: CGFloat = 409.0
let layoutTopInset: CGFloat = max(layout.statusBarHeight ?? 0.0, layout.safeInsets.top) let layoutTopInset: CGFloat = max(layout.statusBarHeight ?? 0.0, layout.safeInsets.top)

View File

@ -13,10 +13,12 @@ class InviteLinkInviteHeaderItem: ListViewItem, ItemListItem {
var sectionId: ItemListSectionId = 0 var sectionId: ItemListSectionId = 0
let theme: PresentationTheme let theme: PresentationTheme
let title: String
let text: String let text: String
init(theme: PresentationTheme, text: String) { init(theme: PresentationTheme, title: String, text: String) {
self.theme = theme self.theme = theme
self.title = title
self.text = text self.text = text
} }
@ -57,58 +59,86 @@ class InviteLinkInviteHeaderItem: ListViewItem, ItemListItem {
} }
} }
private let titleFont = Font.regular(13.0) private let titleFont = Font.medium(23.0)
private let textFont = Font.regular(13.0)
class InviteLinkInviteHeaderItemNode: ListViewItemNode { class InviteLinkInviteHeaderItemNode: ListViewItemNode {
private let titleNode: TextNode private let titleNode: TextNode
private var animationNode: AnimatedStickerNode private let textNode: TextNode
private let iconBackgroundNode: ASImageNode
private let iconNode: ASImageNode
private var item: InviteLinkInviteHeaderItem? private var item: InviteLinkInviteHeaderItem?
init() { init() {
self.titleNode = TextNode() self.titleNode = TextNode()
self.titleNode.isUserInteractionEnabled = false self.titleNode.isUserInteractionEnabled = false
self.titleNode.contentMode = .left
self.titleNode.contentsScale = UIScreen.main.scale
self.animationNode = AnimatedStickerNode() self.textNode = TextNode()
if let path = getAppBundle().path(forResource: "Invite", ofType: "tgs") { self.textNode.isUserInteractionEnabled = false
self.animationNode.setup(source: AnimatedStickerNodeLocalFileSource(path: path), width: 192, height: 192, playbackMode: .loop, mode: .direct(cachePathPrefix: nil))
self.animationNode.visibility = true self.iconBackgroundNode = ASImageNode()
} self.iconBackgroundNode.displaysAsynchronously = false
self.iconBackgroundNode.displayWithoutProcessing = true
self.iconNode = ASImageNode()
self.iconNode.contentMode = .center
self.iconNode.displaysAsynchronously = false
self.iconNode.displayWithoutProcessing = true
super.init(layerBacked: false, dynamicBounce: false) super.init(layerBacked: false, dynamicBounce: false)
self.addSubnode(self.titleNode) self.addSubnode(self.titleNode)
self.addSubnode(self.animationNode) self.addSubnode(self.textNode)
self.addSubnode(self.iconBackgroundNode)
self.addSubnode(self.iconNode)
} }
func asyncLayout() -> (_ item: InviteLinkInviteHeaderItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) { func asyncLayout() -> (_ item: InviteLinkInviteHeaderItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) {
let makeTitleLayout = TextNode.asyncLayout(self.titleNode) let makeTitleLayout = TextNode.asyncLayout(self.titleNode)
let makeTextLayout = TextNode.asyncLayout(self.textNode)
let currentItem = self.item
return { item, params, neighbors in return { item, params, neighbors in
let leftInset: CGFloat = 32.0 + params.leftInset let leftInset: CGFloat = 40.0 + params.leftInset
let topInset: CGFloat = 92.0 let topInset: CGFloat = 98.0
let spacing: CGFloat = 8.0
let bottomInset: CGFloat = 24.0
let attributedText = NSAttributedString(string: item.text, font: titleFont, textColor: item.theme.list.freeTextColor) var updatedTheme: PresentationTheme?
let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: attributedText, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: params.width - params.rightInset - leftInset * 2.0, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets())) if currentItem?.theme !== item.theme {
updatedTheme = item.theme
}
let contentSize = CGSize(width: params.width, height: topInset + titleLayout.size.height) let titleAttributedText = NSAttributedString(string: item.title, font: titleFont, textColor: item.theme.list.itemPrimaryTextColor)
let insets = itemListNeighborsGroupedInsets(neighbors) let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: titleAttributedText, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: params.width - params.rightInset - leftInset * 2.0, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets()))
let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets) let attributedText = NSAttributedString(string: item.text, font: textFont, textColor: item.theme.list.freeTextColor)
let (textLayout, textApply) = makeTextLayout(TextNodeLayoutArguments(attributedString: attributedText, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: params.width - params.rightInset - leftInset * 2.0, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets()))
let contentSize = CGSize(width: params.width, height: topInset + titleLayout.size.height + spacing + textLayout.size.height + bottomInset)
let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: UIEdgeInsets())
return (layout, { [weak self] in return (layout, { [weak self] in
if let strongSelf = self { if let strongSelf = self {
strongSelf.item = item strongSelf.item = item
strongSelf.accessibilityLabel = attributedText.string strongSelf.accessibilityLabel = attributedText.string
if let _ = updatedTheme {
strongSelf.iconBackgroundNode.image = generateFilledCircleImage(diameter: 92.0, color: item.theme.actionSheet.controlAccentColor)
strongSelf.iconNode.image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Links/LargeLink"), color: item.theme.list.itemCheckColors.foregroundColor)
}
let iconSize = CGSize(width: 96.0, height: 96.0) let iconSize = CGSize(width: 92.0, height: 92.0)
strongSelf.animationNode.frame = CGRect(origin: CGPoint(x: floor((layout.size.width - iconSize.width) / 2.0), y: -10.0), size: iconSize) strongSelf.iconBackgroundNode.frame = CGRect(origin: CGPoint(x: floor((layout.size.width - iconSize.width) / 2.0), y: -10.0), size: iconSize)
strongSelf.animationNode.updateLayout(size: iconSize) strongSelf.iconNode.frame = strongSelf.iconBackgroundNode.frame
let _ = titleApply() let _ = titleApply()
strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: floor((layout.size.width - titleLayout.size.width) / 2.0), y: topInset + 8.0), size: titleLayout.size) strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: floor((layout.size.width - titleLayout.size.width) / 2.0), y: topInset + 8.0), size: titleLayout.size)
let _ = textApply()
strongSelf.textNode.frame = CGRect(origin: CGPoint(x: floor((layout.size.width - textLayout.size.width) / 2.0), y: topInset + 8.0 + titleLayout.size.height + spacing), size: textLayout.size)
} }
}) })
} }

View File

@ -0,0 +1,113 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import SwiftSignalKit
import TelegramPresentationData
import ItemListUI
import PresentationDataUtils
import AnimatedStickerNode
import AppBundle
class InviteLinkInviteManageItem: ListViewItem, ItemListItem {
var sectionId: ItemListSectionId = 0
let theme: PresentationTheme
let text: String
let action: () -> Void
init(theme: PresentationTheme, text: String, action: @escaping () -> Void) {
self.theme = theme
self.text = text
self.action = action
}
func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) {
async {
let node = InviteLinkInviteManageItemNode()
let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
node.contentSize = layout.contentSize
node.insets = layout.insets
Queue.mainQueue().async {
completion(node, {
return (nil, { _ in apply() })
})
}
}
}
func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) {
Queue.mainQueue().async {
guard let nodeValue = node() as? InviteLinkInviteManageItemNode else {
assertionFailure()
return
}
let makeLayout = nodeValue.asyncLayout()
async {
let (layout, apply) = makeLayout(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
Queue.mainQueue().async {
completion(layout, { _ in
apply()
})
}
}
}
}
}
private let titleFont = Font.medium(23.0)
private let textFont = Font.regular(13.0)
class InviteLinkInviteManageItemNode: ListViewItemNode {
private let backgroundNode: ASDisplayNode
private let buttonNode: HighlightableButtonNode
private var item: InviteLinkInviteManageItem?
init() {
self.backgroundNode = ASDisplayNode()
self.buttonNode = HighlightableButtonNode()
super.init(layerBacked: false, dynamicBounce: false)
self.addSubnode(self.backgroundNode)
self.addSubnode(self.buttonNode)
self.buttonNode.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: .touchUpInside)
}
@objc private func buttonPressed() {
self.item?.action()
}
func asyncLayout() -> (_ item: InviteLinkInviteManageItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) {
return { item, params, neighbors in
let contentSize = CGSize(width: params.width, height: 70.0)
let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: UIEdgeInsets())
return (layout, { [weak self] in
if let strongSelf = self {
strongSelf.item = item
strongSelf.buttonNode.setTitle(item.text, with: Font.regular(17.0), with: item.theme.actionSheet.controlAccentColor, for: .normal)
let size = strongSelf.buttonNode.measure(layout.contentSize)
strongSelf.buttonNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((layout.contentSize.width - size.width) / 2.0), y: floorToScreenPixels((layout.contentSize.height - size.height) / 2.0)), size: size)
}
})
}
}
override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) {
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4)
}
override func animateRemoved(_ currentTimestamp: Double, duration: Double) {
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false)
}
}

View File

@ -178,7 +178,7 @@ private enum InviteLinksListEntry: ItemListNodeEntry {
case let .mainLinkHeader(_, text): case let .mainLinkHeader(_, text):
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
case let .mainLink(_, invite, peers): case let .mainLink(_, invite, peers):
return ItemListPermanentInviteLinkItem(context: arguments.context, presentationData: presentationData, invite: invite, peers: peers, buttonColor: nil, sectionId: self.section, style: .blocks, shareAction: { return ItemListPermanentInviteLinkItem(context: arguments.context, presentationData: presentationData, invite: invite, peers: peers, displayButton: true, displayImporters: true, buttonColor: nil, sectionId: self.section, style: .blocks, shareAction: {
arguments.shareMainLink(invite) arguments.shareMainLink(invite)
}, contextAction: { node in }, contextAction: { node in
arguments.mainLinkContextAction(invite, node, nil) arguments.mainLinkContextAction(invite, node, nil)
@ -194,7 +194,7 @@ private enum InviteLinksListEntry: ItemListNodeEntry {
arguments.createLink() arguments.createLink()
}) })
case let .links(_, _, invites): case let .links(_, _, invites):
return ItemListInviteLinkGridItem(presentationData: presentationData, invites: invites, sectionId: self.section, style: .blocks, tapAction: { invite in return ItemListInviteLinkGridItem(presentationData: presentationData, invites: invites, share: false, sectionId: self.section, style: .blocks, tapAction: { invite in
arguments.openLink(invite) arguments.openLink(invite)
}, contextAction: { invite, node in }, contextAction: { invite, node in
arguments.linkContextAction(invite, node, nil) arguments.linkContextAction(invite, node, nil)
@ -208,7 +208,7 @@ private enum InviteLinksListEntry: ItemListNodeEntry {
arguments.deleteAllRevokedLinks() arguments.deleteAllRevokedLinks()
}) })
case let .revokedLinks(_, _, invites): case let .revokedLinks(_, _, invites):
return ItemListInviteLinkGridItem(presentationData: presentationData, invites: invites, sectionId: self.section, style: .blocks, tapAction: { invite in return ItemListInviteLinkGridItem(presentationData: presentationData, invites: invites, share: false, sectionId: self.section, style: .blocks, tapAction: { invite in
arguments.openLink(invite) arguments.openLink(invite)
}, contextAction: { invite, node in }, contextAction: { invite, node in
arguments.linkContextAction(invite, node, nil) arguments.linkContextAction(invite, node, nil)
@ -468,8 +468,8 @@ public func inviteLinkListController(context: AccountContext, peerId: PeerId) ->
ActionSheetTextItem(title: presentationData.strings.GroupInfo_InviteLink_RevokeAlert_Text), ActionSheetTextItem(title: presentationData.strings.GroupInfo_InviteLink_RevokeAlert_Text),
ActionSheetButtonItem(title: presentationData.strings.GroupInfo_InviteLink_RevokeLink, color: .destructive, action: { ActionSheetButtonItem(title: presentationData.strings.GroupInfo_InviteLink_RevokeLink, color: .destructive, action: {
dismissAction() dismissAction()
revokeLinkDisposable.set((revokePeerExportedInvitation(account: context.account, peerId: peerId, link: invite.link) |> deliverOnMainQueue).start(completed: { revokeLinkDisposable.set((deletePeerExportedInvitation(account: context.account, peerId: peerId, link: invite.link) |> deliverOnMainQueue).start(completed: {
})) }))
}) })

View File

@ -72,7 +72,7 @@ private enum InviteLinkViewEntry: Comparable, Identifiable {
case creatorHeader(PresentationTheme, String) case creatorHeader(PresentationTheme, String)
case creator(PresentationTheme, PresentationDateTimeFormat, Peer, Int32) case creator(PresentationTheme, PresentationDateTimeFormat, Peer, Int32)
case importerHeader(PresentationTheme, String) case importerHeader(PresentationTheme, String)
case importer(Int32, PresentationTheme, PresentationDateTimeFormat, Peer, Int32) case importer(Int32, PresentationTheme, PresentationDateTimeFormat, Peer, Int32, Bool)
var stableId: InviteLinkViewEntryId { var stableId: InviteLinkViewEntryId {
switch self { switch self {
@ -84,7 +84,7 @@ private enum InviteLinkViewEntry: Comparable, Identifiable {
return .creator return .creator
case .importerHeader: case .importerHeader:
return .importerHeader return .importerHeader
case let .importer(_, _, _, peer, _): case let .importer(_, _, _, peer, _, _):
return .importer(peer.id) return .importer(peer.id)
} }
} }
@ -115,8 +115,8 @@ private enum InviteLinkViewEntry: Comparable, Identifiable {
} else { } else {
return false return false
} }
case let .importer(lhsIndex, lhsTheme, lhsDateTimeFormat, lhsPeer, lhsDate): case let .importer(lhsIndex, lhsTheme, lhsDateTimeFormat, lhsPeer, lhsDate, lhsLoading):
if case let .importer(rhsIndex, rhsTheme, rhsDateTimeFormat, rhsPeer, rhsDate) = rhs, lhsIndex == rhsIndex, lhsTheme === rhsTheme, lhsDateTimeFormat == rhsDateTimeFormat, arePeersEqual(lhsPeer, rhsPeer), lhsDate == rhsDate { if case let .importer(rhsIndex, rhsTheme, rhsDateTimeFormat, rhsPeer, rhsDate, rhsLoading) = rhs, lhsIndex == rhsIndex, lhsTheme === rhsTheme, lhsDateTimeFormat == rhsDateTimeFormat, arePeersEqual(lhsPeer, rhsPeer), lhsDate == rhsDate {
return true return true
} else { } else {
return false return false
@ -154,11 +154,11 @@ private enum InviteLinkViewEntry: Comparable, Identifiable {
case .creator, .importer: case .creator, .importer:
return true return true
} }
case let .importer(lhsIndex, _, _, _, _): case let .importer(lhsIndex, _, _, _, _, _):
switch rhs { switch rhs {
case .link, .creatorHeader, .creator, .importerHeader: case .link, .creatorHeader, .creator, .importerHeader:
return false return false
case let .importer(rhsIndex, _, _, _, _): case let .importer(rhsIndex, _, _, _, _, _):
return lhsIndex < rhsIndex return lhsIndex < rhsIndex
} }
} }
@ -168,7 +168,8 @@ private enum InviteLinkViewEntry: Comparable, Identifiable {
switch self { switch self {
case let .link(_, invite): case let .link(_, invite):
let buttonColor = color(for: invite) let buttonColor = color(for: invite)
return ItemListPermanentInviteLinkItem(context: interaction.context, presentationData: ItemListPresentationData(presentationData), invite: invite, peers: [], buttonColor: buttonColor, sectionId: 0, style: .plain, shareAction: { let availability = invitationAvailability(invite)
return ItemListPermanentInviteLinkItem(context: interaction.context, presentationData: ItemListPresentationData(presentationData), invite: invite, peers: [], displayButton: !invite.isRevoked && !availability.isZero, displayImporters: false, buttonColor: buttonColor, sectionId: 0, style: .plain, shareAction: {
interaction.shareLink(invite) interaction.shareLink(invite)
}, contextAction: { node in }, contextAction: { node in
interaction.contextAction(invite, node, nil) interaction.contextAction(invite, node, nil)
@ -183,11 +184,11 @@ private enum InviteLinkViewEntry: Comparable, Identifiable {
}, setPeerIdWithRevealedOptions: { _, _ in }, removePeer: { _ in }, hasTopStripe: false, noInsets: true, tag: nil) }, setPeerIdWithRevealedOptions: { _, _ in }, removePeer: { _ in }, hasTopStripe: false, noInsets: true, tag: nil)
case let .importerHeader(_, title): case let .importerHeader(_, title):
return SectionHeaderItem(presentationData: ItemListPresentationData(presentationData), title: title) return SectionHeaderItem(presentationData: ItemListPresentationData(presentationData), title: title)
case let .importer(_, _, dateTimeFormat, peer, date): case let .importer(_, _, dateTimeFormat, peer, date, loading):
let dateString = stringForFullDate(timestamp: date, strings: presentationData.strings, dateTimeFormat: dateTimeFormat) let dateString = stringForFullDate(timestamp: date, strings: presentationData.strings, dateTimeFormat: dateTimeFormat)
return ItemListPeerItem(presentationData: ItemListPresentationData(presentationData), dateTimeFormat: dateTimeFormat, nameDisplayOrder: presentationData.nameDisplayOrder, context: interaction.context, peer: peer, height: .generic, nameStyle: .distinctBold, presence: nil, text: .text(dateString, .secondary), label: .none, editing: ItemListPeerItemEditing(editable: false, editing: false, revealed: false), revealOptions: nil, switchValue: nil, enabled: true, selectable: true, sectionId: 0, action: { return ItemListPeerItem(presentationData: ItemListPresentationData(presentationData), dateTimeFormat: dateTimeFormat, nameDisplayOrder: presentationData.nameDisplayOrder, context: interaction.context, peer: peer, height: .generic, nameStyle: .distinctBold, presence: nil, text: .text(dateString, .secondary), label: .none, editing: ItemListPeerItemEditing(editable: false, editing: false, revealed: false), revealOptions: nil, switchValue: nil, enabled: true, selectable: true, sectionId: 0, action: {
interaction.openPeer(peer.id) interaction.openPeer(peer.id)
}, setPeerIdWithRevealedOptions: { _, _ in }, removePeer: { _ in }, hasTopStripe: false, noInsets: true, tag: nil) }, setPeerIdWithRevealedOptions: { _, _ in }, removePeer: { _ in }, hasTopStripe: false, noInsets: true, tag: nil, shimmering: ItemListPeerItemShimmering(alternationIndex: 0))
} }
} }
} }
@ -403,7 +404,16 @@ public final class InviteLinkViewController: ViewController {
self?.controller?.present(OverlayStatusController(theme: presentationData.theme, type: .genericSuccess(presentationData.strings.Username_LinkCopied, false)), in: .window(.root)) self?.controller?.present(OverlayStatusController(theme: presentationData.theme, type: .genericSuccess(presentationData.strings.Username_LinkCopied, false)), in: .window(.root))
}))) })))
if !invite.isRevoked { if invite.isRevoked {
items.append(.action(ContextMenuActionItem(text: presentationData.strings.InviteLink_ContextDelete, textColor: .destructive, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.actionSheet.destructiveActionTextColor)
}, action: { [weak self] _, f in
f(.dismissWithoutContent)
let controller = InviteLinkQRCodeController(context: context, invite: invite)
self?.controller?.present(controller, in: .window(.root))
})))
} else {
items.append(.action(ContextMenuActionItem(text: presentationData.strings.InviteLink_ContextGetQRCode, icon: { theme in items.append(.action(ContextMenuActionItem(text: presentationData.strings.InviteLink_ContextGetQRCode, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Wallet/QrIcon"), color: theme.contextMenu.primaryColor) return generateTintedImage(image: UIImage(bundleImageName: "Wallet/QrIcon"), color: theme.contextMenu.primaryColor)
}, action: { [weak self] _, f in }, action: { [weak self] _, f in
@ -430,16 +440,23 @@ public final class InviteLinkViewController: ViewController {
entries.append(.creatorHeader(presentationData.theme, presentationData.strings.InviteLink_CreatedBy.uppercased())) entries.append(.creatorHeader(presentationData.theme, presentationData.strings.InviteLink_CreatedBy.uppercased()))
entries.append(.creator(presentationData.theme, presentationData.dateTimeFormat, creatorPeer, invite.date)) entries.append(.creator(presentationData.theme, presentationData.dateTimeFormat, creatorPeer, invite.date))
if !state.importers.isEmpty { if !state.importers.isEmpty || (state.isLoadingMore && state.count > 0) {
entries.append(.importerHeader(presentationData.theme, presentationData.strings.InviteLink_PeopleJoined(Int32(state.count)).uppercased())) entries.append(.importerHeader(presentationData.theme, presentationData.strings.InviteLink_PeopleJoined(Int32(state.count)).uppercased()))
} }
var index: Int32 = 0 var index: Int32 = 0
for importer in state.importers { if state.importers.isEmpty && state.isLoadingMore {
if let peer = importer.peer.peer { let fakeUser = TelegramUser(id: PeerId(namespace: -1, id: 0), accessHash: nil, firstName: "", lastName: "", username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [])
entries.append(.importer(index, presentationData.theme, presentationData.dateTimeFormat, peer, importer.date)) for i in 0 ..< min(4, state.count) {
entries.append(.importer(Int32(i), presentationData.theme, presentationData.dateTimeFormat, fakeUser, 0, true))
}
} else {
for importer in state.importers {
if let peer = importer.peer.peer {
entries.append(.importer(index, presentationData.theme, presentationData.dateTimeFormat, peer, importer.date, false))
}
index += 1
} }
index += 1
} }
let previousEntries = previousEntries.swap(entries) let previousEntries = previousEntries.swap(entries)

View File

@ -42,10 +42,35 @@ func invitationAvailability(_ invite: ExportedInvitation) -> CGFloat {
let fraction = 1.0 - (CGFloat(count) / CGFloat(usageLimit)) let fraction = 1.0 - (CGFloat(count) / CGFloat(usageLimit))
availability = min(fraction, availability) availability = min(fraction, availability)
} }
return availability return max(0.0, min(1.0, availability))
}
private enum ItemBackgroundColor: Equatable {
case blue
case green
case yellow
case red
case gray
var colors: (top: UIColor, bottom: UIColor, text: UIColor) {
switch self {
case .blue:
return (UIColor(rgb: 0x00b5f7), UIColor(rgb: 0x00b2f6), UIColor(rgb: 0xa7f4ff))
case .green:
return (UIColor(rgb: 0x4aca62), UIColor(rgb: 0x43c85c), UIColor(rgb: 0xc5ffe6))
case .yellow:
return (UIColor(rgb: 0xf8a953), UIColor(rgb: 0xf7a64e), UIColor(rgb: 0xfeffd7))
case .red:
return (UIColor(rgb: 0xf2656a), UIColor(rgb: 0xf25f65), UIColor(rgb: 0xffd3de))
case .gray:
return (UIColor(rgb: 0xd4d8db), UIColor(rgb: 0xced2d5), UIColor(rgb: 0xf8f9f9))
}
}
} }
private class ItemNode: ASDisplayNode { private class ItemNode: ASDisplayNode {
private let selectionNode: HighlightTrackingButtonNode
private let wrapperNode: ASDisplayNode
private let backgroundNode: ASImageNode private let backgroundNode: ASImageNode
private let iconNode: ASImageNode private let iconNode: ASImageNode
@ -59,19 +84,28 @@ private class ItemNode: ASDisplayNode {
private let titleNode: ImmediateTextNode private let titleNode: ImmediateTextNode
private let subtitleNode: ImmediateTextNode private let subtitleNode: ImmediateTextNode
private var params: (size: CGSize, wide: Bool, invite: ExportedInvitation, presentationData: ItemListPresentationData)? private var updateTimer: SwiftSignalKit.Timer?
private var params: (size: CGSize, wide: Bool, invite: ExportedInvitation, color: ItemBackgroundColor, presentationData: ItemListPresentationData)?
var action: (() -> Void)? var action: (() -> Void)?
var contextAction: ((ASDisplayNode) -> Void)? var contextAction: ((ASDisplayNode) -> Void)?
private let hapticFeedback = HapticFeedback()
override init() { override init() {
self.selectionNode = HighlightTrackingButtonNode()
self.wrapperNode = ASDisplayNode()
self.backgroundNode = ASImageNode() self.backgroundNode = ASImageNode()
self.backgroundNode.displaysAsynchronously = false self.backgroundNode.displaysAsynchronously = false
self.backgroundNode.displayWithoutProcessing = true self.backgroundNode.displayWithoutProcessing = true
self.backgroundNode.isUserInteractionEnabled = false
self.iconNode = ASImageNode() self.iconNode = ASImageNode()
self.iconNode.displaysAsynchronously = false self.iconNode.displaysAsynchronously = false
self.iconNode.displayWithoutProcessing = true self.iconNode.displayWithoutProcessing = true
self.iconNode.isUserInteractionEnabled = false
self.buttonNode = HighlightTrackingButtonNode() self.buttonNode = HighlightTrackingButtonNode()
self.extractedContainerNode = ContextExtractedContentContainingNode() self.extractedContainerNode = ContextExtractedContentContainingNode()
@ -94,23 +128,41 @@ private class ItemNode: ASDisplayNode {
self.titleNode = ImmediateTextNode() self.titleNode = ImmediateTextNode()
self.titleNode.maximumNumberOfLines = 2 self.titleNode.maximumNumberOfLines = 2
self.titleNode.isUserInteractionEnabled = false
self.subtitleNode = ImmediateTextNode() self.subtitleNode = ImmediateTextNode()
self.subtitleNode.maximumNumberOfLines = 1 self.subtitleNode.maximumNumberOfLines = 1
self.subtitleNode.isUserInteractionEnabled = false
super.init() super.init()
self.addSubnode(self.backgroundNode) self.addSubnode(self.wrapperNode)
self.addSubnode(self.iconNode) self.wrapperNode.addSubnode(self.backgroundNode)
self.wrapperNode.addSubnode(self.iconNode)
self.containerNode.addSubnode(self.extractedContainerNode) self.containerNode.addSubnode(self.extractedContainerNode)
self.extractedContainerNode.contentNode.addSubnode(self.buttonIconNode) self.extractedContainerNode.contentNode.addSubnode(self.buttonIconNode)
self.containerNode.targetNodeForActivationProgress = self.extractedContainerNode.contentNode self.containerNode.targetNodeForActivationProgress = self.extractedContainerNode.contentNode
self.buttonNode.addSubnode(self.containerNode) self.buttonNode.addSubnode(self.containerNode)
self.addSubnode(self.buttonNode)
self.addSubnode(self.titleNode) self.wrapperNode.addSubnode(self.selectionNode)
self.addSubnode(self.subtitleNode) self.wrapperNode.addSubnode(self.buttonNode)
self.wrapperNode.addSubnode(self.titleNode)
self.wrapperNode.addSubnode(self.subtitleNode)
self.selectionNode.addTarget(self, action: #selector(self.tapped), forControlEvents: .touchUpInside)
self.selectionNode.highligthedChanged = { [weak self] highlighted in
if let strongSelf = self {
if highlighted {
let transition: ContainedViewLayoutTransition = .animated(duration: 0.18, curve: .linear)
transition.updateSublayerTransformScale(node: strongSelf, scale: 0.95)
} else {
let transition: ContainedViewLayoutTransition = .animated(duration: 0.25, curve: .linear)
transition.updateSublayerTransformScale(node: strongSelf, scale: 1.0)
}
}
}
self.buttonNode.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: .touchUpInside) self.buttonNode.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: .touchUpInside)
self.buttonNode.highligthedChanged = { [weak self] highlighted in self.buttonNode.highligthedChanged = { [weak self] highlighted in
@ -126,11 +178,12 @@ private class ItemNode: ASDisplayNode {
} }
} }
override func didLoad() { deinit {
self.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:)))) self.updateTimer?.invalidate()
} }
@objc private func tapGesture(_ gestureRecognizer: UITapGestureRecognizer) { @objc private func tapped() {
self.hapticFeedback.impact(.light)
self.action?() self.action?()
} }
@ -138,35 +191,66 @@ private class ItemNode: ASDisplayNode {
self.contextAction?(self.extractedContainerNode) self.contextAction?(self.extractedContainerNode)
} }
func update(size: CGSize, wide: Bool, invite: ExportedInvitation, presentationData: ItemListPresentationData, transition: ContainedViewLayoutTransition) -> CGSize { func update(size: CGSize, wide: Bool, share: Bool, invite: ExportedInvitation, presentationData: ItemListPresentationData, transition: ContainedViewLayoutTransition) -> CGSize {
let updated = self.params?.size != size || self.params?.wide != wide || self.params?.invite != invite
self.params = (size, wide, invite, presentationData)
let currentTime = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970) let currentTime = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970)
let availability = invitationAvailability(invite) let availability = invitationAvailability(invite)
let color: ItemBackgroundColor
var isExpired = false
let secondaryTextColor: UIColor
if invite.isRevoked { if invite.isRevoked {
self.backgroundNode.image = generateBackgroundImage(colors: [UIColor(rgb: 0xd4d8db).cgColor, UIColor(rgb: 0xced2d5).cgColor]) color = .gray
secondaryTextColor = UIColor(rgb: 0xf8f9f9)
} else if invite.expireDate == nil && invite.usageLimit == nil { } else if invite.expireDate == nil && invite.usageLimit == nil {
self.backgroundNode.image = generateBackgroundImage(colors: [UIColor(rgb: 0x00b5f7).cgColor, UIColor(rgb: 0x00b2f6).cgColor]) color = .blue
secondaryTextColor = UIColor(rgb: 0xa7f4ff)
} else if availability >= 0.5 { } else if availability >= 0.5 {
self.backgroundNode.image = generateBackgroundImage(colors: [UIColor(rgb: 0x4aca62).cgColor, UIColor(rgb: 0x43c85c).cgColor]) color = .green
secondaryTextColor = UIColor(rgb: 0xc5ffe6)
} else if availability > 0.0 { } else if availability > 0.0 {
self.backgroundNode.image = generateBackgroundImage(colors: [UIColor(rgb: 0xf8a953).cgColor, UIColor(rgb: 0xf7a64e).cgColor]) color = .yellow
secondaryTextColor = UIColor(rgb: 0xfeffd7)
} else { } else {
self.backgroundNode.image = generateBackgroundImage(colors: [UIColor(rgb: 0xf2656a).cgColor, UIColor(rgb: 0xf25f65).cgColor]) color = .red
secondaryTextColor = UIColor(rgb: 0xffd3de)
isExpired = true
} }
let itemWidth = wide ? size.width : floor((size.width - itemSpacing) / 2.0) let previousParams = self.params
self.params = (size, wide, invite, color, presentationData)
let previousExpireDate = previousParams?.invite.expireDate
if previousExpireDate != invite.expireDate {
self.updateTimer?.invalidate()
self.updateTimer = nil
if let _ = invite.expireDate, availability > 0.0 {
let updateTimer = SwiftSignalKit.Timer(timeout: 5.0, repeat: true, completion: { [weak self] in
if let strongSelf = self {
if let (size, wide, invite, _, presentationData) = strongSelf.params {
let _ = strongSelf.update(size: size, wide: wide, share: share, invite: invite, presentationData: presentationData, transition: .animated(duration: 0.3, curve: .linear))
}
}
}, queue: Queue.mainQueue())
self.updateTimer = updateTimer
updateTimer.start()
}
} else if availability.isZero {
self.updateTimer?.invalidate()
self.updateTimer = nil
}
let colors: NSArray = [color.colors.top.cgColor, color.colors.bottom.cgColor]
if let (_, _, previousInvite, previousColor, _) = previousParams, previousInvite == invite {
if previousColor != color {
if let snapshotView = self.wrapperNode.view.snapshotContentTree() {
snapshotView.frame = self.wrapperNode.bounds
self.wrapperNode.view.addSubview(snapshotView)
snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { [weak snapshotView] _ in
snapshotView?.removeFromSuperview()
})
}
self.backgroundNode.image = generateBackgroundImage(colors: colors)
}
} else {
self.backgroundNode.image = generateBackgroundImage(colors: colors)
}
let secondaryTextColor = color.colors.text
let itemWidth = wide ? size.width : floor((size.width - itemSpacing) / 2.0)
var inviteLink = invite.link.replacingOccurrences(of: "https://", with: "") var inviteLink = invite.link.replacingOccurrences(of: "https://", with: "")
if !wide { if !wide {
inviteLink = inviteLink.replacingOccurrences(of: "joinchat/", with: "joinchat/\n") inviteLink = inviteLink.replacingOccurrences(of: "joinchat/", with: "joinchat/\n")
@ -184,7 +268,7 @@ private class ItemNode: ASDisplayNode {
if let count = invite.count { if let count = invite.count {
subtitleText = presentationData.strings.InviteLink_PeopleJoinedShort(count) subtitleText = presentationData.strings.InviteLink_PeopleJoinedShort(count)
} else { } else {
subtitleText = isExpired || invite.isRevoked ? presentationData.strings.InviteLink_PeopleJoinedShortNoneExpired : presentationData.strings.InviteLink_PeopleJoinedShortNone subtitleText = [.red, .gray].contains(color) ? presentationData.strings.InviteLink_PeopleJoinedShortNoneExpired : presentationData.strings.InviteLink_PeopleJoinedShortNone
} }
if invite.isRevoked { if invite.isRevoked {
if !subtitleText.isEmpty { if !subtitleText.isEmpty {
@ -194,7 +278,7 @@ private class ItemNode: ASDisplayNode {
self.iconNode.image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Links/Expired"), color: .white) self.iconNode.image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Links/Expired"), color: .white)
self.timerNode?.removeFromSupernode() self.timerNode?.removeFromSupernode()
self.timerNode = nil self.timerNode = nil
} else if let expireDate = invite.expireDate, currentTime > expireDate { } else if let expireDate = invite.expireDate, currentTime >= expireDate {
if !subtitleText.isEmpty { if !subtitleText.isEmpty {
subtitleText += "" subtitleText += ""
} }
@ -202,6 +286,14 @@ private class ItemNode: ASDisplayNode {
self.iconNode.image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Links/Expired"), color: .white) self.iconNode.image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Links/Expired"), color: .white)
self.timerNode?.removeFromSupernode() self.timerNode?.removeFromSupernode()
self.timerNode = nil self.timerNode = nil
} else if let usageLimit = invite.usageLimit, let count = invite.count, count >= usageLimit {
if !subtitleText.isEmpty {
subtitleText += ""
}
subtitleText += presentationData.strings.InviteLink_UsageLimitReached
self.iconNode.image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Links/Expired"), color: .white)
self.timerNode?.removeFromSupernode()
self.timerNode = nil
} else if let expireDate = invite.expireDate { } else if let expireDate = invite.expireDate {
self.iconNode.image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Links/Flame"), color: .white) self.iconNode.image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Links/Flame"), color: .white)
let timerNode: TimerNode let timerNode: TimerNode
@ -209,6 +301,7 @@ private class ItemNode: ASDisplayNode {
timerNode = current timerNode = current
} else { } else {
timerNode = TimerNode() timerNode = TimerNode()
timerNode.isUserInteractionEnabled = false
self.timerNode = timerNode self.timerNode = timerNode
self.addSubnode(timerNode) self.addSubnode(timerNode)
} }
@ -222,8 +315,7 @@ private class ItemNode: ASDisplayNode {
self.iconNode.frame = CGRect(x: 10.0, y: 10.0, width: 30.0, height: 30.0) self.iconNode.frame = CGRect(x: 10.0, y: 10.0, width: 30.0, height: 30.0)
self.timerNode?.frame = CGRect(x: 8.0, y: 8.0, width: 34.0, height: 34.0) self.timerNode?.frame = CGRect(x: 8.0, y: 8.0, width: 34.0, height: 34.0)
let subtitle: NSMutableAttributedString = NSMutableAttributedString(string: subtitleText, font: subtitleFont, textColor: secondaryTextColor) self.subtitleNode.attributedText = NSAttributedString(string: subtitleText, font: subtitleFont, textColor: secondaryTextColor)
self.subtitleNode.attributedText = subtitle
let titleSize = self.titleNode.updateLayout(CGSize(width: itemWidth - 24.0, height: 100.0)) let titleSize = self.titleNode.updateLayout(CGSize(width: itemWidth - 24.0, height: 100.0))
let subtitleSize = self.subtitleNode.updateLayout(CGSize(width: itemWidth - 24.0, height: 100.0)) let subtitleSize = self.subtitleNode.updateLayout(CGSize(width: itemWidth - 24.0, height: 100.0))
@ -234,7 +326,9 @@ private class ItemNode: ASDisplayNode {
let itemSize = CGSize(width: itemWidth, height: wide ? 102.0 : 122.0) let itemSize = CGSize(width: itemWidth, height: wide ? 102.0 : 122.0)
let backgroundFrame = CGRect(origin: CGPoint(), size: itemSize) let backgroundFrame = CGRect(origin: CGPoint(), size: itemSize)
transition.updateFrame(node: self.wrapperNode, frame: backgroundFrame)
transition.updateFrame(node: self.backgroundNode, frame: backgroundFrame) transition.updateFrame(node: self.backgroundNode, frame: backgroundFrame)
transition.updateFrame(node: self.selectionNode, frame: backgroundFrame)
let buttonSize = CGSize(width: 26.0, height: 26.0) let buttonSize = CGSize(width: 26.0, height: 26.0)
let buttonFrame = CGRect(origin: CGPoint(x: itemSize.width - buttonSize.width - 12.0, y: 12.0), size: buttonSize) let buttonFrame = CGRect(origin: CGPoint(x: itemSize.width - buttonSize.width - 12.0, y: 12.0), size: buttonSize)
@ -260,7 +354,7 @@ class InviteLinksGridNode: ASDisplayNode {
return result return result
} }
func update(size: CGSize, safeInset: CGFloat, items: [ExportedInvitation], presentationData: ItemListPresentationData, transition: ContainedViewLayoutTransition) -> CGSize { func update(size: CGSize, safeInset: CGFloat, items: [ExportedInvitation], share: Bool, presentationData: ItemListPresentationData, transition: ContainedViewLayoutTransition) -> CGSize {
self.items = items self.items = items
var contentSize: CGSize = size var contentSize: CGSize = size
@ -288,7 +382,7 @@ class InviteLinksGridNode: ASDisplayNode {
let col = CGFloat(i % 2) let col = CGFloat(i % 2)
let row = floor(CGFloat(i) / 2.0) let row = floor(CGFloat(i) / 2.0)
let wide = (i == self.items.count - 1 && (self.items.count % 2) != 0) let wide = (i == self.items.count - 1 && (self.items.count % 2) != 0)
let itemSize = itemNode.update(size: CGSize(width: size.width - sideInset * 2.0, height: size.height), wide: wide, invite: invite, presentationData: presentationData, transition: transition) let itemSize = itemNode.update(size: CGSize(width: size.width - sideInset * 2.0, height: size.height), wide: wide, share: share, invite: invite, presentationData: presentationData, transition: transition)
var itemFrame = CGRect(origin: CGPoint(x: sideInset, y: 4.0 + row * (122.0 + itemSpacing)), size: itemSize) var itemFrame = CGRect(origin: CGPoint(x: sideInset, y: 4.0 + row * (122.0 + itemSpacing)), size: itemSize)
if !wide && col > 0 { if !wide && col > 0 {
itemFrame.origin.x += itemSpacing + itemSize.width itemFrame.origin.x += itemSpacing + itemSize.width
@ -408,7 +502,7 @@ private final class TimerNode: ASDisplayNode {
let currentTimestamp = Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970) let currentTimestamp = Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970)
var fraction = CGFloat(params.deadlineTimestamp - currentTimestamp) / CGFloat(params.deadlineTimestamp - params.creationTimestamp) var fraction = CGFloat(params.deadlineTimestamp - currentTimestamp) / CGFloat(params.deadlineTimestamp - params.creationTimestamp)
fraction = 1.0 - max(0.0, min(0.94, fraction)) fraction = 1.0 - max(0.0, min(1.0, fraction))
let image: UIImage? let image: UIImage?
@ -424,44 +518,47 @@ private final class TimerNode: ASDisplayNode {
let startAngle: CGFloat = -CGFloat.pi / 2.0 let startAngle: CGFloat = -CGFloat.pi / 2.0
let endAngle: CGFloat = -CGFloat.pi / 2.0 + 2.0 * CGFloat.pi * fraction let endAngle: CGFloat = -CGFloat.pi / 2.0 + 2.0 * CGFloat.pi * fraction
let v = CGPoint(x: sin(endAngle), y: -cos(endAngle)) let sparks = fraction > 0.1 && fraction != 1.0
let c = CGPoint(x: -v.y * radius + center.x, y: v.x * radius + center.y) if sparks {
let v = CGPoint(x: sin(endAngle), y: -cos(endAngle))
let dt: CGFloat = 1.0 / 60.0 let c = CGPoint(x: -v.y * radius + center.x, y: v.x * radius + center.y)
var removeIndices: [Int] = []
for i in 0 ..< self.particles.count { let dt: CGFloat = 1.0 / 60.0
let currentTime = timestamp - self.particles[i].beginTime var removeIndices: [Int] = []
if currentTime > self.particles[i].lifetime { for i in 0 ..< self.particles.count {
removeIndices.append(i) let currentTime = timestamp - self.particles[i].beginTime
} else { if currentTime > self.particles[i].lifetime {
let input: CGFloat = CGFloat(currentTime / self.particles[i].lifetime) removeIndices.append(i)
let decelerated: CGFloat = (1.0 - (1.0 - input) * (1.0 - input)) } else {
self.particles[i].alpha = 1.0 - decelerated let input: CGFloat = CGFloat(currentTime / self.particles[i].lifetime)
let decelerated: CGFloat = (1.0 - (1.0 - input) * (1.0 - input))
var p = self.particles[i].position self.particles[i].alpha = 1.0 - decelerated
let d = self.particles[i].direction
let v = self.particles[i].velocity var p = self.particles[i].position
p = CGPoint(x: p.x + d.x * v * dt, y: p.y + d.y * v * dt) let d = self.particles[i].direction
self.particles[i].position = p let v = self.particles[i].velocity
p = CGPoint(x: p.x + d.x * v * dt, y: p.y + d.y * v * dt)
self.particles[i].position = p
}
} }
}
for i in removeIndices.reversed() {
self.particles.remove(at: i)
}
let newParticleCount = 1
for _ in 0 ..< newParticleCount {
let degrees: CGFloat = CGFloat(arc4random_uniform(140)) - 40.0
let angle: CGFloat = degrees * CGFloat.pi / 180.0
let direction = CGPoint(x: v.x * cos(angle) - v.y * sin(angle), y: v.x * sin(angle) + v.y * cos(angle)) for i in removeIndices.reversed() {
let velocity = (20.0 + (CGFloat(arc4random()) / CGFloat(UINT32_MAX)) * 4.0) * 0.3 self.particles.remove(at: i)
}
let lifetime = Double(0.4 + CGFloat(arc4random_uniform(100)) * 0.01) let newParticleCount = 1
for _ in 0 ..< newParticleCount {
let particle = ContentParticle(position: c, direction: direction, velocity: velocity, alpha: 1.0, lifetime: lifetime, beginTime: timestamp) let degrees: CGFloat = CGFloat(arc4random_uniform(140)) - 40.0
self.particles.append(particle) let angle: CGFloat = degrees * CGFloat.pi / 180.0
let direction = CGPoint(x: v.x * cos(angle) - v.y * sin(angle), y: v.x * sin(angle) + v.y * cos(angle))
let velocity = (20.0 + (CGFloat(arc4random()) / CGFloat(UINT32_MAX)) * 4.0) * 0.3
let lifetime = Double(0.4 + CGFloat(arc4random_uniform(100)) * 0.01)
let particle = ContentParticle(position: c, direction: direction, velocity: velocity, alpha: 1.0, lifetime: lifetime, beginTime: timestamp)
self.particles.append(particle)
}
} }
image = generateImage(CGSize(width: diameter + inset, height: diameter + inset), rotatedContext: { size, context in image = generateImage(CGSize(width: diameter + inset, height: diameter + inset), rotatedContext: { size, context in
@ -476,10 +573,12 @@ private final class TimerNode: ASDisplayNode {
context.addPath(path) context.addPath(path)
context.strokePath() context.strokePath()
for particle in self.particles { if sparks {
let size: CGFloat = 2.0 for particle in self.particles {
context.setAlpha(particle.alpha) let size: CGFloat = 2.0
context.fillEllipse(in: CGRect(origin: CGPoint(x: particle.position.x - size / 2.0, y: particle.position.y - size / 2.0), size: CGSize(width: size, height: size))) context.setAlpha(particle.alpha)
context.fillEllipse(in: CGRect(origin: CGPoint(x: particle.position.x - size / 2.0, y: particle.position.y - size / 2.0), size: CGSize(width: size, height: size)))
}
} }
}) })

View File

@ -10,6 +10,7 @@ import ItemListUI
public class ItemListInviteLinkGridItem: ListViewItem, ItemListItem { public class ItemListInviteLinkGridItem: ListViewItem, ItemListItem {
let presentationData: ItemListPresentationData let presentationData: ItemListPresentationData
let invites: [ExportedInvitation]? let invites: [ExportedInvitation]?
let share: Bool
public let sectionId: ItemListSectionId public let sectionId: ItemListSectionId
let style: ItemListStyle let style: ItemListStyle
let tapAction: ((ExportedInvitation) -> Void)? let tapAction: ((ExportedInvitation) -> Void)?
@ -19,6 +20,7 @@ public class ItemListInviteLinkGridItem: ListViewItem, ItemListItem {
public init( public init(
presentationData: ItemListPresentationData, presentationData: ItemListPresentationData,
invites: [ExportedInvitation]?, invites: [ExportedInvitation]?,
share: Bool,
sectionId: ItemListSectionId, sectionId: ItemListSectionId,
style: ItemListStyle, style: ItemListStyle,
tapAction: ((ExportedInvitation) -> Void)?, tapAction: ((ExportedInvitation) -> Void)?,
@ -27,6 +29,7 @@ public class ItemListInviteLinkGridItem: ListViewItem, ItemListItem {
) { ) {
self.presentationData = presentationData self.presentationData = presentationData
self.invites = invites self.invites = invites
self.share = share
self.sectionId = sectionId self.sectionId = sectionId
self.style = style self.style = style
self.tapAction = tapAction self.tapAction = tapAction
@ -167,7 +170,7 @@ public class ItemListInviteLinkGridItemNode: ListViewItemNode, ItemListItemNode
strongSelf.backgroundNode.backgroundColor = itemBackgroundColor strongSelf.backgroundNode.backgroundColor = itemBackgroundColor
} }
let gridSize = strongSelf.gridNode.update(size: contentSize, safeInset: params.leftInset, items: item.invites ?? [], presentationData: item.presentationData, transition: .immediate) let gridSize = strongSelf.gridNode.update(size: contentSize, safeInset: params.leftInset, items: item.invites ?? [], share: item.share, presentationData: item.presentationData, transition: .immediate)
strongSelf.gridNode.frame = CGRect(origin: CGPoint(), size: gridSize) strongSelf.gridNode.frame = CGRect(origin: CGPoint(), size: gridSize)
strongSelf.gridNode.action = { invite in strongSelf.gridNode.action = { invite in
item.tapAction?(invite) item.tapAction?(invite)

View File

@ -30,6 +30,8 @@ public class ItemListPermanentInviteLinkItem: ListViewItem, ItemListItem {
let presentationData: ItemListPresentationData let presentationData: ItemListPresentationData
let invite: ExportedInvitation? let invite: ExportedInvitation?
let peers: [Peer] let peers: [Peer]
let displayButton: Bool
let displayImporters: Bool
let buttonColor: UIColor? let buttonColor: UIColor?
public let sectionId: ItemListSectionId public let sectionId: ItemListSectionId
let style: ItemListStyle let style: ItemListStyle
@ -43,6 +45,8 @@ public class ItemListPermanentInviteLinkItem: ListViewItem, ItemListItem {
presentationData: ItemListPresentationData, presentationData: ItemListPresentationData,
invite: ExportedInvitation?, invite: ExportedInvitation?,
peers: [Peer], peers: [Peer],
displayButton: Bool,
displayImporters: Bool,
buttonColor: UIColor?, buttonColor: UIColor?,
sectionId: ItemListSectionId, sectionId: ItemListSectionId,
style: ItemListStyle, style: ItemListStyle,
@ -55,6 +59,8 @@ public class ItemListPermanentInviteLinkItem: ListViewItem, ItemListItem {
self.presentationData = presentationData self.presentationData = presentationData
self.invite = invite self.invite = invite
self.peers = peers self.peers = peers
self.displayButton = displayButton
self.displayImporters = displayImporters
self.buttonColor = buttonColor self.buttonColor = buttonColor
self.sectionId = sectionId self.sectionId = sectionId
self.style = style self.style = style
@ -287,7 +293,6 @@ public class ItemListPermanentInviteLinkItemNode: ListViewItemNode, ItemListItem
switch item.style { switch item.style {
case .plain: case .plain:
height -= 57.0
itemBackgroundColor = item.presentationData.theme.list.plainBackgroundColor itemBackgroundColor = item.presentationData.theme.list.plainBackgroundColor
itemSeparatorColor = .clear itemSeparatorColor = .clear
insets = UIEdgeInsets() insets = UIEdgeInsets()
@ -296,6 +301,14 @@ public class ItemListPermanentInviteLinkItemNode: ListViewItemNode, ItemListItem
itemSeparatorColor = item.presentationData.theme.list.itemBlocksSeparatorColor itemSeparatorColor = item.presentationData.theme.list.itemBlocksSeparatorColor
insets = itemListNeighborsGroupedInsets(neighbors) insets = itemListNeighborsGroupedInsets(neighbors)
} }
if !item.displayImporters {
height -= 57.0
}
if !item.displayButton {
height -= 63.0
}
contentSize = CGSize(width: params.width, height: height) contentSize = CGSize(width: params.width, height: height)
let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets) let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets)
@ -432,6 +445,11 @@ public class ItemListPermanentInviteLinkItemNode: ListViewItemNode, ItemListItem
strongSelf.avatarsButtonNode.frame = CGRect(x: floorToScreenPixels((params.width - totalWidth) / 2.0), y: fieldFrame.maxY + 87.0, width: totalWidth, height: 32.0) strongSelf.avatarsButtonNode.frame = CGRect(x: floorToScreenPixels((params.width - totalWidth) / 2.0), y: fieldFrame.maxY + 87.0, width: totalWidth, height: 32.0)
strongSelf.avatarsButtonNode.isUserInteractionEnabled = !item.peers.isEmpty strongSelf.avatarsButtonNode.isUserInteractionEnabled = !item.peers.isEmpty
strongSelf.shareButtonNode?.isHidden = !item.displayButton
strongSelf.avatarsButtonNode.isHidden = !item.displayImporters
strongSelf.avatarsNode.isHidden = !item.displayImporters
strongSelf.invitedPeersNode.isHidden = !item.displayImporters
} }
}) })
} }

View File

@ -368,17 +368,17 @@ private enum ChannelAdminEntry: ItemListNodeEntry {
func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem { func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem {
let arguments = arguments as! ChannelAdminControllerArguments let arguments = arguments as! ChannelAdminControllerArguments
switch self { switch self {
case let .info(theme, strings, dateTimeFormat, peer, presence): case let .info(_, strings, dateTimeFormat, peer, presence):
return ItemListAvatarAndNameInfoItem(accountContext: arguments.context, presentationData: presentationData, dateTimeFormat: dateTimeFormat, mode: .generic, peer: peer, presence: presence, cachedData: nil, state: ItemListAvatarAndNameInfoItemState(), sectionId: self.section, style: .blocks(withTopInset: true, withExtendedBottomInset: false), editingNameUpdated: { _ in return ItemListAvatarAndNameInfoItem(accountContext: arguments.context, presentationData: presentationData, dateTimeFormat: dateTimeFormat, mode: .generic, peer: peer, presence: presence, cachedData: nil, state: ItemListAvatarAndNameInfoItemState(), sectionId: self.section, style: .blocks(withTopInset: true, withExtendedBottomInset: false), editingNameUpdated: { _ in
}, avatarTapped: { }, avatarTapped: {
}) })
case let .rankTitle(theme, text, count, limit): case let .rankTitle(_, text, count, limit):
var accessoryText: ItemListSectionHeaderAccessoryText? var accessoryText: ItemListSectionHeaderAccessoryText?
if let count = count { if let count = count {
accessoryText = ItemListSectionHeaderAccessoryText(value: "\(limit - count)", color: count > limit ? .destructive : .generic) accessoryText = ItemListSectionHeaderAccessoryText(value: "\(limit - count)", color: count > limit ? .destructive : .generic)
} }
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, accessoryText: accessoryText, sectionId: self.section) return ItemListSectionHeaderItem(presentationData: presentationData, text: text, accessoryText: accessoryText, sectionId: self.section)
case let .rank(theme, strings, placeholder, text, enabled): case let .rank(_, _, placeholder, text, enabled):
return ItemListSingleLineInputItem(presentationData: presentationData, title: NSAttributedString(string: "", textColor: .black), text: text, placeholder: placeholder, type: .regular(capitalization: false, autocorrection: true), spacing: 0.0, clearType: enabled ? .always : .none, enabled: enabled, tag: ChannelAdminEntryTag.rank, sectionId: self.section, textUpdated: { updatedText in return ItemListSingleLineInputItem(presentationData: presentationData, title: NSAttributedString(string: "", textColor: .black), text: text, placeholder: placeholder, type: .regular(capitalization: false, autocorrection: true), spacing: 0.0, clearType: enabled ? .always : .none, enabled: enabled, tag: ChannelAdminEntryTag.rank, sectionId: self.section, textUpdated: { updatedText in
arguments.updateRank(text, updatedText) arguments.updateRank(text, updatedText)
}, shouldUpdateText: { text in }, shouldUpdateText: { text in
@ -392,23 +392,23 @@ private enum ChannelAdminEntry: ItemListNodeEntry {
}, action: { }, action: {
arguments.dismissInput() arguments.dismissInput()
}) })
case let .rankInfo(theme, text): case let .rankInfo(_, text):
return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section) return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section)
case let .rightsTitle(theme, text): case let .rightsTitle(_, text):
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
case let .rightItem(theme, _, text, right, flags, value, enabled): case let .rightItem(_, _, text, right, flags, value, enabled):
return ItemListSwitchItem(presentationData: presentationData, title: text, value: value, type: .icon, enabled: enabled, sectionId: self.section, style: .blocks, updated: { _ in return ItemListSwitchItem(presentationData: presentationData, title: text, value: value, type: .icon, enabled: enabled, sectionId: self.section, style: .blocks, updated: { _ in
arguments.toggleRight(right, flags) arguments.toggleRight(right, flags)
}, activatedWhileDisabled: { }, activatedWhileDisabled: {
arguments.toggleRightWhileDisabled(right, flags) arguments.toggleRightWhileDisabled(right, flags)
}) })
case let .addAdminsInfo(theme, text): case let .addAdminsInfo(_, text):
return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section) return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section)
case let .transfer(theme, text): case let .transfer(_, text):
return ItemListActionItem(presentationData: presentationData, title: text, kind: .generic, alignment: .center, sectionId: self.section, style: .blocks, action: { return ItemListActionItem(presentationData: presentationData, title: text, kind: .generic, alignment: .center, sectionId: self.section, style: .blocks, action: {
arguments.transferOwnership() arguments.transferOwnership()
}, tag: nil) }, tag: nil)
case let .dismiss(theme, text): case let .dismiss(_, text):
return ItemListActionItem(presentationData: presentationData, title: text, kind: .destructive, alignment: .center, sectionId: self.section, style: .blocks, action: { return ItemListActionItem(presentationData: presentationData, title: text, kind: .destructive, alignment: .center, sectionId: self.section, style: .blocks, action: {
arguments.dismissAdmin() arguments.dismissAdmin()
}, tag: nil) }, tag: nil)
@ -1004,12 +1004,12 @@ public func channelAdminController(context: AccountContext, peerId: PeerId, admi
var currentRank: String? var currentRank: String?
var currentFlags: TelegramChatAdminRightsFlags? var currentFlags: TelegramChatAdminRightsFlags?
switch initialParticipant { switch initialParticipant {
case let .creator(creator): case let .creator(_, adminInfo, rank):
currentRank = creator.rank currentRank = rank
currentFlags = maskRightsFlags currentFlags = adminInfo?.rights.flags ?? maskRightsFlags.subtracting(.canBeAnonymous)
case let .member(member): case let .member(_, _, adminInfo, _, rank):
if updateFlags == nil { if updateFlags == nil {
if member.adminInfo?.rights == nil { if adminInfo?.rights == nil {
if channel.flags.contains(.isCreator) { if channel.flags.contains(.isCreator) {
updateFlags = maskRightsFlags.subtracting([.canAddAdmins, .canBeAnonymous]) updateFlags = maskRightsFlags.subtracting([.canAddAdmins, .canBeAnonymous])
} else if let adminRights = channel.adminRights { } else if let adminRights = channel.adminRights {
@ -1019,8 +1019,8 @@ public func channelAdminController(context: AccountContext, peerId: PeerId, admi
} }
} }
} }
currentRank = member.rank currentRank = rank
currentFlags = member.adminInfo?.rights.flags currentFlags = adminInfo?.rights.flags
} }
let effectiveRank = updateRank ?? currentRank let effectiveRank = updateRank ?? currentRank

View File

@ -462,7 +462,7 @@ public func channelMembersController(context: AccountContext, peerId: PeerId) ->
pushControllerImpl?(controller) pushControllerImpl?(controller)
} }
}, inviteViaLink: { }, inviteViaLink: {
pushControllerImpl?(InviteLinkInviteController(context: context, peerId: peerId)) presentControllerImpl?(InviteLinkInviteController(context: context, peerId: peerId), nil)
}) })
let peerView = context.account.viewTracker.peerView(peerId) let peerView = context.account.viewTracker.peerView(peerId)

View File

@ -291,7 +291,7 @@ private enum ChannelVisibilityEntry: ItemListNodeEntry {
case let .privateLinkHeader(_, title): case let .privateLinkHeader(_, title):
return ItemListSectionHeaderItem(presentationData: presentationData, text: title, sectionId: self.section) return ItemListSectionHeaderItem(presentationData: presentationData, text: title, sectionId: self.section)
case let .privateLink(_, invite): case let .privateLink(_, invite):
return ItemListPermanentInviteLinkItem(context: arguments.context, presentationData: presentationData, invite: invite, peers: [], buttonColor: nil, sectionId: self.section, style: .blocks, shareAction: { return ItemListPermanentInviteLinkItem(context: arguments.context, presentationData: presentationData, invite: invite, peers: [], displayButton: true, displayImporters: true, buttonColor: nil, sectionId: self.section, style: .blocks, shareAction: {
arguments.shareLink() arguments.shareLink()
}, contextAction: { node in }, contextAction: { node in
arguments.linkContextAction(node) arguments.linkContextAction(node)

View File

@ -251,6 +251,205 @@ public func deleteAllRevokedPeerExportedInvitations(account: Account, peerId: Pe
|> switchToLatest |> switchToLatest
} }
private let cachedPeerExportedInvitationsCollectionSpec = ItemCacheCollectionSpec(lowWaterItemCount: 10, highWaterItemCount: 20)
public struct PeerExportedInvitationsState: Equatable {
public var invitations: [ExportedInvitation]
public var isLoadingMore: Bool
public var hasLoadedOnce: Bool
public var canLoadMore: Bool
public var count: Int32
}
final class CachedPeerExportedInvitations: PostboxCoding {
let invitations: [ExportedInvitation]
let canLoadMore: Bool
let count: Int32
public static func key(peerId: PeerId) -> ValueBoxKey {
let key = ValueBoxKey(length: 8 + 4)
key.setInt64(0, value: peerId.toInt64())
return key
}
init(invitations: [ExportedInvitation], canLoadMore: Bool, count: Int32) {
self.invitations = invitations
self.canLoadMore = canLoadMore
self.count = count
}
public init(decoder: PostboxDecoder) {
self.invitations = decoder.decodeObjectArrayForKey("invitations")
self.canLoadMore = decoder.decodeBoolForKey("canLoadMore", orElse: false)
self.count = decoder.decodeInt32ForKey("count", orElse: 0)
}
public func encode(_ encoder: PostboxEncoder) {
encoder.encodeObjectArray(self.invitations, forKey: "invitations")
encoder.encodeBool(self.canLoadMore, forKey: "canLoadMore")
encoder.encodeInt32(self.count, forKey: "count")
}
}
private final class PeerExportedInvitationsContextImpl {
private let queue: Queue
private let account: Account
private let peerId: PeerId
private let disposable = MetaDisposable()
private var isLoadingMore: Bool = false
private var hasLoadedOnce: Bool = false
private var canLoadMore: Bool = true
private var results: [ExportedInvitation] = []
private var count: Int32
private var populateCache: Bool = true
let state = Promise<PeerExportedInvitationsState>()
init(queue: Queue, account: Account, peerId: PeerId) {
self.queue = queue
self.account = account
self.peerId = peerId
self.count = 0
self.isLoadingMore = true
self.disposable.set((account.postbox.transaction { transaction -> CachedPeerExportedInvitations? in
return transaction.retrieveItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedPeerInvitationImporters, key: CachedPeerExportedInvitations.key(peerId: peerId))) as? CachedPeerExportedInvitations
}
|> deliverOn(self.queue)).start(next: { [weak self] cachedResult in
guard let strongSelf = self else {
return
}
strongSelf.isLoadingMore = false
if let cachedResult = cachedResult {
strongSelf.results = cachedResult.invitations
strongSelf.count = cachedResult.count
strongSelf.hasLoadedOnce = true
strongSelf.canLoadMore = cachedResult.canLoadMore
}
strongSelf.loadMore()
}))
self.loadMore()
}
deinit {
self.disposable.dispose()
}
func loadMore() {
if self.isLoadingMore {
return
}
self.isLoadingMore = true
let account = self.account
let peerId = self.peerId
let lastResult = self.results.last
let populateCache = self.populateCache
self.disposable.set((self.account.postbox.transaction { transaction -> Api.InputPeer? in
return transaction.getPeer(peerId).flatMap(apiInputPeer)
}
|> mapToSignal { inputPeer -> Signal<([ExportedInvitation], Int32), NoError> in
if let inputPeer = inputPeer {
let offsetLink = lastResult?.link
let signal = account.network.request(Api.functions.messages.getExportedChatInvites(flags: 0, peer: inputPeer, adminId: nil, offsetLink: offsetLink, limit: lastResult == nil ? 50 : 100))
|> map(Optional.init)
|> `catch` { _ -> Signal<Api.messages.ExportedChatInvites?, NoError> in
return .single(nil)
}
|> mapToSignal { result -> Signal<([ExportedInvitation], Int32), NoError> in
return account.postbox.transaction { transaction -> ([ExportedInvitation], Int32) in
guard let result = result else {
return ([], 0)
}
switch result {
case let .exportedChatInvites(count, invites, users):
var peers: [Peer] = []
for apiUser in users {
peers.append(TelegramUser(user: apiUser))
}
updatePeers(transaction: transaction, peers: peers, update: { _, updated in
return updated
})
let invitations: [ExportedInvitation] = invites.compactMap { ExportedInvitation(apiExportedInvite: $0) }
if populateCache {
transaction.putItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedPeerInvitationImporters, key: CachedPeerExportedInvitations.key(peerId: peerId)), entry: CachedPeerExportedInvitations(invitations: invitations, canLoadMore: count >= 50, count: count), collectionSpec: cachedPeerExportedInvitationsCollectionSpec)
}
return (invitations, count)
}
}
}
return signal
} else {
return .single(([], 0))
}
}
|> deliverOn(self.queue)).start(next: { [weak self] invitations, updatedCount in
guard let strongSelf = self else {
return
}
if strongSelf.populateCache {
strongSelf.populateCache = false
strongSelf.results.removeAll()
}
var existingLinks = Set(strongSelf.results.map { $0.link })
for invitation in invitations {
if !existingLinks.contains(invitation.link) {
strongSelf.results.append(invitation)
existingLinks.insert(invitation.link)
}
}
strongSelf.isLoadingMore = false
strongSelf.hasLoadedOnce = true
strongSelf.canLoadMore = !invitations.isEmpty
if strongSelf.canLoadMore {
strongSelf.count = max(updatedCount, Int32(strongSelf.results.count))
} else {
strongSelf.count = Int32(strongSelf.results.count)
}
strongSelf.updateState()
}))
self.updateState()
}
private func updateState() {
self.state.set(.single(PeerExportedInvitationsState(invitations: self.results, isLoadingMore: self.isLoadingMore, hasLoadedOnce: self.hasLoadedOnce, canLoadMore: self.canLoadMore, count: self.count)))
}
}
public final class PeerExportedInvitationsContext {
private let queue: Queue = Queue()
private let impl: QueueLocalObject<PeerExportedInvitationsContextImpl>
public var state: Signal<PeerExportedInvitationsState, NoError> {
return Signal { subscriber in
let disposable = MetaDisposable()
self.impl.with { impl in
disposable.set(impl.state.get().start(next: { value in
subscriber.putNext(value)
}))
}
return disposable
}
}
public init(account: Account, peerId: PeerId, invite: ExportedInvitation) {
let queue = self.queue
self.impl = QueueLocalObject(queue: queue, generate: {
return PeerExportedInvitationsContextImpl(queue: queue, account: account, peerId: peerId)
})
}
public func loadMore() {
self.impl.with { impl in
impl.loadMore()
}
}
}
private let cachedPeerInvitationImportersCollectionSpec = ItemCacheCollectionSpec(lowWaterItemCount: 10, highWaterItemCount: 20) private let cachedPeerInvitationImportersCollectionSpec = ItemCacheCollectionSpec(lowWaterItemCount: 10, highWaterItemCount: 20)
public struct PeerInvitationImportersState: Equatable { public struct PeerInvitationImportersState: Equatable {
@ -262,7 +461,7 @@ public struct PeerInvitationImportersState: Equatable {
public var isLoadingMore: Bool public var isLoadingMore: Bool
public var hasLoadedOnce: Bool public var hasLoadedOnce: Bool
public var canLoadMore: Bool public var canLoadMore: Bool
public var count: Int public var count: Int32
} }
final class CachedPeerInvitationImporters: PostboxCoding { final class CachedPeerInvitationImporters: PostboxCoding {
@ -331,7 +530,7 @@ private final class PeerInvitationImportersContextImpl {
private var hasLoadedOnce: Bool = false private var hasLoadedOnce: Bool = false
private var canLoadMore: Bool = true private var canLoadMore: Bool = true
private var results: [PeerInvitationImportersState.Importer] = [] private var results: [PeerInvitationImportersState.Importer] = []
private var count: Int private var count: Int32
private var populateCache: Bool = true private var populateCache: Bool = true
let state = Promise<PeerInvitationImportersState>() let state = Promise<PeerInvitationImportersState>()
@ -342,7 +541,7 @@ private final class PeerInvitationImportersContextImpl {
self.peerId = peerId self.peerId = peerId
self.link = invite.link self.link = invite.link
let count = invite.count.flatMap { Int($0) } ?? 0 let count = invite.count ?? 0
self.count = count self.count = count
self.isLoadingMore = true self.isLoadingMore = true
@ -395,7 +594,7 @@ private final class PeerInvitationImportersContextImpl {
self.disposable.set((self.account.postbox.transaction { transaction -> Api.InputPeer? in self.disposable.set((self.account.postbox.transaction { transaction -> Api.InputPeer? in
return transaction.getPeer(peerId).flatMap(apiInputPeer) return transaction.getPeer(peerId).flatMap(apiInputPeer)
} }
|> mapToSignal { inputPeer -> Signal<([PeerInvitationImportersState.Importer], Int), NoError> in |> mapToSignal { inputPeer -> Signal<([PeerInvitationImportersState.Importer], Int32), NoError> in
if let inputPeer = inputPeer { if let inputPeer = inputPeer {
let offsetUser = lastResult?.peer.peer.flatMap { apiInputUser($0) } ?? .inputUserEmpty let offsetUser = lastResult?.peer.peer.flatMap { apiInputUser($0) } ?? .inputUserEmpty
let offsetDate = lastResult?.date ?? 0 let offsetDate = lastResult?.date ?? 0
@ -404,8 +603,8 @@ private final class PeerInvitationImportersContextImpl {
|> `catch` { _ -> Signal<Api.messages.ChatInviteImporters?, NoError> in |> `catch` { _ -> Signal<Api.messages.ChatInviteImporters?, NoError> in
return .single(nil) return .single(nil)
} }
|> mapToSignal { result -> Signal<([PeerInvitationImportersState.Importer], Int), NoError> in |> mapToSignal { result -> Signal<([PeerInvitationImportersState.Importer], Int32), NoError> in
return account.postbox.transaction { transaction -> ([PeerInvitationImportersState.Importer], Int) in return account.postbox.transaction { transaction -> ([PeerInvitationImportersState.Importer], Int32) in
guard let result = result else { guard let result = result else {
return ([], 0) return ([], 0)
} }
@ -434,7 +633,7 @@ private final class PeerInvitationImportersContextImpl {
if populateCache { if populateCache {
transaction.putItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedPeerInvitationImporters, key: CachedPeerInvitationImporters.key(peerId: peerId, link: link)), entry: CachedPeerInvitationImporters(importers: resultImporters, count: count), collectionSpec: cachedPeerInvitationImportersCollectionSpec) transaction.putItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedPeerInvitationImporters, key: CachedPeerInvitationImporters.key(peerId: peerId, link: link)), entry: CachedPeerInvitationImporters(importers: resultImporters, count: count), collectionSpec: cachedPeerInvitationImportersCollectionSpec)
} }
return (resultImporters, Int(count)) return (resultImporters, count)
} }
} }
} }
@ -462,9 +661,9 @@ private final class PeerInvitationImportersContextImpl {
strongSelf.hasLoadedOnce = true strongSelf.hasLoadedOnce = true
strongSelf.canLoadMore = !importers.isEmpty strongSelf.canLoadMore = !importers.isEmpty
if strongSelf.canLoadMore { if strongSelf.canLoadMore {
strongSelf.count = max(updatedCount, strongSelf.results.count) strongSelf.count = max(updatedCount, Int32(strongSelf.results.count))
} else { } else {
strongSelf.count = strongSelf.results.count strongSelf.count = Int32(strongSelf.results.count)
} }
strongSelf.updateState() strongSelf.updateState()
})) }))

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "ic_biglink.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -1255,11 +1255,9 @@ private func editingItems(data: PeerInfoScreenData?, context: AccountContext, pr
interaction.editingOpenPublicLinkSetup() interaction.editingOpenPublicLinkSetup()
})) }))
if !isPublic { items[.peerPublicSettings]!.append(PeerInfoScreenDisclosureItem(id: ItemInviteLinks, label: .text(""), text: presentationData.strings.GroupInfo_InviteLinks, action: {
items[.peerPublicSettings]!.append(PeerInfoScreenDisclosureItem(id: ItemInviteLinks, label: .text(""), text: presentationData.strings.GroupInfo_InviteLinks, action: { interaction.editingOpenInviteLinksSetup()
interaction.editingOpenInviteLinksSetup() }))
}))
}
if let linkedDiscussionPeer = data.linkedDiscussionPeer { if let linkedDiscussionPeer = data.linkedDiscussionPeer {
let peerTitle: String let peerTitle: String
@ -1318,9 +1316,10 @@ private func editingItems(data: PeerInfoScreenData?, context: AccountContext, pr
} }
} else if let group = data.peer as? TelegramGroup { } else if let group = data.peer as? TelegramGroup {
let ItemUsername = 1 let ItemUsername = 1
let ItemPreHistory = 2 let ItemInviteLinks = 2
let ItemPermissions = 3 let ItemPreHistory = 3
let ItemAdmins = 4 let ItemPermissions = 4
let ItemAdmins = 5
if case .creator = group.role { if case .creator = group.role {
if let cachedData = data.cachedData as? CachedGroupData { if let cachedData = data.cachedData as? CachedGroupData {
@ -1330,6 +1329,11 @@ private func editingItems(data: PeerInfoScreenData?, context: AccountContext, pr
})) }))
} }
} }
items[.peerPublicSettings]!.append(PeerInfoScreenDisclosureItem(id: ItemInviteLinks, label: .text(""), text: presentationData.strings.GroupInfo_InviteLinks, action: {
interaction.editingOpenInviteLinksSetup()
}))
items[.peerPublicSettings]!.append(PeerInfoScreenDisclosureItem(id: ItemPreHistory, label: .text(presentationData.strings.GroupInfo_GroupHistoryHidden), text: presentationData.strings.GroupInfo_GroupHistory, action: { items[.peerPublicSettings]!.append(PeerInfoScreenDisclosureItem(id: ItemPreHistory, label: .text(presentationData.strings.GroupInfo_GroupHistoryHidden), text: presentationData.strings.GroupInfo_GroupHistory, action: {
interaction.editingOpenPreHistorySetup() interaction.editingOpenPreHistorySetup()
})) }))
@ -4547,7 +4551,7 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD
contactsController?.push(visibilityController) contactsController?.push(visibilityController)
} else { } else {
contactsController?.push(InviteLinkInviteController(context: context, peerId: groupPeer.id)) contactsController?.present(InviteLinkInviteController(context: context, peerId: groupPeer.id), in: .window(.root))
} }
} }

View File

@ -70,9 +70,10 @@ final class ReplyAccessoryPanelNode: AccessoryPanelNode {
self.addSubnode(self.imageNode) self.addSubnode(self.imageNode)
self.addSubnode(self.actionArea) self.addSubnode(self.actionArea)
self.messageDisposable.set((context.account.postbox.messageAtId(messageId) self.messageDisposable.set((context.account.postbox.messageView(messageId)
|> deliverOnMainQueue).start(next: { [weak self] message in |> deliverOnMainQueue).start(next: { [weak self] messageView in
if let strongSelf = self { if let strongSelf = self {
let message = messageView.message
var authorName = "" var authorName = ""
var text = "" var text = ""
if let author = message?.effectiveAuthor { if let author = message?.effectiveAuthor {