mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
Poll improvements
This commit is contained in:
parent
048cc4c10d
commit
ca8656a1cd
@ -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";
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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))
|
||||
|
@ -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?()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
})
|
||||
|
@ -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
|
||||
|
@ -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 {
|
||||
|
@ -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 {
|
||||
|
@ -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))
|
||||
}
|
||||
|
@ -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),
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -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
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -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))
|
||||
}
|
||||
|
Binary file not shown.
Binary file not shown.
@ -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)
|
||||
|
Loading…
x
Reference in New Issue
Block a user