Poll improvements

This commit is contained in:
Ali 2020-01-10 20:50:03 +04:00
parent 048cc4c10d
commit ca8656a1cd
27 changed files with 5007 additions and 4136 deletions

View File

@ -5248,6 +5248,7 @@ Any member of this group will be able to see messages in the channel.";
"CreatePoll.Anonymous" = "Anonymous Votes";
"CreatePoll.MultipleChoice" = "Multiple Choice";
"CreatePoll.MultipleChoiceQuizAlert" = "Quiz can have only one correct answer.";
"CreatePoll.Quiz" = "Quiz Mode";
"CreatePoll.QuizInfo" = "Quiz has only one correct answer. Users can't revoke their votes.";
"CreatePoll.QuizTip" = "Tap to select the correct option";
@ -5257,8 +5258,19 @@ Any member of this group will be able to see messages in the channel.";
"MessagePoll.LabelQuiz" = "Quiz";
"MessagePoll.SubmitVote" = "Submit Vote";
"MessagePoll.ViewResults" = "View Results";
"MessagePoll.QuizNoUsers" = "No one played";
"MessagePoll.QuizCount_0" = "%@ played";
"MessagePoll.QuizCount_1" = "1 played";
"MessagePoll.QuizCount_2" = "2 played";
"MessagePoll.QuizCount_3_10" = "%@ played";
"MessagePoll.QuizCount_many" = "%@ played";
"MessagePoll.QuizCount_any" = "%@ played";
"PollResults.Title" = "Poll Results";
"PollResults.Collapse" = "COLLAPSE";
"PollResults.ShowMore_1" = "Show %@ More Voter";
"PollResults.ShowMore_any" = "Show %@ More Voters";
"Conversation.StopQuiz" = "Stop Quiz";
"Conversation.StopQuizConfirmationTitle" = "If you stop this quiz now, nobody will be able to participate in it anymore. This action cannot be undone.";
"Conversation.StopQuizConfirmation" = "Stop Quiz";

View File

@ -755,7 +755,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
var currentSecretIconImage: UIImage?
var selectableControlSizeAndApply: (CGFloat, (CGSize, Bool) -> ItemListSelectableControlNode)?
var reorderControlSizeAndApply: (CGFloat, (CGFloat, Bool) -> ItemListEditableReorderControlNode)?
var reorderControlSizeAndApply: (CGFloat, (CGFloat, Bool, ContainedViewLayoutTransition) -> ItemListEditableReorderControlNode)?
let editingOffset: CGFloat
var reorderInset: CGFloat = 0.0
@ -1331,7 +1331,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
if let reorderControlSizeAndApply = reorderControlSizeAndApply {
let reorderControlFrame = CGRect(origin: CGPoint(x: params.width + revealOffset - params.rightInset - reorderControlSizeAndApply.0, y: layoutOffset), size: CGSize(width: reorderControlSizeAndApply.0, height: layout.contentSize.height))
if strongSelf.reorderControlNode == nil {
let reorderControlNode = reorderControlSizeAndApply.1(layout.contentSize.height, false)
let reorderControlNode = reorderControlSizeAndApply.1(layout.contentSize.height, false, .immediate)
strongSelf.reorderControlNode = reorderControlNode
strongSelf.addSubnode(reorderControlNode)
reorderControlNode.frame = reorderControlFrame
@ -1344,7 +1344,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
transition.updateAlpha(node: strongSelf.pinnedIconNode, alpha: 0.0)
transition.updateAlpha(node: strongSelf.statusNode, alpha: 0.0)
} else if let reorderControlNode = strongSelf.reorderControlNode {
let _ = reorderControlSizeAndApply.1(layout.contentSize.height, false)
let _ = reorderControlSizeAndApply.1(layout.contentSize.height, false, .immediate)
transition.updateFrame(node: reorderControlNode, frame: reorderControlFrame)
}
} else if let reorderControlNode = strongSelf.reorderControlNode {

View File

@ -12,8 +12,131 @@ import AccountContext
import AlertUI
import PresentationDataUtils
private struct OrderedLinkedListItemOrderingId: RawRepresentable, Hashable {
var rawValue: Int
}
private struct OrderedLinkedListItemOrdering: Comparable {
var id: OrderedLinkedListItemOrderingId
var lowerItemIds: Set<OrderedLinkedListItemOrderingId>
var higherItemIds: Set<OrderedLinkedListItemOrderingId>
static func <(lhs: OrderedLinkedListItemOrdering, rhs: OrderedLinkedListItemOrdering) -> Bool {
if lhs.lowerItemIds.contains(rhs.id) {
return false
}
if rhs.lowerItemIds.contains(lhs.id) {
return true
}
if lhs.higherItemIds.contains(rhs.id) {
return true
}
if rhs.higherItemIds.contains(lhs.id) {
return false
}
assertionFailure()
return false
}
}
private struct OrderedLinkedListItem<T: Equatable> {
var item: T
var ordering: OrderedLinkedListItemOrdering
}
private struct OrderedLinkedList<T: Equatable>: Sequence, Equatable {
private var items: [OrderedLinkedListItem<T>] = []
private var nextId: Int = 0
init(items: [T]) {
for i in 0 ..< items.count {
self.insert(items[i], at: i, id: nil)
}
}
static func ==(lhs: OrderedLinkedList<T>, rhs: OrderedLinkedList<T>) -> Bool {
if lhs.items.count != rhs.items.count {
return false
}
for i in 0 ..< lhs.items.count {
if lhs.items[i].item != rhs.items[i].item {
return false
}
}
return true
}
func makeIterator() -> AnyIterator<OrderedLinkedListItem<T>> {
var index = 0
return AnyIterator { () -> OrderedLinkedListItem<T>? in
if index < self.items.count {
let currentIndex = index
index += 1
return self.items[currentIndex]
}
return nil
}
}
subscript(index: Int) -> OrderedLinkedListItem<T> {
return self.items[index]
}
mutating func update(at index: Int, _ f: (inout T) -> Void) {
f(&self.items[index].item)
}
var count: Int {
return self.items.count
}
var isEmpty: Bool {
return self.items.isEmpty
}
var last: OrderedLinkedListItem<T>? {
return self.items.last
}
mutating func append(_ item: T, id: OrderedLinkedListItemOrderingId?) {
self.insert(item, at: self.items.count, id: id)
}
mutating func insert(_ item: T, at index: Int, id: OrderedLinkedListItemOrderingId?) {
let previousId = id
let id = previousId ?? OrderedLinkedListItemOrderingId(rawValue: self.nextId)
self.nextId += 1
if let previousId = previousId {
for i in 0 ..< self.items.count {
self.items[i].ordering.higherItemIds.remove(previousId)
self.items[i].ordering.lowerItemIds.remove(previousId)
}
}
var lowerItemIds = Set<OrderedLinkedListItemOrderingId>()
var higherItemIds = Set<OrderedLinkedListItemOrderingId>()
for i in 0 ..< self.items.count {
if i < index {
lowerItemIds.insert(self.items[i].ordering.id)
self.items[i].ordering.higherItemIds.insert(id)
} else {
higherItemIds.insert(self.items[i].ordering.id)
self.items[i].ordering.lowerItemIds.insert(id)
}
}
self.items.insert(OrderedLinkedListItem(item: item, ordering: OrderedLinkedListItemOrdering(id: id, lowerItemIds: lowerItemIds, higherItemIds: higherItemIds)), at: index)
}
mutating func remove(at index: Int) {
self.items.remove(at: index)
}
}
private let maxTextLength = 255
private let maxOptionLength = 100
private let maxOptionCount = 10
private func processPollText(_ text: String) -> String {
var text = text.trimmingCharacters(in: .whitespacesAndNewlines)
@ -29,14 +152,15 @@ private final class CreatePollControllerArguments {
let moveToNextOption: (Int) -> Void
let moveToPreviousOption: (Int) -> Void
let removeOption: (Int, Bool) -> Void
let optionFocused: (Int) -> Void
let optionFocused: (Int, Bool) -> Void
let setItemIdWithRevealedOptions: (Int?, Int?) -> Void
let toggleOptionSelected: (Int) -> Void
let updateAnonymous: (Bool) -> Void
let updateMultipleChoice: (Bool) -> Void
let displayMultipleChoiceDisabled: () -> Void
let updateQuiz: (Bool) -> Void
init(updatePollText: @escaping (String) -> Void, updateOptionText: @escaping (Int, String) -> Void, moveToNextOption: @escaping (Int) -> Void, moveToPreviousOption: @escaping (Int) -> Void, removeOption: @escaping (Int, Bool) -> Void, optionFocused: @escaping (Int) -> Void, setItemIdWithRevealedOptions: @escaping (Int?, Int?) -> Void, toggleOptionSelected: @escaping (Int) -> Void, updateAnonymous: @escaping (Bool) -> Void, updateMultipleChoice: @escaping (Bool) -> Void, updateQuiz: @escaping (Bool) -> Void) {
init(updatePollText: @escaping (String) -> Void, updateOptionText: @escaping (Int, String) -> Void, moveToNextOption: @escaping (Int) -> Void, moveToPreviousOption: @escaping (Int) -> Void, removeOption: @escaping (Int, Bool) -> Void, optionFocused: @escaping (Int, Bool) -> Void, setItemIdWithRevealedOptions: @escaping (Int?, Int?) -> Void, toggleOptionSelected: @escaping (Int) -> Void, updateAnonymous: @escaping (Bool) -> Void, updateMultipleChoice: @escaping (Bool) -> Void, displayMultipleChoiceDisabled: @escaping () -> Void, updateQuiz: @escaping (Bool) -> Void) {
self.updatePollText = updatePollText
self.updateOptionText = updateOptionText
self.moveToNextOption = moveToNextOption
@ -47,6 +171,7 @@ private final class CreatePollControllerArguments {
self.toggleOptionSelected = toggleOptionSelected
self.updateAnonymous = updateAnonymous
self.updateMultipleChoice = updateMultipleChoice
self.displayMultipleChoiceDisabled = displayMultipleChoiceDisabled
self.updateQuiz = updateQuiz
}
}
@ -86,7 +211,7 @@ private enum CreatePollEntry: ItemListNodeEntry {
case textHeader(String, ItemListSectionHeaderAccessoryText)
case text(String, String, Int)
case optionsHeader(String)
case option(id: Int, index: Int, placeholder: String, text: String, revealed: Bool, hasNext: Bool, isLast: Bool, isSelected: Bool?)
case option(id: Int, ordering: OrderedLinkedListItemOrdering, placeholder: String, text: String, revealed: Bool, hasNext: Bool, isLast: Bool, isSelected: Bool?)
case optionsInfo(String)
case anonymousVotes(String, Bool)
case multipleChoice(String, Bool, Bool)
@ -148,7 +273,7 @@ private enum CreatePollEntry: ItemListNodeEntry {
case .optionsHeader:
return 2
case let .option(option):
return 3 + option.index
return 3
case .optionsInfo:
return 1001
case .anonymousVotes:
@ -163,6 +288,17 @@ private enum CreatePollEntry: ItemListNodeEntry {
}
static func <(lhs: CreatePollEntry, rhs: CreatePollEntry) -> Bool {
switch lhs {
case let .option(lhsOption):
switch rhs {
case let .option(rhsOption):
return lhsOption.ordering < rhsOption.ordering
default:
break
}
default:
break
}
return lhs.sortId < rhs.sortId
}
@ -191,8 +327,8 @@ private enum CreatePollEntry: ItemListNodeEntry {
arguments.moveToPreviousOption(id)
}
}, canDelete: !isLast,
focused: {
arguments.optionFocused(id)
focused: { isFocused in
arguments.optionFocused(id, isFocused)
}, toggleSelected: {
arguments.toggleOptionSelected(id)
}, tag: CreatePollEntryTag.option(id))
@ -205,6 +341,8 @@ private enum CreatePollEntry: ItemListNodeEntry {
case let .multipleChoice(text, value, enabled):
return ItemListSwitchItem(presentationData: presentationData, title: text, value: value, enabled: enabled, sectionId: self.section, style: .blocks, updated: { value in
arguments.updateMultipleChoice(value)
}, activatedWhileDisabled: {
arguments.displayMultipleChoiceDisabled()
})
case let .quiz(text, value):
return ItemListSwitchItem(presentationData: presentationData, title: text, value: value, sectionId: self.section, style: .blocks, updated: { value in
@ -224,7 +362,7 @@ private struct CreatePollControllerOption: Equatable {
private struct CreatePollControllerState: Equatable {
var text: String = ""
var options: [CreatePollControllerOption] = [CreatePollControllerOption(text: "", id: 0, isSelected: false), CreatePollControllerOption(text: "", id: 1, isSelected: false)]
var options = OrderedLinkedList<CreatePollControllerOption>(items: [CreatePollControllerOption(text: "", id: 0, isSelected: false), CreatePollControllerOption(text: "", id: 1, isSelected: false)])
var nextOptionId: Int = 2
var focusOptionId: Int?
var optionIdWithRevealControls: Int?
@ -245,10 +383,13 @@ private func createPollControllerEntries(presentationData: PresentationData, sta
entries.append(.text(presentationData.strings.CreatePoll_TextPlaceholder, state.text, Int(limitsConfiguration.maxMediaCaptionLength)))
entries.append(.optionsHeader(presentationData.strings.CreatePoll_OptionsHeader))
for i in 0 ..< state.options.count {
entries.append(.option(id: state.options[i].id, index: i, placeholder: presentationData.strings.CreatePoll_OptionPlaceholder, text: state.options[i].text, revealed: state.optionIdWithRevealControls == state.options[i].id, hasNext: i != 9, isLast: i == state.options.count - 1, isSelected: state.isQuiz ? state.options[i].isSelected : nil))
let isSecondLast = state.options.count == 2 && i == 0
let isLast = i == state.options.count - 1
let option = state.options[i].item
entries.append(.option(id: option.id, ordering: state.options[i].ordering, placeholder: isLast ? presentationData.strings.CreatePoll_AddOption : presentationData.strings.CreatePoll_OptionPlaceholder, text: option.text, revealed: state.optionIdWithRevealControls == option.id, hasNext: i != 9, isLast: isLast || isSecondLast, isSelected: state.isQuiz ? option.isSelected : nil))
}
if state.options.count < 10 {
entries.append(.optionsInfo(presentationData.strings.CreatePoll_AddMoreOptions(Int32(10 - state.options.count))))
if state.options.count < maxOptionCount {
entries.append(.optionsInfo(presentationData.strings.CreatePoll_AddMoreOptions(Int32(maxOptionCount - state.options.count))))
} else {
entries.append(.optionsInfo(presentationData.strings.CreatePoll_AllOptionsAdded))
}
@ -272,7 +413,7 @@ public func createPollController(context: AccountContext, peerId: PeerId, comple
var dismissImpl: (() -> Void)?
var ensureTextVisibleImpl: (() -> Void)?
var ensureOptionVisibleImpl: ((Int) -> Void)?
var displayQuizTooltipImpl: (() -> Void)?
var displayQuizTooltipImpl: ((Bool) -> Void)?
let actionsDisposable = DisposableSet()
@ -285,49 +426,50 @@ public func createPollController(context: AccountContext, peerId: PeerId, comple
let arguments = CreatePollControllerArguments(updatePollText: { value in
updateState { state in
var state = state
state.focusOptionId = nil
state.text = value
return state
}
ensureTextVisibleImpl?()
}, updateOptionText: { id, value in
var ensureVisibleId = id
updateState { state in
var state = state
for i in 0 ..< state.options.count {
if state.options[i].id == id {
state.options[i].text = value
if !value.isEmpty && i == state.options.count - 1 {
state.options.append(CreatePollControllerOption(text: "", id: state.nextOptionId, isSelected: false))
if state.options[i].item.id == id {
state.focusOptionId = id
state.options.update(at: i, { option in
option.text = value
})
if !value.isEmpty && i == state.options.count - 1 && state.options.count < maxOptionCount {
state.options.append(CreatePollControllerOption(text: "", id: state.nextOptionId, isSelected: false), id: nil)
state.nextOptionId += 1
}
if i != state.options.count - 1 {
ensureVisibleId = state.options[i + 1].item.id
}
break
}
}
if state.options.count > 2 {
for i in (1 ..< state.options.count - 1).reversed() {
if state.options[i - 1].text.isEmpty && state.options[i].text.isEmpty {
state.options.remove(at: i)
}
}
}
return state
}
ensureOptionVisibleImpl?(id)
ensureOptionVisibleImpl?(ensureVisibleId)
}, moveToNextOption: { id in
var resetFocusOptionId: Int?
updateState { state in
var state = state
for i in 0 ..< state.options.count {
if state.options[i].id == id {
if state.options[i].item.id == id {
if i == state.options.count - 1 {
state.options.append(CreatePollControllerOption(text: "", id: state.nextOptionId, isSelected: false))
/*state.options.append(CreatePollControllerOption(text: "", id: state.nextOptionId, isSelected: false))
state.focusOptionId = state.nextOptionId
state.nextOptionId += 1
state.nextOptionId += 1*/
} else {
if state.focusOptionId == state.options[i + 1].id {
resetFocusOptionId = state.options[i + 1].id
if state.focusOptionId == state.options[i + 1].item.id {
resetFocusOptionId = state.options[i + 1].item.id
state.focusOptionId = -1
} else {
state.focusOptionId = state.options[i + 1].id
state.focusOptionId = state.options[i + 1].item.id
}
}
break
@ -347,13 +489,13 @@ public func createPollController(context: AccountContext, peerId: PeerId, comple
updateState { state in
var state = state
for i in 0 ..< state.options.count {
if state.options[i].id == id {
if state.options[i].item.id == id {
if i != 0 {
if state.focusOptionId == state.options[i - 1].id {
resetFocusOptionId = state.options[i - 1].id
if state.focusOptionId == state.options[i - 1].item.id {
resetFocusOptionId = state.options[i - 1].item.id
state.focusOptionId = -1
} else {
state.focusOptionId = state.options[i - 1].id
state.focusOptionId = state.options[i - 1].item.id
}
}
break
@ -372,10 +514,10 @@ public func createPollController(context: AccountContext, peerId: PeerId, comple
updateState { state in
var state = state
for i in 0 ..< state.options.count {
if state.options[i].id == id {
if state.options[i].item.id == id {
state.options.remove(at: i)
if focused && i != 0 {
state.focusOptionId = state.options[i - 1].id
state.focusOptionId = state.options[i - 1].item.id
}
break
}
@ -384,19 +526,36 @@ public func createPollController(context: AccountContext, peerId: PeerId, comple
if state.options.count < 2 {
for i in 0 ..< (2 - state.options.count) {
if i == 0 && focusOnFirst {
state.options.append(CreatePollControllerOption(text: "", id: state.nextOptionId, isSelected: false))
state.options.append(CreatePollControllerOption(text: "", id: state.nextOptionId, isSelected: false), id: nil)
state.focusOptionId = state.nextOptionId
state.nextOptionId += 1
} else {
state.options.append(CreatePollControllerOption(text: "", id: state.nextOptionId, isSelected: false))
state.options.append(CreatePollControllerOption(text: "", id: state.nextOptionId, isSelected: false), id: nil)
state.nextOptionId += 1
}
}
}
return state
}
}, optionFocused: { id in
ensureOptionVisibleImpl?(id)
}, optionFocused: { id, isFocused in
if isFocused {
ensureOptionVisibleImpl?(id)
} else {
updateState { state in
var state = state
if state.options.count > 2 {
for i in 0 ..< state.options.count {
if state.options[i].item.id == id {
if state.options[i].item.text.isEmpty && i != state.options.count - 1 {
state.options.remove(at: i)
}
break
}
}
}
return state
}
}
}, setItemIdWithRevealedOptions: { id, fromId in
updateState { state in
var state = state
@ -411,12 +570,16 @@ public func createPollController(context: AccountContext, peerId: PeerId, comple
updateState { state in
var state = state
for i in 0 ..< state.options.count {
if state.options[i].id == id {
state.options[i].isSelected = !state.options[i].isSelected
if state.options[i].isSelected && state.isQuiz {
if state.options[i].item.id == id {
state.options.update(at: i, { option in
option.isSelected = !option.isSelected
})
if state.options[i].item.isSelected && state.isQuiz {
for j in 0 ..< state.options.count {
if i != j {
state.options[j].isSelected = false
state.options.update(at: j, { option in
option.isSelected = false
})
}
}
}
@ -428,28 +591,39 @@ public func createPollController(context: AccountContext, peerId: PeerId, comple
}, updateAnonymous: { value in
updateState { state in
var state = state
state.focusOptionId = -1
state.isAnonymous = value
return state
}
}, updateMultipleChoice: { value in
updateState { state in
var state = state
state.focusOptionId = -1
state.isMultipleChoice = value
return state
}
}, displayMultipleChoiceDisabled: {
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
presentControllerImpl?(textAlertController(context: context, title: nil, text: presentationData.strings.CreatePoll_MultipleChoiceQuizAlert, actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_OK, action: {})]), nil)
}, updateQuiz: { value in
if !value {
displayQuizTooltipImpl?(value)
}
updateState { state in
var state = state
state.focusOptionId = -1
state.isQuiz = value
if value {
state.isMultipleChoice = false
var foundSelectedOption = false
for i in 0 ..< state.options.count {
if state.options[i].isSelected {
if state.options[i].item.isSelected {
if !foundSelectedOption {
foundSelectedOption = true
} else {
state.options[i].isSelected = false
state.options.update(at: i, { option in
option.isSelected = false
})
}
}
}
@ -457,7 +631,7 @@ public func createPollController(context: AccountContext, peerId: PeerId, comple
return state
}
if value {
displayQuizTooltipImpl?()
displayQuizTooltipImpl?(value)
}
})
@ -478,17 +652,17 @@ public func createPollController(context: AccountContext, peerId: PeerId, comple
var nonEmptyOptionCount = 0
var hasSelectedOptions = false
for option in state.options {
if !option.text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
if !option.item.text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
nonEmptyOptionCount += 1
}
if option.text.count > maxOptionLength {
if option.item.text.count > maxOptionLength {
enabled = false
}
if option.isSelected {
if option.item.isSelected {
hasSelectedOptions = true
}
if state.isQuiz {
if option.text.isEmpty && option.isSelected {
if option.item.text.isEmpty && option.item.isSelected {
enabled = false
}
}
@ -507,11 +681,11 @@ public func createPollController(context: AccountContext, peerId: PeerId, comple
var options: [TelegramMediaPollOption] = []
var correctAnswers: [Data]?
for i in 0 ..< state.options.count {
let optionText = state.options[i].text.trimmingCharacters(in: .whitespacesAndNewlines)
let optionText = state.options[i].item.text.trimmingCharacters(in: .whitespacesAndNewlines)
if !optionText.isEmpty {
let optionData = "\(i)".data(using: .utf8)!
options.append(TelegramMediaPollOption(text: optionText, opaqueIdentifier: optionData))
if state.isQuiz && state.options[i].isSelected {
if state.isQuiz && state.options[i].item.isSelected {
correctAnswers = [optionData]
}
}
@ -536,7 +710,7 @@ public func createPollController(context: AccountContext, peerId: PeerId, comple
let state = stateValue.with { $0 }
var hasNonEmptyOptions = false
for i in 0 ..< state.options.count {
let optionText = state.options[i].text.trimmingCharacters(in: .whitespacesAndNewlines)
let optionText = state.options[i].item.text.trimmingCharacters(in: .whitespacesAndNewlines)
if !optionText.isEmpty {
hasNonEmptyOptions = true
}
@ -551,14 +725,14 @@ public func createPollController(context: AccountContext, peerId: PeerId, comple
}
})
let optionIds = state.options.map { $0.id }
let optionIds = state.options.map { $0.item.id }
let previousIds = previousOptionIds.swap(optionIds)
var focusItemTag: ItemListItemTag?
var ensureVisibleItemTag: ItemListItemTag?
if let focusOptionId = state.focusOptionId {
focusItemTag = CreatePollEntryTag.option(focusOptionId)
if focusOptionId == state.options.last?.id {
if focusOptionId == state.options.last?.item.id {
ensureVisibleItemTag = nil
} else {
ensureVisibleItemTag = focusItemTag
@ -577,6 +751,7 @@ public func createPollController(context: AccountContext, peerId: PeerId, comple
actionsDisposable.dispose()
}
weak var currentTooltipController: TooltipController?
let controller = ItemListController(context: context, state: signal)
controller.navigationPresentation = .modal
presentControllerImpl = { [weak controller] c, a in
@ -615,7 +790,7 @@ public func createPollController(context: AccountContext, peerId: PeerId, comple
var resultItemNode: ListViewItemNode?
let state = stateValue.with({ $0 })
if state.options.last?.id == id {
if state.options.last?.item.id == id {
}
if resultItemNode == nil {
let _ = controller.frameForItemNode({ itemNode in
@ -634,27 +809,38 @@ public func createPollController(context: AccountContext, peerId: PeerId, comple
}
})
}
displayQuizTooltipImpl = { [weak controller] in
displayQuizTooltipImpl = { [weak controller] display in
guard let controller = controller else {
return
}
var resultItemNode: CreatePollOptionItemNode?
let insets = controller.listInsets
let _ = controller.frameForItemNode({ itemNode in
if resultItemNode == nil, let itemNode = itemNode as? CreatePollOptionItemNode {
resultItemNode = itemNode
return true
if itemNode.frame.minY >= insets.top {
resultItemNode = itemNode
return true
}
}
return false
})
if let resultItemNode = resultItemNode {
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let tooltipController = TooltipController(content: .text(presentationData.strings.CreatePoll_QuizTip), baseFontSize: presentationData.listsFontSize.baseDisplaySize, dismissByTapOutside: true)
controller.present(tooltipController, in: .window(.root), with: TooltipControllerPresentationArguments(sourceViewAndRect: { [weak resultItemNode] in
if let resultItemNode = resultItemNode {
return (resultItemNode.view, CGRect(origin: CGPoint(x: 0.0, y: 4.0), size: CGSize(width: 54.0, height: resultItemNode.bounds.height - 8.0)))
}
return nil
}))
if let resultItemNode = resultItemNode, let localCheckNodeFrame = resultItemNode.checkNodeFrame {
let checkNodeFrame = resultItemNode.view.convert(localCheckNodeFrame, to: controller.view)
if display {
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let tooltipController = TooltipController(content: .text(presentationData.strings.CreatePoll_QuizTip), baseFontSize: presentationData.listsFontSize.baseDisplaySize, dismissByTapOutside: true)
controller.present(tooltipController, in: .window(.root), with: TooltipControllerPresentationArguments(sourceViewAndRect: { [weak controller] in
if let controller = controller {
return (controller.view, checkNodeFrame.insetBy(dx: 0.0, dy: 0.0))
}
return nil
}))
tooltipController.displayNode.layer.animatePosition(from: CGPoint(x: -checkNodeFrame.maxX, y: 0.0), to: CGPoint(), duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, additive: true)
currentTooltipController = tooltipController
} else if let tooltipController = currentTooltipController{
currentTooltipController = nil
tooltipController.displayNode.layer.animatePosition(from: CGPoint(), to: CGPoint(x: -checkNodeFrame.maxX, y: 0.0), duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, additive: true)
}
}
}
controller.setReorderEntry({ (fromIndex: Int, toIndex: Int, entries: [CreatePollEntry]) -> Void in
@ -683,9 +869,9 @@ public func createPollController(context: AccountContext, peerId: PeerId, comple
updateState { state in
var state = state
var options = state.options
var reorderOption: CreatePollControllerOption?
var reorderOption: OrderedLinkedListItem<CreatePollControllerOption>?
for i in 0 ..< options.count {
if options[i].id == id {
if options[i].item.id == id {
reorderOption = options[i]
options.remove(at: i)
break
@ -694,24 +880,32 @@ public func createPollController(context: AccountContext, peerId: PeerId, comple
if let reorderOption = reorderOption {
if let referenceId = referenceId {
var inserted = false
for i in 0 ..< options.count {
if options[i].id == referenceId {
for i in 0 ..< options.count - 1 {
if options[i].item.id == referenceId {
if fromIndex < toIndex {
options.insert(reorderOption, at: i + 1)
options.insert(reorderOption.item, at: i + 1, id: reorderOption.ordering.id)
} else {
options.insert(reorderOption, at: i)
options.insert(reorderOption.item, at: i, id: reorderOption.ordering.id)
}
inserted = true
break
}
}
if !inserted {
options.append(reorderOption)
if options.count >= 2 {
options.insert(reorderOption.item, at: options.count - 1, id: reorderOption.ordering.id)
} else {
options.append(reorderOption.item, id: reorderOption.ordering.id)
}
}
} else if beforeAll {
options.insert(reorderOption, at: 0)
options.insert(reorderOption.item, at: 0, id: reorderOption.ordering.id)
} else if afterAll {
options.append(reorderOption)
if options.count >= 2 {
options.insert(reorderOption.item, at: options.count - 1, id: reorderOption.ordering.id)
} else {
options.append(reorderOption.item, id: reorderOption.ordering.id)
}
}
state.options = options
}
@ -721,6 +915,7 @@ public func createPollController(context: AccountContext, peerId: PeerId, comple
controller.isOpaqueWhenInOverlay = true
controller.blocksBackgroundWhenInOverlay = true
controller.experimentalSnapScrollToItem = true
controller.alwaysSynchronous = true
return controller
}

View File

@ -27,11 +27,11 @@ class CreatePollOptionItem: ListViewItem, ItemListItem {
let next: (() -> Void)?
let delete: (Bool) -> Void
let canDelete: Bool
let focused: () -> Void
let focused: (Bool) -> Void
let toggleSelected: () -> Void
let tag: ItemListItemTag?
init(presentationData: ItemListPresentationData, id: Int, placeholder: String, value: String, isSelected: Bool?, maxLength: Int, editing: CreatePollOptionItemEditing, sectionId: ItemListSectionId, setItemIdWithRevealedOptions: @escaping (Int?, Int?) -> Void, updated: @escaping (String) -> Void, next: (() -> Void)?, delete: @escaping (Bool) -> Void, canDelete: Bool, focused: @escaping () -> Void, toggleSelected: @escaping () -> Void, tag: ItemListItemTag?) {
init(presentationData: ItemListPresentationData, id: Int, placeholder: String, value: String, isSelected: Bool?, maxLength: Int, editing: CreatePollOptionItemEditing, sectionId: ItemListSectionId, setItemIdWithRevealedOptions: @escaping (Int?, Int?) -> Void, updated: @escaping (String) -> Void, next: (() -> Void)?, delete: @escaping (Bool) -> Void, canDelete: Bool, focused: @escaping (Bool) -> Void, toggleSelected: @escaping () -> Void, tag: ItemListItemTag?) {
self.presentationData = presentationData
self.id = id
self.placeholder = placeholder
@ -115,6 +115,13 @@ class CreatePollOptionItemNode: ItemListRevealOptionsItemNode, ItemListItemNode,
return self.containerNode
}
var checkNodeFrame: CGRect? {
guard let _ = self.layoutParams, let checkNode = self.checkNode else {
return nil
}
return checkNode.frame
}
init() {
self.containerNode = ASDisplayNode()
self.containerNode.clipsToBounds = true
@ -171,7 +178,11 @@ class CreatePollOptionItemNode: ItemListRevealOptionsItemNode, ItemListItemNode,
}
func editableTextNodeDidBeginEditing(_ editableTextNode: ASEditableTextNode) {
self.item?.focused()
self.item?.focused(true)
}
func editableTextNodeDidFinishEditing(_ editableTextNode: ASEditableTextNode) {
self.item?.focused(false)
}
func editableTextNode(_ editableTextNode: ASEditableTextNode, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
@ -287,8 +298,6 @@ class CreatePollOptionItemNode: ItemListRevealOptionsItemNode, ItemListItemNode,
strongSelf.item = item
strongSelf.layoutParams = params
strongSelf.containerNode.frame = CGRect(origin: CGPoint(), size: layout.contentSize)
if let _ = updatedTheme {
strongSelf.topStripeNode.backgroundColor = item.presentationData.theme.list.itemBlocksSeparatorColor
strongSelf.bottomStripeNode.backgroundColor = item.presentationData.theme.list.itemBlocksSeparatorColor
@ -404,14 +413,18 @@ class CreatePollOptionItemNode: ItemListRevealOptionsItemNode, ItemListItemNode,
strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.presentationData.theme, top: hasTopCorners, bottom: hasBottomCorners) : nil
strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight)))
strongSelf.maskNode.frame = strongSelf.backgroundNode.frame.insetBy(dx: params.leftInset, dy: 0.0)
strongSelf.topStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: layout.contentSize.width, height: separatorHeight))
transition.updateFrame(node: strongSelf.bottomStripeNode, frame: CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height - UIScreenPixel), size: CGSize(width: layout.contentSize.width - bottomStripeInset, height: separatorHeight)))
if strongSelf.animationForKey("apparentHeight") == nil {
strongSelf.containerNode.frame = CGRect(origin: CGPoint(), size: layout.contentSize)
strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight)))
strongSelf.maskNode.frame = strongSelf.backgroundNode.frame.insetBy(dx: params.leftInset, dy: 0.0)
strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height - UIScreenPixel), size: CGSize(width: layout.contentSize.width - bottomStripeInset, height: separatorHeight))
}
let _ = reorderSizeAndApply.1(layout.contentSize.height, displayTextLimit && layout.contentSize.height <= 44.0)
let _ = reorderSizeAndApply.1(layout.contentSize.height, displayTextLimit, transition)
let reorderControlFrame = CGRect(origin: CGPoint(x: params.width + revealOffset - params.rightInset - reorderSizeAndApply.0, y: 0.0), size: CGSize(width: reorderSizeAndApply.0, height: layout.contentSize.height))
strongSelf.reorderControlNode.frame = reorderControlFrame
strongSelf.reorderControlNode.isHidden = !item.canDelete
let _ = textLimitApply()
strongSelf.textLimitNode.frame = CGRect(origin: CGPoint(x: reorderControlFrame.minX + floor((reorderControlFrame.width - textLimitLayout.size.width) / 2.0) - 4.0 - UIScreenPixel, y: max(floor(reorderControlFrame.midY + 2.0), layout.contentSize.height - 15.0 - textLimitLayout.size.height)), size: textLimitLayout.size)
@ -473,7 +486,7 @@ class CreatePollOptionItemNode: ItemListRevealOptionsItemNode, ItemListItemNode,
}
override func isReorderable(at point: CGPoint) -> Bool {
if self.reorderControlNode.frame.contains(point), !self.isDisplayingRevealedOptions {
if self.reorderControlNode.frame.contains(point), !self.reorderControlNode.isHidden, !self.isDisplayingRevealedOptions {
return true
}
return false
@ -487,5 +500,14 @@ class CreatePollOptionItemNode: ItemListRevealOptionsItemNode, ItemListItemNode,
self.bottomStripeNode.frame = separatorFrame
self.containerNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: self.containerNode.bounds.width, height: currentValue))
let insets = self.insets
let separatorHeight = UIScreenPixel
guard let params = self.layoutParams else {
return
}
self.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: self.containerNode.bounds.width, height: currentValue + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight)))
self.maskNode.frame = self.backgroundNode.frame.insetBy(dx: params.leftInset, dy: 0.0)
}
}

View File

@ -3024,7 +3024,7 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture
stickLocationDistanceFactor = max(0.0, min(1.0, stickLocationDistance / itemHeaderHeight))
case .topEdge:
headerFrame = CGRect(origin: CGPoint(x: 0.0, y: min(max(upperDisplayBound, upperBoundEdge - itemHeaderHeight), lowerBound - itemHeaderHeight)), size: CGSize(width: self.visibleSize.width, height: itemHeaderHeight))
stickLocationDistance = headerFrame.minY - upperBoundEdge - itemHeaderHeight
stickLocationDistance = headerFrame.maxY - upperBoundEdge - itemHeaderHeight
stickLocationDistanceFactor = max(0.0, min(1.0, stickLocationDistance / itemHeaderHeight))
case .bottom:
headerFrame = CGRect(origin: CGPoint(x: 0.0, y: max(upperBound, min(lowerBound, lowerDisplayBound) - itemHeaderHeight)), size: CGSize(width: self.visibleSize.width, height: itemHeaderHeight))

View File

@ -20,9 +20,9 @@ public class ItemListPeerActionItem: ListViewItem, ItemListItem {
let editing: Bool
let height: ItemListPeerActionItemHeight
public let sectionId: ItemListSectionId
let action: () -> Void
let action: (() -> Void)?
public init(presentationData: ItemListPresentationData, icon: UIImage?, title: String, alwaysPlain: Bool = false, sectionId: ItemListSectionId, height: ItemListPeerActionItemHeight = .peerList, editing: Bool, action: @escaping () -> Void) {
public init(presentationData: ItemListPresentationData, icon: UIImage?, title: String, alwaysPlain: Bool = false, sectionId: ItemListSectionId, height: ItemListPeerActionItemHeight = .peerList, editing: Bool, action: (() -> Void)?) {
self.presentationData = presentationData
self.icon = icon
self.title = title
@ -79,11 +79,13 @@ public class ItemListPeerActionItem: ListViewItem, ItemListItem {
}
}
public var selectable: Bool = true
public var selectable: Bool {
return self.action != nil
}
public func selected(listView: ListView){
listView.clearHighlightAnimated(true)
self.action()
self.action?()
}
}

View File

@ -16,6 +16,195 @@ import PeerPresenceStatusManager
import ContextUI
import AccountContext
private final class ShimmerEffectNode: ASDisplayNode {
private var currentBackgroundColor: UIColor?
private var currentForegroundColor: UIColor?
private let imageNodeContainer: ASDisplayNode
private let imageNode: ASImageNode
private var absoluteLocation: (CGRect, CGSize)?
private var isCurrentlyInHierarchy = false
private var shouldBeAnimating = false
override init() {
self.imageNodeContainer = ASDisplayNode()
self.imageNodeContainer.isLayerBacked = true
self.imageNode = ASImageNode()
self.imageNode.isLayerBacked = true
self.imageNode.displaysAsynchronously = false
self.imageNode.displayWithoutProcessing = true
self.imageNode.contentMode = .scaleToFill
super.init()
self.isLayerBacked = true
self.clipsToBounds = true
self.imageNodeContainer.addSubnode(self.imageNode)
self.addSubnode(self.imageNodeContainer)
}
override func didEnterHierarchy() {
super.didEnterHierarchy()
self.isCurrentlyInHierarchy = true
self.updateAnimation()
}
override func didExitHierarchy() {
super.didExitHierarchy()
self.isCurrentlyInHierarchy = false
self.updateAnimation()
}
func update(backgroundColor: UIColor, foregroundColor: UIColor) {
if let currentBackgroundColor = self.currentBackgroundColor, currentBackgroundColor.isEqual(backgroundColor), let currentForegroundColor = self.currentForegroundColor, currentForegroundColor.isEqual(foregroundColor) {
return
}
self.currentBackgroundColor = backgroundColor
self.currentForegroundColor = foregroundColor
self.imageNode.image = generateImage(CGSize(width: 8.0, height: 100.0), opaque: true, scale: 1.0, rotatedContext: { size, context in
context.setFillColor(backgroundColor.cgColor)
context.fill(CGRect(origin: CGPoint(), size: size))
context.clip(to: CGRect(origin: CGPoint(), size: size))
let transparentColor = foregroundColor.withAlphaComponent(0.0).cgColor
let peakColor = foregroundColor.cgColor
var locations: [CGFloat] = [0.0, 0.2, 0.5, 0.8, 1.0]
let colors: [CGColor] = [transparentColor, transparentColor, peakColor, transparentColor, transparentColor]
let colorSpace = CGColorSpaceCreateDeviceRGB()
let gradient = CGGradient(colorsSpace: colorSpace, colors: colors as CFArray, locations: &locations)!
context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: 0.0, y: size.height), options: CGGradientDrawingOptions())
})
}
func updateAbsoluteRect(_ rect: CGRect, within containerSize: CGSize) {
if let absoluteLocation = self.absoluteLocation, absoluteLocation.0 == rect && absoluteLocation.1 == containerSize {
return
}
let sizeUpdated = self.absoluteLocation?.1 != containerSize
let frameUpdated = self.absoluteLocation?.0 != rect
self.absoluteLocation = (rect, containerSize)
if sizeUpdated {
if self.shouldBeAnimating {
self.imageNode.layer.removeAnimation(forKey: "shimmer")
self.addImageAnimation()
}
}
if frameUpdated {
self.imageNodeContainer.frame = CGRect(origin: CGPoint(x: -rect.minX, y: -rect.minY), size: containerSize)
}
}
private func updateAnimation() {
let shouldBeAnimating = self.isCurrentlyInHierarchy && self.absoluteLocation != nil
if shouldBeAnimating != self.shouldBeAnimating {
self.shouldBeAnimating = shouldBeAnimating
if shouldBeAnimating {
self.addImageAnimation()
} else {
self.imageNode.layer.removeAnimation(forKey: "shimmer")
}
}
}
private func addImageAnimation() {
guard let containerSize = self.absoluteLocation?.1 else {
return
}
let gradientHeight: CGFloat = 250.0
self.imageNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -gradientHeight), size: CGSize(width: containerSize.width, height: gradientHeight))
let animation = self.imageNode.layer.makeAnimation(from: 0.0 as NSNumber, to: (containerSize.height + gradientHeight) as NSNumber, keyPath: "position.y", timingFunction: CAMediaTimingFunctionName.easeOut.rawValue, duration: 1.3 * 1.0, delay: 0.0, mediaTimingFunction: nil, removeOnCompletion: true, additive: true)
animation.repeatCount = Float.infinity
animation.beginTime = 1.0
self.imageNode.layer.add(animation, forKey: "shimmer")
}
}
private final class LoadingShimmerNode: ASDisplayNode {
enum Shape: Equatable {
case circle(CGRect)
case roundedRectLine(startPoint: CGPoint, width: CGFloat, diameter: CGFloat)
}
private let backgroundNode: ASDisplayNode
private let effectNode: ShimmerEffectNode
private let foregroundNode: ASImageNode
private var currentShapes: [Shape] = []
private var currentBackgroundColor: UIColor?
private var currentForegroundColor: UIColor?
private var currentShimmeringColor: UIColor?
private var currentSize = CGSize()
override init() {
self.backgroundNode = ASDisplayNode()
self.effectNode = ShimmerEffectNode()
self.foregroundNode = ASImageNode()
self.foregroundNode.displaysAsynchronously = false
self.foregroundNode.displayWithoutProcessing = true
super.init()
self.addSubnode(self.backgroundNode)
self.addSubnode(self.effectNode)
self.addSubnode(self.foregroundNode)
}
func updateAbsoluteRect(_ rect: CGRect, within containerSize: CGSize) {
self.effectNode.updateAbsoluteRect(rect, within: containerSize)
}
func update(backgroundColor: UIColor, foregroundColor: UIColor, shimmeringColor: UIColor, shapes: [Shape], size: CGSize) {
if self.currentShapes == shapes, let currentBackgroundColor = self.currentBackgroundColor, currentBackgroundColor.isEqual(backgroundColor), let currentForegroundColor = self.currentForegroundColor, currentForegroundColor.isEqual(foregroundColor), let currentShimmeringColor = self.currentShimmeringColor, currentShimmeringColor.isEqual(shimmeringColor), self.currentSize == size {
return
}
self.currentBackgroundColor = backgroundColor
self.currentForegroundColor = foregroundColor
self.currentShimmeringColor = shimmeringColor
self.currentShapes = shapes
self.currentSize = size
self.backgroundNode.backgroundColor = foregroundColor
self.effectNode.update(backgroundColor: foregroundColor, foregroundColor: shimmeringColor)
self.foregroundNode.image = generateImage(size, rotatedContext: { size, context in
context.setFillColor(backgroundColor.cgColor)
context.setBlendMode(.copy)
context.fill(CGRect(origin: CGPoint(), size: size))
context.setFillColor(UIColor.clear.cgColor)
for shape in shapes {
switch shape {
case let .circle(frame):
context.fillEllipse(in: frame)
case let .roundedRectLine(startPoint, width, diameter):
context.fillEllipse(in: CGRect(origin: startPoint, size: CGSize(width: diameter, height: diameter)))
context.fillEllipse(in: CGRect(origin: CGPoint(x: startPoint.x + width - diameter, y: startPoint.y), size: CGSize(width: diameter, height: diameter)))
context.fill(CGRect(origin: CGPoint(x: startPoint.x + diameter / 2.0, y: startPoint.y), size: CGSize(width: width - diameter, height: diameter)))
}
}
})
self.backgroundNode.frame = CGRect(origin: CGPoint(), size: size)
self.foregroundNode.frame = CGRect(origin: CGPoint(), size: size)
self.effectNode.frame = CGRect(origin: CGPoint(), size: size)
}
}
public struct ItemListPeerItemEditing: Equatable {
public var editable: Bool
public var editing: Bool
@ -107,6 +296,14 @@ public struct ItemListPeerItemRevealOptions {
}
}
public struct ItemListPeerItemShimmering {
public var alternationIndex: Int
public init(alternationIndex: Int) {
self.alternationIndex = alternationIndex
}
}
public final class ItemListPeerItem: ListViewItem, ItemListItem {
let presentationData: ItemListPresentationData
let dateTimeFormat: PresentationDateTimeFormat
@ -136,8 +333,9 @@ public final class ItemListPeerItem: ListViewItem, ItemListItem {
let noInsets: Bool
public let tag: ItemListItemTag?
let header: ListViewItemHeader?
let shimmering: ItemListPeerItemShimmering?
public init(presentationData: ItemListPresentationData, dateTimeFormat: PresentationDateTimeFormat, nameDisplayOrder: PresentationPersonNameOrder, context: AccountContext, peer: Peer, height: ItemListPeerItemHeight = .peerList, aliasHandling: ItemListPeerItemAliasHandling = .standard, nameColor: ItemListPeerItemNameColor = .primary, nameStyle: ItemListPeerItemNameStyle = .distinctBold, presence: PeerPresence?, text: ItemListPeerItemText, label: ItemListPeerItemLabel, editing: ItemListPeerItemEditing, revealOptions: ItemListPeerItemRevealOptions? = nil, switchValue: ItemListPeerItemSwitch?, enabled: Bool, selectable: Bool, sectionId: ItemListSectionId, action: (() -> Void)?, setPeerIdWithRevealedOptions: @escaping (PeerId?, PeerId?) -> Void, removePeer: @escaping (PeerId) -> Void, toggleUpdated: ((Bool) -> Void)? = nil, contextAction: ((ASDisplayNode, ContextGesture?) -> Void)? = nil, hasTopStripe: Bool = true, hasTopGroupInset: Bool = true, noInsets: Bool = false, tag: ItemListItemTag? = nil, header: ListViewItemHeader? = nil) {
public init(presentationData: ItemListPresentationData, dateTimeFormat: PresentationDateTimeFormat, nameDisplayOrder: PresentationPersonNameOrder, context: AccountContext, peer: Peer, height: ItemListPeerItemHeight = .peerList, aliasHandling: ItemListPeerItemAliasHandling = .standard, nameColor: ItemListPeerItemNameColor = .primary, nameStyle: ItemListPeerItemNameStyle = .distinctBold, presence: PeerPresence?, text: ItemListPeerItemText, label: ItemListPeerItemLabel, editing: ItemListPeerItemEditing, revealOptions: ItemListPeerItemRevealOptions? = nil, switchValue: ItemListPeerItemSwitch?, enabled: Bool, selectable: Bool, sectionId: ItemListSectionId, action: (() -> Void)?, setPeerIdWithRevealedOptions: @escaping (PeerId?, PeerId?) -> Void, removePeer: @escaping (PeerId) -> Void, toggleUpdated: ((Bool) -> Void)? = nil, contextAction: ((ASDisplayNode, ContextGesture?) -> Void)? = nil, hasTopStripe: Bool = true, hasTopGroupInset: Bool = true, noInsets: Bool = false, tag: ItemListItemTag? = nil, header: ListViewItemHeader? = nil, shimmering: ItemListPeerItemShimmering? = nil) {
self.presentationData = presentationData
self.dateTimeFormat = dateTimeFormat
self.nameDisplayOrder = nameDisplayOrder
@ -166,6 +364,7 @@ public final class ItemListPeerItem: ListViewItem, ItemListItem {
self.noInsets = noInsets
self.tag = tag
self.header = header
self.shimmering = shimmering
}
public func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) {
@ -247,6 +446,9 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo
private var switchNode: SwitchNode?
private var checkNode: ASImageNode?
private var shimmerNode: LoadingShimmerNode?
private var absoluteLocation: (CGRect, CGSize)?
private var peerPresenceManager: PeerPresenceStatusManager?
private var layoutParams: (ItemListPeerItem, ListViewItemLayoutParams, ItemListNeighbors, Bool)?
@ -847,6 +1049,44 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo
strongSelf.peerPresenceManager?.reset(presence: presence)
}
if let shimmering = item.shimmering {
strongSelf.avatarNode.isHidden = true
strongSelf.titleNode.isHidden = true
let shimmerNode: LoadingShimmerNode
if let current = strongSelf.shimmerNode {
shimmerNode = current
} else {
shimmerNode = LoadingShimmerNode()
strongSelf.shimmerNode = shimmerNode
strongSelf.insertSubnode(shimmerNode, aboveSubnode: strongSelf.backgroundNode)
}
shimmerNode.frame = CGRect(origin: CGPoint(), size: layout.contentSize)
if let (rect, size) = strongSelf.absoluteLocation {
shimmerNode.updateAbsoluteRect(rect, within: size)
}
var shapes: [LoadingShimmerNode.Shape] = []
shapes.append(.circle(strongSelf.avatarNode.frame))
let possibleLines: [[CGFloat]] = [
[50.0, 40.0],
[70.0, 45.0]
]
let titleFrame = strongSelf.titleNode.frame
let lineDiameter: CGFloat = 10.0
var lineStart = titleFrame.minX
for lineWidth in possibleLines[shimmering.alternationIndex % possibleLines.count] {
shapes.append(.roundedRectLine(startPoint: CGPoint(x: lineStart, y: titleFrame.minY + floor((titleFrame.height - lineDiameter) / 2.0)), width: lineWidth, diameter: lineDiameter))
lineStart += lineWidth + lineDiameter
}
shimmerNode.update(backgroundColor: item.presentationData.theme.list.itemBlocksBackgroundColor, foregroundColor: item.presentationData.theme.list.mediaPlaceholderColor, shimmeringColor: item.presentationData.theme.list.itemBlocksBackgroundColor.withAlphaComponent(0.4), shapes: shapes, size: layout.contentSize)
} else if let shimmerNode = strongSelf.shimmerNode {
strongSelf.avatarNode.isHidden = false
strongSelf.titleNode.isHidden = false
strongSelf.shimmerNode = nil
shimmerNode.removeFromSupernode()
}
strongSelf.updateLayout(size: layout.contentSize, leftInset: params.leftInset, rightInset: params.rightInset)
strongSelf.setRevealOptions((left: [], right: peerRevealOptions))
@ -993,6 +1233,15 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo
override public func header() -> ListViewItemHeader? {
return self.layoutParams?.0.header
}
override public func updateAbsoluteRect(_ rect: CGRect, within containerSize: CGSize) {
var rect = rect
rect.origin.y += self.insets.top
self.absoluteLocation = (rect, containerSize)
if let shimmerNode = self.shimmerNode {
shimmerNode.updateAbsoluteRect(rect, within: containerSize)
}
}
}
public final class ItemListPeerItemHeader: ListViewItemHeader {
@ -1033,6 +1282,7 @@ public final class ItemListPeerItemHeaderNode: ListViewItemHeaderNode {
private var validLayout: (size: CGSize, leftInset: CGFloat, rightInset: CGFloat)?
private let backgroundNode: ASDisplayNode
private let snappedBackgroundNode: ASDisplayNode
private let separatorNode: ASDisplayNode
private let textNode: ImmediateTextNode
private let actionTextNode: ImmediateTextNode
@ -1049,6 +1299,10 @@ public final class ItemListPeerItemHeaderNode: ListViewItemHeaderNode {
self.backgroundNode = ASDisplayNode()
self.backgroundNode.backgroundColor = theme.list.blocksBackgroundColor
self.snappedBackgroundNode = ASDisplayNode()
self.snappedBackgroundNode.backgroundColor = theme.rootController.navigationBar.backgroundColor
self.snappedBackgroundNode.alpha = 0.0
self.separatorNode = ASDisplayNode()
self.separatorNode.backgroundColor = theme.list.itemBlocksSeparatorColor
@ -1070,6 +1324,7 @@ public final class ItemListPeerItemHeaderNode: ListViewItemHeaderNode {
super.init()
self.addSubnode(self.backgroundNode)
self.addSubnode(self.snappedBackgroundNode)
self.addSubnode(self.separatorNode)
self.addSubnode(self.textNode)
self.addSubnode(self.actionTextNode)
@ -1117,6 +1372,7 @@ public final class ItemListPeerItemHeaderNode: ListViewItemHeaderNode {
override public func updateLayout(size: CGSize, leftInset: CGFloat, rightInset: CGFloat) {
self.validLayout = (size, leftInset, rightInset)
self.backgroundNode.frame = CGRect(origin: CGPoint(), size: size)
self.snappedBackgroundNode.frame = CGRect(origin: CGPoint(), size: size)
self.separatorNode.frame = CGRect(origin: CGPoint(x: 0.0, y: size.height - UIScreenPixel), size: CGSize(width: size.width, height: UIScreenPixel))
let sideInset: CGFloat = 15.0 + leftInset
@ -1135,6 +1391,6 @@ public final class ItemListPeerItemHeaderNode: ListViewItemHeaderNode {
}
override public func updateStickDistanceFactor(_ factor: CGFloat, transition: ContainedViewLayoutTransition) {
//transition.updateAlpha(node: self.separatorNode, alpha: (1.0 - factor) * 0.0 + factor * 1.0)
transition.updateAlpha(node: self.snappedBackgroundNode, alpha: (1.0 - factor) * 0.0 + factor * 1.0)
}
}

View File

@ -324,7 +324,7 @@ class ItemListStickerPackItemNode: ItemListRevealOptionsItemNode {
let separatorHeight = UIScreenPixel
var editableControlSizeAndApply: (CGFloat, (CGFloat) -> ItemListEditableControlNode)?
var reorderControlSizeAndApply: (CGFloat, (CGFloat, Bool) -> ItemListEditableReorderControlNode)?
var reorderControlSizeAndApply: (CGFloat, (CGFloat, Bool, ContainedViewLayoutTransition) -> ItemListEditableReorderControlNode)?
var editingOffset: CGFloat = 0.0
var reorderInset: CGFloat = 0.0
@ -485,7 +485,7 @@ class ItemListStickerPackItemNode: ItemListRevealOptionsItemNode {
if let reorderControlSizeAndApply = reorderControlSizeAndApply {
if strongSelf.reorderControlNode == nil {
let reorderControlNode = reorderControlSizeAndApply.1(layout.contentSize.height, false)
let reorderControlNode = reorderControlSizeAndApply.1(layout.contentSize.height, false, .immediate)
strongSelf.reorderControlNode = reorderControlNode
strongSelf.addSubnode(reorderControlNode)
reorderControlNode.alpha = 0.0

View File

@ -517,6 +517,10 @@ open class ItemListController: ViewController, KeyShortcutResponder, Presentable
self.didDisappear?(animated)
}
public var listInsets: UIEdgeInsets {
return (self.displayNode as! ItemListControllerNode).listNode.insets
}
public func frameForItemNode(_ predicate: (ListViewItemNode) -> Bool) -> CGRect? {
var result: CGRect?
(self.displayNode as! ItemListControllerNode).listNode.forEachItemNode { itemNode in

View File

@ -19,7 +19,7 @@ public final class ItemListEditableReorderControlNode: ASDisplayNode {
self.addSubnode(self.iconNode)
}
public static func asyncLayout(_ node: ItemListEditableReorderControlNode?) -> (_ theme: PresentationTheme) -> (CGFloat, (CGFloat, Bool) -> ItemListEditableReorderControlNode) {
public static func asyncLayout(_ node: ItemListEditableReorderControlNode?) -> (_ theme: PresentationTheme) -> (CGFloat, (CGFloat, Bool, ContainedViewLayoutTransition) -> ItemListEditableReorderControlNode) {
return { theme in
let image = PresentationResourcesItemList.itemListReorderIndicatorIcon(theme)
@ -31,9 +31,9 @@ public final class ItemListEditableReorderControlNode: ASDisplayNode {
}
resultNode.iconNode.image = image
return (40.0, { height, offsetForLabel in
return (40.0, { height, offsetForLabel, transition in
if let image = image {
resultNode.iconNode.frame = CGRect(origin: CGPoint(x: 7.0, y: floor((height - image.size.height) / 2.0) - (offsetForLabel ? 6.0 : 0.0)), size: image.size)
transition.updateFrame(node: resultNode.iconNode, frame: CGRect(origin: CGPoint(x: 7.0, y: floor((height - image.size.height) / 2.0) - (offsetForLabel ? 6.0 : 0.0)), size: image.size))
}
return resultNode
})

View File

@ -241,7 +241,7 @@ private final class ProxySettingsServerItemNode: ItemListRevealOptionsItemNode {
let statusAttributedString = NSAttributedString(string: item.label, font: statusFont, textColor: item.labelAccent ? item.theme.list.itemAccentColor : item.theme.list.itemSecondaryTextColor)
var editableControlSizeAndApply: (CGFloat, (CGFloat) -> ItemListEditableControlNode)?
var reorderControlSizeAndApply: (CGFloat, (CGFloat, Bool) -> ItemListEditableReorderControlNode)?
var reorderControlSizeAndApply: (CGFloat, (CGFloat, Bool, ContainedViewLayoutTransition) -> ItemListEditableReorderControlNode)?
let editingOffset: CGFloat
var reorderInset: CGFloat = 0.0
@ -341,7 +341,7 @@ private final class ProxySettingsServerItemNode: ItemListRevealOptionsItemNode {
if let reorderControlSizeAndApply = reorderControlSizeAndApply {
if strongSelf.reorderControlNode == nil {
let reorderControlNode = reorderControlSizeAndApply.1(layout.contentSize.height, false)
let reorderControlNode = reorderControlSizeAndApply.1(layout.contentSize.height, false, .immediate)
strongSelf.reorderControlNode = reorderControlNode
strongSelf.addSubnode(reorderControlNode)
reorderControlNode.alpha = 0.0

View File

@ -172,10 +172,19 @@ private func collectExternalShareItems(strings: PresentationStrings, dateTimeFor
text.append("\n\(option.text)")
}
let totalVoters = poll.results.totalVoters ?? 0
if totalVoters == 0 {
text.append("\n\(strings.MessagePoll_NoVotes)")
} else {
text.append("\n\(strings.MessagePoll_VotedCount(totalVoters))")
switch poll.kind {
case .poll:
if totalVoters == 0 {
text.append("\n\(strings.MessagePoll_NoVotes)")
} else {
text.append("\n\(strings.MessagePoll_VotedCount(totalVoters))")
}
case .quiz:
if totalVoters == 0 {
text.append("\n\(strings.MessagePoll_QuizNoUsers)")
} else {
text.append("\n\(strings.MessagePoll_QuizCount(totalVoters))")
}
}
signals.append(.single(.done(.text(text))))
} else if let mediaReference = item.mediaReference, let contact = mediaReference.media as? TelegramMediaContact {

View File

@ -445,12 +445,12 @@ struct RevalidatedMediaResource {
func revalidateMediaResourceReference(postbox: Postbox, network: Network, revalidationContext: MediaReferenceRevalidationContext, info: TelegramCloudMediaResourceFetchInfo, resource: MediaResource) -> Signal<RevalidatedMediaResource, RevalidateMediaReferenceError> {
var updatedReference = info.reference
if case let .media(media, resource) = updatedReference {
if case let .message(_, mediaValue) = media {
if case let .message(messageReference, mediaValue) = media {
if let file = mediaValue as? TelegramMediaFile {
if let partialReference = file.partialReference {
updatedReference = partialReference.mediaReference(media.media).resourceReference(resource)
}
if file.isSticker {
if file.isSticker, messageReference.isSecret == true {
var stickerPackReference: StickerPackReference?
for attribute in file.attributes {
if case let .Sticker(sticker) = attribute {

View File

@ -10,7 +10,7 @@ public enum RequestMessageSelectPollOptionError {
case generic
}
public func requestMessageSelectPollOption(account: Account, messageId: MessageId, opaqueIdentifiers: [Data]) -> Signal<Never, RequestMessageSelectPollOptionError> {
public func requestMessageSelectPollOption(account: Account, messageId: MessageId, opaqueIdentifiers: [Data]) -> Signal<TelegramMediaPoll?, RequestMessageSelectPollOptionError> {
return account.postbox.loadedPeerWithId(messageId.peerId)
|> take(1)
|> castError(RequestMessageSelectPollOptionError.self)
@ -20,12 +20,46 @@ public func requestMessageSelectPollOption(account: Account, messageId: MessageI
|> mapError { _ -> RequestMessageSelectPollOptionError in
return .generic
}
|> mapToSignal { result -> Signal<Never, RequestMessageSelectPollOptionError> in
|> mapToSignal { result -> Signal<TelegramMediaPoll?, RequestMessageSelectPollOptionError> in
var resultPoll: TelegramMediaPoll?
switch result {
case let .updates(updates, _, _, _, _):
for update in updates {
switch update {
case let .updateMessagePoll(_, pollId, poll, results):
if let poll = poll {
switch poll {
case let .poll(id, flags, question, answers):
let publicity: TelegramMediaPollPublicity
if (flags & (1 << 1)) != 0 {
publicity = .public
} else {
publicity = .anonymous
}
let kind: TelegramMediaPollKind
if (flags & (1 << 3)) != 0 {
kind = .quiz
} else {
kind = .poll(multipleAnswers: (flags & (1 << 2)) != 0)
}
resultPoll = TelegramMediaPoll(pollId: MediaId(namespace: Namespaces.Media.CloudPoll, id: id), publicity: publicity, kind: kind, text: question, options: answers.map(TelegramMediaPollOption.init(apiOption:)), correctAnswers: nil, results: TelegramMediaPollResults(apiResults: results), isClosed: (flags & (1 << 0)) != 0)
default:
break
}
}
default:
break
}
}
break
default:
break
}
account.stateManager.addUpdates(result)
return .complete()
return .single(resultPoll)
}
} else {
return .complete()
return .single(nil)
}
}
}
@ -131,7 +165,7 @@ private final class PollResultsOptionContext {
}
|> mapToSignal { inputPeer -> Signal<([RenderedPeer], Int, String?), NoError> in
if let inputPeer = inputPeer {
return account.network.request(Api.functions.messages.getPollVotes(flags: 1 << 0, peer: inputPeer, id: messageId.id, option: Buffer(data: opaqueIdentifier), offset: nextOffset, limit: nextOffset == nil ? 15 : 50))
let signal = account.network.request(Api.functions.messages.getPollVotes(flags: 1 << 0, peer: inputPeer, id: messageId.id, option: Buffer(data: opaqueIdentifier), offset: nextOffset, limit: nextOffset == nil ? 1 : 50))
|> map(Optional.init)
|> `catch` { _ -> Signal<Api.messages.VotesList?, NoError> in
return .single(nil)
@ -163,6 +197,10 @@ private final class PollResultsOptionContext {
}
}
}
#if DEBUG
return signal |> delay(4.0, queue: .concurrentDefaultQueue())
#endif
return signal
} else {
return .single(([], 0, nil))
}

View File

@ -226,6 +226,7 @@ public func customizeDefaultDayTheme(theme: PresentationTheme, editing: Bool, ti
linkHighlightColor: accentColor?.withAlphaComponent(0.3),
accentTextColor: accentColor,
accentControlColor: accentColor,
accentControlDisabledColor: accentColor?.withAlphaComponent(0.7),
mediaActiveControlColor: accentColor,
fileTitleColor: accentColor,
polls: chat.message.incoming.polls.withUpdated(
@ -261,6 +262,7 @@ public func customizeDefaultDayTheme(theme: PresentationTheme, editing: Bool, ti
scamColor: outgoingScamColor,
accentTextColor: outgoingAccentTextColor,
accentControlColor: outgoingControlColor,
accentControlDisabledColor: outgoingControlColor?.withAlphaComponent(0.7),
mediaActiveControlColor: outgoingControlColor,
mediaInactiveControlColor: outgoingInactiveControlColor,
mediaControlInnerBackgroundColor: .clear,
@ -516,7 +518,7 @@ public func makeDefaultDayPresentationTheme(extendingThemeReference: Presentatio
textHighlightColor: UIColor(rgb: 0xffe438),
accentTextColor: UIColor(rgb: 0x00a700),
accentControlColor: UIColor(rgb: 0x3fc33b),
accentControlDisabledColor: UIColor(rgb: 0x00A700).withAlphaComponent(0.7),
accentControlDisabledColor: UIColor(rgb: 0x3fc33b).withAlphaComponent(0.7),
mediaActiveControlColor: UIColor(rgb: 0x3fc33b),
mediaInactiveControlColor: UIColor(rgb: 0x93d987),
mediaControlInnerBackgroundColor: UIColor(rgb: 0xe1ffc7),

View File

@ -1538,11 +1538,29 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
guard let strongSelf = self, let controllerInteraction = strongSelf.controllerInteraction else {
return
}
guard !strongSelf.presentationInterfaceState.isScheduledMessages else {
strongSelf.present(textAlertController(context: strongSelf.context, title: nil, text: strongSelf.presentationData.strings.ScheduledMessages_PollUnavailable, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root))
return
}
if controllerInteraction.pollActionState.pollMessageIdsInProgress[id] == nil {
#if DEBUG
if false {
var found = false
strongSelf.chatDisplayNode.historyNode.forEachVisibleItemNode { itemNode in
if !found, let itemNode = itemNode as? ChatMessageBubbleItemNode, itemNode.item?.message.id == id {
found = true
itemNode.animateQuizInvalidOptionSelected()
}
}
return;
}
if false {
strongSelf.chatDisplayNode.animateQuizCorrectOptionSelected()
return;
}
#endif
controllerInteraction.pollActionState.pollMessageIdsInProgress[id] = opaqueIdentifiers
strongSelf.chatDisplayNode.historyNode.requestMessageUpdate(id)
let disposables: DisposableDict<MessageId>
@ -1554,7 +1572,38 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
}
let signal = requestMessageSelectPollOption(account: strongSelf.context.account, messageId: id, opaqueIdentifiers: opaqueIdentifiers)
disposables.set((signal
|> deliverOnMainQueue).start(error: { _ in
|> deliverOnMainQueue).start(next: { resultPoll in
guard let strongSelf = self, let resultPoll = resultPoll else {
return
}
guard let message = strongSelf.chatDisplayNode.historyNode.messageInCurrentHistoryView(id) else {
return
}
switch resultPoll.kind {
case .poll:
break
case .quiz:
if let voters = resultPoll.results.voters {
for voter in voters {
if voter.selected {
if voter.isCorrect {
strongSelf.chatDisplayNode.animateQuizCorrectOptionSelected()
} else {
var found = false
strongSelf.chatDisplayNode.historyNode.forEachVisibleItemNode { itemNode in
if !found, let itemNode = itemNode as? ChatMessageBubbleItemNode, itemNode.item?.message.id == id {
found = true
itemNode.animateQuizInvalidOptionSelected()
}
}
}
break
}
}
}
}
}, error: { _ in
guard let strongSelf = self, let controllerInteraction = strongSelf.controllerInteraction else {
return
}
@ -3993,14 +4042,37 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
strongSelf.selectPollOptionFeedback?.success()
}), forKey: id)
}, requestStopPollInMessage: { [weak self] id in
guard let strongSelf = self else {
guard let strongSelf = self, let message = strongSelf.chatDisplayNode.historyNode.messageInCurrentHistoryView(id) else {
return
}
var maybePoll: TelegramMediaPoll?
for media in message.media {
if let poll = media as? TelegramMediaPoll {
maybePoll = poll
break
}
}
guard let poll = maybePoll else {
return
}
let actionTitle: String
let actionButtonText: String
switch poll.kind {
case .poll:
actionTitle = strongSelf.presentationData.strings.Conversation_StopPollConfirmationTitle
actionButtonText = strongSelf.presentationData.strings.Conversation_StopPollConfirmation
case .quiz:
actionTitle = strongSelf.presentationData.strings.Conversation_StopQuizConfirmationTitle
actionButtonText = strongSelf.presentationData.strings.Conversation_StopQuizConfirmation
}
let actionSheet = ActionSheetController(presentationData: strongSelf.presentationData)
actionSheet.setItemGroups([ActionSheetItemGroup(items: [
ActionSheetTextItem(title: strongSelf.presentationData.strings.Conversation_StopPollConfirmationTitle),
ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_StopPollConfirmation, color: .destructive, action: { [weak self, weak actionSheet] in
ActionSheetTextItem(title: actionTitle),
ActionSheetButtonItem(title: actionButtonText, color: .destructive, action: { [weak self, weak actionSheet] in
actionSheet?.dismissAnimated()
guard let strongSelf = self else {
return

View File

@ -2324,4 +2324,124 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate {
transition.updateAlpha(node: self.inputPanelBackgroundSeparatorNode, alpha: resolvedValue, beginWithCurrentState: true)
}
}
func animateQuizCorrectOptionSelected() {
class ConfettiView: UIView {
private let direction: Bool
private let confettiViewEmitterLayer = CAEmitterLayer()
private let confettiViewEmitterCell = CAEmitterCell()
init(frame: CGRect, direction: Bool) {
self.direction = direction
super.init(frame: frame)
self.isUserInteractionEnabled = false
self.setupConfettiEmitterLayer()
self.confettiViewEmitterLayer.frame = self.bounds
self.confettiViewEmitterLayer.emitterCells = generateConfettiEmitterCells()
self.layer.addSublayer(self.confettiViewEmitterLayer)
let animation = CAKeyframeAnimation(keyPath: #keyPath(CAEmitterLayer.birthRate))
animation.duration = 0.5
animation.timingFunction = CAMediaTimingFunction(name: .easeIn)
animation.fillMode = .forwards
animation.values = [1, 0, 0]
animation.keyTimes = [0, 0.5, 1]
animation.isRemovedOnCompletion = false
self.confettiViewEmitterLayer.beginTime = CACurrentMediaTime()
self.confettiViewEmitterLayer.birthRate = 1.0
CATransaction.begin()
CATransaction.setCompletionBlock { [weak self] in
guard let strongSelf = self else {
return
}
strongSelf.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, delay: 1.0, removeOnCompletion: false, completion: { _ in
self?.removeFromSuperview()
})
}
self.confettiViewEmitterLayer.add(animation, forKey: nil)
CATransaction.commit()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func setupConfettiEmitterLayer() {
let emitterWidth: CGFloat = self.bounds.width / 4.0
self.confettiViewEmitterLayer.emitterSize = CGSize(width: emitterWidth, height: 2.0)
self.confettiViewEmitterLayer.emitterShape = .line
self.confettiViewEmitterLayer.emitterPosition = CGPoint(x: direction ? 0.0 : (self.bounds.width - emitterWidth * 0.0), y: self.bounds.height)
}
private func generateConfettiEmitterCells() -> [CAEmitterCell] {
var cells = [CAEmitterCell]()
let cellImageCircle = generateFilledCircleImage(diameter: 4.0, color: .white)!.cgImage!
let cellImageLine = generateImage(CGSize(width: 4.0, height: 10.0), opaque: false, rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
context.setFillColor(UIColor.white.cgColor)
context.fillEllipse(in: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: size.width)))
context.fillEllipse(in: CGRect(origin: CGPoint(x: 0.0, y: size.height - size.width), size: CGSize(width: size.width, height: size.width)))
context.fill(CGRect(origin: CGPoint(x: 0.0, y: size.width / 2.0), size: CGSize(width: size.width, height: size.height - size.width)))
})!.cgImage!
for index in 0 ..< 4 {
let cell = CAEmitterCell()
cell.color = self.nextColor(i: index).cgColor
cell.contents = index % 2 == 0 ? cellImageCircle : cellImageLine
cell.birthRate = 60.0
cell.lifetime = 14.0
cell.lifetimeRange = 0
if index % 2 == 0 {
cell.scale = 0.8
cell.scaleRange = 0.4
} else {
cell.scale = 0.5
cell.scaleRange = 0.1
}
cell.velocity = -self.randomVelocity
cell.velocityRange = abs(cell.velocity) * 0.3
cell.yAcceleration = 3000.0
cell.emissionLongitude = (self.direction ? -1.0 : 1.0) * (CGFloat.pi * 0.95)
cell.emissionRange = 0.2
cell.spin = 5.5
cell.spinRange = 1.0
cells.append(cell)
}
return cells
}
var randomNumber: Int {
let dimension = 4
return Int(arc4random_uniform(UInt32(dimension)))
}
var randomVelocity: CGFloat {
let velocities: [CGFloat] = [100.0, 120.0, 130.0, 140.0]
return velocities[self.randomNumber] * 12.0
}
private let colors: [UIColor] = ([
0x56CE6B,
0xCD89D0,
0x1E9AFF,
0xFF8724
] as [UInt32]).map(UIColor.init(rgb:))
private func nextColor(i: Int) -> UIColor {
return self.colors[i % self.colors.count]
}
}
self.view.insertSubview(ConfettiView(frame: self.view.bounds, direction: true), aboveSubview: self.historyNode.view)
self.view.insertSubview(ConfettiView(frame: self.view.bounds, direction: false), aboveSubview: self.historyNode.view)
}
}

View File

@ -565,7 +565,14 @@ func contextMenuForChatPresentationIntefaceState(chatPresentationInterfaceState:
}
if canStopPoll {
actions.append(.action(ContextMenuActionItem(text: chatPresentationInterfaceState.strings.Conversation_StopPoll, icon: { theme in
let stopPollAction: String
switch activePoll.kind {
case .poll:
stopPollAction = chatPresentationInterfaceState.strings.Conversation_StopPoll
case .quiz:
stopPollAction = chatPresentationInterfaceState.strings.Conversation_StopQuiz
}
actions.append(.action(ContextMenuActionItem(text: stopPollAction, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/StopPoll"), color: theme.actionSheet.primaryTextColor)
}, action: { _, f in
interfaceInteraction.requestStopPollInMessage(messages[0].id)

View File

@ -2895,4 +2895,53 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePrevewItemNode
let isPreview = self.item?.presentationData.isPreview ?? false
return self.contextSourceNode.isExtractedToContextPreview || hasWallpaper || isPreview
}
func animateQuizInvalidOptionSelected() {
let duration: Double = 0.5
let minScale: CGFloat = -0.03
let scaleAnimation0 = self.layer.makeAnimation(from: 0.0 as NSNumber, to: minScale as NSNumber, keyPath: "transform.scale", timingFunction: CAMediaTimingFunctionName.linear.rawValue, duration: duration / 2.0, removeOnCompletion: false, additive: true, completion: { [weak self] _ in
guard let strongSelf = self else {
return
}
let scaleAnimation1 = strongSelf.layer.makeAnimation(from: minScale as NSNumber, to: 0.0 as NSNumber, keyPath: "transform.scale", timingFunction: CAMediaTimingFunctionName.linear.rawValue, duration: duration / 2.0, additive: true)
strongSelf.layer.add(scaleAnimation1, forKey: "quizInvalidScale")
})
self.layer.add(scaleAnimation0, forKey: "quizInvalidScale")
let k = Float(UIView.animationDurationFactor())
var speed: Float = 1.0
if k != 0 && k != 1 {
speed = Float(1.0) / k
}
let count = 4
let animation = CAKeyframeAnimation(keyPath: "transform.rotation.z")
var values: [CGFloat] = []
values.append(0.0)
let rotationAmplitude: CGFloat = CGFloat.pi / 180.0 * 3.0
for i in 0 ..< count {
let sign: CGFloat = (i % 2 == 0) ? 1.0 : -1.0
let amplitude: CGFloat = rotationAmplitude
values.append(amplitude * sign)
}
values.append(0.0)
animation.values = values.map { ($0 as NSNumber) as AnyObject }
var keyTimes: [NSNumber] = []
for i in 0 ..< values.count {
if i == 0 {
keyTimes.append(0.0)
} else if i == values.count - 1 {
keyTimes.append(1.0)
} else {
keyTimes.append((Double(i) / Double(values.count - 1)) as NSNumber)
}
}
animation.keyTimes = keyTimes
animation.speed = speed
animation.duration = duration
animation.isAdditive = true
self.layer.add(animation, forKey: "quizInvalidRotation")
}
}

View File

@ -518,6 +518,7 @@ private final class ChatMessagePollOptionNode: ASDisplayNode {
node.option = option
let previousResult = node.currentResult
node.currentResult = optionResult
node.currentSelection = selection
node.highlightedBackgroundNode.backgroundColor = incoming ? presentationData.theme.theme.chat.message.incoming.polls.highlight : presentationData.theme.theme.chat.message.outgoing.polls.highlight
@ -745,6 +746,10 @@ class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode {
}
}
}
self.avatarsNode.pressed = { [weak self] in
self?.buttonPressed()
}
}
required init?(coder aDecoder: NSCoder) {
@ -921,11 +926,20 @@ class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode {
let (typeLayout, typeApply) = makeTypeLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: typeText, font: labelsFont, textColor: messageTheme.secondaryTextColor), backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: textConstrainedSize, alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
let votersString: String
if let totalVoters = poll?.results.totalVoters {
if totalVoters == 0 {
votersString = item.presentationData.strings.MessagePoll_NoVotes
} else {
votersString = item.presentationData.strings.MessagePoll_VotedCount(totalVoters)
if let poll = poll, let totalVoters = poll.results.totalVoters {
switch poll.kind {
case .poll:
if totalVoters == 0 {
votersString = item.presentationData.strings.MessagePoll_NoVotes
} else {
votersString = item.presentationData.strings.MessagePoll_VotedCount(totalVoters)
}
case .quiz:
if totalVoters == 0 {
votersString = item.presentationData.strings.MessagePoll_QuizNoUsers
} else {
votersString = item.presentationData.strings.MessagePoll_QuizCount(totalVoters)
}
}
} else {
votersString = " "
@ -1058,7 +1072,7 @@ class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode {
}
let optionsVotersSpacing: CGFloat = 11.0
let optionsButtonSpacing: CGFloat = 8.0
let optionsButtonSpacing: CGFloat = 9.0
let votersBottomSpacing: CGFloat = 11.0
resultSize.height += optionsVotersSpacing + votersLayout.size.height + votersBottomSpacing
@ -1165,8 +1179,17 @@ class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode {
}
let typeFrame = CGRect(origin: CGPoint(x: textFrame.minX, y: textFrame.maxY + titleTypeSpacing), size: typeLayout.size)
strongSelf.typeNode.frame = typeFrame
strongSelf.avatarsNode.frame = CGRect(origin: CGPoint(x: typeFrame.maxX + 6.0, y: typeFrame.minY + floor((typeFrame.height - mergedImageSize) / 2.0)), size: CGSize(width: mergedImageSpacing * 3.0, height: mergedImageSize))
let avatarsFrame = CGRect(origin: CGPoint(x: typeFrame.maxX + 6.0, y: typeFrame.minY + floor((typeFrame.height - mergedImageSize) / 2.0)), size: CGSize(width: mergedImageSize + mergedImageSpacing * 2.0, height: mergedImageSize))
strongSelf.avatarsNode.frame = avatarsFrame
strongSelf.avatarsNode.updateLayout(size: avatarsFrame.size)
strongSelf.avatarsNode.update(context: item.context, peers: avatarPeers, synchronousLoad: synchronousLoad)
let alphaTransition: ContainedViewLayoutTransition
if animation.isAnimated {
alphaTransition = .animated(duration: 0.25, curve: .easeInOut)
alphaTransition.updateAlpha(node: strongSelf.avatarsNode, alpha: avatarPeers.isEmpty ? 0.0 : 1.0)
} else {
alphaTransition = .immediate
}
let _ = votersApply()
strongSelf.votersNode.frame = CGRect(origin: CGPoint(x: floor((resultSize.width - votersLayout.size.width) / 2.0), y: verticalOffset + optionsVotersSpacing), size: votersLayout.size)
@ -1211,6 +1234,13 @@ class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode {
}
}
var hasResults = false
if let totalVoters = poll.results.totalVoters, totalVoters != 0 {
if let _ = poll.results.voters {
hasResults = true
}
}
if hasSelection && poll.pollId.namespace == Namespaces.Media.CloudPoll {
self.votersNode.isHidden = true
self.buttonViewResultsTextNode.isHidden = true
@ -1218,7 +1248,7 @@ class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode {
self.buttonSubmitActiveTextNode.isHidden = !hasSelectedOptions
self.buttonNode.isHidden = !hasSelectedOptions
} else {
if case .public = poll.publicity {
if case .public = poll.publicity, hasResults {
self.votersNode.isHidden = true
self.buttonViewResultsTextNode.isHidden = false
self.buttonNode.isHidden = false
@ -1230,6 +1260,8 @@ class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode {
self.buttonSubmitInactiveTextNode.isHidden = true
self.buttonSubmitActiveTextNode.isHidden = true
}
self.avatarsNode.isUserInteractionEnabled = !self.buttonViewResultsTextNode.isHidden
}
override func animateInsertion(_ currentTimestamp: Double, duration: Double) {
@ -1272,12 +1304,21 @@ class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode {
if optionNode.frame.contains(point) {
if optionNode.isUserInteractionEnabled {
return .ignore
} else if let result = optionNode.currentResult, let item = self.item {
} else if let result = optionNode.currentResult, let item = self.item, let poll = self.poll {
let string: String
if result.count == 0 {
string = item.presentationData.strings.MessagePoll_NoVotes
} else {
string = item.presentationData.strings.MessagePoll_VotedCount(result.count)
switch poll.kind {
case .poll:
if result.count == 0 {
string = item.presentationData.strings.MessagePoll_NoVotes
} else {
string = item.presentationData.strings.MessagePoll_VotedCount(result.count)
}
case .quiz:
if result.count == 0 {
string = item.presentationData.strings.MessagePoll_QuizNoUsers
} else {
string = item.presentationData.strings.MessagePoll_QuizCount(result.count)
}
}
return .tooltip(string, optionNode, optionNode.bounds.offsetBy(dx: 0.0, dy: 10.0))
}
@ -1286,6 +1327,9 @@ class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode {
if self.buttonNode.isUserInteractionEnabled, !self.buttonNode.isHidden, self.buttonNode.frame.contains(point) {
return .ignore
}
if self.avatarsNode.isUserInteractionEnabled, !self.avatarsNode.isHidden, self.avatarsNode.frame.contains(point) {
return .ignore
}
return .none
}
}
@ -1341,12 +1385,19 @@ private final class MergedAvatarsNode: ASDisplayNode {
private var peers: [PeerAvatarReference] = []
private var images: [PeerId: UIImage] = [:]
private var disposables: [PeerId: Disposable] = [:]
private let buttonNode: HighlightTrackingButtonNode
var pressed: (() -> Void)?
override init() {
self.buttonNode = HighlightTrackingButtonNode()
super.init()
self.isOpaque = false
self.displaysAsynchronously = true
self.buttonNode.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: .touchUpInside)
self.addSubnode(self.buttonNode)
}
deinit {
@ -1355,6 +1406,14 @@ private final class MergedAvatarsNode: ASDisplayNode {
}
}
@objc private func buttonPressed() {
self.pressed?()
}
func updateLayout(size: CGSize) {
self.buttonNode.frame = CGRect(origin: CGPoint(), size: size)
}
func update(context: AccountContext, peers: [Peer], synchronousLoad: Bool) {
var filteredPeers = peers.map(PeerAvatarReference.init)
if filteredPeers.count > 3 {
@ -1443,20 +1502,23 @@ private final class MergedAvatarsNode: ASDisplayNode {
context.setFillColor(UIColor.clear.cgColor)
context.fillEllipse(in: imageRect.insetBy(dx: -1.0, dy: -1.0))
context.saveGState()
switch parameters.peers[i] {
case let .letters(peerId, letters):
context.saveGState()
context.translateBy(x: currentX, y: 0.0)
drawPeerAvatarLetters(context: context, size: CGSize(width: mergedImageSize, height: mergedImageSize), font: avatarFont, letters: letters, peerId: peerId)
context.restoreGState()
case let .image(reference):
if let image = parameters.images[parameters.peers[i].peerId] {
context.translateBy(x: imageRect.midX, y: imageRect.midY)
context.scaleBy(x: 1.0, y: -1.0)
context.translateBy(x: -imageRect.midX, y: -imageRect.midY)
context.draw(image.cgImage!, in: imageRect)
} else {
context.setFillColor(UIColor.gray.cgColor)
context.fillEllipse(in: imageRect)
}
}
context.restoreGState()
currentX -= mergedImageSpacing
}
}

View File

@ -135,11 +135,16 @@ func legacyInstantVideoController(theme: PresentationTheme, panelFrame: CGRect,
done?(time)
}
}
controller.finishedWithVideo = { videoUrl, previewImage, _, duration, dimensions, liveUploadData, adjustments, isSilent, scheduleTimestamp in
controller.finishedWithVideo = { [weak controller] videoUrl, previewImage, _, duration, dimensions, liveUploadData, adjustments, isSilent, scheduleTimestamp in
guard let videoUrl = videoUrl else {
return
}
let strongController = controller
Queue.mainQueue().after(4.0, {
strongController?.resignFirstResponder()
})
var finalDimensions: CGSize = dimensions
var finalDuration: Double = duration

View File

@ -62,6 +62,7 @@ final class LegacyLiveUploadInterface: VideoConversionWatcher, TGLiveUploadInter
override func fileUpdated(_ completed: Bool) -> Any! {
let _ = super.fileUpdated(completed)
print("**fileUpdated \(completed)")
if completed {
let result = self.dataValue.modify { dataValue in
if let dataValue = dataValue {

View File

@ -10,7 +10,7 @@ import Display
import ItemListPeerItem
import ItemListPeerActionItem
private let collapsedResultCount: Int = 10
private let collapsedResultCount: Int = 1
private final class PollResultsControllerArguments {
let context: AccountContext
@ -48,8 +48,8 @@ private enum PollResultsEntryId: Hashable {
private enum PollResultsEntry: ItemListNodeEntry {
case text(String)
case optionPeer(optionId: Int, index: Int, peer: RenderedPeer, optionText: String, optionPercentage: Int, optionExpanded: Bool, opaqueIdentifier: Data)
case optionExpand(optionId: Int, opaqueIdentifier: Data, text: String)
case optionPeer(optionId: Int, index: Int, peer: RenderedPeer, optionText: String, optionPercentage: Int, optionExpanded: Bool, opaqueIdentifier: Data, shimmeringAlternation: Int?)
case optionExpand(optionId: Int, opaqueIdentifier: Data, text: String, enabled: Bool)
var section: ItemListSectionId {
switch self {
@ -124,19 +124,19 @@ private enum PollResultsEntry: ItemListNodeEntry {
switch self {
case let .text(text):
return ItemListTextItem(presentationData: presentationData, text: .large(text), sectionId: self.section)
case let .optionPeer(optionId, _, peer, optionText, optionPercentage, optionExpanded, opaqueIdentifier):
case let .optionPeer(optionId, _, peer, optionText, optionPercentage, optionExpanded, opaqueIdentifier, shimmeringAlternation):
let header = ItemListPeerItemHeader(theme: presentationData.theme, strings: presentationData.strings, text: optionText, actionTitle: optionExpanded ? presentationData.strings.PollResults_Collapse : "\(optionPercentage)%", id: Int64(optionId), action: optionExpanded ? {
arguments.collapseOption(opaqueIdentifier)
} : nil)
return ItemListPeerItem(presentationData: presentationData, dateTimeFormat: PresentationDateTimeFormat(timeFormat: .regular, dateFormat: .dayFirst, dateSeparator: ".", decimalSeparator: ".", groupingSeparator: ""), nameDisplayOrder: .firstLast, context: arguments.context, peer: peer.peers[peer.peerId]!, presence: nil, text: .none, label: .none, editing: ItemListPeerItemEditing(editable: false, editing: false, revealed: false), switchValue: nil, enabled: true, selectable: true, sectionId: self.section, action: {
return ItemListPeerItem(presentationData: presentationData, dateTimeFormat: PresentationDateTimeFormat(timeFormat: .regular, dateFormat: .dayFirst, dateSeparator: ".", decimalSeparator: ".", groupingSeparator: ""), nameDisplayOrder: .firstLast, context: arguments.context, peer: peer.peers[peer.peerId]!, presence: nil, text: .none, label: .none, editing: ItemListPeerItemEditing(editable: false, editing: false, revealed: false), switchValue: nil, enabled: true, selectable: shimmeringAlternation == nil, sectionId: self.section, action: {
arguments.openPeer(peer)
}, setPeerIdWithRevealedOptions: { _, _ in
}, removePeer: { _ in
}, noInsets: true, header: header)
case let .optionExpand(_, opaqueIdentifier, text):
return ItemListPeerActionItem(presentationData: presentationData, icon: PresentationResourcesItemList.downArrowImage(presentationData.theme), title: text, sectionId: self.section, editing: false, action: {
}, noInsets: true, header: header, shimmering: shimmeringAlternation.flatMap { ItemListPeerItemShimmering(alternationIndex: $0) })
case let .optionExpand(_, opaqueIdentifier, text, enabled):
return ItemListPeerActionItem(presentationData: presentationData, icon: PresentationResourcesItemList.downArrowImage(presentationData.theme), title: text, sectionId: self.section, editing: false, action: enabled ? {
arguments.expandOption(opaqueIdentifier)
})
} : nil)
}
}
}
@ -158,10 +158,6 @@ private func pollResultsControllerEntries(presentationData: PresentationData, po
entries.append(.text(poll.text))
if isEmpty {
return entries
}
var optionVoterCount: [Int: Int32] = [:]
let totalVoterCount = poll.results.totalVoters ?? 0
var optionPercentage: [Int] = []
@ -182,34 +178,48 @@ private func pollResultsControllerEntries(presentationData: PresentationData, po
}
for i in 0 ..< poll.options.count {
let percentage = optionPercentage.count > i ? optionPercentage[i] : 0
let option = poll.options[i]
if let optionState = resultsState.options[option.opaqueIdentifier], !optionState.peers.isEmpty {
let percentage = optionPercentage.count > i ? optionPercentage[i] : 0
var peerIndex = 0
var hasMore = false
let optionExpanded = state.expandedOptions.contains(option.opaqueIdentifier)
var peers = optionState.peers
var count = optionState.count
/*#if DEBUG
for _ in 0 ..< 10 {
peers += peers
}
count = max(count, peers.count)
#endif*/
inner: for peer in peers {
if !optionExpanded && peerIndex >= collapsedResultCount {
hasMore = true
break inner
if isEmpty {
if let voterCount = optionVoterCount[i], voterCount != 0 {
let displayCount = min(collapsedResultCount, Int(voterCount))
for peerIndex in 0 ..< displayCount {
let fakeUser = TelegramUser(id: PeerId(namespace: -1, id: 0), accessHash: nil, firstName: "", lastName: "", username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [])
let peer = RenderedPeer(peer: fakeUser)
entries.append(.optionPeer(optionId: i, index: peerIndex, peer: peer, optionText: option.text, optionPercentage: percentage, optionExpanded: false, opaqueIdentifier: option.opaqueIdentifier, shimmeringAlternation: peerIndex % 2))
}
if displayCount < Int(voterCount) {
let remainingCount = Int(voterCount) - displayCount
entries.append(.optionExpand(optionId: i, opaqueIdentifier: option.opaqueIdentifier, text: presentationData.strings.PollResults_ShowMore(Int32(remainingCount)), enabled: false))
}
entries.append(.optionPeer(optionId: i, index: peerIndex, peer: peer, optionText: option.text, optionPercentage: percentage, optionExpanded: optionExpanded, opaqueIdentifier: option.opaqueIdentifier))
peerIndex += 1
}
if hasMore {
} else {
if let optionState = resultsState.options[option.opaqueIdentifier], !optionState.peers.isEmpty {
var peerIndex = 0
var hasMore = false
let optionExpanded = state.expandedOptions.contains(option.opaqueIdentifier)
var peers = optionState.peers
var count = optionState.count
/*#if DEBUG
for _ in 0 ..< 10 {
peers += peers
}
count = max(count, peers.count)
#endif*/
inner: for peer in peers {
if !optionExpanded && peerIndex >= collapsedResultCount {
break inner
}
entries.append(.optionPeer(optionId: i, index: peerIndex, peer: peer, optionText: option.text, optionPercentage: percentage, optionExpanded: optionExpanded, opaqueIdentifier: option.opaqueIdentifier, shimmeringAlternation: nil))
peerIndex += 1
}
let remainingCount = count - peerIndex
entries.append(.optionExpand(optionId: i, opaqueIdentifier: option.opaqueIdentifier, text: presentationData.strings.PollResults_ShowMore(Int32(remainingCount))))
if remainingCount > 0 {
entries.append(.optionExpand(optionId: i, opaqueIdentifier: option.opaqueIdentifier, text: presentationData.strings.PollResults_ShowMore(Int32(remainingCount)), enabled: true))
}
}
}
}
@ -281,15 +291,10 @@ public func pollResultsController(context: AccountContext, messageId: MessageId,
}
}
var emptyStateItem: ItemListControllerEmptyStateItem?
if isEmpty {
emptyStateItem = ItemListLoadingIndicatorEmptyStateItem(theme: presentationData.theme)
}
let previousWasEmptyValue = previousWasEmpty.swap(isEmpty)
let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(presentationData.strings.PollResults_Title), leftNavigationButton: leftNavigationButton, rightNavigationButton: nil, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: false)
let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: pollResultsControllerEntries(presentationData: presentationData, poll: poll, state: state, resultsState: resultsState), style: .blocks, focusItemTag: nil, ensureVisibleItemTag: nil, emptyStateItem: emptyStateItem, crossfadeState: previousWasEmptyValue != nil && previousWasEmptyValue == true && isEmpty == false, animateChanges: false)
let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: pollResultsControllerEntries(presentationData: presentationData, poll: poll, state: state, resultsState: resultsState), style: .blocks, focusItemTag: nil, ensureVisibleItemTag: nil, emptyStateItem: nil, crossfadeState: previousWasEmptyValue != nil && previousWasEmptyValue == true && isEmpty == false, animateChanges: false)
return (controllerState, (listState, arguments))
}

View File

@ -448,12 +448,12 @@ public final class WalletStrings: Equatable {
public var Wallet_SecureStorageReset_Title: String { return self._s[218]! }
public var Wallet_Receive_CommentHeader: String { return self._s[219]! }
public var Wallet_Info_ReceiveGrams: String { return self._s[220]! }
public func Wallet_Updated_MinutesAgo(_ value: Int32) -> String {
public func Wallet_Updated_HoursAgo(_ value: Int32) -> String {
let form = getPluralizationForm(self.lc, value)
let stringValue = walletStringsFormattedNumber(value, self.groupingSeparator)
return String(format: self._ps[0 * 6 + Int(form.rawValue)]!, stringValue)
}
public func Wallet_Updated_HoursAgo(_ value: Int32) -> String {
public func Wallet_Updated_MinutesAgo(_ value: Int32) -> String {
let form = getPluralizationForm(self.lc, value)
let stringValue = walletStringsFormattedNumber(value, self.groupingSeparator)
return String(format: self._ps[1 * 6 + Int(form.rawValue)]!, stringValue)