Merge branches 'master' and 'master' of gitlab.com:peter-iakovlev/telegram-ios

This commit is contained in:
overtake 2021-01-18 18:58:49 +03:00
commit c7292fa83f
20 changed files with 4084 additions and 3528 deletions

View File

@ -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";

View File

@ -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()
})
}
}
}
@ -136,13 +178,14 @@ public final class InviteLinkInviteController: ViewController {
private let context: AccountContext
private let peerId: PeerId
private weak var parentNavigationController: NavigationController?
private var presentationDataDisposable: Disposable?
public init(context: AccountContext, peerId: PeerId) {
fatalError()
public init(context: AccountContext, peerId: PeerId, parentNavigationController: NavigationController?) {
self.context = context
self.peerId = peerId
self.parentNavigationController = parentNavigationController
super.init(navigationBarPresentationData: nil)
@ -196,7 +239,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)
})
}
}
@ -348,7 +391,7 @@ public final class InviteLinkInviteController: ViewController {
self?.controller?.present(shareController, in: .window(.root))
}, manageLinks: { [weak self] in
let controller = inviteLinkListController(context: context, peerId: peerId)
self?.controller?.push(controller)
self?.controller?.parentNavigationController?.pushViewController(controller)
self?.controller?.dismiss()
})
@ -360,12 +403,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,14 +553,14 @@ 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)
let listTopInset = layoutTopInset + headerHeight
let listNodeSize = CGSize(width: layout.size.width, height: layout.size.height - listTopInset)
insets.top = max(0.0, listNodeSize.height - visibleItemsHeight)
insets.top = max(0.0, listNodeSize.height - visibleItemsHeight - insets.bottom)
let (duration, curve) = listViewAnimationDurationAndCurve(transition: transition)
let updateSizeAndInsets = ListViewUpdateSizeAndInsets(size: listNodeSize, insets: insets, duration: duration, curve: curve)

View File

@ -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)
}
})
}

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):
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: {
}))
})

View File

@ -209,7 +209,7 @@ public final class InviteLinkQRCodeController: ViewController {
self.contentContainerNode.addSubnode(self.qrIconNode)
self.contentContainerNode.addSubnode(self.qrButtonNode)
let textFont = Font.regular(16.0)
let textFont = Font.regular(13.0)
self.textNode.attributedText = NSAttributedString(string: self.presentationData.strings.InviteLink_QRCode_Info, font: textFont, textColor: secondaryTextColor)
self.buttonNode.title = self.presentationData.strings.InviteLink_QRCode_Share
@ -352,7 +352,7 @@ public final class InviteLinkQRCodeController: ViewController {
let _ = imageApply()
let imageFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - imageSize.width) / 2.0), y: insets.top + 24.0), size: imageSize)
let imageFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - imageSize.width) / 2.0), y: insets.top + 16.0), size: imageSize)
transition.updateFrame(node: self.qrImageNode, frame: imageFrame)
transition.updateFrame(node: self.qrButtonNode, frame: imageFrame)
@ -365,7 +365,7 @@ public final class InviteLinkQRCodeController: ViewController {
let inset: CGFloat = 22.0
let textSize = self.textNode.updateLayout(CGSize(width: layout.size.width - inset * 2.0, height: CGFloat.greatestFiniteMagnitude))
let textFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - textSize.width) / 2.0), y: imageFrame.maxX + 20.0), size: textSize)
let textFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - textSize.width) / 2.0), y: imageFrame.maxY + 20.0), size: textSize)
transition.updateFrame(node: self.textNode, frame: textFrame)
let buttonSideInset: CGFloat = 16.0
@ -379,7 +379,7 @@ public final class InviteLinkQRCodeController: ViewController {
let titleHeight: CGFloat = 54.0
let contentHeight = titleHeight + textSize.height + imageSize.height + bottomInset + 52.0 + 77.0
let contentHeight = titleHeight + textSize.height + imageSize.height + bottomInset + 121.0
let width = horizontalContainerFillingSizeForLayout(layout: layout, sideInset: layout.safeInsets.left)
@ -401,7 +401,7 @@ public final class InviteLinkQRCodeController: ViewController {
transition.updateFrame(node: self.titleNode, frame: titleFrame)
let cancelSize = self.cancelButton.measure(CGSize(width: width, height: titleHeight))
let cancelFrame = CGRect(origin: CGPoint(x: width - cancelSize.width - 16.0, y: 16.0), size: cancelSize)
let cancelFrame = CGRect(origin: CGPoint(x: width - cancelSize.width - 16.0, y: 18.0), size: cancelSize)
transition.updateFrame(node: self.cancelButton, frame: cancelFrame)
let buttonInset: CGFloat = 16.0

View File

@ -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)

View File

@ -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)))
}
}
})

View File

@ -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)

View File

@ -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
}
})
}

View File

@ -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

View File

@ -342,6 +342,8 @@ public func channelMembersController(context: AccountContext, peerId: PeerId) ->
var pushControllerImpl: ((ViewController) -> Void)?
var dismissInputImpl: (() -> Void)?
var getControllerImpl: (() -> ViewController?)?
let actionsDisposable = DisposableSet()
let addMembersDisposable = MetaDisposable()
@ -462,7 +464,9 @@ public func channelMembersController(context: AccountContext, peerId: PeerId) ->
pushControllerImpl?(controller)
}
}, inviteViaLink: {
pushControllerImpl?(InviteLinkInviteController(context: context, peerId: peerId))
if let controller = getControllerImpl?() {
presentControllerImpl?(InviteLinkInviteController(context: context, peerId: peerId, parentNavigationController: controller.navigationController as? NavigationController), nil)
}
})
let peerView = context.account.viewTracker.peerView(peerId)
@ -551,6 +555,9 @@ public func channelMembersController(context: AccountContext, peerId: PeerId) ->
dismissInputImpl = { [weak controller] in
controller?.view.endEditing(true)
}
getControllerImpl = { [weak controller] in
return controller
}
controller.visibleBottomContentOffsetChanged = { offset in
if let loadMoreControl = loadMoreControl, case let .known(value) = offset, value < 40.0 {
context.peerChannelMemberCategoriesContextsManager.loadMore(peerId: peerId, control: loadMoreControl)

View File

@ -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)

View File

@ -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 = populateCache ? 0 : 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()
}))

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()
}))
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, parentNavigationController: contactsController?.navigationController as? NavigationController), in: .window(.root))
}
}

View File

@ -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 {