Invite Links Improvements

This commit is contained in:
Ilya Laktyushin 2021-02-16 21:18:47 +04:00
parent 7aa3c20140
commit e5882cb0dc
22 changed files with 2659 additions and 1780 deletions

View File

@ -1087,6 +1087,8 @@
"Time.PreciseDate_m11" = "Nov %1$@, %2$@ at %3$@"; "Time.PreciseDate_m11" = "Nov %1$@, %2$@ at %3$@";
"Time.PreciseDate_m12" = "Dec %1$@, %2$@ at %3$@"; "Time.PreciseDate_m12" = "Dec %1$@, %2$@ at %3$@";
"Time.MediumDate" = "%1$@ at %2$@";
"MuteFor.Hours_1" = "Mute for 1 hour"; "MuteFor.Hours_1" = "Mute for 1 hour";
"MuteFor.Hours_2" = "Mute for 2 hours"; "MuteFor.Hours_2" = "Mute for 2 hours";
"MuteFor.Hours_3_10" = "Mute for %@ hours"; "MuteFor.Hours_3_10" = "Mute for %@ hours";
@ -6102,3 +6104,5 @@ Sorry for the inconvenience.";
"Conversation.AutoremoveTimerRemovedGroup" = "A group admin disabled the auto-delete timer"; "Conversation.AutoremoveTimerRemovedGroup" = "A group admin disabled the auto-delete timer";
"Conversation.AutoremoveTimerSetChannel" = "Messages in this channel will be automatically deleted after %1$@"; "Conversation.AutoremoveTimerSetChannel" = "Messages in this channel will be automatically deleted after %1$@";
"Conversation.AutoremoveTimerRemovedChannel" = "Messages in this channel will no longer be automatically deleted"; "Conversation.AutoremoveTimerRemovedChannel" = "Messages in this channel will no longer be automatically deleted";
"Conversation.GigagroupDescription" = "Only admins can send messages in this group.";

View File

@ -917,29 +917,67 @@ private class TimeInputView: UIView, UIKeyInput {
return !self.text.isEmpty return !self.text.isEmpty
} }
var focusUpdated: ((Bool) -> Void)?
var textUpdated: ((String) -> Void)? var textUpdated: ((String) -> Void)?
override func becomeFirstResponder() -> Bool {
if self.isFirstResponder {
self.didReset = false
}
let result = super.becomeFirstResponder()
self.focusUpdated?(true)
return result
}
override func resignFirstResponder() -> Bool {
let result = super.resignFirstResponder()
self.focusUpdated?(false)
return result
}
var didReset = false
private let nonDigits = CharacterSet.decimalDigits.inverted private let nonDigits = CharacterSet.decimalDigits.inverted
func insertText(_ text: String) { func insertText(_ text: String) {
if text.rangeOfCharacter(from: nonDigits) != nil { if text.rangeOfCharacter(from: nonDigits) != nil {
return return
} }
if !self.didReset {
self.text = ""
self.didReset = true
}
var updatedText = self.text var updatedText = self.text
updatedText.append(updatedText) updatedText.append(text)
updatedText = String(updatedText.suffix(4))
self.text = updatedText self.text = updatedText
self.textUpdated?(self.text) self.textUpdated?(self.text)
} }
func deleteBackward() { func deleteBackward() {
self.didReset = true
var updatedText = self.text var updatedText = self.text
if !updatedText.isEmpty {
updatedText.removeLast() updatedText.removeLast()
}
self.text = updatedText self.text = updatedText
self.textUpdated?(self.text) self.textUpdated?(self.text)
} }
} }
private class TimeInputNode: ASDisplayNode { private class TimeInputNode: ASDisplayNode {
var text: String {
get {
if let view = self.view as? TimeInputView {
return view.text
} else {
return ""
}
}
set {
if let view = self.view as? TimeInputView {
view.text = newValue
}
}
}
var textUpdated: ((String) -> Void)? { var textUpdated: ((String) -> Void)? {
didSet { didSet {
if let view = self.view as? TimeInputView { if let view = self.view as? TimeInputView {
@ -948,6 +986,14 @@ private class TimeInputNode: ASDisplayNode {
} }
} }
var focusUpdated: ((Bool) -> Void)? {
didSet {
if let view = self.view as? TimeInputView {
view.focusUpdated = self.focusUpdated
}
}
}
override init() { override init() {
super.init() super.init()
@ -959,56 +1005,43 @@ private class TimeInputNode: ASDisplayNode {
override func didLoad() { override func didLoad() {
super.didLoad() super.didLoad()
self.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.handleTap)))
if let view = self.view as? TimeInputView { if let view = self.view as? TimeInputView {
view.textUpdated = self.textUpdated view.textUpdated = self.textUpdated
} }
} }
@objc private func handleTap() {
self.view.becomeFirstResponder()
}
}
public func stringTimestamp(hours: Int32, minutes: Int32, dateTimeFormat: PresentationDateTimeFormat) -> String {
switch dateTimeFormat.timeFormat {
case .regular:
let hourString: String
if hours == 0 {
hourString = "12"
} else if hours > 12 {
hourString = "\(hours - 12)"
} else {
hourString = "\(hours)"
}
let periodString: String
if hours >= 12 {
periodString = "PM"
} else {
periodString = "AM"
}
if minutes >= 10 {
return "\(hourString) \(minutes) \(periodString)"
} else {
return "\(hourString):0\(minutes) \(periodString)"
}
case .military:
return String(format: "%02d %02d", arguments: [Int(hours), Int(minutes)])
}
} }
private final class TimePickerNode: ASDisplayNode { private final class TimePickerNode: ASDisplayNode {
enum Selection {
case none
case hours
case minutes
case all
}
private var theme: DatePickerTheme private var theme: DatePickerTheme
private let dateTimeFormat: PresentationDateTimeFormat private let dateTimeFormat: PresentationDateTimeFormat
private let backgroundNode: ASDisplayNode private let backgroundNode: ASDisplayNode
private let textNode: ImmediateTextNode private let hoursNode: TapeNode
private let minutesNode: TapeNode
private let hoursTopMaskNode: ASDisplayNode
private let hoursBottomMaskNode: ASDisplayNode
private let minutesTopMaskNode: ASDisplayNode
private let minutesBottomMaskNode: ASDisplayNode
private let colonNode: ImmediateTextNode private let colonNode: ImmediateTextNode
private let borderNode: ASDisplayNode
private let inputNode: TimeInputNode private let inputNode: TimeInputNode
private let amPMSelectorNode: SegmentedControlNode private let amPMSelectorNode: SegmentedControlNode
private var typing = false
private var typingString = ""
private var typingHours: Int?
private var typingMinutes: Int?
private let hoursTypingNode: ImmediateTextNode
private let minutesTypingNode: ImmediateTextNode
var date: Date? { var date: Date? {
didSet { didSet {
if let size = self.validLayout { if let size = self.validLayout {
@ -1027,15 +1060,42 @@ private final class TimePickerNode: ASDisplayNode {
self.theme = theme self.theme = theme
self.dateTimeFormat = dateTimeFormat self.dateTimeFormat = dateTimeFormat
self.date = date self.date = date
self.selection = .none
self.backgroundNode = ASDisplayNode() self.backgroundNode = ASDisplayNode()
self.backgroundNode.backgroundColor = theme.segmentedControlTheme.backgroundColor self.backgroundNode.backgroundColor = theme.segmentedControlTheme.backgroundColor
self.backgroundNode.cornerRadius = 9.0 self.backgroundNode.cornerRadius = 9.0
self.textNode = ImmediateTextNode() self.borderNode = ASDisplayNode()
self.borderNode.cornerRadius = 9.0
self.borderNode.isUserInteractionEnabled = false
self.borderNode.isHidden = true
self.borderNode.borderWidth = 2.0
self.borderNode.borderColor = theme.accentColor.cgColor
self.colonNode = ImmediateTextNode() self.colonNode = ImmediateTextNode()
self.hoursNode = TapeNode()
self.minutesNode = TapeNode()
self.hoursTypingNode = ImmediateTextNode()
self.hoursTypingNode.isHidden = true
self.hoursTypingNode.textAlignment = .right
self.minutesTypingNode = ImmediateTextNode()
self.minutesTypingNode.isHidden = true
self.minutesTypingNode.textAlignment = .right
self.inputNode = TimeInputNode() self.inputNode = TimeInputNode()
self.hoursTopMaskNode = ASDisplayNode()
self.hoursTopMaskNode.backgroundColor = theme.segmentedControlTheme.backgroundColor
self.hoursBottomMaskNode = ASDisplayNode()
self.hoursBottomMaskNode.backgroundColor = theme.segmentedControlTheme.backgroundColor
self.minutesTopMaskNode = ASDisplayNode()
self.minutesTopMaskNode.backgroundColor = theme.segmentedControlTheme.backgroundColor
self.minutesBottomMaskNode = ASDisplayNode()
self.minutesBottomMaskNode.backgroundColor = theme.segmentedControlTheme.backgroundColor
let isPM: Bool let isPM: Bool
if let date = date { if let date = date {
let hours = calendar.component(.hour, from: date) let hours = calendar.component(.hour, from: date)
@ -1049,13 +1109,21 @@ private final class TimePickerNode: ASDisplayNode {
super.init() super.init()
self.addSubnode(self.backgroundNode) self.addSubnode(self.backgroundNode)
self.addSubnode(self.textNode) self.addSubnode(self.colonNode)
// self.addSubnode(self.colonNode) self.addSubnode(self.hoursNode)
// self.addSubnode(self.inputNode) self.addSubnode(self.minutesNode)
self.addSubnode(self.hoursTopMaskNode)
self.addSubnode(self.hoursBottomMaskNode)
self.addSubnode(self.minutesTopMaskNode)
self.addSubnode(self.minutesBottomMaskNode)
self.addSubnode(self.hoursTypingNode)
self.addSubnode(self.minutesTypingNode)
self.addSubnode(self.borderNode)
self.addSubnode(self.inputNode)
self.addSubnode(self.amPMSelectorNode) self.addSubnode(self.amPMSelectorNode)
self.amPMSelectorNode.selectedIndexChanged = { index in self.amPMSelectorNode.selectedIndexChanged = { [weak self] index in
guard let date = self.date else { guard let strongSelf = self, let date = strongSelf.date else {
return return
} }
let hours = calendar.component(.hour, from: date) let hours = calendar.component(.hour, from: date)
@ -1066,12 +1134,376 @@ private final class TimePickerNode: ASDisplayNode {
components.hour = hours + 12 components.hour = hours + 12
} }
if let newDate = calendar.date(from: components) { if let newDate = calendar.date(from: components) {
strongSelf.date = newDate
strongSelf.valueChanged?(newDate)
}
}
self.inputNode.textUpdated = { [weak self] text in
self?.handleTextInput(text)
}
self.inputNode.focusUpdated = { [weak self] focus in
if focus {
self?.selection = .all
} else {
self?.selection = .none
}
}
self.hoursNode.count = {
switch dateTimeFormat.timeFormat {
case .military:
return 24
case .regular:
return 12
}
}
self.hoursNode.titleAt = { i in
switch dateTimeFormat.timeFormat {
case .military:
if i < 10 {
return "0\(i)"
} else {
return "\(i)"
}
case .regular:
if i < 10 {
return "0\(i)"
} else {
return "\(1 + i)"
}
}
}
self.hoursNode.isScrollingUpdated = { [weak self] scrolling in
if let strongSelf = self {
if scrolling {
strongSelf.typing = false
strongSelf.selection = .hours
} else {
if strongSelf.inputNode.view.isFirstResponder {
strongSelf.selection = .all
} else {
strongSelf.selection = .none
}
}
}
}
self.hoursNode.selected = { [weak self] index in
guard let strongSelf = self else {
return
}
switch dateTimeFormat.timeFormat {
case .military:
let hour = index
if let date = strongSelf.date {
var components = calendar.dateComponents([.year, .month, .day, .hour, .minute], from: date)
components.hour = hour
if let newDate = calendar.date(from: components) {
strongSelf.date = newDate
strongSelf.valueChanged?(newDate)
}
}
case .regular:
break
}
}
self.minutesNode.count = {
return 60
}
self.minutesNode.titleAt = { i in
if i < 10 {
return "0\(i)"
} else {
return "\(i)"
}
}
self.minutesNode.isScrollingUpdated = { [weak self] scrolling in
if let strongSelf = self {
if scrolling {
strongSelf.typing = false
strongSelf.selection = .minutes
} else {
if strongSelf.inputNode.view.isFirstResponder {
strongSelf.selection = .all
} else {
strongSelf.selection = .none
}
}
}
}
self.minutesNode.selected = { [weak self] index in
guard let strongSelf = self else {
return
}
switch dateTimeFormat.timeFormat {
case .military:
let minute = index
if let date = strongSelf.date {
var components = calendar.dateComponents([.year, .month, .day, .hour, .minute], from: date)
components.minute = minute
if let newDate = calendar.date(from: components) {
strongSelf.date = newDate
strongSelf.valueChanged?(newDate)
}
}
case .regular:
break
}
}
}
override func didLoad() {
super.didLoad()
self.view.disablesInteractiveTransitionGestureRecognizer = true
self.view.disablesInteractiveModalDismiss = true
self.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.handleTap(_:))))
}
private func handleTextInput(_ input: String) {
self.typing = true
var text = input
var typingHours: Int?
var typingMinutes: Int?
if self.selection == .all {
text = String(text.suffix(4))
if text.count < 2 {
typingHours = nil
} else {
if var value = Int(String(text.prefix(2))) {
if value > 24 {
value = value % 10
}
typingHours = value
}
}
if var value = Int(String(text.suffix(2))) {
if value >= 60 {
value = value % 10
}
typingMinutes = value
}
} else if self.selection == .hours {
text = String(text.suffix(2))
if var value = Int(text) {
if value > 24 {
value = value % 10
}
typingHours = value
} else {
typingHours = nil
}
} else if self.selection == .minutes {
text = String(text.suffix(2))
if var value = Int(text) {
if value >= 60 {
value = value % 10
}
typingMinutes = value
} else {
typingMinutes = nil
}
}
self.typingHours = typingHours
self.typingMinutes = typingMinutes
if let date = self.date {
var components = calendar.dateComponents([.year, .month, .day, .hour, .minute], from: date)
if let typingHours = typingHours {
components.hour = typingHours
}
if let typingMinutes = typingMinutes {
components.minute = typingMinutes
}
if let newDate = calendar.date(from: components) {
self.date = newDate
self.valueChanged?(newDate) self.valueChanged?(newDate)
self.updateTapes()
} }
} }
self.inputNode.textUpdated = { text in self.update()
}
private var selection: Selection {
didSet {
self.update()
}
}
private func update() {
if case .none = self.selection {
self.borderNode.isHidden = true
} else {
self.borderNode.isHidden = false
}
let colonColor: UIColor
switch self.selection {
case .none:
colonColor = self.theme.textColor
self.colonNode.alpha = 1.0
self.hoursNode.textColor = self.theme.textColor
self.minutesNode.textColor = self.theme.textColor
self.hoursNode.alpha = 1.0
self.minutesNode.alpha = 1.0
self.hoursTopMaskNode.alpha = 1.0
self.hoursBottomMaskNode.alpha = 1.0
self.minutesTopMaskNode.alpha = 1.0
self.minutesBottomMaskNode.alpha = 1.0
self.typing = false
self.typingHours = nil
self.typingMinutes = nil
self.hoursTypingNode.isHidden = true
self.minutesTypingNode.isHidden = true
self.hoursNode.isHidden = false
self.minutesNode.isHidden = false
case .hours:
colonColor = self.theme.textColor
self.colonNode.alpha = 0.35
self.hoursNode.textColor = self.theme.accentColor
self.minutesNode.textColor = self.theme.textColor
self.hoursNode.alpha = 1.0
self.minutesNode.alpha = 0.35
self.hoursTopMaskNode.alpha = 0.5
self.hoursBottomMaskNode.alpha = 0.5
self.minutesTopMaskNode.alpha = 1.0
self.minutesBottomMaskNode.alpha = 1.0
if self.typing {
self.hoursTypingNode.isHidden = false
self.minutesTypingNode.isHidden = true
self.hoursNode.isHidden = true
self.minutesNode.isHidden = false
} else {
self.hoursTypingNode.isHidden = true
self.minutesTypingNode.isHidden = true
self.hoursNode.isHidden = false
self.minutesNode.isHidden = false
}
case .minutes:
colonColor = self.theme.textColor
self.colonNode.alpha = 0.35
self.hoursNode.textColor = self.theme.textColor
self.minutesNode.textColor = self.theme.accentColor
self.hoursNode.alpha = 0.35
self.minutesNode.alpha = 1.0
self.hoursTopMaskNode.alpha = 1.0
self.hoursBottomMaskNode.alpha = 1.0
self.minutesTopMaskNode.alpha = 0.5
self.minutesBottomMaskNode.alpha = 0.5
if self.typing {
self.hoursTypingNode.isHidden = true
self.minutesTypingNode.isHidden = false
self.hoursNode.isHidden = false
self.minutesNode.isHidden = true
} else {
self.hoursTypingNode.isHidden = true
self.minutesTypingNode.isHidden = true
self.hoursNode.isHidden = false
self.minutesNode.isHidden = false
}
case .all:
colonColor = self.theme.accentColor
self.colonNode.alpha = 1.0
self.hoursNode.textColor = self.theme.accentColor
self.minutesNode.textColor = self.theme.accentColor
self.hoursNode.alpha = 1.0
self.minutesNode.alpha = 1.0
self.hoursTopMaskNode.alpha = 0.5
self.hoursBottomMaskNode.alpha = 0.5
self.minutesTopMaskNode.alpha = 0.5
self.minutesBottomMaskNode.alpha = 0.5
if self.typing {
self.hoursTypingNode.isHidden = false
self.minutesTypingNode.isHidden = false
self.hoursNode.isHidden = true
self.minutesNode.isHidden = true
} else {
self.hoursTypingNode.isHidden = true
self.minutesTypingNode.isHidden = true
self.hoursNode.isHidden = false
self.minutesNode.isHidden = false
}
}
if let size = self.validLayout {
let hoursString: String
if let typingHours = self.typingHours {
if typingHours < 10 {
hoursString = "0\(typingHours)"
} else {
hoursString = "\(typingHours)"
}
} else {
hoursString = ""
}
let minutesString: String
if let typingMinutes = self.typingMinutes {
if typingMinutes < 10 {
minutesString = "0\(typingMinutes)"
} else {
minutesString = "\(typingMinutes)"
}
} else {
minutesString = ""
}
self.hoursTypingNode.attributedText = NSAttributedString(string: hoursString, font: Font.with(size: 21.0, design: .regular, weight: .regular, traits: [.monospacedNumbers]), textColor: theme.textColor)
let hoursSize = self.hoursTypingNode.updateLayout(size)
self.hoursTypingNode.frame = CGRect(origin: CGPoint(x: 37.0 - hoursSize.width - 3.0 + UIScreenPixel, y: 6.0), size: hoursSize)
self.minutesTypingNode.attributedText = NSAttributedString(string: minutesString, font: Font.with(size: 21.0, design: .regular, weight: .regular, traits: [.monospacedNumbers]), textColor: theme.textColor)
let minutesSize = self.minutesTypingNode.updateLayout(size)
self.minutesTypingNode.frame = CGRect(origin: CGPoint(x: 75.0 - minutesSize.width - 9.0 + UIScreenPixel, y: 6.0), size: minutesSize)
self.colonNode.attributedText = NSAttributedString(string: ":", font: Font.with(size: 21.0, design: .regular, weight: .regular, traits: [.monospacedNumbers]), textColor: colonColor)
let _ = self.colonNode.updateLayout(size)
}
}
@objc func handleTap(_ gestureRecognizer: UITapGestureRecognizer) {
if !self.inputNode.view.isFirstResponder {
self.inputNode.view.becomeFirstResponder()
self.selection = .all
} else {
let location = gestureRecognizer.location(in: self.view)
if location.x < 37.0 {
if self.selection == .hours {
self.selection = .all
} else {
self.selection = .hours
}
} else if location.x > 37.0 && location.x < 75.0 {
if self.selection == .minutes {
self.selection = .all
} else {
self.selection = .minutes
}
}
} }
} }
@ -1079,35 +1511,60 @@ private final class TimePickerNode: ASDisplayNode {
self.theme = theme self.theme = theme
self.backgroundNode.backgroundColor = theme.segmentedControlTheme.backgroundColor self.backgroundNode.backgroundColor = theme.segmentedControlTheme.backgroundColor
self.borderNode.borderColor = theme.accentColor.cgColor
} }
func updateLayout(size: CGSize) -> CGSize { func updateTapes() {
self.backgroundNode.frame = CGRect(x: 0.0, y: 0.0, width: 75.0, height: 36.0)
var contentSize = CGSize()
let hours: Int32 let hours: Int32
let minutes: Int32 let minutes: Int32
if let date = self.date { if let date = self.date {
hours = Int32(calendar.component(.hour, from: date)) hours = Int32(calendar.component(.hour, from: date))
minutes = Int32(calendar.component(.hour, from: date)) minutes = Int32(calendar.component(.minute, from: date))
} else { } else {
hours = 11 hours = 11
minutes = 0 minutes = 0
} }
let string = stringForShortTimestamp(hours: hours, minutes: minutes, dateTimeFormat: self.dateTimeFormat).replacingOccurrences(of: " AM", with: "").replacingOccurrences(of: " PM", with: "")
self.textNode.attributedText = NSAttributedString(string: string, font: Font.with(size: 21.0, design: .regular, weight: .regular, traits: [.monospacedNumbers]), textColor: self.theme.textColor) switch self.dateTimeFormat.timeFormat {
case .military:
self.hoursNode.selectRow(Int(hours), animated: false)
self.minutesNode.selectRow(Int(minutes), animated: false)
case .regular:
var h12Hours = hours
if hours == 0 {
h12Hours = 12
} else if hours > 12 {
h12Hours = hours - 12
}
self.hoursNode.selectRow(Int(h12Hours - 1), animated: false)
self.minutesNode.selectRow(Int(minutes), animated: false)
}
}
func updateLayout(size: CGSize) -> CGSize {
self.validLayout = size
self.backgroundNode.frame = CGRect(x: 0.0, y: 0.0, width: 75.0, height: 36.0)
self.borderNode.frame = self.backgroundNode.frame
var contentSize = CGSize()
self.updateTapes()
self.hoursNode.frame = CGRect(x: 3.0, y: 0.0, width: 36.0, height: 36.0)
self.minutesNode.frame = CGRect(x: 35.0, y: 0.0, width: 36.0, height: 36.0)
self.hoursTopMaskNode.frame = CGRect(x: 9.0, y: 0.0, width: 28.0, height: 5.0)
self.hoursBottomMaskNode.frame = CGRect(x: 9.0, y: 36.0 - 5.0, width: 28.0, height: 5.0)
self.minutesTopMaskNode.frame = CGRect(x: 37.0, y: 0.0, width: 28.0, height: 5.0)
self.minutesBottomMaskNode.frame = CGRect(x: 37.0, y: 36.0 - 5.0, width: 28.0, height: 5.0)
self.colonNode.attributedText = NSAttributedString(string: ":", font: Font.with(size: 21.0, design: .regular, weight: .regular, traits: [.monospacedNumbers]), textColor: self.theme.textColor) self.colonNode.attributedText = NSAttributedString(string: ":", font: Font.with(size: 21.0, design: .regular, weight: .regular, traits: [.monospacedNumbers]), textColor: self.theme.textColor)
let textSize = self.textNode.updateLayout(size)
self.textNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((self.backgroundNode.frame.width - textSize.width) / 2.0), y: floorToScreenPixels((self.backgroundNode.frame.height - textSize.height) / 2.0)), size: textSize)
let colonSize = self.colonNode.updateLayout(size) let colonSize = self.colonNode.updateLayout(size)
self.colonNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((self.backgroundNode.frame.width - colonSize.width) / 2.0) - 7.0, y: floorToScreenPixels((self.backgroundNode.frame.height - colonSize.height) / 2.0) - 2.0), size: colonSize) self.colonNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((self.backgroundNode.frame.width - colonSize.width) / 2.0), y: floorToScreenPixels((self.backgroundNode.frame.height - colonSize.height) / 2.0) - 2.0), size: colonSize)
self.inputNode.frame = self.backgroundNode.frame self.inputNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: 1.0, height: 1.0))
if self.dateTimeFormat.timeFormat == .military { if self.dateTimeFormat.timeFormat == .military {
contentSize = self.backgroundNode.frame.size contentSize = self.backgroundNode.frame.size

View File

@ -0,0 +1,271 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import AudioToolbox
@objc public protocol PickerViewDelegate: class {
func pickerViewHeightForRows(_ pickerView: TapeNode) -> CGFloat
@objc optional func pickerView(_ pickerView: TapeNode, didSelectRow row: Int)
@objc optional func pickerView(_ pickerView: TapeNode, didTapRow row: Int)
@objc optional func pickerView(_ pickerView: TapeNode, styleForLabel label: UILabel, highlighted: Bool)
@objc optional func pickerView(_ pickerView: TapeNode, viewForRow row: Int, highlighted: Bool, reusingView view: UIView?) -> UIView?
}
open class TapeNode: ASDisplayNode {
fileprivate class Cell: UITableViewCell {
lazy var titleLabel: UILabel = {
let titleLabel = UILabel(frame: CGRect(x: 0.0, y: 0.0, width: self.contentView.frame.width, height: self.contentView.frame.height))
titleLabel.textAlignment = .center
return titleLabel
}()
var customView: UIView?
}
var textColor: UIColor = .black {
didSet {
for cell in self.tableView.visibleCells {
if let cell = cell as? Cell {
cell.titleLabel.textColor = self.textColor
}
}
}
}
private let hapticFeedback = HapticFeedback()
private var previousRoundedRow: Int?
var count: (() -> Int)?
var titleAt: ((Int) -> String)?
var selected: ((Int) -> Void)?
var isScrollingUpdated: ((Bool) -> Void)?
var numberOfRows: Int {
get {
return self.count?() ?? 0
}
}
private var indexesCount: Int {
return self.numberOfRows > 0 ? self.numberOfRows - 1 : self.numberOfRows
}
private var rowHeight: CGFloat {
return 21.0
}
private let cellIdentifier = "tapeCell"
public lazy var tableView: UITableView = {
return UITableView()
}()
private var infinityRowsMultiplier: Int = 1
var currentSelectedRow: Int?
var currentSelectedIndex: Int {
get {
if let currentSelectedRow = self.currentSelectedRow {
return self.indexForRow(currentSelectedRow)
} else {
return 0
}
}
}
private var isScrolling = false
private var initialized = false
private var shouldSelectNearbyToMiddleRow = false
open override func didLoad() {
super.didLoad()
self.setup()
}
fileprivate func setup() {
self.infinityRowsMultiplier = self.generateInfinityRowsMultiplier()
if #available(iOS 11.0, *) {
self.tableView.contentInsetAdjustmentBehavior = .never
}
self.tableView.estimatedRowHeight = 0
self.tableView.estimatedSectionFooterHeight = 0
self.tableView.estimatedSectionHeaderHeight = 0
self.tableView.backgroundColor = .clear
self.tableView.separatorStyle = .none
self.tableView.separatorColor = .none
self.tableView.allowsSelection = true
self.tableView.allowsMultipleSelection = false
self.tableView.showsVerticalScrollIndicator = false
self.tableView.showsHorizontalScrollIndicator = false
self.tableView.scrollsToTop = false
self.tableView.register(Cell.classForCoder(), forCellReuseIdentifier: self.cellIdentifier)
self.view.addSubview(tableView)
self.tableView.delegate = self
self.tableView.dataSource = self
self.tableView.reloadData()
}
fileprivate func generateInfinityRowsMultiplier() -> Int {
if self.numberOfRows > 100 {
return 100
} else if self.numberOfRows < 100 && self.numberOfRows > 50 {
return 200
} else if self.numberOfRows < 50 && self.numberOfRows > 25 {
return 400
} else {
return 800
}
}
open override func layout() {
super.layout()
self.tableView.frame = self.bounds
if !self.initialized {
self.setup()
self.initialized = true
}
}
fileprivate func indexForRow(_ row: Int) -> Int {
return row % (self.numberOfRows > 0 ? self.numberOfRows : 1)
}
fileprivate func selectTappedRow(_ row: Int) {
self.selectRow(row, animated: true)
if let currentSelectedRow = self.currentSelectedRow {
self.selected?(currentSelectedRow)
}
}
fileprivate func visibleIndexOfSelectedRow() -> Int {
let middleMultiplier = (self.infinityRowsMultiplier / 2)
let middleIndex = self.numberOfRows * middleMultiplier
let indexForSelectedRow: Int
if let currentSelectedRow = self.currentSelectedRow {
indexForSelectedRow = middleIndex - (self.numberOfRows - currentSelectedRow)
} else {
let middleRow = Int(floor(Float(self.indexesCount) / 2.0))
indexForSelectedRow = middleIndex - (self.numberOfRows - middleRow)
}
return indexForSelectedRow
}
open func selectRow(_ row : Int, animated: Bool) {
if self.currentSelectedIndex == self.indexForRow(row) {
return
}
var finalRow = row
if row <= self.numberOfRows {
let middleMultiplier = (self.infinityRowsMultiplier / 2)
let middleIndex = self.numberOfRows * middleMultiplier
finalRow = middleIndex - (self.numberOfRows - finalRow)
}
self.currentSelectedRow = finalRow
if let currentSelectedRow = self.currentSelectedRow {
self.tableView.setContentOffset(CGPoint(x: 0.0, y: CGFloat(currentSelectedRow) * self.rowHeight), animated: animated)
}
}
open func reloadPickerView() {
self.tableView.reloadData()
}
}
extension TapeNode: UITableViewDataSource {
public func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return self.numberOfRows * self.infinityRowsMultiplier
}
public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let pickerViewCell = tableView.dequeueReusableCell(withIdentifier: cellIdentifier, for: indexPath) as! Cell
pickerViewCell.selectionStyle = .none
let centerY = (indexPath as NSIndexPath).row == 0 ? (self.frame.height / 2) - (self.rowHeight / 2) : 0.0
pickerViewCell.backgroundColor = .clear
pickerViewCell.contentView.backgroundColor = .clear
pickerViewCell.contentView.addSubview(pickerViewCell.titleLabel)
pickerViewCell.titleLabel.backgroundColor = .clear
pickerViewCell.titleLabel.font = Font.with(size: 21.0, design: .regular, weight: .regular, traits: [.monospacedNumbers])
pickerViewCell.titleLabel.text = self.titleAt?(indexForRow((indexPath as NSIndexPath).row))
pickerViewCell.titleLabel.textColor = self.textColor
pickerViewCell.titleLabel.frame = CGRect(x: 0.0, y: centerY, width: frame.width, height: self.rowHeight)
return pickerViewCell
}
}
extension TapeNode: UITableViewDelegate {
public func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
self.selectTappedRow((indexPath as NSIndexPath).row)
}
public func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
let numberOfRows = (self.count?() ?? 0) * self.infinityRowsMultiplier
if (indexPath as NSIndexPath).row == 0 {
return (self.frame.height / 2) + (self.rowHeight / 2)
} else if numberOfRows > 0 && (indexPath as NSIndexPath).row == numberOfRows - 1 {
return (self.frame.height / 2) + (self.rowHeight / 2)
}
return self.rowHeight
}
}
extension TapeNode: UIScrollViewDelegate {
public func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
self.isScrolling = true
self.isScrollingUpdated?(true)
}
public func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
let partialRow = Float(targetContentOffset.pointee.y / self.rowHeight)
var roundedRow = Int(lroundf(partialRow))
if roundedRow < 0 {
roundedRow = 0
} else {
targetContentOffset.pointee.y = CGFloat(roundedRow) * self.rowHeight
}
self.currentSelectedRow = self.indexForRow(roundedRow)
if let currentSelectedRow = self.currentSelectedRow {
self.selected?(currentSelectedRow)
}
}
public func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
if !decelerate {
self.isScrolling = false
self.isScrollingUpdated?(false)
}
}
public func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
self.isScrolling = false
self.isScrollingUpdated?(false)
}
public func scrollViewDidScroll(_ scrollView: UIScrollView) {
let partialRow = Float(scrollView.contentOffset.y / self.rowHeight)
let roundedRow = Int(lroundf(partialRow))
if self.previousRoundedRow != roundedRow && self.isScrolling {
self.previousRoundedRow = roundedRow
self.hapticFeedback.impact()
AudioServicesPlaySystemSound(1157)
}
}
}

View File

@ -497,7 +497,12 @@ final class NavigationModalContainer: ASDisplayNode, UIScrollViewDelegate, UIGes
if currentParent == nil { if currentParent == nil {
break break
} }
if let scrollView = currentParent as? UIScrollView { if currentParent is UIKeyInput {
if currentParent?.disablesInteractiveModalDismiss == true {
enableScrolling = false
break
}
} else if let scrollView = currentParent as? UIScrollView {
if scrollView === self.scrollNode.view { if scrollView === self.scrollNode.view {
break break
} }

View File

@ -54,7 +54,7 @@ func isValidNumberOfUsers(_ number: String) -> Bool {
private enum InviteLinksEditEntry: ItemListNodeEntry { private enum InviteLinksEditEntry: ItemListNodeEntry {
case timeHeader(PresentationTheme, String) case timeHeader(PresentationTheme, String)
case timePicker(PresentationTheme, InviteLinkTimeLimit) case timePicker(PresentationTheme, InviteLinkTimeLimit)
case timeExpiryDate(PresentationTheme, Int32?, Bool) case timeExpiryDate(PresentationTheme, PresentationDateTimeFormat, Int32?, Bool)
case timeCustomPicker(PresentationTheme, PresentationDateTimeFormat, Int32?) case timeCustomPicker(PresentationTheme, PresentationDateTimeFormat, Int32?)
case timeInfo(PresentationTheme, String) case timeInfo(PresentationTheme, String)
@ -115,8 +115,8 @@ private enum InviteLinksEditEntry: ItemListNodeEntry {
} else { } else {
return false return false
} }
case let .timeExpiryDate(lhsTheme, lhsDate, lhsActive): case let .timeExpiryDate(lhsTheme, lhsDateTimeFormat, lhsDate, lhsActive):
if case let .timeExpiryDate(rhsTheme, rhsDate, rhsActive) = rhs, lhsTheme === rhsTheme, lhsDate == rhsDate, lhsActive == rhsActive { if case let .timeExpiryDate(rhsTheme, rhsDateTimeFormat, rhsDate, rhsActive) = rhs, lhsTheme === rhsTheme, lhsDateTimeFormat == rhsDateTimeFormat, lhsDate == rhsDate, lhsActive == rhsActive {
return true return true
} else { } else {
return false return false
@ -186,10 +186,10 @@ private enum InviteLinksEditEntry: ItemListNodeEntry {
return updatedState return updatedState
}) })
}) })
case let .timeExpiryDate(theme, value, active): case let .timeExpiryDate(theme, dateTimeFormat, value, active):
let text: String let text: String
if let value = value { if let value = value {
text = stringForFullDate(timestamp: value, strings: presentationData.strings, dateTimeFormat: PresentationDateTimeFormat(timeFormat: .regular, dateFormat: .monthFirst, dateSeparator: ".", decimalSeparator: ".", groupingSeparator: ".")) text = stringForMediumDate(timestamp: value, strings: presentationData.strings, dateTimeFormat: dateTimeFormat)
} else { } else {
text = presentationData.strings.InviteLink_Create_TimeLimitExpiryDateNever text = presentationData.strings.InviteLink_Create_TimeLimitExpiryDateNever
} }
@ -287,7 +287,7 @@ private func inviteLinkEditControllerEntries(invite: ExportedInvitation?, state:
} else if let value = state.time.value { } else if let value = state.time.value {
time = currentTime + value time = currentTime + value
} }
entries.append(.timeExpiryDate(presentationData.theme, time, state.pickingTimeLimit)) entries.append(.timeExpiryDate(presentationData.theme, presentationData.dateTimeFormat, time, state.pickingTimeLimit))
if state.pickingTimeLimit { if state.pickingTimeLimit {
entries.append(.timeCustomPicker(presentationData.theme, presentationData.dateTimeFormat, time)) entries.append(.timeCustomPicker(presentationData.theme, presentationData.dateTimeFormat, time))
} }
@ -484,6 +484,9 @@ public func inviteLinkEditController(context: AccountContext, peerId: PeerId, in
} }
let controller = ItemListController(context: context, state: signal) let controller = ItemListController(context: context, state: signal)
controller.beganInteractiveDragging = {
dismissInputImpl?()
}
presentControllerImpl = { [weak controller] c, p in presentControllerImpl = { [weak controller] c, p in
if let controller = controller { if let controller = controller {
controller.present(c, in: .window(.root), with: p) controller.present(c, in: .window(.root), with: p)

View File

@ -104,6 +104,8 @@ public class ItemListDatePickerItemNode: ListViewItemNode, ItemListItemNode {
self.bottomStripeNode.isLayerBacked = true self.bottomStripeNode.isLayerBacked = true
super.init(layerBacked: false, dynamicBounce: false) super.init(layerBacked: false, dynamicBounce: false)
self.allowsGroupOpacity = true
} }
public func asyncLayout() -> (_ item: ItemListDatePickerItem, _ params: ListViewItemLayoutParams, _ insets: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) { public func asyncLayout() -> (_ item: ItemListDatePickerItem, _ params: ListViewItemLayoutParams, _ insets: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) {

View File

@ -164,6 +164,14 @@ open class ItemListController: ViewController, KeyShortcutResponder, Presentable
} }
} }
public var beganInteractiveDragging: (() -> Void)? {
didSet {
if self.isNodeLoaded {
(self.displayNode as! ItemListControllerNode).beganInteractiveDragging = self.beganInteractiveDragging
}
}
}
public var visibleBottomContentOffsetChanged: ((ListViewVisibleContentOffset) -> Void)? { public var visibleBottomContentOffsetChanged: ((ListViewVisibleContentOffset) -> Void)? {
didSet { didSet {
if self.isNodeLoaded { if self.isNodeLoaded {
@ -447,6 +455,7 @@ open class ItemListController: ViewController, KeyShortcutResponder, Presentable
displayNode.enableInteractiveDismiss = self.enableInteractiveDismiss displayNode.enableInteractiveDismiss = self.enableInteractiveDismiss
displayNode.alwaysSynchronous = self.alwaysSynchronous displayNode.alwaysSynchronous = self.alwaysSynchronous
displayNode.visibleEntriesUpdated = self.visibleEntriesUpdated displayNode.visibleEntriesUpdated = self.visibleEntriesUpdated
displayNode.beganInteractiveDragging = self.beganInteractiveDragging
displayNode.visibleBottomContentOffsetChanged = self.visibleBottomContentOffsetChanged displayNode.visibleBottomContentOffsetChanged = self.visibleBottomContentOffsetChanged
displayNode.contentOffsetChanged = self.contentOffsetChanged displayNode.contentOffsetChanged = self.contentOffsetChanged
displayNode.contentScrollingEnded = self.contentScrollingEnded displayNode.contentScrollingEnded = self.contentScrollingEnded

View File

@ -210,6 +210,7 @@ open class ItemListControllerNode: ASDisplayNode, UIScrollViewDelegate {
public var visibleEntriesUpdated: ((ItemListNodeVisibleEntries) -> Void)? public var visibleEntriesUpdated: ((ItemListNodeVisibleEntries) -> Void)?
public var visibleBottomContentOffsetChanged: ((ListViewVisibleContentOffset) -> Void)? public var visibleBottomContentOffsetChanged: ((ListViewVisibleContentOffset) -> Void)?
public var beganInteractiveDragging: (() -> Void)?
public var contentOffsetChanged: ((ListViewVisibleContentOffset, Bool) -> Void)? public var contentOffsetChanged: ((ListViewVisibleContentOffset, Bool) -> Void)?
public var contentScrollingEnded: ((ListView) -> Bool)? public var contentScrollingEnded: ((ListView) -> Bool)?
public var searchActivated: ((Bool) -> Void)? public var searchActivated: ((Bool) -> Void)?
@ -291,6 +292,12 @@ open class ItemListControllerNode: ASDisplayNode, UIScrollViewDelegate {
self?.contentOffsetChanged?(offset, inVoiceOver) self?.contentOffsetChanged?(offset, inVoiceOver)
} }
self.listNode.beganInteractiveDragging = { [weak self] in
if let strongSelf = self {
strongSelf.beganInteractiveDragging?()
}
}
self.listNode.didEndScrolling = { [weak self] in self.listNode.didEndScrolling = { [weak self] in
if let strongSelf = self { if let strongSelf = self {
let _ = strongSelf.contentScrollingEnded?(strongSelf.listNode) let _ = strongSelf.contentScrollingEnded?(strongSelf.listNode)

View File

@ -63,6 +63,7 @@ private enum RecentSessionsEntry: ItemListNodeEntry {
case currentSession(PresentationTheme, PresentationStrings, PresentationDateTimeFormat, RecentAccountSession) case currentSession(PresentationTheme, PresentationStrings, PresentationDateTimeFormat, RecentAccountSession)
case terminateOtherSessions(PresentationTheme, String) case terminateOtherSessions(PresentationTheme, String)
case terminateAllWebSessions(PresentationTheme, String) case terminateAllWebSessions(PresentationTheme, String)
case currentAddDevice(PresentationTheme, String)
case currentSessionInfo(PresentationTheme, String) case currentSessionInfo(PresentationTheme, String)
case pendingSessionsHeader(PresentationTheme, String) case pendingSessionsHeader(PresentationTheme, String)
case pendingSession(index: Int32, theme: PresentationTheme, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, session: RecentAccountSession, enabled: Bool, editing: Bool, revealed: Bool) case pendingSession(index: Int32, theme: PresentationTheme, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, session: RecentAccountSession, enabled: Bool, editing: Bool, revealed: Bool)
@ -75,7 +76,7 @@ private enum RecentSessionsEntry: ItemListNodeEntry {
var section: ItemListSectionId { var section: ItemListSectionId {
switch self { switch self {
case .currentSessionHeader, .currentSession, .terminateOtherSessions, .terminateAllWebSessions, .currentSessionInfo: case .currentSessionHeader, .currentSession, .terminateOtherSessions, .terminateAllWebSessions, .currentAddDevice, .currentSessionInfo:
return RecentSessionsSection.currentSession.rawValue return RecentSessionsSection.currentSession.rawValue
case .pendingSessionsHeader, .pendingSession, .pendingSessionsInfo: case .pendingSessionsHeader, .pendingSession, .pendingSessionsInfo:
return RecentSessionsSection.pendingSessions.rawValue return RecentSessionsSection.pendingSessions.rawValue
@ -94,18 +95,20 @@ private enum RecentSessionsEntry: ItemListNodeEntry {
return .index(2) return .index(2)
case .terminateAllWebSessions: case .terminateAllWebSessions:
return .index(3) return .index(3)
case .currentAddDevice:
return .index(3)
case .currentSessionInfo: case .currentSessionInfo:
return .index(4)
case .pendingSessionsHeader:
return .index(5) return .index(5)
case .pendingSessionsHeader:
return .index(6)
case let .pendingSession(_, _, _, _, session, _, _, _): case let .pendingSession(_, _, _, _, session, _, _, _):
return .session(session.hash) return .session(session.hash)
case .pendingSessionsInfo: case .pendingSessionsInfo:
return .index(6)
case .otherSessionsHeader:
return .index(7) return .index(7)
case .addDevice: case .otherSessionsHeader:
return .index(8) return .index(8)
case .addDevice:
return .index(9)
case let .session(_, _, _, _, session, _, _, _): case let .session(_, _, _, _, session, _, _, _):
return .session(session.hash) return .session(session.hash)
case let .website(_, _, _, _, _, website, _, _, _, _): case let .website(_, _, _, _, _, website, _, _, _, _):
@ -135,6 +138,12 @@ private enum RecentSessionsEntry: ItemListNodeEntry {
} else { } else {
return false return false
} }
case let .currentAddDevice(lhsTheme, lhsText):
if case let .currentAddDevice(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
case let .currentSessionInfo(lhsTheme, lhsText): case let .currentSessionInfo(lhsTheme, lhsText):
if case let .currentSessionInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { if case let .currentSessionInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true return true
@ -257,39 +266,48 @@ private enum RecentSessionsEntry: ItemListNodeEntry {
func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem { func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem {
let arguments = arguments as! RecentSessionsControllerArguments let arguments = arguments as! RecentSessionsControllerArguments
switch self { switch self {
case let .currentSessionHeader(theme, text): case let .currentSessionHeader(_, text):
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
case let .currentSession(theme, strings, dateTimeFormat, session): case let .currentSession(_, strings, dateTimeFormat, session):
return ItemListRecentSessionItem(presentationData: presentationData, dateTimeFormat: dateTimeFormat, session: session, enabled: true, editable: false, editing: false, revealed: false, sectionId: self.section, setSessionIdWithRevealedOptions: { _, _ in return ItemListRecentSessionItem(presentationData: presentationData, dateTimeFormat: dateTimeFormat, session: session, enabled: true, editable: false, editing: false, revealed: false, sectionId: self.section, setSessionIdWithRevealedOptions: { _, _ in
}, removeSession: { _ in }, removeSession: { _ in
}) })
case let .terminateOtherSessions(theme, text): case let .terminateOtherSessions(_, text):
return ItemListActionItem(presentationData: presentationData, title: text, kind: .destructive, alignment: .natural, sectionId: self.section, style: .blocks, action: { return ItemListActionItem(presentationData: presentationData, title: text, kind: .destructive, alignment: .natural, sectionId: self.section, style: .blocks, action: {
arguments.terminateOtherSessions() arguments.terminateOtherSessions()
}) })
case let .terminateAllWebSessions(theme, text): case let .terminateAllWebSessions(_, text):
return ItemListActionItem(presentationData: presentationData, title: text, kind: .destructive, alignment: .natural, sectionId: self.section, style: .blocks, action: { return ItemListActionItem(presentationData: presentationData, title: text, kind: .destructive, alignment: .natural, sectionId: self.section, style: .blocks, action: {
arguments.terminateAllWebSessions() arguments.terminateAllWebSessions()
}) })
case let .currentSessionInfo(theme, text): case let .currentAddDevice(_, text):
return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section) return ItemListActionItem(presentationData: presentationData, title: text, kind: .generic, alignment: .natural, sectionId: self.section, style: .blocks, action: {
case let .pendingSessionsHeader(theme, text): arguments.addDevice()
})
case let .currentSessionInfo(_, text):
return ItemListTextItem(presentationData: presentationData, text: .markdown(text), sectionId: self.section, linkAction: { action in
switch action {
case .tap:
arguments.openOtherAppsUrl()
}
})
case let .pendingSessionsHeader(_, text):
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
case let .pendingSession(_, theme, strings, dateTimeFormat, session, enabled, editing, revealed): case let .pendingSession(_, _, _, dateTimeFormat, session, enabled, editing, revealed):
return ItemListRecentSessionItem(presentationData: presentationData, dateTimeFormat: dateTimeFormat, session: session, enabled: enabled, editable: true, editing: editing, revealed: revealed, sectionId: self.section, setSessionIdWithRevealedOptions: { previousId, id in return ItemListRecentSessionItem(presentationData: presentationData, dateTimeFormat: dateTimeFormat, session: session, enabled: enabled, editable: true, editing: editing, revealed: revealed, sectionId: self.section, setSessionIdWithRevealedOptions: { previousId, id in
arguments.setSessionIdWithRevealedOptions(previousId, id) arguments.setSessionIdWithRevealedOptions(previousId, id)
}, removeSession: { id in }, removeSession: { id in
arguments.removeSession(id) arguments.removeSession(id)
}) })
case let .pendingSessionsInfo(theme, text): case let .pendingSessionsInfo(_, text):
return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section) return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section)
case let .otherSessionsHeader(theme, text): case let .otherSessionsHeader(_, text):
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
case let .addDevice(theme, text): case let .addDevice(_, text):
return ItemListActionItem(presentationData: presentationData, title: text, kind: .generic, alignment: .natural, sectionId: self.section, style: .blocks, action: { return ItemListActionItem(presentationData: presentationData, title: text, kind: .generic, alignment: .natural, sectionId: self.section, style: .blocks, action: {
arguments.addDevice() arguments.addDevice()
}) })
case let .session(_, theme, strings, dateTimeFormat, session, enabled, editing, revealed): case let .session(_, _, _, dateTimeFormat, session, enabled, editing, revealed):
return ItemListRecentSessionItem(presentationData: presentationData, dateTimeFormat: dateTimeFormat, session: session, enabled: enabled, editable: true, editing: editing, revealed: revealed, sectionId: self.section, setSessionIdWithRevealedOptions: { previousId, id in return ItemListRecentSessionItem(presentationData: presentationData, dateTimeFormat: dateTimeFormat, session: session, enabled: enabled, editable: true, editing: editing, revealed: revealed, sectionId: self.section, setSessionIdWithRevealedOptions: { previousId, id in
arguments.setSessionIdWithRevealedOptions(previousId, id) arguments.setSessionIdWithRevealedOptions(previousId, id)
}, removeSession: { id in }, removeSession: { id in
@ -301,7 +319,7 @@ private enum RecentSessionsEntry: ItemListNodeEntry {
}, removeSession: { id in }, removeSession: { id in
arguments.removeWebSession(id) arguments.removeWebSession(id)
}) })
case let .devicesInfo(theme, text): case let .devicesInfo(_, text):
return ItemListTextItem(presentationData: presentationData, text: .markdown(text), sectionId: self.section, linkAction: { action in return ItemListTextItem(presentationData: presentationData, text: .markdown(text), sectionId: self.section, linkAction: { action in
switch action { switch action {
case .tap: case .tap:
@ -377,9 +395,16 @@ private func recentSessionsControllerEntries(presentationData: PresentationData,
entries.append(.currentSession(presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, sessionsState.sessions[index])) entries.append(.currentSession(presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, sessionsState.sessions[index]))
} }
var hasAddDevice = false
if sessionsState.sessions.count > 1 || enableQRLogin { if sessionsState.sessions.count > 1 || enableQRLogin {
if sessionsState.sessions.count > 1 {
entries.append(.terminateOtherSessions(presentationData.theme, presentationData.strings.AuthSessions_TerminateOtherSessions)) entries.append(.terminateOtherSessions(presentationData.theme, presentationData.strings.AuthSessions_TerminateOtherSessions))
entries.append(.currentSessionInfo(presentationData.theme, presentationData.strings.AuthSessions_TerminateOtherSessionsHelp)) entries.append(.currentSessionInfo(presentationData.theme, presentationData.strings.AuthSessions_TerminateOtherSessionsHelp))
} else if enableQRLogin {
hasAddDevice = true
entries.append(.currentAddDevice(presentationData.theme, presentationData.strings.AuthSessions_AddDevice))
entries.append(.currentSessionInfo(presentationData.theme, presentationData.strings.AuthSessions_OtherDevices))
}
let filteredPendingSessions: [RecentAccountSession] = sessionsState.sessions.filter({ $0.flags.contains(.passwordPending) }) let filteredPendingSessions: [RecentAccountSession] = sessionsState.sessions.filter({ $0.flags.contains(.passwordPending) })
if !filteredPendingSessions.isEmpty { if !filteredPendingSessions.isEmpty {
@ -393,9 +418,11 @@ private func recentSessionsControllerEntries(presentationData: PresentationData,
entries.append(.pendingSessionsInfo(presentationData.theme, presentationData.strings.AuthSessions_IncompleteAttemptsInfo)) entries.append(.pendingSessionsInfo(presentationData.theme, presentationData.strings.AuthSessions_IncompleteAttemptsInfo))
} }
if sessionsState.sessions.count > 1 {
entries.append(.otherSessionsHeader(presentationData.theme, presentationData.strings.AuthSessions_OtherSessions)) entries.append(.otherSessionsHeader(presentationData.theme, presentationData.strings.AuthSessions_OtherSessions))
}
if enableQRLogin { if enableQRLogin && !hasAddDevice {
entries.append(.addDevice(presentationData.theme, presentationData.strings.AuthSessions_AddDevice)) entries.append(.addDevice(presentationData.theme, presentationData.strings.AuthSessions_AddDevice))
} }
@ -410,7 +437,7 @@ private func recentSessionsControllerEntries(presentationData: PresentationData,
} }
} }
if enableQRLogin { if enableQRLogin && !hasAddDevice {
entries.append(.devicesInfo(presentationData.theme, presentationData.strings.AuthSessions_OtherDevices)) entries.append(.devicesInfo(presentationData.theme, presentationData.strings.AuthSessions_OtherDevices))
} }
} }
@ -654,7 +681,12 @@ public func recentSessionsController(context: AccountContext, activeSessionsCont
} }
} }
let emptyStateItem: ItemListControllerEmptyStateItem? = nil let emptyStateItem: ItemListControllerEmptyStateItem?
if sessionsState.sessions.count == 1 && mode == .sessions {
emptyStateItem = RecentSessionsEmptyStateItem(theme: presentationData.theme, strings: presentationData.strings)
} else {
emptyStateItem = nil
}
let title: ItemListControllerTitle let title: ItemListControllerTitle
let entries: [RecentSessionsEntry] let entries: [RecentSessionsEntry]

View File

@ -42,6 +42,29 @@ public func stringForMessageTimestamp(timestamp: Int32, dateTimeFormat: Presenta
return stringForShortTimestamp(hours: timeinfo.tm_hour, minutes: timeinfo.tm_min, dateTimeFormat: dateTimeFormat) return stringForShortTimestamp(hours: timeinfo.tm_hour, minutes: timeinfo.tm_min, dateTimeFormat: dateTimeFormat)
} }
public func stringForMediumDate(timestamp: Int32, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat) -> String {
var t: time_t = Int(timestamp)
var timeinfo = tm()
localtime_r(&t, &timeinfo);
let day = timeinfo.tm_mday
let month = timeinfo.tm_mon + 1
let year = timeinfo.tm_year
let dateString: String
let separator = dateTimeFormat.dateSeparator
switch dateTimeFormat.dateFormat {
case .monthFirst:
dateString = String(format: "%d%@%d%@%02d", month, separator, day, separator, year - 100)
case .dayFirst:
dateString = String(format: "%d%@%02d%@%02d", day, separator, month, separator, year - 100)
}
let timeString = stringForShortTimestamp(hours: Int32(timeinfo.tm_hour), minutes: Int32(timeinfo.tm_min), dateTimeFormat: dateTimeFormat)
return strings.Time_MediumDate(dateString, timeString).0
}
public func stringForFullDate(timestamp: Int32, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat) -> String { public func stringForFullDate(timestamp: Int32, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat) -> String {
var t: time_t = Int(timestamp) var t: time_t = Int(timestamp)
var timeinfo = tm() var timeinfo = tm()

View File

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

View File

@ -10,6 +10,7 @@ import TelegramPresentationData
import AlertUI import AlertUI
import PresentationDataUtils import PresentationDataUtils
import PeerInfoUI import PeerInfoUI
import UndoUI
private enum SubscriberAction: Equatable { private enum SubscriberAction: Equatable {
case join case join
@ -95,6 +96,8 @@ final class ChatChannelSubscriberInputPanelNode: ChatInputPanelNode {
private let badgeText: ImmediateTextNode private let badgeText: ImmediateTextNode
private let activityIndicator: UIActivityIndicatorView private let activityIndicator: UIActivityIndicatorView
private let helpButton: HighlightableButtonNode
private var action: SubscriberAction? private var action: SubscriberAction?
private let actionDisposable = MetaDisposable() private let actionDisposable = MetaDisposable()
@ -122,6 +125,8 @@ final class ChatChannelSubscriberInputPanelNode: ChatInputPanelNode {
self.badgeText.displaysAsynchronously = false self.badgeText.displaysAsynchronously = false
self.badgeText.isHidden = true self.badgeText.isHidden = true
self.helpButton = HighlightableButtonNode()
self.discussButton.addSubnode(self.discussButtonText) self.discussButton.addSubnode(self.discussButtonText)
self.discussButton.addSubnode(self.badgeBackground) self.discussButton.addSubnode(self.badgeBackground)
self.discussButton.addSubnode(self.badgeText) self.discussButton.addSubnode(self.badgeText)
@ -131,9 +136,11 @@ final class ChatChannelSubscriberInputPanelNode: ChatInputPanelNode {
self.addSubnode(self.button) self.addSubnode(self.button)
self.addSubnode(self.discussButton) self.addSubnode(self.discussButton)
self.view.addSubview(self.activityIndicator) self.view.addSubview(self.activityIndicator)
self.addSubnode(self.helpButton)
self.button.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: .touchUpInside) self.button.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: .touchUpInside)
self.discussButton.addTarget(self, action: #selector(self.discussPressed), forControlEvents: .touchUpInside) self.discussButton.addTarget(self, action: #selector(self.discussPressed), forControlEvents: .touchUpInside)
self.helpButton.addTarget(self, action: #selector(self.helpPressed), forControlEvents: .touchUpInside)
} }
deinit { deinit {
@ -148,6 +155,10 @@ final class ChatChannelSubscriberInputPanelNode: ChatInputPanelNode {
return super.hitTest(point, with: event) return super.hitTest(point, with: event)
} }
@objc func helpPressed() {
self.interfaceInteraction?.presentGigagroupHelp()
}
@objc func buttonPressed() { @objc func buttonPressed() {
guard let context = self.context, let action = self.action, let presentationInterfaceState = self.presentationInterfaceState, let peer = presentationInterfaceState.renderedPeer?.peer else { guard let context = self.context, let action = self.action, let presentationInterfaceState = self.presentationInterfaceState, let peer = presentationInterfaceState.renderedPeer?.peer else {
return return
@ -213,6 +224,7 @@ final class ChatChannelSubscriberInputPanelNode: ChatInputPanelNode {
if previousState?.theme !== interfaceState.theme { if previousState?.theme !== interfaceState.theme {
self.badgeBackground.image = PresentationResourcesChatList.badgeBackgroundActive(interfaceState.theme, diameter: 20.0) self.badgeBackground.image = PresentationResourcesChatList.badgeBackgroundActive(interfaceState.theme, diameter: 20.0)
self.helpButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Accessory Panels/Help"), color: interfaceState.theme.chat.inputPanel.panelControlAccentColor), for: .normal)
} }
if let peer = interfaceState.renderedPeer?.peer, previousState?.renderedPeer?.peer == nil || !peer.isEqual(previousState!.renderedPeer!.peer!) || previousState?.theme !== interfaceState.theme || previousState?.strings !== interfaceState.strings || previousState?.peerIsMuted != interfaceState.peerIsMuted || previousState?.pinnedMessage != interfaceState.pinnedMessage { if let peer = interfaceState.renderedPeer?.peer, previousState?.renderedPeer?.peer == nil || !peer.isEqual(previousState!.renderedPeer!.peer!) || previousState?.theme !== interfaceState.theme || previousState?.strings !== interfaceState.strings || previousState?.peerIsMuted != interfaceState.peerIsMuted || previousState?.pinnedMessage != interfaceState.pinnedMessage {
@ -234,10 +246,20 @@ final class ChatChannelSubscriberInputPanelNode: ChatInputPanelNode {
if let action = self.action, action == .muteNotifications || action == .unmuteNotifications { if let action = self.action, action == .muteNotifications || action == .unmuteNotifications {
let buttonWidth = self.button.titleNode.calculateSizeThatFits(CGSize(width: width, height: panelHeight)).width + 24.0 let buttonWidth = self.button.titleNode.calculateSizeThatFits(CGSize(width: width, height: panelHeight)).width + 24.0
self.button.frame = CGRect(origin: CGPoint(x: floor((width - buttonWidth) / 2.0), y: 0.0), size: CGSize(width: buttonWidth, height: panelHeight)) self.button.frame = CGRect(origin: CGPoint(x: floor((width - buttonWidth) / 2.0), y: 0.0), size: CGSize(width: buttonWidth, height: panelHeight))
if let peer = interfaceState.renderedPeer?.peer as? TelegramChannel, peer.flags.contains(.isGigagroup) {
self.helpButton.isHidden = false
} else { } else {
self.button.frame = CGRect(origin: CGPoint(x: leftInset, y: 0.0), size: CGSize(width: width - leftInset - rightInset, height: panelHeight)) self.helpButton.isHidden = true
} }
} else { } else {
self.button.frame = CGRect(origin: CGPoint(x: leftInset, y: 0.0), size: CGSize(width: width - leftInset - rightInset, height: panelHeight))
self.helpButton.isHidden = true
}
self.helpButton.frame = CGRect(x: width - rightInset - panelHeight, y: 0.0, width: panelHeight, height: panelHeight)
} else {
self.helpButton.isHidden = true
let availableWidth = min(600.0, width - leftInset - rightInset) let availableWidth = min(600.0, width - leftInset - rightInset)
let leftOffset = floor((width - availableWidth) / 2.0) let leftOffset = floor((width - availableWidth) / 2.0)
self.button.frame = CGRect(origin: CGPoint(x: leftOffset, y: 0.0), size: CGSize(width: floor(availableWidth / 2.0), height: panelHeight)) self.button.frame = CGRect(origin: CGPoint(x: leftOffset, y: 0.0), size: CGSize(width: floor(availableWidth / 2.0), height: panelHeight))

View File

@ -6457,6 +6457,10 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
return return
} }
presentAddMembers(context: strongSelf.context, parentController: strongSelf, groupPeer: peer, selectAddMemberDisposable: strongSelf.selectAddMemberDisposable, addMemberDisposable: strongSelf.addMemberDisposable) presentAddMembers(context: strongSelf.context, parentController: strongSelf, groupPeer: peer, selectAddMemberDisposable: strongSelf.selectAddMemberDisposable, addMemberDisposable: strongSelf.addMemberDisposable)
}, presentGigagroupHelp: { [weak self] in
if let strongSelf = self {
strongSelf.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .info(text: strongSelf.presentationData.strings.Conversation_GigagroupDescription), elevatedLayout: false, action: { _ in return true }), in: .current)
}
}, editMessageMedia: { [weak self] messageId, draw in }, editMessageMedia: { [weak self] messageId, draw in
if let strongSelf = self { if let strongSelf = self {
strongSelf.controllerInteraction?.editMessageMedia(messageId, draw) strongSelf.controllerInteraction?.editMessageMedia(messageId, draw)

View File

@ -129,6 +129,7 @@ final class ChatPanelInterfaceInteraction {
let editMessageMedia: (MessageId, Bool) -> Void let editMessageMedia: (MessageId, Bool) -> Void
let joinGroupCall: (CachedChannelData.ActiveCall) -> Void let joinGroupCall: (CachedChannelData.ActiveCall) -> Void
let presentInviteMembers: () -> Void let presentInviteMembers: () -> Void
let presentGigagroupHelp: () -> Void
let statuses: ChatPanelInterfaceInteractionStatuses? let statuses: ChatPanelInterfaceInteractionStatuses?
init( init(
@ -210,6 +211,7 @@ final class ChatPanelInterfaceInteraction {
activatePinnedListPreview: @escaping (ASDisplayNode, ContextGesture) -> Void, activatePinnedListPreview: @escaping (ASDisplayNode, ContextGesture) -> Void,
joinGroupCall: @escaping (CachedChannelData.ActiveCall) -> Void, joinGroupCall: @escaping (CachedChannelData.ActiveCall) -> Void,
presentInviteMembers: @escaping () -> Void, presentInviteMembers: @escaping () -> Void,
presentGigagroupHelp: @escaping () -> Void,
editMessageMedia: @escaping (MessageId, Bool) -> Void, editMessageMedia: @escaping (MessageId, Bool) -> Void,
statuses: ChatPanelInterfaceInteractionStatuses? statuses: ChatPanelInterfaceInteractionStatuses?
) { ) {
@ -292,6 +294,7 @@ final class ChatPanelInterfaceInteraction {
self.editMessageMedia = editMessageMedia self.editMessageMedia = editMessageMedia
self.joinGroupCall = joinGroupCall self.joinGroupCall = joinGroupCall
self.presentInviteMembers = presentInviteMembers self.presentInviteMembers = presentInviteMembers
self.presentGigagroupHelp = presentGigagroupHelp
self.statuses = statuses self.statuses = statuses
} }
} }

View File

@ -134,6 +134,7 @@ final class ChatRecentActionsController: TelegramBaseController {
}, activatePinnedListPreview: { _, _ in }, activatePinnedListPreview: { _, _ in
}, joinGroupCall: { _ in }, joinGroupCall: { _ in
}, presentInviteMembers: { }, presentInviteMembers: {
}, presentGigagroupHelp: {
}, editMessageMedia: { _, _ in }, editMessageMedia: { _, _ in
}, statuses: nil) }, statuses: nil)

View File

@ -154,11 +154,34 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode {
} }
case let .editExportedInvitation(_, invite), let .revokeExportedInvitation(invite), let .deleteExportedInvitation(invite), let .participantJoinedViaInvite(invite): case let .editExportedInvitation(_, invite), let .revokeExportedInvitation(invite), let .deleteExportedInvitation(invite), let .participantJoinedViaInvite(invite):
if !invite.link.hasSuffix("...") { if !invite.link.hasSuffix("...") {
if invite.isPermanent {
let actionSheet = ActionSheetController(presentationData: strongSelf.presentationData)
var items: [ActionSheetItem] = []
items.append(ActionSheetTextItem(title: invite.link))
items.append(ActionSheetButtonItem(title: strongSelf.presentationData.strings.InviteLink_ContextRevoke, color: .destructive, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
if let strongSelf = self {
let _ = (revokePeerExportedInvitation(account: strongSelf.context.account, peerId: peer.id, link: invite.link)
|> deliverOnMainQueue).start(completed: { [weak self] in
self?.eventLogContext.reload()
})
}
}))
actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [
ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
})
])])
strongSelf.presentController(actionSheet, nil)
} else {
let controller = inviteLinkEditController(context: strongSelf.context, peerId: peer.id, invite: invite, completion: { [weak self] _ in let controller = inviteLinkEditController(context: strongSelf.context, peerId: peer.id, invite: invite, completion: { [weak self] _ in
self?.eventLogContext.reload() self?.eventLogContext.reload()
}) })
controller.navigationPresentation = .modal controller.navigationPresentation = .modal
strongSelf.pushController(controller) strongSelf.pushController(controller)
}
return true return true
} }
case .changeHistoryTTL: case .changeHistoryTTL:

View File

@ -1187,7 +1187,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable {
var text: String = "" var text: String = ""
var entities: [MessageTextEntity] = [] 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)") 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 / 100)%")
appendAttributedText(text: rawText, generateEntities: { index in appendAttributedText(text: rawText, generateEntities: { index in
if index == 0, let author = author { if index == 0, let author = author {

View File

@ -765,16 +765,12 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen
let membersData: Signal<PeerInfoMembersData?, NoError> = combineLatest(membersContext.state, context.account.viewTracker.peerView(groupId, updateData: false)) let membersData: Signal<PeerInfoMembersData?, NoError> = combineLatest(membersContext.state, context.account.viewTracker.peerView(groupId, updateData: false))
|> map { state, view -> PeerInfoMembersData? in |> map { state, view -> PeerInfoMembersData? in
if let peer = peerViewMainPeer(view) as? TelegramChannel, peer.flags.contains(.isGigagroup) {
return nil
} else {
if state.members.count > 5 { if state.members.count > 5 {
return .longList(membersContext) return .longList(membersContext)
} else { } else {
return .shortList(membersContext: membersContext, members: state.members) return .shortList(membersContext: membersContext, members: state.members)
} }
} }
}
|> distinctUntilChanged |> distinctUntilChanged
let globalNotificationsKey: PostboxViewKey = .preferences(keys: Set<ValueBoxKey>([PreferencesKeys.globalNotifications])) let globalNotificationsKey: PostboxViewKey = .preferences(keys: Set<ValueBoxKey>([PreferencesKeys.globalNotifications]))

View File

@ -446,6 +446,7 @@ final class PeerInfoSelectionPanelNode: ASDisplayNode {
}, activatePinnedListPreview: { _, _ in }, activatePinnedListPreview: { _, _ in
}, joinGroupCall: { _ in }, joinGroupCall: { _ in
}, presentInviteMembers: { }, presentInviteMembers: {
}, presentGigagroupHelp: {
}, editMessageMedia: { _, _ in }, editMessageMedia: { _, _ in
}, statuses: nil) }, statuses: nil)