mirror of
https://github.com/TelegramMessenger/Telegram-iOS.git
synced 2026-07-05 19:28:46 +02:00
Various Improvements
This commit is contained in:
parent
ca993ccfa5
commit
1f6b5ba9e0
79 changed files with 38285 additions and 36596 deletions
BIN
Telegram/Telegram-iOS/Resources/Channels.tgs
Normal file
BIN
Telegram/Telegram-iOS/Resources/Channels.tgs
Normal file
Binary file not shown.
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -598,7 +598,7 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture
|
|||
if strongSelf.reorderFeedback == nil {
|
||||
strongSelf.reorderFeedback = HapticFeedback()
|
||||
}
|
||||
strongSelf.reorderFeedback?.tap()
|
||||
strongSelf.reorderFeedback?.impact()
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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] = []
|
||||
|
|
|
|||
|
|
@ -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? {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
827
submodules/InviteLinksUI/Sources/ItemListInviteLinkItem.swift
Normal file
827
submodules/InviteLinksUI/Sources/ItemListInviteLinkItem.swift
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
7621
submodules/TelegramApi/Sources/Api4.swift
Normal file
7621
submodules/TelegramApi/Sources/Api4.swift
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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)])
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 [:]
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ swift_library(
|
|||
"//submodules/SolidRoundedButtonNode:SolidRoundedButtonNode",
|
||||
"//submodules/AppBundle:AppBundle",
|
||||
"//submodules/PresentationDataUtils:PresentationDataUtils",
|
||||
"//submodules/AnimatedStickerNode:AnimatedStickerNode",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -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)
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
|||
12
submodules/TelegramUI/Images.xcassets/Chat/Info/GroupChannelIcon.imageset/Contents.json
vendored
Normal file
12
submodules/TelegramUI/Images.xcassets/Chat/Info/GroupChannelIcon.imageset/Contents.json
vendored
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "ic_channel.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
BIN
submodules/TelegramUI/Images.xcassets/Chat/Info/GroupChannelIcon.imageset/ic_channel.pdf
vendored
Normal file
BIN
submodules/TelegramUI/Images.xcassets/Chat/Info/GroupChannelIcon.imageset/ic_channel.pdf
vendored
Normal file
Binary file not shown.
12
submodules/TelegramUI/Images.xcassets/Chat/Info/GroupDiscussionIcon.imageset/Contents.json
vendored
Normal file
12
submodules/TelegramUI/Images.xcassets/Chat/Info/GroupDiscussionIcon.imageset/Contents.json
vendored
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "ic_discussion.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
BIN
submodules/TelegramUI/Images.xcassets/Chat/Info/GroupDiscussionIcon.imageset/ic_discussion.pdf
vendored
Normal file
BIN
submodules/TelegramUI/Images.xcassets/Chat/Info/GroupDiscussionIcon.imageset/ic_discussion.pdf
vendored
Normal file
Binary file not shown.
12
submodules/TelegramUI/Images.xcassets/Chat/Info/GroupLinksIcon.imageset/Contents.json
vendored
Normal file
12
submodules/TelegramUI/Images.xcassets/Chat/Info/GroupLinksIcon.imageset/Contents.json
vendored
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "Links.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
BIN
submodules/TelegramUI/Images.xcassets/Chat/Info/GroupLinksIcon.imageset/Links.pdf
vendored
Normal file
BIN
submodules/TelegramUI/Images.xcassets/Chat/Info/GroupLinksIcon.imageset/Links.pdf
vendored
Normal file
Binary file not shown.
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
BIN
submodules/TelegramUI/Images.xcassets/Chat/Info/GroupMembersIcon.imageset/ic_members.pdf
vendored
Normal file
BIN
submodules/TelegramUI/Images.xcassets/Chat/Info/GroupMembersIcon.imageset/ic_members.pdf
vendored
Normal file
Binary file not shown.
12
submodules/TelegramUI/Images.xcassets/Chat/Info/GroupPreviewIcon.imageset/Contents.json
vendored
Normal file
12
submodules/TelegramUI/Images.xcassets/Chat/Info/GroupPreviewIcon.imageset/Contents.json
vendored
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "ic_preview.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
BIN
submodules/TelegramUI/Images.xcassets/Chat/Info/GroupPreviewIcon.imageset/ic_preview.pdf
vendored
Normal file
BIN
submodules/TelegramUI/Images.xcassets/Chat/Info/GroupPreviewIcon.imageset/ic_preview.pdf
vendored
Normal file
Binary file not shown.
12
submodules/TelegramUI/Images.xcassets/Chat/Info/GroupRemovedIcon.imageset/Contents.json
vendored
Normal file
12
submodules/TelegramUI/Images.xcassets/Chat/Info/GroupRemovedIcon.imageset/Contents.json
vendored
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "Removed.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
BIN
submodules/TelegramUI/Images.xcassets/Chat/Info/GroupRemovedIcon.imageset/Removed.pdf
vendored
Normal file
BIN
submodules/TelegramUI/Images.xcassets/Chat/Info/GroupRemovedIcon.imageset/Removed.pdf
vendored
Normal file
Binary file not shown.
12
submodules/TelegramUI/Images.xcassets/Chat/Info/GroupSignIcon.imageset/Contents.json
vendored
Normal file
12
submodules/TelegramUI/Images.xcassets/Chat/Info/GroupSignIcon.imageset/Contents.json
vendored
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "ic_sign.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
BIN
submodules/TelegramUI/Images.xcassets/Chat/Info/GroupSignIcon.imageset/ic_sign.pdf
vendored
Normal file
BIN
submodules/TelegramUI/Images.xcassets/Chat/Info/GroupSignIcon.imageset/ic_sign.pdf
vendored
Normal file
Binary file not shown.
12
submodules/TelegramUI/Images.xcassets/Chat/Info/GroupSoundIcon.imageset/Contents.json
vendored
Normal file
12
submodules/TelegramUI/Images.xcassets/Chat/Info/GroupSoundIcon.imageset/Contents.json
vendored
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "ic_sound.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
BIN
submodules/TelegramUI/Images.xcassets/Chat/Info/GroupSoundIcon.imageset/ic_sound.pdf
vendored
Normal file
BIN
submodules/TelegramUI/Images.xcassets/Chat/Info/GroupSoundIcon.imageset/ic_sound.pdf
vendored
Normal file
Binary file not shown.
|
|
@ -1,9 +1,9 @@
|
|||
{
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
},
|
||||
"properties" : {
|
||||
"provides-namespace" : true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
12
submodules/TelegramUI/Images.xcassets/Item List/Reorder.imageset/Contents.json
vendored
Normal file
12
submodules/TelegramUI/Images.xcassets/Item List/Reorder.imageset/Contents.json
vendored
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "ic_dragcell.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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?
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -453,7 +453,7 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode {
|
|||
}, animateDiceSuccess: { _ in
|
||||
}, greetingStickerNode: {
|
||||
return nil
|
||||
}, openPeerContextMenu: { _, _, _, _ in
|
||||
}, openPeerContextMenu: { _, _, _, _, _ in
|
||||
}, openMessageReplies: { _, _, _ in
|
||||
}, openReplyThreadOriginalMessage: { _ in
|
||||
}, openMessageStats: { _ in
|
||||
|
|
|
|||
|
|
@ -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()))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -145,7 +145,7 @@ private final class DrawingStickersScreenNode: ViewControllerTracingNode {
|
|||
}, animateDiceSuccess: { _ in
|
||||
}, greetingStickerNode: {
|
||||
return nil
|
||||
}, openPeerContextMenu: { _, _, _, _ in
|
||||
}, openPeerContextMenu: { _, _, _, _, _ in
|
||||
}, openMessageReplies: { _, _, _ in
|
||||
}, openReplyThreadOriginalMessage: { _ in
|
||||
}, openMessageStats: { _ in
|
||||
|
|
|
|||
|
|
@ -138,7 +138,7 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, UIGestu
|
|||
}, animateDiceSuccess: { _ in
|
||||
}, greetingStickerNode: {
|
||||
return nil
|
||||
}, openPeerContextMenu: { _, _, _, _ in
|
||||
}, openPeerContextMenu: { _, _, _, _, _ in
|
||||
}, openMessageReplies: { _, _, _ in
|
||||
}, openReplyThreadOriginalMessage: { _ in
|
||||
}, openMessageStats: { _ in
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1256,7 +1256,7 @@ public final class SharedAccountContextImpl: SharedAccountContext {
|
|||
}, animateDiceSuccess: { _ in
|
||||
}, greetingStickerNode: {
|
||||
return nil
|
||||
}, openPeerContextMenu: { _, _, _, _ in
|
||||
}, openPeerContextMenu: { _, _, _, _, _ in
|
||||
}, openMessageReplies: { _, _, _ in
|
||||
}, openReplyThreadOriginalMessage: { _ in
|
||||
}, openMessageStats: { _ in
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue