mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
Invite Links Improvements
This commit is contained in:
parent
d5a25602ce
commit
ebe05a1e8a
@ -5842,6 +5842,7 @@ Sorry for the inconvenience.";
|
||||
"InviteLink.Expired" = "expired";
|
||||
"InviteLink.UsageLimitReached" = "limit reached";
|
||||
"InviteLink.Revoked" = "revoked";
|
||||
"InviteLink.TapToCopy" = "tap to copy";
|
||||
|
||||
"InviteLink.AdditionalLinks" = "Additional Links";
|
||||
"InviteLink.Create" = "Create a New Link";
|
||||
@ -5882,9 +5883,11 @@ Sorry for the inconvenience.";
|
||||
"InviteLink.InviteLink" = "Invite Link";
|
||||
"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.ExpiresIn" = "expires in %@";
|
||||
|
||||
"Conversation.ChecksTooltip.Delivered" = "Delivered";
|
||||
"Conversation.ChecksTooltip.Read" = "Read";
|
||||
|
||||
|
@ -45,25 +45,39 @@ private struct InviteLinkInviteTransaction {
|
||||
}
|
||||
|
||||
private enum InviteLinkInviteEntryId: Hashable {
|
||||
case header
|
||||
case mainLink
|
||||
case links(Int32)
|
||||
case manage
|
||||
}
|
||||
|
||||
private enum InviteLinkInviteEntry: Comparable, Identifiable {
|
||||
case header(PresentationTheme, String, String)
|
||||
case mainLink(PresentationTheme, ExportedInvitation)
|
||||
case links(Int32, PresentationTheme, [ExportedInvitation])
|
||||
case manage(PresentationTheme, String)
|
||||
|
||||
var stableId: InviteLinkInviteEntryId {
|
||||
switch self {
|
||||
case .header:
|
||||
return .header
|
||||
case .mainLink:
|
||||
return .mainLink
|
||||
case let .links(index, _, _):
|
||||
return .links(index)
|
||||
case .manage:
|
||||
return .manage
|
||||
}
|
||||
}
|
||||
|
||||
static func ==(lhs: InviteLinkInviteEntry, rhs: InviteLinkInviteEntry) -> Bool {
|
||||
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):
|
||||
if case let .mainLink(rhsTheme, rhsInvitation) = rhs, lhsTheme === rhsTheme, lhsInvitation == rhsInvitation {
|
||||
return true
|
||||
@ -76,43 +90,71 @@ private enum InviteLinkInviteEntry: Comparable, Identifiable {
|
||||
} else {
|
||||
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 {
|
||||
switch lhs {
|
||||
case .header:
|
||||
switch rhs {
|
||||
case .header:
|
||||
return false
|
||||
case .mainLink, .links, .manage:
|
||||
return true
|
||||
}
|
||||
case .mainLink:
|
||||
switch rhs {
|
||||
case .mainLink:
|
||||
case .header, .mainLink:
|
||||
return false
|
||||
case .links:
|
||||
case .links, .manage:
|
||||
return true
|
||||
}
|
||||
case let .links(lhsIndex, _, _):
|
||||
switch rhs {
|
||||
case .mainLink:
|
||||
case .header, .mainLink:
|
||||
return false
|
||||
case let .links(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 {
|
||||
switch self {
|
||||
case let .header(theme, title, text):
|
||||
return InviteLinkInviteHeaderItem(theme: theme, title: title, text: text)
|
||||
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)
|
||||
}, contextAction: { node in
|
||||
interaction.mainLinkContextAction(invite, node, nil)
|
||||
}, viewAction: {
|
||||
})
|
||||
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)
|
||||
}, contextAction: { invite, _ in
|
||||
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?
|
||||
|
||||
public init(context: AccountContext, peerId: PeerId) {
|
||||
fatalError()
|
||||
self.context = context
|
||||
self.peerId = peerId
|
||||
|
||||
@ -196,7 +237,7 @@ public final class InviteLinkInviteController: ViewController {
|
||||
|
||||
self.controllerNode.animateOut(completion: { [weak self] in
|
||||
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 {
|
||||
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 {
|
||||
entries.append(.mainLink(presentationData.theme, invite))
|
||||
} else if let cachedData = view.cachedData as? CachedChannelData, let invite = cachedData.exportedInvitation {
|
||||
entries.append(.mainLink(presentationData.theme, invite))
|
||||
}
|
||||
|
||||
entries.append(.manage(presentationData.theme, presentationData.strings.InviteLink_Manage))
|
||||
|
||||
let previousEntries = previousEntries.swap(entries)
|
||||
|
||||
@ -507,7 +551,7 @@ public final class InviteLinkInviteController: ViewController {
|
||||
insets.bottom = layout.intrinsicInsets.bottom
|
||||
|
||||
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)
|
||||
|
||||
|
@ -13,10 +13,12 @@ class InviteLinkInviteHeaderItem: ListViewItem, ItemListItem {
|
||||
var sectionId: ItemListSectionId = 0
|
||||
|
||||
let theme: PresentationTheme
|
||||
let title: String
|
||||
let text: String
|
||||
|
||||
init(theme: PresentationTheme, text: String) {
|
||||
init(theme: PresentationTheme, title: String, text: String) {
|
||||
self.theme = theme
|
||||
self.title = title
|
||||
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 {
|
||||
private let titleNode: TextNode
|
||||
private var animationNode: AnimatedStickerNode
|
||||
private let textNode: TextNode
|
||||
private let iconBackgroundNode: ASImageNode
|
||||
private let iconNode: ASImageNode
|
||||
|
||||
private var item: InviteLinkInviteHeaderItem?
|
||||
|
||||
init() {
|
||||
self.titleNode = TextNode()
|
||||
self.titleNode.isUserInteractionEnabled = false
|
||||
self.titleNode.contentMode = .left
|
||||
self.titleNode.contentsScale = UIScreen.main.scale
|
||||
|
||||
self.animationNode = AnimatedStickerNode()
|
||||
if let path = getAppBundle().path(forResource: "Invite", ofType: "tgs") {
|
||||
self.animationNode.setup(source: AnimatedStickerNodeLocalFileSource(path: path), width: 192, height: 192, playbackMode: .loop, mode: .direct(cachePathPrefix: nil))
|
||||
self.animationNode.visibility = true
|
||||
}
|
||||
self.textNode = TextNode()
|
||||
self.textNode.isUserInteractionEnabled = false
|
||||
|
||||
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)
|
||||
|
||||
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) {
|
||||
let makeTitleLayout = TextNode.asyncLayout(self.titleNode)
|
||||
let makeTextLayout = TextNode.asyncLayout(self.textNode)
|
||||
let currentItem = self.item
|
||||
|
||||
return { item, params, neighbors in
|
||||
let leftInset: CGFloat = 32.0 + params.leftInset
|
||||
let topInset: CGFloat = 92.0
|
||||
let leftInset: CGFloat = 40.0 + params.leftInset
|
||||
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)
|
||||
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()))
|
||||
var updatedTheme: PresentationTheme?
|
||||
if currentItem?.theme !== item.theme {
|
||||
updatedTheme = item.theme
|
||||
}
|
||||
|
||||
let contentSize = CGSize(width: params.width, height: topInset + titleLayout.size.height)
|
||||
let insets = itemListNeighborsGroupedInsets(neighbors)
|
||||
let titleAttributedText = NSAttributedString(string: item.title, font: titleFont, textColor: item.theme.list.itemPrimaryTextColor)
|
||||
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
|
||||
if let strongSelf = self {
|
||||
strongSelf.item = item
|
||||
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)
|
||||
strongSelf.animationNode.frame = CGRect(origin: CGPoint(x: floor((layout.size.width - iconSize.width) / 2.0), y: -10.0), size: iconSize)
|
||||
strongSelf.animationNode.updateLayout(size: iconSize)
|
||||
let iconSize = CGSize(width: 92.0, height: 92.0)
|
||||
strongSelf.iconBackgroundNode.frame = CGRect(origin: CGPoint(x: floor((layout.size.width - iconSize.width) / 2.0), y: -10.0), size: iconSize)
|
||||
strongSelf.iconNode.frame = strongSelf.iconBackgroundNode.frame
|
||||
|
||||
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)
|
||||
|
||||
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)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
@ -178,7 +178,7 @@ private enum InviteLinksListEntry: ItemListNodeEntry {
|
||||
case let .mainLinkHeader(_, text):
|
||||
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
|
||||
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)
|
||||
}, contextAction: { node in
|
||||
arguments.mainLinkContextAction(invite, node, nil)
|
||||
@ -194,7 +194,7 @@ private enum InviteLinksListEntry: ItemListNodeEntry {
|
||||
arguments.createLink()
|
||||
})
|
||||
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)
|
||||
}, contextAction: { invite, node in
|
||||
arguments.linkContextAction(invite, node, nil)
|
||||
@ -208,7 +208,7 @@ private enum InviteLinksListEntry: ItemListNodeEntry {
|
||||
arguments.deleteAllRevokedLinks()
|
||||
})
|
||||
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)
|
||||
}, contextAction: { invite, node in
|
||||
arguments.linkContextAction(invite, node, nil)
|
||||
@ -468,8 +468,8 @@ public func inviteLinkListController(context: AccountContext, peerId: PeerId) ->
|
||||
ActionSheetTextItem(title: presentationData.strings.GroupInfo_InviteLink_RevokeAlert_Text),
|
||||
ActionSheetButtonItem(title: presentationData.strings.GroupInfo_InviteLink_RevokeLink, color: .destructive, action: {
|
||||
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: {
|
||||
|
||||
}))
|
||||
})
|
||||
|
@ -72,7 +72,7 @@ private enum InviteLinkViewEntry: Comparable, Identifiable {
|
||||
case creatorHeader(PresentationTheme, String)
|
||||
case creator(PresentationTheme, PresentationDateTimeFormat, Peer, Int32)
|
||||
case importerHeader(PresentationTheme, String)
|
||||
case importer(Int32, PresentationTheme, PresentationDateTimeFormat, Peer, Int32)
|
||||
case importer(Int32, PresentationTheme, PresentationDateTimeFormat, Peer, Int32, Bool)
|
||||
|
||||
var stableId: InviteLinkViewEntryId {
|
||||
switch self {
|
||||
@ -84,7 +84,7 @@ private enum InviteLinkViewEntry: Comparable, Identifiable {
|
||||
return .creator
|
||||
case .importerHeader:
|
||||
return .importerHeader
|
||||
case let .importer(_, _, _, peer, _):
|
||||
case let .importer(_, _, _, peer, _, _):
|
||||
return .importer(peer.id)
|
||||
}
|
||||
}
|
||||
@ -115,8 +115,8 @@ private enum InviteLinkViewEntry: Comparable, Identifiable {
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .importer(lhsIndex, lhsTheme, lhsDateTimeFormat, lhsPeer, lhsDate):
|
||||
if case let .importer(rhsIndex, rhsTheme, rhsDateTimeFormat, rhsPeer, rhsDate) = rhs, lhsIndex == rhsIndex, lhsTheme === rhsTheme, lhsDateTimeFormat == rhsDateTimeFormat, arePeersEqual(lhsPeer, rhsPeer), lhsDate == rhsDate {
|
||||
case let .importer(lhsIndex, lhsTheme, lhsDateTimeFormat, lhsPeer, lhsDate, lhsLoading):
|
||||
if case let .importer(rhsIndex, rhsTheme, rhsDateTimeFormat, rhsPeer, rhsDate, rhsLoading) = rhs, lhsIndex == rhsIndex, lhsTheme === rhsTheme, lhsDateTimeFormat == rhsDateTimeFormat, arePeersEqual(lhsPeer, rhsPeer), lhsDate == rhsDate {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
@ -154,11 +154,11 @@ private enum InviteLinkViewEntry: Comparable, Identifiable {
|
||||
case .creator, .importer:
|
||||
return true
|
||||
}
|
||||
case let .importer(lhsIndex, _, _, _, _):
|
||||
case let .importer(lhsIndex, _, _, _, _, _):
|
||||
switch rhs {
|
||||
case .link, .creatorHeader, .creator, .importerHeader:
|
||||
return false
|
||||
case let .importer(rhsIndex, _, _, _, _):
|
||||
case let .importer(rhsIndex, _, _, _, _, _):
|
||||
return lhsIndex < rhsIndex
|
||||
}
|
||||
}
|
||||
@ -168,7 +168,8 @@ private enum InviteLinkViewEntry: Comparable, Identifiable {
|
||||
switch self {
|
||||
case let .link(_, 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)
|
||||
}, contextAction: { node in
|
||||
interaction.contextAction(invite, node, nil)
|
||||
@ -183,11 +184,11 @@ private enum InviteLinkViewEntry: Comparable, Identifiable {
|
||||
}, setPeerIdWithRevealedOptions: { _, _ in }, removePeer: { _ in }, hasTopStripe: false, noInsets: true, tag: nil)
|
||||
case let .importerHeader(_, 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)
|
||||
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)
|
||||
}, 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))
|
||||
})))
|
||||
|
||||
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
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Wallet/QrIcon"), color: theme.contextMenu.primaryColor)
|
||||
}, 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(.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()))
|
||||
}
|
||||
|
||||
var index: Int32 = 0
|
||||
for importer in state.importers {
|
||||
if let peer = importer.peer.peer {
|
||||
entries.append(.importer(index, presentationData.theme, presentationData.dateTimeFormat, peer, importer.date))
|
||||
if state.importers.isEmpty && state.isLoadingMore {
|
||||
let fakeUser = TelegramUser(id: PeerId(namespace: -1, id: 0), accessHash: nil, firstName: "", lastName: "", username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [])
|
||||
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)
|
||||
|
@ -42,10 +42,35 @@ func invitationAvailability(_ invite: ExportedInvitation) -> CGFloat {
|
||||
let fraction = 1.0 - (CGFloat(count) / CGFloat(usageLimit))
|
||||
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 let selectionNode: HighlightTrackingButtonNode
|
||||
private let wrapperNode: ASDisplayNode
|
||||
private let backgroundNode: ASImageNode
|
||||
|
||||
private let iconNode: ASImageNode
|
||||
@ -59,19 +84,28 @@ private class ItemNode: ASDisplayNode {
|
||||
private let titleNode: 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 contextAction: ((ASDisplayNode) -> Void)?
|
||||
|
||||
private let hapticFeedback = HapticFeedback()
|
||||
|
||||
override init() {
|
||||
self.selectionNode = HighlightTrackingButtonNode()
|
||||
self.wrapperNode = ASDisplayNode()
|
||||
|
||||
self.backgroundNode = ASImageNode()
|
||||
self.backgroundNode.displaysAsynchronously = false
|
||||
self.backgroundNode.displayWithoutProcessing = true
|
||||
self.backgroundNode.isUserInteractionEnabled = false
|
||||
|
||||
self.iconNode = ASImageNode()
|
||||
self.iconNode.displaysAsynchronously = false
|
||||
self.iconNode.displayWithoutProcessing = true
|
||||
self.iconNode.isUserInteractionEnabled = false
|
||||
|
||||
self.buttonNode = HighlightTrackingButtonNode()
|
||||
self.extractedContainerNode = ContextExtractedContentContainingNode()
|
||||
@ -94,23 +128,41 @@ private class ItemNode: ASDisplayNode {
|
||||
|
||||
self.titleNode = ImmediateTextNode()
|
||||
self.titleNode.maximumNumberOfLines = 2
|
||||
self.titleNode.isUserInteractionEnabled = false
|
||||
|
||||
self.subtitleNode = ImmediateTextNode()
|
||||
self.subtitleNode.maximumNumberOfLines = 1
|
||||
self.subtitleNode.isUserInteractionEnabled = false
|
||||
|
||||
super.init()
|
||||
|
||||
self.addSubnode(self.backgroundNode)
|
||||
self.addSubnode(self.iconNode)
|
||||
|
||||
self.addSubnode(self.wrapperNode)
|
||||
self.wrapperNode.addSubnode(self.backgroundNode)
|
||||
self.wrapperNode.addSubnode(self.iconNode)
|
||||
|
||||
self.containerNode.addSubnode(self.extractedContainerNode)
|
||||
self.extractedContainerNode.contentNode.addSubnode(self.buttonIconNode)
|
||||
self.containerNode.targetNodeForActivationProgress = self.extractedContainerNode.contentNode
|
||||
self.buttonNode.addSubnode(self.containerNode)
|
||||
self.addSubnode(self.buttonNode)
|
||||
|
||||
self.addSubnode(self.titleNode)
|
||||
self.addSubnode(self.subtitleNode)
|
||||
self.wrapperNode.addSubnode(self.selectionNode)
|
||||
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.highligthedChanged = { [weak self] highlighted in
|
||||
@ -126,11 +178,12 @@ private class ItemNode: ASDisplayNode {
|
||||
}
|
||||
}
|
||||
|
||||
override func didLoad() {
|
||||
self.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:))))
|
||||
deinit {
|
||||
self.updateTimer?.invalidate()
|
||||
}
|
||||
|
||||
@objc private func tapGesture(_ gestureRecognizer: UITapGestureRecognizer) {
|
||||
@objc private func tapped() {
|
||||
self.hapticFeedback.impact(.light)
|
||||
self.action?()
|
||||
}
|
||||
|
||||
@ -138,35 +191,66 @@ private class ItemNode: ASDisplayNode {
|
||||
self.contextAction?(self.extractedContainerNode)
|
||||
}
|
||||
|
||||
func update(size: CGSize, wide: 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)
|
||||
|
||||
func update(size: CGSize, wide: Bool, share: Bool, invite: ExportedInvitation, presentationData: ItemListPresentationData, transition: ContainedViewLayoutTransition) -> CGSize {
|
||||
let currentTime = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970)
|
||||
|
||||
let availability = invitationAvailability(invite)
|
||||
|
||||
var isExpired = false
|
||||
let secondaryTextColor: UIColor
|
||||
let color: ItemBackgroundColor
|
||||
if invite.isRevoked {
|
||||
self.backgroundNode.image = generateBackgroundImage(colors: [UIColor(rgb: 0xd4d8db).cgColor, UIColor(rgb: 0xced2d5).cgColor])
|
||||
secondaryTextColor = UIColor(rgb: 0xf8f9f9)
|
||||
color = .gray
|
||||
} else if invite.expireDate == nil && invite.usageLimit == nil {
|
||||
self.backgroundNode.image = generateBackgroundImage(colors: [UIColor(rgb: 0x00b5f7).cgColor, UIColor(rgb: 0x00b2f6).cgColor])
|
||||
secondaryTextColor = UIColor(rgb: 0xa7f4ff)
|
||||
color = .blue
|
||||
} else if availability >= 0.5 {
|
||||
self.backgroundNode.image = generateBackgroundImage(colors: [UIColor(rgb: 0x4aca62).cgColor, UIColor(rgb: 0x43c85c).cgColor])
|
||||
secondaryTextColor = UIColor(rgb: 0xc5ffe6)
|
||||
color = .green
|
||||
} else if availability > 0.0 {
|
||||
self.backgroundNode.image = generateBackgroundImage(colors: [UIColor(rgb: 0xf8a953).cgColor, UIColor(rgb: 0xf7a64e).cgColor])
|
||||
secondaryTextColor = UIColor(rgb: 0xfeffd7)
|
||||
color = .yellow
|
||||
} else {
|
||||
self.backgroundNode.image = generateBackgroundImage(colors: [UIColor(rgb: 0xf2656a).cgColor, UIColor(rgb: 0xf25f65).cgColor])
|
||||
secondaryTextColor = UIColor(rgb: 0xffd3de)
|
||||
isExpired = true
|
||||
color = .red
|
||||
}
|
||||
|
||||
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: "")
|
||||
if !wide {
|
||||
inviteLink = inviteLink.replacingOccurrences(of: "joinchat/", with: "joinchat/\n")
|
||||
@ -184,7 +268,7 @@ private class ItemNode: ASDisplayNode {
|
||||
if let count = invite.count {
|
||||
subtitleText = presentationData.strings.InviteLink_PeopleJoinedShort(count)
|
||||
} 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 !subtitleText.isEmpty {
|
||||
@ -194,7 +278,7 @@ private class ItemNode: ASDisplayNode {
|
||||
self.iconNode.image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Links/Expired"), color: .white)
|
||||
self.timerNode?.removeFromSupernode()
|
||||
self.timerNode = nil
|
||||
} else if let expireDate = invite.expireDate, currentTime > expireDate {
|
||||
} else if let expireDate = invite.expireDate, currentTime >= expireDate {
|
||||
if !subtitleText.isEmpty {
|
||||
subtitleText += " • "
|
||||
}
|
||||
@ -202,6 +286,14 @@ private class ItemNode: ASDisplayNode {
|
||||
self.iconNode.image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Links/Expired"), color: .white)
|
||||
self.timerNode?.removeFromSupernode()
|
||||
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 {
|
||||
self.iconNode.image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Links/Flame"), color: .white)
|
||||
let timerNode: TimerNode
|
||||
@ -209,6 +301,7 @@ private class ItemNode: ASDisplayNode {
|
||||
timerNode = current
|
||||
} else {
|
||||
timerNode = TimerNode()
|
||||
timerNode.isUserInteractionEnabled = false
|
||||
self.timerNode = 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.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 = subtitle
|
||||
self.subtitleNode.attributedText = NSAttributedString(string: subtitleText, font: subtitleFont, textColor: secondaryTextColor)
|
||||
|
||||
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))
|
||||
@ -234,7 +326,9 @@ private class ItemNode: ASDisplayNode {
|
||||
let itemSize = CGSize(width: itemWidth, height: wide ? 102.0 : 122.0)
|
||||
|
||||
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.selectionNode, frame: backgroundFrame)
|
||||
|
||||
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)
|
||||
@ -260,7 +354,7 @@ class InviteLinksGridNode: ASDisplayNode {
|
||||
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
|
||||
|
||||
var contentSize: CGSize = size
|
||||
@ -288,7 +382,7 @@ class InviteLinksGridNode: ASDisplayNode {
|
||||
let col = CGFloat(i % 2)
|
||||
let row = floor(CGFloat(i) / 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)
|
||||
if !wide && col > 0 {
|
||||
itemFrame.origin.x += itemSpacing + itemSize.width
|
||||
@ -408,7 +502,7 @@ private final class TimerNode: ASDisplayNode {
|
||||
|
||||
let currentTimestamp = Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970)
|
||||
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?
|
||||
|
||||
@ -424,44 +518,47 @@ private final class TimerNode: ASDisplayNode {
|
||||
let startAngle: CGFloat = -CGFloat.pi / 2.0
|
||||
let endAngle: CGFloat = -CGFloat.pi / 2.0 + 2.0 * CGFloat.pi * fraction
|
||||
|
||||
let v = CGPoint(x: sin(endAngle), y: -cos(endAngle))
|
||||
let c = CGPoint(x: -v.y * radius + center.x, y: v.x * radius + center.y)
|
||||
|
||||
let dt: CGFloat = 1.0 / 60.0
|
||||
var removeIndices: [Int] = []
|
||||
for i in 0 ..< self.particles.count {
|
||||
let currentTime = timestamp - self.particles[i].beginTime
|
||||
if currentTime > self.particles[i].lifetime {
|
||||
removeIndices.append(i)
|
||||
} else {
|
||||
let input: CGFloat = CGFloat(currentTime / self.particles[i].lifetime)
|
||||
let decelerated: CGFloat = (1.0 - (1.0 - input) * (1.0 - input))
|
||||
self.particles[i].alpha = 1.0 - decelerated
|
||||
|
||||
var p = self.particles[i].position
|
||||
let d = self.particles[i].direction
|
||||
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
|
||||
let sparks = fraction > 0.1 && fraction != 1.0
|
||||
if sparks {
|
||||
let v = CGPoint(x: sin(endAngle), y: -cos(endAngle))
|
||||
let c = CGPoint(x: -v.y * radius + center.x, y: v.x * radius + center.y)
|
||||
|
||||
let dt: CGFloat = 1.0 / 60.0
|
||||
var removeIndices: [Int] = []
|
||||
for i in 0 ..< self.particles.count {
|
||||
let currentTime = timestamp - self.particles[i].beginTime
|
||||
if currentTime > self.particles[i].lifetime {
|
||||
removeIndices.append(i)
|
||||
} else {
|
||||
let input: CGFloat = CGFloat(currentTime / self.particles[i].lifetime)
|
||||
let decelerated: CGFloat = (1.0 - (1.0 - input) * (1.0 - input))
|
||||
self.particles[i].alpha = 1.0 - decelerated
|
||||
|
||||
var p = self.particles[i].position
|
||||
let d = self.particles[i].direction
|
||||
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))
|
||||
let velocity = (20.0 + (CGFloat(arc4random()) / CGFloat(UINT32_MAX)) * 4.0) * 0.3
|
||||
for i in removeIndices.reversed() {
|
||||
self.particles.remove(at: i)
|
||||
}
|
||||
|
||||
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)
|
||||
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))
|
||||
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
|
||||
@ -476,10 +573,12 @@ private final class TimerNode: ASDisplayNode {
|
||||
context.addPath(path)
|
||||
context.strokePath()
|
||||
|
||||
for particle in self.particles {
|
||||
let size: CGFloat = 2.0
|
||||
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)))
|
||||
if sparks {
|
||||
for particle in self.particles {
|
||||
let size: CGFloat = 2.0
|
||||
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)))
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
|
@ -10,6 +10,7 @@ import ItemListUI
|
||||
public class ItemListInviteLinkGridItem: ListViewItem, ItemListItem {
|
||||
let presentationData: ItemListPresentationData
|
||||
let invites: [ExportedInvitation]?
|
||||
let share: Bool
|
||||
public let sectionId: ItemListSectionId
|
||||
let style: ItemListStyle
|
||||
let tapAction: ((ExportedInvitation) -> Void)?
|
||||
@ -19,6 +20,7 @@ public class ItemListInviteLinkGridItem: ListViewItem, ItemListItem {
|
||||
public init(
|
||||
presentationData: ItemListPresentationData,
|
||||
invites: [ExportedInvitation]?,
|
||||
share: Bool,
|
||||
sectionId: ItemListSectionId,
|
||||
style: ItemListStyle,
|
||||
tapAction: ((ExportedInvitation) -> Void)?,
|
||||
@ -27,6 +29,7 @@ public class ItemListInviteLinkGridItem: ListViewItem, ItemListItem {
|
||||
) {
|
||||
self.presentationData = presentationData
|
||||
self.invites = invites
|
||||
self.share = share
|
||||
self.sectionId = sectionId
|
||||
self.style = style
|
||||
self.tapAction = tapAction
|
||||
@ -167,7 +170,7 @@ public class ItemListInviteLinkGridItemNode: ListViewItemNode, ItemListItemNode
|
||||
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.action = { invite in
|
||||
item.tapAction?(invite)
|
||||
|
@ -30,6 +30,8 @@ public class ItemListPermanentInviteLinkItem: ListViewItem, ItemListItem {
|
||||
let presentationData: ItemListPresentationData
|
||||
let invite: ExportedInvitation?
|
||||
let peers: [Peer]
|
||||
let displayButton: Bool
|
||||
let displayImporters: Bool
|
||||
let buttonColor: UIColor?
|
||||
public let sectionId: ItemListSectionId
|
||||
let style: ItemListStyle
|
||||
@ -43,6 +45,8 @@ public class ItemListPermanentInviteLinkItem: ListViewItem, ItemListItem {
|
||||
presentationData: ItemListPresentationData,
|
||||
invite: ExportedInvitation?,
|
||||
peers: [Peer],
|
||||
displayButton: Bool,
|
||||
displayImporters: Bool,
|
||||
buttonColor: UIColor?,
|
||||
sectionId: ItemListSectionId,
|
||||
style: ItemListStyle,
|
||||
@ -55,6 +59,8 @@ public class ItemListPermanentInviteLinkItem: ListViewItem, ItemListItem {
|
||||
self.presentationData = presentationData
|
||||
self.invite = invite
|
||||
self.peers = peers
|
||||
self.displayButton = displayButton
|
||||
self.displayImporters = displayImporters
|
||||
self.buttonColor = buttonColor
|
||||
self.sectionId = sectionId
|
||||
self.style = style
|
||||
@ -287,7 +293,6 @@ public class ItemListPermanentInviteLinkItemNode: ListViewItemNode, ItemListItem
|
||||
|
||||
switch item.style {
|
||||
case .plain:
|
||||
height -= 57.0
|
||||
itemBackgroundColor = item.presentationData.theme.list.plainBackgroundColor
|
||||
itemSeparatorColor = .clear
|
||||
insets = UIEdgeInsets()
|
||||
@ -296,6 +301,14 @@ public class ItemListPermanentInviteLinkItemNode: ListViewItemNode, ItemListItem
|
||||
itemSeparatorColor = item.presentationData.theme.list.itemBlocksSeparatorColor
|
||||
insets = itemListNeighborsGroupedInsets(neighbors)
|
||||
}
|
||||
|
||||
if !item.displayImporters {
|
||||
height -= 57.0
|
||||
}
|
||||
if !item.displayButton {
|
||||
height -= 63.0
|
||||
}
|
||||
|
||||
contentSize = CGSize(width: params.width, height: height)
|
||||
|
||||
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.isUserInteractionEnabled = !item.peers.isEmpty
|
||||
|
||||
strongSelf.shareButtonNode?.isHidden = !item.displayButton
|
||||
strongSelf.avatarsButtonNode.isHidden = !item.displayImporters
|
||||
strongSelf.avatarsNode.isHidden = !item.displayImporters
|
||||
strongSelf.invitedPeersNode.isHidden = !item.displayImporters
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -368,17 +368,17 @@ private enum ChannelAdminEntry: ItemListNodeEntry {
|
||||
func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem {
|
||||
let arguments = arguments as! ChannelAdminControllerArguments
|
||||
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
|
||||
}, avatarTapped: {
|
||||
})
|
||||
case let .rankTitle(theme, text, count, limit):
|
||||
case let .rankTitle(_, text, count, limit):
|
||||
var accessoryText: ItemListSectionHeaderAccessoryText?
|
||||
if let count = count {
|
||||
accessoryText = ItemListSectionHeaderAccessoryText(value: "\(limit - count)", color: count > limit ? .destructive : .generic)
|
||||
}
|
||||
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
|
||||
arguments.updateRank(text, updatedText)
|
||||
}, shouldUpdateText: { text in
|
||||
@ -392,23 +392,23 @@ private enum ChannelAdminEntry: ItemListNodeEntry {
|
||||
}, action: {
|
||||
arguments.dismissInput()
|
||||
})
|
||||
case let .rankInfo(theme, text):
|
||||
case let .rankInfo(_, text):
|
||||
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)
|
||||
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
|
||||
arguments.toggleRight(right, flags)
|
||||
}, activatedWhileDisabled: {
|
||||
arguments.toggleRightWhileDisabled(right, flags)
|
||||
})
|
||||
case let .addAdminsInfo(theme, text):
|
||||
case let .addAdminsInfo(_, text):
|
||||
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: {
|
||||
arguments.transferOwnership()
|
||||
}, 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: {
|
||||
arguments.dismissAdmin()
|
||||
}, tag: nil)
|
||||
@ -1004,12 +1004,12 @@ public func channelAdminController(context: AccountContext, peerId: PeerId, admi
|
||||
var currentRank: String?
|
||||
var currentFlags: TelegramChatAdminRightsFlags?
|
||||
switch initialParticipant {
|
||||
case let .creator(creator):
|
||||
currentRank = creator.rank
|
||||
currentFlags = maskRightsFlags
|
||||
case let .member(member):
|
||||
case let .creator(_, adminInfo, rank):
|
||||
currentRank = rank
|
||||
currentFlags = adminInfo?.rights.flags ?? maskRightsFlags.subtracting(.canBeAnonymous)
|
||||
case let .member(_, _, adminInfo, _, rank):
|
||||
if updateFlags == nil {
|
||||
if member.adminInfo?.rights == nil {
|
||||
if adminInfo?.rights == nil {
|
||||
if channel.flags.contains(.isCreator) {
|
||||
updateFlags = maskRightsFlags.subtracting([.canAddAdmins, .canBeAnonymous])
|
||||
} else if let adminRights = channel.adminRights {
|
||||
@ -1019,8 +1019,8 @@ public func channelAdminController(context: AccountContext, peerId: PeerId, admi
|
||||
}
|
||||
}
|
||||
}
|
||||
currentRank = member.rank
|
||||
currentFlags = member.adminInfo?.rights.flags
|
||||
currentRank = rank
|
||||
currentFlags = adminInfo?.rights.flags
|
||||
}
|
||||
|
||||
let effectiveRank = updateRank ?? currentRank
|
||||
|
@ -462,7 +462,7 @@ public func channelMembersController(context: AccountContext, peerId: PeerId) ->
|
||||
pushControllerImpl?(controller)
|
||||
}
|
||||
}, inviteViaLink: {
|
||||
pushControllerImpl?(InviteLinkInviteController(context: context, peerId: peerId))
|
||||
presentControllerImpl?(InviteLinkInviteController(context: context, peerId: peerId), nil)
|
||||
})
|
||||
|
||||
let peerView = context.account.viewTracker.peerView(peerId)
|
||||
|
@ -291,7 +291,7 @@ private enum ChannelVisibilityEntry: ItemListNodeEntry {
|
||||
case let .privateLinkHeader(_, title):
|
||||
return ItemListSectionHeaderItem(presentationData: presentationData, text: title, sectionId: self.section)
|
||||
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()
|
||||
}, contextAction: { node in
|
||||
arguments.linkContextAction(node)
|
||||
|
@ -251,6 +251,205 @@ public func deleteAllRevokedPeerExportedInvitations(account: Account, peerId: Pe
|
||||
|> 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)
|
||||
|
||||
public struct PeerInvitationImportersState: Equatable {
|
||||
@ -262,7 +461,7 @@ public struct PeerInvitationImportersState: Equatable {
|
||||
public var isLoadingMore: Bool
|
||||
public var hasLoadedOnce: Bool
|
||||
public var canLoadMore: Bool
|
||||
public var count: Int
|
||||
public var count: Int32
|
||||
}
|
||||
|
||||
final class CachedPeerInvitationImporters: PostboxCoding {
|
||||
@ -331,7 +530,7 @@ private final class PeerInvitationImportersContextImpl {
|
||||
private var hasLoadedOnce: Bool = false
|
||||
private var canLoadMore: Bool = true
|
||||
private var results: [PeerInvitationImportersState.Importer] = []
|
||||
private var count: Int
|
||||
private var count: Int32
|
||||
private var populateCache: Bool = true
|
||||
|
||||
let state = Promise<PeerInvitationImportersState>()
|
||||
@ -342,7 +541,7 @@ private final class PeerInvitationImportersContextImpl {
|
||||
self.peerId = peerId
|
||||
self.link = invite.link
|
||||
|
||||
let count = invite.count.flatMap { Int($0) } ?? 0
|
||||
let count = invite.count ?? 0
|
||||
self.count = count
|
||||
|
||||
self.isLoadingMore = true
|
||||
@ -395,7 +594,7 @@ private final class PeerInvitationImportersContextImpl {
|
||||
self.disposable.set((self.account.postbox.transaction { transaction -> Api.InputPeer? in
|
||||
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 {
|
||||
let offsetUser = lastResult?.peer.peer.flatMap { apiInputUser($0) } ?? .inputUserEmpty
|
||||
let offsetDate = lastResult?.date ?? 0
|
||||
@ -404,8 +603,8 @@ private final class PeerInvitationImportersContextImpl {
|
||||
|> `catch` { _ -> Signal<Api.messages.ChatInviteImporters?, NoError> in
|
||||
return .single(nil)
|
||||
}
|
||||
|> mapToSignal { result -> Signal<([PeerInvitationImportersState.Importer], Int), NoError> in
|
||||
return account.postbox.transaction { transaction -> ([PeerInvitationImportersState.Importer], Int) in
|
||||
|> mapToSignal { result -> Signal<([PeerInvitationImportersState.Importer], Int32), NoError> in
|
||||
return account.postbox.transaction { transaction -> ([PeerInvitationImportersState.Importer], Int32) in
|
||||
guard let result = result else {
|
||||
return ([], 0)
|
||||
}
|
||||
@ -434,7 +633,7 @@ private final class PeerInvitationImportersContextImpl {
|
||||
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)
|
||||
}
|
||||
return (resultImporters, Int(count))
|
||||
return (resultImporters, count)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -462,9 +661,9 @@ private final class PeerInvitationImportersContextImpl {
|
||||
strongSelf.hasLoadedOnce = true
|
||||
strongSelf.canLoadMore = !importers.isEmpty
|
||||
if strongSelf.canLoadMore {
|
||||
strongSelf.count = max(updatedCount, strongSelf.results.count)
|
||||
strongSelf.count = max(updatedCount, Int32(strongSelf.results.count))
|
||||
} else {
|
||||
strongSelf.count = strongSelf.results.count
|
||||
strongSelf.count = Int32(strongSelf.results.count)
|
||||
}
|
||||
strongSelf.updateState()
|
||||
}))
|
||||
|
File diff suppressed because it is too large
Load Diff
12
submodules/TelegramUI/Images.xcassets/Chat/Links/LargeLink.imageset/Contents.json
vendored
Normal file
12
submodules/TelegramUI/Images.xcassets/Chat/Links/LargeLink.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "ic_biglink.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
BIN
submodules/TelegramUI/Images.xcassets/Chat/Links/LargeLink.imageset/ic_biglink.pdf
vendored
Normal file
BIN
submodules/TelegramUI/Images.xcassets/Chat/Links/LargeLink.imageset/ic_biglink.pdf
vendored
Normal file
Binary file not shown.
Binary file not shown.
@ -1255,11 +1255,9 @@ private func editingItems(data: PeerInfoScreenData?, context: AccountContext, pr
|
||||
interaction.editingOpenPublicLinkSetup()
|
||||
}))
|
||||
|
||||
if !isPublic {
|
||||
items[.peerPublicSettings]!.append(PeerInfoScreenDisclosureItem(id: ItemInviteLinks, label: .text(""), text: presentationData.strings.GroupInfo_InviteLinks, action: {
|
||||
interaction.editingOpenInviteLinksSetup()
|
||||
}))
|
||||
}
|
||||
items[.peerPublicSettings]!.append(PeerInfoScreenDisclosureItem(id: ItemInviteLinks, label: .text(""), text: presentationData.strings.GroupInfo_InviteLinks, action: {
|
||||
interaction.editingOpenInviteLinksSetup()
|
||||
}))
|
||||
|
||||
if let linkedDiscussionPeer = data.linkedDiscussionPeer {
|
||||
let peerTitle: String
|
||||
@ -1318,9 +1316,10 @@ private func editingItems(data: PeerInfoScreenData?, context: AccountContext, pr
|
||||
}
|
||||
} else if let group = data.peer as? TelegramGroup {
|
||||
let ItemUsername = 1
|
||||
let ItemPreHistory = 2
|
||||
let ItemPermissions = 3
|
||||
let ItemAdmins = 4
|
||||
let ItemInviteLinks = 2
|
||||
let ItemPreHistory = 3
|
||||
let ItemPermissions = 4
|
||||
let ItemAdmins = 5
|
||||
|
||||
if case .creator = group.role {
|
||||
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: {
|
||||
interaction.editingOpenPreHistorySetup()
|
||||
}))
|
||||
@ -4547,7 +4551,7 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD
|
||||
|
||||
contactsController?.push(visibilityController)
|
||||
} else {
|
||||
contactsController?.push(InviteLinkInviteController(context: context, peerId: groupPeer.id))
|
||||
contactsController?.present(InviteLinkInviteController(context: context, peerId: groupPeer.id), in: .window(.root))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -70,9 +70,10 @@ final class ReplyAccessoryPanelNode: AccessoryPanelNode {
|
||||
self.addSubnode(self.imageNode)
|
||||
self.addSubnode(self.actionArea)
|
||||
|
||||
self.messageDisposable.set((context.account.postbox.messageAtId(messageId)
|
||||
|> deliverOnMainQueue).start(next: { [weak self] message in
|
||||
self.messageDisposable.set((context.account.postbox.messageView(messageId)
|
||||
|> deliverOnMainQueue).start(next: { [weak self] messageView in
|
||||
if let strongSelf = self {
|
||||
let message = messageView.message
|
||||
var authorName = ""
|
||||
var text = ""
|
||||
if let author = message?.effectiveAuthor {
|
||||
|
Loading…
x
Reference in New Issue
Block a user