Updated polls

This commit is contained in:
Ali 2020-04-03 17:56:18 +04:00
parent bcba9c093b
commit 286ce686e6
31 changed files with 792 additions and 97 deletions

View File

@ -3,7 +3,7 @@
@implementation Serialization
- (NSUInteger)currentLayer {
return 111;
return 112;
}
- (id _Nullable)parseMessage:(NSData * _Nullable)data {

View File

@ -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: {

View File

@ -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)
}
}

View File

@ -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) }

View File

@ -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 {

View File

@ -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>) {

View File

@ -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)

View File

@ -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

View File

@ -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 {

View File

@ -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)

View File

@ -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! {

View File

@ -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)

View File

@ -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)
}
}
}

View File

@ -236,4 +236,6 @@ public enum PresentationResourceParameterKey: Hashable {
case chatPrincipalThemeEssentialGraphics(hasWallpaper: Bool, bubbleCorners: PresentationChatBubbleCorners)
case chatPrincipalThemeAdditionalGraphics(isCustomWallpaper: Bool, bubbleCorners: PresentationChatBubbleCorners)
case chatBubbleLamp(incoming: Bool)
}

View File

@ -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)
})
}
}

View File

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

View File

@ -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)

View File

@ -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,

View File

@ -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
}
}
}

View File

@ -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)
})
}

View File

@ -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

View File

@ -423,6 +423,7 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode {
}, dismissReplyMarkupMessage: { _ in
}, openMessagePollResults: { _, _ in
}, openPollCreation: { _ in
}, displayPollSolution: { _ in
}, requestMessageUpdate: { _ in
}, cancelInteractiveKeyboardGestures: {
}, automaticMediaDownloadSettings: self.automaticMediaDownloadSettings,

View File

@ -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))

View File

@ -1527,6 +1527,7 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD
}, dismissReplyMarkupMessage: { _ in
}, openMessagePollResults: { _, _ in
}, openPollCreation: { _ in
}, displayPollSolution: { _ in
}, requestMessageUpdate: { _ in
}, cancelInteractiveKeyboardGestures: {
}, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings,

View File

@ -427,6 +427,7 @@ public class PeerMediaCollectionController: TelegramBaseController {
}, dismissReplyMarkupMessage: { _ in
}, openMessagePollResults: { _, _ in
}, openPollCreation: { _ in
}, displayPollSolution: { _ in
}, requestMessageUpdate: { _ in
}, cancelInteractiveKeyboardGestures: {
}, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings,

View 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
}
}
}
}

View File

@ -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)

View File

@ -1135,6 +1135,7 @@ public final class SharedAccountContextImpl: SharedAccountContext {
}, dismissReplyMarkupMessage: { _ in
}, openMessagePollResults: { _, _ in
}, openPollCreation: { _ in
}, displayPollSolution: { _ in
}, requestMessageUpdate: { _ in
}, cancelInteractiveKeyboardGestures: {
}, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings,

View File

@ -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)

View File

@ -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() {