mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-12-02 12:48:45 +00:00
Update API
This commit is contained in:
parent
e055885c52
commit
a4abbc4068
@ -4109,7 +4109,9 @@ public final class ChatController: TelegramController, KeyShortcutResponder, Gal
|
||||
|
||||
private func presentPollCreation() {
|
||||
if case let .peer(peerId) = self.chatLocation {
|
||||
self.present(createPollController(account: self.account, peerId: peerId), in: .window(.root), with: ViewControllerPresentationArguments(presentationAnimation: .modalSheet))
|
||||
self.present(createPollController(account: self.account, peerId: peerId, completion: { [weak self] message in
|
||||
self?.sendMessages([message])
|
||||
}), in: .window(.root), with: ViewControllerPresentationArguments(presentationAnimation: .modalSheet))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -430,7 +430,7 @@ func contextMenuForChatPresentationIntefaceState(chatPresentationInterfaceState:
|
||||
})))
|
||||
}
|
||||
|
||||
if let activePoll = activePoll {
|
||||
if let _ = activePoll, messages[0].forwardInfo == nil {
|
||||
actions.append(.sheet(ChatMessageContextMenuSheetAction(color: .accent, title: chatPresentationInterfaceState.strings.Conversation_StopPoll, action: {
|
||||
interfaceInteraction.requestStopPollInMessage(messages[0].id)
|
||||
})))
|
||||
@ -575,7 +575,9 @@ func chatAvailableMessageActions(postbox: Postbox, accountPeerId: PeerId, messag
|
||||
}
|
||||
}
|
||||
if id.peerId == accountPeerId {
|
||||
optionsMap[id]!.insert(.forward)
|
||||
if !(message.flags.isSending || message.flags.contains(.Failed)) {
|
||||
optionsMap[id]!.insert(.forward)
|
||||
}
|
||||
optionsMap[id]!.insert(.deleteLocally)
|
||||
} else if let peer = transaction.getPeer(id.peerId) {
|
||||
var isAction = false
|
||||
@ -603,7 +605,9 @@ func chatAvailableMessageActions(postbox: Postbox, accountPeerId: PeerId, messag
|
||||
}
|
||||
if !message.containsSecretMedia && !isAction {
|
||||
if message.id.peerId.namespace != Namespaces.Peer.SecretChat {
|
||||
optionsMap[id]!.insert(.forward)
|
||||
if !(message.flags.isSending || message.flags.contains(.Failed)) {
|
||||
optionsMap[id]!.insert(.forward)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -617,7 +621,9 @@ func chatAvailableMessageActions(postbox: Postbox, accountPeerId: PeerId, messag
|
||||
} else if let group = peer as? TelegramGroup {
|
||||
if message.id.peerId.namespace != Namespaces.Peer.SecretChat && !message.containsSecretMedia {
|
||||
if !isAction {
|
||||
optionsMap[id]!.insert(.forward)
|
||||
if !(message.flags.isSending || message.flags.contains(.Failed)) {
|
||||
optionsMap[id]!.insert(.forward)
|
||||
}
|
||||
}
|
||||
}
|
||||
optionsMap[id]!.insert(.deleteLocally)
|
||||
@ -643,7 +649,9 @@ func chatAvailableMessageActions(postbox: Postbox, accountPeerId: PeerId, messag
|
||||
}
|
||||
} else if let user = peer as? TelegramUser {
|
||||
if message.id.peerId.namespace != Namespaces.Peer.SecretChat && !message.containsSecretMedia && !isAction {
|
||||
optionsMap[id]!.insert(.forward)
|
||||
if !(message.flags.isSending || message.flags.contains(.Failed)) {
|
||||
optionsMap[id]!.insert(.forward)
|
||||
}
|
||||
}
|
||||
optionsMap[id]!.insert(.deleteLocally)
|
||||
if canPerformEditingActions(limits: limitsConfiguration, accountPeerId: accountPeerId, message: message) {
|
||||
|
||||
@ -36,57 +36,57 @@ public func chatListItemStrings(strings: PresentationStrings, nameDisplayOrder:
|
||||
var isVideo = false
|
||||
inner: for attribute in fileMedia.attributes {
|
||||
switch attribute {
|
||||
case .Animated:
|
||||
isAnimated = true
|
||||
break inner
|
||||
case let .Audio(isVoice, _, title, performer, _):
|
||||
if isVoice {
|
||||
messageText = strings.Message_Audio
|
||||
case .Animated:
|
||||
isAnimated = true
|
||||
break inner
|
||||
} else {
|
||||
let descriptionString: String
|
||||
if let title = title, let performer = performer, !title.isEmpty, !performer.isEmpty {
|
||||
descriptionString = title + " — " + performer
|
||||
} else if let title = title, !title.isEmpty {
|
||||
descriptionString = title
|
||||
} else if let performer = performer, !performer.isEmpty {
|
||||
descriptionString = performer
|
||||
} else if let fileName = fileMedia.fileName {
|
||||
descriptionString = fileName
|
||||
case let .Audio(isVoice, _, title, performer, _):
|
||||
if isVoice {
|
||||
messageText = strings.Message_Audio
|
||||
break inner
|
||||
} else {
|
||||
descriptionString = strings.Message_Audio
|
||||
}
|
||||
messageText = descriptionString
|
||||
break inner
|
||||
}
|
||||
case let .Sticker(displayText, _, _):
|
||||
if displayText.isEmpty {
|
||||
messageText = strings.Message_Sticker
|
||||
break inner
|
||||
} else {
|
||||
messageText = strings.Message_StickerText(displayText).0
|
||||
break inner
|
||||
}
|
||||
case let .Video(_, _, flags):
|
||||
if flags.contains(.instantRoundVideo) {
|
||||
messageText = strings.Message_VideoMessage
|
||||
break inner
|
||||
} else {
|
||||
if message.text.isEmpty {
|
||||
isVideo = true
|
||||
} else if #available(iOSApplicationExtension 9.0, *) {
|
||||
if !fileMedia.isAnimated {
|
||||
messageText = "📹 \(messageText)"
|
||||
let descriptionString: String
|
||||
if let title = title, let performer = performer, !title.isEmpty, !performer.isEmpty {
|
||||
descriptionString = title + " — " + performer
|
||||
} else if let title = title, !title.isEmpty {
|
||||
descriptionString = title
|
||||
} else if let performer = performer, !performer.isEmpty {
|
||||
descriptionString = performer
|
||||
} else if let fileName = fileMedia.fileName {
|
||||
descriptionString = fileName
|
||||
} else {
|
||||
descriptionString = strings.Message_Audio
|
||||
}
|
||||
messageText = descriptionString
|
||||
break inner
|
||||
}
|
||||
}
|
||||
default:
|
||||
if !message.text.isEmpty {
|
||||
messageText = "📎 \(messageText)"
|
||||
break inner
|
||||
}
|
||||
break
|
||||
case let .Sticker(displayText, _, _):
|
||||
if displayText.isEmpty {
|
||||
messageText = strings.Message_Sticker
|
||||
break inner
|
||||
} else {
|
||||
messageText = strings.Message_StickerText(displayText).0
|
||||
break inner
|
||||
}
|
||||
case let .Video(_, _, flags):
|
||||
if flags.contains(.instantRoundVideo) {
|
||||
messageText = strings.Message_VideoMessage
|
||||
break inner
|
||||
} else {
|
||||
if message.text.isEmpty {
|
||||
isVideo = true
|
||||
} else if #available(iOSApplicationExtension 9.0, *) {
|
||||
if !fileMedia.isAnimated {
|
||||
messageText = "📹 \(messageText)"
|
||||
}
|
||||
break inner
|
||||
}
|
||||
}
|
||||
default:
|
||||
if !message.text.isEmpty {
|
||||
messageText = "📎 \(messageText)"
|
||||
break inner
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
if isAnimated {
|
||||
@ -139,7 +139,7 @@ public func chatListItemStrings(strings: PresentationStrings, nameDisplayOrder:
|
||||
messageText = text
|
||||
}
|
||||
case let poll as TelegramMediaPoll:
|
||||
messageText = poll.text
|
||||
messageText = "📊 \(strings.Message_Poll)"
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
@ -431,6 +431,14 @@ class ChatMessageBubbleItemNode: ChatMessageItemView {
|
||||
tmpWidth = baseWidth - 32.0
|
||||
}
|
||||
}
|
||||
|
||||
var deliveryFailedInset: CGFloat = 0.0
|
||||
if item.content.firstMessage.flags.contains(.Failed) {
|
||||
deliveryFailedInset += 24.0
|
||||
}
|
||||
|
||||
tmpWidth -= deliveryFailedInset
|
||||
|
||||
let maximumContentWidth = floor(tmpWidth - layoutConstants.bubble.edgeInset - layoutConstants.bubble.edgeInset - layoutConstants.bubble.contentInsets.left - layoutConstants.bubble.contentInsets.right - avatarInset)
|
||||
|
||||
var contentPropertiesAndPrepareLayouts: [(Message, Bool, (_ item: ChatMessageBubbleContentItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ preparePosition: ChatMessageBubblePreparePosition, _ messageSelection: Bool?, _ constrainedSize: CGSize) -> (ChatMessageBubbleContentProperties, CGSize?, CGFloat, (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool) -> Void))))] = []
|
||||
@ -459,8 +467,12 @@ class ChatMessageBubbleItemNode: ChatMessageItemView {
|
||||
var authorNameString: String?
|
||||
let authorIsAdmin: Bool
|
||||
switch content {
|
||||
case let .message(_, _, _, isAdmin):
|
||||
authorIsAdmin = isAdmin
|
||||
case let .message(message, _, _, isAdmin):
|
||||
if let peer = message.peers[message.id.peerId] as? TelegramChannel, case .broadcast = peer.info {
|
||||
authorIsAdmin = false
|
||||
} else {
|
||||
authorIsAdmin = isAdmin
|
||||
}
|
||||
case .group:
|
||||
authorIsAdmin = false
|
||||
}
|
||||
@ -1050,11 +1062,6 @@ class ChatMessageBubbleItemNode: ChatMessageItemView {
|
||||
contentVerticalOffset = floorToScreenPixels((minimalContentSize.height - calculatedBubbleHeight) / 2.0)
|
||||
}
|
||||
|
||||
var deliveryFailedInset: CGFloat = 0.0
|
||||
if item.content.firstMessage.flags.contains(.Failed) {
|
||||
deliveryFailedInset += 24.0
|
||||
}
|
||||
|
||||
let backgroundFrame: CGRect
|
||||
let contentOrigin: CGPoint
|
||||
let contentUpperRightCorner: CGPoint
|
||||
|
||||
@ -549,7 +549,6 @@ class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
let bubbleTheme = item.presentationData.theme.theme.chat.bubble
|
||||
|
||||
let attributedText = NSAttributedString(string: poll?.text ?? "", font: item.presentationData.messageBoldFont, textColor: incoming ? bubbleTheme.incomingPrimaryTextColor : bubbleTheme.outgoingPrimaryTextColor)
|
||||
@ -566,7 +565,7 @@ class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
let (typeLayout, typeApply) = makeTypeLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: typeText, font: labelsFont, textColor: incoming ? bubbleTheme.incomingSecondaryTextColor : bubbleTheme.outgoingSecondaryTextColor), backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: textConstrainedSize, alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||||
|
||||
let votersString: String
|
||||
if let totalVoters = poll?.results.totalVoters, totalVoters != 0 {
|
||||
if let totalVoters = poll?.results.totalVoters {
|
||||
votersString = item.presentationData.strings.MessagePoll_VotedCount(totalVoters)
|
||||
} else {
|
||||
votersString = " "
|
||||
@ -597,14 +596,28 @@ class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
var optionVoterCount: [Int: Int32] = [:]
|
||||
var maxOptionVoterCount: Int32 = 0
|
||||
var totalVoterCount: Int32 = 0
|
||||
if let voters = poll.results.voters, let totalVoters = poll.results.totalVoters {
|
||||
let voters: [TelegramMediaPollOptionVoters]?
|
||||
if poll.isClosed {
|
||||
voters = poll.results.voters ?? []
|
||||
} else {
|
||||
voters = poll.results.voters
|
||||
}
|
||||
if let voters = voters, let totalVoters = poll.results.totalVoters {
|
||||
var didVote = false
|
||||
for voter in voters {
|
||||
if voter.selected {
|
||||
didVote = true
|
||||
}
|
||||
}
|
||||
totalVoterCount = totalVoters
|
||||
for i in 0 ..< poll.options.count {
|
||||
inner: for optionVoters in voters {
|
||||
if optionVoters.opaqueIdentifier == poll.options[i].opaqueIdentifier {
|
||||
optionVoterCount[i] = optionVoters.count
|
||||
maxOptionVoterCount = max(maxOptionVoterCount, optionVoters.count)
|
||||
break inner
|
||||
if didVote {
|
||||
for i in 0 ..< poll.options.count {
|
||||
inner: for optionVoters in voters {
|
||||
if optionVoters.opaqueIdentifier == poll.options[i].opaqueIdentifier {
|
||||
optionVoterCount[i] = optionVoters.count
|
||||
maxOptionVoterCount = max(maxOptionVoterCount, optionVoters.count)
|
||||
break inner
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -620,8 +633,14 @@ class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
makeLayout = ChatMessagePollOptionNode.asyncLayout(nil)
|
||||
}
|
||||
var optionResult: ChatMessagePollOptionResult?
|
||||
if let count = optionVoterCount[i], maxOptionVoterCount != 0, totalVoterCount != 0 {
|
||||
optionResult = ChatMessagePollOptionResult(normalized: CGFloat(count) / CGFloat(maxOptionVoterCount), absolute: CGFloat(count) / CGFloat(totalVoterCount))
|
||||
if let count = optionVoterCount[i] {
|
||||
if maxOptionVoterCount != 0 && totalVoterCount != 0 {
|
||||
optionResult = ChatMessagePollOptionResult(normalized: CGFloat(count) / CGFloat(maxOptionVoterCount), absolute: CGFloat(count) / CGFloat(totalVoterCount))
|
||||
} else if poll.isClosed {
|
||||
optionResult = ChatMessagePollOptionResult(normalized: 0, absolute: 0)
|
||||
}
|
||||
} else if poll.isClosed {
|
||||
optionResult = ChatMessagePollOptionResult(normalized: 0, absolute: 0)
|
||||
}
|
||||
let result = makeLayout(item.account.peerId, item.presentationData, item.message, option, optionResult, constrainedSize.width - layoutConstants.bubble.borderInset * 2.0)
|
||||
boundingSize.width = max(boundingSize.width, result.minimumWidth + layoutConstants.bubble.borderInset * 2.0)
|
||||
@ -632,7 +651,7 @@ class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
boundingSize.width = max(boundingSize.width, min(270.0, constrainedSize.width))
|
||||
|
||||
var canVote = false
|
||||
if item.message.id.namespace == Namespaces.Message.Cloud, let poll = poll, poll.pollId.namespace == Namespaces.Media.CloudPoll {
|
||||
if item.message.id.namespace == Namespaces.Message.Cloud, let poll = poll, poll.pollId.namespace == Namespaces.Media.CloudPoll, !poll.isClosed {
|
||||
var hasVoted = false
|
||||
if let voters = poll.results.voters {
|
||||
for voter in voters {
|
||||
|
||||
@ -4,6 +4,9 @@ import SwiftSignalKit
|
||||
import Postbox
|
||||
import TelegramCore
|
||||
|
||||
private let maxTextLength = 255
|
||||
private let maxOptionLength = 100
|
||||
|
||||
private final class CreatePollControllerArguments {
|
||||
let updatePollText: (String) -> Void
|
||||
let updateOptionText: (Int, String) -> Void
|
||||
@ -44,7 +47,7 @@ private enum CreatePollEntryTag: Equatable, ItemListItemTag {
|
||||
}
|
||||
|
||||
private enum CreatePollEntry: ItemListNodeEntry {
|
||||
case textHeader(PresentationTheme, String, String)
|
||||
case textHeader(PresentationTheme, String, ItemListSectionHeaderAccessoryText)
|
||||
case text(PresentationTheme, String, String, Int)
|
||||
case optionsHeader(PresentationTheme, String)
|
||||
case option(PresentationTheme, PresentationStrings, Int, Int, String, String, Bool, Bool)
|
||||
@ -109,7 +112,7 @@ private enum CreatePollEntry: ItemListNodeEntry {
|
||||
case let .optionsHeader(theme, text):
|
||||
return ItemListSectionHeaderItem(theme: theme, text: text, sectionId: self.section)
|
||||
case let .option(theme, strings, id, _, placeholder, text, revealed, hasNext):
|
||||
return CreatePollOptionItem(theme: theme, strings: strings, id: id, placeholder: placeholder, value: text, maxLength: 256, editing: CreatePollOptionItemEditing(editable: true, hasActiveRevealControls: revealed), sectionId: self.section, setItemIdWithRevealedOptions: { id, fromId in
|
||||
return CreatePollOptionItem(theme: theme, strings: strings, id: id, placeholder: placeholder, value: text, maxLength: maxOptionLength, editing: CreatePollOptionItemEditing(editable: true, hasActiveRevealControls: revealed), sectionId: self.section, setItemIdWithRevealedOptions: { id, fromId in
|
||||
arguments.setItemIdWithRevealedOptions(id, fromId)
|
||||
}, updated: { value in
|
||||
arguments.updateOptionText(id, value)
|
||||
@ -146,9 +149,10 @@ private struct CreatePollControllerState: Equatable {
|
||||
private func createPollControllerEntries(presentationData: PresentationData, state: CreatePollControllerState, limitsConfiguration: LimitsConfiguration) -> [CreatePollEntry] {
|
||||
var entries: [CreatePollEntry] = []
|
||||
|
||||
var textLimitText = ""
|
||||
if state.text.count >= Int(limitsConfiguration.maxMediaCaptionLength) * 70 / 100 {
|
||||
textLimitText = "\(Int(limitsConfiguration.maxMediaCaptionLength) - state.text.count)"
|
||||
var textLimitText = ItemListSectionHeaderAccessoryText(value: "", color: .generic)
|
||||
if state.text.count >= Int(maxTextLength) * 70 / 100 {
|
||||
let remainingCount = Int(maxTextLength) - state.text.count
|
||||
textLimitText = ItemListSectionHeaderAccessoryText(value: "\(remainingCount)", color: remainingCount < 0 ? .destructive : .generic)
|
||||
}
|
||||
entries.append(.textHeader(presentationData.theme, presentationData.strings.CreatePoll_TextHeader, textLimitText))
|
||||
entries.append(.text(presentationData.theme, presentationData.strings.CreatePoll_TextPlaceholder, state.text, Int(limitsConfiguration.maxMediaCaptionLength)))
|
||||
@ -166,7 +170,7 @@ private func createPollControllerEntries(presentationData: PresentationData, sta
|
||||
return entries
|
||||
}
|
||||
|
||||
public func createPollController(account: Account, peerId: PeerId) -> ViewController {
|
||||
public func createPollController(account: Account, peerId: PeerId, completion: @escaping (EnqueueMessage) -> Void) -> ViewController {
|
||||
let statePromise = ValuePromise(CreatePollControllerState(), ignoreRepeated: true)
|
||||
let stateValue = Atomic(value: CreatePollControllerState())
|
||||
let updateState: ((CreatePollControllerState) -> CreatePollControllerState) -> Void = { f in
|
||||
@ -175,6 +179,7 @@ public func createPollController(account: Account, peerId: PeerId) -> ViewContro
|
||||
|
||||
var presentControllerImpl: ((ViewController, Any?) -> Void)?
|
||||
var dismissImpl: (() -> Void)?
|
||||
var ensureTextVisibleImpl: (() -> Void)?
|
||||
var ensureOptionVisibleImpl: ((Int) -> Void)?
|
||||
|
||||
let actionsDisposable = DisposableSet()
|
||||
@ -191,6 +196,7 @@ public func createPollController(account: Account, peerId: PeerId) -> ViewContro
|
||||
state.text = value
|
||||
return state
|
||||
}
|
||||
ensureTextVisibleImpl?()
|
||||
}, updateOptionText: { id, value in
|
||||
updateState { state in
|
||||
var state = state
|
||||
@ -201,6 +207,7 @@ public func createPollController(account: Account, peerId: PeerId) -> ViewContro
|
||||
}
|
||||
return state
|
||||
}
|
||||
ensureOptionVisibleImpl?(id)
|
||||
}, moveToNextOption: { id in
|
||||
updateState { state in
|
||||
var state = state
|
||||
@ -257,11 +264,17 @@ public func createPollController(account: Account, peerId: PeerId) -> ViewContro
|
||||
if state.text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
|
||||
enabled = false
|
||||
}
|
||||
if state.text.count > maxTextLength {
|
||||
enabled = false
|
||||
}
|
||||
var hasNonEmptyOptions = false
|
||||
for option in state.options {
|
||||
if !option.text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
|
||||
hasNonEmptyOptions = true
|
||||
}
|
||||
if option.text.count > maxOptionLength {
|
||||
enabled = false
|
||||
}
|
||||
}
|
||||
if !hasNonEmptyOptions {
|
||||
enabled = false
|
||||
@ -276,7 +289,7 @@ public func createPollController(account: Account, peerId: PeerId) -> ViewContro
|
||||
options.append(TelegramMediaPollOption(text: optionText, opaqueIdentifier: "\(i)".data(using: .utf8)!))
|
||||
}
|
||||
}
|
||||
let _ = enqueueMessages(account: account, peerId: peerId, messages: [.message(text: "", attributes: [], mediaReference: .standalone(media: TelegramMediaPoll(pollId: MediaId(namespace: Namespaces.Media.LocalPoll, id: arc4random64()), text: state.text.trimmingCharacters(in: .whitespacesAndNewlines), options: options, results: TelegramMediaPollResults(voters: nil, totalVoters: nil), isClosed: false)), replyToMessageId: nil, localGroupingKey: nil)]).start()
|
||||
completion(.message(text: "", attributes: [], mediaReference: .standalone(media: TelegramMediaPoll(pollId: MediaId(namespace: Namespaces.Media.LocalPoll, id: arc4random64()), text: state.text.trimmingCharacters(in: .whitespacesAndNewlines), options: options, results: TelegramMediaPollResults(voters: nil, totalVoters: nil), isClosed: false)), replyToMessageId: nil, localGroupingKey: nil))
|
||||
dismissImpl?()
|
||||
})
|
||||
|
||||
@ -333,6 +346,27 @@ public func createPollController(account: Account, peerId: PeerId) -> ViewContro
|
||||
controller?.view.endEditing(true)
|
||||
controller?.dismiss()
|
||||
}
|
||||
ensureTextVisibleImpl = { [weak controller] in
|
||||
controller?.afterLayout({
|
||||
guard let controller = controller else {
|
||||
return
|
||||
}
|
||||
|
||||
var resultItemNode: ListViewItemNode?
|
||||
let _ = controller.frameForItemNode({ itemNode in
|
||||
if let itemNode = itemNode as? ItemListItemNode {
|
||||
if let tag = itemNode.tag, tag.isEqual(to: CreatePollEntryTag.text) {
|
||||
resultItemNode = itemNode as? ListViewItemNode
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
})
|
||||
if let resultItemNode = resultItemNode {
|
||||
controller.ensureItemNodeVisible(resultItemNode)
|
||||
}
|
||||
})
|
||||
}
|
||||
ensureOptionVisibleImpl = { [weak controller] id in
|
||||
controller?.afterLayout({
|
||||
guard let controller = controller else {
|
||||
|
||||
@ -151,21 +151,6 @@ class CreatePollOptionItemNode: ItemListRevealOptionsItemNode, ItemListItemNode,
|
||||
}
|
||||
}
|
||||
|
||||
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
|
||||
var updatedText = ((textField.text ?? "") as NSString).replacingCharacters(in: range, with: string)
|
||||
if let item = self.item {
|
||||
if updatedText.count > item.maxLength {
|
||||
updatedText = String(updatedText[..<updatedText.index(updatedText.startIndex, offsetBy: item.maxLength)])
|
||||
if textField.text != updatedText {
|
||||
textField.text = updatedText
|
||||
self.textFieldTextChanged(textField)
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
|
||||
if let item = self.item {
|
||||
if let next = item.next {
|
||||
@ -210,8 +195,9 @@ class CreatePollOptionItemNode: ItemListRevealOptionsItemNode, ItemListItemNode,
|
||||
|
||||
let textLength = item.value.count
|
||||
let displayTextLimit = textLength > item.maxLength * 70 / 100
|
||||
let remainingCount = item.maxLength - textLength
|
||||
|
||||
let (textLimitLayout, textLimitApply) = makeTextLimitLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: "\(item.maxLength - textLength)", font: Font.regular(13.0), textColor: item.theme.list.itemSecondaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: 100.0, height: .greatestFiniteMagnitude), alignment: .left, lineSpacing: 0.0, cutout: nil, insets: UIEdgeInsets()))
|
||||
let (textLimitLayout, textLimitApply) = makeTextLimitLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: "\(remainingCount)", font: Font.regular(13.0), textColor: remainingCount < 0 ? item.theme.list.itemDestructiveColor : item.theme.list.itemSecondaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: 100.0, height: .greatestFiniteMagnitude), alignment: .left, lineSpacing: 0.0, cutout: nil, insets: UIEdgeInsets()))
|
||||
|
||||
return (layout, { [weak self] in
|
||||
if let strongSelf = self {
|
||||
|
||||
@ -93,7 +93,31 @@ final class ItemListNodeVisibleEntries<Entry: ItemListNodeEntry>: Sequence {
|
||||
}
|
||||
}
|
||||
|
||||
class ItemListControllerNode<Entry: ItemListNodeEntry>: ViewControllerTracingNode, UIScrollViewDelegate {
|
||||
final class ItemListControllerNodeView: UITracingLayerView {
|
||||
var onLayout: (() -> Void)?
|
||||
|
||||
override func layoutSubviews() {
|
||||
super.layoutSubviews()
|
||||
|
||||
self.onLayout?()
|
||||
}
|
||||
|
||||
private var inHitTest = false
|
||||
var hitTestImpl: ((CGPoint, UIEvent?) -> UIView?)?
|
||||
|
||||
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
||||
if self.inHitTest {
|
||||
return super.hitTest(point, with: event)
|
||||
} else {
|
||||
self.inHitTest = true
|
||||
let result = self.hitTestImpl?(point, event)
|
||||
self.inHitTest = false
|
||||
return result
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class ItemListControllerNode<Entry: ItemListNodeEntry>: ASDisplayNode, UIScrollViewDelegate {
|
||||
private var _ready = ValuePromise<Bool>()
|
||||
public var ready: Signal<Bool, NoError> {
|
||||
return self._ready.get()
|
||||
@ -142,6 +166,10 @@ class ItemListControllerNode<Entry: ItemListNodeEntry>: ViewControllerTracingNod
|
||||
|
||||
super.init()
|
||||
|
||||
self.setViewBlock({
|
||||
return ItemListControllerNodeView()
|
||||
})
|
||||
|
||||
self.backgroundColor = nil
|
||||
self.isOpaque = false
|
||||
|
||||
@ -203,6 +231,23 @@ class ItemListControllerNode<Entry: ItemListNodeEntry>: ViewControllerTracingNod
|
||||
|
||||
override func didLoad() {
|
||||
super.didLoad()
|
||||
|
||||
(self.view as? ItemListControllerNodeView)?.onLayout = { [weak self] in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
if !strongSelf.afterLayoutActions.isEmpty {
|
||||
let afterLayoutActions = strongSelf.afterLayoutActions
|
||||
strongSelf.afterLayoutActions = []
|
||||
for f in afterLayoutActions {
|
||||
f()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
(self.view as? ItemListControllerNodeView)?.hitTestImpl = { [weak self] point, event in
|
||||
return self?.hitTest(point, with: event)
|
||||
}
|
||||
}
|
||||
|
||||
func animateIn() {
|
||||
|
||||
@ -166,7 +166,7 @@ class ItemListMultilineInputItemNode: ListViewItemNode, ASEditableTextNodeDelega
|
||||
}
|
||||
let attributedMeasureText = NSAttributedString(string: measureText, font: Font.regular(17.0), textColor: .black)
|
||||
let attributedText = NSAttributedString(string: item.text, font: Font.regular(17.0), textColor: item.theme.list.itemPrimaryTextColor)
|
||||
let (textLayout, textApply) = makeTextLayout(TextNodeLayoutArguments(attributedString: attributedMeasureText, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: params.width - 16.0 - leftInset - rightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||||
let (textLayout, textApply) = makeTextLayout(TextNodeLayoutArguments(attributedString: attributedMeasureText, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: params.width - 16.0 - leftInset - rightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, lineSpacing: 0.0, cutout: nil, insets: UIEdgeInsets()))
|
||||
|
||||
let separatorHeight = UIScreenPixel
|
||||
|
||||
|
||||
@ -3,15 +3,25 @@ import Display
|
||||
import AsyncDisplayKit
|
||||
import SwiftSignalKit
|
||||
|
||||
enum ItemListSectionHeaderAccessoryTextColor {
|
||||
case generic
|
||||
case destructive
|
||||
}
|
||||
|
||||
struct ItemListSectionHeaderAccessoryText: Equatable {
|
||||
let value: String
|
||||
let color: ItemListSectionHeaderAccessoryTextColor
|
||||
}
|
||||
|
||||
class ItemListSectionHeaderItem: ListViewItem, ItemListItem {
|
||||
let theme: PresentationTheme
|
||||
let text: String
|
||||
let accessoryText: String
|
||||
let accessoryText: ItemListSectionHeaderAccessoryText?
|
||||
let sectionId: ItemListSectionId
|
||||
|
||||
let isAlwaysPlain: Bool = true
|
||||
|
||||
init(theme: PresentationTheme, text: String, accessoryText: String = "", sectionId: ItemListSectionId) {
|
||||
init(theme: PresentationTheme, text: String, accessoryText: ItemListSectionHeaderAccessoryText? = nil, sectionId: ItemListSectionId) {
|
||||
self.theme = theme
|
||||
self.text = text
|
||||
self.accessoryText = accessoryText
|
||||
@ -86,7 +96,18 @@ class ItemListSectionHeaderItemNode: ListViewItemNode {
|
||||
let leftInset: CGFloat = 15.0 + params.leftInset
|
||||
|
||||
let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.text, font: titleFont, textColor: item.theme.list.sectionHeaderTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - params.leftInset - params.rightInset - 20.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||||
let (accessoryLayout, accessoryApply) = makeAccessoryTextLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.accessoryText, font: titleFont, textColor: item.theme.list.sectionHeaderTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - params.leftInset - params.rightInset - 20.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||||
var accessoryTextString: NSAttributedString?
|
||||
if let accessoryText = item.accessoryText {
|
||||
let color: UIColor
|
||||
switch accessoryText.color {
|
||||
case .generic:
|
||||
color = item.theme.list.sectionHeaderTextColor
|
||||
case .destructive:
|
||||
color = item.theme.list.freeTextErrorColor
|
||||
}
|
||||
accessoryTextString = NSAttributedString(string: accessoryText.value, font: titleFont, textColor: color)
|
||||
}
|
||||
let (accessoryLayout, accessoryApply) = makeAccessoryTextLayout(TextNodeLayoutArguments(attributedString: accessoryTextString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - params.leftInset - params.rightInset - 20.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||||
|
||||
let contentSize: CGSize
|
||||
var insets = UIEdgeInsets()
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Binary file not shown.
Loading…
x
Reference in New Issue
Block a user