mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
Updated polls
This commit is contained in:
parent
bcba9c093b
commit
286ce686e6
@ -3,7 +3,7 @@
|
||||
@implementation Serialization
|
||||
|
||||
- (NSUInteger)currentLayer {
|
||||
return 111;
|
||||
return 112;
|
||||
}
|
||||
|
||||
- (id _Nullable)parseMessage:(NSData * _Nullable)data {
|
||||
|
@ -159,8 +159,9 @@ private final class CreatePollControllerArguments {
|
||||
let updateMultipleChoice: (Bool) -> Void
|
||||
let displayMultipleChoiceDisabled: () -> Void
|
||||
let updateQuiz: (Bool) -> Void
|
||||
let updateSolutionText: (String) -> Void
|
||||
|
||||
init(updatePollText: @escaping (String) -> Void, updateOptionText: @escaping (Int, String, Bool) -> 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) {
|
||||
init(updatePollText: @escaping (String) -> Void, updateOptionText: @escaping (Int, String, Bool) -> 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, updateSolutionText: @escaping (String) -> Void) {
|
||||
self.updatePollText = updatePollText
|
||||
self.updateOptionText = updateOptionText
|
||||
self.moveToNextOption = moveToNextOption
|
||||
@ -173,6 +174,7 @@ private final class CreatePollControllerArguments {
|
||||
self.updateMultipleChoice = updateMultipleChoice
|
||||
self.displayMultipleChoiceDisabled = displayMultipleChoiceDisabled
|
||||
self.updateQuiz = updateQuiz
|
||||
self.updateSolutionText = updateSolutionText
|
||||
}
|
||||
}
|
||||
|
||||
@ -180,6 +182,7 @@ private enum CreatePollSection: Int32 {
|
||||
case text
|
||||
case options
|
||||
case settings
|
||||
case quizSolution
|
||||
}
|
||||
|
||||
private enum CreatePollEntryId: Hashable {
|
||||
@ -192,6 +195,9 @@ private enum CreatePollEntryId: Hashable {
|
||||
case multipleChoice
|
||||
case quiz
|
||||
case quizInfo
|
||||
case quizSolutionHeader
|
||||
case quizSolutionText
|
||||
case quizSolutionInfo
|
||||
}
|
||||
|
||||
private enum CreatePollEntryTag: Equatable, ItemListItemTag {
|
||||
@ -218,6 +224,9 @@ private enum CreatePollEntry: ItemListNodeEntry {
|
||||
case multipleChoice(String, Bool, Bool)
|
||||
case quiz(String, Bool)
|
||||
case quizInfo(String)
|
||||
case quizSolutionHeader(String)
|
||||
case quizSolutionText(placeholder: String, text: String)
|
||||
case quizSolutionInfo(String)
|
||||
|
||||
var section: ItemListSectionId {
|
||||
switch self {
|
||||
@ -227,6 +236,8 @@ private enum CreatePollEntry: ItemListNodeEntry {
|
||||
return CreatePollSection.options.rawValue
|
||||
case .anonymousVotes, .multipleChoice, .quiz, .quizInfo:
|
||||
return CreatePollSection.settings.rawValue
|
||||
case .quizSolutionHeader, .quizSolutionText, .quizSolutionInfo:
|
||||
return CreatePollSection.quizSolution.rawValue
|
||||
}
|
||||
}
|
||||
|
||||
@ -262,6 +273,12 @@ private enum CreatePollEntry: ItemListNodeEntry {
|
||||
return .quiz
|
||||
case .quizInfo:
|
||||
return .quizInfo
|
||||
case .quizSolutionHeader:
|
||||
return .quizSolutionHeader
|
||||
case .quizSolutionText:
|
||||
return .quizSolutionText
|
||||
case .quizSolutionInfo:
|
||||
return .quizSolutionInfo
|
||||
}
|
||||
}
|
||||
|
||||
@ -285,6 +302,12 @@ private enum CreatePollEntry: ItemListNodeEntry {
|
||||
return 1004
|
||||
case .quizInfo:
|
||||
return 1005
|
||||
case .quizSolutionHeader:
|
||||
return 1006
|
||||
case .quizSolutionText:
|
||||
return 1007
|
||||
case .quizSolutionInfo:
|
||||
return 1008
|
||||
}
|
||||
}
|
||||
|
||||
@ -352,6 +375,14 @@ private enum CreatePollEntry: ItemListNodeEntry {
|
||||
})
|
||||
case let .quizInfo(text):
|
||||
return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section)
|
||||
case let .quizSolutionHeader(text):
|
||||
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
|
||||
case let .quizSolutionText(placeholder, text):
|
||||
return ItemListMultilineInputItem(presentationData: presentationData, text: text, placeholder: placeholder, maxLength: ItemListMultilineInputItemTextLimit(value: 400, display: true), sectionId: self.section, style: .blocks, textUpdated: { text in
|
||||
arguments.updateSolutionText(text)
|
||||
})
|
||||
case let .quizSolutionInfo(text):
|
||||
return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -371,6 +402,7 @@ private struct CreatePollControllerState: Equatable {
|
||||
var isAnonymous: Bool = true
|
||||
var isMultipleChoice: Bool = false
|
||||
var isQuiz: Bool = false
|
||||
var solutionText: String = ""
|
||||
}
|
||||
|
||||
private func createPollControllerEntries(presentationData: PresentationData, peer: Peer, state: CreatePollControllerState, limitsConfiguration: LimitsConfiguration, defaultIsQuiz: Bool?) -> [CreatePollEntry] {
|
||||
@ -410,14 +442,24 @@ private func createPollControllerEntries(presentationData: PresentationData, pee
|
||||
if canBePublic {
|
||||
entries.append(.anonymousVotes(presentationData.strings.CreatePoll_Anonymous, state.isAnonymous))
|
||||
}
|
||||
var isQuiz = false
|
||||
if let defaultIsQuiz = defaultIsQuiz {
|
||||
if !defaultIsQuiz {
|
||||
entries.append(.multipleChoice(presentationData.strings.CreatePoll_MultipleChoice, state.isMultipleChoice && !state.isQuiz, !state.isQuiz))
|
||||
} else {
|
||||
isQuiz = true
|
||||
}
|
||||
} else {
|
||||
entries.append(.multipleChoice(presentationData.strings.CreatePoll_MultipleChoice, state.isMultipleChoice && !state.isQuiz, !state.isQuiz))
|
||||
entries.append(.quiz(presentationData.strings.CreatePoll_Quiz, state.isQuiz))
|
||||
entries.append(.quizInfo(presentationData.strings.CreatePoll_QuizInfo))
|
||||
isQuiz = state.isQuiz
|
||||
}
|
||||
|
||||
if isQuiz {
|
||||
entries.append(.quizSolutionHeader("EXPLANATION"))
|
||||
entries.append(.quizSolutionText(placeholder: "Add a Comment (Optional)", text: state.solutionText))
|
||||
entries.append(.quizSolutionInfo("Users will see this comment after choosing a wrong answer, good for educational purposes."))
|
||||
}
|
||||
|
||||
return entries
|
||||
@ -663,6 +705,12 @@ public func createPollController(context: AccountContext, peer: Peer, isQuiz: Bo
|
||||
if value {
|
||||
displayQuizTooltipImpl?(value)
|
||||
}
|
||||
}, updateSolutionText: { text in
|
||||
updateState { state in
|
||||
var state = state
|
||||
state.solutionText = text
|
||||
return state
|
||||
}
|
||||
})
|
||||
|
||||
let previousOptionIds = Atomic<[Int]?>(value: nil)
|
||||
@ -726,14 +774,23 @@ public func createPollController(context: AccountContext, peer: Peer, isQuiz: Bo
|
||||
} else {
|
||||
publicity = .public
|
||||
}
|
||||
var resolvedSolution: String?
|
||||
let kind: TelegramMediaPollKind
|
||||
if state.isQuiz {
|
||||
kind = .quiz
|
||||
resolvedSolution = state.solutionText.isEmpty ? nil : state.solutionText
|
||||
} else {
|
||||
kind = .poll(multipleAnswers: state.isMultipleChoice)
|
||||
}
|
||||
|
||||
var deadlineTimeout: Int32?
|
||||
#if DEBUG
|
||||
deadlineTimeout = 65
|
||||
#endif
|
||||
|
||||
dismissImpl?()
|
||||
completion(.message(text: "", attributes: [], mediaReference: .standalone(media: TelegramMediaPoll(pollId: MediaId(namespace: Namespaces.Media.LocalPoll, id: arc4random64()), publicity: publicity, kind: kind, text: processPollText(state.text), options: options, correctAnswers: correctAnswers, results: TelegramMediaPollResults(voters: nil, totalVoters: nil, recentVoters: []), isClosed: false)), replyToMessageId: nil, localGroupingKey: nil))
|
||||
|
||||
completion(.message(text: "", attributes: [], mediaReference: .standalone(media: TelegramMediaPoll(pollId: MediaId(namespace: Namespaces.Media.LocalPoll, id: arc4random64()), publicity: publicity, kind: kind, text: processPollText(state.text), options: options, correctAnswers: correctAnswers, results: TelegramMediaPollResults(voters: nil, totalVoters: nil, recentVoters: [], solution: resolvedSolution), isClosed: false, deadlineTimeout: deadlineTimeout)), replyToMessageId: nil, localGroupingKey: nil))
|
||||
})
|
||||
|
||||
let leftNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Cancel), style: .regular, enabled: true, action: {
|
||||
|
@ -53,17 +53,20 @@ public struct TelegramMediaPollResults: Equatable, PostboxCoding {
|
||||
public let voters: [TelegramMediaPollOptionVoters]?
|
||||
public let totalVoters: Int32?
|
||||
public let recentVoters: [PeerId]
|
||||
public let solution: String?
|
||||
|
||||
public init(voters: [TelegramMediaPollOptionVoters]?, totalVoters: Int32?, recentVoters: [PeerId]) {
|
||||
public init(voters: [TelegramMediaPollOptionVoters]?, totalVoters: Int32?, recentVoters: [PeerId], solution: String?) {
|
||||
self.voters = voters
|
||||
self.totalVoters = totalVoters
|
||||
self.recentVoters = recentVoters
|
||||
self.solution = solution
|
||||
}
|
||||
|
||||
public init(decoder: PostboxDecoder) {
|
||||
self.voters = decoder.decodeOptionalObjectArrayWithDecoderForKey("v")
|
||||
self.totalVoters = decoder.decodeOptionalInt32ForKey("t")
|
||||
self.recentVoters = decoder.decodeInt64ArrayForKey("rv").map(PeerId.init)
|
||||
self.solution = decoder.decodeOptionalStringForKey("sol")
|
||||
}
|
||||
|
||||
public func encode(_ encoder: PostboxEncoder) {
|
||||
@ -78,6 +81,11 @@ public struct TelegramMediaPollResults: Equatable, PostboxCoding {
|
||||
encoder.encodeNil(forKey: "t")
|
||||
}
|
||||
encoder.encodeInt64Array(self.recentVoters.map { $0.toInt64() }, forKey: "rv")
|
||||
if let solution = self.solution {
|
||||
encoder.encodeString(solution, forKey: "sol")
|
||||
} else {
|
||||
encoder.encodeNil(forKey: "sol")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -130,8 +138,9 @@ public final class TelegramMediaPoll: Media, Equatable {
|
||||
public let correctAnswers: [Data]?
|
||||
public let results: TelegramMediaPollResults
|
||||
public let isClosed: Bool
|
||||
public let deadlineTimeout: Int32?
|
||||
|
||||
public init(pollId: MediaId, publicity: TelegramMediaPollPublicity, kind: TelegramMediaPollKind, text: String, options: [TelegramMediaPollOption], correctAnswers: [Data]?, results: TelegramMediaPollResults, isClosed: Bool) {
|
||||
public init(pollId: MediaId, publicity: TelegramMediaPollPublicity, kind: TelegramMediaPollKind, text: String, options: [TelegramMediaPollOption], correctAnswers: [Data]?, results: TelegramMediaPollResults, isClosed: Bool, deadlineTimeout: Int32?) {
|
||||
self.pollId = pollId
|
||||
self.publicity = publicity
|
||||
self.kind = kind
|
||||
@ -140,6 +149,7 @@ public final class TelegramMediaPoll: Media, Equatable {
|
||||
self.correctAnswers = correctAnswers
|
||||
self.results = results
|
||||
self.isClosed = isClosed
|
||||
self.deadlineTimeout = deadlineTimeout
|
||||
}
|
||||
|
||||
public init(decoder: PostboxDecoder) {
|
||||
@ -153,8 +163,9 @@ public final class TelegramMediaPoll: Media, Equatable {
|
||||
self.text = decoder.decodeStringForKey("t", orElse: "")
|
||||
self.options = decoder.decodeObjectArrayWithDecoderForKey("os")
|
||||
self.correctAnswers = decoder.decodeOptionalDataArrayForKey("ca")
|
||||
self.results = decoder.decodeObjectForKey("rs", decoder: { TelegramMediaPollResults(decoder: $0) }) as? TelegramMediaPollResults ?? TelegramMediaPollResults(voters: nil, totalVoters: nil, recentVoters: [])
|
||||
self.results = decoder.decodeObjectForKey("rs", decoder: { TelegramMediaPollResults(decoder: $0) }) as? TelegramMediaPollResults ?? TelegramMediaPollResults(voters: nil, totalVoters: nil, recentVoters: [], solution: nil)
|
||||
self.isClosed = decoder.decodeInt32ForKey("ic", orElse: 0) != 0
|
||||
self.deadlineTimeout = decoder.decodeOptionalInt32ForKey("dt")
|
||||
}
|
||||
|
||||
public func encode(_ encoder: PostboxEncoder) {
|
||||
@ -172,6 +183,11 @@ public final class TelegramMediaPoll: Media, Equatable {
|
||||
}
|
||||
encoder.encodeObject(results, forKey: "rs")
|
||||
encoder.encodeInt32(self.isClosed ? 1 : 0, forKey: "ic")
|
||||
if let deadlineTimeout = self.deadlineTimeout {
|
||||
encoder.encodeInt32(deadlineTimeout, forKey: "dt")
|
||||
} else {
|
||||
encoder.encodeNil(forKey: "dt")
|
||||
}
|
||||
}
|
||||
|
||||
public func isEqual(to other: Media) -> Bool {
|
||||
@ -210,6 +226,9 @@ public final class TelegramMediaPoll: Media, Equatable {
|
||||
if lhs.isClosed != rhs.isClosed {
|
||||
return false
|
||||
}
|
||||
if lhs.deadlineTimeout != rhs.deadlineTimeout {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
@ -229,15 +248,15 @@ public final class TelegramMediaPoll: Media, Equatable {
|
||||
}
|
||||
updatedResults = TelegramMediaPollResults(voters: updatedVoters.map({ voters in
|
||||
return TelegramMediaPollOptionVoters(selected: selectedOpaqueIdentifiers.contains(voters.opaqueIdentifier), opaqueIdentifier: voters.opaqueIdentifier, count: voters.count, isCorrect: correctOpaqueIdentifiers.contains(voters.opaqueIdentifier))
|
||||
}), totalVoters: results.totalVoters, recentVoters: results.recentVoters)
|
||||
}), totalVoters: results.totalVoters, recentVoters: results.recentVoters, solution: results.solution)
|
||||
} else if let updatedVoters = results.voters {
|
||||
updatedResults = TelegramMediaPollResults(voters: updatedVoters, totalVoters: results.totalVoters, recentVoters: results.recentVoters)
|
||||
updatedResults = TelegramMediaPollResults(voters: updatedVoters, totalVoters: results.totalVoters, recentVoters: results.recentVoters, solution: results.solution)
|
||||
} else {
|
||||
updatedResults = TelegramMediaPollResults(voters: self.results.voters, totalVoters: results.totalVoters, recentVoters: results.recentVoters)
|
||||
updatedResults = TelegramMediaPollResults(voters: self.results.voters, totalVoters: results.totalVoters, recentVoters: results.recentVoters, solution: results.solution)
|
||||
}
|
||||
} else {
|
||||
updatedResults = results
|
||||
}
|
||||
return TelegramMediaPoll(pollId: self.pollId, publicity: self.publicity, kind: self.kind, text: self.text, options: self.options, correctAnswers: self.correctAnswers, results: updatedResults, isClosed: self.isClosed)
|
||||
return TelegramMediaPoll(pollId: self.pollId, publicity: self.publicity, kind: self.kind, text: self.text, options: self.options, correctAnswers: self.correctAnswers, results: updatedResults, isClosed: self.isClosed, deadlineTimeout: self.deadlineTimeout)
|
||||
}
|
||||
}
|
||||
|
@ -11,7 +11,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = {
|
||||
dict[-784000893] = { return Api.payments.ValidatedRequestedInfo.parse_validatedRequestedInfo($0) }
|
||||
dict[461151667] = { return Api.ChatFull.parse_chatFull($0) }
|
||||
dict[-253335766] = { return Api.ChatFull.parse_channelFull($0) }
|
||||
dict[-932174686] = { return Api.PollResults.parse_pollResults($0) }
|
||||
dict[-1159937629] = { return Api.PollResults.parse_pollResults($0) }
|
||||
dict[-925415106] = { return Api.ChatParticipant.parse_chatParticipant($0) }
|
||||
dict[-636267638] = { return Api.ChatParticipant.parse_chatParticipantCreator($0) }
|
||||
dict[-489233354] = { return Api.ChatParticipant.parse_chatParticipantAdmin($0) }
|
||||
@ -297,7 +297,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = {
|
||||
dict[-175567375] = { return Api.InputTheme.parse_inputThemeSlug($0) }
|
||||
dict[1158290442] = { return Api.messages.FoundGifs.parse_foundGifs($0) }
|
||||
dict[-1132476723] = { return Api.FileLocation.parse_fileLocationToBeDeprecated($0) }
|
||||
dict[-716006138] = { return Api.Poll.parse_poll($0) }
|
||||
dict[-2032041631] = { return Api.Poll.parse_poll($0) }
|
||||
dict[423314455] = { return Api.InputNotifyPeer.parse_inputNotifyUsers($0) }
|
||||
dict[1251338318] = { return Api.InputNotifyPeer.parse_inputNotifyChats($0) }
|
||||
dict[-1311015810] = { return Api.InputNotifyPeer.parse_inputNotifyBroadcasts($0) }
|
||||
@ -360,8 +360,8 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = {
|
||||
dict[-78455655] = { return Api.InputMedia.parse_inputMediaDocumentExternal($0) }
|
||||
dict[-122978821] = { return Api.InputMedia.parse_inputMediaContact($0) }
|
||||
dict[-833715459] = { return Api.InputMedia.parse_inputMediaGeoLive($0) }
|
||||
dict[-1410741723] = { return Api.InputMedia.parse_inputMediaPoll($0) }
|
||||
dict[-1358977017] = { return Api.InputMedia.parse_inputMediaDice($0) }
|
||||
dict[261416433] = { return Api.InputMedia.parse_inputMediaPoll($0) }
|
||||
dict[2134579434] = { return Api.InputPeer.parse_inputPeerEmpty($0) }
|
||||
dict[2107670217] = { return Api.InputPeer.parse_inputPeerSelf($0) }
|
||||
dict[396093539] = { return Api.InputPeer.parse_inputPeerChat($0) }
|
||||
|
@ -2028,13 +2028,13 @@ public extension Api {
|
||||
|
||||
}
|
||||
public enum PollResults: TypeConstructorDescription {
|
||||
case pollResults(flags: Int32, results: [Api.PollAnswerVoters]?, totalVoters: Int32?, recentVoters: [Int32]?)
|
||||
case pollResults(flags: Int32, results: [Api.PollAnswerVoters]?, totalVoters: Int32?, recentVoters: [Int32]?, solution: String?, solutionEntities: [Api.MessageEntity]?)
|
||||
|
||||
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
|
||||
switch self {
|
||||
case .pollResults(let flags, let results, let totalVoters, let recentVoters):
|
||||
case .pollResults(let flags, let results, let totalVoters, let recentVoters, let solution, let solutionEntities):
|
||||
if boxed {
|
||||
buffer.appendInt32(-932174686)
|
||||
buffer.appendInt32(-1159937629)
|
||||
}
|
||||
serializeInt32(flags, buffer: buffer, boxed: false)
|
||||
if Int(flags) & Int(1 << 1) != 0 {buffer.appendInt32(481674261)
|
||||
@ -2048,14 +2048,20 @@ public extension Api {
|
||||
for item in recentVoters! {
|
||||
serializeInt32(item, buffer: buffer, boxed: false)
|
||||
}}
|
||||
if Int(flags) & Int(1 << 4) != 0 {serializeString(solution!, buffer: buffer, boxed: false)}
|
||||
if Int(flags) & Int(1 << 4) != 0 {buffer.appendInt32(481674261)
|
||||
buffer.appendInt32(Int32(solutionEntities!.count))
|
||||
for item in solutionEntities! {
|
||||
item.serialize(buffer, true)
|
||||
}}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
public func descriptionFields() -> (String, [(String, Any)]) {
|
||||
switch self {
|
||||
case .pollResults(let flags, let results, let totalVoters, let recentVoters):
|
||||
return ("pollResults", [("flags", flags), ("results", results), ("totalVoters", totalVoters), ("recentVoters", recentVoters)])
|
||||
case .pollResults(let flags, let results, let totalVoters, let recentVoters, let solution, let solutionEntities):
|
||||
return ("pollResults", [("flags", flags), ("results", results), ("totalVoters", totalVoters), ("recentVoters", recentVoters), ("solution", solution), ("solutionEntities", solutionEntities)])
|
||||
}
|
||||
}
|
||||
|
||||
@ -2072,12 +2078,20 @@ public extension Api {
|
||||
if Int(_1!) & Int(1 << 3) != 0 {if let _ = reader.readInt32() {
|
||||
_4 = Api.parseVector(reader, elementSignature: -1471112230, elementType: Int32.self)
|
||||
} }
|
||||
var _5: String?
|
||||
if Int(_1!) & Int(1 << 4) != 0 {_5 = parseString(reader) }
|
||||
var _6: [Api.MessageEntity]?
|
||||
if Int(_1!) & Int(1 << 4) != 0 {if let _ = reader.readInt32() {
|
||||
_6 = Api.parseVector(reader, elementSignature: 0, elementType: Api.MessageEntity.self)
|
||||
} }
|
||||
let _c1 = _1 != nil
|
||||
let _c2 = (Int(_1!) & Int(1 << 1) == 0) || _2 != nil
|
||||
let _c3 = (Int(_1!) & Int(1 << 2) == 0) || _3 != nil
|
||||
let _c4 = (Int(_1!) & Int(1 << 3) == 0) || _4 != nil
|
||||
if _c1 && _c2 && _c3 && _c4 {
|
||||
return Api.PollResults.pollResults(flags: _1!, results: _2, totalVoters: _3, recentVoters: _4)
|
||||
let _c5 = (Int(_1!) & Int(1 << 4) == 0) || _5 != nil
|
||||
let _c6 = (Int(_1!) & Int(1 << 4) == 0) || _6 != nil
|
||||
if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 {
|
||||
return Api.PollResults.pollResults(flags: _1!, results: _2, totalVoters: _3, recentVoters: _4, solution: _5, solutionEntities: _6)
|
||||
}
|
||||
else {
|
||||
return nil
|
||||
@ -9162,13 +9176,13 @@ public extension Api {
|
||||
|
||||
}
|
||||
public enum Poll: TypeConstructorDescription {
|
||||
case poll(id: Int64, flags: Int32, question: String, answers: [Api.PollAnswer])
|
||||
case poll(id: Int64, flags: Int32, question: String, answers: [Api.PollAnswer], closePeriod: Int32?, closeDate: Int32?)
|
||||
|
||||
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
|
||||
switch self {
|
||||
case .poll(let id, let flags, let question, let answers):
|
||||
case .poll(let id, let flags, let question, let answers, let closePeriod, let closeDate):
|
||||
if boxed {
|
||||
buffer.appendInt32(-716006138)
|
||||
buffer.appendInt32(-2032041631)
|
||||
}
|
||||
serializeInt64(id, buffer: buffer, boxed: false)
|
||||
serializeInt32(flags, buffer: buffer, boxed: false)
|
||||
@ -9178,14 +9192,16 @@ public extension Api {
|
||||
for item in answers {
|
||||
item.serialize(buffer, true)
|
||||
}
|
||||
if Int(flags) & Int(1 << 4) != 0 {serializeInt32(closePeriod!, buffer: buffer, boxed: false)}
|
||||
if Int(flags) & Int(1 << 5) != 0 {serializeInt32(closeDate!, buffer: buffer, boxed: false)}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
public func descriptionFields() -> (String, [(String, Any)]) {
|
||||
switch self {
|
||||
case .poll(let id, let flags, let question, let answers):
|
||||
return ("poll", [("id", id), ("flags", flags), ("question", question), ("answers", answers)])
|
||||
case .poll(let id, let flags, let question, let answers, let closePeriod, let closeDate):
|
||||
return ("poll", [("id", id), ("flags", flags), ("question", question), ("answers", answers), ("closePeriod", closePeriod), ("closeDate", closeDate)])
|
||||
}
|
||||
}
|
||||
|
||||
@ -9200,12 +9216,18 @@ public extension Api {
|
||||
if let _ = reader.readInt32() {
|
||||
_4 = Api.parseVector(reader, elementSignature: 0, elementType: Api.PollAnswer.self)
|
||||
}
|
||||
var _5: Int32?
|
||||
if Int(_2!) & Int(1 << 4) != 0 {_5 = reader.readInt32() }
|
||||
var _6: Int32?
|
||||
if Int(_2!) & Int(1 << 5) != 0 {_6 = reader.readInt32() }
|
||||
let _c1 = _1 != nil
|
||||
let _c2 = _2 != nil
|
||||
let _c3 = _3 != nil
|
||||
let _c4 = _4 != nil
|
||||
if _c1 && _c2 && _c3 && _c4 {
|
||||
return Api.Poll.poll(id: _1!, flags: _2!, question: _3!, answers: _4!)
|
||||
let _c5 = (Int(_2!) & Int(1 << 4) == 0) || _5 != nil
|
||||
let _c6 = (Int(_2!) & Int(1 << 5) == 0) || _6 != nil
|
||||
if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 {
|
||||
return Api.Poll.poll(id: _1!, flags: _2!, question: _3!, answers: _4!, closePeriod: _5, closeDate: _6)
|
||||
}
|
||||
else {
|
||||
return nil
|
||||
@ -10488,8 +10510,8 @@ public extension Api {
|
||||
case inputMediaDocumentExternal(flags: Int32, url: String, ttlSeconds: Int32?)
|
||||
case inputMediaContact(phoneNumber: String, firstName: String, lastName: String, vcard: String)
|
||||
case inputMediaGeoLive(flags: Int32, geoPoint: Api.InputGeoPoint, period: Int32?)
|
||||
case inputMediaPoll(flags: Int32, poll: Api.Poll, correctAnswers: [Buffer]?)
|
||||
case inputMediaDice
|
||||
case inputMediaPoll(flags: Int32, poll: Api.Poll, correctAnswers: [Buffer]?, solution: String?, solutionEntities: [Api.MessageEntity]?)
|
||||
|
||||
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
|
||||
switch self {
|
||||
@ -10625,9 +10647,15 @@ public extension Api {
|
||||
geoPoint.serialize(buffer, true)
|
||||
if Int(flags) & Int(1 << 1) != 0 {serializeInt32(period!, buffer: buffer, boxed: false)}
|
||||
break
|
||||
case .inputMediaPoll(let flags, let poll, let correctAnswers):
|
||||
case .inputMediaDice:
|
||||
if boxed {
|
||||
buffer.appendInt32(-1410741723)
|
||||
buffer.appendInt32(-1358977017)
|
||||
}
|
||||
|
||||
break
|
||||
case .inputMediaPoll(let flags, let poll, let correctAnswers, let solution, let solutionEntities):
|
||||
if boxed {
|
||||
buffer.appendInt32(261416433)
|
||||
}
|
||||
serializeInt32(flags, buffer: buffer, boxed: false)
|
||||
poll.serialize(buffer, true)
|
||||
@ -10636,12 +10664,12 @@ public extension Api {
|
||||
for item in correctAnswers! {
|
||||
serializeBytes(item, buffer: buffer, boxed: false)
|
||||
}}
|
||||
break
|
||||
case .inputMediaDice:
|
||||
if boxed {
|
||||
buffer.appendInt32(-1358977017)
|
||||
}
|
||||
|
||||
if Int(flags) & Int(1 << 1) != 0 {serializeString(solution!, buffer: buffer, boxed: false)}
|
||||
if Int(flags) & Int(1 << 1) != 0 {buffer.appendInt32(481674261)
|
||||
buffer.appendInt32(Int32(solutionEntities!.count))
|
||||
for item in solutionEntities! {
|
||||
item.serialize(buffer, true)
|
||||
}}
|
||||
break
|
||||
}
|
||||
}
|
||||
@ -10676,10 +10704,10 @@ public extension Api {
|
||||
return ("inputMediaContact", [("phoneNumber", phoneNumber), ("firstName", firstName), ("lastName", lastName), ("vcard", vcard)])
|
||||
case .inputMediaGeoLive(let flags, let geoPoint, let period):
|
||||
return ("inputMediaGeoLive", [("flags", flags), ("geoPoint", geoPoint), ("period", period)])
|
||||
case .inputMediaPoll(let flags, let poll, let correctAnswers):
|
||||
return ("inputMediaPoll", [("flags", flags), ("poll", poll), ("correctAnswers", correctAnswers)])
|
||||
case .inputMediaDice:
|
||||
return ("inputMediaDice", [])
|
||||
case .inputMediaPoll(let flags, let poll, let correctAnswers, let solution, let solutionEntities):
|
||||
return ("inputMediaPoll", [("flags", flags), ("poll", poll), ("correctAnswers", correctAnswers), ("solution", solution), ("solutionEntities", solutionEntities)])
|
||||
}
|
||||
}
|
||||
|
||||
@ -10967,6 +10995,9 @@ public extension Api {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
public static func parse_inputMediaDice(_ reader: BufferReader) -> InputMedia? {
|
||||
return Api.InputMedia.inputMediaDice
|
||||
}
|
||||
public static func parse_inputMediaPoll(_ reader: BufferReader) -> InputMedia? {
|
||||
var _1: Int32?
|
||||
_1 = reader.readInt32()
|
||||
@ -10978,19 +11009,24 @@ public extension Api {
|
||||
if Int(_1!) & Int(1 << 0) != 0 {if let _ = reader.readInt32() {
|
||||
_3 = Api.parseVector(reader, elementSignature: -1255641564, elementType: Buffer.self)
|
||||
} }
|
||||
var _4: String?
|
||||
if Int(_1!) & Int(1 << 1) != 0 {_4 = parseString(reader) }
|
||||
var _5: [Api.MessageEntity]?
|
||||
if Int(_1!) & Int(1 << 1) != 0 {if let _ = reader.readInt32() {
|
||||
_5 = Api.parseVector(reader, elementSignature: 0, elementType: Api.MessageEntity.self)
|
||||
} }
|
||||
let _c1 = _1 != nil
|
||||
let _c2 = _2 != nil
|
||||
let _c3 = (Int(_1!) & Int(1 << 0) == 0) || _3 != nil
|
||||
if _c1 && _c2 && _c3 {
|
||||
return Api.InputMedia.inputMediaPoll(flags: _1!, poll: _2!, correctAnswers: _3)
|
||||
let _c4 = (Int(_1!) & Int(1 << 1) == 0) || _4 != nil
|
||||
let _c5 = (Int(_1!) & Int(1 << 1) == 0) || _5 != nil
|
||||
if _c1 && _c2 && _c3 && _c4 && _c5 {
|
||||
return Api.InputMedia.inputMediaPoll(flags: _1!, poll: _2!, correctAnswers: _3, solution: _4, solutionEntities: _5)
|
||||
}
|
||||
else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
public static func parse_inputMediaDice(_ reader: BufferReader) -> InputMedia? {
|
||||
return Api.InputMedia.inputMediaDice
|
||||
}
|
||||
|
||||
}
|
||||
public enum InputPeer: TypeConstructorDescription {
|
||||
|
@ -3963,21 +3963,6 @@ public extension Api {
|
||||
}
|
||||
}
|
||||
public struct stats {
|
||||
public static func getBroadcastStats(flags: Int32, channel: Api.InputChannel) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.stats.BroadcastStats>) {
|
||||
let buffer = Buffer()
|
||||
buffer.appendInt32(-1421720550)
|
||||
serializeInt32(flags, buffer: buffer, boxed: false)
|
||||
channel.serialize(buffer, true)
|
||||
return (FunctionDescription(name: "stats.getBroadcastStats", parameters: [("flags", flags), ("channel", channel)]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.stats.BroadcastStats? in
|
||||
let reader = BufferReader(buffer)
|
||||
var result: Api.stats.BroadcastStats?
|
||||
if let signature = reader.readInt32() {
|
||||
result = Api.parse(reader, signature: signature) as? Api.stats.BroadcastStats
|
||||
}
|
||||
return result
|
||||
})
|
||||
}
|
||||
|
||||
public static func loadAsyncGraph(flags: Int32, token: String, x: Int64?) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.StatsGraph>) {
|
||||
let buffer = Buffer()
|
||||
buffer.appendInt32(1646092192)
|
||||
@ -3993,6 +3978,22 @@ public extension Api {
|
||||
return result
|
||||
})
|
||||
}
|
||||
|
||||
public static func getBroadcastStats(flags: Int32, channel: Api.InputChannel, tzOffset: Int32) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.stats.BroadcastStats>) {
|
||||
let buffer = Buffer()
|
||||
buffer.appendInt32(-433058374)
|
||||
serializeInt32(flags, buffer: buffer, boxed: false)
|
||||
channel.serialize(buffer, true)
|
||||
serializeInt32(tzOffset, buffer: buffer, boxed: false)
|
||||
return (FunctionDescription(name: "stats.getBroadcastStats", parameters: [("flags", flags), ("channel", channel), ("tzOffset", tzOffset)]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.stats.BroadcastStats? in
|
||||
let reader = BufferReader(buffer)
|
||||
var result: Api.stats.BroadcastStats?
|
||||
if let signature = reader.readInt32() {
|
||||
result = Api.parse(reader, signature: signature) as? Api.stats.BroadcastStats
|
||||
}
|
||||
return result
|
||||
})
|
||||
}
|
||||
}
|
||||
public struct auth {
|
||||
public static func checkPhone(phoneNumber: String) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.auth.CheckedPhone>) {
|
||||
|
@ -2363,7 +2363,7 @@ func replayFinalState(accountManager: AccountManager, postbox: Postbox, accountP
|
||||
}
|
||||
if let apiPoll = apiPoll {
|
||||
switch apiPoll {
|
||||
case let .poll(id, flags, question, answers):
|
||||
case let .poll(id, flags, question, answers, closePeriod, _):
|
||||
let publicity: TelegramMediaPollPublicity
|
||||
if (flags & (1 << 1)) != 0 {
|
||||
publicity = .public
|
||||
@ -2376,7 +2376,7 @@ func replayFinalState(accountManager: AccountManager, postbox: Postbox, accountP
|
||||
} else {
|
||||
kind = .poll(multipleAnswers: (flags & (1 << 2)) != 0)
|
||||
}
|
||||
updatedPoll = TelegramMediaPoll(pollId: MediaId(namespace: Namespaces.Media.CloudPoll, id: id), publicity: publicity, kind: kind, text: question, options: answers.map(TelegramMediaPollOption.init(apiOption:)), correctAnswers: nil, results: poll.results, isClosed: (flags & (1 << 0)) != 0)
|
||||
updatedPoll = TelegramMediaPoll(pollId: MediaId(namespace: Namespaces.Media.CloudPoll, id: id), publicity: publicity, kind: kind, text: question, options: answers.map(TelegramMediaPollOption.init(apiOption:)), correctAnswers: nil, results: poll.results, isClosed: (flags & (1 << 0)) != 0, deadlineTimeout: closePeriod)
|
||||
}
|
||||
}
|
||||
updatedPoll = updatedPoll.withUpdatedResults(TelegramMediaPollResults(apiResults: results), min: resultsMin)
|
||||
|
@ -202,10 +202,10 @@ private func requestStats(postbox: Postbox, network: Network, datacenterId: Int3
|
||||
signal = network.download(datacenterId: Int(datacenterId), isMedia: false, tag: nil)
|
||||
|> castError(MTRpcError.self)
|
||||
|> mapToSignal { worker in
|
||||
return worker.request(Api.functions.stats.getBroadcastStats(flags: flags, channel: inputChannel))
|
||||
return worker.request(Api.functions.stats.getBroadcastStats(flags: flags, channel: inputChannel, tzOffset: 0))
|
||||
}
|
||||
} else {
|
||||
signal = network.request(Api.functions.stats.getBroadcastStats(flags: flags, channel: inputChannel))
|
||||
signal = network.request(Api.functions.stats.getBroadcastStats(flags: flags, channel: inputChannel, tzOffset: 0))
|
||||
}
|
||||
|
||||
return signal
|
||||
|
@ -175,9 +175,15 @@ func mediaContentToUpload(network: Network, postbox: Postbox, auxiliaryMethods:
|
||||
pollMediaFlags |= 1 << 0
|
||||
correctAnswers = correctAnswersValue.map { Buffer(data: $0) }
|
||||
}
|
||||
let inputPoll = Api.InputMedia.inputMediaPoll(flags: pollMediaFlags, poll: Api.Poll.poll(id: 0, flags: pollFlags, question: poll.text, answers: poll.options.map({ $0.apiOption })), correctAnswers: correctAnswers)
|
||||
if poll.deadlineTimeout != nil {
|
||||
pollFlags |= 1 << 4
|
||||
}
|
||||
if poll.results.solution != nil {
|
||||
pollMediaFlags |= 1 << 1
|
||||
}
|
||||
let inputPoll = Api.InputMedia.inputMediaPoll(flags: pollMediaFlags, poll: Api.Poll.poll(id: 0, flags: pollFlags, question: poll.text, answers: poll.options.map({ $0.apiOption }), closePeriod: poll.deadlineTimeout, closeDate: nil), correctAnswers: correctAnswers, solution: poll.results.solution, solutionEntities: poll.results.solution != nil ? [] : nil)
|
||||
return .single(.content(PendingMessageUploadedContentAndReuploadInfo(content: .media(inputPoll, text), reuploadInfo: nil)))
|
||||
} else if let dice = media as? TelegramMediaDice {
|
||||
} else if let _ = media as? TelegramMediaDice {
|
||||
let input = Api.InputMedia.inputMediaDice
|
||||
return .single(.content(PendingMessageUploadedContentAndReuploadInfo(content: .media(input, text), reuploadInfo: nil)))
|
||||
} else {
|
||||
|
@ -32,7 +32,7 @@ public func requestMessageSelectPollOption(account: Account, messageId: MessageI
|
||||
resultPoll = transaction.getMedia(pollId) as? TelegramMediaPoll
|
||||
if let poll = poll {
|
||||
switch poll {
|
||||
case let .poll(id, flags, question, answers):
|
||||
case let .poll(id, flags, question, answers, closePeriod, _):
|
||||
let publicity: TelegramMediaPollPublicity
|
||||
if (flags & (1 << 1)) != 0 {
|
||||
publicity = .public
|
||||
@ -45,7 +45,7 @@ public func requestMessageSelectPollOption(account: Account, messageId: MessageI
|
||||
} else {
|
||||
kind = .poll(multipleAnswers: (flags & (1 << 2)) != 0)
|
||||
}
|
||||
resultPoll = TelegramMediaPoll(pollId: pollId, publicity: publicity, kind: kind, text: question, options: answers.map(TelegramMediaPollOption.init(apiOption:)), correctAnswers: nil, results: TelegramMediaPollResults(apiResults: results), isClosed: (flags & (1 << 0)) != 0)
|
||||
resultPoll = TelegramMediaPoll(pollId: pollId, publicity: publicity, kind: kind, text: question, options: answers.map(TelegramMediaPollOption.init(apiOption:)), correctAnswers: nil, results: TelegramMediaPollResults(apiResults: results), isClosed: (flags & (1 << 0)) != 0, deadlineTimeout: closePeriod)
|
||||
default:
|
||||
break
|
||||
}
|
||||
@ -126,7 +126,14 @@ public func requestClosePoll(postbox: Postbox, network: Network, stateManager: A
|
||||
|
||||
pollFlags |= 1 << 0
|
||||
|
||||
return network.request(Api.functions.messages.editMessage(flags: flags, peer: inputPeer, id: messageId.id, message: nil, media: .inputMediaPoll(flags: pollMediaFlags, poll: .poll(id: poll.pollId.id, flags: pollFlags, question: poll.text, answers: poll.options.map({ $0.apiOption })), correctAnswers: correctAnswers), replyMarkup: nil, entities: nil, scheduleDate: nil))
|
||||
if poll.deadlineTimeout != nil {
|
||||
pollFlags |= 1 << 4
|
||||
}
|
||||
if poll.results.solution != nil {
|
||||
pollMediaFlags |= 1 << 1
|
||||
}
|
||||
|
||||
return network.request(Api.functions.messages.editMessage(flags: flags, peer: inputPeer, id: messageId.id, message: nil, media: .inputMediaPoll(flags: pollMediaFlags, poll: .poll(id: poll.pollId.id, flags: pollFlags, question: poll.text, answers: poll.options.map({ $0.apiOption }), closePeriod: poll.deadlineTimeout, closeDate: nil), correctAnswers: correctAnswers, solution: poll.results.solution, solutionEntities: poll.results.solution != nil ? [] : nil), replyMarkup: nil, entities: nil, scheduleDate: nil))
|
||||
|> map(Optional.init)
|
||||
|> `catch` { _ -> Signal<Api.Updates?, NoError> in
|
||||
return .single(nil)
|
||||
|
@ -210,7 +210,7 @@ public class BoxedMessage: NSObject {
|
||||
|
||||
public class Serialization: NSObject, MTSerialization {
|
||||
public func currentLayer() -> UInt {
|
||||
return 111
|
||||
return 112
|
||||
}
|
||||
|
||||
public func parseMessage(_ data: Data!) -> Any! {
|
||||
|
@ -327,7 +327,7 @@ func textMediaAndExpirationTimerFromApiMedia(_ media: Api.MessageMedia?, _ peerI
|
||||
return (TelegramMediaInvoice(title: title, description: description, photo: photo.flatMap(TelegramMediaWebFile.init), receiptMessageId: receiptMsgId.flatMap { MessageId(peerId: peerId, namespace: Namespaces.Message.Cloud, id: $0) }, currency: currency, totalAmount: totalAmount, startParam: startParam, flags: parsedFlags), nil)
|
||||
case let .messageMediaPoll(poll, results):
|
||||
switch poll {
|
||||
case let .poll(id, flags, question, answers):
|
||||
case let .poll(id, flags, question, answers, closePeriod, _):
|
||||
let publicity: TelegramMediaPollPublicity
|
||||
if (flags & (1 << 1)) != 0 {
|
||||
publicity = .public
|
||||
@ -340,7 +340,7 @@ func textMediaAndExpirationTimerFromApiMedia(_ media: Api.MessageMedia?, _ peerI
|
||||
} else {
|
||||
kind = .poll(multipleAnswers: (flags & (1 << 2)) != 0)
|
||||
}
|
||||
return (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), nil)
|
||||
return (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, deadlineTimeout: closePeriod), nil)
|
||||
}
|
||||
case let .messageMediaDice(value):
|
||||
return (TelegramMediaDice(value: value), nil)
|
||||
|
@ -29,10 +29,10 @@ extension TelegramMediaPollOptionVoters {
|
||||
extension TelegramMediaPollResults {
|
||||
init(apiResults: Api.PollResults) {
|
||||
switch apiResults {
|
||||
case let .pollResults(_, results, totalVoters, recentVoters):
|
||||
case let .pollResults(_, results, totalVoters, recentVoters, solution, _):
|
||||
self.init(voters: results.flatMap({ $0.map(TelegramMediaPollOptionVoters.init(apiVoters:)) }), totalVoters: totalVoters, recentVoters: recentVoters.flatMap { recentVoters in
|
||||
return recentVoters.map { PeerId(namespace: Namespaces.Peer.CloudUser, id: $0) }
|
||||
} ?? [])
|
||||
} ?? [], solution: solution)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -236,4 +236,6 @@ public enum PresentationResourceParameterKey: Hashable {
|
||||
|
||||
case chatPrincipalThemeEssentialGraphics(hasWallpaper: Bool, bubbleCorners: PresentationChatBubbleCorners)
|
||||
case chatPrincipalThemeAdditionalGraphics(isCustomWallpaper: Bool, bubbleCorners: PresentationChatBubbleCorners)
|
||||
|
||||
case chatBubbleLamp(incoming: Bool)
|
||||
}
|
||||
|
@ -955,4 +955,10 @@ public struct PresentationResourcesChat {
|
||||
return mediaBubbleCornerImage(incoming: incoming, radius: mainRadius, inset: inset)
|
||||
})
|
||||
}
|
||||
|
||||
public static func chatBubbleLamp(_ theme: PresentationTheme, incoming: Bool) -> UIImage? {
|
||||
return theme.image(PresentationResourceParameterKey.chatBubbleLamp(incoming: incoming), { theme in
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/Lamp"), color: incoming ? theme.chat.message.incoming.accentControlColor : theme.chat.message.outgoing.accentControlColor)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
12
submodules/TelegramUI/Images.xcassets/Chat/Message/Lamp.imageset/Contents.json
vendored
Normal file
12
submodules/TelegramUI/Images.xcassets/Chat/Message/Lamp.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "ic_lamp (1).pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
BIN
submodules/TelegramUI/Images.xcassets/Chat/Message/Lamp.imageset/ic_lamp (1).pdf
vendored
Normal file
BIN
submodules/TelegramUI/Images.xcassets/Chat/Message/Lamp.imageset/ic_lamp (1).pdf
vendored
Normal file
Binary file not shown.
@ -1637,6 +1637,10 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
strongSelf.chatDisplayNode.animateQuizCorrectOptionSelected()
|
||||
return;
|
||||
}
|
||||
if false {
|
||||
strongSelf.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .info(text: "controllerInteraction.pollActionState.pollMessageIdsInProgress[id] = opaqueIdentifiers"), elevatedLayout: true, action: { _ in return false }), in: .window(.root))
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
|
||||
controllerInteraction.pollActionState.pollMessageIdsInProgress[id] = opaqueIdentifiers
|
||||
@ -1654,7 +1658,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
guard let strongSelf = self, let resultPoll = resultPoll else {
|
||||
return
|
||||
}
|
||||
guard let message = strongSelf.chatDisplayNode.historyNode.messageInCurrentHistoryView(id) else {
|
||||
guard let _ = strongSelf.chatDisplayNode.historyNode.messageInCurrentHistoryView(id) else {
|
||||
return
|
||||
}
|
||||
|
||||
@ -1686,6 +1690,10 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
strongSelf.selectPollOptionFeedback?.error()
|
||||
|
||||
itemNode.animateQuizInvalidOptionSelected()
|
||||
|
||||
if let solution = resultPoll.results.solution {
|
||||
strongSelf.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .info(text: solution), elevatedLayout: true, action: { _ in return false }), in: .window(.root))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1930,6 +1938,11 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
return
|
||||
}
|
||||
strongSelf.presentPollCreation(isQuiz: isQuiz)
|
||||
}, displayPollSolution: { [weak self] text in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
strongSelf.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .info(text: text), elevatedLayout: true, action: { _ in return false }), in: .window(.root))
|
||||
}, requestMessageUpdate: { [weak self] id in
|
||||
if let strongSelf = self {
|
||||
strongSelf.chatDisplayNode.historyNode.requestMessageUpdate(id)
|
||||
|
@ -107,6 +107,7 @@ public final class ChatControllerInteraction {
|
||||
let dismissReplyMarkupMessage: (Message) -> Void
|
||||
let openMessagePollResults: (MessageId, Data) -> Void
|
||||
let openPollCreation: (Bool?) -> Void
|
||||
let displayPollSolution: (String) -> Void
|
||||
|
||||
let requestMessageUpdate: (MessageId) -> Void
|
||||
let cancelInteractiveKeyboardGestures: () -> Void
|
||||
@ -121,7 +122,7 @@ public final class ChatControllerInteraction {
|
||||
var searchTextHighightState: (String, [MessageIndex])?
|
||||
var seenOneTimeAnimatedMedia = Set<MessageId>()
|
||||
|
||||
init(openMessage: @escaping (Message, ChatControllerInteractionOpenMessageMode) -> Bool, openPeer: @escaping (PeerId?, ChatControllerInteractionNavigateToPeer, Message?) -> Void, openPeerMention: @escaping (String) -> Void, openMessageContextMenu: @escaping (Message, Bool, ASDisplayNode, CGRect, UIGestureRecognizer?) -> Void, openMessageContextActions: @escaping (Message, ASDisplayNode, CGRect, ContextGesture?) -> Void, navigateToMessage: @escaping (MessageId, MessageId) -> Void, tapMessage: ((Message) -> Void)?, clickThroughMessage: @escaping () -> Void, toggleMessagesSelection: @escaping ([MessageId], Bool) -> Void, sendCurrentMessage: @escaping (Bool) -> Void, sendMessage: @escaping (String) -> Void, sendSticker: @escaping (FileMediaReference, Bool, ASDisplayNode, CGRect) -> Bool, sendGif: @escaping (FileMediaReference, ASDisplayNode, CGRect) -> Bool, requestMessageActionCallback: @escaping (MessageId, MemoryBuffer?, Bool) -> Void, requestMessageActionUrlAuth: @escaping (String, MessageId, Int32) -> Void, activateSwitchInline: @escaping (PeerId?, String) -> Void, openUrl: @escaping (String, Bool, Bool?, Message?) -> Void, shareCurrentLocation: @escaping () -> Void, shareAccountContact: @escaping () -> Void, sendBotCommand: @escaping (MessageId?, String) -> Void, openInstantPage: @escaping (Message, ChatMessageItemAssociatedData?) -> Void, openWallpaper: @escaping (Message) -> Void, openTheme: @escaping (Message) -> Void, openHashtag: @escaping (String?, String) -> Void, updateInputState: @escaping ((ChatTextInputState) -> ChatTextInputState) -> Void, updateInputMode: @escaping ((ChatInputMode) -> ChatInputMode) -> Void, openMessageShareMenu: @escaping (MessageId) -> Void, presentController: @escaping (ViewController, Any?) -> Void, navigationController: @escaping () -> NavigationController?, chatControllerNode: @escaping () -> ASDisplayNode?, reactionContainerNode: @escaping () -> ReactionSelectionParentNode?, presentGlobalOverlayController: @escaping (ViewController, Any?) -> Void, callPeer: @escaping (PeerId) -> Void, longTap: @escaping (ChatControllerInteractionLongTapAction, Message?) -> Void, openCheckoutOrReceipt: @escaping (MessageId) -> Void, openSearch: @escaping () -> Void, setupReply: @escaping (MessageId) -> Void, canSetupReply: @escaping (Message) -> Bool, navigateToFirstDateMessage: @escaping(Int32) ->Void, requestRedeliveryOfFailedMessages: @escaping (MessageId) -> Void, addContact: @escaping (String) -> Void, rateCall: @escaping (Message, CallId) -> Void, requestSelectMessagePollOptions: @escaping (MessageId, [Data]) -> Void, requestOpenMessagePollResults: @escaping (MessageId, MediaId) -> Void, openAppStorePage: @escaping () -> Void, displayMessageTooltip: @escaping (MessageId, String, ASDisplayNode?, CGRect?) -> Void, seekToTimecode: @escaping (Message, Double, Bool) -> Void, scheduleCurrentMessage: @escaping () -> Void, sendScheduledMessagesNow: @escaping ([MessageId]) -> Void, editScheduledMessagesTime: @escaping ([MessageId]) -> Void, performTextSelectionAction: @escaping (UInt32, String, TextSelectionAction) -> Void, updateMessageReaction: @escaping (MessageId, String?) -> Void, openMessageReactions: @escaping (MessageId) -> Void, displaySwipeToReplyHint: @escaping () -> Void, dismissReplyMarkupMessage: @escaping (Message) -> Void, openMessagePollResults: @escaping (MessageId, Data) -> Void, openPollCreation: @escaping (Bool?) -> Void, requestMessageUpdate: @escaping (MessageId) -> Void, cancelInteractiveKeyboardGestures: @escaping () -> Void, automaticMediaDownloadSettings: MediaAutoDownloadSettings, pollActionState: ChatInterfacePollActionState, stickerSettings: ChatInterfaceStickerSettings) {
|
||||
init(openMessage: @escaping (Message, ChatControllerInteractionOpenMessageMode) -> Bool, openPeer: @escaping (PeerId?, ChatControllerInteractionNavigateToPeer, Message?) -> Void, openPeerMention: @escaping (String) -> Void, openMessageContextMenu: @escaping (Message, Bool, ASDisplayNode, CGRect, UIGestureRecognizer?) -> Void, openMessageContextActions: @escaping (Message, ASDisplayNode, CGRect, ContextGesture?) -> Void, navigateToMessage: @escaping (MessageId, MessageId) -> Void, tapMessage: ((Message) -> Void)?, clickThroughMessage: @escaping () -> Void, toggleMessagesSelection: @escaping ([MessageId], Bool) -> Void, sendCurrentMessage: @escaping (Bool) -> Void, sendMessage: @escaping (String) -> Void, sendSticker: @escaping (FileMediaReference, Bool, ASDisplayNode, CGRect) -> Bool, sendGif: @escaping (FileMediaReference, ASDisplayNode, CGRect) -> Bool, requestMessageActionCallback: @escaping (MessageId, MemoryBuffer?, Bool) -> Void, requestMessageActionUrlAuth: @escaping (String, MessageId, Int32) -> Void, activateSwitchInline: @escaping (PeerId?, String) -> Void, openUrl: @escaping (String, Bool, Bool?, Message?) -> Void, shareCurrentLocation: @escaping () -> Void, shareAccountContact: @escaping () -> Void, sendBotCommand: @escaping (MessageId?, String) -> Void, openInstantPage: @escaping (Message, ChatMessageItemAssociatedData?) -> Void, openWallpaper: @escaping (Message) -> Void, openTheme: @escaping (Message) -> Void, openHashtag: @escaping (String?, String) -> Void, updateInputState: @escaping ((ChatTextInputState) -> ChatTextInputState) -> Void, updateInputMode: @escaping ((ChatInputMode) -> ChatInputMode) -> Void, openMessageShareMenu: @escaping (MessageId) -> Void, presentController: @escaping (ViewController, Any?) -> Void, navigationController: @escaping () -> NavigationController?, chatControllerNode: @escaping () -> ASDisplayNode?, reactionContainerNode: @escaping () -> ReactionSelectionParentNode?, presentGlobalOverlayController: @escaping (ViewController, Any?) -> Void, callPeer: @escaping (PeerId) -> Void, longTap: @escaping (ChatControllerInteractionLongTapAction, Message?) -> Void, openCheckoutOrReceipt: @escaping (MessageId) -> Void, openSearch: @escaping () -> Void, setupReply: @escaping (MessageId) -> Void, canSetupReply: @escaping (Message) -> Bool, navigateToFirstDateMessage: @escaping(Int32) ->Void, requestRedeliveryOfFailedMessages: @escaping (MessageId) -> Void, addContact: @escaping (String) -> Void, rateCall: @escaping (Message, CallId) -> Void, requestSelectMessagePollOptions: @escaping (MessageId, [Data]) -> Void, requestOpenMessagePollResults: @escaping (MessageId, MediaId) -> Void, openAppStorePage: @escaping () -> Void, displayMessageTooltip: @escaping (MessageId, String, ASDisplayNode?, CGRect?) -> Void, seekToTimecode: @escaping (Message, Double, Bool) -> Void, scheduleCurrentMessage: @escaping () -> Void, sendScheduledMessagesNow: @escaping ([MessageId]) -> Void, editScheduledMessagesTime: @escaping ([MessageId]) -> Void, performTextSelectionAction: @escaping (UInt32, String, TextSelectionAction) -> Void, updateMessageReaction: @escaping (MessageId, String?) -> Void, openMessageReactions: @escaping (MessageId) -> Void, displaySwipeToReplyHint: @escaping () -> Void, dismissReplyMarkupMessage: @escaping (Message) -> Void, openMessagePollResults: @escaping (MessageId, Data) -> Void, openPollCreation: @escaping (Bool?) -> Void, displayPollSolution: @escaping (String) -> Void, requestMessageUpdate: @escaping (MessageId) -> Void, cancelInteractiveKeyboardGestures: @escaping () -> Void, automaticMediaDownloadSettings: MediaAutoDownloadSettings, pollActionState: ChatInterfacePollActionState, stickerSettings: ChatInterfaceStickerSettings) {
|
||||
self.openMessage = openMessage
|
||||
self.openPeer = openPeer
|
||||
self.openPeerMention = openPeerMention
|
||||
@ -167,6 +168,7 @@ public final class ChatControllerInteraction {
|
||||
self.requestSelectMessagePollOptions = requestSelectMessagePollOptions
|
||||
self.requestOpenMessagePollResults = requestOpenMessagePollResults
|
||||
self.openPollCreation = openPollCreation
|
||||
self.displayPollSolution = displayPollSolution
|
||||
self.openAppStorePage = openAppStorePage
|
||||
self.displayMessageTooltip = displayMessageTooltip
|
||||
self.seekToTimecode = seekToTimecode
|
||||
@ -220,6 +222,7 @@ public final class ChatControllerInteraction {
|
||||
}, dismissReplyMarkupMessage: { _ in
|
||||
}, openMessagePollResults: { _, _ in
|
||||
}, openPollCreation: { _ in
|
||||
}, displayPollSolution: { _ in
|
||||
}, requestMessageUpdate: { _ in
|
||||
}, cancelInteractiveKeyboardGestures: {
|
||||
}, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings,
|
||||
|
@ -518,7 +518,9 @@ func contextMenuForChatPresentationIntefaceState(chatPresentationInterfaceState:
|
||||
var activePoll: TelegramMediaPoll?
|
||||
for media in message.media {
|
||||
if let poll = media as? TelegramMediaPoll, !poll.isClosed, message.id.namespace == Namespaces.Message.Cloud, poll.pollId.namespace == Namespaces.Media.CloudPoll {
|
||||
activePoll = poll
|
||||
if !isPollEffectivelyClosed(message: message, poll: poll) {
|
||||
activePoll = poll
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -2544,13 +2544,13 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePrevewItemNode
|
||||
}
|
||||
case let .tooltip(text, node, rect):
|
||||
if let item = self.item {
|
||||
return .action({
|
||||
return .optionalAction({
|
||||
let _ = item.controllerInteraction.displayMessageTooltip(item.message.id, text, node, rect)
|
||||
})
|
||||
}
|
||||
case let .openPollResults(option):
|
||||
if let item = self.item {
|
||||
return .action({
|
||||
return .optionalAction({
|
||||
item.controllerInteraction.openMessagePollResults(item.message.id, option)
|
||||
})
|
||||
}
|
||||
|
@ -12,6 +12,28 @@ import AccountContext
|
||||
import AvatarNode
|
||||
import TelegramPresentationData
|
||||
|
||||
func isPollEffectivelyClosed(message: Message, poll: TelegramMediaPoll) -> Bool {
|
||||
if poll.isClosed {
|
||||
return true
|
||||
} else if let deadlineTimeout = poll.deadlineTimeout, message.id.namespace == Namespaces.Message.Cloud {
|
||||
let startDate: Int32
|
||||
if let forwardInfo = message.forwardInfo {
|
||||
startDate = forwardInfo.date
|
||||
} else {
|
||||
startDate = message.timestamp
|
||||
}
|
||||
|
||||
let timestamp = Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970)
|
||||
if timestamp >= startDate + deadlineTimeout {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
private struct PercentCounterItem: Comparable {
|
||||
var index: Int = 0
|
||||
var percent: Int = 0
|
||||
@ -187,7 +209,6 @@ private final class ChatMessagePollOptionRadioNode: ASDisplayNode {
|
||||
func update(staticColor: UIColor, animatedColor: UIColor, fillColor: UIColor, foregroundColor: UIColor, isSelectable: Bool, isAnimating: Bool) {
|
||||
var updated = false
|
||||
let shouldHaveBeenAnimating = self.shouldBeAnimating
|
||||
let wasAnimating = self.isAnimating
|
||||
if !staticColor.isEqual(self.staticColor) {
|
||||
self.staticColor = staticColor
|
||||
updated = true
|
||||
@ -779,9 +800,48 @@ private final class ChatMessagePollOptionNode: ASDisplayNode {
|
||||
|
||||
private let labelsFont = Font.regular(14.0)
|
||||
|
||||
private final class SolutionButtonNode: HighlightableButtonNode {
|
||||
private let pressed: () -> Void
|
||||
private let iconNode: ASImageNode
|
||||
|
||||
private var theme: PresentationTheme?
|
||||
private var incoming: Bool?
|
||||
|
||||
init(pressed: @escaping () -> Void) {
|
||||
self.pressed = pressed
|
||||
|
||||
self.iconNode = ASImageNode()
|
||||
self.iconNode.displaysAsynchronously = false
|
||||
|
||||
super.init()
|
||||
|
||||
self.addSubnode(self.iconNode)
|
||||
|
||||
self.addTarget(self, action: #selector(self.pressedEvent), forControlEvents: .touchUpInside)
|
||||
}
|
||||
|
||||
@objc private func pressedEvent() {
|
||||
self.pressed()
|
||||
}
|
||||
|
||||
func update(size: CGSize, theme: PresentationTheme, incoming: Bool) {
|
||||
if self.theme !== theme || self.incoming != incoming {
|
||||
self.theme = theme
|
||||
self.incoming = incoming
|
||||
self.iconNode.image = PresentationResourcesChat.chatBubbleLamp(theme, incoming: incoming)
|
||||
}
|
||||
|
||||
if let image = self.iconNode.image {
|
||||
self.iconNode.frame = CGRect(origin: CGPoint(x: floor((size.width - image.size.width) / 2.0), y: floor((size.height - image.size.height) / 2.0)), size: image.size)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
private let textNode: TextNode
|
||||
private let typeNode: TextNode
|
||||
private var timerNode: PollBubbleTimerNode?
|
||||
private var solutionButtonNode: SolutionButtonNode?
|
||||
private let avatarsNode: MergedAvatarsNode
|
||||
private let votersNode: TextNode
|
||||
private let buttonSubmitInactiveTextNode: TextNode
|
||||
@ -918,14 +978,10 @@ class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
}
|
||||
|
||||
var previousOptionNodeLayouts: [Data: (_ accountPeerId: PeerId, _ presentationData: ChatPresentationData, _ message: Message, _ poll: TelegramMediaPoll, _ option: TelegramMediaPollOption, _ optionResult: ChatMessagePollOptionResult?, _ constrainedWidth: CGFloat) -> (minimumWidth: CGFloat, layout: ((CGFloat) -> (CGSize, (Bool, Bool) -> ChatMessagePollOptionNode)))] = [:]
|
||||
var hasSelectedOptions = false
|
||||
for optionNode in self.optionNodes {
|
||||
if let option = optionNode.option {
|
||||
previousOptionNodeLayouts[option.opaqueIdentifier] = ChatMessagePollOptionNode.asyncLayout(optionNode)
|
||||
}
|
||||
if let isChecked = optionNode.radioNode?.isChecked, isChecked {
|
||||
hasSelectedOptions = true
|
||||
}
|
||||
}
|
||||
|
||||
return { item, layoutConstants, _, _, _ in
|
||||
@ -1020,7 +1076,7 @@ class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
}
|
||||
}
|
||||
|
||||
if let poll = poll, poll.isClosed {
|
||||
if let poll = poll, isPollEffectivelyClosed(message: message, poll: poll) {
|
||||
typeText = item.presentationData.strings.MessagePoll_LabelClosed
|
||||
} else if let poll = poll {
|
||||
switch poll.kind {
|
||||
@ -1090,13 +1146,20 @@ class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
boundingSize.width += layoutConstants.text.bubbleInsets.left + layoutConstants.text.bubbleInsets.right
|
||||
boundingSize.height += layoutConstants.text.bubbleInsets.top + layoutConstants.text.bubbleInsets.bottom
|
||||
|
||||
let isClosed: Bool
|
||||
if let poll = poll {
|
||||
isClosed = isPollEffectivelyClosed(message: message, poll: poll)
|
||||
} else {
|
||||
isClosed = false
|
||||
}
|
||||
|
||||
var pollOptionsFinalizeLayouts: [(CGFloat) -> (CGSize, (Bool, Bool) -> ChatMessagePollOptionNode)] = []
|
||||
if let poll = poll {
|
||||
var optionVoterCount: [Int: Int32] = [:]
|
||||
var maxOptionVoterCount: Int32 = 0
|
||||
var totalVoterCount: Int32 = 0
|
||||
let voters: [TelegramMediaPollOptionVoters]?
|
||||
if poll.isClosed {
|
||||
if isClosed {
|
||||
voters = poll.results.voters ?? []
|
||||
} else {
|
||||
voters = poll.results.voters
|
||||
@ -1109,7 +1172,7 @@ class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
}
|
||||
}
|
||||
totalVoterCount = totalVoters
|
||||
if didVote || poll.isClosed {
|
||||
if didVote || isClosed {
|
||||
for i in 0 ..< poll.options.count {
|
||||
inner: for optionVoters in voters {
|
||||
if optionVoters.opaqueIdentifier == poll.options[i].opaqueIdentifier {
|
||||
@ -1142,10 +1205,10 @@ class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
if let count = optionVoterCount[i] {
|
||||
if maxOptionVoterCount != 0 && totalVoterCount != 0 {
|
||||
optionResult = ChatMessagePollOptionResult(normalized: CGFloat(count) / CGFloat(maxOptionVoterCount), percent: optionVoterCounts[i], count: count)
|
||||
} else if poll.isClosed {
|
||||
} else if isClosed {
|
||||
optionResult = ChatMessagePollOptionResult(normalized: 0, percent: 0, count: 0)
|
||||
}
|
||||
} else if poll.isClosed {
|
||||
} else if isClosed {
|
||||
optionResult = ChatMessagePollOptionResult(normalized: 0, percent: 0, count: 0)
|
||||
}
|
||||
let result = makeLayout(item.context.account.peerId, item.presentationData, item.message, poll, option, optionResult, constrainedSize.width - layoutConstants.bubble.borderInset * 2.0)
|
||||
@ -1157,7 +1220,7 @@ class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
boundingSize.width = max(boundingSize.width, min(270.0, constrainedSize.width))
|
||||
|
||||
var canVote = false
|
||||
if (item.message.id.namespace == Namespaces.Message.Cloud || Namespaces.Message.allScheduled.contains(item.message.id.namespace)), let poll = poll, poll.pollId.namespace == Namespaces.Media.CloudPoll, !poll.isClosed {
|
||||
if (item.message.id.namespace == Namespaces.Message.Cloud || Namespaces.Message.allScheduled.contains(item.message.id.namespace)), let poll = poll, poll.pollId.namespace == Namespaces.Media.CloudPoll, !isClosed {
|
||||
var hasVoted = false
|
||||
if let voters = poll.results.voters {
|
||||
for voter in voters {
|
||||
@ -1304,6 +1367,130 @@ class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
}
|
||||
let typeFrame = CGRect(origin: CGPoint(x: textFrame.minX, y: textFrame.maxY + titleTypeSpacing), size: typeLayout.size)
|
||||
strongSelf.typeNode.frame = typeFrame
|
||||
|
||||
let deadlineTimeout = poll?.deadlineTimeout
|
||||
var displayDeadline = true
|
||||
var hasSelected = false
|
||||
|
||||
if let poll = poll {
|
||||
if let voters = poll.results.voters {
|
||||
for voter in voters {
|
||||
if voter.selected {
|
||||
displayDeadline = false
|
||||
hasSelected = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let deadlineTimeout = deadlineTimeout, !isClosed {
|
||||
var endDate: Int32?
|
||||
|
||||
if message.id.namespace == Namespaces.Message.Cloud {
|
||||
let startDate: Int32
|
||||
if let forwardInfo = message.forwardInfo {
|
||||
startDate = forwardInfo.date
|
||||
} else {
|
||||
startDate = message.timestamp
|
||||
}
|
||||
endDate = startDate + deadlineTimeout
|
||||
}
|
||||
|
||||
let timerNode: PollBubbleTimerNode
|
||||
if let current = strongSelf.timerNode {
|
||||
timerNode = current
|
||||
let timerTransition: ContainedViewLayoutTransition
|
||||
if animation.isAnimated {
|
||||
timerTransition = .animated(duration: 0.25, curve: .easeInOut)
|
||||
} else {
|
||||
timerTransition = .immediate
|
||||
}
|
||||
if displayDeadline {
|
||||
timerTransition.updateAlpha(node: timerNode, alpha: 1.0)
|
||||
} else {
|
||||
timerTransition.updateAlpha(node: timerNode, alpha: 0.0)
|
||||
}
|
||||
} else {
|
||||
timerNode = PollBubbleTimerNode()
|
||||
strongSelf.timerNode = timerNode
|
||||
strongSelf.addSubnode(timerNode)
|
||||
timerNode.reachedTimeout = {
|
||||
guard let strongSelf = self, let item = strongSelf.item else {
|
||||
return
|
||||
}
|
||||
item.controllerInteraction.requestMessageUpdate(item.message.id)
|
||||
}
|
||||
|
||||
let timerTransition: ContainedViewLayoutTransition
|
||||
if animation.isAnimated {
|
||||
timerTransition = .animated(duration: 0.25, curve: .easeInOut)
|
||||
} else {
|
||||
timerTransition = .immediate
|
||||
}
|
||||
if displayDeadline {
|
||||
timerNode.alpha = 0.0
|
||||
timerTransition.updateAlpha(node: timerNode, alpha: 1.0)
|
||||
} else {
|
||||
timerNode.alpha = 0.0
|
||||
}
|
||||
}
|
||||
timerNode.update(regularColor: messageTheme.secondaryTextColor, proximityColor: messageTheme.scamColor, timeout: deadlineTimeout, deadlineTimestamp: endDate)
|
||||
timerNode.frame = CGRect(origin: CGPoint(x: resultSize.width - layoutConstants.text.bubbleInsets.right, y: typeFrame.minY), size: CGSize())
|
||||
} else if let timerNode = strongSelf.timerNode {
|
||||
strongSelf.timerNode = nil
|
||||
|
||||
let timerTransition: ContainedViewLayoutTransition
|
||||
if animation.isAnimated {
|
||||
timerTransition = .animated(duration: 0.25, curve: .easeInOut)
|
||||
} else {
|
||||
timerTransition = .immediate
|
||||
}
|
||||
timerTransition.updateAlpha(node: timerNode, alpha: 0.0, completion: { [weak timerNode] _ in
|
||||
timerNode?.removeFromSupernode()
|
||||
})
|
||||
timerTransition.updateTransformScale(node: timerNode, scale: 0.1)
|
||||
}
|
||||
|
||||
if (strongSelf.timerNode == nil || !displayDeadline), let poll = poll, case .anonymous = poll.publicity, case .quiz = poll.kind, let solution = poll.results.solution, !solution.isEmpty, (isClosed || hasSelected) {
|
||||
let solutionButtonNode: SolutionButtonNode
|
||||
if let current = strongSelf.solutionButtonNode {
|
||||
solutionButtonNode = current
|
||||
} else {
|
||||
solutionButtonNode = SolutionButtonNode(pressed: {
|
||||
guard let strongSelf = self, let item = strongSelf.item else {
|
||||
return
|
||||
}
|
||||
item.controllerInteraction.displayPollSolution(solution)
|
||||
})
|
||||
strongSelf.solutionButtonNode = solutionButtonNode
|
||||
strongSelf.addSubnode(solutionButtonNode)
|
||||
|
||||
let timerTransition: ContainedViewLayoutTransition
|
||||
if animation.isAnimated {
|
||||
timerTransition = .animated(duration: 0.25, curve: .easeInOut)
|
||||
} else {
|
||||
timerTransition = .immediate
|
||||
}
|
||||
solutionButtonNode.alpha = 0.0
|
||||
timerTransition.updateAlpha(node: solutionButtonNode, alpha: 1.0)
|
||||
}
|
||||
let buttonSize = CGSize(width: 32.0, height: 32.0)
|
||||
solutionButtonNode.update(size: buttonSize, theme: item.presentationData.theme.theme, incoming: item.message.flags.contains(.Incoming))
|
||||
solutionButtonNode.frame = CGRect(origin: CGPoint(x: resultSize.width - layoutConstants.text.bubbleInsets.right - buttonSize.width + 5.0, y: typeFrame.minY - 16.0), size: buttonSize)
|
||||
} else if let solutionButtonNode = strongSelf.solutionButtonNode {
|
||||
let timerTransition: ContainedViewLayoutTransition
|
||||
if animation.isAnimated {
|
||||
timerTransition = .animated(duration: 0.25, curve: .easeInOut)
|
||||
} else {
|
||||
timerTransition = .immediate
|
||||
}
|
||||
timerTransition.updateAlpha(node: solutionButtonNode, alpha: 0.0, completion: { [weak solutionButtonNode] _ in
|
||||
solutionButtonNode?.removeFromSupernode()
|
||||
})
|
||||
timerTransition.updateTransformScale(node: solutionButtonNode, scale: 0.1)
|
||||
}
|
||||
|
||||
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)
|
||||
@ -1367,8 +1554,10 @@ class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
}
|
||||
}
|
||||
|
||||
let isClosed = isPollEffectivelyClosed(message: item.message, poll: poll)
|
||||
|
||||
var hasResults = false
|
||||
if poll.isClosed {
|
||||
if isClosed {
|
||||
hasResults = true
|
||||
hasSelection = false
|
||||
if let totalVoters = poll.results.totalVoters, totalVoters == 0 {
|
||||
@ -1503,6 +1692,9 @@ class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
if self.avatarsNode.isUserInteractionEnabled, !self.avatarsNode.isHidden, self.avatarsNode.frame.contains(point) {
|
||||
return .ignore
|
||||
}
|
||||
if let solutionButtonNode = self.solutionButtonNode, solutionButtonNode.isUserInteractionEnabled, !solutionButtonNode.isHidden, solutionButtonNode.frame.contains(point) {
|
||||
return .ignore
|
||||
}
|
||||
return .none
|
||||
}
|
||||
}
|
||||
@ -1590,7 +1782,7 @@ private final class MergedAvatarsNode: ASDisplayNode {
|
||||
func update(context: AccountContext, peers: [Peer], synchronousLoad: Bool) {
|
||||
var filteredPeers = peers.map(PeerAvatarReference.init)
|
||||
if filteredPeers.count > 3 {
|
||||
filteredPeers.dropLast(filteredPeers.count - 3)
|
||||
let _ = filteredPeers.dropLast(filteredPeers.count - 3)
|
||||
}
|
||||
if filteredPeers != self.peers {
|
||||
self.peers = filteredPeers
|
||||
@ -1667,7 +1859,6 @@ private final class MergedAvatarsNode: ASDisplayNode {
|
||||
return
|
||||
}
|
||||
|
||||
let imageOverlaySpacing: CGFloat = 1.0
|
||||
context.setBlendMode(.copy)
|
||||
|
||||
var currentX = mergedImageSize + mergedImageSpacing * CGFloat(parameters.peers.count - 1) - mergedImageSize
|
||||
|
@ -423,6 +423,7 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode {
|
||||
}, dismissReplyMarkupMessage: { _ in
|
||||
}, openMessagePollResults: { _, _ in
|
||||
}, openPollCreation: { _ in
|
||||
}, displayPollSolution: { _ in
|
||||
}, requestMessageUpdate: { _ in
|
||||
}, cancelInteractiveKeyboardGestures: {
|
||||
}, automaticMediaDownloadSettings: self.automaticMediaDownloadSettings,
|
||||
|
@ -122,6 +122,7 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, UIGestu
|
||||
}, dismissReplyMarkupMessage: { _ in
|
||||
}, openMessagePollResults: { _, _ in
|
||||
}, openPollCreation: { _ in
|
||||
}, displayPollSolution: { _ in
|
||||
}, requestMessageUpdate: { _ in
|
||||
}, cancelInteractiveKeyboardGestures: {
|
||||
}, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings, pollActionState: ChatInterfacePollActionState(), stickerSettings: ChatInterfaceStickerSettings(loopAnimatedStickers: false))
|
||||
|
@ -1527,6 +1527,7 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD
|
||||
}, dismissReplyMarkupMessage: { _ in
|
||||
}, openMessagePollResults: { _, _ in
|
||||
}, openPollCreation: { _ in
|
||||
}, displayPollSolution: { _ in
|
||||
}, requestMessageUpdate: { _ in
|
||||
}, cancelInteractiveKeyboardGestures: {
|
||||
}, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings,
|
||||
|
@ -427,6 +427,7 @@ public class PeerMediaCollectionController: TelegramBaseController {
|
||||
}, dismissReplyMarkupMessage: { _ in
|
||||
}, openMessagePollResults: { _, _ in
|
||||
}, openPollCreation: { _ in
|
||||
}, displayPollSolution: { _ in
|
||||
}, requestMessageUpdate: { _ in
|
||||
}, cancelInteractiveKeyboardGestures: {
|
||||
}, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings,
|
||||
|
264
submodules/TelegramUI/Sources/PollBubbleTimerNode.swift
Normal file
264
submodules/TelegramUI/Sources/PollBubbleTimerNode.swift
Normal file
@ -0,0 +1,264 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import AsyncDisplayKit
|
||||
import Display
|
||||
|
||||
private func textForTimeout(value: Int) -> String {
|
||||
//TODO: localize
|
||||
if value > 60 * 60 {
|
||||
let hours = value / (60 * 60)
|
||||
return "\(hours)h"
|
||||
} else {
|
||||
let minutes = value / 60
|
||||
let seconds = value % 60
|
||||
let minutesPadding = minutes < 10 ? "0" : ""
|
||||
let secondsPadding = seconds < 10 ? "0" : ""
|
||||
return "\(minutesPadding)\(minutes):\(secondsPadding)\(seconds)"
|
||||
}
|
||||
}
|
||||
|
||||
private enum ContentState: Equatable {
|
||||
case clock(UIColor)
|
||||
case timeout(UIColor, CGFloat)
|
||||
}
|
||||
|
||||
private struct ContentParticle {
|
||||
var position: CGPoint
|
||||
var direction: CGPoint
|
||||
var velocity: CGFloat
|
||||
var alpha: CGFloat
|
||||
var lifetime: Double
|
||||
var beginTime: Double
|
||||
|
||||
init(position: CGPoint, direction: CGPoint, velocity: CGFloat, alpha: CGFloat, lifetime: Double, beginTime: Double) {
|
||||
self.position = position
|
||||
self.direction = direction
|
||||
self.velocity = velocity
|
||||
self.alpha = alpha
|
||||
self.lifetime = lifetime
|
||||
self.beginTime = beginTime
|
||||
}
|
||||
}
|
||||
|
||||
final class PollBubbleTimerNode: ASDisplayNode {
|
||||
private struct Params: Equatable {
|
||||
var regularColor: UIColor
|
||||
var proximityColor: UIColor
|
||||
var timeout: Int32
|
||||
var deadlineTimestamp: Int32?
|
||||
}
|
||||
|
||||
private let hierarchyTrackingNode: HierarchyTrackingNode
|
||||
private var inHierarchyValue: Bool = false
|
||||
|
||||
private var animator: ConstantDisplayLinkAnimator?
|
||||
private let textNode: ImmediateTextNode
|
||||
private let contentNode: ASDisplayNode
|
||||
private var currentContentState: ContentState?
|
||||
private var particles: [ContentParticle] = []
|
||||
|
||||
private var currentParams: Params?
|
||||
|
||||
var reachedTimeout: (() -> Void)?
|
||||
|
||||
override init() {
|
||||
var updateInHierarchy: ((Bool) -> Void)?
|
||||
self.hierarchyTrackingNode = HierarchyTrackingNode({ value in
|
||||
updateInHierarchy?(value)
|
||||
})
|
||||
|
||||
self.textNode = ImmediateTextNode()
|
||||
self.textNode.displaysAsynchronously = false
|
||||
|
||||
self.contentNode = ASDisplayNode()
|
||||
|
||||
super.init()
|
||||
|
||||
self.addSubnode(self.textNode)
|
||||
self.addSubnode(self.contentNode)
|
||||
|
||||
updateInHierarchy = { [weak self] value in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
strongSelf.inHierarchyValue = value
|
||||
strongSelf.animator?.isPaused = value
|
||||
}
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.animator?.invalidate()
|
||||
}
|
||||
|
||||
func update(regularColor: UIColor, proximityColor: UIColor, timeout: Int32, deadlineTimestamp: Int32?) {
|
||||
let params = Params(
|
||||
regularColor: regularColor,
|
||||
proximityColor: proximityColor,
|
||||
timeout: timeout,
|
||||
deadlineTimestamp: deadlineTimestamp
|
||||
)
|
||||
self.currentParams = params
|
||||
|
||||
self.updateValues()
|
||||
}
|
||||
|
||||
private func updateValues() {
|
||||
guard let params = self.currentParams else {
|
||||
return
|
||||
}
|
||||
|
||||
let fractionalTimeout: Double
|
||||
|
||||
if let deadlineTimestamp = params.deadlineTimestamp {
|
||||
let fractionalTimestamp = CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970
|
||||
fractionalTimeout = max(0.0, Double(deadlineTimestamp) - fractionalTimestamp)
|
||||
} else {
|
||||
fractionalTimeout = Double(params.timeout)
|
||||
}
|
||||
|
||||
let timeout = Int(round(fractionalTimeout))
|
||||
|
||||
let proximityInterval: Double = 5.0
|
||||
let timerInterval: Double = 60.0
|
||||
|
||||
let isProximity = timeout <= Int(proximityInterval)
|
||||
let isTimer = timeout <= Int(timerInterval)
|
||||
|
||||
let color = isProximity ? params.proximityColor : params.regularColor
|
||||
self.textNode.attributedText = NSAttributedString(string: textForTimeout(value: timeout), font: Font.regular(14.0), textColor: color)
|
||||
let textSize = textNode.updateLayout(CGSize(width: 100.0, height: 100.0))
|
||||
self.textNode.frame = CGRect(origin: CGPoint(x: -22.0 - textSize.width, y: 0.0), size: textSize)
|
||||
|
||||
let contentState: ContentState
|
||||
if isTimer {
|
||||
var fraction: CGFloat = 1.0
|
||||
if fractionalTimeout <= timerInterval {
|
||||
fraction = CGFloat(fractionalTimeout) / min(CGFloat(timerInterval), CGFloat(params.timeout))
|
||||
}
|
||||
fraction = max(0.0, min(0.99, fraction))
|
||||
contentState = .timeout(color, 1.0 - fraction)
|
||||
} else {
|
||||
contentState = .clock(color)
|
||||
}
|
||||
|
||||
if self.currentContentState != contentState {
|
||||
self.currentContentState = contentState
|
||||
let image: UIImage?
|
||||
|
||||
let diameter: CGFloat = 14.0
|
||||
let inset: CGFloat = 8.0
|
||||
let lineWidth: CGFloat = 1.2
|
||||
|
||||
switch contentState {
|
||||
case let .clock(color):
|
||||
image = generateImage(CGSize(width: diameter + inset, height: diameter + inset), rotatedContext: { size, context in
|
||||
context.clear(CGRect(origin: CGPoint(), size: size))
|
||||
context.setStrokeColor(color.cgColor)
|
||||
context.setLineWidth(lineWidth)
|
||||
context.setLineCap(.round)
|
||||
|
||||
let clockFrame = CGRect(origin: CGPoint(x: (size.width - diameter) / 2.0, y: (size.height - diameter) / 2.0), size: CGSize(width: diameter, height: diameter))
|
||||
context.strokeEllipse(in: clockFrame.insetBy(dx: lineWidth / 2.0, dy: lineWidth / 2.0))
|
||||
|
||||
context.move(to: CGPoint(x: size.width / 2.0, y: size.height / 2.0))
|
||||
context.addLine(to: CGPoint(x: size.width / 2.0, y: clockFrame.minY + 4.0))
|
||||
context.strokePath()
|
||||
|
||||
let topWidth: CGFloat = 4.0
|
||||
context.move(to: CGPoint(x: size.width / 2.0 - topWidth / 2.0, y: clockFrame.minY - 2.0))
|
||||
context.addLine(to: CGPoint(x: size.width / 2.0 + topWidth / 2.0, y: clockFrame.minY - 2.0))
|
||||
context.strokePath()
|
||||
})
|
||||
case let .timeout(color, fraction):
|
||||
let timestamp = CACurrentMediaTime()
|
||||
|
||||
let center = CGPoint(x: (diameter + inset) / 2.0, y: (diameter + inset) / 2.0)
|
||||
let radius: CGFloat = (diameter - lineWidth / 2.0) / 2.0
|
||||
|
||||
let startAngle: CGFloat = -CGFloat.pi / 2.0
|
||||
let endAngle: CGFloat = -CGFloat.pi / 2.0 + 2.0 * CGFloat.pi * fraction
|
||||
|
||||
let v = CGPoint(x: sin(endAngle), y: -cos(endAngle))
|
||||
let c = CGPoint(x: -v.y * radius + center.x, y: v.x * radius + center.y)
|
||||
|
||||
let dt: CGFloat = 1.0 / 60.0
|
||||
var removeIndices: [Int] = []
|
||||
for i in 0 ..< self.particles.count {
|
||||
let currentTime = timestamp - self.particles[i].beginTime
|
||||
if currentTime > self.particles[i].lifetime {
|
||||
removeIndices.append(i)
|
||||
} else {
|
||||
let input: CGFloat = CGFloat(currentTime / self.particles[i].lifetime)
|
||||
let decelerated: CGFloat = (1.0 - (1.0 - input) * (1.0 - input))
|
||||
self.particles[i].alpha = 1.0 - decelerated
|
||||
|
||||
var p = self.particles[i].position
|
||||
let d = self.particles[i].direction
|
||||
let v = self.particles[i].velocity
|
||||
p = CGPoint(x: p.x + d.x * v * dt, y: p.y + d.y * v * dt)
|
||||
self.particles[i].position = p
|
||||
}
|
||||
}
|
||||
|
||||
for i in removeIndices.reversed() {
|
||||
self.particles.remove(at: i)
|
||||
}
|
||||
|
||||
let newParticleCount = 1
|
||||
for _ in 0 ..< newParticleCount {
|
||||
let degrees: CGFloat = CGFloat(arc4random_uniform(140)) - 40.0
|
||||
let angle: CGFloat = degrees * CGFloat.pi / 180.0
|
||||
|
||||
let direction = CGPoint(x: v.x * cos(angle) - v.y * sin(angle), y: v.x * sin(angle) + v.y * cos(angle))
|
||||
let velocity = (20.0 + (CGFloat(arc4random()) / CGFloat(UINT32_MAX)) * 4.0) * 0.3
|
||||
|
||||
let lifetime = Double(0.4 + CGFloat(arc4random_uniform(100)) * 0.01)
|
||||
|
||||
let particle = ContentParticle(position: c, direction: direction, velocity: velocity, alpha: 1.0, lifetime: lifetime, beginTime: timestamp)
|
||||
self.particles.append(particle)
|
||||
}
|
||||
|
||||
image = generateImage(CGSize(width: diameter + inset, height: diameter + inset), rotatedContext: { size, context in
|
||||
context.clear(CGRect(origin: CGPoint(), size: size))
|
||||
context.setStrokeColor(color.cgColor)
|
||||
context.setFillColor(color.cgColor)
|
||||
context.setLineWidth(lineWidth)
|
||||
context.setLineCap(.round)
|
||||
|
||||
let path = CGMutablePath()
|
||||
path.addArc(center: center, radius: radius, startAngle: startAngle, endAngle: endAngle, clockwise: true)
|
||||
context.addPath(path)
|
||||
context.strokePath()
|
||||
|
||||
for particle in self.particles {
|
||||
let size: CGFloat = 1.15
|
||||
context.setAlpha(particle.alpha)
|
||||
context.fillEllipse(in: CGRect(origin: CGPoint(x: particle.position.x - size / 2.0, y: particle.position.y - size / 2.0), size: CGSize(width: size, height: size)))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
self.contentNode.contents = image?.cgImage
|
||||
if let image = image {
|
||||
self.contentNode.frame = CGRect(origin: CGPoint(x: -image.size.width, y: -3.0), size: image.size)
|
||||
}
|
||||
}
|
||||
|
||||
if let reachedTimeout = self.reachedTimeout, fractionalTimeout <= .ulpOfOne {
|
||||
reachedTimeout()
|
||||
}
|
||||
|
||||
if fractionalTimeout <= .ulpOfOne {
|
||||
self.animator?.invalidate()
|
||||
self.animator = nil
|
||||
} else {
|
||||
if self.animator == nil {
|
||||
let animator = ConstantDisplayLinkAnimator(update: { [weak self] in
|
||||
self?.updateValues()
|
||||
})
|
||||
self.animator = animator
|
||||
animator.isPaused = self.inHierarchyValue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -18,25 +18,30 @@ private final class PollResultsControllerArguments {
|
||||
let collapseOption: (Data) -> Void
|
||||
let expandOption: (Data) -> Void
|
||||
let openPeer: (RenderedPeer) -> Void
|
||||
let expandSolution: () -> Void
|
||||
|
||||
init(context: AccountContext, collapseOption: @escaping (Data) -> Void, expandOption: @escaping (Data) -> Void, openPeer: @escaping (RenderedPeer) -> Void) {
|
||||
init(context: AccountContext, collapseOption: @escaping (Data) -> Void, expandOption: @escaping (Data) -> Void, openPeer: @escaping (RenderedPeer) -> Void, expandSolution: @escaping () -> Void) {
|
||||
self.context = context
|
||||
self.collapseOption = collapseOption
|
||||
self.expandOption = expandOption
|
||||
self.openPeer = openPeer
|
||||
self.expandSolution = expandSolution
|
||||
}
|
||||
}
|
||||
|
||||
private enum PollResultsSection {
|
||||
case text
|
||||
case solution
|
||||
case option(Int)
|
||||
|
||||
var rawValue: Int32 {
|
||||
switch self {
|
||||
case .text:
|
||||
return 0
|
||||
case .solution:
|
||||
return 1
|
||||
case let .option(index):
|
||||
return 1 + Int32(index)
|
||||
return 2 + Int32(index)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -45,6 +50,8 @@ private enum PollResultsEntryId: Hashable {
|
||||
case text
|
||||
case optionPeer(Int, Int)
|
||||
case optionExpand(Int)
|
||||
case solutionHeader
|
||||
case solutionText
|
||||
}
|
||||
|
||||
private enum PollResultsItemTag: ItemListItemTag, Equatable {
|
||||
@ -63,6 +70,8 @@ private enum PollResultsEntry: ItemListNodeEntry {
|
||||
case text(String)
|
||||
case optionPeer(optionId: Int, index: Int, peer: RenderedPeer, optionText: String, optionAdditionalText: String, optionCount: Int32, optionExpanded: Bool, opaqueIdentifier: Data, shimmeringAlternation: Int?, isFirstInOption: Bool)
|
||||
case optionExpand(optionId: Int, opaqueIdentifier: Data, text: String, enabled: Bool)
|
||||
case solutionHeader(String)
|
||||
case solutionText(String)
|
||||
|
||||
var section: ItemListSectionId {
|
||||
switch self {
|
||||
@ -72,6 +81,8 @@ private enum PollResultsEntry: ItemListNodeEntry {
|
||||
return PollResultsSection.option(optionPeer.optionId).rawValue
|
||||
case let .optionExpand(optionExpand):
|
||||
return PollResultsSection.option(optionExpand.optionId).rawValue
|
||||
case .solutionHeader, .solutionText:
|
||||
return PollResultsSection.solution.rawValue
|
||||
}
|
||||
}
|
||||
|
||||
@ -83,6 +94,10 @@ private enum PollResultsEntry: ItemListNodeEntry {
|
||||
return .optionPeer(optionPeer.optionId, optionPeer.index)
|
||||
case let .optionExpand(optionExpand):
|
||||
return .optionExpand(optionExpand.optionId)
|
||||
case .solutionHeader:
|
||||
return .solutionHeader
|
||||
case .solutionText:
|
||||
return .solutionText
|
||||
}
|
||||
}
|
||||
|
||||
@ -95,10 +110,34 @@ private enum PollResultsEntry: ItemListNodeEntry {
|
||||
default:
|
||||
return true
|
||||
}
|
||||
case .solutionHeader:
|
||||
switch rhs {
|
||||
case .text:
|
||||
return false
|
||||
case .solutionHeader:
|
||||
return false
|
||||
default:
|
||||
return true
|
||||
}
|
||||
case .solutionText:
|
||||
switch rhs {
|
||||
case .text:
|
||||
return false
|
||||
case .solutionHeader:
|
||||
return false
|
||||
case .solutionText:
|
||||
return false
|
||||
default:
|
||||
return true
|
||||
}
|
||||
case let .optionPeer(lhsOptionPeer):
|
||||
switch rhs {
|
||||
case .text:
|
||||
return false
|
||||
case .solutionHeader:
|
||||
return false
|
||||
case .solutionText:
|
||||
return false
|
||||
case let .optionPeer(rhsOptionPeer):
|
||||
if lhsOptionPeer.optionId == rhsOptionPeer.optionId {
|
||||
return lhsOptionPeer.index < rhsOptionPeer.index
|
||||
@ -116,6 +155,10 @@ private enum PollResultsEntry: ItemListNodeEntry {
|
||||
switch rhs {
|
||||
case .text:
|
||||
return false
|
||||
case .solutionHeader:
|
||||
return false
|
||||
case .solutionText:
|
||||
return false
|
||||
case let .optionPeer(rhsOptionPeer):
|
||||
if lhsOptionExpand.optionId == rhsOptionPeer.optionId {
|
||||
return false
|
||||
@ -137,6 +180,10 @@ private enum PollResultsEntry: ItemListNodeEntry {
|
||||
switch self {
|
||||
case let .text(text):
|
||||
return ItemListTextItem(presentationData: presentationData, text: .large(text), sectionId: self.section)
|
||||
case let .solutionHeader(text):
|
||||
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
|
||||
case let .solutionText(text):
|
||||
return ItemListMultilineTextItem(presentationData: presentationData, text: text, enabledEntityTypes: [], sectionId: self.section, style: .blocks)
|
||||
case let .optionPeer(optionId, _, peer, optionText, optionAdditionalText, optionCount, optionExpanded, opaqueIdentifier, shimmeringAlternation, isFirstInOption):
|
||||
let header = ItemListPeerItemHeader(theme: presentationData.theme, strings: presentationData.strings, text: optionText, additionalText: optionAdditionalText, actionTitle: optionExpanded ? presentationData.strings.PollResults_Collapse : presentationData.strings.MessagePoll_VotedCount(optionCount), id: Int64(optionId), action: optionExpanded ? {
|
||||
arguments.collapseOption(opaqueIdentifier)
|
||||
@ -156,6 +203,7 @@ private enum PollResultsEntry: ItemListNodeEntry {
|
||||
|
||||
private struct PollResultsControllerState: Equatable {
|
||||
var expandedOptions: [Data: Int] = [:]
|
||||
var isSolutionExpanded: Bool = false
|
||||
}
|
||||
|
||||
private func pollResultsControllerEntries(presentationData: PresentationData, poll: TelegramMediaPoll, state: PollResultsControllerState, resultsState: PollResultsState) -> [PollResultsEntry] {
|
||||
@ -171,12 +219,18 @@ private func pollResultsControllerEntries(presentationData: PresentationData, po
|
||||
|
||||
entries.append(.text(poll.text))
|
||||
|
||||
if let solution = poll.results.solution, !solution.isEmpty {
|
||||
//TODO:localize
|
||||
entries.append(.solutionHeader("EXPLANATION"))
|
||||
entries.append(.solutionText(solution))
|
||||
}
|
||||
|
||||
var optionVoterCount: [Int: Int32] = [:]
|
||||
let totalVoterCount = poll.results.totalVoters ?? 0
|
||||
var optionPercentage: [Int] = []
|
||||
|
||||
if totalVoterCount != 0 {
|
||||
if let voters = poll.results.voters, let totalVoters = poll.results.totalVoters {
|
||||
if let voters = poll.results.voters, let _ = poll.results.totalVoters {
|
||||
for i in 0 ..< poll.options.count {
|
||||
inner: for optionVoters in voters {
|
||||
if optionVoters.opaqueIdentifier == poll.options[i].opaqueIdentifier {
|
||||
@ -215,7 +269,6 @@ private func pollResultsControllerEntries(presentationData: PresentationData, po
|
||||
}
|
||||
} else {
|
||||
if let optionState = resultsState.options[option.opaqueIdentifier], !optionState.peers.isEmpty {
|
||||
var hasMore = false
|
||||
let optionExpandedAtCount = state.expandedOptions[option.opaqueIdentifier]
|
||||
|
||||
let peers = optionState.peers
|
||||
@ -307,6 +360,8 @@ public func pollResultsController(context: AccountContext, messageId: MessageId,
|
||||
pushControllerImpl?(controller)
|
||||
}
|
||||
}
|
||||
}, expandSolution: {
|
||||
|
||||
})
|
||||
|
||||
let previousWasEmpty = Atomic<Bool?>(value: nil)
|
||||
|
@ -1135,6 +1135,7 @@ public final class SharedAccountContextImpl: SharedAccountContext {
|
||||
}, dismissReplyMarkupMessage: { _ in
|
||||
}, openMessagePollResults: { _, _ in
|
||||
}, openPollCreation: { _ in
|
||||
}, displayPollSolution: { _ in
|
||||
}, requestMessageUpdate: { _ in
|
||||
}, cancelInteractiveKeyboardGestures: {
|
||||
}, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings,
|
||||
|
@ -12,6 +12,7 @@ public enum UndoOverlayContent {
|
||||
case hidArchive(title: String, text: String, undo: Bool)
|
||||
case revealedArchive(title: String, text: String, undo: Bool)
|
||||
case succeed(text: String)
|
||||
case info(text: String)
|
||||
case emoji(path: String, text: String)
|
||||
case swipeToReply(title: String, text: String)
|
||||
case actionSucceeded(title: String, text: String, cancel: String)
|
||||
|
@ -137,6 +137,19 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode {
|
||||
self.textNode.maximumNumberOfLines = 2
|
||||
displayUndo = false
|
||||
self.originalRemainingSeconds = 5
|
||||
case let .info(text):
|
||||
self.iconNode = nil
|
||||
self.iconCheckNode = nil
|
||||
self.animationNode = AnimationNode(animation: "anim_infotip", colors: ["info1.info1.stroke": self.animationBackgroundColor, "info2.info2.Fill": self.animationBackgroundColor], scale: 1.0)
|
||||
self.animatedStickerNode = nil
|
||||
|
||||
let body = MarkdownAttributeSet(font: Font.regular(14.0), textColor: .white)
|
||||
let bold = MarkdownAttributeSet(font: Font.semibold(14.0), textColor: .white)
|
||||
let attributedText = parseMarkdownIntoAttributedString(text, attributes: MarkdownAttributes(body: body, bold: bold, link: body, linkAttribute: { _ in return nil }), textAlignment: .natural)
|
||||
self.textNode.attributedText = attributedText
|
||||
self.textNode.maximumNumberOfLines = 2
|
||||
displayUndo = false
|
||||
self.originalRemainingSeconds = max(5, min(8, text.count / 14))
|
||||
case let .actionSucceeded(title, text, cancel):
|
||||
self.iconNode = nil
|
||||
self.iconCheckNode = nil
|
||||
@ -296,6 +309,8 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode {
|
||||
self.panelWrapperNode.addSubnode(self.timerTextNode)
|
||||
case .archivedChat, .hidArchive, .revealedArchive, .succeed, .emoji, .swipeToReply, .actionSucceeded, .stickersModified:
|
||||
break
|
||||
case .info:
|
||||
self.isUserInteractionEnabled = false
|
||||
}
|
||||
self.statusNode.flatMap(self.panelWrapperNode.addSubnode)
|
||||
self.iconNode.flatMap(self.panelWrapperNode.addSubnode)
|
||||
@ -350,7 +365,7 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode {
|
||||
}
|
||||
|
||||
@objc private func undoButtonPressed() {
|
||||
self.action(.undo)
|
||||
let _ = self.action(.undo)
|
||||
self.dismiss()
|
||||
}
|
||||
|
||||
@ -359,7 +374,7 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode {
|
||||
self.remainingSeconds -= 1
|
||||
}
|
||||
if self.remainingSeconds == 0 {
|
||||
self.action(.commit)
|
||||
let _ = self.action(.commit)
|
||||
self.dismiss()
|
||||
} else {
|
||||
if !self.timerTextNode.bounds.size.width.isZero, let snapshot = self.timerTextNode.view.snapshotContentTree() {
|
||||
|
Loading…
x
Reference in New Issue
Block a user