Various Improvements

This commit is contained in:
Ilya Laktyushin 2021-02-06 22:38:32 +04:00
parent ca993ccfa5
commit 1f6b5ba9e0
79 changed files with 38285 additions and 36596 deletions

Binary file not shown.

View file

@ -5848,6 +5848,18 @@ Sorry for the inconvenience.";
"InviteLink.PeopleJoinedShort_many" = "%@ joined";
"InviteLink.PeopleJoinedShort_any" = "%@ joined";
"InviteLink.PeopleCanJoin_1" = "%@ can join";
"InviteLink.PeopleCanJoin_2" = "%@ can join";
"InviteLink.PeopleCanJoin_3_10" = "%@ can join";
"InviteLink.PeopleCanJoin_many" = "%@ can join";
"InviteLink.PeopleCanJoin_any" = "%@ can join";
"InviteLink.PeopleRemaining_1" = "%@ remaining";
"InviteLink.PeopleRemaining_2" = "%@ remaining";
"InviteLink.PeopleRemaining_3_10" = "%@ remaining";
"InviteLink.PeopleRemaining_many" = "%@ remaining";
"InviteLink.PeopleRemaining_any" = "%@ remaining";
"InviteLink.Expired" = "expired";
"InviteLink.UsageLimitReached" = "limit reached";
"InviteLink.Revoked" = "revoked";
@ -5905,6 +5917,13 @@ Sorry for the inconvenience.";
"InviteLink.OtherAdminsLinks" = "Invite Links Created By Other Admins";
"InviteLink.OtherPermanentLinkInfo" = "**%1$@** can see this link and use it to invite new members to **%2$@**.";
"InviteLink.InviteLinks_0" = "%@ invite links";
"InviteLink.InviteLinks_1" = "%@ invite link";
"InviteLink.InviteLinks_2" = "%@ invite links";
"InviteLink.InviteLinks_3_10" = "%@ invite links";
"InviteLink.InviteLinks_many" = "%@ invite links";
"InviteLink.InviteLinks_any" = "%@ invite links";
"Conversation.ChecksTooltip.Delivered" = "Delivered";
"Conversation.ChecksTooltip.Read" = "Read";
@ -5992,3 +6011,35 @@ Sorry for the inconvenience.";
"Conversation.ForwardTooltip.SavedMessages.One" = "Message forwarded to **Saved Messages**";
"Conversation.ForwardTooltip.SavedMessages.Many" = "Messages forwarded to **Saved Messages**";
"Channel.AdminLog.UpdatedParticipantVolume" = "%1$@ changed %2$@ volume to %3$@";
"Channel.AdminLog.DeletedInviteLink" = "%1$@ deleted invite link %2$@";
"Channel.AdminLog.RevokedInviteLink" = "%1$@ revoked invite link %2$@";
"Channel.AdminLog.EditedInviteLink" = "%1$@ edited invite link %2$@";
"GroupInfo.Permissions.BroadcastTitle" = "Broadcast Channel";
"GroupInfo.Permissions.BroadcastConvert" = "Convert Group to Channel";
"GroupInfo.Permissions.BroadcastConvertInfo" = "You can permanently turn your group to a one to many channel ([like this]()) where only admins can post.";
"ChannelIntro.ChannelsTitle" = "Telegram Channels";
"ChannelIntro.ChannelsText" = "• No limit on the number of members.\n\n• Only admins can post.\n\n• Only admins see members list\n\n• You can enable comments for posts.";
"ChannelIntro.ConvertToChannel" = "Convert to Channel";
"ChannelIntro.ConvertCancel" = "Cancel";
"ConvertToChannel.ConfirmationAlert.Title" = "Convert to Channel";
"ConvertToChannel.ConfirmationAlert.Text" = "Do you want to convert your group to a channel? This action cannot be undone.";
"ConvertToChannel.ConfirmationAlert.Proceed" = "Proceed";
"ConvertToChannel.CommentsAlert.Title" = "Comments for Posts";
"ConvertToChannel.CommentsAlert.Text" = "Do you want to allow subscribers of your channel to comment on posts of the channel?";
"ConvertToChannel.LimitAlert.Title" = "Limit Reached";
"ConvertToChannel.LimitAlert.Text" = "Your group has reached a limit of %@ members. You can increase this limit by converting the group to a broadcast channel where only admins can post, but users can comment (if you allow them). Interested?";
"ConvertToChannel.LimitAlert.LearnMore" = "Learn More";
"GroupInfo.GroupHistoryShort" = "Chat History";
"PeerInfo.CustomizeNotifications" = "Customize";
"Conversation.ContextMenuSpeak" = "Speak";

View file

@ -141,6 +141,9 @@ public final class CallListController: ViewController {
}
self.navigationItem.titleView = self.segmentedTitleView
if case .navigation = self.mode {
self.navigationItem.backBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Back, style: .plain, target: nil, action: nil)
}
}
required public init(coder aDecoder: NSCoder) {

View file

@ -13,8 +13,9 @@ public final class DatePickerTheme: Equatable {
public let disabledColor: UIColor
public let selectionColor: UIColor
public let selectionTextColor: UIColor
public let separatorColor: UIColor
public init(backgroundColor: UIColor, textColor: UIColor, secondaryTextColor: UIColor, accentColor: UIColor, disabledColor: UIColor, selectionColor: UIColor, selectionTextColor: UIColor) {
public init(backgroundColor: UIColor, textColor: UIColor, secondaryTextColor: UIColor, accentColor: UIColor, disabledColor: UIColor, selectionColor: UIColor, selectionTextColor: UIColor, separatorColor: UIColor) {
self.backgroundColor = backgroundColor
self.textColor = textColor
self.secondaryTextColor = secondaryTextColor
@ -22,6 +23,7 @@ public final class DatePickerTheme: Equatable {
self.disabledColor = disabledColor
self.selectionColor = selectionColor
self.selectionTextColor = selectionTextColor
self.separatorColor = separatorColor
}
public static func ==(lhs: DatePickerTheme, rhs: DatePickerTheme) -> Bool {
@ -43,13 +45,16 @@ public final class DatePickerTheme: Equatable {
if lhs.selectionTextColor != rhs.selectionTextColor {
return false
}
if lhs.separatorColor != rhs.separatorColor {
return false
}
return true
}
}
public extension DatePickerTheme {
convenience init(theme: PresentationTheme) {
self.init(backgroundColor: theme.list.itemBlocksBackgroundColor, textColor: theme.list.itemPrimaryTextColor, secondaryTextColor: theme.list.itemSecondaryTextColor, accentColor: theme.list.itemAccentColor, disabledColor: theme.list.itemDisabledTextColor, selectionColor: theme.list.itemCheckColors.fillColor, selectionTextColor: theme.list.itemCheckColors.foregroundColor)
self.init(backgroundColor: theme.list.itemBlocksBackgroundColor, textColor: theme.list.itemPrimaryTextColor, secondaryTextColor: theme.list.itemSecondaryTextColor, accentColor: theme.list.itemAccentColor, disabledColor: theme.list.itemDisabledTextColor, selectionColor: theme.list.itemCheckColors.fillColor, selectionTextColor: theme.list.itemCheckColors.foregroundColor, separatorColor: theme.list.itemBlocksSeparatorColor)
}
}
@ -104,6 +109,12 @@ private func generateNavigationArrowImage(color: UIColor, mirror: Bool) -> UIIma
})
}
private func yearRange(for state: DatePickerNode.State) -> Range<Int> {
let minYear = calendar.component(.year, from: state.minDate)
let maxYear = calendar.component(.year, from: state.maxDate)
return minYear ..< maxYear + 1
}
public final class DatePickerNode: ASDisplayNode {
class MonthNode: ASDisplayNode {
private let month: Date
@ -117,29 +128,9 @@ public final class DatePickerNode: ASDisplayNode {
}
}
var maximumDate: Date? {
didSet {
if let size = self.validSize {
self.updateLayout(size: size)
}
}
}
var minimumDate: Date? {
didSet {
if let size = self.validSize {
self.updateLayout(size: size)
}
}
}
var date: Date? {
didSet {
if let size = self.validSize {
self.updateLayout(size: size)
}
}
}
var maximumDate: Date?
var minimumDate: Date?
var date: Date?
private var validSize: CGSize?
@ -267,6 +258,7 @@ public final class DatePickerNode: ASDisplayNode {
private let timeTitleNode: ImmediateTextNode
private let timeFieldNode: ASImageNode
private let timeSeparatorNode: ASDisplayNode
private let dayNodes: [ImmediateTextNode]
private var currentIndex = 0
@ -290,6 +282,8 @@ public final class DatePickerNode: ASDisplayNode {
private var validLayout: CGSize?
public var valueUpdated: ((Date) -> Void)?
public var maximumDate: Date {
get {
return self.state.maxDate
@ -348,10 +342,15 @@ public final class DatePickerNode: ASDisplayNode {
self.state = State(minDate: telegramReleaseDate, maxDate: upperLimitDate, date: Date(), displayingMonthSelection: false, selectedMonth: monthForDate(Date()))
self.timeTitleNode = ImmediateTextNode()
self.timeTitleNode.attributedText = NSAttributedString(string: "Time", font: Font.regular(17.0), textColor: theme.textColor)
self.timeFieldNode = ASImageNode()
self.timeFieldNode.displaysAsynchronously = false
self.timeFieldNode.displayWithoutProcessing = true
self.timeSeparatorNode = ASDisplayNode()
self.timeSeparatorNode.backgroundColor = theme.separatorColor
self.dayNodes = (0..<7).map { _ in ImmediateTextNode() }
self.contentNode = ASDisplayNode()
@ -361,8 +360,9 @@ public final class DatePickerNode: ASDisplayNode {
self.pickerBackgroundNode.backgroundColor = theme.backgroundColor
self.pickerBackgroundNode.isUserInteractionEnabled = false
self.pickerNode = MonthPickerNode(theme: theme, strings: strings, date: self.state.date, yearRange: 2013 ..< 2038, valueChanged: { date in
var monthChangedImpl: ((Date) -> Void)?
self.pickerNode = MonthPickerNode(theme: theme, strings: strings, date: self.state.date, yearRange: yearRange(for: self.state), valueChanged: { date in
monthChangedImpl?(date)
})
self.monthButtonNode = HighlightTrackingButtonNode()
@ -380,9 +380,13 @@ public final class DatePickerNode: ASDisplayNode {
self.clipsToBounds = true
self.backgroundColor = theme.backgroundColor
self.addSubnode(self.contentNode)
self.addSubnode(self.timeTitleNode)
self.addSubnode(self.timeFieldNode)
self.addSubnode(self.timeSeparatorNode)
self.addSubnode(self.contentNode)
self.dayNodes.forEach { self.addSubnode($0) }
self.addSubnode(self.previousButtonNode)
@ -420,6 +424,15 @@ public final class DatePickerNode: ASDisplayNode {
self.previousButtonNode.addTarget(self, action: #selector(self.previousButtonPressed), forControlEvents: .touchUpInside)
self.nextButtonNode.addTarget(self, action: #selector(self.nextButtonPressed), forControlEvents: .touchUpInside)
monthChangedImpl = { [weak self] date in
if let strongSelf = self {
let updatedState = State(minDate: strongSelf.state.minDate, maxDate: strongSelf.state.maxDate, date: date, displayingMonthSelection: strongSelf.state.displayingMonthSelection, selectedMonth: monthForDate(date))
strongSelf.updateState(updatedState, animated: false)
strongSelf.valueUpdated?(date)
}
}
}
override public func didLoad() {
@ -436,6 +449,7 @@ public final class DatePickerNode: ASDisplayNode {
self.state = state
if previousState.minDate != state.minDate || previousState.maxDate != state.maxDate {
self.pickerNode.yearRange = yearRange(for: state)
self.setupItems()
} else if previousState.selectedMonth != state.selectedMonth {
for i in 0 ..< self.months.count {
@ -445,6 +459,7 @@ public final class DatePickerNode: ASDisplayNode {
}
}
}
self.pickerNode.date = self.state.date
if let size = self.validLayout {
self.updateLayout(size: size, transition: animated ? .animated(duration: 0.3, curve: .spring) : .immediate)
@ -455,9 +470,7 @@ public final class DatePickerNode: ASDisplayNode {
let startMonth = monthForDate(self.state.minDate)
let endMonth = monthForDate(self.state.maxDate)
let selectedMonth = monthForDate(self.state.selectedMonth)
let calendar = Calendar.current
var currentIndex = 0
var months: [Date] = [startMonth]
@ -524,7 +537,7 @@ public final class DatePickerNode: ASDisplayNode {
}
self.transitionFraction = transitionFraction
if let size = self.validLayout {
let topInset: CGFloat = 78.0
let topInset: CGFloat = 78.0 + 44.0
let containerSize = CGSize(width: size.width, height: size.height - topInset)
self.updateItems(size: containerSize, transition: .animated(duration: 0.3, curve: .spring))
}
@ -565,10 +578,13 @@ public final class DatePickerNode: ASDisplayNode {
var wasAdded = false
if let current = self.monthNodes[self.months[i]] {
itemNode = current
current.minimumDate = self.state.minDate
current.maximumDate = self.state.maxDate
current.date = self.state.date
current.updateLayout(size: size)
} else {
wasAdded = true
let addedItemNode = MonthNode(theme: self.theme, month: self.months[i], minimumDate: self.minimumDate, maximumDate: self.maximumDate, date: self.date)
let addedItemNode = MonthNode(theme: self.theme, month: self.months[i], minimumDate: self.state.minDate, maximumDate: self.state.maxDate, date: self.state.date)
itemNode = addedItemNode
self.monthNodes[self.months[i]] = addedItemNode
self.contentNode.addSubnode(addedItemNode)
@ -604,18 +620,29 @@ public final class DatePickerNode: ASDisplayNode {
public func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) {
self.validLayout = size
let topInset: CGFloat = 78.0
let timeHeight: CGFloat = 44.0
let topInset: CGFloat = 78.0 + timeHeight
let sideInset: CGFloat = 16.0
let month = monthForDate(self.state.selectedMonth)
let components = calendar.dateComponents([.month, .year], from: month)
let timeTitleSize = self.timeTitleNode.updateLayout(size)
self.timeTitleNode.frame = CGRect(origin: CGPoint(x: 16.0, y: 11.0), size: timeTitleSize)
self.timeSeparatorNode.frame = CGRect(x: 16.0, y: timeHeight, width: size.width - 16.0, height: UIScreenPixel)
self.monthTextNode.attributedText = NSAttributedString(string: stringForMonth(strings: self.strings, month: components.month.flatMap { Int32($0) - 1 } ?? 0, ofYear: components.year.flatMap { Int32($0) - 1900 } ?? 100), font: controlFont, textColor: theme.textColor)
let monthSize = self.monthTextNode.updateLayout(size)
let monthTextFrame = CGRect(x: sideInset, y: 10.0, width: monthSize.width, height: monthSize.height)
let monthTextFrame = CGRect(x: sideInset, y: 11.0 + timeHeight, width: monthSize.width, height: monthSize.height)
self.monthTextNode.frame = monthTextFrame
self.monthArrowNode.frame = CGRect(x: monthTextFrame.maxX + 10.0, y: monthTextFrame.minY + 4.0, width: 7.0, height: 12.0)
let monthArrowFrame = CGRect(x: monthTextFrame.maxX + 10.0, y: monthTextFrame.minY + 4.0, width: 7.0, height: 12.0)
self.monthArrowNode.position = monthArrowFrame.center
self.monthArrowNode.bounds = CGRect(origin: CGPoint(), size: monthArrowFrame.size)
transition.updateTransformRotation(node: self.monthArrowNode, angle: self.state.displayingMonthSelection ? CGFloat.pi / 2.0 : 0.0)
self.monthButtonNode.frame = monthTextFrame.inset(by: UIEdgeInsets(top: -6.0, left: -6.0, bottom: -6.0, right: -30.0))
self.previousButtonNode.frame = CGRect(x: size.width - sideInset - 54.0, y: monthTextFrame.minY + 1.0, width: 10.0, height: 17.0)
@ -629,7 +656,7 @@ public final class DatePickerNode: ASDisplayNode {
dayNode.attributedText = NSAttributedString(string: shortStringForDayOfWeek(strings: self.strings, day: Int32(i)).uppercased(), font: dayFont, textColor: theme.secondaryTextColor)
let textSize = dayNode.updateLayout(size)
let cellFrame = CGRect(x: daysSideInset + CGFloat(i) * cellSize, y: 40.0, width: cellSize, height: cellSize)
let cellFrame = CGRect(x: daysSideInset + CGFloat(i) * cellSize, y: topInset - 38.0, width: cellSize, height: cellSize)
let textFrame = CGRect(origin: CGPoint(x: cellFrame.minX + floor((cellFrame.width - textSize.width) / 2.0), y: cellFrame.minY + floor((cellFrame.height - textSize.height) / 2.0)), size: textSize)
dayNode.frame = textFrame
@ -679,10 +706,10 @@ private final class MonthPickerNode: ASDisplayNode, UIPickerViewDelegate, UIPick
private let theme: DatePickerTheme
private let strings: PresentationStrings
private var date: Date
private var yearRange: Range<Int> {
var date: Date
var yearRange: Range<Int> {
didSet {
self.pickerView.reloadAllComponents()
self.reload()
}
}
@ -706,9 +733,16 @@ private final class MonthPickerNode: ASDisplayNode, UIPickerViewDelegate, UIPick
self.pickerView.dataSource = self
self.view.addSubview(self.pickerView)
self.reload()
}
private func reload() {
self.pickerView.reloadAllComponents()
// self.pickerView.selectRow(index, inComponent: 0, animated: false)
let month = calendar.component(.month, from: date)
let year = calendar.component(.year, from: date)
self.pickerView.selectRow(month - 1, inComponent: 0, animated: false)
self.pickerView.selectRow(year - yearRange.startIndex, inComponent: 1, animated: false)
}
override func calculateSizeThatFits(_ constrainedSize: CGSize) -> CGSize {
@ -742,7 +776,23 @@ private final class MonthPickerNode: ASDisplayNode, UIPickerViewDelegate, UIPick
}
func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {
// self.valueChanged(timeoutValues[row])
let month = pickerView.selectedRow(inComponent: 0) + 1
let year = self.yearRange.startIndex + pickerView.selectedRow(inComponent: 1)
var components = calendar.dateComponents([.year, .month, .day, .hour, .minute, .second], from: self.date)
let day = components.day ?? 1
components.day = 1
components.month = month
components.year = year
let tempDate = calendar.date(from: components)!
let numberOfDays = calendar.range(of: .day, in: .month, for: tempDate)!.count
components.day = min(day, numberOfDays)
let date = calendar.date(from: components)!
self.date = date
self.valueChanged(date)
}
override func layout() {

View file

@ -28,7 +28,7 @@ public func smartInvertColorsEnabled() -> Bool {
}
}
public func reduceMotionEnabled() -> Signal<Bool, NoError> {
public func isReduceMotionEnabled() -> Signal<Bool, NoError> {
return Signal { subscriber in
subscriber.putNext(UIAccessibility.isReduceMotionEnabled)
@ -41,10 +41,30 @@ public func reduceMotionEnabled() -> Signal<Bool, NoError> {
NotificationCenter.default.removeObserver(observer)
}
}
} |> runOn(Queue.mainQueue())
} |> runOn(Queue.mainQueue())
}
func boldTextEnabled() -> Signal<Bool, NoError> {
public func isSpeakSelectionEnabled() -> Bool {
return UIAccessibility.isSpeakSelectionEnabled
}
public func isSpeakSelectionEnabledSignal() -> Signal<Bool, NoError> {
return Signal { subscriber in
subscriber.putNext(UIAccessibility.isSpeakSelectionEnabled)
let observer = NotificationCenter.default.addObserver(forName: UIAccessibility.speakSelectionStatusDidChangeNotification, object: nil, queue: .main, using: { _ in
subscriber.putNext(UIAccessibility.isSpeakSelectionEnabled)
})
return ActionDisposable {
Queue.mainQueue().async {
NotificationCenter.default.removeObserver(observer)
}
}
} |> runOn(Queue.mainQueue())
}
public func isBoldTextEnabled() -> Signal<Bool, NoError> {
return Signal { subscriber in
subscriber.putNext(UIAccessibility.isBoldTextEnabled)

View file

@ -598,7 +598,7 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture
if strongSelf.reorderFeedback == nil {
strongSelf.reorderFeedback = HapticFeedback()
}
strongSelf.reorderFeedback?.tap()
strongSelf.reorderFeedback?.impact()
}))
}
}

View file

@ -3,7 +3,7 @@ import UIKit
import AsyncDisplayKit
private func generateShadowImage(mirror: Bool) -> UIImage? {
return generateImage(CGSize(width: 30.0, height: 30.0), rotatedContext: { size, context in
return generateImage(CGSize(width: 30.0, height: 45.0), rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
if mirror {
@ -12,7 +12,7 @@ private func generateShadowImage(mirror: Bool) -> UIImage? {
context.translateBy(x: -size.width / 2.0, y: -size.height / 2.0)
}
context.setShadow(offset: CGSize(width: 0.0, height: 0.0), blur: 10.0, color: UIColor(white: 0.0, alpha: 0.4).cgColor)
context.setShadow(offset: CGSize(width: 0.0, height: 0.0), blur: 18.0, color: UIColor(white: 0.0, alpha: 0.35).cgColor)
context.setFillColor(UIColor(white: 0.0, alpha: 1.0).cgColor)
for _ in 0 ..< 1 {
context.fill(CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: 15.0)))
@ -67,10 +67,10 @@ final class ListViewReorderingItemNode: ASDisplayNode {
self.view.addSubview(self.copyView)
self.copyView.frame = CGRect(origin: CGPoint(x: initialLocation.x, y: initialLocation.y), size: itemNode.bounds.size)
self.copyView.topShadow.frame = CGRect(origin: CGPoint(x: 0.0, y: -15.0), size: CGSize(width: copyView.bounds.size.width, height: 30.0))
self.copyView.topShadow.frame = CGRect(origin: CGPoint(x: 0.0, y: -30.0), size: CGSize(width: copyView.bounds.size.width, height: 45.0))
self.copyView.bottomShadow.image = generateShadowImage(mirror: false)
self.copyView.bottomShadow.frame = CGRect(origin: CGPoint(x: 0.0, y: self.copyView.bounds.size.height - 15.0), size: CGSize(width: self.copyView.bounds.size.width, height: 30.0))
self.copyView.bottomShadow.frame = CGRect(origin: CGPoint(x: 0.0, y: self.copyView.bounds.size.height - 15.0), size: CGSize(width: self.copyView.bounds.size.width, height: 45.0))
self.copyView.topShadow.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25)
self.copyView.bottomShadow.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25)

View file

@ -475,7 +475,7 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, UIScroll
} else if let channel = peer as? TelegramChannel {
if message.flags.contains(.Incoming) {
canDelete = channel.hasPermission(.deleteAllMessages)
canEdit = canEdit && channel.hasPermission(.editAllMessages)
canEdit = canEdit && channel.hasPermission(.sendMessages)
} else {
canDelete = true
}

View file

@ -193,7 +193,7 @@ private enum InviteLinksEditEntry: ItemListNodeEntry {
arguments.dismissInput()
arguments.updateState { state in
var updatedState = state
// updatedState.pickingTimeLimit = !state.pickingTimeLimit
updatedState.pickingTimeLimit = !state.pickingTimeLimit
return updatedState
}
})

View file

@ -236,6 +236,8 @@ public final class InviteLinkInviteController: ViewController {
self.isDismissed = true
self.didAppearOnce = false
self.dismissAllTooltips()
self.controllerNode.animateOut(completion: { [weak self] in
completion?()
self?.presentingViewController?.dismiss(animated: false, completion: nil)
@ -243,6 +245,20 @@ public final class InviteLinkInviteController: ViewController {
}
}
private func dismissAllTooltips() {
self.window?.forEachController({ controller in
if let controller = controller as? UndoOverlayController {
controller.dismissWithCommitAction()
}
})
self.forEachController({ controller in
if let controller = controller as? UndoOverlayController {
controller.dismissWithCommitAction()
}
return true
})
}
override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
super.containerLayoutUpdated(layout, transition: transition)
@ -343,6 +359,8 @@ public final class InviteLinkInviteController: ViewController {
if let invite = invite {
UIPasteboard.general.string = invite.link
self?.controller?.dismissAllTooltips()
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
self?.controller?.present(UndoOverlayController(presentationData: presentationData, content: .linkCopied(text: presentationData.strings.InviteLink_InviteLinkCopiedText), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .window(.root))
}
@ -374,9 +392,10 @@ public final class InviteLinkInviteController: ViewController {
ActionSheetButtonItem(title: presentationData.strings.GroupInfo_InviteLink_RevokeLink, color: .destructive, action: {
dismissAction()
self?.revokeDisposable.set((revokePersistentPeerExportedInvitation(account: context.account, peerId: peerId) |> deliverOnMainQueue).start(completed: {
}))
// revokePeerExportedInvitation(account: <#T##Account#>, peerId: <#T##PeerId#>, link: <#T##String#>)
// self?.revokeDisposable.set((revokePersistentPeerExportedInvitation(account: context.account, peerId: peerId) |> deliverOnMainQueue).start(completed: {
//
// }))
})
]),
ActionSheetItemGroup(items: [ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, action: { dismissAction() })])
@ -384,11 +403,13 @@ public final class InviteLinkInviteController: ViewController {
self?.controller?.present(controller, in: .window(.root))
})))
let contextController = ContextController(account: context.account, presentationData: presentationData, source: .extracted(InviteLinkContextExtractedContentSource(controller: controller, sourceNode: node)), items: .single(items), reactionItems: [], gesture: gesture)
let contextController = ContextController(account: context.account, presentationData: presentationData, source: .extracted(InviteLinkContextExtractedContentSource(controller: controller, sourceNode: node, blurBackground: false)), items: .single(items), reactionItems: [], gesture: gesture)
self?.controller?.presentInGlobalOverlay(contextController)
}, copyLink: { [weak self] invite in
UIPasteboard.general.string = invite.link
self?.controller?.dismissAllTooltips()
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
self?.controller?.present(UndoOverlayController(presentationData: presentationData, content: .linkCopied(text: presentationData.strings.InviteLink_InviteLinkCopiedText), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .window(.root))
}, shareLink: { [weak self] invite in
@ -403,7 +424,8 @@ public final class InviteLinkInviteController: ViewController {
let previousEntries = Atomic<[InviteLinkInviteEntry]?>(value: nil)
let peerView = context.account.postbox.peerView(id: peerId)
self.disposable = (combineLatest(self.presentationDataPromise.get(), peerView, self.invitesContext.state)
let invites: Signal<PeerExportedInvitationsState, NoError> = .single(PeerExportedInvitationsState())
self.disposable = (combineLatest(self.presentationDataPromise.get(), peerView, invites)
|> deliverOnMainQueue).start(next: { [weak self] presentationData, view, invites in
if let strongSelf = self {
var entries: [InviteLinkInviteEntry] = []

View file

@ -52,8 +52,8 @@ private enum InviteLinksListSection: Int32 {
case header
case mainLink
case links
case admins
case revokedLinks
case admins
}
private enum InviteLinksListEntry: ItemListNodeEntry {
@ -65,15 +65,15 @@ private enum InviteLinksListEntry: ItemListNodeEntry {
case linksHeader(PresentationTheme, String)
case linksCreate(PresentationTheme, String)
case links(Int32, PresentationTheme, [ExportedInvitation]?, Int)
case link(Int32, PresentationTheme, ExportedInvitation?, Int32?)
case linksInfo(PresentationTheme, String)
case adminsHeader(PresentationTheme, String)
case admin(Int32, PresentationTheme, ExportedInvitationCreator)
case revokedLinksHeader(PresentationTheme, String)
case revokedLinksDeleteAll(PresentationTheme, String)
case revokedLinks(Int32, PresentationTheme, [ExportedInvitation]?)
case revokedLinks(Int32, PresentationTheme, ExportedInvitation?)
case adminsHeader(PresentationTheme, String)
case admin(Int32, PresentationTheme, ExportedInvitationCreator)
var section: ItemListSectionId {
switch self {
@ -81,12 +81,12 @@ private enum InviteLinksListEntry: ItemListNodeEntry {
return InviteLinksListSection.header.rawValue
case .mainLinkHeader, .mainLink, .mainLinkOtherInfo:
return InviteLinksListSection.mainLink.rawValue
case .linksHeader, .linksCreate, .links, .linksInfo:
case .linksHeader, .linksCreate, .link, .linksInfo:
return InviteLinksListSection.links.rawValue
case .adminsHeader, .admin:
return InviteLinksListSection.admins.rawValue
case .revokedLinksHeader, .revokedLinksDeleteAll, .revokedLinks:
return InviteLinksListSection.revokedLinks.rawValue
case .adminsHeader, .admin:
return InviteLinksListSection.admins.rawValue
}
}
@ -104,20 +104,20 @@ private enum InviteLinksListEntry: ItemListNodeEntry {
return 4
case .linksCreate:
return 5
case let .links(index, _, _, _):
case let .link(index, _, _, _):
return 6 + index
case .linksInfo:
return 10000
case .adminsHeader:
return 10001
case let .admin(index, _, _):
return 10002 + index
case .revokedLinksHeader:
return 20001
return 10001
case .revokedLinksDeleteAll:
return 20002
return 10002
case let .revokedLinks(index, _, _):
return 20003 + index
return 10003 + index
case .adminsHeader:
return 20001
case let .admin(index, _, _):
return 20002 + index
}
}
@ -159,8 +159,8 @@ private enum InviteLinksListEntry: ItemListNodeEntry {
} else {
return false
}
case let .links(lhsIndex, lhsTheme, lhsLinks, lhsCount):
if case let .links(rhsIndex, rhsTheme, rhsLinks, rhsCount) = rhs, lhsIndex == rhsIndex, lhsTheme === rhsTheme, lhsLinks == rhsLinks, lhsCount == rhsCount {
case let .link(lhsIndex, lhsTheme, lhsLink, lhsTick):
if case let .link(rhsIndex, rhsTheme, rhsLink, rhsTick) = rhs, lhsIndex == rhsIndex, lhsTheme === rhsTheme, lhsLink == rhsLink, lhsTick == rhsTick {
return true
} else {
return false
@ -171,18 +171,6 @@ private enum InviteLinksListEntry: ItemListNodeEntry {
} else {
return false
}
case let .adminsHeader(lhsTheme, lhsText):
if case let .adminsHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
case let .admin(lhsIndex, lhsTheme, lhsCreator):
if case let .admin(rhsIndex, rhsTheme, rhsCreator) = rhs, lhsIndex == rhsIndex, lhsTheme === rhsTheme, lhsCreator == rhsCreator {
return true
} else {
return false
}
case let .revokedLinksHeader(lhsTheme, lhsText):
if case let .revokedLinksHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
@ -195,8 +183,20 @@ private enum InviteLinksListEntry: ItemListNodeEntry {
} else {
return false
}
case let .revokedLinks(lhsIndex, lhsTheme, lhsLinks):
if case let .revokedLinks(rhsIndex, rhsTheme, rhsLinks) = rhs, lhsIndex == rhsIndex, lhsTheme === rhsTheme, lhsLinks == rhsLinks {
case let .revokedLinks(lhsIndex, lhsTheme, lhsLink):
if case let .revokedLinks(rhsIndex, rhsTheme, rhsLink) = rhs, lhsIndex == rhsIndex, lhsTheme === rhsTheme, lhsLink == rhsLink {
return true
} else {
return false
}
case let .adminsHeader(lhsTheme, lhsText):
if case let .adminsHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
case let .admin(lhsIndex, lhsTheme, lhsCreator):
if case let .admin(rhsIndex, rhsTheme, rhsCreator) = rhs, lhsIndex == rhsIndex, lhsTheme === rhsTheme, lhsCreator == rhsCreator {
return true
} else {
return false
@ -236,40 +236,40 @@ private enum InviteLinksListEntry: ItemListNodeEntry {
case let .linksHeader(_, text):
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
case let .linksCreate(theme, text):
return ItemListPeerActionItem(presentationData: presentationData, icon: PresentationResourcesItemList.plusIconImage(theme), title: text, hasSeparator: false, sectionId: self.section, editing: false, action: {
return ItemListPeerActionItem(presentationData: presentationData, icon: PresentationResourcesItemList.plusIconImage(theme), title: text, sectionId: self.section, editing: false, action: {
arguments.createLink()
})
case let .links(_, _, invites, count):
return ItemListInviteLinkGridItem(presentationData: presentationData, invites: invites, count: count, share: false, sectionId: self.section, style: .blocks, tapAction: { invite in
case let .link(_, _, invite, _):
return ItemListInviteLinkItem(presentationData: presentationData, invite: invite, share: false, sectionId: self.section, style: .blocks) { invite in
arguments.openLink(invite)
}, contextAction: { invite, node in
arguments.linkContextAction(invite, node, nil)
})
} contextAction: { invite, node, gesture in
arguments.linkContextAction(invite, node, gesture)
}
case let .linksInfo(_, text):
return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section)
case let .revokedLinksHeader(_, text):
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
case let .revokedLinksDeleteAll(theme, text):
return ItemListPeerActionItem(presentationData: presentationData, icon: PresentationResourcesItemList.deleteIconImage(theme), title: text, sectionId: self.section, color: .destructive, editing: false, action: {
arguments.deleteAllRevokedLinks()
})
case let .revokedLinks(_, _, invite):
return ItemListInviteLinkItem(presentationData: presentationData, invite: invite, share: false, sectionId: self.section, style: .blocks) { invite in
arguments.openLink(invite)
} contextAction: { invite, node, gesture in
arguments.linkContextAction(invite, node, gesture)
}
case let .adminsHeader(_, text):
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
case let .admin(_, _, creator):
return ItemListPeerItem(presentationData: presentationData, dateTimeFormat: PresentationDateTimeFormat(timeFormat: .regular, dateFormat: .monthFirst, dateSeparator: ".", decimalSeparator: ".", groupingSeparator: "."), nameDisplayOrder: .firstLast, context: arguments.context, peer: creator.peer.peer!, height: .peerList, aliasHandling: .standard, nameColor: .primary, nameStyle: .plain, presence: nil, text: .none, label: .disclosure("\(creator.count)"), editing: ItemListPeerItemEditing(editable: false, editing: false, revealed: nil), revealOptions: nil, switchValue: nil, enabled: true, highlighted: false, selectable: true, sectionId: self.section, action: {
arguments.openAdmin(creator)
}, setPeerIdWithRevealedOptions: { _, _ in }, removePeer: { _ in }, toggleUpdated: nil, contextAction: nil)
case let .revokedLinksHeader(_, text):
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
case let .revokedLinksDeleteAll(theme, text):
return ItemListPeerActionItem(presentationData: presentationData, icon: PresentationResourcesItemList.deleteIconImage(theme), title: text, hasSeparator: false, sectionId: self.section, color: .destructive, editing: false, action: {
arguments.deleteAllRevokedLinks()
})
case let .revokedLinks(_, _, invites):
return ItemListInviteLinkGridItem(presentationData: presentationData, invites: invites, count: 0, share: false, sectionId: self.section, style: .blocks, tapAction: { invite in
arguments.openLink(invite)
}, contextAction: { invite, node in
arguments.linkContextAction(invite, node, nil)
})
}
}
}
private func inviteLinkListControllerEntries(presentationData: PresentationData, view: PeerView, invites: [ExportedInvitation]?, revokedInvites: [ExportedInvitation]?, importers: PeerInvitationImportersState?, creators: [ExportedInvitationCreator], admin: ExportedInvitationCreator?) -> [InviteLinksListEntry] {
private func inviteLinkListControllerEntries(presentationData: PresentationData, view: PeerView, invites: [ExportedInvitation]?, revokedInvites: [ExportedInvitation]?, importers: PeerInvitationImportersState?, creators: [ExportedInvitationCreator], admin: ExportedInvitationCreator?, tick: Int32) -> [InviteLinksListEntry] {
var entries: [InviteLinksListEntry] = []
if admin == nil {
@ -318,24 +318,33 @@ private func inviteLinkListControllerEntries(presentationData: PresentationData,
}
if let additionalInvites = additionalInvites {
var index: Int32 = 0
for i in stride(from: 0, to: additionalInvites.endIndex, by: 2) {
var invitesPair: [ExportedInvitation] = []
invitesPair.append(additionalInvites[i])
if i + 1 < additionalInvites.count {
invitesPair.append(additionalInvites[i + 1])
}
entries.append(.links(index, presentationData.theme, invitesPair, invitesPair.count))
for invite in additionalInvites {
entries.append(.link(index, presentationData.theme, invite, invite.expireDate != nil ? tick : nil))
index += 1
}
} else if let admin = admin {
} else if let admin = admin, admin.count > 0 {
var index: Int32 = 0
for _ in stride(from: 0, to: admin.count, by: 2) {
entries.append(.links(index, presentationData.theme, nil, 1))
for _ in 0 ..< admin.count - 1 {
entries.append(.link(index, presentationData.theme, nil, nil))
index += 1
}
}
entries.append(.linksInfo(presentationData.theme, presentationData.strings.InviteLink_CreateInfo))
if admin == nil {
entries.append(.linksInfo(presentationData.theme, presentationData.strings.InviteLink_CreateInfo))
}
if let revokedInvites = revokedInvites, !revokedInvites.isEmpty {
entries.append(.revokedLinksHeader(presentationData.theme, presentationData.strings.InviteLink_RevokedLinks.uppercased()))
if admin == nil {
entries.append(.revokedLinksDeleteAll(presentationData.theme, presentationData.strings.InviteLink_DeleteAllRevokedLinks))
}
var index: Int32 = 0
for invite in revokedInvites {
entries.append(.revokedLinks(index, presentationData.theme, invite))
index += 1
}
}
if !creators.isEmpty {
entries.append(.adminsHeader(presentationData.theme, presentationData.strings.InviteLink_OtherAdminsLinks.uppercased()))
var index: Int32 = 0
@ -347,23 +356,6 @@ private func inviteLinkListControllerEntries(presentationData: PresentationData,
}
}
if let revokedInvites = revokedInvites, !revokedInvites.isEmpty {
entries.append(.revokedLinksHeader(presentationData.theme, presentationData.strings.InviteLink_RevokedLinks.uppercased()))
if admin == nil {
entries.append(.revokedLinksDeleteAll(presentationData.theme, presentationData.strings.InviteLink_DeleteAllRevokedLinks))
}
var index: Int32 = 0
for i in stride(from: 0, to: revokedInvites.endIndex, by: 2) {
var invitesPair: [ExportedInvitation] = []
invitesPair.append(revokedInvites[i])
if i + 1 < revokedInvites.count {
invitesPair.append(revokedInvites[i + 1])
}
entries.append(.revokedLinks(index, presentationData.theme, invitesPair))
index += 1
}
}
return entries
}
@ -371,12 +363,13 @@ private struct InviteLinkListControllerState: Equatable {
var revokingPrivateLink: Bool
}
public func inviteLinkListController(context: AccountContext, peerId: PeerId, admin: ExportedInvitationCreator?) -> ViewController {
var pushControllerImpl: ((ViewController) -> Void)?
var presentControllerImpl: ((ViewController, ViewControllerPresentationArguments?) -> Void)?
var presentInGlobalOverlayImpl: ((ViewController) -> Void)?
var dismissTooltipsImpl: (() -> Void)?
let actionsDisposable = DisposableSet()
let statePromise = ValuePromise(InviteLinkListControllerState(revokingPrivateLink: false), ignoreRepeated: true)
@ -413,6 +406,8 @@ public func inviteLinkListController(context: AccountContext, peerId: PeerId, ad
}, copyLink: { invite in
UIPasteboard.general.string = invite.link
dismissTooltipsImpl?()
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
presentControllerImpl?(UndoOverlayController(presentationData: presentationData, content: .linkCopied(text: presentationData.strings.InviteLink_InviteLinkCopiedText), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), nil)
}, mainLinkContextAction: { invite, node, gesture in
@ -427,6 +422,8 @@ public func inviteLinkListController(context: AccountContext, peerId: PeerId, ad
}, action: { _, f in
f(.dismissWithoutContent)
dismissTooltipsImpl?()
UIPasteboard.general.string = invite.link
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
@ -470,16 +467,16 @@ public func inviteLinkListController(context: AccountContext, peerId: PeerId, ad
}
}
if revoke {
revokeLinkDisposable.set((revokePersistentPeerExportedInvitation(account: context.account, peerId: peerId) |> deliverOnMainQueue).start(completed: {
updateState { state in
var updatedState = state
updatedState.revokingPrivateLink = false
return updatedState
}
invitesContext.reload()
revokedInvitesContext.reload()
}))
// revokeLinkDisposable.set((revokePersistentPeerExportedInvitation(account: context.account, peerId: peerId) |> deliverOnMainQueue).start(completed: {
// updateState { state in
// var updatedState = state
// updatedState.revokingPrivateLink = false
// return updatedState
// }
//
// invitesContext.reload()
// revokedInvitesContext.reload()
// }))
}
})
]),
@ -489,7 +486,7 @@ public func inviteLinkListController(context: AccountContext, peerId: PeerId, ad
})))
}
let contextController = ContextController(account: context.account, presentationData: presentationData, source: .extracted(InviteLinkContextExtractedContentSource(controller: controller, sourceNode: node)), items: .single(items), reactionItems: [], gesture: gesture)
let contextController = ContextController(account: context.account, presentationData: presentationData, source: .extracted(InviteLinkContextExtractedContentSource(controller: controller, sourceNode: node, blurBackground: false)), items: .single(items), reactionItems: [], gesture: gesture)
presentInGlobalOverlayImpl?(contextController)
}, createLink: {
let controller = inviteLinkEditController(context: context, peerId: peerId, invite: nil, completion: { invite in
@ -517,6 +514,8 @@ public func inviteLinkListController(context: AccountContext, peerId: PeerId, ad
}, action: { _, f in
f(.dismissWithoutContent)
dismissTooltipsImpl?()
UIPasteboard.general.string = invite.link
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
@ -619,7 +618,7 @@ public func inviteLinkListController(context: AccountContext, peerId: PeerId, ad
})))
}
let contextController = ContextController(account: context.account, presentationData: presentationData, source: .extracted(InviteLinkContextExtractedContentSource(controller: controller, sourceNode: node)), items: .single(items), reactionItems: [], gesture: gesture)
let contextController = ContextController(account: context.account, presentationData: presentationData, source: .extracted(InviteLinkContextExtractedContentSource(controller: controller, sourceNode: node, blurBackground: true)), items: .single(items), reactionItems: [], gesture: gesture)
presentInGlobalOverlayImpl?(contextController)
}, openAdmin: { admin in
let controller = inviteLinkListController(context: context, peerId: peerId, admin: admin)
@ -636,7 +635,7 @@ public func inviteLinkListController(context: AccountContext, peerId: PeerId, ad
ActionSheetButtonItem(title: presentationData.strings.InviteLink_DeleteAllRevokedLinksAlert_Action, color: .destructive, action: {
dismissAction()
deleteAllRevokedLinksDisposable.set((deleteAllRevokedPeerExportedInvitations(account: context.account, peerId: peerId) |> deliverOnMainQueue).start(completed: {
deleteAllRevokedLinksDisposable.set((deleteAllRevokedPeerExportedInvitations(account: context.account, peerId: peerId, adminId: adminId ?? context.account.peerId) |> deliverOnMainQueue).start(completed: {
}))
revokedInvitesContext.clear()
@ -673,10 +672,16 @@ public func inviteLinkListController(context: AccountContext, peerId: PeerId, ad
}
}
let timerPromise = ValuePromise<Int32>(0)
let timer = SwiftSignalKit.Timer(timeout: 1.0, repeat: true, completion: {
timerPromise.set(Int32(CFAbsoluteTimeGetCurrent()))
}, queue: Queue.mainQueue())
timer.start()
let previousRevokedInvites = Atomic<PeerExportedInvitationsState?>(value: nil)
let signal = combineLatest(context.sharedContext.presentationData, peerView, importersContext, importersState.get(), invitesContext.state, revokedInvitesContext.state, creators)
let signal = combineLatest(context.sharedContext.presentationData, peerView, importersContext, importersState.get(), invitesContext.state, revokedInvitesContext.state, creators, timerPromise.get())
|> deliverOnMainQueue
|> map { presentationData, view, importersContext, importers, invites, revokedInvites, creators -> (ItemListControllerState, (ItemListNodeState, Any)) in
|> map { presentationData, view, importersContext, importers, invites, revokedInvites, creators, tick -> (ItemListControllerState, (ItemListNodeState, Any)) in
let previousRevokedInvites = previousRevokedInvites.swap(invites)
var crossfade = false
@ -686,21 +691,25 @@ public func inviteLinkListController(context: AccountContext, peerId: PeerId, ad
let title: ItemListControllerTitle
if let admin = admin, let peer = admin.peer.peer {
title = .textWithSubtitle(peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder), "\(admin.count) invite links")
title = .textWithSubtitle(peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder), presentationData.strings.InviteLink_InviteLinks(admin.count))
} else {
title = .text(presentationData.strings.InviteLink_Title)
}
let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: title, leftNavigationButton: nil, rightNavigationButton: nil, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: true)
let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: inviteLinkListControllerEntries(presentationData: presentationData, view: view, invites: invites.hasLoadedOnce ? invites.invitations : nil, revokedInvites: revokedInvites.invitations, importers: importers, creators: creators, admin: admin), style: .blocks, emptyStateItem: nil, crossfadeState: crossfade, animateChanges: false)
let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: inviteLinkListControllerEntries(presentationData: presentationData, view: view, invites: invites.hasLoadedOnce ? invites.invitations : nil, revokedInvites: revokedInvites.invitations, importers: importers, creators: creators, admin: admin, tick: tick), style: .blocks, emptyStateItem: nil, crossfadeState: crossfade, animateChanges: false)
return (controllerState, (listState, arguments))
}
|> afterDisposed {
timer.invalidate()
actionsDisposable.dispose()
}
let controller = ItemListController(context: context, state: signal)
controller.willDisappear = { _ in
dismissTooltipsImpl?()
}
controller.didDisappear = { [weak controller] _ in
controller?.clearItemNodesHighlight(animated: true)
}
@ -727,6 +736,19 @@ public func inviteLinkListController(context: AccountContext, peerId: PeerId, ad
getControllerImpl = { [weak controller] in
return controller
}
dismissTooltipsImpl = { [weak controller] in
controller?.window?.forEachController({ controller in
if let controller = controller as? UndoOverlayController {
controller.dismissWithCommitAction()
}
})
controller?.forEachController({ controller in
if let controller = controller as? UndoOverlayController {
controller.dismissWithCommitAction()
}
return true
})
}
return controller
}
@ -739,11 +761,11 @@ final class InviteLinkContextExtractedContentSource: ContextExtractedContentSour
private let controller: ViewController
private let sourceNode: ContextExtractedContentContainingNode
init(controller: ViewController, sourceNode: ContextExtractedContentContainingNode) {
init(controller: ViewController, sourceNode: ContextExtractedContentContainingNode, blurBackground: Bool) {
self.controller = controller
self.sourceNode = sourceNode
self.keepInPlace = true
self.blurBackground = false
self.blurBackground = blurBackground
}
func takeView() -> ContextControllerTakeViewInfo? {

View file

@ -193,7 +193,7 @@ private func preparedTransition(from fromEntries: [InviteLinkViewEntry], to toEn
private let titleFont = Font.bold(17.0)
private let subtitleFont = Font.with(size: 13, design: .regular, weight: .regular, traits: .monospacedNumbers)
private func textForTimeout(value: Int32) -> String {
func textForTimeout(value: Int32) -> String {
if value < 3600 {
let minutes = value / 60
let seconds = value % 60
@ -283,6 +283,8 @@ public final class InviteLinkViewController: ViewController {
self.isDismissed = true
self.didAppearOnce = false
self.dismissAllTooltips()
self.controllerNode.animateOut(completion: { [weak self] in
completion?()
self?.dismiss(animated: false)
@ -290,6 +292,20 @@ public final class InviteLinkViewController: ViewController {
}
}
private func dismissAllTooltips() {
self.window?.forEachController({ controller in
if let controller = controller as? UndoOverlayController {
controller.dismissWithCommitAction()
}
})
self.forEachController({ controller in
if let controller = controller as? UndoOverlayController {
controller.dismissWithCommitAction()
}
return true
})
}
override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
super.containerLayoutUpdated(layout, transition: transition)
@ -398,6 +414,8 @@ public final class InviteLinkViewController: ViewController {
}, copyLink: { [weak self] invite in
UIPasteboard.general.string = invite.link
self?.controller?.dismissAllTooltips()
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
self?.controller?.present(UndoOverlayController(presentationData: presentationData, content: .linkCopied(text: presentationData.strings.InviteLink_InviteLinkCopiedText), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .window(.root))
}, shareLink: { [weak self] invite in
@ -418,6 +436,8 @@ public final class InviteLinkViewController: ViewController {
UIPasteboard.general.string = invite.link
self?.controller?.dismissAllTooltips()
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
self?.controller?.present(UndoOverlayController(presentationData: presentationData, content: .linkCopied(text: presentationData.strings.InviteLink_InviteLinkCopiedText), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .window(.root))
})))
@ -458,10 +478,8 @@ public final class InviteLinkViewController: ViewController {
self?.controller?.present(controller, in: .window(.root))
})))
}
let contextController = ContextController(account: context.account, presentationData: presentationData, source: .extracted(InviteLinkContextExtractedContentSource(controller: controller, sourceNode: node)), items: .single(items), reactionItems: [], gesture: gesture)
let contextController = ContextController(account: context.account, presentationData: presentationData, source: .extracted(InviteLinkContextExtractedContentSource(controller: controller, sourceNode: node, blurBackground: false)), items: .single(items), reactionItems: [], gesture: gesture)
self?.controller?.presentInGlobalOverlay(contextController)
})

View file

@ -13,38 +13,6 @@ private let itemSpacing: CGFloat = 10.0
private let titleFont = Font.semibold(17.0)
private let subtitleFont = Font.regular(12.0)
private func generateBackgroundImage(colors: NSArray) -> UIImage? {
return generateImage(CGSize(width: 45, height: 45), contextGenerator: { size, context in
let bounds = CGRect(origin: CGPoint(), size: size)
context.clear(bounds)
let path = UIBezierPath(roundedRect: CGRect(origin: CGPoint(), size: size), cornerRadius: 15)
context.addPath(path.cgPath)
context.clip()
var locations: [CGFloat] = [0.0, 1.0]
let colorSpace = CGColorSpaceCreateDeviceRGB()
let gradient = CGGradient(colorsSpace: colorSpace, colors: colors, locations: &locations)!
context.drawLinearGradient(gradient, start: CGPoint(), end: CGPoint(x: 0.0, y: bounds.size.height), options: CGGradientDrawingOptions())
})?.stretchableImage(withLeftCapWidth: 22, topCapHeight: 22)
}
func invitationAvailability(_ invite: ExportedInvitation) -> CGFloat {
let currentTime = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970)
var availability: CGFloat = 1.0
if let expireDate = invite.expireDate {
let startDate = invite.startDate ?? invite.date
let fraction = CGFloat(expireDate - currentTime) / CGFloat(expireDate - startDate)
availability = min(fraction, availability)
}
if let usageLimit = invite.usageLimit, let count = invite.count {
let fraction = 1.0 - (CGFloat(count) / CGFloat(usageLimit))
availability = min(fraction, availability)
}
return max(0.0, min(1.0, availability))
}
private enum ItemBackgroundColor: Equatable {
case blue
case green

View file

@ -6,6 +6,7 @@ import SwiftSignalKit
import SyncCore
import TelegramPresentationData
import ItemListUI
import DatePickerNode
public class ItemListDatePickerItem: ListViewItem, ItemListItem {
let presentationData: ItemListPresentationData
@ -73,7 +74,7 @@ public class ItemListDatePickerItemNode: ListViewItemNode, ItemListItemNode {
private let bottomStripeNode: ASDisplayNode
private let maskNode: ASImageNode
private var datePicker: UIDatePicker?
private var datePickerNode: DatePickerNode?
private var item: ItemListDatePickerItem?
@ -101,29 +102,13 @@ public class ItemListDatePickerItemNode: ListViewItemNode, ItemListItemNode {
super.init(layerBacked: false, dynamicBounce: false)
}
public override func didLoad() {
super.didLoad()
let datePicker = UIDatePicker()
datePicker.minimumDate = Date()
datePicker.datePickerMode = .dateAndTime
if #available(iOS 14.0, *) {
datePicker.preferredDatePickerStyle = .inline
}
datePicker.addTarget(self, action: #selector(self.datePickerUpdated), for: .valueChanged)
self.view.addSubview(datePicker)
self.datePicker = datePicker
}
@objc private func datePickerUpdated() {
guard let datePicker = self.datePicker else {
return
}
self.item?.updated?(Int32(datePicker.date.timeIntervalSince1970))
}
// @objc private func datePickerUpdated() {
// guard let datePicker = self.datePicker else {
// return
// }
// self.item?.updated?(Int32(datePicker.date.timeIntervalSince1970))
// }
public func asyncLayout() -> (_ item: ItemListDatePickerItem, _ params: ListViewItemLayoutParams, _ insets: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) {
let currentItem = self.item
@ -166,10 +151,28 @@ public class ItemListDatePickerItemNode: ListViewItemNode, ItemListItemNode {
strongSelf.topStripeNode.backgroundColor = itemSeparatorColor
strongSelf.bottomStripeNode.backgroundColor = itemSeparatorColor
strongSelf.backgroundNode.backgroundColor = itemBackgroundColor
strongSelf.datePickerNode?.updateTheme(DatePickerTheme(theme: item.presentationData.theme))
}
let datePickerNode: DatePickerNode
if let current = strongSelf.datePickerNode {
datePickerNode = current
} else {
datePickerNode = DatePickerNode(theme: DatePickerTheme(theme: item.presentationData.theme), strings: item.presentationData.strings)
strongSelf.addSubnode(datePickerNode)
strongSelf.datePickerNode = datePickerNode
}
datePickerNode.valueUpdated = { date in
strongSelf.item?.updated?(Int32(date.timeIntervalSince1970))
}
strongSelf.datePicker?.date = item.date.flatMap { Date(timeIntervalSince1970: TimeInterval($0)) } ?? Date()
strongSelf.datePicker?.frame = CGRect(origin: CGPoint(x: 16.0, y: 3.0), size: CGSize(width: contentSize.width - 32.0, height: contentSize.height))
datePickerNode.minimumDate = Date()
datePickerNode.date = item.date.flatMap { Date(timeIntervalSince1970: TimeInterval($0)) } ?? Date()
let datePickerSize = CGSize(width: contentSize.width - params.leftInset - params.rightInset, height: contentSize.height)
datePickerNode.frame = CGRect(origin: CGPoint(x: params.leftInset, y: 0.0), size: datePickerSize)
datePickerNode.updateLayout(size: datePickerSize, transition: .immediate)
switch item.style {
case .plain:

View file

@ -0,0 +1,827 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import SwiftSignalKit
import SyncCore
import TelegramPresentationData
import ItemListUI
func invitationAvailability(_ invite: ExportedInvitation) -> CGFloat {
if invite.isRevoked {
return 0.0
}
let currentTime = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970)
var availability: CGFloat = 1.0
if let expireDate = invite.expireDate {
let startDate = invite.startDate ?? invite.date
let fraction = CGFloat(expireDate - currentTime) / CGFloat(expireDate - startDate)
availability = min(fraction, availability)
}
if let usageLimit = invite.usageLimit, let count = invite.count {
let fraction = 1.0 - (CGFloat(count) / CGFloat(usageLimit))
availability = min(fraction, 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: 0xa8b2bb), UIColor(rgb: 0xa2abb4), UIColor(rgb: 0xe3e6e8))
}
}
}
public class ItemListInviteLinkItem: ListViewItem, ItemListItem {
let presentationData: ItemListPresentationData
let invite: ExportedInvitation?
let share: Bool
public let sectionId: ItemListSectionId
let style: ItemListStyle
let tapAction: ((ExportedInvitation) -> Void)?
let contextAction: ((ExportedInvitation, ASDisplayNode, ContextGesture?) -> Void)?
public let tag: ItemListItemTag?
public init(
presentationData: ItemListPresentationData,
invite: ExportedInvitation?,
share: Bool,
sectionId: ItemListSectionId,
style: ItemListStyle,
tapAction: ((ExportedInvitation) -> Void)?,
contextAction: ((ExportedInvitation, ASDisplayNode, ContextGesture?) -> Void)?,
tag: ItemListItemTag? = nil
) {
self.presentationData = presentationData
self.invite = invite
self.share = share
self.sectionId = sectionId
self.style = style
self.tapAction = tapAction
self.contextAction = contextAction
self.tag = tag
}
public 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 {
var firstWithHeader = false
var last = false
if self.style == .plain {
if previousItem == nil {
firstWithHeader = true
}
if nextItem == nil {
last = true
}
}
let node = ItemListInviteLinkItemNode()
let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem), firstWithHeader, last)
node.contentSize = layout.contentSize
node.insets = layout.insets
Queue.mainQueue().async {
completion(node, {
return (nil, { _ in apply() })
})
}
}
}
public 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 {
if let nodeValue = node() as? ItemListInviteLinkItemNode {
let makeLayout = nodeValue.asyncLayout()
async {
var firstWithHeader = false
var last = false
if self.style == .plain {
if previousItem == nil {
firstWithHeader = true
}
if nextItem == nil {
last = true
}
}
let (layout, apply) = makeLayout(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem), firstWithHeader, last)
Queue.mainQueue().async {
completion(layout, { _ in
apply()
})
}
}
}
}
}
public var selectable: Bool = true
public func selected(listView: ListView) {
listView.clearHighlightAnimated(true)
if let invite = self.invite {
self.tapAction?(invite)
}
}
}
public class ItemListInviteLinkItemNode: ListViewItemNode, ItemListItemNode {
private let backgroundNode: ASDisplayNode
private let topStripeNode: ASDisplayNode
private let bottomStripeNode: ASDisplayNode
private let highlightedBackgroundNode: ASDisplayNode
private let maskNode: ASImageNode
private let extractedBackgroundImageNode: ASImageNode
private let containerNode: ContextControllerSourceNode
private let contextSourceNode: ContextExtractedContentContainingNode
private var extractedRect: CGRect?
private var nonExtractedRect: CGRect?
private let offsetContainerNode: ASDisplayNode
private let iconBackgroundNode: ASDisplayNode
private let iconNode: ASImageNode
private var timerNode: TimerNode?
private let titleNode: TextNode
private let subtitleNode: TextNode
private var currentColor: ItemBackgroundColor?
private var layoutParams: (ItemListInviteLinkItem, ListViewItemLayoutParams, ItemListNeighbors, Bool, Bool)?
public var tag: ItemListItemTag?
public init() {
self.backgroundNode = ASDisplayNode()
self.backgroundNode.isLayerBacked = true
self.topStripeNode = ASDisplayNode()
self.topStripeNode.isLayerBacked = true
self.bottomStripeNode = ASDisplayNode()
self.bottomStripeNode.isLayerBacked = true
self.maskNode = ASImageNode()
self.extractedBackgroundImageNode = ASImageNode()
self.extractedBackgroundImageNode.displaysAsynchronously = false
self.extractedBackgroundImageNode.alpha = 0.0
self.contextSourceNode = ContextExtractedContentContainingNode()
self.containerNode = ContextControllerSourceNode()
self.offsetContainerNode = ASDisplayNode()
self.iconBackgroundNode = ASImageNode()
self.iconBackgroundNode.setLayerBlock { () -> CALayer in
return CAShapeLayer()
}
self.iconNode = ASImageNode()
self.iconNode.displaysAsynchronously = false
self.iconNode.displayWithoutProcessing = true
self.iconNode.contentMode = .center
self.titleNode = TextNode()
self.titleNode.isUserInteractionEnabled = false
self.titleNode.contentMode = .left
self.titleNode.contentsScale = UIScreen.main.scale
self.subtitleNode = TextNode()
self.subtitleNode.isUserInteractionEnabled = false
self.subtitleNode.contentMode = .left
self.subtitleNode.contentsScale = UIScreen.main.scale
self.highlightedBackgroundNode = ASDisplayNode()
self.highlightedBackgroundNode.isLayerBacked = true
super.init(layerBacked: false, dynamicBounce: false, rotated: false, seeThrough: false)
self.isAccessibilityElement = true
self.containerNode.addSubnode(self.contextSourceNode)
self.containerNode.targetNodeForActivationProgress = self.contextSourceNode.contentNode
self.addSubnode(self.containerNode)
self.contextSourceNode.contentNode.addSubnode(self.extractedBackgroundImageNode)
self.contextSourceNode.contentNode.addSubnode(self.offsetContainerNode)
self.offsetContainerNode.addSubnode(self.iconBackgroundNode)
self.offsetContainerNode.addSubnode(self.iconNode)
self.offsetContainerNode.addSubnode(self.titleNode)
self.offsetContainerNode.addSubnode(self.subtitleNode)
self.containerNode.activated = { [weak self] gesture, _ in
guard let strongSelf = self, let item = strongSelf.layoutParams?.0, let invite = item.invite, let contextAction = item.contextAction else {
gesture.cancel()
return
}
contextAction(invite, strongSelf.contextSourceNode, gesture)
}
self.contextSourceNode.willUpdateIsExtractedToContextPreview = { [weak self] isExtracted, transition in
guard let strongSelf = self, let item = strongSelf.layoutParams?.0 else {
return
}
if isExtracted {
strongSelf.extractedBackgroundImageNode.image = generateStretchableFilledCircleImage(diameter: 28.0, color: item.presentationData.theme.list.plainBackgroundColor)
}
if let extractedRect = strongSelf.extractedRect, let nonExtractedRect = strongSelf.nonExtractedRect {
let rect = isExtracted ? extractedRect : nonExtractedRect
transition.updateFrame(node: strongSelf.extractedBackgroundImageNode, frame: rect)
}
transition.updateSublayerTransformOffset(layer: strongSelf.offsetContainerNode.layer, offset: CGPoint(x: isExtracted ? 12.0 : 0.0, y: 0.0))
transition.updateAlpha(node: strongSelf.extractedBackgroundImageNode, alpha: isExtracted ? 1.0 : 0.0, completion: { _ in
if !isExtracted {
self?.extractedBackgroundImageNode.image = nil
}
})
}
}
public override func didLoad() {
super.didLoad()
if let shapeLayer = self.iconBackgroundNode.layer as? CAShapeLayer {
shapeLayer.path = UIBezierPath(ovalIn: CGRect(x: 0.0, y: 0.0, width: 40.0, height: 40.0)).cgPath
}
}
public func asyncLayout() -> (_ item: ItemListInviteLinkItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors, _ firstWithHeader: Bool, _ last: Bool) -> (ListViewItemNodeLayout, () -> Void) {
let makeTitleLayout = TextNode.asyncLayout(self.titleNode)
let makeSubtitleLayout = TextNode.asyncLayout(self.subtitleNode)
let currentItem = self.layoutParams?.0
return { item, params, neighbors, firstWithHeader, last in
var updatedTheme: PresentationTheme?
let titleFont = Font.regular(item.presentationData.fontSize.itemListBaseFontSize)
let subtitleFont = Font.regular(floor(item.presentationData.fontSize.itemListBaseFontSize * 14.0 / 17.0))
if currentItem?.presentationData.theme !== item.presentationData.theme {
updatedTheme = item.presentationData.theme
}
let currentTime = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970)
let availability = item.invite.flatMap { invitationAvailability($0) } ?? 0.0
let color: ItemBackgroundColor
let nextColor: ItemBackgroundColor
let transitionFraction: CGFloat
if let invite = item.invite {
if invite.isRevoked {
color = .gray
nextColor = .gray
transitionFraction = 0.0
} else if invite.expireDate == nil && invite.usageLimit == nil {
color = .blue
nextColor = .blue
transitionFraction = 0.0
} else if availability >= 0.5 {
color = .green
nextColor = .yellow
transitionFraction = (availability - 0.5) / 0.5
} else if availability > 0.0 {
color = .yellow
nextColor = .red
transitionFraction = availability / 0.5
} else {
color = .red
nextColor = .red
transitionFraction = 0.0
}
} else {
color = .gray
nextColor = .gray
transitionFraction = 0.0
}
let topColor = color.colors.top
let nextTopColor = nextColor.colors.top
let iconColor: UIColor
if let _ = item.invite {
if case .blue = color {
iconColor = item.presentationData.theme.list.itemAccentColor
} else {
iconColor = nextTopColor.mixedWith(topColor, alpha: transitionFraction)
}
} else {
iconColor = item.presentationData.theme.list.mediaPlaceholderColor
}
let inviteLink = item.invite?.link.replacingOccurrences(of: "https://", with: "") ?? ""
var titleText = inviteLink
var subtitleText: String = ""
var timerValue: TimerNode.Value?
if let invite = item.invite {
let count = invite.count ?? 0
if count > 0 {
subtitleText = item.presentationData.strings.InviteLink_PeopleJoinedShort(count)
} else {
if let usageLimit = invite.usageLimit, count == 0 {
subtitleText = item.presentationData.strings.InviteLink_PeopleCanJoin(usageLimit)
} else {
subtitleText = availability.isZero ? item.presentationData.strings.InviteLink_PeopleJoinedShortNoneExpired : item.presentationData.strings.InviteLink_PeopleJoinedShortNone
}
}
if invite.isRevoked {
if !subtitleText.isEmpty {
subtitleText += ""
}
subtitleText += item.presentationData.strings.InviteLink_Revoked
} else {
var isExpired = false
if let expireDate = invite.expireDate, currentTime >= expireDate {
isExpired = true
}
if let usageLimit = invite.usageLimit {
if !isExpired {
let remaining = usageLimit - count
if remaining > 0 && remaining != usageLimit {
subtitleText += ", "
subtitleText += item.presentationData.strings.InviteLink_PeopleRemaining(remaining)
let fraction = CGFloat(remaining) / CGFloat(usageLimit)
if abs(fraction - availability) < 0.0001 {
timerValue = .fraction(fraction)
}
} else if remaining == 0 {
if !subtitleText.isEmpty {
subtitleText += ""
}
subtitleText += item.presentationData.strings.InviteLink_UsageLimitReached
}
}
}
if let expireDate = invite.expireDate {
if !isExpired {
if !subtitleText.isEmpty {
subtitleText += ""
}
let elapsedTime = expireDate - currentTime
if elapsedTime >= 86400 {
subtitleText += item.presentationData.strings.InviteLink_ExpiresIn(timeIntervalString(strings: item.presentationData.strings, value: elapsedTime)).0
} else {
subtitleText += item.presentationData.strings.InviteLink_ExpiresIn(textForTimeout(value: elapsedTime)).0
}
if timerValue == nil {
timerValue = .timestamp(creation: invite.startDate ?? invite.date, deadline: expireDate)
}
} else {
if !subtitleText.isEmpty {
subtitleText += ""
}
subtitleText += item.presentationData.strings.InviteLink_Expired
}
}
}
} else {
titleText = " "
subtitleText = " "
self.iconNode.isHidden = true
}
let titleAttributedString = NSAttributedString(string: titleText, font: titleFont, textColor: item.presentationData.theme.list.itemPrimaryTextColor)
let subtitleAttributedString = NSAttributedString(string: subtitleText, font: subtitleFont, textColor: item.presentationData.theme.list.itemSecondaryTextColor)
let leftInset: CGFloat = 65.0 + params.leftInset
let rightInset: CGFloat = 16.0 + params.rightInset
let verticalInset: CGFloat = subtitleAttributedString.string.isEmpty ? 14.0 : 8.0
let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: titleAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - rightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
let (subtitleLayout, subtitleApply) = makeSubtitleLayout(TextNodeLayoutArguments(attributedString: subtitleAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - rightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
let titleSpacing: CGFloat = 1.0
let minHeight: CGFloat = titleLayout.size.height + verticalInset * 2.0
let rawHeight: CGFloat = verticalInset * 2.0 + titleLayout.size.height + titleSpacing + subtitleLayout.size.height
var insets: UIEdgeInsets
let itemBackgroundColor: UIColor
let itemSeparatorColor: UIColor
switch item.style {
case .plain:
itemBackgroundColor = item.presentationData.theme.list.plainBackgroundColor
itemSeparatorColor = item.presentationData.theme.list.itemPlainSeparatorColor
insets = itemListNeighborsPlainInsets(neighbors)
insets.top = firstWithHeader ? 29.0 : 0.0
insets.bottom = 0.0
case .blocks:
itemBackgroundColor = item.presentationData.theme.list.itemBlocksBackgroundColor
itemSeparatorColor = item.presentationData.theme.list.itemBlocksSeparatorColor
insets = itemListNeighborsGroupedInsets(neighbors)
}
let contentSize = CGSize(width: params.width, height: max(minHeight, rawHeight))
let separatorHeight = UIScreenPixel
let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets)
return (layout, { [weak self] in
if let strongSelf = self {
strongSelf.layoutParams = (item, params, neighbors, firstWithHeader, last)
strongSelf.accessibilityLabel = titleAttributedString.string
strongSelf.accessibilityValue = subtitleAttributedString.string
strongSelf.containerNode.frame = CGRect(origin: CGPoint(), size: layout.contentSize)
strongSelf.contextSourceNode.frame = CGRect(origin: CGPoint(), size: layout.contentSize)
strongSelf.offsetContainerNode.frame = CGRect(origin: CGPoint(), size: layout.contentSize)
strongSelf.contextSourceNode.contentNode.frame = CGRect(origin: CGPoint(), size: layout.contentSize)
strongSelf.containerNode.isGestureEnabled = item.contextAction != nil
let nonExtractedRect = CGRect(origin: CGPoint(), size: CGSize(width: layout.contentSize.width - 16.0, height: layout.contentSize.height))
let extractedRect = CGRect(origin: CGPoint(), size: layout.contentSize).insetBy(dx: 16.0 + params.leftInset, dy: 0.0)
strongSelf.extractedRect = extractedRect
strongSelf.nonExtractedRect = nonExtractedRect
if strongSelf.contextSourceNode.isExtractedToContextPreview {
strongSelf.extractedBackgroundImageNode.frame = extractedRect
} else {
strongSelf.extractedBackgroundImageNode.frame = nonExtractedRect
}
strongSelf.contextSourceNode.contentRect = extractedRect
if let layer = strongSelf.iconBackgroundNode.layer as? CAShapeLayer {
layer.fillColor = iconColor.cgColor
}
if let _ = updatedTheme {
strongSelf.topStripeNode.backgroundColor = itemSeparatorColor
strongSelf.bottomStripeNode.backgroundColor = itemSeparatorColor
strongSelf.backgroundNode.backgroundColor = itemBackgroundColor
strongSelf.highlightedBackgroundNode.backgroundColor = item.presentationData.theme.list.itemHighlightedBackgroundColor
strongSelf.iconNode.image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Link"), color: item.presentationData.theme.list.itemCheckColors.foregroundColor)
}
let transition = ContainedViewLayoutTransition.immediate
let _ = titleApply()
let _ = subtitleApply()
switch item.style {
case .plain:
if strongSelf.backgroundNode.supernode != nil {
strongSelf.backgroundNode.removeFromSupernode()
}
if strongSelf.topStripeNode.supernode != nil {
strongSelf.topStripeNode.removeFromSupernode()
}
if strongSelf.bottomStripeNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 0)
}
if strongSelf.maskNode.supernode != nil {
strongSelf.maskNode.removeFromSupernode()
}
let stripeInset: CGFloat
if case .none = neighbors.bottom {
stripeInset = 0.0
} else {
stripeInset = leftInset
}
strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: stripeInset, y: contentSize.height - separatorHeight), size: CGSize(width: params.width - stripeInset, height: separatorHeight))
strongSelf.bottomStripeNode.isHidden = last
case .blocks:
if strongSelf.backgroundNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.backgroundNode, at: 0)
}
if strongSelf.topStripeNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.topStripeNode, at: 1)
}
if strongSelf.bottomStripeNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 2)
}
if strongSelf.maskNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.maskNode, at: 3)
}
let hasCorners = itemListHasRoundedBlockLayout(params)
var hasTopCorners = false
var hasBottomCorners = false
switch neighbors.top {
case .sameSection(false):
strongSelf.topStripeNode.isHidden = true
default:
hasTopCorners = true
strongSelf.topStripeNode.isHidden = hasCorners
}
let bottomStripeInset: CGFloat
switch neighbors.bottom {
case .sameSection(false):
bottomStripeInset = leftInset
default:
bottomStripeInset = 0.0
hasBottomCorners = true
strongSelf.bottomStripeNode.isHidden = hasCorners
}
strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.presentationData.theme, top: hasTopCorners, bottom: hasBottomCorners) : nil
strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight)))
strongSelf.maskNode.frame = strongSelf.backgroundNode.frame.insetBy(dx: params.leftInset, dy: 0.0)
strongSelf.topStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: separatorHeight))
strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height - separatorHeight), size: CGSize(width: params.width - bottomStripeInset, height: separatorHeight))
}
let iconSize: CGSize = CGSize(width: 40.0, height: 40.0)
let iconFrame = CGRect(origin: CGPoint(x: params.leftInset + 12.0, y: floorToScreenPixels((layout.contentSize.height - iconSize.height) / 2.0)), size: iconSize)
strongSelf.iconBackgroundNode.bounds = CGRect(origin: CGPoint(), size: iconSize)
strongSelf.iconBackgroundNode.position = iconFrame.center
strongSelf.iconNode.frame = iconFrame
transition.updateTransformScale(node: strongSelf.iconBackgroundNode, scale: timerValue != nil ? 0.875 : 1.0)
if let timerValue = timerValue {
let timerNode: TimerNode
if let current = strongSelf.timerNode {
timerNode = current
} else {
timerNode = TimerNode()
timerNode.isUserInteractionEnabled = false
strongSelf.timerNode = timerNode
strongSelf.addSubnode(timerNode)
}
timerNode.update(color: iconColor, value: timerValue)
} else if let timerNode = strongSelf.timerNode {
strongSelf.timerNode = nil
timerNode.removeFromSupernode()
}
strongSelf.timerNode?.frame = iconFrame.insetBy(dx: -5.0, dy: -5.0)
transition.updateFrame(node: strongSelf.titleNode, frame: CGRect(origin: CGPoint(x: leftInset, y: verticalInset), size: titleLayout.size))
transition.updateFrame(node: strongSelf.subtitleNode, frame: CGRect(origin: CGPoint(x: leftInset, y: verticalInset + titleLayout.size.height + titleSpacing), size: subtitleLayout.size))
strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: params.width, height: contentSize.height + UIScreenPixel + UIScreenPixel))
}
})
}
}
override public func setHighlighted(_ highlighted: Bool, at point: CGPoint, animated: Bool) {
super.setHighlighted(highlighted, at: point, animated: animated)
if highlighted {
self.highlightedBackgroundNode.alpha = 1.0
if self.highlightedBackgroundNode.supernode == nil {
var anchorNode: ASDisplayNode?
if self.bottomStripeNode.supernode != nil {
anchorNode = self.bottomStripeNode
} else if self.topStripeNode.supernode != nil {
anchorNode = self.topStripeNode
} else if self.backgroundNode.supernode != nil {
anchorNode = self.backgroundNode
}
if let anchorNode = anchorNode {
self.insertSubnode(self.highlightedBackgroundNode, aboveSubnode: anchorNode)
} else {
self.addSubnode(self.highlightedBackgroundNode)
}
}
} else {
if self.highlightedBackgroundNode.supernode != nil {
if animated {
self.highlightedBackgroundNode.layer.animateAlpha(from: self.highlightedBackgroundNode.alpha, to: 0.0, duration: 0.4, completion: { [weak self] completed in
if let strongSelf = self {
if completed {
strongSelf.highlightedBackgroundNode.removeFromSupernode()
}
}
})
self.highlightedBackgroundNode.alpha = 0.0
} else {
self.highlightedBackgroundNode.removeFromSupernode()
}
}
}
}
override public func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) {
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4)
}
override public func animateRemoved(_ currentTimestamp: Double, duration: Double) {
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false)
}
@objc private func infoPressed() {
// self.item?.infoAction?()
}
}
private struct ContentParticle {
var position: CGPoint
var direction: CGPoint
var velocity: CGFloat
var alpha: CGFloat
var lifetime: Double
var beginTime: Double
init(position: CGPoint, direction: CGPoint, velocity: CGFloat, alpha: CGFloat, lifetime: Double, beginTime: Double) {
self.position = position
self.direction = direction
self.velocity = velocity
self.alpha = alpha
self.lifetime = lifetime
self.beginTime = beginTime
}
}
private final class TimerNode: ASDisplayNode {
enum Value: Equatable {
case timestamp(creation: Int32, deadline: Int32)
case fraction(CGFloat)
}
private struct Params: Equatable {
var color: UIColor
var value: Value
}
private let hierarchyTrackingNode: HierarchyTrackingNode
private var inHierarchyValue: Bool = false
private var animator: ConstantDisplayLinkAnimator?
private let contentNode: ASDisplayNode
private var particles: [ContentParticle] = []
private var currentParams: Params?
var reachedTimeout: (() -> Void)?
override init() {
var updateInHierarchy: ((Bool) -> Void)?
self.hierarchyTrackingNode = HierarchyTrackingNode({ value in
updateInHierarchy?(value)
})
self.contentNode = ASDisplayNode()
super.init()
self.addSubnode(self.contentNode)
updateInHierarchy = { [weak self] value in
guard let strongSelf = self else {
return
}
strongSelf.inHierarchyValue = value
strongSelf.animator?.isPaused = value
}
}
deinit {
self.animator?.invalidate()
}
func update(color: UIColor, value: Value) {
let params = Params(
color: color,
value: value
)
self.currentParams = params
self.updateValues()
}
private func updateValues() {
guard let params = self.currentParams else {
return
}
let color = params.color
let currentTimestamp = Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970)
var fraction: CGFloat
switch params.value {
case let .fraction(value):
fraction = value
case let .timestamp(creation, deadline):
fraction = CGFloat(deadline - currentTimestamp) / CGFloat(deadline - creation)
}
fraction = max(0.0001, 1.0 - max(0.0, min(1.0, fraction)))
let image: UIImage?
let diameter: CGFloat = 42.0
let inset: CGFloat = 8.0
let lineWidth: CGFloat = 2.0
let timestamp = CACurrentMediaTime()
let center = CGPoint(x: (diameter + inset) / 2.0, y: (diameter + inset) / 2.0)
let radius: CGFloat = (diameter - lineWidth / 2.0) / 2.0
let startAngle: CGFloat = -CGFloat.pi / 2.0
let endAngle: CGFloat = -CGFloat.pi / 2.0 + 2.0 * CGFloat.pi * fraction
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
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
context.clear(CGRect(origin: CGPoint(), size: size))
context.setStrokeColor(color.cgColor)
context.setFillColor(color.cgColor)
context.setLineWidth(lineWidth)
context.setLineCap(.round)
let path = CGMutablePath()
path.addArc(center: center, radius: radius, startAngle: startAngle, endAngle: endAngle, clockwise: true)
context.addPath(path)
context.strokePath()
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)))
}
}
})
self.contentNode.contents = image?.cgImage
if let image = image {
self.contentNode.frame = CGRect(origin: CGPoint(), size: image.size)
}
if fraction <= .ulpOfOne {
self.animator?.invalidate()
self.animator = nil
} else {
if self.animator == nil {
let animator = ConstantDisplayLinkAnimator(update: { [weak self] in
self?.updateValues()
})
self.animator = animator
animator.isPaused = self.inHierarchyValue
}
}
}
}

View file

@ -442,8 +442,8 @@ public class ItemListPermanentInviteLinkItemNode: ListViewItemNode, ItemListItem
}
shareButtonNode = SolidRoundedButtonNode(theme: buttonTheme, height: 50.0, cornerRadius: 10.0)
shareButtonNode.title = item.presentationData.strings.InviteLink_Share
shareButtonNode.pressed = {
item.shareAction?()
shareButtonNode.pressed = { [weak self] in
self?.item?.shareAction?()
}
strongSelf.addSubnode(shareButtonNode)
strongSelf.shareButtonNode = shareButtonNode

View file

@ -33,7 +33,7 @@ public final class ItemListEditableReorderControlNode: ASDisplayNode {
return (40.0, { height, offsetForLabel, transition in
if let image = image {
transition.updateFrame(node: resultNode.iconNode, frame: CGRect(origin: CGPoint(x: 7.0, y: floor((height - image.size.height) / 2.0) - (offsetForLabel ? 6.0 : 0.0)), size: image.size))
transition.updateFrame(node: resultNode.iconNode, frame: CGRect(origin: CGPoint(x: 0.0, y: floor((height - image.size.height) / 2.0) - (offsetForLabel ? 6.0 : 0.0)), size: image.size))
}
return resultNode
})

View file

@ -71,11 +71,6 @@ public final class ItemListVenueItem: ListViewItem, ItemListItem {
if let nodeValue = node() as? ItemListVenueItemNode {
let makeLayout = nodeValue.asyncLayout()
var animated = true
if case .None = animation {
animated = false
}
async {
var firstWithHeader = false
var last = false

View file

@ -29,9 +29,11 @@ private final class ChannelPermissionsControllerArguments {
let openPeerInfo: (Peer) -> Void
let openKicked: () -> Void
let presentRestrictedPermissionAlert: (TelegramChatBannedRightsFlags) -> Void
let presentConversionToChannel: () -> Void
let openChannelExample: () -> Void
let updateSlowmode: (Int32) -> Void
init(context: AccountContext, updatePermission: @escaping (TelegramChatBannedRightsFlags, Bool) -> Void, setPeerIdWithRevealedOptions: @escaping (PeerId?, PeerId?) -> Void, addPeer: @escaping () -> Void, removePeer: @escaping (PeerId) -> Void, openPeer: @escaping (ChannelParticipant) -> Void, openPeerInfo: @escaping (Peer) -> Void, openKicked: @escaping () -> Void, presentRestrictedPermissionAlert: @escaping (TelegramChatBannedRightsFlags) -> Void, updateSlowmode: @escaping (Int32) -> Void) {
init(context: AccountContext, updatePermission: @escaping (TelegramChatBannedRightsFlags, Bool) -> Void, setPeerIdWithRevealedOptions: @escaping (PeerId?, PeerId?) -> Void, addPeer: @escaping () -> Void, removePeer: @escaping (PeerId) -> Void, openPeer: @escaping (ChannelParticipant) -> Void, openPeerInfo: @escaping (Peer) -> Void, openKicked: @escaping () -> Void, presentRestrictedPermissionAlert: @escaping (TelegramChatBannedRightsFlags) -> Void, presentConversionToChannel: @escaping () -> Void, openChannelExample: @escaping () -> Void, updateSlowmode: @escaping (Int32) -> Void) {
self.context = context
self.updatePermission = updatePermission
self.addPeer = addPeer
@ -41,6 +43,8 @@ private final class ChannelPermissionsControllerArguments {
self.openPeerInfo = openPeerInfo
self.openKicked = openKicked
self.presentRestrictedPermissionAlert = presentRestrictedPermissionAlert
self.presentConversionToChannel = presentConversionToChannel
self.openChannelExample = openChannelExample
self.updateSlowmode = updateSlowmode
}
}
@ -48,6 +52,7 @@ private final class ChannelPermissionsControllerArguments {
private enum ChannelPermissionsSection: Int32 {
case permissions
case slowmode
case conversion
case kicked
case exceptions
}
@ -63,6 +68,9 @@ private enum ChannelPermissionsEntry: ItemListNodeEntry {
case slowmodeHeader(PresentationTheme, String)
case slowmode(PresentationTheme, PresentationStrings, Int32)
case slowmodeInfo(PresentationTheme, String)
case conversionHeader(PresentationTheme, String)
case conversion(PresentationTheme, String)
case conversionInfo(PresentationTheme, String)
case kicked(PresentationTheme, String, String)
case exceptionsHeader(PresentationTheme, String)
case add(PresentationTheme, String)
@ -74,6 +82,8 @@ private enum ChannelPermissionsEntry: ItemListNodeEntry {
return ChannelPermissionsSection.permissions.rawValue
case .slowmodeHeader, .slowmode, .slowmodeInfo:
return ChannelPermissionsSection.slowmode.rawValue
case .conversionHeader, .conversion, .conversionInfo:
return ChannelPermissionsSection.conversion.rawValue
case .kicked:
return ChannelPermissionsSection.kicked.rawValue
case .exceptionsHeader, .add, .peerItem:
@ -93,12 +103,18 @@ private enum ChannelPermissionsEntry: ItemListNodeEntry {
return .index(999)
case .slowmodeInfo:
return .index(1000)
case .kicked:
case .conversionHeader:
return .index(1001)
case .exceptionsHeader:
case .conversion:
return .index(1002)
case .add:
case .conversionInfo:
return .index(1003)
case .kicked:
return .index(1004)
case .exceptionsHeader:
return .index(1005)
case .add:
return .index(1006)
case let .peerItem(_, _, _, _, _, participant, _, _, _, _):
return .peer(participant.peer.id)
}
@ -136,6 +152,24 @@ private enum ChannelPermissionsEntry: ItemListNodeEntry {
} else {
return false
}
case let .conversionHeader(lhsTheme, lhsValue):
if case let .conversionHeader(rhsTheme, rhsValue) = rhs, lhsTheme === rhsTheme, lhsValue == rhsValue {
return true
} else {
return false
}
case let .conversion(lhsTheme, lhsText):
if case let .conversion(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
case let .conversionInfo(lhsTheme, lhsValue):
if case let .conversionInfo(rhsTheme, rhsValue) = rhs, lhsTheme === rhsTheme, lhsValue == rhsValue {
return true
} else {
return false
}
case let .kicked(lhsTheme, lhsText, lhsValue):
if case let .kicked(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue {
return true
@ -219,9 +253,9 @@ private enum ChannelPermissionsEntry: ItemListNodeEntry {
func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem {
let arguments = arguments as! ChannelPermissionsControllerArguments
switch self {
case let .permissionsHeader(theme, text):
case let .permissionsHeader(_, text):
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
case let .permission(theme, _, title, value, rights, enabled):
case let .permission(_, _, title, value, rights, enabled):
return ItemListSwitchItem(presentationData: presentationData, title: title, value: value, type: .icon, enableInteractiveChanges: enabled != nil, enabled: enabled ?? true, sectionId: self.section, style: .blocks, updated: { value in
if let _ = enabled {
arguments.updatePermission(rights, value)
@ -231,25 +265,35 @@ private enum ChannelPermissionsEntry: ItemListNodeEntry {
}, activatedWhileDisabled: {
arguments.presentRestrictedPermissionAlert(rights)
})
case let .slowmodeHeader(theme, value):
case let .slowmodeHeader(_, value):
return ItemListSectionHeaderItem(presentationData: presentationData, text: value, sectionId: self.section)
case let .slowmode(theme, strings, value):
return ChatSlowmodeItem(theme: theme, strings: strings, value: value, enabled: true, sectionId: self.section, updated: { value in
arguments.updateSlowmode(value)
})
case let .slowmodeInfo(theme, value):
case let .slowmodeInfo(_, value):
return ItemListTextItem(presentationData: presentationData, text: .plain(value), sectionId: self.section)
case let .kicked(theme, text, value):
case let .conversionHeader(_, value):
return ItemListSectionHeaderItem(presentationData: presentationData, text: value, sectionId: self.section)
case let .conversion(_, text):
return ItemListActionItem(presentationData: presentationData, title: text, kind: .generic, alignment: .natural, sectionId: self.section, style: .blocks) {
arguments.presentConversionToChannel()
}
case let .conversionInfo(_, value):
return ItemListTextItem(presentationData: presentationData, text: .markdown(value), sectionId: self.section) { _ in
arguments.openChannelExample()
}
case let .kicked(_, text, value):
return ItemListDisclosureItem(presentationData: presentationData, title: text, label: value, sectionId: self.section, style: .blocks, action: {
arguments.openKicked()
})
case let .exceptionsHeader(theme, text):
case let .exceptionsHeader(_, text):
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
case let .add(theme, text):
return ItemListPeerActionItem(presentationData: presentationData, icon: PresentationResourcesItemList.addPersonIcon(theme), title: text, sectionId: self.section, editing: false, action: {
arguments.addPeer()
})
case let .peerItem(theme, strings, dateTimeFormat, nameDisplayOrder, _, participant, editing, enabled, canOpen, defaultBannedRights):
case let .peerItem(_, strings, dateTimeFormat, nameDisplayOrder, _, participant, editing, enabled, canOpen, defaultBannedRights):
var text: ItemListPeerItemText = .none
switch participant.participant {
case let .member(_, _, _, banInfo, _):
@ -421,9 +465,15 @@ private func channelPermissionsControllerEntries(presentationData: PresentationD
rightIndex += 1
}
entries.append(.slowmodeHeader(presentationData.theme, presentationData.strings.GroupInfo_Permissions_SlowmodeHeader))
entries.append(.slowmode(presentationData.theme, presentationData.strings, state.modifiedSlowmodeTimeout ?? (cachedData.slowModeTimeout ?? 0)))
entries.append(.slowmodeInfo(presentationData.theme, presentationData.strings.GroupInfo_Permissions_SlowmodeInfo))
if channel.flags.contains(.isCreator) && effectiveRightsFlags.contains(.banSendMessages) {
entries.append(.conversionHeader(presentationData.theme, presentationData.strings.GroupInfo_Permissions_BroadcastTitle.uppercased()))
entries.append(.conversion(presentationData.theme, presentationData.strings.GroupInfo_Permissions_BroadcastConvert))
entries.append(.conversionInfo(presentationData.theme, presentationData.strings.GroupInfo_Permissions_BroadcastConvertInfo))
} else {
entries.append(.slowmodeHeader(presentationData.theme, presentationData.strings.GroupInfo_Permissions_SlowmodeHeader))
entries.append(.slowmode(presentationData.theme, presentationData.strings, state.modifiedSlowmodeTimeout ?? (cachedData.slowModeTimeout ?? 0)))
entries.append(.slowmodeInfo(presentationData.theme, presentationData.strings.GroupInfo_Permissions_SlowmodeInfo))
}
entries.append(.kicked(presentationData.theme, presentationData.strings.GroupInfo_Permissions_Removed, cachedData.participantsSummary.kickedCount.flatMap({ $0 == 0 ? "" : "\($0)" }) ?? ""))
entries.append(.exceptionsHeader(presentationData.theme, presentationData.strings.GroupInfo_Permissions_Exceptions))
@ -471,11 +521,15 @@ public func channelPermissionsController(context: AccountContext, peerId origina
var presentControllerImpl: ((ViewController, Any?) -> Void)?
var pushControllerImpl: ((ViewController) -> Void)?
var navigateToChatControllerImpl: ((PeerId) -> Void)?
var dismissInputImpl: (() -> Void)?
var resetSlowmodeVisualValueImpl: (() -> Void)?
let actionsDisposable = DisposableSet()
let resolveDisposable = MetaDisposable()
actionsDisposable.add(resolveDisposable)
let updateBannedDisposable = MetaDisposable()
actionsDisposable.add(updateBannedDisposable)
@ -698,6 +752,26 @@ public func channelPermissionsController(context: AccountContext, peerId origina
}
}
})
}, presentConversionToChannel: {
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let controller = PermissionController(context: context, splashScreen: true)
controller.navigationPresentation = .modal
controller.setState(.custom(icon: .animation("Channels"), title: presentationData.strings.ChannelIntro_ChannelsTitle, subtitle: nil, text: presentationData.strings.ChannelIntro_ChannelsText, buttonTitle: presentationData.strings.ChannelIntro_ConvertToChannel, secondaryButtonTitle: presentationData.strings.Common_Cancel, footerText: nil), animated: false)
controller.proceed = { result in
presentControllerImpl?(textAlertController(context: context, title: presentationData.strings.ConvertToChannel_ConfirmationAlert_Title, text: presentationData.strings.ConvertToChannel_ConfirmationAlert_Text, actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .defaultAction, title: presentationData.strings.ConvertToChannel_ConfirmationAlert_Proceed, action: {
})]), nil)
// (strongSelf.navigationController as? NavigationController)?.replaceTopController(createChannelController(context: strongSelf.context), animated: true)
}
pushControllerImpl?(controller)
}, openChannelExample: {
resolveDisposable.set((resolvePeerByName(account: context.account, name: "durov") |> deliverOnMainQueue).start(next: { peerId in
if let peerId = peerId {
navigateToChatControllerImpl?(peerId)
}
}))
}, updateSlowmode: { value in
let _ = (peerView.get()
|> take(1)
@ -864,6 +938,11 @@ public func channelPermissionsController(context: AccountContext, peerId origina
(controller.navigationController as? NavigationController)?.pushViewController(c)
}
}
navigateToChatControllerImpl = { [weak controller] peerId in
if let controller = controller, let navigationController = controller.navigationController as? NavigationController {
context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(peerId), keepStack: .always))
}
}
dismissInputImpl = { [weak controller] in
controller?.view.endEditing(true)
}

View file

@ -21,6 +21,7 @@ import ItemListPeerActionItem
import AccountContext
import InviteLinksUI
import ContextUI
import UndoUI
private final class ChannelVisibilityControllerArguments {
let context: AccountContext
@ -837,6 +838,8 @@ public func channelVisibilityController(context: AccountContext, peerId: PeerId,
var presentInGlobalOverlayImpl: ((ViewController) -> Void)?
var getControllerImpl: (() -> ViewController?)?
var dismissTooltipsImpl: (() -> Void)?
let actionsDisposable = DisposableSet()
let checkAddressNameDisposable = MetaDisposable()
@ -908,8 +911,11 @@ public func channelVisibilityController(context: AccountContext, peerId: PeerId,
}))
}, copyLink: { invite in
UIPasteboard.general.string = invite.link
dismissTooltipsImpl?()
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
presentControllerImpl?(OverlayStatusController(theme: presentationData.theme, type: .genericSuccess(presentationData.strings.Username_LinkCopied, false)), nil)
presentControllerImpl?(UndoOverlayController(presentationData: presentationData, content: .linkCopied(text: presentationData.strings.InviteLink_InviteLinkCopiedText), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), nil)
}, shareLink: { invite in
let shareController = ShareController(context: context, subject: .url(invite.link))
presentControllerImpl?(shareController, nil)
@ -937,8 +943,11 @@ public func channelVisibilityController(context: AccountContext, peerId: PeerId,
} |> deliverOnMainQueue).start(next: { link in
if let link = link {
UIPasteboard.general.string = link
dismissTooltipsImpl?()
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
presentControllerImpl?(OverlayStatusController(theme: presentationData.theme, type: .genericSuccess(presentationData.strings.Username_LinkCopied, false)), nil)
presentControllerImpl?(UndoOverlayController(presentationData: presentationData, content: .linkCopied(text: presentationData.strings.InviteLink_InviteLinkCopiedText), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), nil)
}
})
})))
@ -990,11 +999,11 @@ public func channelVisibilityController(context: AccountContext, peerId: PeerId,
}
}
if revoke {
revokeLinkDisposable.set((revokePersistentPeerExportedInvitation(account: context.account, peerId: peerId) |> deliverOnMainQueue).start(completed: {
updateState {
$0.withUpdatedRevokingPrivateLink(false)
}
}))
// revokeLinkDisposable.set((revokePersistentPeerExportedInvitation(account: context.account, peerId: peerId) |> deliverOnMainQueue).start(completed: {
// updateState {
// $0.withUpdatedRevokingPrivateLink(false)
// }
// }))
}
})
]),
@ -1260,6 +1269,9 @@ public func channelVisibilityController(context: AccountContext, peerId: PeerId,
}
let controller = ItemListController(context: context, state: signal)
controller.willDisappear = { _ in
dismissTooltipsImpl?()
}
dismissImpl = { [weak controller, weak onDismissRemoveController] in
guard let controller = controller else {
return
@ -1391,6 +1403,19 @@ public func channelVisibilityController(context: AccountContext, peerId: PeerId,
getControllerImpl = { [weak controller] in
return controller
}
dismissTooltipsImpl = { [weak controller] in
controller?.window?.forEachController({ controller in
if let controller = controller as? UndoOverlayController {
controller.dismissWithCommitAction()
}
})
controller?.forEachController({ controller in
if let controller = controller as? UndoOverlayController {
controller.dismissWithCommitAction()
}
return true
})
}
return controller
}

View file

@ -522,7 +522,7 @@ public func peersNearbyController(context: AccountContext) -> ViewController {
if available {
let controller = PermissionController(context: context, splashScreen: true)
controller.navigationPresentation = .modalInLargeLayout
controller.setState(.custom(icon: PermissionControllerCustomIcon(light: UIImage(bundleImageName: "Location/LocalGroupLightIcon"), dark: UIImage(bundleImageName: "Location/LocalGroupDarkIcon")), title: presentationData.strings.LocalGroup_Title, subtitle: address, text: presentationData.strings.LocalGroup_Text, buttonTitle: presentationData.strings.LocalGroup_ButtonTitle, footerText: presentationData.strings.LocalGroup_IrrelevantWarning), animated: false)
controller.setState(.custom(icon: .icon(PermissionControllerCustomIcon(light: UIImage(bundleImageName: "Location/LocalGroupLightIcon"), dark: UIImage(bundleImageName: "Location/LocalGroupDarkIcon"))), title: presentationData.strings.LocalGroup_Title, subtitle: address, text: presentationData.strings.LocalGroup_Text, buttonTitle: presentationData.strings.LocalGroup_ButtonTitle, secondaryButtonTitle: nil, footerText: presentationData.strings.LocalGroup_IrrelevantWarning), animated: false)
controller.proceed = { result in
let controller = context.sharedContext.makeCreateGroupController(context: context, peerIds: [], initialTitle: nil, mode: .locatedGroup(latitude: latitude, longitude: longitude, address: address), completion: nil)
controller.navigationPresentation = .modalInLargeLayout

View file

@ -253,6 +253,8 @@ private func notificationPeerExceptionEntries(presentationData: PresentationData
}
entries.append(.soundClassicHeader(index: index, theme: presentationData.theme, title: presentationData.strings.Notifications_ClassicTones))
index += 1
for i in 0 ..< 8 {
let sound: PeerMessageSound = .bundledClassic(id: Int32(i))
entries.append(.sound(index: index, section: .soundClassic, theme: presentationData.theme, text: localizedPeerNotificationSoundString(strings: presentationData.strings, sound: sound), sound: sound, selected: sound == state.selectedSound))
@ -314,7 +316,7 @@ private struct NotificationExceptionPeerState : Equatable {
}
func notificationPeerExceptionController(context: AccountContext, peer: Peer, mode: NotificationExceptionMode, updatePeerSound: @escaping(PeerId, PeerMessageSound) -> Void, updatePeerNotificationInterval: @escaping(PeerId, Int32?) -> Void, updatePeerDisplayPreviews: @escaping(PeerId, PeerNotificationDisplayPreviews) -> Void, removePeerFromExceptions: @escaping () -> Void, modifiedPeer: @escaping () -> Void) -> ViewController {
public func notificationPeerExceptionController(context: AccountContext, peer: Peer, mode: NotificationExceptionMode, edit: Bool = false, updatePeerSound: @escaping(PeerId, PeerMessageSound) -> Void, updatePeerNotificationInterval: @escaping(PeerId, Int32?) -> Void, updatePeerDisplayPreviews: @escaping(PeerId, PeerNotificationDisplayPreviews) -> Void, removePeerFromExceptions: @escaping () -> Void, modifiedPeer: @escaping () -> Void) -> ViewController {
let initialState = NotificationExceptionPeerState(canRemove: false)
let statePromise = Promise(initialState)
let stateValue = Atomic(value: initialState)
@ -370,7 +372,7 @@ func notificationPeerExceptionController(context: AccountContext, peer: Peer, mo
arguments.cancel()
})
let rightNavigationButton = ItemListNavigationButton(content: .text(state.canRemove ? presentationData.strings.Common_Done : presentationData.strings.Notification_Exceptions_Add), style: .bold, enabled: true, action: {
let rightNavigationButton = ItemListNavigationButton(content: .text(state.canRemove || edit ? presentationData.strings.Common_Done : presentationData.strings.Notification_Exceptions_Add), style: .bold, enabled: true, action: {
arguments.complete()
})

View file

@ -293,7 +293,7 @@ private indirect enum InstalledStickerPacksEntry: ItemListNodeEntry {
func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem {
let arguments = arguments as! InstalledStickerPacksControllerArguments
switch self {
case let .suggestOptions(theme, text, value):
case let .suggestOptions(_, text, value):
return ItemListDisclosureItem(presentationData: presentationData, title: text, label: value, sectionId: self.section, style: .blocks, action: {
arguments.openSuggestOptions()
}, tag: InstalledStickerPacksEntryTag.suggestOptions)
@ -301,23 +301,23 @@ private indirect enum InstalledStickerPacksEntry: ItemListNodeEntry {
return ItemListDisclosureItem(presentationData: presentationData, title: text, label: count == 0 ? "" : "\(count)", labelStyle: .badge(theme.list.itemAccentColor), sectionId: self.section, style: .blocks, action: {
arguments.openFeatured()
})
case let .masks(theme, text):
case let .masks(_, text):
return ItemListDisclosureItem(presentationData: presentationData, title: text, label: "", sectionId: self.section, style: .blocks, action: {
arguments.openMasks()
})
case let .archived(theme, text, count, archived):
case let .archived(_, text, count, archived):
return ItemListDisclosureItem(presentationData: presentationData, title: text, label: count == 0 ? "" : "\(count)", sectionId: self.section, style: .blocks, action: {
arguments.openArchived(archived)
})
case let .animatedStickers(theme, text, value):
case let .animatedStickers(_, text, value):
return ItemListSwitchItem(presentationData: presentationData, title: text, value: value, sectionId: self.section, style: .blocks, updated: { value in
arguments.toggleAnimatedStickers(value)
})
case let .animatedStickersInfo(theme, text):
case let .animatedStickersInfo(_, text):
return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section)
case let .packsTitle(theme, text):
case let .packsTitle(_, text):
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
case let .pack(_, theme, strings, info, topItem, count, animatedStickers, enabled, editing):
case let .pack(_, _, strings, info, topItem, count, animatedStickers, enabled, editing):
return ItemListStickerPackItem(presentationData: presentationData, account: arguments.account, packInfo: info, itemCount: count, topItem: topItem, unread: false, control: .none, editing: editing, enabled: enabled, playAnimatedStickers: animatedStickers, sectionId: self.section, action: {
arguments.openStickerPack(info)
}, setPackIdWithRevealedOptions: { current, previous in
@ -326,7 +326,7 @@ private indirect enum InstalledStickerPacksEntry: ItemListNodeEntry {
}, removePack: {
arguments.removePack(ArchivedStickerPackItem(info: info, topItems: topItem != nil ? [topItem!] : []))
})
case let .packsInfo(theme, text):
case let .packsInfo(_, text):
return ItemListTextItem(presentationData: presentationData, text: .markdown(text), sectionId: self.section, linkAction: { _ in
arguments.openStickersBot()
})

View file

@ -554,6 +554,16 @@ public class WallpaperGalleryController: ViewController {
strongSelf.containerLayoutUpdated(layout, transition: .animated(duration: 0.3, curve: .spring))
}
}
if let entry = self.currentEntry(), case let .wallpaper(wallpaper, _) = entry, case let .file(_, _, _, _, true, _, _, _ , settings) = wallpaper, let color = settings.color {
if self.patternPanelNode?.backgroundColors != nil, let snapshotView = self.patternPanelNode?.scrollNode.view.snapshotContentTree() {
self.patternPanelNode?.view.addSubview(snapshotView)
snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false) { [weak snapshotView] _ in
snapshotView?.removeFromSuperview()
}
}
self.patternPanelNode?.backgroundColors = (UIColor(rgb: color), nil, nil)
}
}
}

View file

@ -161,13 +161,6 @@ private final class WallpaperPatternItemNode : ListViewItemNode {
}
}
final class WallpaperPatternPanelNode: ASDisplayNode {
private let context: AccountContext
private var theme: PresentationTheme
@ -175,7 +168,7 @@ final class WallpaperPatternPanelNode: ASDisplayNode {
private let backgroundNode: ASDisplayNode
private let topSeparatorNode: ASDisplayNode
private let scrollNode: ASScrollNode
let scrollNode: ASScrollNode
private let titleNode: ImmediateTextNode
private let labelNode: ImmediateTextNode

View file

@ -24,7 +24,7 @@ public final class SolidRoundedButtonNode: ASDisplayNode {
private var theme: SolidRoundedButtonTheme
private var font: SolidRoundedButtonFont
private let buttonBackgroundNode: ASImageNode
private let buttonBackgroundNode: ASDisplayNode
private let buttonGlossNode: SolidRoundedButtonGlossNode
private let buttonNode: HighlightTrackingButtonNode
private let titleNode: ImmediateTextNode
@ -60,10 +60,12 @@ public final class SolidRoundedButtonNode: ASDisplayNode {
self.buttonCornerRadius = cornerRadius
self.title = title
self.buttonBackgroundNode = ASImageNode()
self.buttonBackgroundNode.isLayerBacked = true
self.buttonBackgroundNode.displaysAsynchronously = false
self.buttonBackgroundNode.image = generateStretchableFilledCircleImage(radius: cornerRadius, color: theme.backgroundColor)
self.buttonBackgroundNode = ASDisplayNode()
self.buttonBackgroundNode.backgroundColor = theme.backgroundColor
self.buttonBackgroundNode.cornerRadius = cornerRadius
if #available(iOS 13.0, *) {
self.buttonBackgroundNode.layer.cornerCurve = .continuous
}
self.buttonGlossNode = SolidRoundedButtonGlossNode(color: theme.foregroundColor, cornerRadius: cornerRadius)
@ -122,7 +124,7 @@ public final class SolidRoundedButtonNode: ASDisplayNode {
}
self.theme = theme
self.buttonBackgroundNode.image = generateStretchableFilledCircleImage(radius: self.buttonCornerRadius, color: theme.backgroundColor)
self.buttonBackgroundNode.backgroundColor = theme.backgroundColor
self.buttonGlossNode.color = theme.foregroundColor
self.titleNode.attributedText = NSAttributedString(string: self.title ?? "", font: self.font == .bold ? Font.semibold(17.0) : Font.regular(17.0), textColor: theme.foregroundColor)
self.subtitleNode.attributedText = NSAttributedString(string: self.subtitle ?? "", font: Font.regular(14.0), textColor: theme.foregroundColor)

View file

@ -11,8 +11,8 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = {
dict[-457104426] = { return Api.InputGeoPoint.parse_inputGeoPointEmpty($0) }
dict[1210199983] = { return Api.InputGeoPoint.parse_inputGeoPoint($0) }
dict[-784000893] = { return Api.payments.ValidatedRequestedInfo.parse_validatedRequestedInfo($0) }
dict[-1415563086] = { return Api.ChatFull.parse_channelFull($0) }
dict[-261341160] = { return Api.ChatFull.parse_chatFull($0) }
dict[-500874592] = { return Api.ChatFull.parse_chatFull($0) }
dict[-66811386] = { return Api.ChatFull.parse_channelFull($0) }
dict[-1159937629] = { return Api.PollResults.parse_pollResults($0) }
dict[-925415106] = { return Api.ChatParticipant.parse_chatParticipant($0) }
dict[-636267638] = { return Api.ChatParticipant.parse_chatParticipantCreator($0) }
@ -106,7 +106,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = {
dict[483104362] = { return Api.RichText.parse_textPhone($0) }
dict[136105807] = { return Api.RichText.parse_textImage($0) }
dict[894777186] = { return Api.RichText.parse_textAnchor($0) }
dict[328899191] = { return Api.UserFull.parse_userFull($0) }
dict[-1522240089] = { return Api.UserFull.parse_userFull($0) }
dict[-292807034] = { return Api.InputChannel.parse_inputChannelEmpty($0) }
dict[-1343524562] = { return Api.InputChannel.parse_inputChannel($0) }
dict[414687501] = { return Api.DcOption.parse_dcOption($0) }
@ -117,6 +117,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = {
dict[-1000708810] = { return Api.help.AppUpdate.parse_noAppUpdate($0) }
dict[497489295] = { return Api.help.AppUpdate.parse_appUpdate($0) }
dict[-209337866] = { return Api.LangPackDifference.parse_langPackDifference($0) }
dict[-794007333] = { return Api.PeerHistoryTTL.parse_peerHistoryTTL($0) }
dict[84438264] = { return Api.WallPaperSettings.parse_wallPaperSettings($0) }
dict[-1519029347] = { return Api.EmojiURL.parse_emojiURL($0) }
dict[1611985938] = { return Api.StatsGroupTopAdmin.parse_statsGroupTopAdmin($0) }
@ -151,6 +152,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = {
dict[-1160714821] = { return Api.Peer.parse_peerChat($0) }
dict[-1109531342] = { return Api.Peer.parse_peerChannel($0) }
dict[410107472] = { return Api.messages.ExportedChatInvite.parse_exportedChatInvite($0) }
dict[572915951] = { return Api.messages.ExportedChatInvite.parse_exportedChatInviteReplaced($0) }
dict[-1868808300] = { return Api.PaymentRequestedInfo.parse_paymentRequestedInfo($0) }
dict[164646985] = { return Api.UserStatus.parse_userStatusEmpty($0) }
dict[-306628279] = { return Api.UserStatus.parse_userStatusOnline($0) }
@ -448,6 +450,11 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = {
dict[-115071790] = { return Api.ChannelAdminLogEventAction.parse_channelAdminLogEventActionParticipantMute($0) }
dict[-431740480] = { return Api.ChannelAdminLogEventAction.parse_channelAdminLogEventActionParticipantUnmute($0) }
dict[1456906823] = { return Api.ChannelAdminLogEventAction.parse_channelAdminLogEventActionToggleGroupCallSetting($0) }
dict[1515256996] = { return Api.ChannelAdminLogEventAction.parse_channelAdminLogEventActionExportedInviteDelete($0) }
dict[1091179342] = { return Api.ChannelAdminLogEventAction.parse_channelAdminLogEventActionExportedInviteRevoke($0) }
dict[-384910503] = { return Api.ChannelAdminLogEventAction.parse_channelAdminLogEventActionExportedInviteEdit($0) }
dict[1557846647] = { return Api.ChannelAdminLogEventAction.parse_channelAdminLogEventActionParticipantJoinByInvite($0) }
dict[1048537159] = { return Api.ChannelAdminLogEventAction.parse_channelAdminLogEventActionParticipantVolume($0) }
dict[-543777747] = { return Api.auth.ExportedAuthorization.parse_exportedAuthorization($0) }
dict[2103482845] = { return Api.SecurePlainData.parse_securePlainPhone($0) }
dict[569137759] = { return Api.SecurePlainData.parse_securePlainEmail($0) }
@ -587,9 +594,9 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = {
dict[2027216577] = { return Api.Updates.parse_updateShort($0) }
dict[1918567619] = { return Api.Updates.parse_updatesCombined($0) }
dict[1957577280] = { return Api.Updates.parse_updates($0) }
dict[301019932] = { return Api.Updates.parse_updateShortSentMessage($0) }
dict[580309704] = { return Api.Updates.parse_updateShortMessage($0) }
dict[1076714939] = { return Api.Updates.parse_updateShortChatMessage($0) }
dict[-84936653] = { return Api.Updates.parse_updateShortMessage($0) }
dict[290961496] = { return Api.Updates.parse_updateShortChatMessage($0) }
dict[-1877614335] = { return Api.Updates.parse_updateShortSentMessage($0) }
dict[-276825834] = { return Api.stats.MegagroupStats.parse_megagroupStats($0) }
dict[-884757282] = { return Api.StatsAbsValueAndPrev.parse_statsAbsValueAndPrev($0) }
dict[1038967584] = { return Api.MessageMedia.parse_messageMediaEmpty($0) }
@ -1013,6 +1020,8 @@ public struct Api {
_1.serialize(buffer, boxed)
case let _1 as Api.LangPackDifference:
_1.serialize(buffer, boxed)
case let _1 as Api.PeerHistoryTTL:
_1.serialize(buffer, boxed)
case let _1 as Api.WallPaperSettings:
_1.serialize(buffer, boxed)
case let _1 as Api.EmojiURL:

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -74,7 +74,7 @@ func applyUpdateMessage(postbox: Postbox, stateManager: AccountStateManager, mes
}
} else {
switch result {
case let .updateShortSentMessage(_, _, _, _, date, _, _):
case let .updateShortSentMessage(_, _, _, _, date, _, _, _):
updatedTimestamp = date
default:
break
@ -117,7 +117,7 @@ func applyUpdateMessage(postbox: Postbox, stateManager: AccountStateManager, mes
attributes = updatedMessage.attributes
text = updatedMessage.text
forwardInfo = updatedMessage.forwardInfo
} else if case let .updateShortSentMessage(_, _, _, _, _, apiMedia, entities) = result {
} else if case let .updateShortSentMessage(_, _, _, _, _, apiMedia, entities, _) = result {
let (mediaValue, _) = textMediaAndExpirationTimerFromApiMedia(apiMedia, currentMessage.id.peerId)
if let mediaValue = mediaValue {
media = [mediaValue]

View file

@ -58,6 +58,10 @@ public enum AdminLogEventAction {
case endGroupCall
case groupCallUpdateParticipantMuteStatus(peerId: PeerId, isMuted: Bool)
case updateGroupCallSettings(joinMuted: Bool)
case groupCallUpdateParticipantVolume(peerId: PeerId, volume: Int32)
case deleteExportedInvitation(ExportedInvitation)
case revokeExportedInvitation(ExportedInvitation)
case editExportedInvitation(previous: ExportedInvitation, updated: ExportedInvitation)
}
public enum ChannelAdminLogEventError {
@ -89,12 +93,13 @@ public struct AdminLogEventsFlags: OptionSet {
public static let editMessages = AdminLogEventsFlags(rawValue: 1 << 12)
public static let deleteMessages = AdminLogEventsFlags(rawValue: 1 << 13)
public static let calls = AdminLogEventsFlags(rawValue: 1 << 14)
public static let invites = AdminLogEventsFlags(rawValue: 1 << 15)
public static var all: AdminLogEventsFlags {
return [.join, .leave, .invite, .ban, .unban, .kick, .unkick, .promote, .demote, .info, .settings, .pinnedMessages, .editMessages, .deleteMessages, .calls]
return [.join, .leave, .invite, .ban, .unban, .kick, .unkick, .promote, .demote, .info, .settings, .pinnedMessages, .editMessages, .deleteMessages, .calls, .invites]
}
public static var flags: AdminLogEventsFlags {
return [.join, .leave, .invite, .ban, .unban, .kick, .unkick, .promote, .demote, .info, .settings, .pinnedMessages, .editMessages, .deleteMessages, .calls]
return [.join, .leave, .invite, .ban, .unban, .kick, .unkick, .promote, .demote, .info, .settings, .pinnedMessages, .editMessages, .deleteMessages, .calls, .invites]
}
}
@ -230,6 +235,17 @@ public func channelAdminLogEvents(postbox: Postbox, network: Network, peerId: Pe
action = .groupCallUpdateParticipantMuteStatus(peerId: parsedParticipant.peerId, isMuted: false)
case let .channelAdminLogEventActionToggleGroupCallSetting(joinMuted):
action = .updateGroupCallSettings(joinMuted: joinMuted == .boolTrue)
case let .channelAdminLogEventActionExportedInviteDelete(invite):
action = .deleteExportedInvitation(ExportedInvitation(apiExportedInvite: invite))
case let .channelAdminLogEventActionExportedInviteRevoke(invite):
action = .revokeExportedInvitation(ExportedInvitation(apiExportedInvite: invite))
case let .channelAdminLogEventActionExportedInviteEdit(prevInvite, newInvite):
action = .editExportedInvitation(previous: ExportedInvitation(apiExportedInvite: prevInvite), updated: ExportedInvitation(apiExportedInvite: newInvite))
case let .channelAdminLogEventActionParticipantVolume(participant):
let parsedParticipant = GroupCallParticipantsContext.Update.StateUpdate.ParticipantUpdate(participant)
action = .groupCallUpdateParticipantVolume(peerId: parsedParticipant.peerId, volume: parsedParticipant.volume ?? 10000)
case let .channelAdminLogEventActionParticipantJoinByInvite(invite):
action = nil
}
let peerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: userId)
if let action = action {

View file

@ -5,7 +5,7 @@ import TelegramApi
import SyncCore
extension ExportedInvitation {
init?(apiExportedInvite: Api.ExportedChatInvite) {
init(apiExportedInvite: Api.ExportedChatInvite) {
switch apiExportedInvite {
case let .chatInviteExported(flags, link, adminId, date, startDate, expireDate, usageLimit, usage):
self = ExportedInvitation(link: link, isPermanent: (flags & (1 << 5)) != 0, isRevoked: (flags & (1 << 0)) != 0, adminId: PeerId(namespace: Namespaces.Peer.CloudUser, id: adminId), date: date, startDate: startDate, expireDate: expireDate, usageLimit: usageLimit, count: usage)

View file

@ -6,58 +6,6 @@ import MtProtoKit
import SyncCore
public func revokePersistentPeerExportedInvitation(account: Account, peerId: PeerId) -> Signal<ExportedInvitation?, NoError> {
return account.postbox.transaction { transaction -> Signal<ExportedInvitation?, NoError> in
if let peer = transaction.getPeer(peerId), let inputPeer = apiInputPeer(peer) {
let flags: Int32 = (1 << 2)
if let _ = peer as? TelegramChannel {
return account.network.request(Api.functions.messages.exportChatInvite(flags: flags, peer: inputPeer, expireDate: nil, usageLimit: nil))
|> retryRequest
|> mapToSignal { result -> Signal<ExportedInvitation?, NoError> in
return account.postbox.transaction { transaction -> ExportedInvitation? in
if let invitation = ExportedInvitation(apiExportedInvite: result) {
transaction.updatePeerCachedData(peerIds: Set([peerId]), update: { _, current in
if let current = current as? CachedChannelData {
return current.withUpdatedExportedInvitation(invitation)
} else {
return CachedChannelData().withUpdatedExportedInvitation(invitation)
}
})
return invitation
} else {
return nil
}
}
}
} else if let _ = peer as? TelegramGroup {
return account.network.request(Api.functions.messages.exportChatInvite(flags: flags, peer: inputPeer, expireDate: nil, usageLimit: nil))
|> retryRequest
|> mapToSignal { result -> Signal<ExportedInvitation?, NoError> in
return account.postbox.transaction { transaction -> ExportedInvitation? in
if let invitation = ExportedInvitation(apiExportedInvite: result) {
transaction.updatePeerCachedData(peerIds: Set([peerId]), update: { _, current in
if let current = current as? CachedGroupData {
return current.withUpdatedExportedInvitation(invitation)
} else {
return current
}
})
return invitation
} else {
return nil
}
}
}
} else {
return .complete()
}
} else {
return .complete()
}
} |> switchToLatest
}
public enum CreatePeerExportedInvitationError {
case generic
}
@ -75,11 +23,7 @@ public func createPeerExportedInvitation(account: Account, peerId: PeerId, expir
return account.network.request(Api.functions.messages.exportChatInvite(flags: flags, peer: inputPeer, expireDate: expireDate, usageLimit: usageLimit))
|> mapError { _ in return CreatePeerExportedInvitationError.generic }
|> map { result -> ExportedInvitation? in
if let invitation = ExportedInvitation(apiExportedInvite: result) {
return invitation
} else {
return nil
}
return ExportedInvitation(apiExportedInvite: result)
}
} else {
return .complete()
@ -142,18 +86,27 @@ public func revokePeerExportedInvitation(account: Account, peerId: PeerId, link:
|> mapError { _ in return RevokePeerExportedInvitationError.generic }
|> mapToSignal { result -> Signal<ExportedInvitation?, RevokePeerExportedInvitationError> in
return account.postbox.transaction { transaction in
if case let .exportedChatInvite(invite, users) = result {
var peers: [Peer] = []
for user in users {
let telegramUser = TelegramUser(user: user)
peers.append(telegramUser)
}
updatePeers(transaction: transaction, peers: peers, update: { _, updated -> Peer in
return updated
})
return ExportedInvitation(apiExportedInvite: invite)
} else {
return nil
switch result {
case let .exportedChatInvite(invite, users):
var peers: [Peer] = []
for user in users {
let telegramUser = TelegramUser(user: user)
peers.append(telegramUser)
}
updatePeers(transaction: transaction, peers: peers, update: { _, updated -> Peer in
return updated
})
return ExportedInvitation(apiExportedInvite: invite)
case let .exportedChatInviteReplaced(_, newInvite, users):
var peers: [Peer] = []
for user in users {
let telegramUser = TelegramUser(user: user)
peers.append(telegramUser)
}
updatePeers(transaction: transaction, peers: peers, update: { _, updated -> Peer in
return updated
})
return ExportedInvitation(apiExportedInvite: newInvite)
}
} |> mapError { _ in .generic }
}
@ -199,12 +152,7 @@ public func peerExportedInvitations(account: Account, peerId: PeerId, revoked: B
return updated
})
var invites: [ExportedInvitation] = []
for apiInvite in apiInvites {
if let invite = ExportedInvitation(apiExportedInvite: apiInvite) {
invites.append(invite)
}
}
let invites = apiInvites.map { ExportedInvitation(apiExportedInvite: $0) }
return ExportedInvitations(list: invites, totalCount: count)
} else {
return nil
@ -236,10 +184,10 @@ public func deletePeerExportedInvitation(account: Account, peerId: PeerId, link:
|> switchToLatest
}
public func deleteAllRevokedPeerExportedInvitations(account: Account, peerId: PeerId) -> Signal<Never, NoError> {
public func deleteAllRevokedPeerExportedInvitations(account: Account, peerId: PeerId, adminId: PeerId) -> Signal<Never, NoError> {
return account.postbox.transaction { transaction -> Signal<Never, NoError> in
if let peer = transaction.getPeer(peerId), let inputPeer = apiInputPeer(peer) {
return account.network.request(Api.functions.messages.deleteRevokedExportedChatInvites(peer: inputPeer))
if let peer = transaction.getPeer(peerId), let inputPeer = apiInputPeer(peer), let adminPeer = transaction.getPeer(adminId), let inputAdminId = apiInputUser(adminPeer) {
return account.network.request(Api.functions.messages.deleteRevokedExportedChatInvites(peer: inputPeer, adminId: inputAdminId))
|> `catch` { _ -> Signal<Api.Bool, NoError> in
return .single(.boolFalse)
}

View file

@ -74,6 +74,10 @@ public func searchStickers(account: Account, query: String, scope: SearchSticker
if scope.isEmpty {
return .single([])
}
var query = query
if query == "\u{2764}" {
query = "\u{2764}\u{FE0F}"
}
return account.postbox.transaction { transaction -> ([FoundStickerItem], CachedStickerQueryResult?) in
var result: [FoundStickerItem] = []
if scope.contains(.installed) {

View file

@ -57,14 +57,14 @@ class UpdateMessageService: NSObject, MTMessageService {
if groups.count != 0 {
self.putNext(groups)
}
case let .updateShortChatMessage(flags, id, fromId, chatId, message, pts, ptsCount, date, fwdFrom, viaBotId, replyHeader, entities):
case let .updateShortChatMessage(flags, id, fromId, chatId, message, pts, ptsCount, date, fwdFrom, viaBotId, replyHeader, entities, _):
let generatedMessage = Api.Message.message(flags: flags, id: id, fromId: .peerUser(userId: fromId), peerId: Api.Peer.peerChat(chatId: chatId), fwdFrom: fwdFrom, viaBotId: viaBotId, replyTo: replyHeader, date: date, message: message, media: Api.MessageMedia.messageMediaEmpty, replyMarkup: nil, entities: entities, views: nil, forwards: nil, replies: nil, editDate: nil, postAuthor: nil, groupedId: nil, restrictionReason: nil, ttlPeriod: nil)
let update = Api.Update.updateNewMessage(message: generatedMessage, pts: pts, ptsCount: ptsCount)
let groups = groupUpdates([update], users: [], chats: [], date: date, seqRange: nil)
if groups.count != 0 {
self.putNext(groups)
}
case let .updateShortMessage(flags, id, userId, message, pts, ptsCount, date, fwdFrom, viaBotId, replyHeader, entities):
case let .updateShortMessage(flags, id, userId, message, pts, ptsCount, date, fwdFrom, viaBotId, replyHeader, entities, _):
let generatedFromId: Api.Peer
if (Int(flags) & 1 << 1) != 0 {
generatedFromId = Api.Peer.peerUser(userId: self.peerId.id)
@ -82,7 +82,7 @@ class UpdateMessageService: NSObject, MTMessageService {
}
case .updatesTooLong:
self.pipe.putNext([.reset])
case let .updateShortSentMessage(_, _, pts, ptsCount, _, _, _):
case let .updateShortSentMessage(_, _, pts, ptsCount, _, _, _, _):
self.pipe.putNext([.updatePts(pts: pts, ptsCount: ptsCount)])
}
}

View file

@ -384,13 +384,13 @@ extension Api.Updates {
} else {
return []
}
case let .updateShortSentMessage(_, id, _, _, _, _, _):
case let .updateShortSentMessage(_, id, _, _, _, _, _, _):
return [id]
case .updatesTooLong:
return []
case let .updateShortMessage(_, id, _, _, _, _, _, _, _, _, _):
case let .updateShortMessage(_, id, _, _, _, _, _, _, _, _, _, _):
return [id]
case let .updateShortChatMessage(_, id, _, _, _, _, _, _, _, _, _, _):
case let .updateShortChatMessage(_, id, _, _, _, _, _, _, _, _, _, _, _):
return [id]
}
}
@ -423,9 +423,9 @@ extension Api.Updates {
return []
case .updatesTooLong:
return []
case let .updateShortMessage(_, id, userId, _, _, _, _, _, _, _, _):
case let .updateShortMessage(_, id, userId, _, _, _, _, _, _, _, _, _):
return [MessageId(peerId: PeerId(namespace: Namespaces.Peer.CloudUser, id: userId), namespace: Namespaces.Message.Cloud, id: id)]
case let .updateShortChatMessage(_, id, _, chatId, _, _, _, _, _, _, _, _):
case let .updateShortChatMessage(_, id, _, chatId, _, _, _, _, _, _, _, _, _):
return [MessageId(peerId: PeerId(namespace: Namespaces.Peer.CloudGroup, id: chatId), namespace: Namespaces.Message.Cloud, id: id)]
}
}
@ -458,9 +458,9 @@ extension Api.Updates {
return [:]
case .updatesTooLong:
return [:]
case let .updateShortMessage(_, id, userId, _, _, _, _, _, _, _, _):
case let .updateShortMessage(_, id, userId, _, _, _, _, _, _, _, _, _):
return [:]
case let .updateShortChatMessage(_, id, _, chatId, _, _, _, _, _, _, _, _):
case let .updateShortChatMessage(_, id, _, chatId, _, _, _, _, _, _, _, _, _):
return [:]
}
}

View file

@ -23,6 +23,7 @@ swift_library(
"//submodules/SolidRoundedButtonNode:SolidRoundedButtonNode",
"//submodules/AppBundle:AppBundle",
"//submodules/PresentationDataUtils:PresentationDataUtils",
"//submodules/AnimatedStickerNode:AnimatedStickerNode",
],
visibility = [
"//visibility:public",

View file

@ -9,10 +9,13 @@ import PeersNearbyIconNode
import SolidRoundedButtonNode
import PresentationDataUtils
import Markdown
import AnimatedStickerNode
import AppBundle
public enum PermissionContentIcon {
public enum PermissionContentIcon: Equatable {
case image(UIImage?)
case icon(PermissionControllerCustomIcon)
case animation(String)
public func imageForTheme(_ theme: PresentationTheme) -> UIImage? {
switch self {
@ -20,6 +23,8 @@ public enum PermissionContentIcon {
return image
case let .icon(icon):
return theme.overallDarkAppearance ? (icon.dark ?? icon.light) : icon.light
case .animation:
return nil
}
}
}
@ -30,6 +35,7 @@ public final class PermissionContentNode: ASDisplayNode {
private let iconNode: ASImageNode
private let nearbyIconNode: PeersNearbyIconNode?
private let animationNode: AnimatedStickerNode?
private let titleNode: ImmediateTextNode
private let subtitleNode: ImmediateTextNode
private let textNode: ImmediateTextNode
@ -46,7 +52,7 @@ public final class PermissionContentNode: ASDisplayNode {
public var validLayout: (CGSize, UIEdgeInsets)?
public init(theme: PresentationTheme, strings: PresentationStrings, kind: Int32, icon: PermissionContentIcon, title: String, subtitle: String? = nil, text: String, buttonTitle: String, footerText: String? = nil, buttonAction: @escaping () -> Void, openPrivacyPolicy: (() -> Void)?) {
public init(theme: PresentationTheme, strings: PresentationStrings, kind: Int32, icon: PermissionContentIcon, title: String, subtitle: String? = nil, text: String, buttonTitle: String, secondaryButtonTitle: String? = nil, footerText: String? = nil, buttonAction: @escaping () -> Void, openPrivacyPolicy: (() -> Void)?) {
self.theme = theme
self.kind = kind
@ -62,10 +68,19 @@ public final class PermissionContentNode: ASDisplayNode {
self.iconNode.displayWithoutProcessing = true
self.iconNode.displaysAsynchronously = false
if kind == PermissionKind.nearbyLocation.rawValue {
if case let .animation(animation) = icon {
self.animationNode = AnimatedStickerNode()
if let path = getAppBundle().path(forResource: animation, ofType: "tgs") {
self.animationNode?.setup(source: AnimatedStickerNodeLocalFileSource(path: path), width: 320, height: 320, playbackMode: .loop, mode: .direct(cachePathPrefix: nil))
self.animationNode?.visibility = true
}
self.nearbyIconNode = nil
} else if kind == PermissionKind.nearbyLocation.rawValue {
self.nearbyIconNode = PeersNearbyIconNode(theme: theme)
self.animationNode = nil
} else {
self.nearbyIconNode = nil
self.animationNode = nil
}
self.titleNode = ImmediateTextNode()
@ -81,11 +96,10 @@ public final class PermissionContentNode: ASDisplayNode {
self.subtitleNode.displaysAsynchronously = false
self.textNode = ImmediateTextNode()
self.textNode.textAlignment = .center
self.textNode.maximumNumberOfLines = 0
self.textNode.displaysAsynchronously = false
self.actionButton = SolidRoundedButtonNode(theme: SolidRoundedButtonTheme(theme: theme), height: 48.0, cornerRadius: 9.0, gloss: true)
self.actionButton = SolidRoundedButtonNode(theme: SolidRoundedButtonTheme(theme: theme), height: 52.0, cornerRadius: 9.0, gloss: true)
self.footerNode = ImmediateTextNode()
self.footerNode.textAlignment = .center
@ -93,16 +107,23 @@ public final class PermissionContentNode: ASDisplayNode {
self.footerNode.displaysAsynchronously = false
self.privacyPolicyButton = HighlightableButtonNode()
self.privacyPolicyButton.setTitle(strings.Permissions_PrivacyPolicy, with: Font.regular(16.0), with: theme.list.itemAccentColor, for: .normal)
self.privacyPolicyButton.setTitle(secondaryButtonTitle ?? strings.Permissions_PrivacyPolicy, with: Font.regular(17.0), with: theme.list.itemAccentColor, for: .normal)
super.init()
self.iconNode.image = icon.imageForTheme(theme)
self.title = title
let body = MarkdownAttributeSet(font: Font.regular(16.0), textColor: theme.list.itemPrimaryTextColor)
var secondaryText = false
if case .animation = icon {
secondaryText = true
}
self.textNode.textAlignment = secondaryText ? .natural : .center
let body = MarkdownAttributeSet(font: Font.regular(16.0), textColor: secondaryText ? theme.list.itemSecondaryTextColor : theme.list.itemPrimaryTextColor)
let link = MarkdownAttributeSet(font: Font.regular(16.0), textColor: theme.list.itemAccentColor, additionalAttributes: [TelegramTextAttributes.URL: ""])
self.textNode.attributedText = parseMarkdownIntoAttributedString(text.replacingOccurrences(of: "]", with: "]()"), attributes: MarkdownAttributes(body: body, bold: body, link: link, linkAttribute: { _ in nil }), textAlignment: .center)
self.textNode.attributedText = parseMarkdownIntoAttributedString(text.replacingOccurrences(of: "]", with: "]()"), attributes: MarkdownAttributes(body: body, bold: body, link: link, linkAttribute: { _ in nil }), textAlignment: secondaryText ? .natural : .center)
self.actionButton.title = buttonTitle
self.privacyPolicyButton.isHidden = openPrivacyPolicy == nil
@ -116,9 +137,8 @@ public final class PermissionContentNode: ASDisplayNode {
}
self.addSubnode(self.iconNode)
if let nearbyIconNode = self.nearbyIconNode {
self.addSubnode(nearbyIconNode)
}
self.nearbyIconNode.flatMap { self.addSubnode($0) }
self.animationNode.flatMap { self.addSubnode($0) }
self.addSubnode(self.titleNode)
self.addSubnode(self.subtitleNode)
self.addSubnode(self.textNode)
@ -183,7 +203,7 @@ public final class PermissionContentNode: ASDisplayNode {
let titleSize = self.titleNode.updateLayout(CGSize(width: size.width - sidePadding * 2.0, height: .greatestFiniteMagnitude))
let subtitleSize = self.subtitleNode.updateLayout(CGSize(width: size.width - smallerSidePadding * 2.0, height: .greatestFiniteMagnitude))
let textSize = self.textNode.updateLayout(CGSize(width: size.width - sidePadding * 2.0, height: .greatestFiniteMagnitude))
let buttonInset: CGFloat = 38.0
let buttonInset: CGFloat = 16.0
let buttonWidth = min(size.width, size.height) - buttonInset * 2.0
let buttonHeight = self.actionButton.updateLayout(width: buttonWidth, transition: transition)
let footerSize = self.footerNode.updateLayout(CGSize(width: size.width - smallerSidePadding * 2.0, height: .greatestFiniteMagnitude))
@ -211,7 +231,12 @@ public final class PermissionContentNode: ASDisplayNode {
imageSize = CGSize(width: 120.0, height: 120.0)
contentHeight += imageSize.height + imageSpacing
}
if let _ = self.animationNode, size.width < size.height {
imageSpacing = floor(availableHeight * 0.12)
imageSize = CGSize(width: 200.0, height: 200.0)
contentHeight += imageSize.height + imageSpacing
}
let privacySpacing: CGFloat = max(30.0 + privacyButtonSize.height, (availableHeight - titleTextSpacing - buttonSpacing - imageSize.height - imageSpacing) / 2.0)
var verticalOffset: CGFloat = 0.0
@ -222,6 +247,7 @@ public final class PermissionContentNode: ASDisplayNode {
let contentOrigin = insets.top + floor((size.height - insets.top - insets.bottom - contentHeight) / 2.0) - verticalOffset
let iconFrame = CGRect(origin: CGPoint(x: floor((size.width - imageSize.width) / 2.0), y: contentOrigin), size: imageSize)
let nearbyIconFrame = CGRect(origin: CGPoint(x: floor((size.width - imageSize.width) / 2.0), y: contentOrigin), size: imageSize)
let animationFrame = CGRect(origin: CGPoint(x: floor((size.width - imageSize.width) / 2.0), y: contentOrigin), size: imageSize)
let titleFrame = CGRect(origin: CGPoint(x: floor((size.width - titleSize.width) / 2.0), y: iconFrame.maxY + imageSpacing), size: titleSize)
let subtitleFrame: CGRect
@ -232,16 +258,26 @@ public final class PermissionContentNode: ASDisplayNode {
}
let textFrame = CGRect(origin: CGPoint(x: floor((size.width - textSize.width) / 2.0), y: subtitleFrame.maxY + titleTextSpacing), size: textSize)
let buttonFrame = CGRect(origin: CGPoint(x: floor((size.width - buttonWidth) / 2.0), y: textFrame.maxY + buttonSpacing), size: CGSize(width: buttonWidth, height: buttonHeight))
let footerFrame = CGRect(origin: CGPoint(x: floor((size.width - footerSize.width) / 2.0), y: size.height - footerSize.height - insets.bottom - 8.0), size: footerSize)
let privacyButtonFrame = CGRect(origin: CGPoint(x: floor((size.width - privacyButtonSize.width) / 2.0), y: buttonFrame.maxY + floor((privacySpacing - privacyButtonSize.height) / 2.0)), size: privacyButtonSize)
let buttonFrame: CGRect
let privacyButtonFrame: CGRect
if self.textNode.textAlignment == .natural {
buttonFrame = CGRect(origin: CGPoint(x: floor((size.width - buttonWidth) / 2.0), y: max(textFrame.maxY + buttonSpacing ,size.height - buttonHeight - insets.bottom - 70.0)), size: CGSize(width: buttonWidth, height: buttonHeight))
privacyButtonFrame = CGRect(origin: CGPoint(x: floor((size.width - privacyButtonSize.width) / 2.0), y: buttonFrame.maxY + 29.0), size: privacyButtonSize)
} else {
buttonFrame = CGRect(origin: CGPoint(x: floor((size.width - buttonWidth) / 2.0), y: textFrame.maxY + buttonSpacing), size: CGSize(width: buttonWidth, height: buttonHeight))
privacyButtonFrame = CGRect(origin: CGPoint(x: floor((size.width - privacyButtonSize.width) / 2.0), y: buttonFrame.maxY + floor((privacySpacing - privacyButtonSize.height) / 2.0)), size: privacyButtonSize)
}
transition.updateFrame(node: self.iconNode, frame: iconFrame)
if let nearbyIconNode = self.nearbyIconNode {
transition.updateFrame(node: nearbyIconNode, frame: nearbyIconFrame)
}
if let animationNode = self.animationNode {
transition.updateFrame(node: animationNode, frame: animationFrame)
animationNode.updateLayout(size: animationFrame.size)
}
transition.updateFrame(node: self.titleNode, frame: titleFrame)
transition.updateFrame(node: self.subtitleNode, frame: subtitleFrame)
transition.updateFrame(node: self.textNode, frame: textFrame)

View file

@ -207,7 +207,10 @@ public final class PermissionController: ViewController {
}
}
}
} else {
} else if case let .custom(icon, _, _, _, _, _, _) = state {
if case .animation = icon, case .modal = self.navigationPresentation {
self.navigationItem.leftBarButtonItem = UIBarButtonItem(customDisplayNode: ASDisplayNode())
}
self.allow = { [weak self] in
if let strongSelf = self {
strongSelf.proceed?(true)

View file

@ -22,7 +22,7 @@ public struct PermissionControllerCustomIcon: Equatable {
public enum PermissionControllerContent: Equatable {
case permission(PermissionState?)
case custom(icon: PermissionControllerCustomIcon, title: String, subtitle: String?, text: String, buttonTitle: String, footerText: String?)
case custom(icon: PermissionContentIcon, title: String, subtitle: String?, text: String, buttonTitle: String, secondaryButtonTitle: String?, footerText: String?)
}
private struct PermissionControllerDataState: Equatable {
@ -208,7 +208,7 @@ final class PermissionControllerNode: ASDisplayNode {
hasPrivacyPolicy = false
}
let contentNode = PermissionContentNode(theme: self.presentationData.theme, strings: self.presentationData.strings, kind: dataState.kind.rawValue, icon: .image(icon), title: title, text: text, buttonTitle: buttonTitle, buttonAction: { [weak self] in
let contentNode = PermissionContentNode(theme: self.presentationData.theme, strings: self.presentationData.strings, kind: dataState.kind.rawValue, icon: .image(icon), title: title, text: text, buttonTitle: buttonTitle, secondaryButtonTitle: nil, buttonAction: { [weak self] in
self?.allow?()
}, openPrivacyPolicy: hasPrivacyPolicy ? self.openPrivacyPolicy : nil)
self.insertSubnode(contentNode, at: 0)
@ -233,14 +233,16 @@ final class PermissionControllerNode: ASDisplayNode {
transition.updateFrame(node: contentNode, frame: contentFrame)
contentNode.updateLayout(size: contentFrame.size, insets: insets, transition: transition)
}
case let .custom(icon, title, subtitle, text, buttonTitle, footerText):
case let .custom(icon, title, subtitle, text, buttonTitle, secondaryButtonTitle, footerText):
if let contentNode = self.contentNode {
transition.updateFrame(node: contentNode, frame: contentFrame)
contentNode.updateLayout(size: contentFrame.size, insets: insets, transition: transition)
} else {
let contentNode = PermissionContentNode(theme: self.presentationData.theme, strings: self.presentationData.strings, kind: 0, icon: .icon(icon), title: title, subtitle: subtitle, text: text, buttonTitle: buttonTitle, footerText: footerText, buttonAction: { [weak self] in
let contentNode = PermissionContentNode(theme: self.presentationData.theme, strings: self.presentationData.strings, kind: 0, icon: icon, title: title, subtitle: subtitle, text: text, buttonTitle: buttonTitle, secondaryButtonTitle: secondaryButtonTitle, footerText: footerText, buttonAction: { [weak self] in
self?.allow?()
}, openPrivacyPolicy: nil)
}, openPrivacyPolicy: secondaryButtonTitle != nil ? { [weak self] in
self?.dismiss?()
} : nil)
self.insertSubnode(contentNode, at: 0)
contentNode.updateLayout(size: contentFrame.size, insets: insets, transition: .immediate)
contentNode.frame = contentFrame

View file

@ -85,14 +85,7 @@ public struct PresentationResourcesItemList {
public static func itemListReorderIndicatorIcon(_ theme: PresentationTheme) -> UIImage? {
return theme.image(PresentationResourceKey.itemListReorderIndicatorIcon.rawValue, { theme in
generateImage(CGSize(width: 16.0, height: 9.0), contextGenerator: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
context.setFillColor(theme.list.controlSecondaryColor.cgColor)
context.fill(CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: 1.5)))
context.fill(CGRect(origin: CGPoint(x: 0.0, y: 3.5), size: CGSize(width: size.width, height: 1.5)))
context.fill(CGRect(origin: CGPoint(x: 0.0, y: 7), size: CGSize(width: size.width, height: 1.5)))
})
return generateTintedImage(image: UIImage(bundleImageName: "Item List/Reorder"), color: theme.list.controlSecondaryColor)
})
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -2222,6 +2222,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
controller.popoverPresentationController?.sourceRect = CGRect(origin: CGPoint(x: window.bounds.width / 2.0, y: window.bounds.size.height - 1.0), size: CGSize(width: 1.0, height: 1.0))
window.rootViewController?.present(controller, animated: true)
}
case .speak:
strongSelf.speakText(text.string)
}
}, updateMessageLike: { [weak self] messageId, isLiked in
guard let strongSelf = self else {
@ -2330,7 +2332,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
}
}, greetingStickerNode: { [weak self] in
return self?.chatDisplayNode.greetingStickerNode
}, openPeerContextMenu: { [weak self] peer, node, rect, gesture in
}, openPeerContextMenu: { [weak self] peer, messageId, node, rect, gesture in
guard let strongSelf = self else {
return
}
@ -2342,10 +2344,10 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
strongSelf.dismissAllTooltips()
let context = strongSelf.context
let _ = (context.account.postbox.transaction { transaction -> Peer? in
return transaction.getPeer(peer.id)
let _ = (context.account.postbox.transaction { transaction -> (Peer?, Message?) in
return (transaction.getPeer(peer.id), messageId.flatMap { transaction.getMessage($0) })
}
|> deliverOnMainQueue).start(next: { [weak self] peer in
|> deliverOnMainQueue).start(next: { [weak self] peer, message in
guard let strongSelf = self, let peer = peer, peer.smallProfileImage != nil else {
return
}
@ -2355,7 +2357,10 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
galleryController.setHintWillBePresentedInPreviewingContext(true)
let items: Signal<[ContextMenuItem], NoError> = context.account.postbox.transaction { transaction -> [ContextMenuItem] in
let isChannel = peer.id.namespace == Namespaces.Peer.CloudChannel
var isChannel = false
if let peer = peer as? TelegramChannel, case .broadcast = peer.info {
isChannel = true
}
var items: [ContextMenuItem] = [
.action(ContextMenuActionItem(text: isChannel ? strongSelf.presentationData.strings.Conversation_ContextMenuOpenChannelProfile : strongSelf.presentationData.strings.Conversation_ContextMenuOpenProfile, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Info"), color: theme.actionSheet.primaryTextColor)
@ -11726,6 +11731,15 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
return false
}
}
private func speakText(_ text: String) {
let speechSynthesizer = AVSpeechSynthesizer()
let utterance = AVSpeechUtterance(string: text)
if #available(iOS 11.0, *), let language = NSLinguisticTagger.dominantLanguage(for: text) {
utterance.voice = AVSpeechSynthesisVoice(language: language)
}
speechSynthesizer.speak(utterance)
}
}
private final class ContextControllerContentSourceImpl: ContextControllerContentSource {

View file

@ -113,7 +113,7 @@ public final class ChatControllerInteraction {
let displayDiceTooltip: (TelegramMediaDice) -> Void
let animateDiceSuccess: (Bool) -> Void
let greetingStickerNode: () -> (ASDisplayNode, ASDisplayNode, ASDisplayNode, (@escaping () -> Void) -> Void)?
let openPeerContextMenu: (Peer, ASDisplayNode, CGRect, ContextGesture?) -> Void
let openPeerContextMenu: (Peer, MessageId?, ASDisplayNode, CGRect, ContextGesture?) -> Void
let openMessageReplies: (MessageId, Bool, Bool) -> Void
let openReplyThreadOriginalMessage: (Message) -> Void
let openMessageStats: (MessageId) -> Void
@ -203,7 +203,7 @@ public final class ChatControllerInteraction {
displayDiceTooltip: @escaping (TelegramMediaDice) -> Void,
animateDiceSuccess: @escaping (Bool) -> Void,
greetingStickerNode: @escaping () -> (ASDisplayNode, ASDisplayNode, ASDisplayNode, (@escaping () -> Void) -> Void)?,
openPeerContextMenu: @escaping (Peer, ASDisplayNode, CGRect, ContextGesture?) -> Void,
openPeerContextMenu: @escaping (Peer, MessageId?, ASDisplayNode, CGRect, ContextGesture?) -> Void,
openMessageReplies: @escaping (MessageId, Bool, Bool) -> Void,
openReplyThreadOriginalMessage: @escaping (Message) -> Void,
openMessageStats: @escaping (MessageId) -> Void,
@ -334,7 +334,7 @@ public final class ChatControllerInteraction {
}, animateDiceSuccess: { _ in
}, greetingStickerNode: {
return nil
}, openPeerContextMenu: { _, _, _, _ in
}, openPeerContextMenu: { _, _, _, _, _ in
}, openMessageReplies: { _, _, _ in
}, openReplyThreadOriginalMessage: { _ in
}, openMessageStats: { _ in

View file

@ -577,6 +577,15 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState
}
f(.default)
})))
if isSpeakSelectionEnabled() {
actions.append(.action(ContextMenuActionItem(text: chatPresentationInterfaceState.strings.Conversation_ContextMenuSpeak, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Message"), color: theme.actionSheet.primaryTextColor)
}, action: { _, f in
controllerInteraction.performTextSelectionAction(0, NSAttributedString(string: message.text), .speak)
f(.default)
})))
}
}
if resourceAvailable, !message.containsSecretMedia {
var mediaReference: AnyMediaReference?

View file

@ -120,6 +120,7 @@ final class ChatMessageAvatarAccessoryItem: ListViewAccessoryItem {
final class ChatMessageAvatarAccessoryItemNode: ListViewAccessoryItemNode {
var controllerInteraction: ChatControllerInteraction?
var peer: Peer?
var messageId: MessageId?
let containerNode: ContextControllerSourceNode
let avatarNode: AvatarNode
@ -152,7 +153,7 @@ final class ChatMessageAvatarAccessoryItemNode: ListViewAccessoryItemNode {
guard let strongSelf = self, let controllerInteraction = strongSelf.controllerInteraction, let peer = strongSelf.peer else {
return
}
controllerInteraction.openPeerContextMenu(peer, strongSelf.containerNode, strongSelf.containerNode.bounds, gesture)
controllerInteraction.openPeerContextMenu(peer, strongSelf.messageId, strongSelf.containerNode, strongSelf.containerNode.bounds, gesture)
}
}
@ -168,6 +169,9 @@ final class ChatMessageAvatarAccessoryItemNode: ListViewAccessoryItemNode {
func setPeer(context: AccountContext, theme: PresentationTheme, synchronousLoad: Bool, peer: Peer, authorOfMessage: MessageReference?, emptyColor: UIColor, controllerInteraction: ChatControllerInteraction) {
self.controllerInteraction = controllerInteraction
self.peer = peer
if let messageReference = authorOfMessage, case let .message(m) = messageReference.content {
self.messageId = m.id
}
self.contextActionIsEnabled = peer.smallProfileImage != nil

View file

@ -453,7 +453,7 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode {
}, animateDiceSuccess: { _ in
}, greetingStickerNode: {
return nil
}, openPeerContextMenu: { _, _, _, _ in
}, openPeerContextMenu: { _, _, _, _, _ in
}, openMessageReplies: { _, _, _ in
}, openReplyThreadOriginalMessage: { _ in
}, openMessageStats: { _ in

View file

@ -1169,6 +1169,107 @@ struct ChatRecentActionsEntry: Comparable, Identifiable {
let action = TelegramMediaActionType.customText(text: text, entities: entities)
let message = Message(stableId: self.entry.stableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(bitPattern: self.entry.stableId)), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: "", attributes: [], media: [TelegramMediaAction(action: action)], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: [])
return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes()))
case let .groupCallUpdateParticipantVolume(participantId, volume):
var peers = SimpleDictionary<PeerId, Peer>()
var author: Peer?
var participant: Peer?
if let peer = self.entry.peers[self.entry.event.peerId] {
author = peer
peers[peer.id] = peer
}
if let participantPeer = self.entry.peers[participantId] {
participant = participantPeer
peers[peer.id] = participantPeer
}
var text: String = ""
var entities: [MessageTextEntity] = []
let rawText: (String, [(Int, NSRange)]) = self.presentationData.strings.Channel_AdminLog_UpdatedParticipantVolume(author?.displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder) ?? "", participant?.displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder) ?? "", "\(volume)")
appendAttributedText(text: rawText, generateEntities: { index in
if index == 0, let author = author {
return [.TextMention(peerId: author.id)]
}
return []
}, to: &text, entities: &entities)
let action = TelegramMediaActionType.customText(text: text, entities: entities)
let message = Message(stableId: self.entry.stableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(bitPattern: self.entry.stableId)), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: "", attributes: [], media: [TelegramMediaAction(action: action)], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: [])
return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes()))
case let .deleteExportedInvitation(invite):
var peers = SimpleDictionary<PeerId, Peer>()
var author: Peer?
if let peer = self.entry.peers[self.entry.event.peerId] {
author = peer
peers[peer.id] = peer
}
var text: String = ""
var entities: [MessageTextEntity] = []
let rawText: (String, [(Int, NSRange)]) = self.presentationData.strings.Channel_AdminLog_DeletedInviteLink(author?.displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder) ?? "", invite.link)
appendAttributedText(text: rawText, generateEntities: { index in
if index == 0, let author = author {
return [.TextMention(peerId: author.id)]
}
return []
}, to: &text, entities: &entities)
let action = TelegramMediaActionType.customText(text: text, entities: entities)
let message = Message(stableId: self.entry.stableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(bitPattern: self.entry.stableId)), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: "", attributes: [], media: [TelegramMediaAction(action: action)], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: [])
return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes()))
case let .revokeExportedInvitation(invite):
var peers = SimpleDictionary<PeerId, Peer>()
var author: Peer?
if let peer = self.entry.peers[self.entry.event.peerId] {
author = peer
peers[peer.id] = peer
}
var text: String = ""
var entities: [MessageTextEntity] = []
let rawText: (String, [(Int, NSRange)]) = self.presentationData.strings.Channel_AdminLog_RevokedInviteLink(author?.displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder) ?? "", invite.link)
appendAttributedText(text: rawText, generateEntities: { index in
if index == 0, let author = author {
return [.TextMention(peerId: author.id)]
}
return []
}, to: &text, entities: &entities)
let action = TelegramMediaActionType.customText(text: text, entities: entities)
let message = Message(stableId: self.entry.stableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(bitPattern: self.entry.stableId)), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: "", attributes: [], media: [TelegramMediaAction(action: action)], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: [])
return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes()))
case let .editExportedInvitation(previousInvite, updatedInvite):
var peers = SimpleDictionary<PeerId, Peer>()
var author: Peer?
if let peer = self.entry.peers[self.entry.event.peerId] {
author = peer
peers[peer.id] = peer
}
var text: String = ""
var entities: [MessageTextEntity] = []
let rawText: (String, [(Int, NSRange)]) = self.presentationData.strings.Channel_AdminLog_EditedInviteLink(author?.displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder) ?? "", updatedInvite.link)
appendAttributedText(text: rawText, generateEntities: { index in
if index == 0, let author = author {
return [.TextMention(peerId: author.id)]
}
return []
}, to: &text, entities: &entities)
let action = TelegramMediaActionType.customText(text: text, entities: entities)
let message = Message(stableId: self.entry.stableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(bitPattern: self.entry.stableId)), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: "", attributes: [], media: [TelegramMediaAction(action: action)], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: [])
return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes()))
}

View file

@ -224,7 +224,7 @@ public class ComposeControllerImpl: ViewController, ComposeController {
if let strongSelf = self {
let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 }
let controller = PermissionController(context: strongSelf.context, splashScreen: true)
controller.setState(.custom(icon: PermissionControllerCustomIcon(light: UIImage(bundleImageName: "Chat/Intro/ChannelIntro"), dark: nil), title: presentationData.strings.ChannelIntro_Title, subtitle: nil, text: presentationData.strings.ChannelIntro_Text, buttonTitle: presentationData.strings.ChannelIntro_CreateChannel, footerText: nil), animated: false)
controller.setState(.custom(icon: .animation("Channels"), title: presentationData.strings.ChannelIntro_ChannelsTitle, subtitle: nil, text: presentationData.strings.ChannelIntro_ChannelsText, buttonTitle: presentationData.strings.ChannelIntro_CreateChannel, secondaryButtonTitle: nil, footerText: nil), animated: false)
controller.proceed = { [weak self] result in
if let strongSelf = self {
(strongSelf.navigationController as? NavigationController)?.replaceTopController(createChannelController(context: strongSelf.context), animated: true)

View file

@ -145,7 +145,7 @@ private final class DrawingStickersScreenNode: ViewControllerTracingNode {
}, animateDiceSuccess: { _ in
}, greetingStickerNode: {
return nil
}, openPeerContextMenu: { _, _, _, _ in
}, openPeerContextMenu: { _, _, _, _, _ in
}, openMessageReplies: { _, _, _ in
}, openReplyThreadOriginalMessage: { _ in
}, openMessageStats: { _ in

View file

@ -138,7 +138,7 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, UIGestu
}, animateDiceSuccess: { _ in
}, greetingStickerNode: {
return nil
}, openPeerContextMenu: { _, _, _, _ in
}, openPeerContextMenu: { _, _, _, _, _ in
}, openMessageReplies: { _, _, _ in
}, openReplyThreadOriginalMessage: { _ in
}, openMessageStats: { _ in

View file

@ -36,7 +36,8 @@ private func chatInputStateString(attributedString: NSAttributedString) -> NSAtt
attributedString.enumerateAttributes(in: NSRange(location: 0, length: attributedString.length), options: [], using: { attributes, range, _ in
if let value = attributes[.link], let url = (value as? URL)?.absoluteString {
string.addAttribute(ChatTextInputAttributes.textUrl, value: ChatTextInputTextUrlAttribute(url: url), range: range)
} else if let value = attributes[.font], let font = value as? UIFont {
}
if let value = attributes[.font], let font = value as? UIFont {
let fontName = font.fontName.lowercased()
if fontName.contains("bolditalic") {
string.addAttribute(ChatTextInputAttributes.bold, value: true as NSNumber, range: range)
@ -49,6 +50,12 @@ private func chatInputStateString(attributedString: NSAttributedString) -> NSAtt
string.addAttribute(ChatTextInputAttributes.monospace, value: true as NSNumber, range: range)
}
}
if let _ = attributes[.strikethroughStyle] {
string.addAttribute(ChatTextInputAttributes.strikethrough, value: true as NSNumber, range: range)
}
if let _ = attributes[.underlineStyle] {
string.addAttribute(ChatTextInputAttributes.underline, value: true as NSNumber, range: range)
}
})
return string
}

View file

@ -6,12 +6,14 @@ final class PeerInfoScreenSwitchItem: PeerInfoScreenItem {
let id: AnyHashable
let text: String
let value: Bool
let icon: UIImage?
let toggled: ((Bool) -> Void)?
init(id: AnyHashable, text: String, value: Bool, toggled: ((Bool) -> Void)?) {
init(id: AnyHashable, text: String, value: Bool, icon: UIImage? = nil, toggled: ((Bool) -> Void)?) {
self.id = id
self.text = text
self.value = value
self.icon = icon
self.toggled = toggled
}
@ -22,6 +24,7 @@ final class PeerInfoScreenSwitchItem: PeerInfoScreenItem {
private final class PeerInfoScreenSwitchItemNode: PeerInfoScreenItemNode {
private let selectionNode: PeerInfoScreenSelectableBackgroundNode
private let iconNode: ASImageNode
private let textNode: ImmediateTextNode
private let switchNode: SwitchNode
private let bottomSeparatorNode: ASDisplayNode
@ -35,6 +38,10 @@ private final class PeerInfoScreenSwitchItemNode: PeerInfoScreenItemNode {
var bringToFrontForHighlightImpl: (() -> Void)?
self.selectionNode = PeerInfoScreenSelectableBackgroundNode(bringToFrontForHighlight: { bringToFrontForHighlightImpl?() })
self.iconNode = ASImageNode()
self.iconNode.isLayerBacked = true
self.iconNode.displaysAsynchronously = false
self.textNode = ImmediateTextNode()
self.textNode.displaysAsynchronously = false
self.textNode.isUserInteractionEnabled = false
@ -92,6 +99,8 @@ private final class PeerInfoScreenSwitchItemNode: PeerInfoScreenItemNode {
self.selectionNode.pressed = nil
let sideInset: CGFloat = 16.0 + safeInsets.left
let leftInset = (item.icon == nil ? sideInset : sideInset + 29.0 + 16.0)
let rightInset: CGFloat = 56.0 + safeAreaInsets.right
self.bottomSeparatorNode.backgroundColor = presentationData.theme.list.itemBlocksSeparatorColor
@ -104,11 +113,23 @@ private final class PeerInfoScreenSwitchItemNode: PeerInfoScreenItemNode {
self.activateArea.accessibilityValue = item.value ? presentationData.strings.VoiceOver_Common_On : presentationData.strings.VoiceOver_Common_Off
self.activateArea.accessibilityHint = presentationData.strings.VoiceOver_Common_SwitchHint
let textSize = self.textNode.updateLayout(CGSize(width: width - sideInset * 2.0 - 56.0, height: .greatestFiniteMagnitude))
let textFrame = CGRect(origin: CGPoint(x: sideInset, y: 12.0), size: textSize)
let textSize = self.textNode.updateLayout(CGSize(width: width - leftInset - rightInset, height: .greatestFiniteMagnitude))
let textFrame = CGRect(origin: CGPoint(x: leftInset, y: 12.0), size: textSize)
let height = textSize.height + 24.0
if let icon = item.icon {
if self.iconNode.supernode == nil {
self.addSubnode(self.iconNode)
}
self.iconNode.image = icon
let iconFrame = CGRect(origin: CGPoint(x: sideInset, y: floorToScreenPixels((height - icon.size.height) / 2.0)), size: icon.size)
transition.updateFrame(node: self.iconNode, frame: iconFrame)
} else if self.iconNode.supernode != nil {
self.iconNode.image = nil
self.iconNode.removeFromSupernode()
}
transition.updateFrame(node: self.textNode, frame: textFrame)
if let switchView = self.switchNode.view as? UISwitch {

View file

@ -1067,13 +1067,13 @@ private func infoItems(data: PeerInfoScreenData?, context: AccountContext, prese
let memberCount = cachedData.participantsSummary.memberCount ?? 0
let bannedCount = cachedData.participantsSummary.kickedCount ?? 0
items[.peerInfo]!.append(PeerInfoScreenDisclosureItem(id: ItemAdmins, label: .text("\(adminCount == 0 ? "" : "\(presentationStringsFormattedNumber(adminCount, presentationData.dateTimeFormat.groupingSeparator))")"), text: presentationData.strings.GroupInfo_Administrators, action: {
items[.peerInfo]!.append(PeerInfoScreenDisclosureItem(id: ItemAdmins, label: .text("\(adminCount == 0 ? "" : "\(presentationStringsFormattedNumber(adminCount, presentationData.dateTimeFormat.groupingSeparator))")"), text: presentationData.strings.GroupInfo_Administrators, icon: UIImage(bundleImageName: "Chat/Info/GroupAdminsIcon"), action: {
interaction.openParticipantsSection(.admins)
}))
items[.peerInfo]!.append(PeerInfoScreenDisclosureItem(id: ItemMembers, label: .text("\(memberCount == 0 ? "" : "\(presentationStringsFormattedNumber(memberCount, presentationData.dateTimeFormat.groupingSeparator))")"), text: presentationData.strings.Channel_Info_Subscribers, action: {
items[.peerInfo]!.append(PeerInfoScreenDisclosureItem(id: ItemMembers, label: .text("\(memberCount == 0 ? "" : "\(presentationStringsFormattedNumber(memberCount, presentationData.dateTimeFormat.groupingSeparator))")"), text: presentationData.strings.Channel_Info_Subscribers, icon: UIImage(bundleImageName: "Chat/Info/GroupMembersIcon"), action: {
interaction.openParticipantsSection(.members)
}))
items[.peerInfo]!.append(PeerInfoScreenDisclosureItem(id: ItemBanned, label: .text("\(bannedCount == 0 ? "" : "\(presentationStringsFormattedNumber(bannedCount, presentationData.dateTimeFormat.groupingSeparator))")"), text: presentationData.strings.GroupInfo_Permissions_Removed, action: {
items[.peerInfo]!.append(PeerInfoScreenDisclosureItem(id: ItemBanned, label: .text("\(bannedCount == 0 ? "" : "\(presentationStringsFormattedNumber(bannedCount, presentationData.dateTimeFormat.groupingSeparator))")"), text: presentationData.strings.GroupInfo_Permissions_Removed, icon: UIImage(bundleImageName: "Chat/Info/GroupRemovedIcon"), action: {
interaction.openParticipantsSection(.banned)
}))
}
@ -1141,32 +1141,32 @@ private func editingItems(data: PeerInfoScreenData?, context: AccountContext, pr
items[section] = []
}
if let data = data, let notificationSettings = data.notificationSettings {
let notificationsLabel: String
let soundLabel: String
if case let .muted(until) = notificationSettings.muteState, until >= Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970) {
if until < Int32.max - 1 {
notificationsLabel = stringForRemainingMuteInterval(strings: presentationData.strings, muteInterval: until)
} else {
notificationsLabel = presentationData.strings.UserInfo_NotificationsDisabled
}
} else {
notificationsLabel = presentationData.strings.UserInfo_NotificationsEnabled
}
let globalNotificationSettings: GlobalNotificationSettings = data.globalNotificationSettings ?? GlobalNotificationSettings.defaultSettings
soundLabel = localizedPeerNotificationSoundString(strings: presentationData.strings, sound: notificationSettings.messageSound, default: globalNotificationSettings.effective.privateChats.sound)
items[.notifications]!.append(PeerInfoScreenDisclosureItem(id: 0, label: .text(notificationsLabel), text: presentationData.strings.GroupInfo_Notifications, action: {
interaction.editingOpenNotificationSettings()
}))
items[.notifications]!.append(PeerInfoScreenDisclosureItem(id: 1, label: .text(soundLabel), text: presentationData.strings.GroupInfo_Sound, action: {
interaction.editingOpenSoundSettings()
}))
items[.notifications]!.append(PeerInfoScreenSwitchItem(id: 2, text: presentationData.strings.Notification_Exceptions_PreviewAlwaysOn, value: notificationSettings.displayPreviews != .hide, toggled: { value in
interaction.editingToggleShowMessageText(value)
}))
}
// if let data = data, let notificationSettings = data.notificationSettings {
// let notificationsLabel: String
// let soundLabel: String
// if case let .muted(until) = notificationSettings.muteState, until >= Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970) {
// if until < Int32.max - 1 {
// notificationsLabel = stringForRemainingMuteInterval(strings: presentationData.strings, muteInterval: until)
// } else {
// notificationsLabel = presentationData.strings.UserInfo_NotificationsDisabled
// }
// } else {
// notificationsLabel = presentationData.strings.UserInfo_NotificationsEnabled
// }
//
// let globalNotificationSettings: GlobalNotificationSettings = data.globalNotificationSettings ?? GlobalNotificationSettings.defaultSettings
// soundLabel = localizedPeerNotificationSoundString(strings: presentationData.strings, sound: notificationSettings.messageSound, default: globalNotificationSettings.effective.privateChats.sound)
//
// items[.notifications]!.append(PeerInfoScreenDisclosureItem(id: 0, label: .text(notificationsLabel), text: presentationData.strings.GroupInfo_Notifications, action: {
// interaction.editingOpenNotificationSettings()
// }))
// items[.notifications]!.append(PeerInfoScreenDisclosureItem(id: 1, label: .text(soundLabel), text: presentationData.strings.GroupInfo_Sound, action: {
// interaction.editingOpenSoundSettings()
// }))
// items[.notifications]!.append(PeerInfoScreenSwitchItem(id: 2, text: presentationData.strings.Notification_Exceptions_PreviewAlwaysOn, value: notificationSettings.displayPreviews != .hide, toggled: { value in
// interaction.editingToggleShowMessageText(value)
// }))
// }
if let data = data {
if let _ = data.peer as? TelegramUser {
@ -1192,12 +1192,23 @@ private func editingItems(data: PeerInfoScreenData?, context: AccountContext, pr
} else {
linkText = presentationData.strings.Channel_Setup_TypePrivate
}
items[.peerSettings]!.append(PeerInfoScreenDisclosureItem(id: ItemUsername, label: .text(linkText), text: presentationData.strings.Channel_TypeSetup_Title, action: {
items[.peerSettings]!.append(PeerInfoScreenDisclosureItem(id: ItemUsername, label: .text(linkText), text: presentationData.strings.Channel_TypeSetup_Title, icon: UIImage(bundleImageName: "Chat/Info/GroupChannelIcon"), action: {
interaction.editingOpenPublicLinkSetup()
}))
}
if channel.flags.contains(.isCreator) || (channel.adminRights != nil && channel.hasPermission(.pinMessages)) {
let invitesText: String
if let count = data.invitations?.count, count > 0 {
invitesText = "\(count)"
} else {
invitesText = ""
}
items[.peerSettings]!.append(PeerInfoScreenDisclosureItem(id: ItemInviteLinks, label: .text(invitesText), text: presentationData.strings.GroupInfo_InviteLinks, icon: UIImage(bundleImageName: "Chat/Info/GroupLinksIcon"), action: {
interaction.editingOpenInviteLinksSetup()
}))
let discussionGroupTitle: String
if let _ = data.cachedData as? CachedChannelData {
if let peer = data.linkedDiscussionPeer {
@ -1213,18 +1224,7 @@ private func editingItems(data: PeerInfoScreenData?, context: AccountContext, pr
discussionGroupTitle = "..."
}
let invitesText: String
if let count = data.invitations?.count, count > 0 {
invitesText = "\(count)"
} else {
invitesText = ""
}
items[.peerSettings]!.append(PeerInfoScreenDisclosureItem(id: ItemInviteLinks, label: .text(invitesText), text: presentationData.strings.GroupInfo_InviteLinks, action: {
interaction.editingOpenInviteLinksSetup()
}))
items[.peerSettings]!.append(PeerInfoScreenDisclosureItem(id: ItemDiscussionGroup, label: .text(discussionGroupTitle), text: presentationData.strings.Channel_DiscussionGroup, action: {
items[.peerSettings]!.append(PeerInfoScreenDisclosureItem(id: ItemDiscussionGroup, label: .text(discussionGroupTitle), text: presentationData.strings.Channel_DiscussionGroup, icon: UIImage(bundleImageName: "Chat/Info/GroupDiscussionIcon"), action: {
interaction.editingOpenDiscussionGroupSetup()
}))
@ -1235,7 +1235,7 @@ private func editingItems(data: PeerInfoScreenData?, context: AccountContext, pr
default:
messagesShouldHaveSignatures = false
}
items[.peerSettings]!.append(PeerInfoScreenSwitchItem(id: ItemSignMessages, text: presentationData.strings.Channel_SignMessages, value: messagesShouldHaveSignatures, toggled: { value in
items[.peerSettings]!.append(PeerInfoScreenSwitchItem(id: ItemSignMessages, text: presentationData.strings.Channel_SignMessages, value: messagesShouldHaveSignatures, icon: UIImage(bundleImageName: "Chat/Info/GroupSignIcon"), toggled: { value in
interaction.editingToggleMessageSignatures(value)
}))
items[.peerSettings]!.append(PeerInfoScreenCommentItem(id: ItemSignMessagesHelp, text: presentationData.strings.Channel_SignMessages_Help))
@ -1284,31 +1284,18 @@ private func editingItems(data: PeerInfoScreenData?, context: AccountContext, pr
} else {
linkText = presentationData.strings.GroupInfo_PublicLinkAdd
}
items[.peerSettings]!.append(PeerInfoScreenDisclosureItem(id: ItemUsername, label: .text(linkText), text: presentationData.strings.GroupInfo_PublicLink, action: {
items[.peerSettings]!.append(PeerInfoScreenDisclosureItem(id: ItemUsername, label: .text(linkText), text: presentationData.strings.GroupInfo_PublicLink, icon: UIImage(bundleImageName: "Chat/Info/GroupLinksIcon"), action: {
interaction.editingOpenPublicLinkSetup()
}))
}
} else {
if cachedData.flags.contains(.canChangeUsername) {
items[.peerPublicSettings]!.append(PeerInfoScreenDisclosureItem(id: ItemUsername, label: .text(isPublic ? presentationData.strings.Channel_Setup_TypePublic : presentationData.strings.Channel_Setup_TypePrivate), text: presentationData.strings.GroupInfo_GroupType, action: {
items[.peerPublicSettings]!.append(PeerInfoScreenDisclosureItem(id: ItemUsername, label: .text(isPublic ? presentationData.strings.Channel_Setup_TypePublic : presentationData.strings.Channel_Setup_TypePrivate), text: presentationData.strings.GroupInfo_GroupType, icon: UIImage(bundleImageName: "Chat/Info/GroupMembersIcon"), action: {
interaction.editingOpenPublicLinkSetup()
}))
}
if channel.hasPermission(.inviteMembers) {
let invitesText: String
if let count = data.invitations?.count, count > 0 {
invitesText = "\(count)"
} else {
invitesText = ""
}
items[.peerPublicSettings]!.append(PeerInfoScreenDisclosureItem(id: ItemInviteLinks, label: .text(invitesText), text: presentationData.strings.GroupInfo_InviteLinks, action: {
interaction.editingOpenInviteLinksSetup()
}))
}
if cachedData.flags.contains(.canChangeUsername) {
if let linkedDiscussionPeer = data.linkedDiscussionPeer {
let peerTitle: String
@ -1323,15 +1310,28 @@ private func editingItems(data: PeerInfoScreenData?, context: AccountContext, pr
}
}
if !isPublic, case .known(nil) = cachedData.linkedDiscussionPeerId {
items[.peerPublicSettings]!.append(PeerInfoScreenDisclosureItem(id: ItemPreHistory, label: .text(cachedData.flags.contains(.preHistoryEnabled) ? presentationData.strings.GroupInfo_GroupHistoryVisible : presentationData.strings.GroupInfo_GroupHistoryHidden), text: presentationData.strings.GroupInfo_GroupHistory, action: {
items[.peerPublicSettings]!.append(PeerInfoScreenDisclosureItem(id: ItemPreHistory, label: .text(cachedData.flags.contains(.preHistoryEnabled) ? presentationData.strings.GroupInfo_GroupHistoryVisible : presentationData.strings.GroupInfo_GroupHistoryHidden), text: presentationData.strings.GroupInfo_GroupHistoryShort, icon: UIImage(bundleImageName: "Chat/Info/GroupDiscussionIcon"), action: {
interaction.editingOpenPreHistorySetup()
}))
}
if channel.hasPermission(.inviteMembers) {
let invitesText: String
if let count = data.invitations?.count, count > 0 {
invitesText = "\(count)"
} else {
invitesText = ""
}
items[.peerSettings]!.append(PeerInfoScreenDisclosureItem(id: ItemInviteLinks, label: .text(invitesText), text: presentationData.strings.GroupInfo_InviteLinks, icon: UIImage(bundleImageName: "Chat/Info/GroupLinksIcon"), action: {
interaction.editingOpenInviteLinksSetup()
}))
}
}
}
if cachedData.flags.contains(.canSetStickerSet) && canEditPeerInfo(context: context, peer: channel) {
items[.peerSettings]!.append(PeerInfoScreenDisclosureItem(id: ItemStickerPack, label: .text(cachedData.stickerPack?.title ?? presentationData.strings.GroupInfo_SharedMediaNone), text: presentationData.strings.Stickers_GroupStickers, action: {
items[.peerSettings]!.append(PeerInfoScreenDisclosureItem(id: ItemStickerPack, label: .text(cachedData.stickerPack?.title ?? presentationData.strings.GroupInfo_SharedMediaNone), text: presentationData.strings.Stickers_GroupStickers, icon: UIImage(bundleImageName: "Settings/MenuIcons/Stickers"), action: {
interaction.editingOpenStickerPackSetup()
}))
}
@ -1355,11 +1355,11 @@ private func editingItems(data: PeerInfoScreenData?, context: AccountContext, pr
activePermissionCount = count
}
items[.peerSettings]!.append(PeerInfoScreenDisclosureItem(id: ItemPermissions, label: .text(activePermissionCount.flatMap({ "\($0)/\(allGroupPermissionList.count)" }) ?? ""), text: presentationData.strings.GroupInfo_Permissions, action: {
items[.peerSettings]!.append(PeerInfoScreenDisclosureItem(id: ItemPermissions, label: .text(activePermissionCount.flatMap({ "\($0)/\(allGroupPermissionList.count)" }) ?? ""), text: presentationData.strings.GroupInfo_Permissions, icon: UIImage(bundleImageName: "Settings/MenuIcons/SetPasscode"), action: {
interaction.openPermissions()
}))
items[.peerSettings]!.append(PeerInfoScreenDisclosureItem(id: ItemAdmins, label: .text(cachedData.participantsSummary.adminCount.flatMap { "\(presentationStringsFormattedNumber($0, presentationData.dateTimeFormat.groupingSeparator))" } ?? ""), text: presentationData.strings.GroupInfo_Administrators, action: {
items[.peerSettings]!.append(PeerInfoScreenDisclosureItem(id: ItemAdmins, label: .text(cachedData.participantsSummary.adminCount.flatMap { "\(presentationStringsFormattedNumber($0, presentationData.dateTimeFormat.groupingSeparator))" } ?? ""), text: presentationData.strings.GroupInfo_Administrators, icon: UIImage(bundleImageName: "Chat/Info/GroupAdminsIcon"), action: {
interaction.openParticipantsSection(.admins)
}))
}
@ -1367,32 +1367,21 @@ private func editingItems(data: PeerInfoScreenData?, context: AccountContext, pr
}
} else if let group = data.peer as? TelegramGroup {
let ItemUsername = 1
let ItemInviteLinks = 2
let ItemPreHistory = 3
let ItemPreHistory = 2
let ItemInviteLinks = 3
let ItemPermissions = 4
let ItemAdmins = 5
if case .creator = group.role {
if let cachedData = data.cachedData as? CachedGroupData {
if cachedData.flags.contains(.canChangeUsername) {
items[.peerPublicSettings]!.append(PeerInfoScreenDisclosureItem(id: ItemUsername, label: .text(presentationData.strings.Group_Setup_TypePrivate), text: presentationData.strings.GroupInfo_GroupType, action: {
items[.peerPublicSettings]!.append(PeerInfoScreenDisclosureItem(id: ItemUsername, label: .text(presentationData.strings.Group_Setup_TypePrivate), text: presentationData.strings.GroupInfo_GroupType, icon: UIImage(bundleImageName: "Chat/Info/GroupMembersIcon"), action: {
interaction.editingOpenPublicLinkSetup()
}))
}
}
let invitesText: String
if let count = data.invitations?.count, count > 0 {
invitesText = "\(count)"
} else {
invitesText = ""
}
items[.peerPublicSettings]!.append(PeerInfoScreenDisclosureItem(id: ItemInviteLinks, label: .text(invitesText), text: presentationData.strings.GroupInfo_InviteLinks, action: {
interaction.editingOpenInviteLinksSetup()
}))
items[.peerPublicSettings]!.append(PeerInfoScreenDisclosureItem(id: ItemPreHistory, label: .text(presentationData.strings.GroupInfo_GroupHistoryHidden), text: presentationData.strings.GroupInfo_GroupHistory, action: {
items[.peerPublicSettings]!.append(PeerInfoScreenDisclosureItem(id: ItemPreHistory, label: .text(presentationData.strings.GroupInfo_GroupHistoryHidden), text: presentationData.strings.GroupInfo_GroupHistoryShort, icon: UIImage(bundleImageName: "Chat/Info/GroupDiscussionIcon"), action: {
interaction.editingOpenPreHistorySetup()
}))
var activePermissionCount: Int?
@ -1406,11 +1395,22 @@ private func editingItems(data: PeerInfoScreenData?, context: AccountContext, pr
activePermissionCount = count
}
items[.peerSettings]!.append(PeerInfoScreenDisclosureItem(id: ItemPermissions, label: .text(activePermissionCount.flatMap({ "\($0)/\(allGroupPermissionList.count)" }) ?? ""), text: presentationData.strings.GroupInfo_Permissions, action: {
let invitesText: String
if let count = data.invitations?.count, count > 0 {
invitesText = "\(count)"
} else {
invitesText = ""
}
items[.peerSettings]!.append(PeerInfoScreenDisclosureItem(id: ItemInviteLinks, label: .text(invitesText), text: presentationData.strings.GroupInfo_InviteLinks, icon: UIImage(bundleImageName: "Chat/Info/GroupLinksIcon"), action: {
interaction.editingOpenInviteLinksSetup()
}))
items[.peerSettings]!.append(PeerInfoScreenDisclosureItem(id: ItemPermissions, label: .text(activePermissionCount.flatMap({ "\($0)/\(allGroupPermissionList.count)" }) ?? ""), text: presentationData.strings.GroupInfo_Permissions, icon: UIImage(bundleImageName: "Settings/MenuIcons/SetPasscode"), action: {
interaction.openPermissions()
}))
items[.peerSettings]!.append(PeerInfoScreenDisclosureItem(id: ItemAdmins, text: presentationData.strings.GroupInfo_Administrators, action: {
items[.peerSettings]!.append(PeerInfoScreenDisclosureItem(id: ItemAdmins, text: presentationData.strings.GroupInfo_Administrators, icon: UIImage(bundleImageName: "Chat/Info/GroupAdminsIcon"), action: {
interaction.openParticipantsSection(.admins)
}))
} else if case .admin = group.role {
@ -2054,7 +2054,7 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD
}, animateDiceSuccess: { _ in
}, greetingStickerNode: {
return nil
}, openPeerContextMenu: { _, _, _, _ in
}, openPeerContextMenu: { _, _, _, _, _ in
}, openMessageReplies: { _, _, _ in
}, openReplyThreadOriginalMessage: { _ in
}, openMessageStats: { _ in
@ -2946,7 +2946,6 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD
var items: [ActionSheetItem] = []
let muteValues: [Int32] = [
1 * 60 * 60,
24 * 60 * 60,
2 * 24 * 60 * 60,
Int32.max
]
@ -2964,6 +2963,49 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD
}))
}
items.insert(ActionSheetButtonItem(title: self.presentationData.strings.PeerInfo_CustomizeNotifications, action: {
guard let peer = self.data?.peer else {
return
}
dismissAction()
let context = self.context
let updatePeerSound: (PeerId, PeerMessageSound) -> Signal<Void, NoError> = { peerId, sound in
return updatePeerNotificationSoundInteractive(account: context.account, peerId: peerId, sound: sound) |> deliverOnMainQueue
}
let updatePeerNotificationInterval: (PeerId, Int32?) -> Signal<Void, NoError> = { peerId, muteInterval in
return updatePeerMuteSetting(account: context.account, peerId: peerId, muteInterval: muteInterval) |> deliverOnMainQueue
}
let updatePeerDisplayPreviews:(PeerId, PeerNotificationDisplayPreviews) -> Signal<Void, NoError> = {
peerId, displayPreviews in
return updatePeerDisplayPreviewsSetting(account: context.account, peerId: peerId, displayPreviews: displayPreviews) |> deliverOnMainQueue
}
let exceptionController = notificationPeerExceptionController(context: context, peer: peer, mode: .users([:]), edit: true, updatePeerSound: { peerId, sound in
let _ = (updatePeerSound(peer.id, sound)
|> deliverOnMainQueue).start(next: { _ in
})
}, updatePeerNotificationInterval: { peerId, muteInterval in
let _ = (updatePeerNotificationInterval(peerId, muteInterval)
|> deliverOnMainQueue).start(next: { _ in
})
}, updatePeerDisplayPreviews: { peerId, displayPreviews in
let _ = (updatePeerDisplayPreviews(peerId, displayPreviews)
|> deliverOnMainQueue).start(next: { _ in
})
}, removePeerFromExceptions: {
}, modifiedPeer: {
})
exceptionController.navigationPresentation = .modal
controller.push(exceptionController)
}), at: items.count - 1)
actionSheet.setItemGroups([
ActionSheetItemGroup(items: items),
ActionSheetItemGroup(items: [ActionSheetButtonItem(title: self.presentationData.strings.Common_Cancel, action: { dismissAction() })])
@ -5634,6 +5676,8 @@ public final class PeerInfoScreen: ViewController {
strongSelf.tabBarItem.badgeValue = badgeValue
}
})
self.navigationItem.backBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Back, style: .plain, target: nil, action: nil)
}
self.navigationBar?.makeCustomTransitionNode = { [weak self] other, isInteractive in
@ -5680,6 +5724,10 @@ public final class PeerInfoScreen: ViewController {
if previousTheme !== presentationData.theme || previousStrings !== presentationData.strings {
strongSelf.controllerNode.updatePresentationData(strongSelf.presentationData)
if strongSelf.navigationItem.backBarButtonItem != nil {
strongSelf.navigationItem.backBarButtonItem = UIBarButtonItem(title: strongSelf.presentationData.strings.Common_Back, style: .plain, target: nil, action: nil)
}
}
}
})

View file

@ -1256,7 +1256,7 @@ public final class SharedAccountContextImpl: SharedAccountContext {
}, animateDiceSuccess: { _ in
}, greetingStickerNode: {
return nil
}, openPeerContextMenu: { _, _, _, _ in
}, openPeerContextMenu: { _, _, _, _, _ in
}, openMessageReplies: { _, _, _ in
}, openReplyThreadOriginalMessage: { _ in
}, openMessageStats: { _ in

View file

@ -187,6 +187,7 @@ public enum TextSelectionAction {
case copy
case share
case lookup
case speak
}
public final class TextSelectionNode: ASDisplayNode {
@ -503,10 +504,17 @@ public final class TextSelectionNode: ASDisplayNode {
self?.performAction(attributedText, .lookup)
self?.dismissSelection()
}))
if isSpeakSelectionEnabled() {
actions.append(ContextMenuAction(content: .text(title: self.strings.Conversation_ContextMenuSpeak, accessibilityLabel: self.strings.Conversation_ContextMenuSpeak), action: { [weak self] in
self?.performAction(attributedText, .speak)
self?.dismissSelection()
}))
}
actions.append(ContextMenuAction(content: .text(title: self.strings.Conversation_ContextMenuShare, accessibilityLabel: self.strings.Conversation_ContextMenuShare), action: { [weak self] in
self?.performAction(attributedText, .share)
self?.dismissSelection()
}))
self.present(ContextMenuController(actions: actions, catchTapsOutside: false, hasHapticFeedback: false), ContextMenuControllerPresentationArguments(sourceNodeAndRect: { [weak self] in
guard let strongSelf = self, let rootNode = strongSelf.rootNode else {
return nil

View file

@ -141,6 +141,8 @@ public func parseInternalUrl(query: String) -> ParsedInternalUrl? {
}
} else if pathComponents[0].hasPrefix(phonebookUsernamePathPrefix), let idValue = Int32(String(pathComponents[0][pathComponents[0].index(pathComponents[0].startIndex, offsetBy: phonebookUsernamePathPrefix.count)...])) {
return .peerId(PeerId(namespace: Namespaces.Peer.CloudUser, id: idValue))
} else if pathComponents[0].hasPrefix("+") || pathComponents[0].hasPrefix("%20") {
return .join(String(pathComponents[0].dropFirst()))
}
return .peerName(peerName, nil)
} else if pathComponents.count == 2 || pathComponents.count == 3 {