Updated polls

This commit is contained in:
Ali 2020-01-08 00:53:20 +04:00
parent 54f46ccac3
commit b87c84a17d
54 changed files with 6176 additions and 4293 deletions

View File

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

View File

@ -5245,3 +5245,20 @@ Any member of this group will be able to see messages in the channel.";
"Conversation.ContextMenuCancelEditing" = "Cancel Editing";
"Map.NoPlacesNearby" = "There are no known places nearby.\nTry a different location.";
"CreatePoll.Anonymous" = "Anonymous Votes";
"CreatePoll.MultipleChoice" = "Multiple Choice";
"CreatePoll.Quiz" = "Quiz Mode";
"CreatePoll.QuizInfo" = "Quiz has only one correct answer. Users can't revoke their votes.";
"CreatePoll.QuizTip" = "Tap to select the correct option";
"MessagePoll.LabelPoll" = "Poll";
"MessagePoll.LabelAnonymousQuiz" = "Anonymous Quiz";
"MessagePoll.LabelQuiz" = "Quiz";
"MessagePoll.SubmitVote" = "Submit Vote";
"MessagePoll.ViewResults" = "View Results";
"PollResults.Title" = "Poll Results";
"PollResults.Collapse" = "COLLAPSE";
"PollResults.ShowMore_1" = "Show %@ More Voter";
"PollResults.ShowMore_any" = "Show %@ More Voters";

View File

@ -54,16 +54,6 @@ private class AvatarNodeParameters: NSObject {
}
}
private let gradientColors: [NSArray] = [
[UIColor(rgb: 0xff516a).cgColor, UIColor(rgb: 0xff885e).cgColor],
[UIColor(rgb: 0xffa85c).cgColor, UIColor(rgb: 0xffcd6a).cgColor],
[UIColor(rgb: 0x665fff).cgColor, UIColor(rgb: 0x82b1ff).cgColor],
[UIColor(rgb: 0x54cb68).cgColor, UIColor(rgb: 0xa0de7e).cgColor],
[UIColor(rgb: 0x4acccd).cgColor, UIColor(rgb: 0x00fcfd).cgColor],
[UIColor(rgb: 0x2a9ef1).cgColor, UIColor(rgb: 0x72d5fd).cgColor],
[UIColor(rgb: 0xd669ed).cgColor, UIColor(rgb: 0xe0a2f3).cgColor],
]
private func generateGradientFilledCircleImage(diameter: CGFloat, colors: NSArray) -> UIImage? {
return generateImage(CGSize(width: diameter, height: diameter), contextGenerator: { size, context in
let bounds = CGRect(origin: CGPoint(), size: size)
@ -167,6 +157,16 @@ public final class AvatarEditOverlayNode: ASDisplayNode {
}
public final class AvatarNode: ASDisplayNode {
public static let gradientColors: [NSArray] = [
[UIColor(rgb: 0xff516a).cgColor, UIColor(rgb: 0xff885e).cgColor],
[UIColor(rgb: 0xffa85c).cgColor, UIColor(rgb: 0xffcd6a).cgColor],
[UIColor(rgb: 0x665fff).cgColor, UIColor(rgb: 0x82b1ff).cgColor],
[UIColor(rgb: 0x54cb68).cgColor, UIColor(rgb: 0xa0de7e).cgColor],
[UIColor(rgb: 0x4acccd).cgColor, UIColor(rgb: 0x00fcfd).cgColor],
[UIColor(rgb: 0x2a9ef1).cgColor, UIColor(rgb: 0x72d5fd).cgColor],
[UIColor(rgb: 0xd669ed).cgColor, UIColor(rgb: 0xe0a2f3).cgColor],
]
public var font: UIFont {
didSet {
if oldValue !== font {
@ -318,7 +318,7 @@ public final class AvatarNode: ASDisplayNode {
let parameters: AvatarNodeParameters
if let peer = peer, let signal = peerAvatarImage(account: context.account, peer: peer, authorOfMessage: authorOfMessage, representation: representation, displayDimensions: displayDimensions, emptyColor: emptyColor, synchronousLoad: synchronousLoad) {
if let peer = peer, let signal = peerAvatarImage(account: context.account, peerReference: PeerReference(peer), authorOfMessage: authorOfMessage, representation: representation, displayDimensions: displayDimensions, emptyColor: emptyColor, synchronousLoad: synchronousLoad) {
self.contents = nil
self.displaySuspended = true
self.imageReady.set(self.imageNode.ready)
@ -456,7 +456,7 @@ public final class AvatarNode: ASDisplayNode {
colorsArray = grayscaleColors
}
} else {
colorsArray = gradientColors[colorIndex % gradientColors.count]
colorsArray = AvatarNode.gradientColors[colorIndex % AvatarNode.gradientColors.count]
}
var locations: [CGFloat] = [1.0, 0.0]
@ -555,7 +555,7 @@ public final class AvatarNode: ASDisplayNode {
}
}
public func drawPeerAvatarLetters(context: CGContext, size: CGSize, font: UIFont, letters: [String], accountPeerId: PeerId, peerId: PeerId) {
public func drawPeerAvatarLetters(context: CGContext, size: CGSize, font: UIFont, letters: [String], peerId: PeerId) {
context.beginPath()
context.addEllipse(in: CGRect(x: 0.0, y: 0.0, width: size.width, height:
size.height))
@ -572,7 +572,7 @@ public func drawPeerAvatarLetters(context: CGContext, size: CGSize, font: UIFont
if colorIndex == -1 {
colorsArray = grayscaleColors
} else {
colorsArray = gradientColors[colorIndex % gradientColors.count]
colorsArray = AvatarNode.gradientColors[colorIndex % AvatarNode.gradientColors.count]
}
var locations: [CGFloat] = [1.0, 0.0]

View File

@ -21,7 +21,7 @@ private let roundCorners = { () -> UIImage in
return image
}()
public func peerAvatarImageData(account: Account, peer: Peer, authorOfMessage: MessageReference?, representation: TelegramMediaImageRepresentation?, synchronousLoad: Bool) -> Signal<Data?, NoError>? {
public func peerAvatarImageData(account: Account, peerReference: PeerReference?, authorOfMessage: MessageReference?, representation: TelegramMediaImageRepresentation?, synchronousLoad: Bool) -> Signal<Data?, NoError>? {
if let smallProfileImage = representation {
let resourceData = account.postbox.mediaBox.resourceData(smallProfileImage.resource, attemptSynchronously: synchronousLoad)
let imageData = resourceData
@ -44,7 +44,7 @@ public func peerAvatarImageData(account: Account, peer: Peer, authorOfMessage: M
subscriber.putCompletion()
})
var fetchedDataDisposable: Disposable?
if let peerReference = PeerReference(peer) {
if let peerReference = peerReference {
fetchedDataDisposable = fetchedMediaResource(mediaBox: account.postbox.mediaBox, reference: .avatar(peer: peerReference, resource: smallProfileImage.resource), statsCategory: .generic).start()
} else if let authorOfMessage = authorOfMessage {
fetchedDataDisposable = fetchedMediaResource(mediaBox: account.postbox.mediaBox, reference: .messageAuthorAvatar(message: authorOfMessage, resource: smallProfileImage.resource), statsCategory: .generic).start()
@ -64,8 +64,8 @@ public func peerAvatarImageData(account: Account, peer: Peer, authorOfMessage: M
}
}
public func peerAvatarImage(account: Account, peer: Peer, authorOfMessage: MessageReference?, representation: TelegramMediaImageRepresentation?, displayDimensions: CGSize = CGSize(width: 60.0, height: 60.0), round: Bool = true, inset: CGFloat = 0.0, emptyColor: UIColor? = nil, synchronousLoad: Bool = false) -> Signal<UIImage?, NoError>? {
if let imageData = peerAvatarImageData(account: account, peer: peer, authorOfMessage: authorOfMessage, representation: representation, synchronousLoad: synchronousLoad) {
public func peerAvatarImage(account: Account, peerReference: PeerReference?, authorOfMessage: MessageReference?, representation: TelegramMediaImageRepresentation?, displayDimensions: CGSize = CGSize(width: 60.0, height: 60.0), round: Bool = true, inset: CGFloat = 0.0, emptyColor: UIColor? = nil, synchronousLoad: Bool = false) -> Signal<UIImage?, NoError>? {
if let imageData = peerAvatarImageData(account: account, peerReference: peerReference, authorOfMessage: authorOfMessage, representation: representation, synchronousLoad: synchronousLoad) {
return imageData
|> mapToSignal { data -> Signal<UIImage?, NoError> in
let generate = deferred { () -> Signal<UIImage?, NoError> in

View File

@ -27,31 +27,51 @@ private final class CreatePollControllerArguments {
let updatePollText: (String) -> Void
let updateOptionText: (Int, String) -> Void
let moveToNextOption: (Int) -> Void
let addOption: () -> Void
let moveToPreviousOption: (Int) -> Void
let removeOption: (Int, Bool) -> Void
let optionFocused: (Int) -> Void
let setItemIdWithRevealedOptions: (Int?, Int?) -> Void
let toggleOptionSelected: (Int) -> Void
let updateAnonymous: (Bool) -> Void
let updateMultipleChoice: (Bool) -> Void
let updateQuiz: (Bool) -> Void
init(updatePollText: @escaping (String) -> Void, updateOptionText: @escaping (Int, String) -> Void, moveToNextOption: @escaping (Int) -> Void, addOption: @escaping () -> Void, removeOption: @escaping (Int, Bool) -> Void, optionFocused: @escaping (Int) -> Void, setItemIdWithRevealedOptions: @escaping (Int?, Int?) -> Void) {
init(updatePollText: @escaping (String) -> Void, updateOptionText: @escaping (Int, String) -> Void, moveToNextOption: @escaping (Int) -> Void, moveToPreviousOption: @escaping (Int) -> Void, removeOption: @escaping (Int, Bool) -> Void, optionFocused: @escaping (Int) -> Void, setItemIdWithRevealedOptions: @escaping (Int?, Int?) -> Void, toggleOptionSelected: @escaping (Int) -> Void, updateAnonymous: @escaping (Bool) -> Void, updateMultipleChoice: @escaping (Bool) -> Void, updateQuiz: @escaping (Bool) -> Void) {
self.updatePollText = updatePollText
self.updateOptionText = updateOptionText
self.moveToNextOption = moveToNextOption
self.addOption = addOption
self.moveToPreviousOption = moveToPreviousOption
self.removeOption = removeOption
self.optionFocused = optionFocused
self.setItemIdWithRevealedOptions = setItemIdWithRevealedOptions
self.toggleOptionSelected = toggleOptionSelected
self.updateAnonymous = updateAnonymous
self.updateMultipleChoice = updateMultipleChoice
self.updateQuiz = updateQuiz
}
}
private enum CreatePollSection: Int32 {
case text
case options
case settings
}
private enum CreatePollEntryId: Hashable {
case textHeader
case text
case optionsHeader
case option(Int)
case optionsInfo
case anonymousVotes
case multipleChoice
case quiz
case quizInfo
}
private enum CreatePollEntryTag: Equatable, ItemListItemTag {
case text
case option(Int)
case addOption(Int)
func isEqual(to other: ItemListItemTag) -> Bool {
if let other = other as? CreatePollEntryTag {
@ -63,67 +83,82 @@ private enum CreatePollEntryTag: Equatable, ItemListItemTag {
}
private enum CreatePollEntry: ItemListNodeEntry {
case textHeader(PresentationTheme, String, ItemListSectionHeaderAccessoryText)
case text(PresentationTheme, String, String, Int)
case optionsHeader(PresentationTheme, String)
case option(PresentationTheme, PresentationStrings, Int, Int, String, String, Bool, Bool)
case addOption(PresentationTheme, String, Bool, Int)
case optionsInfo(PresentationTheme, String)
case textHeader(String, ItemListSectionHeaderAccessoryText)
case text(String, String, Int)
case optionsHeader(String)
case option(id: Int, index: Int, placeholder: String, text: String, revealed: Bool, hasNext: Bool, isLast: Bool, isSelected: Bool?)
case optionsInfo(String)
case anonymousVotes(String, Bool)
case multipleChoice(String, Bool, Bool)
case quiz(String, Bool)
case quizInfo(String)
var section: ItemListSectionId {
switch self {
case .textHeader, .text:
return CreatePollSection.text.rawValue
case .optionsHeader, .option, .addOption, .optionsInfo:
return CreatePollSection.options.rawValue
case .textHeader, .text:
return CreatePollSection.text.rawValue
case .optionsHeader, .option, .optionsInfo:
return CreatePollSection.options.rawValue
case .anonymousVotes, .multipleChoice, .quiz, .quizInfo:
return CreatePollSection.settings.rawValue
}
}
var tag: ItemListItemTag? {
switch self {
case .text:
return CreatePollEntryTag.text
case let .option(_, _, id, _, _, _, _, _):
return CreatePollEntryTag.option(id)
case let .addOption(_, _, _, id):
return CreatePollEntryTag.addOption(id)
default:
break
case .text:
return CreatePollEntryTag.text
case let .option(option):
return CreatePollEntryTag.option(option.id)
default:
break
}
return nil
}
var stableId: Int {
var stableId: CreatePollEntryId {
switch self {
case .textHeader:
return 0
case .text:
return 1
case .optionsHeader:
return 2
case let .option(_, _, id, _, _, _, _, _):
return 3 + id
case .addOption:
return 1000
case .optionsInfo:
return 1001
case .textHeader:
return .textHeader
case .text:
return .text
case .optionsHeader:
return .optionsHeader
case let .option(option):
return .option(option.id)
case .optionsInfo:
return .optionsInfo
case .anonymousVotes:
return .anonymousVotes
case .multipleChoice:
return .multipleChoice
case .quiz:
return .quiz
case .quizInfo:
return .quizInfo
}
}
private var sortId: Int {
switch self {
case .textHeader:
return 0
case .text:
return 1
case .optionsHeader:
return 2
case let .option(_, _, _, index, _, _, _, _):
return 3 + index
case .addOption:
return 1000
case .optionsInfo:
return 1001
case .textHeader:
return 0
case .text:
return 1
case .optionsHeader:
return 2
case let .option(option):
return 3 + option.index
case .optionsInfo:
return 1001
case .anonymousVotes:
return 1002
case .multipleChoice:
return 1003
case .quiz:
return 1004
case .quizInfo:
return 1005
}
}
@ -134,32 +169,49 @@ private enum CreatePollEntry: ItemListNodeEntry {
func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem {
let arguments = arguments as! CreatePollControllerArguments
switch self {
case let .textHeader(theme, text, accessoryText):
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, accessoryText: accessoryText, sectionId: self.section)
case let .text(theme, placeholder, text, maxLength):
return ItemListMultilineInputItem(presentationData: presentationData, text: text, placeholder: placeholder, maxLength: ItemListMultilineInputItemTextLimit(value: maxLength, display: false), sectionId: self.section, style: .blocks, textUpdated: { value in
arguments.updatePollText(value)
}, tag: CreatePollEntryTag.text)
case let .optionsHeader(theme, text):
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
case let .option(theme, strings, id, _, placeholder, text, revealed, hasNext):
return CreatePollOptionItem(theme: theme, strings: strings, id: id, placeholder: placeholder, value: text, maxLength: maxOptionLength, editing: CreatePollOptionItemEditing(editable: true, hasActiveRevealControls: revealed), sectionId: self.section, setItemIdWithRevealedOptions: { id, fromId in
arguments.setItemIdWithRevealedOptions(id, fromId)
}, updated: { value in
arguments.updateOptionText(id, value)
}, next: hasNext ? {
arguments.moveToNextOption(id)
} : nil, delete: { focused in
case let .textHeader(text, accessoryText):
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, accessoryText: accessoryText, sectionId: self.section)
case let .text(placeholder, text, maxLength):
return ItemListMultilineInputItem(presentationData: presentationData, text: text, placeholder: placeholder, maxLength: ItemListMultilineInputItemTextLimit(value: maxLength, display: false), sectionId: self.section, style: .blocks, textUpdated: { value in
arguments.updatePollText(value)
}, tag: CreatePollEntryTag.text)
case let .optionsHeader(text):
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
case let .option(id, _, placeholder, text, revealed, hasNext, isLast, isSelected):
return CreatePollOptionItem(presentationData: presentationData, id: id, placeholder: placeholder, value: text, isSelected: isSelected, maxLength: maxOptionLength, editing: CreatePollOptionItemEditing(editable: true, hasActiveRevealControls: revealed), sectionId: self.section, setItemIdWithRevealedOptions: { id, fromId in
arguments.setItemIdWithRevealedOptions(id, fromId)
}, updated: { value in
arguments.updateOptionText(id, value)
}, next: hasNext ? {
arguments.moveToNextOption(id)
} : nil, delete: { focused in
if !isLast {
arguments.removeOption(id, focused)
}, focused: {
arguments.optionFocused(id)
}, tag: CreatePollEntryTag.option(id))
case let .addOption(theme, title, enabled, id):
return CreatePollOptionActionItem(theme: theme, title: title, enabled: enabled, tag: CreatePollEntryTag.addOption(id), sectionId: self.section, action: {
arguments.addOption()
})
case let .optionsInfo(theme, text):
return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section)
} else {
arguments.moveToPreviousOption(id)
}
}, canDelete: !isLast,
focused: {
arguments.optionFocused(id)
}, toggleSelected: {
arguments.toggleOptionSelected(id)
}, tag: CreatePollEntryTag.option(id))
case let .optionsInfo(text):
return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section)
case let .anonymousVotes(text, value):
return ItemListSwitchItem(presentationData: presentationData, title: text, value: value, sectionId: self.section, style: .blocks, updated: { value in
arguments.updateAnonymous(value)
})
case let .multipleChoice(text, value, enabled):
return ItemListSwitchItem(presentationData: presentationData, title: text, value: value, enabled: enabled, sectionId: self.section, style: .blocks, updated: { value in
arguments.updateMultipleChoice(value)
})
case let .quiz(text, value):
return ItemListSwitchItem(presentationData: presentationData, title: text, value: value, sectionId: self.section, style: .blocks, updated: { value in
arguments.updateQuiz(value)
})
case let .quizInfo(text):
return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section)
}
}
}
@ -167,14 +219,18 @@ private enum CreatePollEntry: ItemListNodeEntry {
private struct CreatePollControllerOption: Equatable {
var text: String
let id: Int
var isSelected: Bool
}
private struct CreatePollControllerState: Equatable {
var text: String = ""
var options: [CreatePollControllerOption] = [CreatePollControllerOption(text: "", id: 0), CreatePollControllerOption(text: "", id: 1)]
var options: [CreatePollControllerOption] = [CreatePollControllerOption(text: "", id: 0, isSelected: false), CreatePollControllerOption(text: "", id: 1, isSelected: false)]
var nextOptionId: Int = 2
var focusOptionId: Int?
var optionIdWithRevealControls: Int?
var isAnonymous: Bool = true
var isMultipleChoice: Bool = false
var isQuiz: Bool = false
}
private func createPollControllerEntries(presentationData: PresentationData, state: CreatePollControllerState, limitsConfiguration: LimitsConfiguration) -> [CreatePollEntry] {
@ -185,19 +241,23 @@ private func createPollControllerEntries(presentationData: PresentationData, sta
let remainingCount = Int(maxTextLength) - state.text.count
textLimitText = ItemListSectionHeaderAccessoryText(value: "\(remainingCount)", color: remainingCount < 0 ? .destructive : .generic)
}
entries.append(.textHeader(presentationData.theme, presentationData.strings.CreatePoll_TextHeader, textLimitText))
entries.append(.text(presentationData.theme, presentationData.strings.CreatePoll_TextPlaceholder, state.text, Int(limitsConfiguration.maxMediaCaptionLength)))
entries.append(.optionsHeader(presentationData.theme, presentationData.strings.CreatePoll_OptionsHeader))
entries.append(.textHeader(presentationData.strings.CreatePoll_TextHeader, textLimitText))
entries.append(.text(presentationData.strings.CreatePoll_TextPlaceholder, state.text, Int(limitsConfiguration.maxMediaCaptionLength)))
entries.append(.optionsHeader(presentationData.strings.CreatePoll_OptionsHeader))
for i in 0 ..< state.options.count {
entries.append(.option(presentationData.theme, presentationData.strings, state.options[i].id, i, presentationData.strings.CreatePoll_OptionPlaceholder, state.options[i].text, state.optionIdWithRevealControls == state.options[i].id, i != 9))
entries.append(.option(id: state.options[i].id, index: i, placeholder: presentationData.strings.CreatePoll_OptionPlaceholder, text: state.options[i].text, revealed: state.optionIdWithRevealControls == state.options[i].id, hasNext: i != 9, isLast: i == state.options.count - 1, isSelected: state.isQuiz ? state.options[i].isSelected : nil))
}
if state.options.count < 10 {
entries.append(.addOption(presentationData.theme, presentationData.strings.CreatePoll_AddOption, true, state.options.last?.id ?? -1))
entries.append(.optionsInfo(presentationData.theme, presentationData.strings.CreatePoll_AddMoreOptions(Int32(10 - state.options.count))))
entries.append(.optionsInfo(presentationData.strings.CreatePoll_AddMoreOptions(Int32(10 - state.options.count))))
} else {
entries.append(.optionsInfo(presentationData.theme, presentationData.strings.CreatePoll_AllOptionsAdded))
entries.append(.optionsInfo(presentationData.strings.CreatePoll_AllOptionsAdded))
}
entries.append(.anonymousVotes(presentationData.strings.CreatePoll_Anonymous, state.isAnonymous))
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))
return entries
}
@ -212,6 +272,7 @@ public func createPollController(context: AccountContext, peerId: PeerId, comple
var dismissImpl: (() -> Void)?
var ensureTextVisibleImpl: (() -> Void)?
var ensureOptionVisibleImpl: ((Int) -> Void)?
var displayQuizTooltipImpl: (() -> Void)?
let actionsDisposable = DisposableSet()
@ -234,6 +295,18 @@ public func createPollController(context: AccountContext, peerId: PeerId, comple
for i in 0 ..< state.options.count {
if state.options[i].id == id {
state.options[i].text = value
if !value.isEmpty && i == state.options.count - 1 {
state.options.append(CreatePollControllerOption(text: "", id: state.nextOptionId, isSelected: false))
state.nextOptionId += 1
}
break
}
}
if state.options.count > 2 {
for i in (1 ..< state.options.count - 1).reversed() {
if state.options[i - 1].text.isEmpty && state.options[i].text.isEmpty {
state.options.remove(at: i)
}
}
}
return state
@ -246,7 +319,7 @@ public func createPollController(context: AccountContext, peerId: PeerId, comple
for i in 0 ..< state.options.count {
if state.options[i].id == id {
if i == state.options.count - 1 {
state.options.append(CreatePollControllerOption(text: "", id: state.nextOptionId))
state.options.append(CreatePollControllerOption(text: "", id: state.nextOptionId, isSelected: false))
state.focusOptionId = state.nextOptionId
state.nextOptionId += 1
} else {
@ -269,14 +342,32 @@ public func createPollController(context: AccountContext, peerId: PeerId, comple
return state
}
}
}, addOption: {
}, moveToPreviousOption: { id in
var resetFocusOptionId: Int?
updateState { state in
var state = state
state.options.append(CreatePollControllerOption(text: "", id: state.nextOptionId))
state.focusOptionId = state.nextOptionId
state.nextOptionId += 1
for i in 0 ..< state.options.count {
if state.options[i].id == id {
if i != 0 {
if state.focusOptionId == state.options[i - 1].id {
resetFocusOptionId = state.options[i - 1].id
state.focusOptionId = -1
} else {
state.focusOptionId = state.options[i - 1].id
}
}
break
}
}
return state
}
if let resetFocusOptionId = resetFocusOptionId {
updateState { state in
var state = state
state.focusOptionId = resetFocusOptionId
return state
}
}
}, removeOption: { id, focused in
updateState { state in
var state = state
@ -289,6 +380,19 @@ public func createPollController(context: AccountContext, peerId: PeerId, comple
break
}
}
let focusOnFirst = state.options.isEmpty
if state.options.count < 2 {
for i in 0 ..< (2 - state.options.count) {
if i == 0 && focusOnFirst {
state.options.append(CreatePollControllerOption(text: "", id: state.nextOptionId, isSelected: false))
state.focusOptionId = state.nextOptionId
state.nextOptionId += 1
} else {
state.options.append(CreatePollControllerOption(text: "", id: state.nextOptionId, isSelected: false))
state.nextOptionId += 1
}
}
}
return state
}
}, optionFocused: { id in
@ -303,6 +407,58 @@ public func createPollController(context: AccountContext, peerId: PeerId, comple
return state
}
}
}, toggleOptionSelected: { id in
updateState { state in
var state = state
for i in 0 ..< state.options.count {
if state.options[i].id == id {
state.options[i].isSelected = !state.options[i].isSelected
if state.options[i].isSelected && state.isQuiz {
for j in 0 ..< state.options.count {
if i != j {
state.options[j].isSelected = false
}
}
}
break
}
}
return state
}
}, updateAnonymous: { value in
updateState { state in
var state = state
state.isAnonymous = value
return state
}
}, updateMultipleChoice: { value in
updateState { state in
var state = state
state.isMultipleChoice = value
return state
}
}, updateQuiz: { value in
updateState { state in
var state = state
state.isQuiz = value
if value {
state.isMultipleChoice = false
var foundSelectedOption = false
for i in 0 ..< state.options.count {
if state.options[i].isSelected {
if !foundSelectedOption {
foundSelectedOption = true
} else {
state.options[i].isSelected = false
}
}
}
}
return state
}
if value {
displayQuizTooltipImpl?()
}
})
let previousOptionIds = Atomic<[Int]?>(value: nil)
@ -320,6 +476,7 @@ public func createPollController(context: AccountContext, peerId: PeerId, comple
enabled = false
}
var nonEmptyOptionCount = 0
var hasSelectedOptions = false
for option in state.options {
if !option.text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
nonEmptyOptionCount += 1
@ -327,6 +484,19 @@ public func createPollController(context: AccountContext, peerId: PeerId, comple
if option.text.count > maxOptionLength {
enabled = false
}
if option.isSelected {
hasSelectedOptions = true
}
if state.isQuiz {
if option.text.isEmpty && option.isSelected {
enabled = false
}
}
}
if state.isQuiz {
if !hasSelectedOptions {
enabled = false
}
}
if nonEmptyOptionCount < 2 {
enabled = false
@ -335,13 +505,30 @@ public func createPollController(context: AccountContext, peerId: PeerId, comple
rightNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.CreatePoll_Create), style: .bold, enabled: enabled, action: {
let state = stateValue.with { $0 }
var options: [TelegramMediaPollOption] = []
var correctAnswers: [Data]?
for i in 0 ..< state.options.count {
let optionText = state.options[i].text.trimmingCharacters(in: .whitespacesAndNewlines)
if !optionText.isEmpty {
options.append(TelegramMediaPollOption(text: optionText, opaqueIdentifier: "\(i)".data(using: .utf8)!))
let optionData = "\(i)".data(using: .utf8)!
options.append(TelegramMediaPollOption(text: optionText, opaqueIdentifier: optionData))
if state.isQuiz && state.options[i].isSelected {
correctAnswers = [optionData]
}
}
}
completion(.message(text: "", attributes: [], mediaReference: .standalone(media: TelegramMediaPoll(pollId: MediaId(namespace: Namespaces.Media.LocalPoll, id: arc4random64()), text: processPollText(state.text), options: options, results: TelegramMediaPollResults(voters: nil, totalVoters: nil), isClosed: false)), replyToMessageId: nil, localGroupingKey: nil))
let publicity: TelegramMediaPollPublicity
if state.isAnonymous {
publicity = .anonymous
} else {
publicity = .public
}
let kind: TelegramMediaPollKind
if state.isQuiz {
kind = .quiz
} else {
kind = .poll(multipleAnswers: state.isMultipleChoice)
}
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))
dismissImpl?()
})
@ -372,7 +559,7 @@ public func createPollController(context: AccountContext, peerId: PeerId, comple
if let focusOptionId = state.focusOptionId {
focusItemTag = CreatePollEntryTag.option(focusOptionId)
if focusOptionId == state.options.last?.id {
ensureVisibleItemTag = CreatePollEntryTag.addOption(focusOptionId)
ensureVisibleItemTag = nil
} else {
ensureVisibleItemTag = focusItemTag
}
@ -382,7 +569,7 @@ public func createPollController(context: AccountContext, peerId: PeerId, comple
}
let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(presentationData.strings.CreatePoll_Title), leftNavigationButton: leftNavigationButton, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: false)
let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: createPollControllerEntries(presentationData: presentationData, state: state, limitsConfiguration: limitsConfiguration), style: .blocks, focusItemTag: focusItemTag, ensureVisibleItemTag: ensureVisibleItemTag, animateChanges: previousIds != nil && previousIds != optionIds)
let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: createPollControllerEntries(presentationData: presentationData, state: state, limitsConfiguration: limitsConfiguration), style: .blocks, focusItemTag: focusItemTag, ensureVisibleItemTag: ensureVisibleItemTag, animateChanges: previousIds != nil)
return (controllerState, (listState, arguments))
}
@ -429,15 +616,6 @@ public func createPollController(context: AccountContext, peerId: PeerId, comple
var resultItemNode: ListViewItemNode?
let state = stateValue.with({ $0 })
if state.options.last?.id == id {
let _ = controller.frameForItemNode({ itemNode in
if let itemNode = itemNode as? ItemListItemNode {
if let tag = itemNode.tag, tag.isEqual(to: CreatePollEntryTag.addOption(id)) {
resultItemNode = itemNode as? ListViewItemNode
return true
}
}
return false
})
}
if resultItemNode == nil {
let _ = controller.frameForItemNode({ itemNode in
@ -456,19 +634,42 @@ public func createPollController(context: AccountContext, peerId: PeerId, comple
}
})
}
controller.setReorderEntry({ (fromIndex: Int, toIndex: Int, entries: [CreatePollEntry]) -> Void in
let fromEntry = entries[fromIndex]
guard case let .option(_, _, id, _, _, _, _, _) = fromEntry else {
displayQuizTooltipImpl = { [weak controller] in
guard let controller = controller else {
return
}
var resultItemNode: CreatePollOptionItemNode?
let _ = controller.frameForItemNode({ itemNode in
if resultItemNode == nil, let itemNode = itemNode as? CreatePollOptionItemNode {
resultItemNode = itemNode
return true
}
return false
})
if let resultItemNode = resultItemNode {
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let tooltipController = TooltipController(content: .text(presentationData.strings.CreatePoll_QuizTip), baseFontSize: presentationData.listsFontSize.baseDisplaySize, dismissByTapOutside: true)
controller.present(tooltipController, in: .window(.root), with: TooltipControllerPresentationArguments(sourceViewAndRect: { [weak resultItemNode] in
if let resultItemNode = resultItemNode {
return (resultItemNode.view, CGRect(origin: CGPoint(x: 0.0, y: 4.0), size: CGSize(width: 54.0, height: resultItemNode.bounds.height - 8.0)))
}
return nil
}))
}
}
controller.setReorderEntry({ (fromIndex: Int, toIndex: Int, entries: [CreatePollEntry]) -> Void in
let fromEntry = entries[fromIndex]
guard case let .option(option) = fromEntry else {
return
}
let id = option.id
var referenceId: Int?
var beforeAll = false
var afterAll = false
if toIndex < entries.count {
switch entries[toIndex] {
case let .option(_, _, toId, _, _, _, _, _):
referenceId = toId
case let .option(toOption):
referenceId = toOption.id
default:
if entries[toIndex] < fromEntry {
beforeAll = true

View File

@ -170,7 +170,7 @@ class CreatePollOptionActionItemNode: ListViewItemNode, ItemListItemNode {
strongSelf.iconNode.image = updatedIcon
}
if let image = strongSelf.iconNode.image {
transition.updateFrame(node: strongSelf.iconNode, frame: CGRect(origin: CGPoint(x: params.leftInset + floor((leftInset - params.leftInset - image.size.width) / 2.0 - 1.0), y: floor((contentSize.height - image.size.height) / 2.0)), size: image.size))
transition.updateFrame(node: strongSelf.iconNode, frame: CGRect(origin: CGPoint(x: params.leftInset + floor((leftInset - params.leftInset - image.size.width) / 2.0 - 3.0), y: floor((contentSize.height - image.size.height) / 2.0)), size: image.size))
}
if strongSelf.backgroundNode.supernode == nil {

View File

@ -6,6 +6,7 @@ import SwiftSignalKit
import TelegramPresentationData
import ItemListUI
import PresentationDataUtils
import CheckNode
struct CreatePollOptionItemEditing {
let editable: Bool
@ -13,11 +14,11 @@ struct CreatePollOptionItemEditing {
}
class CreatePollOptionItem: ListViewItem, ItemListItem {
let theme: PresentationTheme
let strings: PresentationStrings
let presentationData: ItemListPresentationData
let id: Int
let placeholder: String
let value: String
let isSelected: Bool?
let maxLength: Int
let editing: CreatePollOptionItemEditing
let sectionId: ItemListSectionId
@ -25,15 +26,17 @@ class CreatePollOptionItem: ListViewItem, ItemListItem {
let updated: (String) -> Void
let next: (() -> Void)?
let delete: (Bool) -> Void
let canDelete: Bool
let focused: () -> Void
let toggleSelected: () -> Void
let tag: ItemListItemTag?
init(theme: PresentationTheme, strings: PresentationStrings, id: Int, placeholder: String, value: String, maxLength: Int, editing: CreatePollOptionItemEditing, sectionId: ItemListSectionId, setItemIdWithRevealedOptions: @escaping (Int?, Int?) -> Void, updated: @escaping (String) -> Void, next: (() -> Void)?, delete: @escaping (Bool) -> Void, focused: @escaping () -> Void, tag: ItemListItemTag?) {
self.theme = theme
self.strings = strings
init(presentationData: ItemListPresentationData, id: Int, placeholder: String, value: String, isSelected: Bool?, maxLength: Int, editing: CreatePollOptionItemEditing, sectionId: ItemListSectionId, setItemIdWithRevealedOptions: @escaping (Int?, Int?) -> Void, updated: @escaping (String) -> Void, next: (() -> Void)?, delete: @escaping (Bool) -> Void, canDelete: Bool, focused: @escaping () -> Void, toggleSelected: @escaping () -> Void, tag: ItemListItemTag?) {
self.presentationData = presentationData
self.id = id
self.placeholder = placeholder
self.value = value
self.isSelected = isSelected
self.maxLength = maxLength
self.editing = editing
self.sectionId = sectionId
@ -41,7 +44,9 @@ class CreatePollOptionItem: ListViewItem, ItemListItem {
self.updated = updated
self.next = next
self.delete = delete
self.canDelete = canDelete
self.focused = focused
self.toggleSelected = toggleSelected
self.tag = tag
}
@ -55,7 +60,7 @@ class CreatePollOptionItem: ListViewItem, ItemListItem {
Queue.mainQueue().async {
completion(node, {
return (nil, { _ in apply() })
return (nil, { _ in apply(.None) })
})
}
}
@ -70,7 +75,7 @@ class CreatePollOptionItem: ListViewItem, ItemListItem {
let (layout, apply) = makeLayout(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
Queue.mainQueue().async {
completion(layout, { _ in
apply()
apply(animation)
})
}
}
@ -84,17 +89,19 @@ class CreatePollOptionItem: ListViewItem, ItemListItem {
private let titleFont = Font.regular(15.0)
class CreatePollOptionItemNode: ItemListRevealOptionsItemNode, ItemListItemNode, ItemListItemFocusableNode, ASEditableTextNodeDelegate {
private let containerNode: ASDisplayNode
private let backgroundNode: ASDisplayNode
private let topStripeNode: ASDisplayNode
private let bottomStripeNode: ASDisplayNode
private let maskNode: ASImageNode
private var checkNode: CheckNode?
private let textClippingNode: ASDisplayNode
private let textNode: EditableTextNode
private let measureTextNode: TextNode
private let textLimitNode: TextNode
private let editableControlNode: ItemListEditableControlNode
private let reorderControlNode: ItemListEditableReorderControlNode
private var item: CreatePollOptionItem?
@ -104,7 +111,14 @@ class CreatePollOptionItemNode: ItemListRevealOptionsItemNode, ItemListItemNode,
return self.item?.tag
}
override var controlsContainer: ASDisplayNode {
return self.containerNode
}
init() {
self.containerNode = ASDisplayNode()
self.containerNode.clipsToBounds = true
self.backgroundNode = ASDisplayNode()
self.backgroundNode.isLayerBacked = true
self.backgroundNode.backgroundColor = .white
@ -117,7 +131,6 @@ class CreatePollOptionItemNode: ItemListRevealOptionsItemNode, ItemListItemNode,
self.maskNode = ASImageNode()
self.editableControlNode = ItemListEditableControlNode()
self.reorderControlNode = ItemListEditableReorderControlNode()
self.textClippingNode = ASDisplayNode()
@ -131,21 +144,17 @@ class CreatePollOptionItemNode: ItemListRevealOptionsItemNode, ItemListItemNode,
super.init(layerBacked: false, dynamicBounce: false, rotated: false, seeThrough: false)
self.clipsToBounds = true
self.addSubnode(self.containerNode)
self.textClippingNode.addSubnode(self.textNode)
self.addSubnode(self.textClippingNode)
self.containerNode.addSubnode(self.textClippingNode)
self.addSubnode(self.editableControlNode)
self.addSubnode(self.reorderControlNode)
self.addSubnode(self.textLimitNode)
self.editableControlNode.tapped = { [weak self] in
if let strongSelf = self {
strongSelf.setRevealOptionsOpened(true, animated: true)
strongSelf.revealOptionsInteractivelyOpened()
}
}
self.containerNode.addSubnode(self.reorderControlNode)
self.containerNode.addSubnode(self.textLimitNode)
}
@objc private func checkNodePressed() {
self.item?.toggleSelected()
}
override func didLoad() {
@ -153,7 +162,7 @@ class CreatePollOptionItemNode: ItemListRevealOptionsItemNode, ItemListItemNode,
var textColor: UIColor = .black
if let item = self.item {
textColor = item.theme.list.itemPrimaryTextColor
textColor = item.presentationData.theme.list.itemPrimaryTextColor
}
self.textNode.typingAttributes = [NSAttributedString.Key.font.rawValue: Font.regular(17.0), NSAttributedString.Key.foregroundColor.rawValue: textColor]
self.textNode.clipsToBounds = true
@ -177,7 +186,7 @@ class CreatePollOptionItemNode: ItemListRevealOptionsItemNode, ItemListItemNode,
if updatedText.count == 1 {
updatedText = ""
}
let updatedAttributedText = NSAttributedString(string: updatedText, font: Font.regular(17.0), textColor: item.theme.list.itemPrimaryTextColor)
let updatedAttributedText = NSAttributedString(string: updatedText, font: Font.regular(17.0), textColor: item.presentationData.theme.list.itemPrimaryTextColor)
self.textNode.attributedText = updatedAttributedText
self.editableTextNodeDidUpdateText(editableTextNode)
}
@ -201,7 +210,7 @@ class CreatePollOptionItemNode: ItemListRevealOptionsItemNode, ItemListItemNode,
hadReturn = true
updatedText = updatedText.replacingOccurrences(of: "\n", with: " ")
}
let updatedAttributedText = NSAttributedString(string: updatedText, font: Font.regular(17.0), textColor: item.theme.list.itemPrimaryTextColor)
let updatedAttributedText = NSAttributedString(string: updatedText, font: Font.regular(17.0), textColor: item.presentationData.theme.list.itemPrimaryTextColor)
if text.string != updatedAttributedText.string {
self.textNode.attributedText = updatedAttributedText
}
@ -220,8 +229,7 @@ class CreatePollOptionItemNode: ItemListRevealOptionsItemNode, ItemListItemNode,
self.item?.delete(editableTextNode.isFirstResponder())
}
func asyncLayout() -> (_ item: CreatePollOptionItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) {
let editableControlLayout = ItemListEditableControlNode.asyncLayout(self.editableControlNode)
func asyncLayout() -> (_ item: CreatePollOptionItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, (ListViewItemUpdateAnimation) -> Void) {
let reorderControlLayout = ItemListEditableReorderControlNode.asyncLayout(self.reorderControlNode)
let makeTextLayout = TextNode.asyncLayout(self.measureTextNode)
let makeTextLimitLayout = TextNode.asyncLayout(self.textLimitNode)
@ -231,32 +239,31 @@ class CreatePollOptionItemNode: ItemListRevealOptionsItemNode, ItemListItemNode,
return { item, params, neighbors in
var updatedTheme: PresentationTheme?
if currentItem?.theme !== item.theme {
updatedTheme = item.theme
if currentItem?.presentationData.theme !== item.presentationData.theme {
updatedTheme = item.presentationData.theme
}
let controlSizeAndApply = editableControlLayout(item.theme, false)
let reorderSizeAndApply = reorderControlLayout(item.theme)
let reorderSizeAndApply = reorderControlLayout(item.presentationData.theme)
let separatorHeight = UIScreenPixel
let insets = itemListNeighborsGroupedInsets(neighbors)
let leftInset: CGFloat = 60.0 + params.leftInset
let leftInset: CGFloat = params.leftInset + (item.isSelected != nil ? 60.0 : 16.0)
let rightInset: CGFloat = 44.0 + params.rightInset
let textLength = item.value.count
let displayTextLimit = textLength > item.maxLength * 70 / 100
let remainingCount = item.maxLength - textLength
let (textLimitLayout, textLimitApply) = makeTextLimitLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: "\(remainingCount)", font: Font.regular(13.0), textColor: remainingCount < 0 ? item.theme.list.itemDestructiveColor : item.theme.list.itemSecondaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: 100.0, height: .greatestFiniteMagnitude), alignment: .left, lineSpacing: 0.0, cutout: nil, insets: UIEdgeInsets()))
let (textLimitLayout, textLimitApply) = makeTextLimitLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: "\(remainingCount)", font: Font.regular(13.0), textColor: remainingCount < 0 ? item.presentationData.theme.list.itemDestructiveColor : item.presentationData.theme.list.itemSecondaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: 100.0, height: .greatestFiniteMagnitude), alignment: .left, lineSpacing: 0.0, cutout: nil, insets: UIEdgeInsets()))
var measureText = item.value
if measureText.hasSuffix("\n") || measureText.isEmpty {
measureText += "|"
}
let attributedMeasureText = NSAttributedString(string: measureText, font: Font.regular(17.0), textColor: .black)
let attributedText = NSAttributedString(string: item.value, font: Font.regular(17.0), textColor: item.theme.list.itemPrimaryTextColor)
let attributedText = NSAttributedString(string: item.value, font: Font.regular(17.0), textColor: item.presentationData.theme.list.itemPrimaryTextColor)
let (textLayout, textApply) = makeTextLayout(TextNodeLayoutArguments(attributedString: attributedMeasureText, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - rightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, lineSpacing: 0.05, cutout: nil, insets: UIEdgeInsets()))
let textTopInset: CGFloat = 11.0
@ -265,21 +272,31 @@ class CreatePollOptionItemNode: ItemListRevealOptionsItemNode, ItemListItemNode,
let contentSize = CGSize(width: params.width, height: textLayout.size.height + textTopInset + textBottomInset)
let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets)
let attributedPlaceholderText = NSAttributedString(string: item.placeholder, font: Font.regular(17.0), textColor: item.theme.list.itemPlaceholderTextColor)
let attributedPlaceholderText = NSAttributedString(string: item.placeholder, font: Font.regular(17.0), textColor: item.presentationData.theme.list.itemPlaceholderTextColor)
return (layout, { [weak self] in
return (layout, { [weak self] animation in
if let strongSelf = self {
let transition: ContainedViewLayoutTransition
switch animation {
case .System:
transition = .animated(duration: 0.3, curve: .spring)
default:
transition = .immediate
}
strongSelf.item = item
strongSelf.layoutParams = params
strongSelf.containerNode.frame = CGRect(origin: CGPoint(), size: layout.contentSize)
if let _ = updatedTheme {
strongSelf.topStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor
strongSelf.bottomStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor
strongSelf.backgroundNode.backgroundColor = item.theme.list.itemBlocksBackgroundColor
strongSelf.topStripeNode.backgroundColor = item.presentationData.theme.list.itemBlocksSeparatorColor
strongSelf.bottomStripeNode.backgroundColor = item.presentationData.theme.list.itemBlocksSeparatorColor
strongSelf.backgroundNode.backgroundColor = item.presentationData.theme.list.itemBlocksBackgroundColor
if strongSelf.isNodeLoaded {
strongSelf.textNode.typingAttributes = [NSAttributedString.Key.font.rawValue: Font.regular(17.0), NSAttributedString.Key.foregroundColor.rawValue: item.theme.list.itemPrimaryTextColor]
strongSelf.textNode.tintColor = item.theme.list.itemAccentColor
strongSelf.textNode.typingAttributes = [NSAttributedString.Key.font.rawValue: Font.regular(17.0), NSAttributedString.Key.foregroundColor.rawValue: item.presentationData.theme.list.itemPrimaryTextColor]
strongSelf.textNode.tintColor = item.presentationData.theme.list.itemAccentColor
}
}
@ -325,54 +342,72 @@ class CreatePollOptionItemNode: ItemListRevealOptionsItemNode, ItemListItemNode,
strongSelf.textNode.attributedPlaceholderText = attributedPlaceholderText
}
strongSelf.textNode.keyboardAppearance = item.theme.rootController.keyboardColor.keyboardAppearance
strongSelf.textNode.keyboardAppearance = item.presentationData.theme.rootController.keyboardColor.keyboardAppearance
strongSelf.textClippingNode.frame = CGRect(origin: CGPoint(x: revealOffset + leftInset, y: textTopInset), size: CGSize(width: params.width - leftInset - params.rightInset, height: textLayout.size.height))
strongSelf.textNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: params.width - leftInset - rightInset, height: textLayout.size.height + 1.0))
let checkSize = CGSize(width: 32.0, height: 32.0)
let checkFrame = CGRect(origin: CGPoint(x: params.leftInset + revealOffset + 11.0, y: floor((layout.contentSize.height - checkSize.height) / 2.0)), size: checkSize)
if let isSelected = item.isSelected {
if let checkNode = strongSelf.checkNode {
transition.updateFrame(node: checkNode, frame: checkFrame)
checkNode.setIsChecked(isSelected, animated: true)
} else {
let checkNode = CheckNode(strokeColor: item.presentationData.theme.list.itemCheckColors.strokeColor, fillColor: item.presentationData.theme.list.itemSwitchColors.positiveColor, foregroundColor: item.presentationData.theme.list.itemCheckColors.foregroundColor, style: .plain)
checkNode.addTarget(target: strongSelf, action: #selector(strongSelf.checkNodePressed))
checkNode.setIsChecked(isSelected, animated: false)
strongSelf.checkNode = checkNode
strongSelf.containerNode.addSubnode(checkNode)
checkNode.frame = checkFrame
transition.animatePositionAdditive(node: checkNode, offset: CGPoint(x: -checkFrame.maxX, y: 0.0))
}
} else if let checkNode = strongSelf.checkNode {
strongSelf.checkNode = nil
transition.updateFrame(node: checkNode, frame: checkFrame.offsetBy(dx: -checkFrame.maxX, dy: 0.0), completion: { [weak checkNode] _ in
checkNode?.removeFromSupernode()
})
}
transition.updateFrame(node: strongSelf.textClippingNode, frame: CGRect(origin: CGPoint(x: revealOffset + leftInset, y: textTopInset), size: CGSize(width: params.width - leftInset - params.rightInset, height: textLayout.size.height)))
transition.updateFrame(node: strongSelf.textNode, frame: CGRect(origin: CGPoint(), size: CGSize(width: params.width - leftInset - rightInset, height: textLayout.size.height + 1.0)))
if strongSelf.backgroundNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.backgroundNode, at: 0)
strongSelf.containerNode.insertSubnode(strongSelf.backgroundNode, at: 0)
}
if strongSelf.topStripeNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.topStripeNode, at: 1)
strongSelf.containerNode.insertSubnode(strongSelf.topStripeNode, at: 1)
}
if strongSelf.bottomStripeNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 2)
strongSelf.containerNode.insertSubnode(strongSelf.bottomStripeNode, at: 2)
}
if strongSelf.maskNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.maskNode, at: 3)
strongSelf.containerNode.insertSubnode(strongSelf.maskNode, at: 3)
}
let hasCorners = itemListHasRoundedBlockLayout(params)
var hasTopCorners = false
var hasBottomCorners = false
switch neighbors.top {
case .sameSection(false):
strongSelf.topStripeNode.isHidden = true
default:
hasTopCorners = true
strongSelf.topStripeNode.isHidden = hasCorners
case .sameSection(false):
strongSelf.topStripeNode.isHidden = true
default:
hasTopCorners = true
strongSelf.topStripeNode.isHidden = hasCorners
}
let bottomStripeInset: CGFloat
switch neighbors.bottom {
case .sameSection(false):
bottomStripeInset = leftInset
default:
bottomStripeInset = 0.0
hasBottomCorners = true
strongSelf.bottomStripeNode.isHidden = hasCorners
case .sameSection(false):
bottomStripeInset = leftInset
default:
bottomStripeInset = 0.0
hasBottomCorners = true
strongSelf.bottomStripeNode.isHidden = hasCorners
}
strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.theme, top: hasTopCorners, bottom: hasBottomCorners) : nil
strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.presentationData.theme, top: hasTopCorners, bottom: hasBottomCorners) : nil
strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight)))
strongSelf.maskNode.frame = strongSelf.backgroundNode.frame.insetBy(dx: params.leftInset, dy: 0.0)
strongSelf.topStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: layout.contentSize.width, height: separatorHeight))
strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height - UIScreenPixel), size: CGSize(width: layout.contentSize.width - bottomStripeInset, height: separatorHeight))
let _ = controlSizeAndApply.1(layout.contentSize.height)
let editableControlFrame = CGRect(origin: CGPoint(x: params.leftInset + 6.0 + revealOffset, y: 0.0), size: CGSize(width: controlSizeAndApply.0, height: contentSize.height))
strongSelf.editableControlNode.frame = editableControlFrame
transition.updateFrame(node: strongSelf.bottomStripeNode, frame: CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height - UIScreenPixel), size: CGSize(width: layout.contentSize.width - bottomStripeInset, height: separatorHeight)))
let _ = reorderSizeAndApply.1(layout.contentSize.height, displayTextLimit && layout.contentSize.height <= 44.0)
let reorderControlFrame = CGRect(origin: CGPoint(x: params.width + revealOffset - params.rightInset - reorderSizeAndApply.0, y: 0.0), size: CGSize(width: reorderSizeAndApply.0, height: layout.contentSize.height))
@ -384,7 +419,7 @@ class CreatePollOptionItemNode: ItemListRevealOptionsItemNode, ItemListItemNode,
strongSelf.updateLayout(size: layout.contentSize, leftInset: params.leftInset, rightInset: params.rightInset)
strongSelf.setRevealOptions((left: [], right: [ItemListRevealOption(key: 0, title: item.strings.Common_Delete, icon: .none, color: item.theme.list.itemDisclosureActions.destructive.fillColor, textColor: item.theme.list.itemDisclosureActions.destructive.foregroundColor)]))
strongSelf.setRevealOptions((left: [], right: item.canDelete ? [ItemListRevealOption(key: 0, title: item.presentationData.strings.Common_Delete, icon: .none, color: item.presentationData.theme.list.itemDisclosureActions.destructive.fillColor, textColor: item.presentationData.theme.list.itemDisclosureActions.destructive.foregroundColor)] : []))
}
})
}
@ -393,18 +428,20 @@ class CreatePollOptionItemNode: ItemListRevealOptionsItemNode, ItemListItemNode,
override func updateRevealOffset(offset: CGFloat, transition: ContainedViewLayoutTransition) {
super.updateRevealOffset(offset: offset, transition: transition)
guard let params = self.layoutParams else {
guard let params = self.layoutParams, let item = self.item else {
return
}
let revealOffset = offset
let leftInset: CGFloat
leftInset = 60.0 + params.leftInset
leftInset = params.leftInset + (item.isSelected != nil ? 60.0 : 16.0)
var controlFrame = self.editableControlNode.frame
controlFrame.origin.x = params.leftInset + 6.0 + revealOffset
transition.updateFrame(node: self.editableControlNode, frame: controlFrame)
if let checkNode = self.checkNode {
var checkNodeFrame = checkNode.frame
checkNodeFrame.origin.x = params.leftInset + 11.0 + revealOffset
transition.updateFrame(node: checkNode, frame: checkNodeFrame)
}
var reorderFrame = self.reorderControlNode.frame
reorderFrame.origin.x = params.width + revealOffset - params.rightInset - reorderFrame.width
@ -448,5 +485,7 @@ class CreatePollOptionItemNode: ItemListRevealOptionsItemNode, ItemListItemNode,
var separatorFrame = self.bottomStripeNode.frame
separatorFrame.origin.y = currentValue - UIScreenPixel
self.bottomStripeNode.frame = separatorFrame
self.containerNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: self.containerNode.bounds.width, height: currentValue))
}
}

View File

@ -2011,7 +2011,11 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture
node.addHeightAnimation(0.0, duration: insertionAnimationDuration * UIView.animationDurationFactor(), beginAt: timestamp)
}
if node.animationForKey("apparentHeight") == nil || !(node is ListViewTempItemNode) {
node.addApparentHeightAnimation(0.0, duration: insertionAnimationDuration * UIView.animationDurationFactor(), beginAt: timestamp)
node.addApparentHeightAnimation(0.0, duration: insertionAnimationDuration * UIView.animationDurationFactor(), beginAt: timestamp, update: { [weak node] progress, currentValue in
if let node = node {
node.animateFrameTransition(progress, currentValue)
}
})
}
node.animateRemoved(timestamp, duration: insertionAnimationDuration * UIView.animationDurationFactor())
} else if animated {
@ -3007,21 +3011,25 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture
let flashing = self.headerItemsAreFlashing()
let addHeader: (_ id: Int64, _ upperBound: CGFloat, _ lowerBound: CGFloat, _ item: ListViewItemHeader, _ hasValidNodes: Bool) -> Void = { id, upperBound, lowerBound, item, hasValidNodes in
func addHeader(id: Int64, upperBound: CGFloat, upperBoundEdge: CGFloat, lowerBound: CGFloat, item: ListViewItemHeader, hasValidNodes: Bool) {
let itemHeaderHeight: CGFloat = item.height
let headerFrame: CGRect
let stickLocationDistanceFactor: CGFloat
let stickLocationDistance: CGFloat
switch item.stickDirection {
case .top:
headerFrame = CGRect(origin: CGPoint(x: 0.0, y: min(max(upperDisplayBound, upperBound), lowerBound - itemHeaderHeight)), size: CGSize(width: self.visibleSize.width, height: itemHeaderHeight))
stickLocationDistance = headerFrame.minY - upperBound
stickLocationDistanceFactor = max(0.0, min(1.0, stickLocationDistance / itemHeaderHeight))
case .bottom:
headerFrame = CGRect(origin: CGPoint(x: 0.0, y: max(upperBound, min(lowerBound, lowerDisplayBound) - itemHeaderHeight)), size: CGSize(width: self.visibleSize.width, height: itemHeaderHeight))
stickLocationDistance = lowerBound - headerFrame.maxY
stickLocationDistanceFactor = max(0.0, min(1.0, stickLocationDistance / itemHeaderHeight))
case .top:
headerFrame = CGRect(origin: CGPoint(x: 0.0, y: min(max(upperDisplayBound, upperBound), lowerBound - itemHeaderHeight)), size: CGSize(width: self.visibleSize.width, height: itemHeaderHeight))
stickLocationDistance = headerFrame.minY - upperBound
stickLocationDistanceFactor = max(0.0, min(1.0, stickLocationDistance / itemHeaderHeight))
case .topEdge:
headerFrame = CGRect(origin: CGPoint(x: 0.0, y: min(max(upperDisplayBound, upperBoundEdge - itemHeaderHeight), lowerBound - itemHeaderHeight)), size: CGSize(width: self.visibleSize.width, height: itemHeaderHeight))
stickLocationDistance = headerFrame.minY - upperBoundEdge - itemHeaderHeight
stickLocationDistanceFactor = max(0.0, min(1.0, stickLocationDistance / itemHeaderHeight))
case .bottom:
headerFrame = CGRect(origin: CGPoint(x: 0.0, y: max(upperBound, min(lowerBound, lowerDisplayBound) - itemHeaderHeight)), size: CGSize(width: self.visibleSize.width, height: itemHeaderHeight))
stickLocationDistance = lowerBound - headerFrame.maxY
stickLocationDistanceFactor = max(0.0, min(1.0, stickLocationDistance / itemHeaderHeight))
}
visibleHeaderNodes.insert(id)
if let headerNode = self.itemHeaderNodes[id] {
@ -3092,31 +3100,32 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture
}
}
var previousHeader: (Int64, CGFloat, CGFloat, ListViewItemHeader, Bool)?
var previousHeader: (id: Int64, upperBound: CGFloat, upperBoundEdge: CGFloat, lowerBound: CGFloat, item: ListViewItemHeader, hasValidNodes: Bool)?
for itemNode in self.itemNodes {
let itemFrame = itemNode.apparentFrame
let itemTopInset = itemNode.insets.top
if let itemHeader = itemNode.header() {
if let (previousHeaderId, previousUpperBound, previousLowerBound, previousHeaderItem, hasValidNodes) = previousHeader {
if let (previousHeaderId, previousUpperBound, previousUpperBoundEdge, previousLowerBound, previousHeaderItem, hasValidNodes) = previousHeader {
if previousHeaderId == itemHeader.id {
previousHeader = (previousHeaderId, previousUpperBound, itemFrame.maxY, previousHeaderItem, hasValidNodes || itemNode.index != nil)
previousHeader = (previousHeaderId, previousUpperBound, previousUpperBoundEdge, itemFrame.maxY, previousHeaderItem, hasValidNodes || itemNode.index != nil)
} else {
addHeader(previousHeaderId, previousUpperBound, previousLowerBound, previousHeaderItem, hasValidNodes)
addHeader(id: previousHeaderId, upperBound: previousUpperBound, upperBoundEdge: previousUpperBoundEdge, lowerBound: previousLowerBound, item: previousHeaderItem, hasValidNodes: hasValidNodes)
previousHeader = (itemHeader.id, itemFrame.minY, itemFrame.maxY, itemHeader, itemNode.index != nil)
previousHeader = (itemHeader.id, itemFrame.minY, itemFrame.minY + itemTopInset, itemFrame.maxY, itemHeader, itemNode.index != nil)
}
} else {
previousHeader = (itemHeader.id, itemFrame.minY, itemFrame.maxY, itemHeader, itemNode.index != nil)
previousHeader = (itemHeader.id, itemFrame.minY, itemFrame.minY + itemTopInset, itemFrame.maxY, itemHeader, itemNode.index != nil)
}
} else {
if let (previousHeaderId, previousUpperBound, previousLowerBound, previousHeaderItem, hasValidNodes) = previousHeader {
addHeader(previousHeaderId, previousUpperBound, previousLowerBound, previousHeaderItem, hasValidNodes)
if let (previousHeaderId, previousUpperBound, previousUpperBoundEdge, previousLowerBound, previousHeaderItem, hasValidNodes) = previousHeader {
addHeader(id: previousHeaderId, upperBound: previousUpperBound, upperBoundEdge: previousUpperBoundEdge, lowerBound: previousLowerBound, item: previousHeaderItem, hasValidNodes: hasValidNodes)
}
previousHeader = nil
}
}
if let (previousHeaderId, previousUpperBound, previousLowerBound, previousHeaderItem, hasValidNodes) = previousHeader {
addHeader(previousHeaderId, previousUpperBound, previousLowerBound, previousHeaderItem, hasValidNodes)
if let (previousHeaderId, previousUpperBound, previousUpperBoundEdge, previousLowerBound, previousHeaderItem, hasValidNodes) = previousHeader {
addHeader(id: previousHeaderId, upperBound: previousUpperBound, upperBoundEdge: previousUpperBoundEdge, lowerBound: previousLowerBound, item: previousHeaderItem, hasValidNodes: hasValidNodes)
}
let currentIds = Set(self.itemHeaderNodes.keys)

View File

@ -4,6 +4,7 @@ import AsyncDisplayKit
public enum ListViewItemHeaderStickDirection {
case top
case topEdge
case bottom
}

View File

@ -150,15 +150,15 @@ class ItemListPeerActionItemNode: ListViewItemNode {
updatedTheme = item.presentationData.theme
}
let leftInset: CGFloat
let vertcalInset: CGFloat
let verticalInset: CGFloat
let verticalOffset: CGFloat
switch item.height {
case .generic:
vertcalInset = 11.0
verticalInset = 11.0
verticalOffset = 0.0
leftInset = 59.0 + params.leftInset
case .peerList:
vertcalInset = 14.0
verticalInset = 14.0
verticalOffset = 0.0
leftInset = 65.0 + params.leftInset
}
@ -170,7 +170,7 @@ class ItemListPeerActionItemNode: ListViewItemNode {
let separatorHeight = UIScreenPixel
let insets = itemListNeighborsGroupedInsets(neighbors)
let contentSize = CGSize(width: params.width, height: titleLayout.size.height + vertcalInset * 2.0)
let contentSize = CGSize(width: params.width, height: titleLayout.size.height + verticalInset * 2.0)
let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets)
let layoutSize = layout.size
@ -247,7 +247,7 @@ class ItemListPeerActionItemNode: ListViewItemNode {
strongSelf.topStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: layoutSize.width, height: separatorHeight))
transition.updateFrame(node: strongSelf.bottomStripeNode, frame: CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height + bottomStripeOffset), size: CGSize(width: layoutSize.width - bottomStripeInset, height: separatorHeight)))
transition.updateFrame(node: strongSelf.titleNode, frame: CGRect(origin: CGPoint(x: leftInset + editingOffset, y: vertcalInset + verticalOffset), size: titleLayout.size))
transition.updateFrame(node: strongSelf.titleNode, frame: CGRect(origin: CGPoint(x: leftInset + editingOffset, y: verticalInset + verticalOffset), size: titleLayout.size))
strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: params.width, height: layout.contentSize.height + UIScreenPixel + UIScreenPixel))
}

View File

@ -135,8 +135,9 @@ public final class ItemListPeerItem: ListViewItem, ItemListItem {
let hasTopGroupInset: Bool
let noInsets: Bool
public let tag: ItemListItemTag?
let header: ListViewItemHeader?
public init(presentationData: ItemListPresentationData, dateTimeFormat: PresentationDateTimeFormat, nameDisplayOrder: PresentationPersonNameOrder, context: AccountContext, peer: Peer, height: ItemListPeerItemHeight = .peerList, aliasHandling: ItemListPeerItemAliasHandling = .standard, nameColor: ItemListPeerItemNameColor = .primary, nameStyle: ItemListPeerItemNameStyle = .distinctBold, presence: PeerPresence?, text: ItemListPeerItemText, label: ItemListPeerItemLabel, editing: ItemListPeerItemEditing, revealOptions: ItemListPeerItemRevealOptions? = nil, switchValue: ItemListPeerItemSwitch?, enabled: Bool, selectable: Bool, sectionId: ItemListSectionId, action: (() -> Void)?, setPeerIdWithRevealedOptions: @escaping (PeerId?, PeerId?) -> Void, removePeer: @escaping (PeerId) -> Void, toggleUpdated: ((Bool) -> Void)? = nil, contextAction: ((ASDisplayNode, ContextGesture?) -> Void)? = nil, hasTopStripe: Bool = true, hasTopGroupInset: Bool = true, noInsets: Bool = false, tag: ItemListItemTag? = nil) {
public init(presentationData: ItemListPresentationData, dateTimeFormat: PresentationDateTimeFormat, nameDisplayOrder: PresentationPersonNameOrder, context: AccountContext, peer: Peer, height: ItemListPeerItemHeight = .peerList, aliasHandling: ItemListPeerItemAliasHandling = .standard, nameColor: ItemListPeerItemNameColor = .primary, nameStyle: ItemListPeerItemNameStyle = .distinctBold, presence: PeerPresence?, text: ItemListPeerItemText, label: ItemListPeerItemLabel, editing: ItemListPeerItemEditing, revealOptions: ItemListPeerItemRevealOptions? = nil, switchValue: ItemListPeerItemSwitch?, enabled: Bool, selectable: Bool, sectionId: ItemListSectionId, action: (() -> Void)?, setPeerIdWithRevealedOptions: @escaping (PeerId?, PeerId?) -> Void, removePeer: @escaping (PeerId) -> Void, toggleUpdated: ((Bool) -> Void)? = nil, contextAction: ((ASDisplayNode, ContextGesture?) -> Void)? = nil, hasTopStripe: Bool = true, hasTopGroupInset: Bool = true, noInsets: Bool = false, tag: ItemListItemTag? = nil, header: ListViewItemHeader? = nil) {
self.presentationData = presentationData
self.dateTimeFormat = dateTimeFormat
self.nameDisplayOrder = nameDisplayOrder
@ -164,12 +165,13 @@ public final class ItemListPeerItem: ListViewItem, ItemListItem {
self.hasTopGroupInset = hasTopGroupInset
self.noInsets = noInsets
self.tag = tag
self.header = header
}
public func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) {
async {
let node = ItemListPeerItemNode()
let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem), self.getHeaderAtTop(top: previousItem, bottom: nextItem))
node.contentSize = layout.contentSize
node.insets = layout.insets
@ -182,6 +184,19 @@ public final class ItemListPeerItem: ListViewItem, ItemListItem {
}
}
private func getHeaderAtTop(top: ListViewItem?, bottom: ListViewItem?) -> Bool {
var headerAtTop = false
if let top = top as? ItemListPeerItem, top.header != nil {
if top.header?.id != self.header?.id {
headerAtTop = true
}
} else if self.header != nil {
headerAtTop = true
}
return headerAtTop
}
public func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) {
Queue.mainQueue().async {
if let nodeValue = node() as? ItemListPeerItemNode {
@ -193,7 +208,7 @@ public final class ItemListPeerItem: ListViewItem, ItemListItem {
}
async {
let (layout, apply) = makeLayout(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
let (layout, apply) = makeLayout(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem), self.getHeaderAtTop(top: previousItem, bottom: nextItem))
Queue.mainQueue().async {
completion(layout, { _ in
apply(false, animated)
@ -233,7 +248,7 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo
private var checkNode: ASImageNode?
private var peerPresenceManager: PeerPresenceStatusManager?
private var layoutParams: (ItemListPeerItem, ListViewItemLayoutParams, ItemListNeighbors)?
private var layoutParams: (ItemListPeerItem, ListViewItemLayoutParams, ItemListNeighbors, Bool)?
private var editableControlNode: ItemListEditableControlNode?
@ -303,7 +318,7 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo
self.peerPresenceManager = PeerPresenceStatusManager(update: { [weak self] in
if let strongSelf = self, let layoutParams = strongSelf.layoutParams {
let (_, apply) = strongSelf.asyncLayout()(layoutParams.0, layoutParams.1, layoutParams.2)
let (_, apply) = strongSelf.asyncLayout()(layoutParams.0, layoutParams.1, layoutParams.2, layoutParams.3)
apply(false, true)
}
})
@ -317,7 +332,7 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo
}
}
public func asyncLayout() -> (_ item: ItemListPeerItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, (Bool, Bool) -> Void) {
public func asyncLayout() -> (_ item: ItemListPeerItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors, _ headerAtTop: Bool) -> (ListViewItemNodeLayout, (Bool, Bool) -> Void) {
let makeTitleLayout = TextNode.asyncLayout(self.titleNode)
let makeStatusLayout = TextNode.asyncLayout(self.statusNode)
let makeLabelLayout = TextNode.asyncLayout(self.labelNode)
@ -334,7 +349,7 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo
let currentHasBadge = self.labelBadgeNode.image != nil
return { item, params, neighbors in
return { item, params, neighbors, headerAtTop in
var updateArrowImage: UIImage?
var updatedTheme: PresentationTheme?
@ -579,8 +594,11 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo
insets.top = 0.0
insets.bottom = 0.0
}
if headerAtTop, let header = item.header {
insets.top += header.height + 18.0
}
let titleSpacing: CGFloat = 1.0
let titleSpacing: CGFloat = statusLayout.size.height == 0.0 ? 0.0 : 1.0
let minHeight: CGFloat = titleLayout.size.height + verticalInset * 2.0
let rawHeight: CGFloat = verticalInset * 2.0 + titleLayout.size.height + titleSpacing + statusLayout.size.height
@ -602,7 +620,7 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo
return (layout, { [weak self] synchronousLoad, animated in
if let strongSelf = self {
strongSelf.layoutParams = (item, params, neighbors)
strongSelf.layoutParams = (item, params, neighbors, headerAtTop)
strongSelf.containerNode.frame = CGRect(origin: CGPoint(), size: layout.contentSize)
strongSelf.containerNode.isGestureEnabled = item.contextAction != nil
@ -940,13 +958,13 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo
}
override public func revealOptionsInteractivelyOpened() {
if let (item, _, _) = self.layoutParams {
if let (item, _, _, _) = self.layoutParams {
item.setPeerIdWithRevealedOptions(item.peer.id, nil)
}
}
override public func revealOptionsInteractivelyClosed() {
if let (item, _, _) = self.layoutParams {
if let (item, _, _, _) = self.layoutParams {
item.setPeerIdWithRevealedOptions(nil, item.peer.id)
}
}
@ -955,7 +973,7 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo
self.setRevealOptionsOpened(false, animated: true)
self.revealOptionsInteractivelyClosed()
if let (item, _, _) = self.layoutParams {
if let (item, _, _, _) = self.layoutParams {
if let revealOptions = item.revealOptions {
if option.key >= 0 && option.key < Int32(revealOptions.options.count) {
revealOptions.options[Int(option.key)].action()
@ -967,8 +985,156 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo
}
private func toggleUpdated(_ value: Bool) {
if let (item, _, _) = self.layoutParams {
if let (item, _, _, _) = self.layoutParams {
item.toggleUpdated?(value)
}
}
override public func header() -> ListViewItemHeader? {
return self.layoutParams?.0.header
}
}
public final class ItemListPeerItemHeader: ListViewItemHeader {
public let id: Int64
public let text: String
public let stickDirection: ListViewItemHeaderStickDirection = .topEdge
public let theme: PresentationTheme
public let strings: PresentationStrings
public let actionTitle: String?
public let action: (() -> Void)?
public let height: CGFloat = 28.0
public init(theme: PresentationTheme, strings: PresentationStrings, text: String, actionTitle: String? = nil, id: Int64, action: (() -> Void)? = nil) {
self.text = text
self.id = id
self.theme = theme
self.strings = strings
self.actionTitle = actionTitle
self.action = action
}
public func node() -> ListViewItemHeaderNode {
return ItemListPeerItemHeaderNode(theme: self.theme, strings: self.strings, text: self.text, actionTitle: self.actionTitle, action: self.action)
}
public func updateNode(_ node: ListViewItemHeaderNode, previous: ListViewItemHeader?, next: ListViewItemHeader?) {
(node as? ItemListPeerItemHeaderNode)?.update(text: self.text, actionTitle: self.actionTitle, action: self.action)
}
}
public final class ItemListPeerItemHeaderNode: ListViewItemHeaderNode {
private var theme: PresentationTheme
private var strings: PresentationStrings
private var actionTitle: String?
private var action: (() -> Void)?
private var validLayout: (size: CGSize, leftInset: CGFloat, rightInset: CGFloat)?
private let backgroundNode: ASDisplayNode
private let separatorNode: ASDisplayNode
private let textNode: ImmediateTextNode
private let actionTextNode: ImmediateTextNode
private let actionButton: HighlightableButtonNode
private var stickDistanceFactor: CGFloat = 0.0
public init(theme: PresentationTheme, strings: PresentationStrings, text: String, actionTitle: String?, action: (() -> Void)?) {
self.theme = theme
self.strings = strings
self.actionTitle = actionTitle
self.action = action
self.backgroundNode = ASDisplayNode()
self.backgroundNode.backgroundColor = theme.list.blocksBackgroundColor
self.separatorNode = ASDisplayNode()
self.separatorNode.backgroundColor = theme.list.itemBlocksSeparatorColor
let titleFont = Font.regular(13.0)
self.textNode = ImmediateTextNode()
self.textNode.displaysAsynchronously = false
self.textNode.maximumNumberOfLines = 1
self.textNode.attributedText = NSAttributedString(string: text, font: titleFont, textColor: theme.list.sectionHeaderTextColor)
self.actionTextNode = ImmediateTextNode()
self.actionTextNode.displaysAsynchronously = false
self.actionTextNode.maximumNumberOfLines = 1
self.actionTextNode.attributedText = NSAttributedString(string: actionTitle ?? "", font: titleFont, textColor: action == nil ? theme.list.sectionHeaderTextColor : theme.list.itemAccentColor)
self.actionButton = HighlightableButtonNode()
self.actionButton.isUserInteractionEnabled = self.action != nil
super.init()
self.addSubnode(self.backgroundNode)
self.addSubnode(self.separatorNode)
self.addSubnode(self.textNode)
self.addSubnode(self.actionTextNode)
self.addSubnode(self.actionButton)
self.actionButton.addTarget(self, action: #selector(self.actionButtonPressed), forControlEvents: .touchUpInside)
self.actionButton.highligthedChanged = { [weak self] highlighted in
if let strongSelf = self {
if highlighted {
strongSelf.actionTextNode.layer.removeAnimation(forKey: "opacity")
strongSelf.actionTextNode.alpha = 0.4
} else {
strongSelf.actionTextNode.alpha = 1.0
strongSelf.actionTextNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2)
}
}
}
}
@objc private func actionButtonPressed() {
self.action?()
}
public func updateTheme(theme: PresentationTheme) {
self.theme = theme
let titleFont = Font.regular(13.0)
self.textNode.attributedText = NSAttributedString(string: self.textNode.attributedText?.string ?? "", font: titleFont, textColor: theme.list.sectionHeaderTextColor)
self.actionTextNode.attributedText = NSAttributedString(string: self.actionTextNode.attributedText?.string ?? "", font: titleFont, textColor: theme.list.sectionHeaderTextColor)
}
public func update(text: String, actionTitle: String?, action: (() -> Void)?) {
self.actionTitle = actionTitle
self.action = action
let titleFont = Font.regular(13.0)
self.textNode.attributedText = NSAttributedString(string: text, font: titleFont, textColor: theme.list.sectionHeaderTextColor)
self.actionTextNode.attributedText = NSAttributedString(string: actionTitle ?? "", font: titleFont, textColor: action == nil ? theme.list.sectionHeaderTextColor : theme.list.itemAccentColor)
self.actionButton.isUserInteractionEnabled = self.action != nil
if let (size, leftInset, rightInset) = self.validLayout {
self.updateLayout(size: size, leftInset: leftInset, rightInset: rightInset)
}
}
override public func updateLayout(size: CGSize, leftInset: CGFloat, rightInset: CGFloat) {
self.validLayout = (size, leftInset, rightInset)
self.backgroundNode.frame = CGRect(origin: CGPoint(), size: size)
self.separatorNode.frame = CGRect(origin: CGPoint(x: 0.0, y: size.height - UIScreenPixel), size: CGSize(width: size.width, height: UIScreenPixel))
let sideInset: CGFloat = 15.0 + leftInset
let actionTextSize = self.actionTextNode.updateLayout(CGSize(width: size.width - sideInset * 2.0, height: size.height))
let textSize = self.textNode.updateLayout(CGSize(width: size.width - sideInset * 2.0 - actionTextSize.width - 8.0, height: size.height))
self.textNode.frame = CGRect(origin: CGPoint(x: sideInset, y: 7.0), size: textSize)
self.actionTextNode.frame = CGRect(origin: CGPoint(x: size.width - sideInset - actionTextSize.width, y: 7.0), size: actionTextSize)
self.actionButton.frame = CGRect(origin: CGPoint(x: size.width - sideInset - actionTextSize.width, y: 0.0), size: CGSize(width: actionTextSize.width, height: size.height))
}
override public func animateRemoved(duration: Double) {
self.alpha = 0.0
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration, removeOnCompletion: true)
}
override public func updateStickDistanceFactor(_ factor: CGFloat, transition: ContainedViewLayoutTransition) {
//transition.updateAlpha(node: self.separatorNode, alpha: (1.0 - factor) * 0.0 + factor * 1.0)
}
}

View File

@ -501,9 +501,12 @@ open class ItemListControllerNode: ASDisplayNode, UIScrollViewDelegate {
options.insert(.PreferSynchronousDrawing)
options.insert(.AnimateAlpha)
} else if transition.crossfade {
options.insert(.PreferSynchronousResourceLoading)
options.insert(.PreferSynchronousDrawing)
options.insert(.AnimateCrossfade)
} else {
options.insert(.Synchronous)
options.insert(.PreferSynchronousResourceLoading)
options.insert(.PreferSynchronousDrawing)
}
if self.alwaysSynchronous {

View File

@ -82,6 +82,10 @@ open class ItemListRevealOptionsItemNode: ListViewItemNode, UIGestureRecognizerD
super.init(layerBacked: layerBacked, dynamicBounce: dynamicBounce, rotated: rotated, seeThrough: seeThrough)
}
open var controlsContainer: ASDisplayNode {
return self
}
override open func didLoad() {
super.didLoad()
@ -310,7 +314,7 @@ open class ItemListRevealOptionsItemNode: ListViewItemNode, UIGestureRecognizerD
revealNode.updateRevealOffset(offset: 0.0, sideInset: leftInset, transition: .immediate)
}
self.addSubnode(revealNode)
self.controlsContainer.addSubnode(revealNode)
}
}
@ -332,7 +336,7 @@ open class ItemListRevealOptionsItemNode: ListViewItemNode, UIGestureRecognizerD
revealNode.updateRevealOffset(offset: 0.0, sideInset: -rightInset, transition: .immediate)
}
self.addSubnode(revealNode)
self.controlsContainer.addSubnode(revealNode)
}
}
@ -492,4 +496,8 @@ open class ItemListRevealOptionsItemNode: ListViewItemNode, UIGestureRecognizerD
}
self.hapticFeedback?.impact(.medium)
}
override open func animateFrameTransition(_ progress: CGFloat, _ currentValue: CGFloat) {
super.animateFrameTransition(progress, currentValue)
}
}

View File

@ -9,6 +9,7 @@ import Markdown
public enum ItemListTextItemText {
case plain(String)
case large(String)
case markdown(String)
}
@ -105,15 +106,19 @@ public class ItemListTextItemNode: ListViewItemNode {
return { item, params, neighbors in
let leftInset: CGFloat = 15.0 + params.leftInset
let verticalInset: CGFloat = 7.0
let topInset: CGFloat = 7.0
var bottomInset: CGFloat = 7.0
let titleFont = Font.regular(item.presentationData.fontSize.itemListBaseHeaderFontSize)
let largeTitleFont = Font.semibold(floor(item.presentationData.fontSize.itemListBaseHeaderFontSize * 2.0))
let titleBoldFont = Font.semibold(item.presentationData.fontSize.itemListBaseHeaderFontSize)
let attributedText: NSAttributedString
switch item.text {
case let .plain(text):
attributedText = NSAttributedString(string: text, font: titleFont, textColor: item.presentationData.theme.list.freeTextColor)
case let .large(text):
attributedText = NSAttributedString(string: text, font: largeTitleFont, textColor: item.presentationData.theme.list.itemPrimaryTextColor)
case let .markdown(text):
attributedText = parseMarkdownIntoAttributedString(text, attributes: MarkdownAttributes(body: MarkdownAttributeSet(font: titleFont, textColor: item.presentationData.theme.list.freeTextColor), bold: MarkdownAttributeSet(font: titleBoldFont, textColor: item.presentationData.theme.list.freeTextColor), link: MarkdownAttributeSet(font: titleFont, textColor: item.presentationData.theme.list.itemAccentColor), linkAttribute: { contents in
return (TelegramTextAttributes.URL, contents)
@ -123,8 +128,12 @@ public class ItemListTextItemNode: ListViewItemNode {
let contentSize: CGSize
contentSize = CGSize(width: params.width, height: titleLayout.size.height + verticalInset + verticalInset)
let insets = itemListNeighborsGroupedInsets(neighbors)
var insets = itemListNeighborsGroupedInsets(neighbors)
if case .large = item.text {
insets.top = 14.0
bottomInset = -6.0
}
contentSize = CGSize(width: params.width, height: titleLayout.size.height + topInset + bottomInset)
let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets)
@ -139,7 +148,7 @@ public class ItemListTextItemNode: ListViewItemNode {
let _ = titleApply()
strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: leftInset, y: verticalInset), size: titleLayout.size)
strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: leftInset, y: topInset), size: titleLayout.size)
}
})
}

View File

@ -24,19 +24,14 @@
NSInteger _number;
UIColor *_checkColor;
CGAffineTransform TGCheckButtonDefaultTransform;
}
@end
@implementation TGCheckButtonView
static NSMutableDictionary *backgroundImages;
static NSMutableDictionary *fillImages;
static CGAffineTransform TGCheckButtonDefaultTransform;
+ (void)resetCache
{
[backgroundImages removeAllObjects];
[fillImages removeAllObjects];
+ (void)resetCache {
}
- (instancetype)initWithStyle:(TGCheckButtonStyle)style {
@ -55,15 +50,12 @@ static CGAffineTransform TGCheckButtonDefaultTransform;
self = [super initWithFrame:CGRectMake(0, 0, size.width, size.height)];
if (self != nil)
{
static dispatch_once_t onceToken;
static CGFloat screenScale = 2.0f;
dispatch_once(&onceToken, ^
{
TGCheckButtonDefaultTransform = CGAffineTransformMakeRotation(-M_PI_4);
backgroundImages = [[NSMutableDictionary alloc] init];
fillImages = [[NSMutableDictionary alloc] init];
screenScale = [UIScreen mainScreen].scale;
});
CGFloat screenScale = 2.0f;
TGCheckButtonDefaultTransform = CGAffineTransformMakeRotation(-M_PI_4);
NSMutableDictionary *backgroundImages = [[NSMutableDictionary alloc] init];
NSMutableDictionary *fillImages = [[NSMutableDictionary alloc] init];
screenScale = [UIScreen mainScreen].scale;
int32_t hex = 0x29c519;
UIColor *greenColor = [[UIColor alloc] initWithRed:(((hex >> 16) & 0xff) / 255.0f) green:(((hex >> 8) & 0xff) / 255.0f) blue:(((hex) & 0xff) / 255.0f) alpha:1.0f];

View File

@ -484,6 +484,22 @@ public final class PostboxEncoder {
}
}
public func encodeDataArray(_ value: [Data], forKey key: StaticString) {
self.encodeKey(key)
var type: Int8 = ValueType.BytesArray.rawValue
self.buffer.write(&type, offset: 0, length: 1)
var length: Int32 = Int32(value.count)
self.buffer.write(&length, offset: 0, length: 4)
for object in value {
var length: Int32 = Int32(object.count)
self.buffer.write(&length, offset: 0, length: 4)
object.withUnsafeBytes { (bytes: UnsafePointer<UInt8>) -> Void in
self.buffer.write(bytes, offset: 0, length: Int(length))
}
}
}
public func encodeObjectDictionary<K, V: PostboxCoding>(_ value: [K : V], forKey key: StaticString) where K: PostboxCoding {
self.encodeKey(key)
var t: Int8 = ValueType.ObjectDictionary.rawValue
@ -1173,6 +1189,31 @@ public final class PostboxDecoder {
}
}
public func decodeOptionalDataArrayForKey(_ key: StaticString) -> [Data]? {
if PostboxDecoder.positionOnKey(self.buffer.memory, offset: &self.offset, maxOffset: self.buffer.length, length: self.buffer.length, key: key, valueType: .BytesArray) {
var length: Int32 = 0
memcpy(&length, self.buffer.memory + self.offset, 4)
self.offset += 4
var array: [Data] = []
array.reserveCapacity(Int(length))
var i: Int32 = 0
while i < length {
var length: Int32 = 0
memcpy(&length, self.buffer.memory + self.offset, 4)
array.append(Data(bytes: self.buffer.memory.advanced(by: self.offset + 4), count: Int(length)))
self.offset += 4 + Int(length)
i += 1
}
return array
} else {
return nil
}
}
public func decodeObjectArrayForKey<T>(_ key: StaticString) -> [T] where T: PostboxCoding {
if PostboxDecoder.positionOnKey(self.buffer.memory, offset: &self.offset, maxOffset: self.buffer.length, length: self.buffer.length, key: key, valueType: .ObjectArray) {
var length: Int32 = 0

View File

@ -525,6 +525,10 @@ public final class Message {
return Message(stableId: self.stableId, stableVersion: self.stableVersion, id: self.id, globallyUniqueId: self.globallyUniqueId, groupingKey: self.groupingKey, groupInfo: self.groupInfo, timestamp: self.timestamp, flags: self.flags, tags: self.tags, globalTags: self.globalTags, localTags: self.localTags, forwardInfo: self.forwardInfo, author: self.author, text: self.text, attributes: self.attributes, media: media, peers: self.peers, associatedMessages: self.associatedMessages, associatedMessageIds: self.associatedMessageIds)
}
public func withUpdatedPeers(_ peers: SimpleDictionary<PeerId, Peer>) -> Message {
return Message(stableId: self.stableId, stableVersion: self.stableVersion, id: self.id, globallyUniqueId: self.globallyUniqueId, groupingKey: self.groupingKey, groupInfo: self.groupInfo, timestamp: self.timestamp, flags: self.flags, tags: self.tags, globalTags: self.globalTags, localTags: self.localTags, forwardInfo: self.forwardInfo, author: self.author, text: self.text, attributes: self.attributes, media: self.media, peers: peers, associatedMessages: self.associatedMessages, associatedMessageIds: self.associatedMessageIds)
}
public func withUpdatedFlags(_ flags: MessageFlags) -> Message {
return Message(stableId: self.stableId, stableVersion: self.stableVersion, id: self.id, globallyUniqueId: self.globallyUniqueId, groupingKey: self.groupingKey, groupInfo: self.groupInfo, timestamp: self.timestamp, flags: flags, tags: self.tags, globalTags: self.globalTags, localTags: self.localTags, forwardInfo: self.forwardInfo, author: self.author, text: self.text, attributes: self.attributes, media: self.media, peers: self.peers, associatedMessages: self.associatedMessages, associatedMessageIds: self.associatedMessageIds)
}

View File

@ -2275,6 +2275,42 @@ final class MessageHistoryTable: Table {
return Message(stableId: message.stableId, stableVersion: message.stableVersion, id: message.id, globallyUniqueId: message.globallyUniqueId, groupingKey: message.groupingKey, groupInfo: message.groupInfo, timestamp: message.timestamp, flags: message.flags, tags: message.tags, globalTags: message.globalTags, localTags: message.localTags, forwardInfo: forwardInfo, author: author, text: message.text, attributes: parsedAttributes, media: parsedMedia, peers: peers, associatedMessages: associatedMessages, associatedMessageIds: associatedMessageIds)
}
func renderMessagePeers(_ message: Message, peerTable: PeerTable) -> Message {
var author: Peer?
var peers = SimpleDictionary<PeerId, Peer>()
if let authorId = message.author?.id {
author = peerTable.get(authorId)
}
if let chatPeer = peerTable.get(message.id.peerId) {
peers[chatPeer.id] = chatPeer
if let associatedPeerId = chatPeer.associatedPeerId {
if let peer = peerTable.get(associatedPeerId) {
peers[peer.id] = peer
}
}
}
for media in message.media {
for peerId in media.peerIds {
if let peer = peerTable.get(peerId) {
peers[peer.id] = peer
}
}
}
for attribute in message.attributes {
for peerId in attribute.associatedPeerIds {
if let peer = peerTable.get(peerId) {
peers[peer.id] = peer
}
}
}
return message.withUpdatedPeers(peers)
}
func renderAssociatedMessages(associatedMessageIds: [MessageId], peerTable: PeerTable) -> SimpleDictionary<MessageId, Message> {
var associatedMessages = SimpleDictionary<MessageId, Message>()
for messageId in associatedMessageIds {

View File

@ -29,13 +29,13 @@ public struct MessageHistoryMessageEntry {
enum MutableMessageHistoryEntry {
case IntermediateMessageEntry(IntermediateMessage, MessageHistoryEntryLocation?, MessageHistoryEntryMonthLocation?)
case MessageEntry(MessageHistoryMessageEntry, reloadAssociatedMessages: Bool)
case MessageEntry(MessageHistoryMessageEntry, reloadAssociatedMessages: Bool, reloadPeers: Bool)
var index: MessageIndex {
switch self {
case let .IntermediateMessageEntry(message, _, _):
return message.index
case let .MessageEntry(message, _):
case let .MessageEntry(message, _, _):
return message.message.index
}
}
@ -44,7 +44,7 @@ enum MutableMessageHistoryEntry {
switch self {
case let .IntermediateMessageEntry(message, _, _):
return message.tags
case let .MessageEntry(message, _):
case let .MessageEntry(message, _, _):
return message.message.tags
}
}
@ -53,8 +53,8 @@ enum MutableMessageHistoryEntry {
switch self {
case let .IntermediateMessageEntry(message, _, monthLocation):
return .IntermediateMessageEntry(message, location, monthLocation)
case let .MessageEntry(message, reloadAssociatedMessages):
return .MessageEntry(MessageHistoryMessageEntry(message: message.message, location: location, monthLocation: message.monthLocation, attributes: message.attributes), reloadAssociatedMessages: reloadAssociatedMessages)
case let .MessageEntry(message, reloadAssociatedMessages, reloadPeers):
return .MessageEntry(MessageHistoryMessageEntry(message: message.message, location: location, monthLocation: message.monthLocation, attributes: message.attributes), reloadAssociatedMessages: reloadAssociatedMessages, reloadPeers: reloadPeers)
}
}
@ -62,8 +62,8 @@ enum MutableMessageHistoryEntry {
switch self {
case let .IntermediateMessageEntry(message, location, _):
return .IntermediateMessageEntry(message, location, monthLocation)
case let .MessageEntry(message, reloadAssociatedMessages):
return .MessageEntry(MessageHistoryMessageEntry(message: message.message, location: message.location, monthLocation: monthLocation, attributes: message.attributes), reloadAssociatedMessages: reloadAssociatedMessages)
case let .MessageEntry(message, reloadAssociatedMessages, reloadPeers):
return .MessageEntry(MessageHistoryMessageEntry(message: message.message, location: message.location, monthLocation: monthLocation, attributes: message.attributes), reloadAssociatedMessages: reloadAssociatedMessages, reloadPeers: reloadPeers)
}
}
@ -79,12 +79,12 @@ enum MutableMessageHistoryEntry {
} else {
return self
}
case let .MessageEntry(message, reloadAssociatedMessages):
case let .MessageEntry(message, reloadAssociatedMessages, reloadPeers):
if let location = message.location {
if message.message.index > index {
return .MessageEntry(MessageHistoryMessageEntry(message: message.message, location: MessageHistoryEntryLocation(index: location.index + 1, count: location.count + 1), monthLocation: message.monthLocation, attributes: message.attributes), reloadAssociatedMessages: reloadAssociatedMessages)
return .MessageEntry(MessageHistoryMessageEntry(message: message.message, location: MessageHistoryEntryLocation(index: location.index + 1, count: location.count + 1), monthLocation: message.monthLocation, attributes: message.attributes), reloadAssociatedMessages: reloadAssociatedMessages, reloadPeers: reloadPeers)
} else {
return .MessageEntry(MessageHistoryMessageEntry(message: message.message, location: MessageHistoryEntryLocation(index: location.index, count: location.count + 1), monthLocation: message.monthLocation, attributes: message.attributes), reloadAssociatedMessages: reloadAssociatedMessages)
return .MessageEntry(MessageHistoryMessageEntry(message: message.message, location: MessageHistoryEntryLocation(index: location.index, count: location.count + 1), monthLocation: message.monthLocation, attributes: message.attributes), reloadAssociatedMessages: reloadAssociatedMessages, reloadPeers: reloadPeers)
}
} else {
return self
@ -107,15 +107,15 @@ enum MutableMessageHistoryEntry {
} else {
return self
}
case let .MessageEntry(message, reloadAssociatedMessages):
case let .MessageEntry(message, reloadAssociatedMessages, reloadPeers):
if let location = message.location {
if message.message.index > index {
//assert(location.index > 0)
//assert(location.count != 0)
return .MessageEntry(MessageHistoryMessageEntry(message: message.message, location: MessageHistoryEntryLocation(index: location.index - 1, count: location.count - 1), monthLocation: message.monthLocation, attributes: message.attributes), reloadAssociatedMessages: reloadAssociatedMessages)
return .MessageEntry(MessageHistoryMessageEntry(message: message.message, location: MessageHistoryEntryLocation(index: location.index - 1, count: location.count - 1), monthLocation: message.monthLocation, attributes: message.attributes), reloadAssociatedMessages: reloadAssociatedMessages, reloadPeers: reloadPeers)
} else {
//assert(location.count != 0)
return .MessageEntry(MessageHistoryMessageEntry(message: message.message, location: MessageHistoryEntryLocation(index: location.index, count: location.count - 1), monthLocation: message.monthLocation, attributes: message.attributes), reloadAssociatedMessages: reloadAssociatedMessages)
return .MessageEntry(MessageHistoryMessageEntry(message: message.message, location: MessageHistoryEntryLocation(index: location.index, count: location.count - 1), monthLocation: message.monthLocation, attributes: message.attributes), reloadAssociatedMessages: reloadAssociatedMessages, reloadPeers: reloadPeers)
}
} else {
return self
@ -128,10 +128,10 @@ enum MutableMessageHistoryEntry {
case let .IntermediateMessageEntry(message, location, monthLocation):
let updatedMessage = IntermediateMessage(stableId: message.stableId, stableVersion: message.stableVersion, id: message.id, globallyUniqueId: message.globallyUniqueId, groupingKey: message.groupingKey, groupInfo: message.groupInfo, timestamp: timestamp, flags: message.flags, tags: message.tags, globalTags: message.globalTags, localTags: message.localTags, forwardInfo: message.forwardInfo, authorId: message.authorId, text: message.text, attributesData: message.attributesData, embeddedMediaData: message.embeddedMediaData, referencedMedia: message.referencedMedia)
return .IntermediateMessageEntry(updatedMessage, location, monthLocation)
case let .MessageEntry(value, reloadAssociatedMessages):
case let .MessageEntry(value, reloadAssociatedMessages, reloadPeers):
let message = value.message
let updatedMessage = Message(stableId: message.stableId, stableVersion: message.stableVersion, id: message.id, globallyUniqueId: message.globallyUniqueId, groupingKey: message.groupingKey, groupInfo: message.groupInfo, timestamp: timestamp, flags: message.flags, tags: message.tags, globalTags: message.globalTags, localTags: message.localTags, forwardInfo: message.forwardInfo, author: message.author, text: message.text, attributes: message.attributes, media: message.media, peers: message.peers, associatedMessages: message.associatedMessages, associatedMessageIds: message.associatedMessageIds)
return .MessageEntry(MessageHistoryMessageEntry(message: updatedMessage, location: value.location, monthLocation: value.monthLocation, attributes: value.attributes), reloadAssociatedMessages: reloadAssociatedMessages)
return .MessageEntry(MessageHistoryMessageEntry(message: updatedMessage, location: value.location, monthLocation: value.monthLocation, attributes: value.attributes), reloadAssociatedMessages: reloadAssociatedMessages, reloadPeers: reloadPeers)
}
}
@ -139,7 +139,7 @@ enum MutableMessageHistoryEntry {
switch self {
case let .IntermediateMessageEntry(message, location, monthLocation):
return []
case let .MessageEntry(value, _):
case let .MessageEntry(value, _, _):
return value.message.associatedMessageIds
}
}

View File

@ -874,10 +874,10 @@ final class HistoryViewLoadedState {
let currentLocation = nextLocation
nextLocation = nextLocation.successor
switch entry {
case let .IntermediateMessageEntry(message, _, monthLocation):
return .IntermediateMessageEntry(message, currentLocation, monthLocation)
case let .MessageEntry(entry, reloadAssociatedMessages):
return .MessageEntry(MessageHistoryMessageEntry(message: entry.message, location: currentLocation, monthLocation: entry.monthLocation, attributes: entry.attributes), reloadAssociatedMessages: reloadAssociatedMessages)
case let .IntermediateMessageEntry(message, _, monthLocation):
return .IntermediateMessageEntry(message, currentLocation, monthLocation)
case let .MessageEntry(entry, reloadAssociatedMessages, reloadPeers):
return .MessageEntry(MessageHistoryMessageEntry(message: entry.message, location: currentLocation, monthLocation: entry.monthLocation, attributes: entry.attributes), reloadAssociatedMessages: reloadAssociatedMessages, reloadPeers: reloadPeers)
}
}
}
@ -900,8 +900,8 @@ final class HistoryViewLoadedState {
switch entry {
case let .IntermediateMessageEntry(message, location, _):
return .IntermediateMessageEntry(message, location, MessageHistoryEntryMonthLocation(indexInMonth: Int32(currentIndexInMonth)))
case let .MessageEntry(entry, reloadAssociatedMessages):
return .MessageEntry(MessageHistoryMessageEntry(message: entry.message, location: entry.location, monthLocation: MessageHistoryEntryMonthLocation(indexInMonth: Int32(currentIndexInMonth)), attributes: entry.attributes), reloadAssociatedMessages: reloadAssociatedMessages)
case let .MessageEntry(entry, reloadAssociatedMessages, reloadPeers):
return .MessageEntry(MessageHistoryMessageEntry(message: entry.message, location: entry.location, monthLocation: MessageHistoryEntryMonthLocation(indexInMonth: Int32(currentIndexInMonth)), attributes: entry.attributes), reloadAssociatedMessages: reloadAssociatedMessages, reloadPeers: reloadPeers)
}
}
}
@ -960,8 +960,8 @@ final class HistoryViewLoadedState {
switch entry {
case let .IntermediateMessageEntry(message, location, monthLocation):
return .IntermediateMessageEntry(message.withUpdatedGroupInfo(groupInfo), location, monthLocation)
case let .MessageEntry(messageEntry, reloadAssociatedMessages):
return .MessageEntry(MessageHistoryMessageEntry(message: messageEntry.message.withUpdatedGroupInfo(groupInfo), location: messageEntry.location, monthLocation: messageEntry.monthLocation, attributes: messageEntry.attributes), reloadAssociatedMessages: reloadAssociatedMessages)
case let .MessageEntry(messageEntry, reloadAssociatedMessages, reloadPeers):
return .MessageEntry(MessageHistoryMessageEntry(message: messageEntry.message.withUpdatedGroupInfo(groupInfo), location: messageEntry.location, monthLocation: messageEntry.monthLocation, attributes: messageEntry.attributes), reloadAssociatedMessages: reloadAssociatedMessages, reloadPeers: reloadPeers)
}
}
return nil
@ -983,8 +983,8 @@ final class HistoryViewLoadedState {
switch entry {
case let .IntermediateMessageEntry(message, location, monthLocation):
return .IntermediateMessageEntry(message.withUpdatedEmbeddedMedia(buffer), location, monthLocation)
case let .MessageEntry(messageEntry, reloadAssociatedMessages):
return .MessageEntry(MessageHistoryMessageEntry(message: messageEntry.message, location: messageEntry.location, monthLocation: messageEntry.monthLocation, attributes: messageEntry.attributes), reloadAssociatedMessages: reloadAssociatedMessages)
case let .MessageEntry(messageEntry, reloadAssociatedMessages, reloadPeers):
return .MessageEntry(MessageHistoryMessageEntry(message: messageEntry.message, location: messageEntry.location, monthLocation: messageEntry.monthLocation, attributes: messageEntry.attributes), reloadAssociatedMessages: reloadAssociatedMessages, reloadPeers: reloadPeers)
}
})
}
@ -994,8 +994,9 @@ final class HistoryViewLoadedState {
for space in self.orderedEntriesBySpace.keys {
let spaceUpdated = self.orderedEntriesBySpace[space]!.mutableScan({ entry in
switch entry {
case let .MessageEntry(value, reloadAssociatedMessages):
case let .MessageEntry(value, reloadAssociatedMessages, reloadPeers):
let message = value.message
var reloadPeers = reloadPeers
var rebuild = false
for media in message.media {
@ -1010,6 +1011,9 @@ final class HistoryViewLoadedState {
for media in message.media {
if let mediaId = media.id, let updated = updatedMedia[mediaId] {
if let updated = updated {
if media.peerIds != updated.peerIds {
reloadPeers = true
}
messageMedia.append(updated)
}
} else {
@ -1017,7 +1021,7 @@ final class HistoryViewLoadedState {
}
}
let updatedMessage = Message(stableId: message.stableId, stableVersion: message.stableVersion, id: message.id, globallyUniqueId: message.globallyUniqueId, groupingKey: message.groupingKey, groupInfo: message.groupInfo, timestamp: message.timestamp, flags: message.flags, tags: message.tags, globalTags: message.globalTags, localTags: message.localTags, forwardInfo: message.forwardInfo, author: message.author, text: message.text, attributes: message.attributes, media: messageMedia, peers: message.peers, associatedMessages: message.associatedMessages, associatedMessageIds: message.associatedMessageIds)
return .MessageEntry(MessageHistoryMessageEntry(message: updatedMessage, location: value.location, monthLocation: value.monthLocation, attributes: value.attributes), reloadAssociatedMessages: reloadAssociatedMessages)
return .MessageEntry(MessageHistoryMessageEntry(message: updatedMessage, location: value.location, monthLocation: value.monthLocation, attributes: value.attributes), reloadAssociatedMessages: reloadAssociatedMessages, reloadPeers: reloadPeers)
}
case .IntermediateMessageEntry:
break
@ -1046,9 +1050,9 @@ final class HistoryViewLoadedState {
switch current {
case .IntermediateMessageEntry:
return current
case let .MessageEntry(messageEntry, _):
case let .MessageEntry(messageEntry, _, reloadPeers):
updated = true
return .MessageEntry(messageEntry, reloadAssociatedMessages: true)
return .MessageEntry(messageEntry, reloadAssociatedMessages: true, reloadPeers: reloadPeers)
}
})
}
@ -1109,11 +1113,11 @@ final class HistoryViewLoadedState {
switch current {
case .IntermediateMessageEntry:
return current
case let .MessageEntry(messageEntry, reloadAssociatedMessages):
case let .MessageEntry(messageEntry, reloadAssociatedMessages, reloadPeers):
updated = true
if let associatedMessages = messageEntry.message.associatedMessages.filteredOut(keysIn: [index.id]) {
return .MessageEntry(MessageHistoryMessageEntry(message: messageEntry.message.withUpdatedAssociatedMessages(associatedMessages), location: messageEntry.location, monthLocation: messageEntry.monthLocation, attributes: messageEntry.attributes), reloadAssociatedMessages: reloadAssociatedMessages)
return .MessageEntry(MessageHistoryMessageEntry(message: messageEntry.message.withUpdatedAssociatedMessages(associatedMessages), location: messageEntry.location, monthLocation: messageEntry.monthLocation, attributes: messageEntry.attributes), reloadAssociatedMessages: reloadAssociatedMessages, reloadPeers: reloadPeers)
} else {
return current
}
@ -1177,14 +1181,22 @@ final class HistoryViewLoadedState {
}
switch entry {
case let .MessageEntry(value, reloadAssociatedMessages):
case let .MessageEntry(value, reloadAssociatedMessages, reloadPeers):
var updatedMessage = value.message
if reloadAssociatedMessages {
let associatedMessages = postbox.messageHistoryTable.renderAssociatedMessages(associatedMessageIds: value.message.associatedMessageIds, peerTable: postbox.peerTable)
let updatedValue = MessageHistoryMessageEntry(message: value.message.withUpdatedAssociatedMessages(associatedMessages), location: value.location, monthLocation: value.monthLocation, attributes: value.attributes)
updatedMessage = value.message.withUpdatedAssociatedMessages(associatedMessages)
}
if reloadPeers {
updatedMessage = postbox.messageHistoryTable.renderMessagePeers(updatedMessage, peerTable: postbox.peerTable)
}
if value.message !== updatedMessage {
let updatedValue = MessageHistoryMessageEntry(message: updatedMessage, location: value.location, monthLocation: value.monthLocation, attributes: value.attributes)
if directionIndex == 0 {
self.orderedEntriesBySpace[space]!.setLowerOrAtAnchorAtArrayIndex(index, to: .MessageEntry(updatedValue, reloadAssociatedMessages: false))
self.orderedEntriesBySpace[space]!.setLowerOrAtAnchorAtArrayIndex(index, to: .MessageEntry(updatedValue, reloadAssociatedMessages: false, reloadPeers: false))
} else {
self.orderedEntriesBySpace[space]!.setHigherThanAnchorAtArrayIndex(index, to: .MessageEntry(updatedValue, reloadAssociatedMessages: false))
self.orderedEntriesBySpace[space]!.setHigherThanAnchorAtArrayIndex(index, to: .MessageEntry(updatedValue, reloadAssociatedMessages: false, reloadPeers: false))
}
result.append(updatedValue)
} else {
@ -1198,9 +1210,9 @@ final class HistoryViewLoadedState {
}
let entry = MessageHistoryMessageEntry(message: renderedMessage, location: location, monthLocation: monthLocation, attributes: MutableMessageHistoryEntryAttributes(authorIsContact: authorIsContact))
if directionIndex == 0 {
self.orderedEntriesBySpace[space]!.setLowerOrAtAnchorAtArrayIndex(index, to: .MessageEntry(entry, reloadAssociatedMessages: false))
self.orderedEntriesBySpace[space]!.setLowerOrAtAnchorAtArrayIndex(index, to: .MessageEntry(entry, reloadAssociatedMessages: false, reloadPeers: false))
} else {
self.orderedEntriesBySpace[space]!.setHigherThanAnchorAtArrayIndex(index, to: .MessageEntry(entry, reloadAssociatedMessages: false))
self.orderedEntriesBySpace[space]!.setHigherThanAnchorAtArrayIndex(index, to: .MessageEntry(entry, reloadAssociatedMessages: false, reloadPeers: false))
}
result.append(entry)
}

View File

@ -1474,7 +1474,7 @@ public func settingsController(context: AccountContext, accountManager: AccountM
if let primary = primary {
let size = CGSize(width: 31.0, height: 31.0)
let inset: CGFloat = 3.0
if let signal = peerAvatarImage(account: primary.0, peer: primary.1, authorOfMessage: nil, representation: primary.1.profileImageRepresentations.first, displayDimensions: size, inset: 3.0, emptyColor: nil, synchronousLoad: false) {
if let signal = peerAvatarImage(account: primary.0, peerReference: PeerReference(primary.1), authorOfMessage: nil, representation: primary.1.profileImageRepresentations.first, displayDimensions: size, inset: 3.0, emptyColor: nil, synchronousLoad: false) {
return signal
|> map { image -> (UIImage, UIImage)? in
if let image = image, let selectedImage = generateImage(size, rotatedContext: { size, context in
@ -1497,13 +1497,13 @@ public func settingsController(context: AccountContext, accountManager: AccountM
let image = generateImage(size, rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
context.translateBy(x: inset, y: inset)
drawPeerAvatarLetters(context: context, size: CGSize(width: size.width - inset * 2.0, height: size.height - inset * 2.0), font: avatarFont, letters: primary.1.displayLetters, accountPeerId: primary.1.id, peerId: primary.1.id)
drawPeerAvatarLetters(context: context, size: CGSize(width: size.width - inset * 2.0, height: size.height - inset * 2.0), font: avatarFont, letters: primary.1.displayLetters, peerId: primary.1.id)
})?.withRenderingMode(.alwaysOriginal)
let selectedImage = generateImage(size, rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
context.translateBy(x: inset, y: inset)
drawPeerAvatarLetters(context: context, size: CGSize(width: size.width - inset * 2.0, height: size.height - inset * 2.0), font: avatarFont, letters: primary.1.displayLetters, accountPeerId: primary.1.id, peerId: primary.1.id)
drawPeerAvatarLetters(context: context, size: CGSize(width: size.width - inset * 2.0, height: size.height - inset * 2.0), font: avatarFont, letters: primary.1.displayLetters, peerId: primary.1.id)
context.translateBy(x: -inset, y: -inset)
context.setLineWidth(1.0)
context.setStrokeColor(primary.2.rootController.tabBar.selectedIconColor.cgColor)

View File

@ -24,38 +24,45 @@ public struct TelegramMediaPollOptionVoters: Equatable, PostboxCoding {
public let selected: Bool
public let opaqueIdentifier: Data
public let count: Int32
public let isCorrect: Bool
public init(selected: Bool, opaqueIdentifier: Data, count: Int32) {
public init(selected: Bool, opaqueIdentifier: Data, count: Int32, isCorrect: Bool) {
self.selected = selected
self.opaqueIdentifier = opaqueIdentifier
self.count = count
self.isCorrect = isCorrect
}
public init(decoder: PostboxDecoder) {
self.selected = decoder.decodeInt32ForKey("s", orElse: 0) != 0
self.opaqueIdentifier = decoder.decodeDataForKey("i") ?? Data()
self.count = decoder.decodeInt32ForKey("c", orElse: 0)
self.isCorrect = decoder.decodeInt32ForKey("cr", orElse: 0) != 0
}
public func encode(_ encoder: PostboxEncoder) {
encoder.encodeInt32(self.selected ? 1 : 0, forKey: "s")
encoder.encodeData(self.opaqueIdentifier, forKey: "i")
encoder.encodeInt32(self.count, forKey: "c")
encoder.encodeInt32(self.isCorrect ? 1 : 0, forKey: "cr")
}
}
public struct TelegramMediaPollResults: Equatable, PostboxCoding {
public let voters: [TelegramMediaPollOptionVoters]?
public let totalVoters: Int32?
public let recentVoters: [PeerId]
public init(voters: [TelegramMediaPollOptionVoters]?, totalVoters: Int32?) {
public init(voters: [TelegramMediaPollOptionVoters]?, totalVoters: Int32?, recentVoters: [PeerId]) {
self.voters = voters
self.totalVoters = totalVoters
self.recentVoters = recentVoters
}
public init(decoder: PostboxDecoder) {
self.voters = decoder.decodeOptionalObjectArrayWithDecoderForKey("v")
self.totalVoters = decoder.decodeOptionalInt32ForKey("t")
self.recentVoters = decoder.decodeInt64ArrayForKey("rv").map(PeerId.init)
}
public func encode(_ encoder: PostboxEncoder) {
@ -69,6 +76,39 @@ public struct TelegramMediaPollResults: Equatable, PostboxCoding {
} else {
encoder.encodeNil(forKey: "t")
}
encoder.encodeInt64Array(self.recentVoters.map { $0.toInt64() }, forKey: "rv")
}
}
public enum TelegramMediaPollPublicity: Int32 {
case anonymous
case `public`
}
public enum TelegramMediaPollKind: Equatable, PostboxCoding {
case poll(multipleAnswers: Bool)
case quiz
public init(decoder: PostboxDecoder) {
switch decoder.decodeInt32ForKey("_v", orElse: 0) {
case 0:
self = .poll(multipleAnswers: decoder.decodeInt32ForKey("m", orElse: 0) != 0)
case 1:
self = .quiz
default:
assertionFailure()
self = .poll(multipleAnswers: false)
}
}
public func encode(_ encoder: PostboxEncoder) {
switch self {
case let .poll(multipleAnswers):
encoder.encodeInt32(0, forKey: "_v")
encoder.encodeInt32(multipleAnswers ? 1 : 0, forKey: "m")
case .quiz:
encoder.encodeInt32(1, forKey: "_v")
}
}
}
@ -77,17 +117,26 @@ public final class TelegramMediaPoll: Media, Equatable {
return self.pollId
}
public let pollId: MediaId
public let peerIds: [PeerId] = []
public var peerIds: [PeerId] {
return results.recentVoters
}
public let publicity: TelegramMediaPollPublicity
public let kind: TelegramMediaPollKind
public let text: String
public let options: [TelegramMediaPollOption]
public let correctAnswers: [Data]?
public let results: TelegramMediaPollResults
public let isClosed: Bool
public init(pollId: MediaId, text: String, options: [TelegramMediaPollOption], results: TelegramMediaPollResults, isClosed: Bool) {
public init(pollId: MediaId, publicity: TelegramMediaPollPublicity, kind: TelegramMediaPollKind, text: String, options: [TelegramMediaPollOption], correctAnswers: [Data]?, results: TelegramMediaPollResults, isClosed: Bool) {
self.pollId = pollId
self.publicity = publicity
self.kind = kind
self.text = text
self.options = options
self.correctAnswers = correctAnswers
self.results = results
self.isClosed = isClosed
}
@ -98,18 +147,28 @@ public final class TelegramMediaPoll: Media, Equatable {
} else {
self.pollId = MediaId(namespace: Namespaces.Media.LocalPoll, id: 0)
}
self.publicity = TelegramMediaPollPublicity(rawValue: decoder.decodeInt32ForKey("pb", orElse: 0)) ?? TelegramMediaPollPublicity.anonymous
self.kind = decoder.decodeObjectForKey("kn", decoder: { TelegramMediaPollKind(decoder: $0) }) as? TelegramMediaPollKind ?? TelegramMediaPollKind.poll(multipleAnswers: false)
self.text = decoder.decodeStringForKey("t", orElse: "")
self.options = decoder.decodeObjectArrayWithDecoderForKey("os")
self.results = decoder.decodeObjectForKey("rs", decoder: { TelegramMediaPollResults(decoder: $0) }) as? TelegramMediaPollResults ?? TelegramMediaPollResults(voters: nil, totalVoters: nil)
self.correctAnswers = decoder.decodeOptionalDataArrayForKey("ca")
self.results = decoder.decodeObjectForKey("rs", decoder: { TelegramMediaPollResults(decoder: $0) }) as? TelegramMediaPollResults ?? TelegramMediaPollResults(voters: nil, totalVoters: nil, recentVoters: [])
self.isClosed = decoder.decodeInt32ForKey("ic", orElse: 0) != 0
}
public func encode(_ encoder: PostboxEncoder) {
let buffer = WriteBuffer()
self.pollId.encodeToBuffer(buffer)
encoder.encodeInt32(self.publicity.rawValue, forKey: "pb")
encoder.encodeObject(self.kind, forKey: "kn")
encoder.encodeBytes(buffer, forKey: "i")
encoder.encodeString(self.text, forKey: "t")
encoder.encodeObjectArray(self.options, forKey: "os")
if let correctAnswers = self.correctAnswers {
encoder.encodeDataArray(correctAnswers, forKey: "ca")
} else {
encoder.encodeNil(forKey: "ca")
}
encoder.encodeObject(results, forKey: "rs")
encoder.encodeInt32(self.isClosed ? 1 : 0, forKey: "ic")
}
@ -129,12 +188,21 @@ public final class TelegramMediaPoll: Media, Equatable {
if lhs.pollId != rhs.pollId {
return false
}
if lhs.publicity != rhs.publicity {
return false
}
if lhs.kind != rhs.kind {
return false
}
if lhs.text != rhs.text {
return false
}
if lhs.options != rhs.options {
return false
}
if lhs.correctAnswers != rhs.correctAnswers {
return false
}
if lhs.results != rhs.results {
return false
}
@ -155,14 +223,14 @@ 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)
}), totalVoters: results.totalVoters)
return TelegramMediaPollOptionVoters(selected: selectedOpaqueIdentifiers.contains(voters.opaqueIdentifier), opaqueIdentifier: voters.opaqueIdentifier, count: voters.count, isCorrect: voters.isCorrect)
}), totalVoters: results.totalVoters, recentVoters: results.recentVoters)
} else {
updatedResults = TelegramMediaPollResults(voters: self.results.voters, totalVoters: results.totalVoters)
updatedResults = TelegramMediaPollResults(voters: self.results.voters, totalVoters: results.totalVoters, recentVoters: results.recentVoters)
}
} else {
updatedResults = results
}
return TelegramMediaPoll(pollId: self.pollId, text: self.text, options: self.options, 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)
}
}

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[763976820] = { return Api.ChatFull.parse_channelFull($0) }
dict[1465219162] = { return Api.PollResults.parse_pollResults($0) }
dict[-932174686] = { 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) }
@ -72,7 +72,6 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = {
dict[1516793212] = { return Api.ChatInvite.parse_chatInviteAlready($0) }
dict[-540871282] = { return Api.ChatInvite.parse_chatInvite($0) }
dict[-532532493] = { return Api.AutoDownloadSettings.parse_autoDownloadSettings($0) }
dict[-767099577] = { return Api.AutoDownloadSettings.parse_autoDownloadSettingsLegacy($0) }
dict[1678812626] = { return Api.StickerSetCovered.parse_stickerSetCovered($0) }
dict[872932635] = { return Api.StickerSetCovered.parse_stickerSetMultiCovered($0) }
dict[1189204285] = { return Api.RecentMeUrl.parse_recentMeUrlUnknown($0) }
@ -247,6 +246,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = {
dict[-2112423005] = { return Api.Update.parse_updateTheme($0) }
dict[-2027964103] = { return Api.Update.parse_updateGeoLiveViewed($0) }
dict[1448076945] = { return Api.Update.parse_updateLoginToken($0) }
dict[136574537] = { return Api.messages.VotesList.parse_votesList($0) }
dict[1558266229] = { return Api.PopularContact.parse_popularContact($0) }
dict[-373643672] = { return Api.FolderPeer.parse_folderPeer($0) }
dict[367766557] = { return Api.ChannelParticipant.parse_channelParticipant($0) }
@ -254,6 +254,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = {
dict[470789295] = { return Api.ChannelParticipant.parse_channelParticipantBanned($0) }
dict[-859915345] = { return Api.ChannelParticipant.parse_channelParticipantAdmin($0) }
dict[-2138237532] = { return Api.ChannelParticipant.parse_channelParticipantCreator($0) }
dict[-233638547] = { return Api.MessageUserVote.parse_messageUserVote($0) }
dict[471043349] = { return Api.contacts.Blocked.parse_blocked($0) }
dict[-1878523231] = { return Api.contacts.Blocked.parse_blockedSlice($0) }
dict[-55902537] = { return Api.InputDialogPeer.parse_inputDialogPeer($0) }
@ -350,8 +351,8 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = {
dict[-440664550] = { return Api.InputMedia.parse_inputMediaPhotoExternal($0) }
dict[-78455655] = { return Api.InputMedia.parse_inputMediaDocumentExternal($0) }
dict[-122978821] = { return Api.InputMedia.parse_inputMediaContact($0) }
dict[112424539] = { return Api.InputMedia.parse_inputMediaPoll($0) }
dict[-833715459] = { return Api.InputMedia.parse_inputMediaGeoLive($0) }
dict[-1410741723] = { 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) }
@ -603,7 +604,6 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = {
dict[1421174295] = { return Api.WebPageAttribute.parse_webPageAttributeTheme($0) }
dict[82699215] = { return Api.messages.FeaturedStickers.parse_featuredStickersNotModified($0) }
dict[-123893531] = { return Api.messages.FeaturedStickers.parse_featuredStickers($0) }
dict[1375940666] = { return Api.auth.LoginTokenInfo.parse_loginTokenInfo($0) }
dict[-2048646399] = { return Api.PhoneCallDiscardReason.parse_phoneCallDiscardReasonMissed($0) }
dict[-527056480] = { return Api.PhoneCallDiscardReason.parse_phoneCallDiscardReasonDisconnect($0) }
dict[1471006352] = { return Api.PhoneCallDiscardReason.parse_phoneCallDiscardReasonHangup($0) }
@ -674,6 +674,8 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = {
dict[-395967805] = { return Api.messages.AllStickers.parse_allStickersNotModified($0) }
dict[-302170017] = { return Api.messages.AllStickers.parse_allStickers($0) }
dict[-1655957568] = { return Api.PhoneConnection.parse_phoneConnection($0) }
dict[-206688531] = { return Api.help.UserInfo.parse_userInfoEmpty($0) }
dict[32192344] = { return Api.help.UserInfo.parse_userInfo($0) }
dict[-1194283041] = { return Api.AccountDaysTTL.parse_accountDaysTTL($0) }
dict[-1658158621] = { return Api.SecureValueType.parse_secureValueTypePersonalDetails($0) }
dict[1034709504] = { return Api.SecureValueType.parse_secureValueTypePassport($0) }
@ -740,7 +742,6 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = {
dict[1363483106] = { return Api.DialogPeer.parse_dialogPeerFolder($0) }
dict[-104284986] = { return Api.WebDocument.parse_webDocumentNoProxy($0) }
dict[475467473] = { return Api.WebDocument.parse_webDocument($0) }
dict[1211967244] = { return Api.Theme.parse_themeDocumentNotModified($0) }
dict[42930452] = { return Api.Theme.parse_theme($0) }
dict[-1290580579] = { return Api.contacts.Found.parse_found($0) }
dict[-368018716] = { return Api.ChannelAdminLogEventsFilter.parse_channelAdminLogEventsFilter($0) }
@ -960,12 +961,16 @@ public struct Api {
_1.serialize(buffer, boxed)
case let _1 as Api.Update:
_1.serialize(buffer, boxed)
case let _1 as Api.messages.VotesList:
_1.serialize(buffer, boxed)
case let _1 as Api.PopularContact:
_1.serialize(buffer, boxed)
case let _1 as Api.FolderPeer:
_1.serialize(buffer, boxed)
case let _1 as Api.ChannelParticipant:
_1.serialize(buffer, boxed)
case let _1 as Api.MessageUserVote:
_1.serialize(buffer, boxed)
case let _1 as Api.contacts.Blocked:
_1.serialize(buffer, boxed)
case let _1 as Api.InputDialogPeer:
@ -1264,8 +1269,6 @@ public struct Api {
_1.serialize(buffer, boxed)
case let _1 as Api.messages.FeaturedStickers:
_1.serialize(buffer, boxed)
case let _1 as Api.auth.LoginTokenInfo:
_1.serialize(buffer, boxed)
case let _1 as Api.PhoneCallDiscardReason:
_1.serialize(buffer, boxed)
case let _1 as Api.NearestDc:
@ -1324,6 +1327,8 @@ public struct Api {
_1.serialize(buffer, boxed)
case let _1 as Api.PhoneConnection:
_1.serialize(buffer, boxed)
case let _1 as Api.help.UserInfo:
_1.serialize(buffer, boxed)
case let _1 as Api.AccountDaysTTL:
_1.serialize(buffer, boxed)
case let _1 as Api.SecureValueType:

View File

@ -219,6 +219,68 @@ public struct messages {
}
}
}
public enum VotesList: TypeConstructorDescription {
case votesList(flags: Int32, count: Int32, votes: [Api.MessageUserVote], users: [Api.User], nextOffset: String?)
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
switch self {
case .votesList(let flags, let count, let votes, let users, let nextOffset):
if boxed {
buffer.appendInt32(136574537)
}
serializeInt32(flags, buffer: buffer, boxed: false)
serializeInt32(count, buffer: buffer, boxed: false)
buffer.appendInt32(481674261)
buffer.appendInt32(Int32(votes.count))
for item in votes {
item.serialize(buffer, true)
}
buffer.appendInt32(481674261)
buffer.appendInt32(Int32(users.count))
for item in users {
item.serialize(buffer, true)
}
if Int(flags) & Int(1 << 0) != 0 {serializeString(nextOffset!, buffer: buffer, boxed: false)}
break
}
}
public func descriptionFields() -> (String, [(String, Any)]) {
switch self {
case .votesList(let flags, let count, let votes, let users, let nextOffset):
return ("votesList", [("flags", flags), ("count", count), ("votes", votes), ("users", users), ("nextOffset", nextOffset)])
}
}
public static func parse_votesList(_ reader: BufferReader) -> VotesList? {
var _1: Int32?
_1 = reader.readInt32()
var _2: Int32?
_2 = reader.readInt32()
var _3: [Api.MessageUserVote]?
if let _ = reader.readInt32() {
_3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.MessageUserVote.self)
}
var _4: [Api.User]?
if let _ = reader.readInt32() {
_4 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self)
}
var _5: String?
if Int(_1!) & Int(1 << 0) != 0 {_5 = parseString(reader) }
let _c1 = _1 != nil
let _c2 = _2 != nil
let _c3 = _3 != nil
let _c4 = _4 != nil
let _c5 = (Int(_1!) & Int(1 << 0) == 0) || _5 != nil
if _c1 && _c2 && _c3 && _c4 && _c5 {
return Api.messages.VotesList.votesList(flags: _1!, count: _2!, votes: _3!, users: _4!, nextOffset: _5)
}
else {
return nil
}
}
}
public enum Stickers: TypeConstructorDescription {
case stickersNotModified
@ -1962,13 +2024,13 @@ public extension Api {
}
public enum PollResults: TypeConstructorDescription {
case pollResults(flags: Int32, results: [Api.PollAnswerVoters]?, totalVoters: Int32?)
case pollResults(flags: Int32, results: [Api.PollAnswerVoters]?, totalVoters: Int32?, recentVoters: [Int32]?)
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
switch self {
case .pollResults(let flags, let results, let totalVoters):
case .pollResults(let flags, let results, let totalVoters, let recentVoters):
if boxed {
buffer.appendInt32(1465219162)
buffer.appendInt32(-932174686)
}
serializeInt32(flags, buffer: buffer, boxed: false)
if Int(flags) & Int(1 << 1) != 0 {buffer.appendInt32(481674261)
@ -1977,14 +2039,19 @@ public extension Api {
item.serialize(buffer, true)
}}
if Int(flags) & Int(1 << 2) != 0 {serializeInt32(totalVoters!, buffer: buffer, boxed: false)}
if Int(flags) & Int(1 << 3) != 0 {buffer.appendInt32(481674261)
buffer.appendInt32(Int32(recentVoters!.count))
for item in recentVoters! {
serializeInt32(item, buffer: buffer, boxed: false)
}}
break
}
}
public func descriptionFields() -> (String, [(String, Any)]) {
switch self {
case .pollResults(let flags, let results, let totalVoters):
return ("pollResults", [("flags", flags), ("results", results), ("totalVoters", totalVoters)])
case .pollResults(let flags, let results, let totalVoters, let recentVoters):
return ("pollResults", [("flags", flags), ("results", results), ("totalVoters", totalVoters), ("recentVoters", recentVoters)])
}
}
@ -1997,11 +2064,16 @@ public extension Api {
} }
var _3: Int32?
if Int(_1!) & Int(1 << 2) != 0 {_3 = reader.readInt32() }
var _4: [Int32]?
if Int(_1!) & Int(1 << 3) != 0 {if let _ = reader.readInt32() {
_4 = Api.parseVector(reader, elementSignature: -1471112230, elementType: Int32.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
if _c1 && _c2 && _c3 {
return Api.PollResults.pollResults(flags: _1!, results: _2, totalVoters: _3)
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)
}
else {
return nil
@ -3601,7 +3673,6 @@ public extension Api {
}
public enum AutoDownloadSettings: TypeConstructorDescription {
case autoDownloadSettings(flags: Int32, photoSizeMax: Int32, videoSizeMax: Int32, fileSizeMax: Int32, videoUploadMaxbitrate: Int32)
case autoDownloadSettingsLegacy(flags: Int32, photoSizeMax: Int32, videoSizeMax: Int32, fileSizeMax: Int32)
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
switch self {
@ -3615,15 +3686,6 @@ public extension Api {
serializeInt32(fileSizeMax, buffer: buffer, boxed: false)
serializeInt32(videoUploadMaxbitrate, buffer: buffer, boxed: false)
break
case .autoDownloadSettingsLegacy(let flags, let photoSizeMax, let videoSizeMax, let fileSizeMax):
if boxed {
buffer.appendInt32(-767099577)
}
serializeInt32(flags, buffer: buffer, boxed: false)
serializeInt32(photoSizeMax, buffer: buffer, boxed: false)
serializeInt32(videoSizeMax, buffer: buffer, boxed: false)
serializeInt32(fileSizeMax, buffer: buffer, boxed: false)
break
}
}
@ -3631,8 +3693,6 @@ public extension Api {
switch self {
case .autoDownloadSettings(let flags, let photoSizeMax, let videoSizeMax, let fileSizeMax, let videoUploadMaxbitrate):
return ("autoDownloadSettings", [("flags", flags), ("photoSizeMax", photoSizeMax), ("videoSizeMax", videoSizeMax), ("fileSizeMax", fileSizeMax), ("videoUploadMaxbitrate", videoUploadMaxbitrate)])
case .autoDownloadSettingsLegacy(let flags, let photoSizeMax, let videoSizeMax, let fileSizeMax):
return ("autoDownloadSettingsLegacy", [("flags", flags), ("photoSizeMax", photoSizeMax), ("videoSizeMax", videoSizeMax), ("fileSizeMax", fileSizeMax)])
}
}
@ -3659,26 +3719,6 @@ public extension Api {
return nil
}
}
public static func parse_autoDownloadSettingsLegacy(_ reader: BufferReader) -> AutoDownloadSettings? {
var _1: Int32?
_1 = reader.readInt32()
var _2: Int32?
_2 = reader.readInt32()
var _3: Int32?
_3 = reader.readInt32()
var _4: Int32?
_4 = 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.AutoDownloadSettings.autoDownloadSettingsLegacy(flags: _1!, photoSizeMax: _2!, videoSizeMax: _3!, fileSizeMax: _4!)
}
else {
return nil
}
}
}
public enum StickerSetCovered: TypeConstructorDescription {
@ -8148,6 +8188,44 @@ public extension Api {
}
}
}
public enum MessageUserVote: TypeConstructorDescription {
case messageUserVote(userId: Int32, option: Buffer)
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
switch self {
case .messageUserVote(let userId, let option):
if boxed {
buffer.appendInt32(-233638547)
}
serializeInt32(userId, buffer: buffer, boxed: false)
serializeBytes(option, buffer: buffer, boxed: false)
break
}
}
public func descriptionFields() -> (String, [(String, Any)]) {
switch self {
case .messageUserVote(let userId, let option):
return ("messageUserVote", [("userId", userId), ("option", option)])
}
}
public static func parse_messageUserVote(_ reader: BufferReader) -> MessageUserVote? {
var _1: Int32?
_1 = reader.readInt32()
var _2: Buffer?
_2 = parseBytes(reader)
let _c1 = _1 != nil
let _c2 = _2 != nil
if _c1 && _c2 {
return Api.MessageUserVote.messageUserVote(userId: _1!, option: _2!)
}
else {
return nil
}
}
}
public enum InputDialogPeer: TypeConstructorDescription {
case inputDialogPeer(peer: Api.InputPeer)
@ -10159,8 +10237,8 @@ public extension Api {
case inputMediaPhotoExternal(flags: Int32, url: String, ttlSeconds: Int32?)
case inputMediaDocumentExternal(flags: Int32, url: String, ttlSeconds: Int32?)
case inputMediaContact(phoneNumber: String, firstName: String, lastName: String, vcard: String)
case inputMediaPoll(poll: Api.Poll)
case inputMediaGeoLive(flags: Int32, geoPoint: Api.InputGeoPoint, period: Int32?)
case inputMediaPoll(flags: Int32, poll: Api.Poll, correctAnswers: [Buffer]?)
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
switch self {
@ -10288,12 +10366,6 @@ public extension Api {
serializeString(lastName, buffer: buffer, boxed: false)
serializeString(vcard, buffer: buffer, boxed: false)
break
case .inputMediaPoll(let poll):
if boxed {
buffer.appendInt32(112424539)
}
poll.serialize(buffer, true)
break
case .inputMediaGeoLive(let flags, let geoPoint, let period):
if boxed {
buffer.appendInt32(-833715459)
@ -10302,6 +10374,18 @@ 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):
if boxed {
buffer.appendInt32(-1410741723)
}
serializeInt32(flags, buffer: buffer, boxed: false)
poll.serialize(buffer, true)
if Int(flags) & Int(1 << 0) != 0 {buffer.appendInt32(481674261)
buffer.appendInt32(Int32(correctAnswers!.count))
for item in correctAnswers! {
serializeBytes(item, buffer: buffer, boxed: false)
}}
break
}
}
@ -10333,10 +10417,10 @@ public extension Api {
return ("inputMediaDocumentExternal", [("flags", flags), ("url", url), ("ttlSeconds", ttlSeconds)])
case .inputMediaContact(let phoneNumber, let firstName, let lastName, let vcard):
return ("inputMediaContact", [("phoneNumber", phoneNumber), ("firstName", firstName), ("lastName", lastName), ("vcard", vcard)])
case .inputMediaPoll(let poll):
return ("inputMediaPoll", [("poll", poll)])
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)])
}
}
@ -10605,19 +10689,6 @@ public extension Api {
return nil
}
}
public static func parse_inputMediaPoll(_ reader: BufferReader) -> InputMedia? {
var _1: Api.Poll?
if let signature = reader.readInt32() {
_1 = Api.parse(reader, signature: signature) as? Api.Poll
}
let _c1 = _1 != nil
if _c1 {
return Api.InputMedia.inputMediaPoll(poll: _1!)
}
else {
return nil
}
}
public static func parse_inputMediaGeoLive(_ reader: BufferReader) -> InputMedia? {
var _1: Int32?
_1 = reader.readInt32()
@ -10637,6 +10708,27 @@ public extension Api {
return nil
}
}
public static func parse_inputMediaPoll(_ reader: BufferReader) -> InputMedia? {
var _1: Int32?
_1 = reader.readInt32()
var _2: Api.Poll?
if let signature = reader.readInt32() {
_2 = Api.parse(reader, signature: signature) as? Api.Poll
}
var _3: [Buffer]?
if Int(_1!) & Int(1 << 0) != 0 {if let _ = reader.readInt32() {
_3 = Api.parseVector(reader, elementSignature: -1255641564, elementType: Buffer.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)
}
else {
return nil
}
}
}
public enum InputPeer: TypeConstructorDescription {
@ -20230,17 +20322,10 @@ public extension Api {
}
public enum Theme: TypeConstructorDescription {
case themeDocumentNotModified
case theme(flags: Int32, id: Int64, accessHash: Int64, slug: String, title: String, document: Api.Document?, settings: Api.ThemeSettings?, installsCount: Int32)
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
switch self {
case .themeDocumentNotModified:
if boxed {
buffer.appendInt32(1211967244)
}
break
case .theme(let flags, let id, let accessHash, let slug, let title, let document, let settings, let installsCount):
if boxed {
buffer.appendInt32(42930452)
@ -20259,16 +20344,11 @@ public extension Api {
public func descriptionFields() -> (String, [(String, Any)]) {
switch self {
case .themeDocumentNotModified:
return ("themeDocumentNotModified", [])
case .theme(let flags, let id, let accessHash, let slug, let title, let document, let settings, let installsCount):
return ("theme", [("flags", flags), ("id", id), ("accessHash", accessHash), ("slug", slug), ("title", title), ("document", document), ("settings", settings), ("installsCount", installsCount)])
}
}
public static func parse_themeDocumentNotModified(_ reader: BufferReader) -> Theme? {
return Api.Theme.themeDocumentNotModified
}
public static func parse_theme(_ reader: BufferReader) -> Theme? {
var _1: Int32?
_1 = reader.readInt32()

View File

@ -859,76 +859,6 @@ public struct auth {
return Api.auth.CodeType.codeTypeFlashCall
}
}
public enum LoginTokenInfo: TypeConstructorDescription {
case loginTokenInfo(dcId: Int32, authKeyId: Int64, deviceModel: String, platform: String, systemVersion: String, apiId: Int32, appName: String, appVersion: String, ip: String, region: String)
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
switch self {
case .loginTokenInfo(let dcId, let authKeyId, let deviceModel, let platform, let systemVersion, let apiId, let appName, let appVersion, let ip, let region):
if boxed {
buffer.appendInt32(1375940666)
}
serializeInt32(dcId, buffer: buffer, boxed: false)
serializeInt64(authKeyId, buffer: buffer, boxed: false)
serializeString(deviceModel, buffer: buffer, boxed: false)
serializeString(platform, buffer: buffer, boxed: false)
serializeString(systemVersion, buffer: buffer, boxed: false)
serializeInt32(apiId, buffer: buffer, boxed: false)
serializeString(appName, buffer: buffer, boxed: false)
serializeString(appVersion, buffer: buffer, boxed: false)
serializeString(ip, buffer: buffer, boxed: false)
serializeString(region, buffer: buffer, boxed: false)
break
}
}
public func descriptionFields() -> (String, [(String, Any)]) {
switch self {
case .loginTokenInfo(let dcId, let authKeyId, let deviceModel, let platform, let systemVersion, let apiId, let appName, let appVersion, let ip, let region):
return ("loginTokenInfo", [("dcId", dcId), ("authKeyId", authKeyId), ("deviceModel", deviceModel), ("platform", platform), ("systemVersion", systemVersion), ("apiId", apiId), ("appName", appName), ("appVersion", appVersion), ("ip", ip), ("region", region)])
}
}
public static func parse_loginTokenInfo(_ reader: BufferReader) -> LoginTokenInfo? {
var _1: Int32?
_1 = reader.readInt32()
var _2: Int64?
_2 = reader.readInt64()
var _3: String?
_3 = parseString(reader)
var _4: String?
_4 = parseString(reader)
var _5: String?
_5 = parseString(reader)
var _6: Int32?
_6 = reader.readInt32()
var _7: String?
_7 = parseString(reader)
var _8: String?
_8 = parseString(reader)
var _9: String?
_9 = parseString(reader)
var _10: String?
_10 = parseString(reader)
let _c1 = _1 != nil
let _c2 = _2 != nil
let _c3 = _3 != nil
let _c4 = _4 != nil
let _c5 = _5 != nil
let _c6 = _6 != nil
let _c7 = _7 != nil
let _c8 = _8 != nil
let _c9 = _9 != nil
let _c10 = _10 != nil
if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 {
return Api.auth.LoginTokenInfo.loginTokenInfo(dcId: _1!, authKeyId: _2!, deviceModel: _3!, platform: _4!, systemVersion: _5!, apiId: _6!, appName: _7!, appVersion: _8!, ip: _9!, region: _10!)
}
else {
return nil
}
}
}
public enum SentCodeType: TypeConstructorDescription {
case sentCodeTypeApp(length: Int32)
@ -1929,6 +1859,70 @@ public struct help {
}
}
}
public enum UserInfo: TypeConstructorDescription {
case userInfoEmpty
case userInfo(message: String, entities: [Api.MessageEntity], author: String, date: Int32)
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
switch self {
case .userInfoEmpty:
if boxed {
buffer.appendInt32(-206688531)
}
break
case .userInfo(let message, let entities, let author, let date):
if boxed {
buffer.appendInt32(32192344)
}
serializeString(message, buffer: buffer, boxed: false)
buffer.appendInt32(481674261)
buffer.appendInt32(Int32(entities.count))
for item in entities {
item.serialize(buffer, true)
}
serializeString(author, buffer: buffer, boxed: false)
serializeInt32(date, buffer: buffer, boxed: false)
break
}
}
public func descriptionFields() -> (String, [(String, Any)]) {
switch self {
case .userInfoEmpty:
return ("userInfoEmpty", [])
case .userInfo(let message, let entities, let author, let date):
return ("userInfo", [("message", message), ("entities", entities), ("author", author), ("date", date)])
}
}
public static func parse_userInfoEmpty(_ reader: BufferReader) -> UserInfo? {
return Api.help.UserInfo.userInfoEmpty
}
public static func parse_userInfo(_ reader: BufferReader) -> UserInfo? {
var _1: String?
_1 = parseString(reader)
var _2: [Api.MessageEntity]?
if let _ = reader.readInt32() {
_2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.MessageEntity.self)
}
var _3: String?
_3 = parseString(reader)
var _4: Int32?
_4 = 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.help.UserInfo.userInfo(message: _1!, entities: _2!, author: _3!, date: _4!)
}
else {
return nil
}
}
}
public enum TermsOfServiceUpdate: TypeConstructorDescription {
case termsOfServiceUpdateEmpty(expires: Int32)

View File

@ -3195,6 +3195,25 @@ public extension Api {
return result
})
}
public static func getPollVotes(flags: Int32, peer: Api.InputPeer, id: Int32, option: Buffer?, offset: String?, limit: Int32) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.messages.VotesList>) {
let buffer = Buffer()
buffer.appendInt32(-1200736242)
serializeInt32(flags, buffer: buffer, boxed: false)
peer.serialize(buffer, true)
serializeInt32(id, buffer: buffer, boxed: false)
if Int(flags) & Int(1 << 0) != 0 {serializeBytes(option!, buffer: buffer, boxed: false)}
if Int(flags) & Int(1 << 1) != 0 {serializeString(offset!, buffer: buffer, boxed: false)}
serializeInt32(limit, buffer: buffer, boxed: false)
return (FunctionDescription(name: "messages.getPollVotes", parameters: [("flags", flags), ("peer", peer), ("id", id), ("option", option), ("offset", offset), ("limit", limit)]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.messages.VotesList? in
let reader = BufferReader(buffer)
var result: Api.messages.VotesList?
if let signature = reader.readInt32() {
result = Api.parse(reader, signature: signature) as? Api.messages.VotesList
}
return result
})
}
}
public struct channels {
public static func readHistory(channel: Api.InputChannel, maxId: Int32) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.Bool>) {
@ -4796,6 +4815,40 @@ public extension Api {
return result
})
}
public static func editUserInfo(userId: Api.InputUser, message: String, entities: [Api.MessageEntity]) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.help.UserInfo>) {
let buffer = Buffer()
buffer.appendInt32(1723407216)
userId.serialize(buffer, true)
serializeString(message, buffer: buffer, boxed: false)
buffer.appendInt32(481674261)
buffer.appendInt32(Int32(entities.count))
for item in entities {
item.serialize(buffer, true)
}
return (FunctionDescription(name: "help.editUserInfo", parameters: [("userId", userId), ("message", message), ("entities", entities)]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.help.UserInfo? in
let reader = BufferReader(buffer)
var result: Api.help.UserInfo?
if let signature = reader.readInt32() {
result = Api.parse(reader, signature: signature) as? Api.help.UserInfo
}
return result
})
}
public static func getUserInfo(userId: Api.InputUser) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.help.UserInfo>) {
let buffer = Buffer()
buffer.appendInt32(59377875)
userId.serialize(buffer, true)
return (FunctionDescription(name: "help.getUserInfo", parameters: [("userId", userId)]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.help.UserInfo? in
let reader = BufferReader(buffer)
var result: Api.help.UserInfo?
if let signature = reader.readInt32() {
result = Api.parse(reader, signature: signature) as? Api.help.UserInfo
}
return result
})
}
}
public struct updates {
public static func getState() -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.updates.State>) {

View File

@ -2347,7 +2347,19 @@ func replayFinalState(accountManager: AccountManager, postbox: Postbox, accountP
if let apiPoll = apiPoll {
switch apiPoll {
case let .poll(id, flags, question, answers):
updatedPoll = TelegramMediaPoll(pollId: MediaId(namespace: Namespaces.Media.CloudPoll, id: id), text: question, options: answers.map(TelegramMediaPollOption.init(apiOption:)), results: TelegramMediaPollResults(apiResults: results), isClosed: (flags & (1 << 0)) != 0)
let publicity: TelegramMediaPollPublicity
if (flags & (1 << 1)) != 0 {
publicity = .public
} else {
publicity = .anonymous
}
let kind: TelegramMediaPollKind
if (flags & (1 << 3)) != 0 {
kind = .quiz
} else {
kind = .poll(multipleAnswers: (flags & (1 << 2)) != 0)
}
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: TelegramMediaPollResults(apiResults: results), isClosed: (flags & (1 << 0)) != 0)
}
}

View File

@ -4,7 +4,7 @@ import TelegramApi
import SyncCore
extension PeerReference {
public extension PeerReference {
var id: PeerId {
switch self {
case let .user(id, _):
@ -15,7 +15,9 @@ extension PeerReference {
return PeerId(namespace: Namespaces.Peer.CloudChannel, id: id)
}
}
}
extension PeerReference {
var inputPeer: Api.InputPeer {
switch self {
case let .user(id, accessHash):

View File

@ -24,8 +24,6 @@ extension AutodownloadPresetSettings {
switch apiAutodownloadSettings {
case let .autoDownloadSettings(flags, photoSizeMax, videoSizeMax, fileSizeMax, videoUploadMaxbitrate):
self.init(disabled: (flags & (1 << 0)) != 0, photoSizeMax: photoSizeMax, videoSizeMax: videoSizeMax, fileSizeMax: fileSizeMax, preloadLargeVideo: (flags & (1 << 1)) != 0, lessDataForPhoneCalls: (flags & (1 << 3)) != 0, videoUploadMaxbitrate: videoUploadMaxbitrate)
case let .autoDownloadSettingsLegacy(flags, photoSizeMax, videoSizeMax, fileSizeMax):
self.init(disabled: (flags & (1 << 0)) != 0, photoSizeMax: photoSizeMax, videoSizeMax: videoSizeMax, fileSizeMax: fileSizeMax, preloadLargeVideo: (flags & (1 << 1)) != 0, lessDataForPhoneCalls: (flags & (1 << 3)) != 0, videoUploadMaxbitrate: 0)
}
}
}

View File

@ -140,7 +140,28 @@ func mediaContentToUpload(network: Network, postbox: Postbox, auxiliaryMethods:
if peerId.namespace == Namespaces.Peer.SecretChat {
return .fail(.generic)
}
let inputPoll = Api.InputMedia.inputMediaPoll(poll: Api.Poll.poll(id: 0, flags: 0, question: poll.text, answers: poll.options.map({ $0.apiOption })))
var pollFlags: Int32 = 0
switch poll.kind {
case let .poll(multipleAnswers):
if multipleAnswers {
pollFlags |= 1 << 2
}
case .quiz:
pollFlags |= 1 << 3
}
switch poll.publicity {
case .anonymous:
break
case .public:
pollFlags |= 1 << 1
}
var pollMediaFlags: Int32 = 0
var correctAnswers: [Buffer]?
if let correctAnswersValue = poll.correctAnswers {
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)
return .single(.content(PendingMessageUploadedContentAndReuploadInfo(content: .media(inputPoll, text), reuploadInfo: nil)))
} else {
return nil

View File

@ -10,13 +10,13 @@ public enum RequestMessageSelectPollOptionError {
case generic
}
public func requestMessageSelectPollOption(account: Account, messageId: MessageId, opaqueIdentifier: Data?) -> Signal<Never, RequestMessageSelectPollOptionError> {
public func requestMessageSelectPollOption(account: Account, messageId: MessageId, opaqueIdentifiers: [Data]) -> Signal<Never, RequestMessageSelectPollOptionError> {
return account.postbox.loadedPeerWithId(messageId.peerId)
|> take(1)
|> castError(RequestMessageSelectPollOptionError.self)
|> mapToSignal { peer in
if let inputPeer = apiInputPeer(peer) {
return account.network.request(Api.functions.messages.sendVote(peer: inputPeer, msgId: messageId.id, options: opaqueIdentifier.flatMap({ [Buffer(data: $0)] }) ?? []))
return account.network.request(Api.functions.messages.sendVote(peer: inputPeer, msgId: messageId.id, options: opaqueIdentifiers.map { Buffer(data: $0) }))
|> mapError { _ -> RequestMessageSelectPollOptionError in
return .generic
}
@ -51,7 +51,32 @@ public func requestClosePoll(postbox: Postbox, network: Network, stateManager: A
}
var flags: Int32 = 0
flags |= 1 << 14
return network.request(Api.functions.messages.editMessage(flags: flags, peer: inputPeer, id: messageId.id, message: nil, media: .inputMediaPoll(poll: .poll(id: poll.pollId.id, flags: 1 << 0, question: poll.text, answers: poll.options.map({ $0.apiOption }))), replyMarkup: nil, entities: nil, scheduleDate: nil))
var pollFlags: Int32 = 0
switch poll.kind {
case let .poll(multipleAnswers):
if multipleAnswers {
pollFlags |= 1 << 2
}
case .quiz:
pollFlags |= 1 << 3
}
switch poll.publicity {
case .anonymous:
break
case .public:
pollFlags |= 1 << 1
}
var pollMediaFlags: Int32 = 0
var correctAnswers: [Buffer]?
if let correctAnswersValue = poll.correctAnswers {
pollMediaFlags |= 1 << 0
correctAnswers = correctAnswersValue.map { Buffer(data: $0) }
}
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))
|> map(Optional.init)
|> `catch` { _ -> Signal<Api.Updates?, NoError> in
return .single(nil)
@ -64,3 +89,198 @@ public func requestClosePoll(postbox: Postbox, network: Network, stateManager: A
}
}
}
private final class PollResultsOptionContext {
private let queue: Queue
private let account: Account
private let messageId: MessageId
private let opaqueIdentifier: Data
private let disposable = MetaDisposable()
private var isLoadingMore: Bool = false
private var hasLoadedOnce: Bool = false
private var canLoadMore: Bool = true
private var nextOffset: String?
private var results: [RenderedPeer] = []
private var count: Int
let state = Promise<PollResultsOptionState>()
init(queue: Queue, account: Account, messageId: MessageId, opaqueIdentifier: Data, count: Int) {
self.queue = queue
self.account = account
self.messageId = messageId
self.opaqueIdentifier = opaqueIdentifier
self.count = count
}
deinit {
self.disposable.dispose()
}
func loadMore() {
if self.isLoadingMore {
return
}
self.isLoadingMore = true
let messageId = self.messageId
let opaqueIdentifier = self.opaqueIdentifier
let account = self.account
let nextOffset = self.nextOffset
self.disposable.set((self.account.postbox.transaction { transaction -> Api.InputPeer? in
return transaction.getPeer(messageId.peerId).flatMap(apiInputPeer)
}
|> mapToSignal { inputPeer -> Signal<([RenderedPeer], Int, String?), NoError> in
if let inputPeer = inputPeer {
return account.network.request(Api.functions.messages.getPollVotes(flags: 1 << 0, peer: inputPeer, id: messageId.id, option: Buffer(data: opaqueIdentifier), offset: nextOffset, limit: nextOffset == nil ? 15 : 50))
|> map(Optional.init)
|> `catch` { _ -> Signal<Api.messages.VotesList?, NoError> in
return .single(nil)
}
|> mapToSignal { result -> Signal<([RenderedPeer], Int, String?), NoError> in
return account.postbox.transaction { transaction -> ([RenderedPeer], Int, String?) in
guard let result = result else {
return ([], 0, nil)
}
switch result {
case let .votesList(_, count, votes, users, nextOffset):
var peers: [Peer] = []
for apiUser in users {
peers.append(TelegramUser(user: apiUser))
}
updatePeers(transaction: transaction, peers: peers, update: { _, updated in
return updated
})
var resultPeers: [RenderedPeer] = []
for vote in votes {
switch vote {
case let .messageUserVote(userId, _):
if let peer = transaction.getPeer(PeerId(namespace: Namespaces.Peer.CloudUser, id: userId)) {
resultPeers.append(RenderedPeer(peer: peer))
}
}
}
return (resultPeers, Int(count), nextOffset)
}
}
}
} else {
return .single(([], 0, nil))
}
}
|> deliverOn(self.queue)).start(next: { [weak self] peers, updatedCount, nextOffset in
guard let strongSelf = self else {
return
}
var existingIds = Set(strongSelf.results.map { $0.peerId })
for peer in peers {
if !existingIds.contains(peer.peerId) {
strongSelf.results.append(peer)
existingIds.insert(peer.peerId)
}
}
strongSelf.isLoadingMore = false
strongSelf.hasLoadedOnce = true
strongSelf.canLoadMore = nextOffset != nil
strongSelf.nextOffset = nextOffset
if strongSelf.canLoadMore {
strongSelf.count = max(updatedCount, strongSelf.results.count)
} else {
strongSelf.count = strongSelf.results.count
}
strongSelf.updateState()
}))
self.updateState()
}
func updateState() {
self.state.set(.single(PollResultsOptionState(peers: self.results, isLoadingMore: self.isLoadingMore, hasLoadedOnce: self.hasLoadedOnce, canLoadMore: self.canLoadMore, count: self.count)))
}
}
public struct PollResultsOptionState: Equatable {
public var peers: [RenderedPeer]
public var isLoadingMore: Bool
public var hasLoadedOnce: Bool
public var canLoadMore: Bool
public var count: Int
}
public struct PollResultsState: Equatable {
public var options: [Data: PollResultsOptionState]
}
private final class PollResultsContextImpl {
private let queue: Queue
private var optionContexts: [Data: PollResultsOptionContext] = [:]
let state = Promise<PollResultsState>()
init(queue: Queue, account: Account, messageId: MessageId, poll: TelegramMediaPoll) {
self.queue = queue
for option in poll.options {
var count = 0
if let voters = poll.results.voters {
for voter in voters {
if voter.opaqueIdentifier == option.opaqueIdentifier {
count = Int(voter.count)
}
}
}
self.optionContexts[option.opaqueIdentifier] = PollResultsOptionContext(queue: self.queue, account: account, messageId: messageId, opaqueIdentifier: option.opaqueIdentifier, count: count)
}
self.state.set(combineLatest(queue: self.queue, self.optionContexts.map { (opaqueIdentifier, context) -> Signal<(Data, PollResultsOptionState), NoError> in
return context.state.get()
|> map { state -> (Data, PollResultsOptionState) in
return (opaqueIdentifier, state)
}
})
|> map { states -> PollResultsState in
var options: [Data: PollResultsOptionState] = [:]
for (opaqueIdentifier, state) in states {
options[opaqueIdentifier] = state
}
return PollResultsState(options: options)
})
for (_, context) in self.optionContexts {
context.loadMore()
}
}
func loadMore(optionOpaqueIdentifier: Data) {
self.optionContexts[optionOpaqueIdentifier]?.loadMore()
}
}
public final class PollResultsContext {
private let queue: Queue = Queue()
private let impl: QueueLocalObject<PollResultsContextImpl>
public var state: Signal<PollResultsState, NoError> {
return Signal { subscriber in
let disposable = MetaDisposable()
self.impl.with { impl in
disposable.set(impl.state.get().start(next: { value in
subscriber.putNext(value)
}))
}
return disposable
}
}
public init(account: Account, messageId: MessageId, poll: TelegramMediaPoll) {
let queue = self.queue
self.impl = QueueLocalObject(queue: queue, generate: {
return PollResultsContextImpl(queue: queue, account: account, messageId: messageId, poll: poll)
})
}
public func loadMore(optionOpaqueIdentifier: Data) {
self.impl.with { impl in
impl.loadMore(optionOpaqueIdentifier: optionOpaqueIdentifier)
}
}
}

View File

@ -210,7 +210,7 @@ public class BoxedMessage: NSObject {
public class Serialization: NSObject, MTSerialization {
public func currentLayer() -> UInt {
return 108
return 109
}
public func parseMessage(_ data: Data!) -> Any! {

View File

@ -328,7 +328,19 @@ func textMediaAndExpirationTimerFromApiMedia(_ media: Api.MessageMedia?, _ peerI
case let .messageMediaPoll(poll, results):
switch poll {
case let .poll(id, flags, question, answers):
return (TelegramMediaPoll(pollId: MediaId(namespace: Namespaces.Media.CloudPoll, id: id), text: question, options: answers.map(TelegramMediaPollOption.init(apiOption:)), results: TelegramMediaPollResults(apiResults: results), isClosed: (flags & (1 << 0)) != 0), nil)
let publicity: TelegramMediaPollPublicity
if (flags & (1 << 1)) != 0 {
publicity = .public
} else {
publicity = .anonymous
}
let kind: TelegramMediaPollKind
if (flags & (1 << 3)) != 0 {
kind = .quiz
} else {
kind = .poll(multipleAnswers: (flags & (1 << 2)) != 0)
}
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)
}
}
}

View File

@ -21,7 +21,7 @@ extension TelegramMediaPollOptionVoters {
init(apiVoters: Api.PollAnswerVoters) {
switch apiVoters {
case let .pollAnswerVoters(flags, option, voters):
self.init(selected: (flags & (1 << 0)) != 0, opaqueIdentifier: option.makeData(), count: voters)
self.init(selected: (flags & (1 << 0)) != 0, opaqueIdentifier: option.makeData(), count: voters, isCorrect: (flags & (1 << 1)) != 0)
}
}
}
@ -29,8 +29,10 @@ extension TelegramMediaPollOptionVoters {
extension TelegramMediaPollResults {
init(apiResults: Api.PollResults) {
switch apiResults {
case let .pollResults(_, results, totalVoters):
self.init(voters: results.flatMap({ $0.map(TelegramMediaPollOptionVoters.init(apiVoters:)) }), totalVoters: totalVoters)
case let .pollResults(_, results, totalVoters, recentVoters):
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) }
} ?? [])
}
}
}

View File

@ -134,7 +134,7 @@ public func donateSendMessageIntent(account: Account, sharedContext: SharedAccou
if peer.id == account.peerId {
signals.append(.single((peer, subject, savedMessagesAvatar)))
} else {
let peerAndAvatar = (peerAvatarImage(account: account, peer: peer, authorOfMessage: nil, representation: peer.smallProfileImage, round: false) ?? .single(nil))
let peerAndAvatar = (peerAvatarImage(account: account, peerReference: PeerReference(peer), authorOfMessage: nil, representation: peer.smallProfileImage, round: false) ?? .single(nil))
|> map { avatarImage in
return (peer, subject, avatarImage)
}

View File

@ -387,8 +387,8 @@ public func makeDefaultDarkPresentationTheme(extendingThemeReference: Presentati
)
let message = PresentationThemeChatMessage(
incoming: PresentationThemePartedColors(bubble: PresentationThemeBubbleColor(withWallpaper: PresentationThemeBubbleColorComponents(fill: UIColor(rgb: 0x262628), highlightedFill: UIColor(rgb: 0x353539), stroke: UIColor(rgb: 0x262628)), withoutWallpaper: PresentationThemeBubbleColorComponents(fill: UIColor(rgb: 0x262628), highlightedFill: UIColor(rgb: 0x353539), stroke: UIColor(rgb: 0x262628))), primaryTextColor: UIColor(rgb: 0xffffff), secondaryTextColor: UIColor(rgb: 0xffffff, alpha: 0.5), linkTextColor: UIColor(rgb: 0xffffff), linkHighlightColor: UIColor(rgb: 0xffffff, alpha: 0.5), scamColor: UIColor(rgb: 0xeb5545), textHighlightColor: UIColor(rgb: 0xf5c038), accentTextColor: UIColor(rgb: 0xffffff), accentControlColor: UIColor(rgb: 0xffffff), mediaActiveControlColor: UIColor(rgb: 0xffffff), mediaInactiveControlColor: UIColor(rgb: 0xffffff, alpha: 0.4), mediaControlInnerBackgroundColor: UIColor(rgb: 0x262628), pendingActivityColor: UIColor(rgb: 0xffffff, alpha: 0.5), fileTitleColor: UIColor(rgb: 0xffffff), fileDescriptionColor: UIColor(rgb: 0xffffff, alpha: 0.5), fileDurationColor: UIColor(rgb: 0xffffff, alpha: 0.5), mediaPlaceholderColor: UIColor(rgb: 0x1f1f1f).mixedWith(UIColor(rgb: 0xffffff), alpha: 0.05), polls: PresentationThemeChatBubblePolls(radioButton: UIColor(rgb: 0x737373), radioProgress: UIColor(rgb: 0xffffff), highlight: UIColor(rgb: 0xffffff, alpha: 0.12), separator: UIColor(rgb: 0x000000), bar: UIColor(rgb: 0xffffff)), actionButtonsFillColor: PresentationThemeVariableColor(withWallpaper: UIColor(rgb: 0x000000, alpha: 0.5), withoutWallpaper: UIColor(rgb: 0x000000, alpha: 0.5)), actionButtonsStrokeColor: PresentationThemeVariableColor(color: UIColor(rgb: 0xb2b2b2, alpha: 0.18)), actionButtonsTextColor: PresentationThemeVariableColor(color: UIColor(rgb: 0xffffff)), textSelectionColor: UIColor(rgb: 0xffffff, alpha: 0.2), textSelectionKnobColor: UIColor(rgb: 0xffffff)),
outgoing: PresentationThemePartedColors(bubble: PresentationThemeBubbleColor(withWallpaper: PresentationThemeBubbleColorComponents(fill: UIColor(rgb: 0x313131), gradientFill: UIColor(rgb: 0x313131), highlightedFill: UIColor(rgb: 0x464646), stroke: UIColor(rgb: 0x313131)), withoutWallpaper: PresentationThemeBubbleColorComponents(fill: UIColor(rgb: 0x313131), gradientFill: UIColor(rgb: 0x313131), highlightedFill: UIColor(rgb: 0x464646), stroke: UIColor(rgb: 0x313131))), primaryTextColor: UIColor(rgb: 0xffffff), secondaryTextColor: UIColor(rgb: 0xffffff, alpha: 0.5), linkTextColor: UIColor(rgb: 0xffffff), linkHighlightColor: UIColor(rgb: 0xffffff, alpha: 0.5), scamColor: UIColor(rgb: 0xeb5545), textHighlightColor: UIColor(rgb: 0xf5c038), accentTextColor: UIColor(rgb: 0xffffff), accentControlColor: UIColor(rgb: 0xffffff), mediaActiveControlColor: UIColor(rgb: 0xffffff), mediaInactiveControlColor: UIColor(rgb: 0xffffff, alpha: 0.5), mediaControlInnerBackgroundColor: UIColor(rgb: 0x313131), pendingActivityColor: UIColor(rgb: 0xffffff, alpha: 0.5), fileTitleColor: UIColor(rgb: 0xffffff), fileDescriptionColor: UIColor(rgb: 0xffffff, alpha: 0.5), fileDurationColor: UIColor(rgb: 0xffffff, alpha: 0.5), mediaPlaceholderColor: UIColor(rgb: 0x313131).mixedWith(UIColor(rgb: 0xffffff), alpha: 0.05), polls: PresentationThemeChatBubblePolls(radioButton: UIColor(rgb: 0xffffff), radioProgress: UIColor(rgb: 0xffffff), highlight: UIColor(rgb: 0xffffff).withAlphaComponent(0.12), separator: UIColor(rgb: 0xffffff, alpha: 0.5), bar: UIColor(rgb: 0xffffff)), actionButtonsFillColor: PresentationThemeVariableColor(withWallpaper: UIColor(rgb: 0x000000, alpha: 0.5), withoutWallpaper: UIColor(rgb: 0x000000, alpha: 0.5)), actionButtonsStrokeColor: PresentationThemeVariableColor(color: UIColor(rgb: 0xb2b2b2, alpha: 0.18)), actionButtonsTextColor: PresentationThemeVariableColor(color: UIColor(rgb: 0xffffff)), textSelectionColor: UIColor(rgb: 0xffffff, alpha: 0.2), textSelectionKnobColor: UIColor(rgb: 0xffffff)),
incoming: PresentationThemePartedColors(bubble: PresentationThemeBubbleColor(withWallpaper: PresentationThemeBubbleColorComponents(fill: UIColor(rgb: 0x262628), highlightedFill: UIColor(rgb: 0x353539), stroke: UIColor(rgb: 0x262628)), withoutWallpaper: PresentationThemeBubbleColorComponents(fill: UIColor(rgb: 0x262628), highlightedFill: UIColor(rgb: 0x353539), stroke: UIColor(rgb: 0x262628))), primaryTextColor: UIColor(rgb: 0xffffff), secondaryTextColor: UIColor(rgb: 0xffffff, alpha: 0.5), linkTextColor: UIColor(rgb: 0xffffff), linkHighlightColor: UIColor(rgb: 0xffffff, alpha: 0.5), scamColor: UIColor(rgb: 0xeb5545), textHighlightColor: UIColor(rgb: 0xf5c038), accentTextColor: UIColor(rgb: 0xffffff), accentControlColor: UIColor(rgb: 0xffffff), accentControlDisabledColor: UIColor(rgb: 0xffffff, alpha: 0.5), mediaActiveControlColor: UIColor(rgb: 0xffffff), mediaInactiveControlColor: UIColor(rgb: 0xffffff, alpha: 0.4), mediaControlInnerBackgroundColor: UIColor(rgb: 0x262628), pendingActivityColor: UIColor(rgb: 0xffffff, alpha: 0.5), fileTitleColor: UIColor(rgb: 0xffffff), fileDescriptionColor: UIColor(rgb: 0xffffff, alpha: 0.5), fileDurationColor: UIColor(rgb: 0xffffff, alpha: 0.5), mediaPlaceholderColor: UIColor(rgb: 0x1f1f1f).mixedWith(UIColor(rgb: 0xffffff), alpha: 0.05), polls: PresentationThemeChatBubblePolls(radioButton: UIColor(rgb: 0x737373), radioProgress: UIColor(rgb: 0xffffff), highlight: UIColor(rgb: 0xffffff, alpha: 0.12), separator: UIColor(rgb: 0x000000), bar: UIColor(rgb: 0xffffff), barIconForeground: .white, barPositive: UIColor(rgb: 0x00A700), barNegative: UIColor(rgb: 0xFE3824)), actionButtonsFillColor: PresentationThemeVariableColor(withWallpaper: UIColor(rgb: 0x000000, alpha: 0.5), withoutWallpaper: UIColor(rgb: 0x000000, alpha: 0.5)), actionButtonsStrokeColor: PresentationThemeVariableColor(color: UIColor(rgb: 0xb2b2b2, alpha: 0.18)), actionButtonsTextColor: PresentationThemeVariableColor(color: UIColor(rgb: 0xffffff)), textSelectionColor: UIColor(rgb: 0xffffff, alpha: 0.2), textSelectionKnobColor: UIColor(rgb: 0xffffff)),
outgoing: PresentationThemePartedColors(bubble: PresentationThemeBubbleColor(withWallpaper: PresentationThemeBubbleColorComponents(fill: UIColor(rgb: 0x313131), gradientFill: UIColor(rgb: 0x313131), highlightedFill: UIColor(rgb: 0x464646), stroke: UIColor(rgb: 0x313131)), withoutWallpaper: PresentationThemeBubbleColorComponents(fill: UIColor(rgb: 0x313131), gradientFill: UIColor(rgb: 0x313131), highlightedFill: UIColor(rgb: 0x464646), stroke: UIColor(rgb: 0x313131))), primaryTextColor: UIColor(rgb: 0xffffff), secondaryTextColor: UIColor(rgb: 0xffffff, alpha: 0.5), linkTextColor: UIColor(rgb: 0xffffff), linkHighlightColor: UIColor(rgb: 0xffffff, alpha: 0.5), scamColor: UIColor(rgb: 0xeb5545), textHighlightColor: UIColor(rgb: 0xf5c038), accentTextColor: UIColor(rgb: 0xffffff), accentControlColor: UIColor(rgb: 0xffffff), accentControlDisabledColor: UIColor(rgb: 0xffffff, alpha: 0.5), mediaActiveControlColor: UIColor(rgb: 0xffffff), mediaInactiveControlColor: UIColor(rgb: 0xffffff, alpha: 0.5), mediaControlInnerBackgroundColor: UIColor(rgb: 0x313131), pendingActivityColor: UIColor(rgb: 0xffffff, alpha: 0.5), fileTitleColor: UIColor(rgb: 0xffffff), fileDescriptionColor: UIColor(rgb: 0xffffff, alpha: 0.5), fileDurationColor: UIColor(rgb: 0xffffff, alpha: 0.5), mediaPlaceholderColor: UIColor(rgb: 0x313131).mixedWith(UIColor(rgb: 0xffffff), alpha: 0.05), polls: PresentationThemeChatBubblePolls(radioButton: UIColor(rgb: 0xffffff), radioProgress: UIColor(rgb: 0xffffff), highlight: UIColor(rgb: 0xffffff).withAlphaComponent(0.12), separator: UIColor(rgb: 0xffffff, alpha: 0.5), bar: UIColor(rgb: 0xffffff), barIconForeground: .white, barPositive: UIColor(rgb: 0xffffff), barNegative: UIColor(rgb: 0xffffff)), actionButtonsFillColor: PresentationThemeVariableColor(withWallpaper: UIColor(rgb: 0x000000, alpha: 0.5), withoutWallpaper: UIColor(rgb: 0x000000, alpha: 0.5)), actionButtonsStrokeColor: PresentationThemeVariableColor(color: UIColor(rgb: 0xb2b2b2, alpha: 0.18)), actionButtonsTextColor: PresentationThemeVariableColor(color: UIColor(rgb: 0xffffff)), textSelectionColor: UIColor(rgb: 0xffffff, alpha: 0.2), textSelectionKnobColor: UIColor(rgb: 0xffffff)),
freeform: PresentationThemeBubbleColor(withWallpaper: PresentationThemeBubbleColorComponents(fill: UIColor(rgb: 0x1f1f1f), highlightedFill: UIColor(rgb: 0x2a2a2a), stroke: UIColor(rgb: 0x1f1f1f)), withoutWallpaper: PresentationThemeBubbleColorComponents(fill: UIColor(rgb: 0x1f1f1f), highlightedFill: UIColor(rgb: 0x2a2a2a), stroke: UIColor(rgb: 0x1f1f1f))),
infoPrimaryTextColor: UIColor(rgb: 0xffffff),
infoLinkTextColor: UIColor(rgb: 0xffffff),

View File

@ -643,8 +643,8 @@ public func makeDefaultDarkTintedPresentationTheme(extendingThemeReference: Pres
let buttonStrokeColor = accentColor.withMultiplied(hue: 1.014, saturation: 0.56, brightness: 0.64).withAlphaComponent(0.15)
let message = PresentationThemeChatMessage(
incoming: PresentationThemePartedColors(bubble: PresentationThemeBubbleColor(withWallpaper: PresentationThemeBubbleColorComponents(fill: mainBackgroundColor, highlightedFill: highlightedIncomingBubbleColor, stroke: mainBackgroundColor), withoutWallpaper: PresentationThemeBubbleColorComponents(fill: mainBackgroundColor, highlightedFill: highlightedIncomingBubbleColor, stroke: mainBackgroundColor)), primaryTextColor: .white, secondaryTextColor: mainSecondaryTextColor.withAlphaComponent(0.5), linkTextColor: accentColor, linkHighlightColor: accentColor.withAlphaComponent(0.5), scamColor: UIColor(rgb: 0xff6767), textHighlightColor: UIColor(rgb: 0xf5c038), accentTextColor: accentColor, accentControlColor: accentColor, mediaActiveControlColor: accentColor, mediaInactiveControlColor: accentColor.withAlphaComponent(0.5), mediaControlInnerBackgroundColor: mainBackgroundColor, pendingActivityColor: mainSecondaryTextColor.withAlphaComponent(0.5), fileTitleColor: accentColor, fileDescriptionColor: mainSecondaryTextColor.withAlphaComponent(0.5), fileDurationColor: mainSecondaryTextColor.withAlphaComponent(0.5), mediaPlaceholderColor: accentColor.withMultiplied(hue: 1.019, saturation: 0.585, brightness: 0.23), polls: PresentationThemeChatBubblePolls(radioButton: accentColor.withMultiplied(hue: 0.995, saturation: 0.317, brightness: 0.51), radioProgress: accentColor, highlight: accentColor.withAlphaComponent(0.12), separator: mainSeparatorColor, bar: accentColor), actionButtonsFillColor: PresentationThemeVariableColor(withWallpaper: additionalBackgroundColor.withAlphaComponent(0.5), withoutWallpaper: additionalBackgroundColor.withAlphaComponent(0.5)), actionButtonsStrokeColor: PresentationThemeVariableColor(color: buttonStrokeColor), actionButtonsTextColor: PresentationThemeVariableColor(color: .white), textSelectionColor: accentColor.withAlphaComponent(0.2), textSelectionKnobColor: accentColor),
outgoing: PresentationThemePartedColors(bubble: PresentationThemeBubbleColor(withWallpaper: PresentationThemeBubbleColorComponents(fill: outgoingBubbleFillColor, gradientFill: outgoingBubbleFillGradientColor, highlightedFill: highlightedOutgoingBubbleColor, stroke: outgoingBubbleFillColor), withoutWallpaper: PresentationThemeBubbleColorComponents(fill: outgoingBubbleFillColor, gradientFill: outgoingBubbleFillGradientColor, highlightedFill: highlightedOutgoingBubbleColor, stroke: outgoingBubbleFillColor)), primaryTextColor: outgoingPrimaryTextColor, secondaryTextColor: outgoingSecondaryTextColor, linkTextColor: outgoingLinkTextColor, linkHighlightColor: UIColor.white.withAlphaComponent(0.5), scamColor: outgoingScamColor, textHighlightColor: UIColor(rgb: 0xf5c038), accentTextColor: outgoingPrimaryTextColor, accentControlColor: outgoingPrimaryTextColor, mediaActiveControlColor: outgoingPrimaryTextColor, mediaInactiveControlColor: outgoingSecondaryTextColor, mediaControlInnerBackgroundColor: outgoingBubbleFillColor, pendingActivityColor: outgoingSecondaryTextColor, fileTitleColor: outgoingPrimaryTextColor, fileDescriptionColor: outgoingSecondaryTextColor, fileDurationColor: outgoingSecondaryTextColor, mediaPlaceholderColor: accentColor.withMultiplied(hue: 1.019, saturation: 0.804, brightness: 0.51), polls: PresentationThemeChatBubblePolls(radioButton: outgoingPrimaryTextColor, radioProgress: accentColor.withMultiplied(hue: 0.99, saturation: 0.56, brightness: 1.0), highlight: accentColor.withMultiplied(hue: 0.99, saturation: 0.56, brightness: 1.0).withAlphaComponent(0.12), separator: mainSeparatorColor, bar: outgoingPrimaryTextColor), actionButtonsFillColor: PresentationThemeVariableColor(withWallpaper: additionalBackgroundColor.withAlphaComponent(0.5), withoutWallpaper: additionalBackgroundColor.withAlphaComponent(0.5)), actionButtonsStrokeColor: PresentationThemeVariableColor(color: buttonStrokeColor), actionButtonsTextColor: PresentationThemeVariableColor(color: .white), textSelectionColor: UIColor.white.withAlphaComponent(0.2), textSelectionKnobColor: UIColor.white),
incoming: PresentationThemePartedColors(bubble: PresentationThemeBubbleColor(withWallpaper: PresentationThemeBubbleColorComponents(fill: mainBackgroundColor, highlightedFill: highlightedIncomingBubbleColor, stroke: mainBackgroundColor), withoutWallpaper: PresentationThemeBubbleColorComponents(fill: mainBackgroundColor, highlightedFill: highlightedIncomingBubbleColor, stroke: mainBackgroundColor)), primaryTextColor: .white, secondaryTextColor: mainSecondaryTextColor.withAlphaComponent(0.5), linkTextColor: accentColor, linkHighlightColor: accentColor.withAlphaComponent(0.5), scamColor: UIColor(rgb: 0xff6767), textHighlightColor: UIColor(rgb: 0xf5c038), accentTextColor: accentColor, accentControlColor: accentColor, accentControlDisabledColor: mainSecondaryTextColor.withAlphaComponent(0.5), mediaActiveControlColor: accentColor, mediaInactiveControlColor: accentColor.withAlphaComponent(0.5), mediaControlInnerBackgroundColor: mainBackgroundColor, pendingActivityColor: mainSecondaryTextColor.withAlphaComponent(0.5), fileTitleColor: accentColor, fileDescriptionColor: mainSecondaryTextColor.withAlphaComponent(0.5), fileDurationColor: mainSecondaryTextColor.withAlphaComponent(0.5), mediaPlaceholderColor: accentColor.withMultiplied(hue: 1.019, saturation: 0.585, brightness: 0.23), polls: PresentationThemeChatBubblePolls(radioButton: accentColor.withMultiplied(hue: 0.995, saturation: 0.317, brightness: 0.51), radioProgress: accentColor, highlight: accentColor.withAlphaComponent(0.12), separator: mainSeparatorColor, bar: accentColor, barIconForeground: .white, barPositive: UIColor(rgb: 0x00A700), barNegative: UIColor(rgb: 0xFE3824)), actionButtonsFillColor: PresentationThemeVariableColor(withWallpaper: additionalBackgroundColor.withAlphaComponent(0.5), withoutWallpaper: additionalBackgroundColor.withAlphaComponent(0.5)), actionButtonsStrokeColor: PresentationThemeVariableColor(color: buttonStrokeColor), actionButtonsTextColor: PresentationThemeVariableColor(color: .white), textSelectionColor: accentColor.withAlphaComponent(0.2), textSelectionKnobColor: accentColor),
outgoing: PresentationThemePartedColors(bubble: PresentationThemeBubbleColor(withWallpaper: PresentationThemeBubbleColorComponents(fill: outgoingBubbleFillColor, gradientFill: outgoingBubbleFillGradientColor, highlightedFill: highlightedOutgoingBubbleColor, stroke: outgoingBubbleFillColor), withoutWallpaper: PresentationThemeBubbleColorComponents(fill: outgoingBubbleFillColor, gradientFill: outgoingBubbleFillGradientColor, highlightedFill: highlightedOutgoingBubbleColor, stroke: outgoingBubbleFillColor)), primaryTextColor: outgoingPrimaryTextColor, secondaryTextColor: outgoingSecondaryTextColor, linkTextColor: outgoingLinkTextColor, linkHighlightColor: UIColor.white.withAlphaComponent(0.5), scamColor: outgoingScamColor, textHighlightColor: UIColor(rgb: 0xf5c038), accentTextColor: outgoingPrimaryTextColor, accentControlColor: outgoingPrimaryTextColor, accentControlDisabledColor: mainSecondaryTextColor.withAlphaComponent(0.5), mediaActiveControlColor: outgoingPrimaryTextColor, mediaInactiveControlColor: outgoingSecondaryTextColor, mediaControlInnerBackgroundColor: outgoingBubbleFillColor, pendingActivityColor: outgoingSecondaryTextColor, fileTitleColor: outgoingPrimaryTextColor, fileDescriptionColor: outgoingSecondaryTextColor, fileDurationColor: outgoingSecondaryTextColor, mediaPlaceholderColor: accentColor.withMultiplied(hue: 1.019, saturation: 0.804, brightness: 0.51), polls: PresentationThemeChatBubblePolls(radioButton: outgoingPrimaryTextColor, radioProgress: accentColor.withMultiplied(hue: 0.99, saturation: 0.56, brightness: 1.0), highlight: accentColor.withMultiplied(hue: 0.99, saturation: 0.56, brightness: 1.0).withAlphaComponent(0.12), separator: mainSeparatorColor, bar: outgoingPrimaryTextColor, barIconForeground: .white, barPositive: outgoingPrimaryTextColor, barNegative: outgoingPrimaryTextColor), actionButtonsFillColor: PresentationThemeVariableColor(withWallpaper: additionalBackgroundColor.withAlphaComponent(0.5), withoutWallpaper: additionalBackgroundColor.withAlphaComponent(0.5)), actionButtonsStrokeColor: PresentationThemeVariableColor(color: buttonStrokeColor), actionButtonsTextColor: PresentationThemeVariableColor(color: .white), textSelectionColor: UIColor.white.withAlphaComponent(0.2), textSelectionKnobColor: UIColor.white),
freeform: PresentationThemeBubbleColor(withWallpaper: PresentationThemeBubbleColorComponents(fill: mainBackgroundColor, highlightedFill: highlightedIncomingBubbleColor, stroke: mainBackgroundColor), withoutWallpaper: PresentationThemeBubbleColorComponents(fill: mainBackgroundColor, highlightedFill: highlightedIncomingBubbleColor, stroke: mainBackgroundColor)),
infoPrimaryTextColor: UIColor(rgb: 0xffffff),
infoLinkTextColor: accentColor,

View File

@ -269,7 +269,7 @@ public func customizeDefaultDayTheme(theme: PresentationTheme, editing: Bool, ti
fileDescriptionColor: outgoingFileDescriptionColor,
fileDurationColor: outgoingFileDurationColor,
mediaPlaceholderColor: day ? accentColor?.withMultipliedBrightnessBy(0.95) : outgoingMediaPlaceholderColor,
polls: chat.message.outgoing.polls.withUpdated(radioButton: outgoingPollsButtonColor, radioProgress: outgoingPollsProgressColor, highlight: outgoingPollsProgressColor?.withAlphaComponent(0.12), separator: outgoingPollsButtonColor, bar: outgoingPollsProgressColor),
polls: chat.message.outgoing.polls.withUpdated(radioButton: outgoingPollsButtonColor, radioProgress: outgoingPollsProgressColor, highlight: outgoingPollsProgressColor?.withAlphaComponent(0.12), separator: outgoingPollsButtonColor, bar: outgoingPollsProgressColor, barPositive: outgoingPollsProgressColor, barNegative: outgoingPollsProgressColor),
actionButtonsFillColor: chat.message.outgoing.actionButtonsFillColor.withUpdated(withWallpaper: serviceBackgroundColor),
actionButtonsStrokeColor: day ? chat.message.outgoing.actionButtonsStrokeColor.withUpdated(withoutWallpaper: accentColor) : nil,
actionButtonsTextColor: day ? chat.message.outgoing.actionButtonsTextColor.withUpdated(withoutWallpaper: accentColor) : nil,
@ -494,6 +494,7 @@ public func makeDefaultDayPresentationTheme(extendingThemeReference: Presentatio
textHighlightColor: UIColor(rgb: 0xffe438),
accentTextColor: UIColor(rgb: 0x007ee5),
accentControlColor: UIColor(rgb: 0x007ee5),
accentControlDisabledColor: UIColor(rgb: 0x525252, alpha: 0.6),
mediaActiveControlColor: UIColor(rgb: 0x007ee5),
mediaInactiveControlColor: UIColor(rgb: 0xcacaca),
mediaControlInnerBackgroundColor: UIColor(rgb: 0xffffff),
@ -502,7 +503,7 @@ public func makeDefaultDayPresentationTheme(extendingThemeReference: Presentatio
fileDescriptionColor: UIColor(rgb: 0x999999),
fileDurationColor: UIColor(rgb: 0x525252, alpha: 0.6),
mediaPlaceholderColor: UIColor(rgb: 0xe8ecf0),
polls: PresentationThemeChatBubblePolls(radioButton: UIColor(rgb: 0xc8c7cc), radioProgress: UIColor(rgb: 0x007ee5), highlight: UIColor(rgb: 0x007ee5, alpha: 0.08), separator: UIColor(rgb: 0xc8c7cc), bar: UIColor(rgb: 0x007ee5)),
polls: PresentationThemeChatBubblePolls(radioButton: UIColor(rgb: 0xc8c7cc), radioProgress: UIColor(rgb: 0x007ee5), highlight: UIColor(rgb: 0x007ee5, alpha: 0.08), separator: UIColor(rgb: 0xc8c7cc), bar: UIColor(rgb: 0x007ee5), barIconForeground: .white, barPositive: UIColor(rgb: 0x2dba45), barNegative: UIColor(rgb: 0xFE3824)),
actionButtonsFillColor: PresentationThemeVariableColor(withWallpaper: serviceBackgroundColor, withoutWallpaper: UIColor(rgb: 0x596e89, alpha: 0.35)), actionButtonsStrokeColor: PresentationThemeVariableColor(color: .clear),
actionButtonsTextColor: PresentationThemeVariableColor(color: UIColor(rgb: 0xffffff)), textSelectionColor: UIColor(rgb: 0x007ee5, alpha: 0.2), textSelectionKnobColor: UIColor(rgb: 0x007ee5)),
outgoing: PresentationThemePartedColors(
@ -515,6 +516,7 @@ public func makeDefaultDayPresentationTheme(extendingThemeReference: Presentatio
textHighlightColor: UIColor(rgb: 0xffe438),
accentTextColor: UIColor(rgb: 0x00a700),
accentControlColor: UIColor(rgb: 0x3fc33b),
accentControlDisabledColor: UIColor(rgb: 0x00A700).withAlphaComponent(0.7),
mediaActiveControlColor: UIColor(rgb: 0x3fc33b),
mediaInactiveControlColor: UIColor(rgb: 0x93d987),
mediaControlInnerBackgroundColor: UIColor(rgb: 0xe1ffc7),
@ -523,7 +525,7 @@ public func makeDefaultDayPresentationTheme(extendingThemeReference: Presentatio
fileDescriptionColor: UIColor(rgb: 0x6fb26a),
fileDurationColor: UIColor(rgb: 0x008c09, alpha: 0.8),
mediaPlaceholderColor: UIColor(rgb: 0xd2f2b6),
polls: PresentationThemeChatBubblePolls(radioButton: UIColor(rgb: 0x93d987), radioProgress: UIColor(rgb: 0x3fc33b), highlight: UIColor(rgb: 0x3fc33b).withAlphaComponent(0.08), separator: UIColor(rgb: 0x93d987), bar: UIColor(rgb: 0x3fc33b)),
polls: PresentationThemeChatBubblePolls(radioButton: UIColor(rgb: 0x93d987), radioProgress: UIColor(rgb: 0x3fc33b), highlight: UIColor(rgb: 0x3fc33b).withAlphaComponent(0.08), separator: UIColor(rgb: 0x93d987), bar: UIColor(rgb: 0x00A700), barIconForeground: .white, barPositive: UIColor(rgb: 0x00A700), barNegative: UIColor(rgb: 0x00A700)),
actionButtonsFillColor: PresentationThemeVariableColor(withWallpaper: serviceBackgroundColor, withoutWallpaper: UIColor(rgb: 0x596e89, alpha: 0.35)),
actionButtonsStrokeColor: PresentationThemeVariableColor(color: .clear),
actionButtonsTextColor: PresentationThemeVariableColor(color: UIColor(rgb: 0xffffff)),
@ -555,6 +557,7 @@ public func makeDefaultDayPresentationTheme(extendingThemeReference: Presentatio
textHighlightColor: UIColor(rgb: 0xffc738),
accentTextColor: UIColor(rgb: 0x007ee5),
accentControlColor: UIColor(rgb: 0x007ee5),
accentControlDisabledColor: UIColor(rgb: 0x525252, alpha: 0.6),
mediaActiveControlColor: UIColor(rgb: 0x007ee5),
mediaInactiveControlColor: UIColor(rgb: 0xcacaca),
mediaControlInnerBackgroundColor: UIColor(rgb: 0xffffff),
@ -563,7 +566,7 @@ public func makeDefaultDayPresentationTheme(extendingThemeReference: Presentatio
fileDescriptionColor: UIColor(rgb: 0x999999),
fileDurationColor: UIColor(rgb: 0x525252, alpha: 0.6),
mediaPlaceholderColor: UIColor(rgb: 0xffffff).withMultipliedBrightnessBy(0.95),
polls: PresentationThemeChatBubblePolls(radioButton: UIColor(rgb: 0xc8c7cc), radioProgress: UIColor(rgb: 0x007ee5), highlight: UIColor(rgb: 0x007ee5, alpha: 0.12), separator: UIColor(rgb: 0xc8c7cc), bar: UIColor(rgb: 0x007ee5)),
polls: PresentationThemeChatBubblePolls(radioButton: UIColor(rgb: 0xc8c7cc), radioProgress: UIColor(rgb: 0x007ee5), highlight: UIColor(rgb: 0x007ee5, alpha: 0.12), separator: UIColor(rgb: 0xc8c7cc), bar: UIColor(rgb: 0x007ee5), barIconForeground: .white, barPositive: UIColor(rgb: 0x00A700), barNegative: UIColor(rgb: 0xFE3824)),
actionButtonsFillColor: PresentationThemeVariableColor(withWallpaper: serviceBackgroundColor, withoutWallpaper: UIColor(rgb: 0xffffff, alpha: 0.8)),
actionButtonsStrokeColor: PresentationThemeVariableColor(withWallpaper: .clear, withoutWallpaper: UIColor(rgb: 0x007ee5)),
actionButtonsTextColor: PresentationThemeVariableColor(withWallpaper: UIColor(rgb: 0xffffff), withoutWallpaper: UIColor(rgb: 0x007ee5)),
@ -579,6 +582,7 @@ public func makeDefaultDayPresentationTheme(extendingThemeReference: Presentatio
textHighlightColor: UIColor(rgb: 0xffc738),
accentTextColor: UIColor(rgb: 0xffffff),
accentControlColor: UIColor(rgb: 0xffffff),
accentControlDisabledColor: UIColor(rgb: 0xffffff).withAlphaComponent(0.5),
mediaActiveControlColor: UIColor(rgb: 0xffffff),
mediaInactiveControlColor: UIColor(rgb: 0xffffff, alpha: 0.65),
mediaControlInnerBackgroundColor: .clear,
@ -587,7 +591,7 @@ public func makeDefaultDayPresentationTheme(extendingThemeReference: Presentatio
fileDescriptionColor: UIColor(rgb: 0xffffff, alpha: 0.65),
fileDurationColor: UIColor(rgb: 0xffffff, alpha: 0.65),
mediaPlaceholderColor: UIColor(rgb: 0x0077d9),
polls: PresentationThemeChatBubblePolls(radioButton: UIColor(rgb: 0xffffff, alpha: 0.65), radioProgress: UIColor(rgb: 0xffffff), highlight: UIColor(rgb: 0xffffff, alpha: 0.12), separator: UIColor(rgb: 0xffffff, alpha: 0.65), bar: UIColor(rgb: 0xffffff)),
polls: PresentationThemeChatBubblePolls(radioButton: UIColor(rgb: 0xffffff, alpha: 0.65), radioProgress: UIColor(rgb: 0xffffff), highlight: UIColor(rgb: 0xffffff, alpha: 0.12), separator: UIColor(rgb: 0xffffff, alpha: 0.65), bar: UIColor(rgb: 0xffffff), barIconForeground: .white, barPositive: UIColor(rgb: 0xffffff), barNegative: UIColor(rgb: 0xffffff)),
actionButtonsFillColor: PresentationThemeVariableColor(withWallpaper: serviceBackgroundColor, withoutWallpaper: UIColor(rgb: 0xffffff, alpha: 0.8)),
actionButtonsStrokeColor: PresentationThemeVariableColor(withWallpaper: .clear, withoutWallpaper: UIColor(rgb: 0x007ee5)),
actionButtonsTextColor: PresentationThemeVariableColor(withWallpaper: UIColor(rgb: 0xffffff), withoutWallpaper: UIColor(rgb: 0x007ee5)),

View File

@ -631,17 +631,23 @@ public final class PresentationThemeChatBubblePolls {
public let highlight: UIColor
public let separator: UIColor
public let bar: UIColor
public let barIconForeground: UIColor
public let barPositive: UIColor
public let barNegative: UIColor
public init(radioButton: UIColor, radioProgress: UIColor, highlight: UIColor, separator: UIColor, bar: UIColor) {
public init(radioButton: UIColor, radioProgress: UIColor, highlight: UIColor, separator: UIColor, bar: UIColor, barIconForeground: UIColor, barPositive: UIColor, barNegative: UIColor) {
self.radioButton = radioButton
self.radioProgress = radioProgress
self.highlight = highlight
self.separator = separator
self.bar = bar
self.barIconForeground = barIconForeground
self.barPositive = barPositive
self.barNegative = barNegative
}
public func withUpdated(radioButton: UIColor? = nil, radioProgress: UIColor? = nil, highlight: UIColor? = nil, separator: UIColor? = nil, bar: UIColor? = nil) -> PresentationThemeChatBubblePolls {
return PresentationThemeChatBubblePolls(radioButton: radioButton ?? self.radioButton, radioProgress: radioProgress ?? self.radioProgress, highlight: highlight ?? self.highlight, separator: separator ?? self.separator, bar: bar ?? self.bar)
public func withUpdated(radioButton: UIColor? = nil, radioProgress: UIColor? = nil, highlight: UIColor? = nil, separator: UIColor? = nil, bar: UIColor? = nil, barIconForeground: UIColor? = nil, barPositive: UIColor? = nil, barNegative: UIColor? = nil) -> PresentationThemeChatBubblePolls {
return PresentationThemeChatBubblePolls(radioButton: radioButton ?? self.radioButton, radioProgress: radioProgress ?? self.radioProgress, highlight: highlight ?? self.highlight, separator: separator ?? self.separator, bar: bar ?? self.bar, barIconForeground: barIconForeground ?? self.barIconForeground, barPositive: barPositive ?? self.barPositive, barNegative: barNegative ?? self.barNegative)
}
}
@ -655,6 +661,7 @@ public final class PresentationThemePartedColors {
public let textHighlightColor: UIColor
public let accentTextColor: UIColor
public let accentControlColor: UIColor
public let accentControlDisabledColor: UIColor
public let mediaActiveControlColor: UIColor
public let mediaInactiveControlColor: UIColor
public let mediaControlInnerBackgroundColor: UIColor
@ -670,7 +677,7 @@ public final class PresentationThemePartedColors {
public let textSelectionColor: UIColor
public let textSelectionKnobColor: UIColor
public init(bubble: PresentationThemeBubbleColor, primaryTextColor: UIColor, secondaryTextColor: UIColor, linkTextColor: UIColor, linkHighlightColor: UIColor, scamColor: UIColor, textHighlightColor: UIColor, accentTextColor: UIColor, accentControlColor: UIColor, mediaActiveControlColor: UIColor, mediaInactiveControlColor: UIColor, mediaControlInnerBackgroundColor: UIColor, pendingActivityColor: UIColor, fileTitleColor: UIColor, fileDescriptionColor: UIColor, fileDurationColor: UIColor, mediaPlaceholderColor: UIColor, polls: PresentationThemeChatBubblePolls, actionButtonsFillColor: PresentationThemeVariableColor, actionButtonsStrokeColor: PresentationThemeVariableColor, actionButtonsTextColor: PresentationThemeVariableColor, textSelectionColor: UIColor, textSelectionKnobColor: UIColor) {
public init(bubble: PresentationThemeBubbleColor, primaryTextColor: UIColor, secondaryTextColor: UIColor, linkTextColor: UIColor, linkHighlightColor: UIColor, scamColor: UIColor, textHighlightColor: UIColor, accentTextColor: UIColor, accentControlColor: UIColor, accentControlDisabledColor: UIColor, mediaActiveControlColor: UIColor, mediaInactiveControlColor: UIColor, mediaControlInnerBackgroundColor: UIColor, pendingActivityColor: UIColor, fileTitleColor: UIColor, fileDescriptionColor: UIColor, fileDurationColor: UIColor, mediaPlaceholderColor: UIColor, polls: PresentationThemeChatBubblePolls, actionButtonsFillColor: PresentationThemeVariableColor, actionButtonsStrokeColor: PresentationThemeVariableColor, actionButtonsTextColor: PresentationThemeVariableColor, textSelectionColor: UIColor, textSelectionKnobColor: UIColor) {
self.bubble = bubble
self.primaryTextColor = primaryTextColor
self.secondaryTextColor = secondaryTextColor
@ -680,6 +687,7 @@ public final class PresentationThemePartedColors {
self.textHighlightColor = textHighlightColor
self.accentTextColor = accentTextColor
self.accentControlColor = accentControlColor
self.accentControlDisabledColor = accentControlDisabledColor
self.mediaActiveControlColor = mediaActiveControlColor
self.mediaInactiveControlColor = mediaInactiveControlColor
self.mediaControlInnerBackgroundColor = mediaControlInnerBackgroundColor
@ -696,8 +704,8 @@ public final class PresentationThemePartedColors {
self.textSelectionKnobColor = textSelectionKnobColor
}
public func withUpdated(bubble: PresentationThemeBubbleColor? = nil, primaryTextColor: UIColor? = nil, secondaryTextColor: UIColor? = nil, linkTextColor: UIColor? = nil, linkHighlightColor: UIColor? = nil, scamColor: UIColor? = nil, textHighlightColor: UIColor? = nil, accentTextColor: UIColor? = nil, accentControlColor: UIColor? = nil, mediaActiveControlColor: UIColor? = nil, mediaInactiveControlColor: UIColor? = nil, mediaControlInnerBackgroundColor: UIColor? = nil, pendingActivityColor: UIColor? = nil, fileTitleColor: UIColor? = nil, fileDescriptionColor: UIColor? = nil, fileDurationColor: UIColor? = nil, mediaPlaceholderColor: UIColor? = nil, polls: PresentationThemeChatBubblePolls? = nil, actionButtonsFillColor: PresentationThemeVariableColor? = nil, actionButtonsStrokeColor: PresentationThemeVariableColor? = nil, actionButtonsTextColor: PresentationThemeVariableColor? = nil, textSelectionColor: UIColor? = nil, textSelectionKnobColor: UIColor? = nil) -> PresentationThemePartedColors {
return PresentationThemePartedColors(bubble: bubble ?? self.bubble, primaryTextColor: primaryTextColor ?? self.primaryTextColor, secondaryTextColor: secondaryTextColor ?? self.secondaryTextColor, linkTextColor: linkTextColor ?? self.linkTextColor, linkHighlightColor: linkHighlightColor ?? self.linkHighlightColor, scamColor: scamColor ?? self.scamColor, textHighlightColor: textHighlightColor ?? self.textHighlightColor, accentTextColor: accentTextColor ?? self.accentTextColor, accentControlColor: accentControlColor ?? self.accentControlColor, mediaActiveControlColor: mediaActiveControlColor ?? self.mediaActiveControlColor, mediaInactiveControlColor: mediaInactiveControlColor ?? self.mediaInactiveControlColor, mediaControlInnerBackgroundColor: mediaControlInnerBackgroundColor ?? self.mediaControlInnerBackgroundColor, pendingActivityColor: pendingActivityColor ?? self.pendingActivityColor, fileTitleColor: fileTitleColor ?? self.fileTitleColor, fileDescriptionColor: fileDescriptionColor ?? self.fileDescriptionColor, fileDurationColor: fileDurationColor ?? self.fileDurationColor, mediaPlaceholderColor: mediaPlaceholderColor ?? self.mediaPlaceholderColor, polls: polls ?? self.polls, actionButtonsFillColor: actionButtonsFillColor ?? self.actionButtonsFillColor, actionButtonsStrokeColor: actionButtonsStrokeColor ?? self.actionButtonsStrokeColor, actionButtonsTextColor: actionButtonsTextColor ?? self.actionButtonsTextColor, textSelectionColor: textSelectionColor ?? self.textSelectionColor, textSelectionKnobColor: textSelectionKnobColor ?? self.textSelectionKnobColor)
public func withUpdated(bubble: PresentationThemeBubbleColor? = nil, primaryTextColor: UIColor? = nil, secondaryTextColor: UIColor? = nil, linkTextColor: UIColor? = nil, linkHighlightColor: UIColor? = nil, scamColor: UIColor? = nil, textHighlightColor: UIColor? = nil, accentTextColor: UIColor? = nil, accentControlColor: UIColor? = nil, accentControlDisabledColor: UIColor? = nil, mediaActiveControlColor: UIColor? = nil, mediaInactiveControlColor: UIColor? = nil, mediaControlInnerBackgroundColor: UIColor? = nil, pendingActivityColor: UIColor? = nil, fileTitleColor: UIColor? = nil, fileDescriptionColor: UIColor? = nil, fileDurationColor: UIColor? = nil, mediaPlaceholderColor: UIColor? = nil, polls: PresentationThemeChatBubblePolls? = nil, actionButtonsFillColor: PresentationThemeVariableColor? = nil, actionButtonsStrokeColor: PresentationThemeVariableColor? = nil, actionButtonsTextColor: PresentationThemeVariableColor? = nil, textSelectionColor: UIColor? = nil, textSelectionKnobColor: UIColor? = nil) -> PresentationThemePartedColors {
return PresentationThemePartedColors(bubble: bubble ?? self.bubble, primaryTextColor: primaryTextColor ?? self.primaryTextColor, secondaryTextColor: secondaryTextColor ?? self.secondaryTextColor, linkTextColor: linkTextColor ?? self.linkTextColor, linkHighlightColor: linkHighlightColor ?? self.linkHighlightColor, scamColor: scamColor ?? self.scamColor, textHighlightColor: textHighlightColor ?? self.textHighlightColor, accentTextColor: accentTextColor ?? self.accentTextColor, accentControlColor: accentControlColor ?? self.accentControlColor, accentControlDisabledColor: accentControlDisabledColor ?? self.accentControlDisabledColor, mediaActiveControlColor: mediaActiveControlColor ?? self.mediaActiveControlColor, mediaInactiveControlColor: mediaInactiveControlColor ?? self.mediaInactiveControlColor, mediaControlInnerBackgroundColor: mediaControlInnerBackgroundColor ?? self.mediaControlInnerBackgroundColor, pendingActivityColor: pendingActivityColor ?? self.pendingActivityColor, fileTitleColor: fileTitleColor ?? self.fileTitleColor, fileDescriptionColor: fileDescriptionColor ?? self.fileDescriptionColor, fileDurationColor: fileDurationColor ?? self.fileDurationColor, mediaPlaceholderColor: mediaPlaceholderColor ?? self.mediaPlaceholderColor, polls: polls ?? self.polls, actionButtonsFillColor: actionButtonsFillColor ?? self.actionButtonsFillColor, actionButtonsStrokeColor: actionButtonsStrokeColor ?? self.actionButtonsStrokeColor, actionButtonsTextColor: actionButtonsTextColor ?? self.actionButtonsTextColor, textSelectionColor: textSelectionColor ?? self.textSelectionColor, textSelectionKnobColor: textSelectionKnobColor ?? self.textSelectionKnobColor)
}
}

View File

@ -1051,15 +1051,24 @@ extension PresentationThemeChatBubblePolls: Codable {
case highlight
case separator
case bar
case barIconForeground
case barPositive
case barNegative
}
public convenience init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
self.init(radioButton: try decodeColor(values, .radioButton),
radioProgress: try decodeColor(values, .radioProgress),
highlight: try decodeColor(values, .highlight),
separator: try decodeColor(values, .separator),
bar: try decodeColor(values, .bar))
let bar = try decodeColor(values, .bar)
self.init(
radioButton: try decodeColor(values, .radioButton),
radioProgress: try decodeColor(values, .radioProgress),
highlight: try decodeColor(values, .highlight),
separator: try decodeColor(values, .separator),
bar: bar,
barIconForeground: (try? decodeColor(values, .barIconForeground)) ?? .white,
barPositive: (try? decodeColor(values, .barPositive)) ?? bar,
barNegative: (try? decodeColor(values, .barNegative)) ?? bar
)
}
public func encode(to encoder: Encoder) throws {
@ -1069,6 +1078,9 @@ extension PresentationThemeChatBubblePolls: Codable {
try encodeColor(&values, self.highlight, .highlight)
try encodeColor(&values, self.separator, .separator)
try encodeColor(&values, self.bar, .bar)
try encodeColor(&values, self.barIconForeground, .barIconForeground)
try encodeColor(&values, self.barPositive, .barPositive)
try encodeColor(&values, self.barNegative, .barNegative)
}
}
@ -1097,11 +1109,13 @@ extension PresentationThemePartedColors: Codable {
case actionButtonsText
case textSelection
case textSelectionKnob
case accentControlDisabled
}
public convenience init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
let codingPath = decoder.codingPath.map { $0.stringValue }.joined(separator: ".")
let accentControlColor = try decodeColor(values, .accentControl)
self.init(
bubble: try values.decode(PresentationThemeBubbleColor.self, forKey: .bubble),
primaryTextColor: try decodeColor(values, .primaryText),
@ -1111,7 +1125,8 @@ extension PresentationThemePartedColors: Codable {
scamColor: try decodeColor(values, .scam),
textHighlightColor: try decodeColor(values, .textHighlight),
accentTextColor: try decodeColor(values, .accentText),
accentControlColor: try decodeColor(values, .accentControl),
accentControlColor: accentControlColor,
accentControlDisabledColor: (try? decodeColor(values, .accentControlDisabled)) ?? accentControlColor.withAlphaComponent(0.5),
mediaActiveControlColor: try decodeColor(values, .mediaActiveControl),
mediaInactiveControlColor: try decodeColor(values, .mediaInactiveControl),
mediaControlInnerBackgroundColor: try decodeColor(values, .mediaControlInnerBg, decoder: decoder, fallbackKey: codingPath + ".bubble.withWp.bg"),

View File

@ -1534,7 +1534,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
strongSelf.chatDisplayNode.dismissInput()
strongSelf.present(controller, in: .window(.root))
}
}, requestSelectMessagePollOption: { [weak self] id, opaqueIdentifier in
}, requestSelectMessagePollOptions: { [weak self] id, opaqueIdentifiers in
guard let strongSelf = self, let controllerInteraction = strongSelf.controllerInteraction else {
return
}
@ -1543,7 +1543,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
return
}
if controllerInteraction.pollActionState.pollMessageIdsInProgress[id] == nil {
controllerInteraction.pollActionState.pollMessageIdsInProgress[id] = opaqueIdentifier
controllerInteraction.pollActionState.pollMessageIdsInProgress[id] = opaqueIdentifiers
strongSelf.chatDisplayNode.historyNode.requestMessageUpdate(id)
let disposables: DisposableDict<MessageId>
if let current = strongSelf.selectMessagePollOptionDisposables {
@ -1552,7 +1552,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
disposables = DisposableDict()
strongSelf.selectMessagePollOptionDisposables = disposables
}
let signal = requestMessageSelectPollOption(account: strongSelf.context.account, messageId: id, opaqueIdentifier: opaqueIdentifier)
let signal = requestMessageSelectPollOption(account: strongSelf.context.account, messageId: id, opaqueIdentifiers: opaqueIdentifiers)
disposables.set((signal
|> deliverOnMainQueue).start(error: { _ in
guard let strongSelf = self, let controllerInteraction = strongSelf.controllerInteraction else {
@ -1574,6 +1574,24 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
strongSelf.selectPollOptionFeedback?.success()
}), forKey: id)
}
}, requestOpenMessagePollResults: { [weak self] messageId, pollId in
guard let strongSelf = self, pollId.namespace == Namespaces.Media.CloudPoll else {
return
}
let _ = (strongSelf.context.account.postbox.transaction { transaction -> Message? in
return transaction.getMessage(messageId)
}
|> deliverOnMainQueue).start(next: { message in
guard let message = message else {
return
}
for media in message.media {
if let poll = media as? TelegramMediaPoll, poll.pollId == pollId {
strongSelf.push(pollResultsController(context: strongSelf.context, messageId: messageId, poll: poll))
break
}
}
})
}, openAppStorePage: { [weak self] in
if let strongSelf = self {
strongSelf.context.sharedContext.applicationBindings.openAppStorePage()
@ -3957,7 +3975,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
}
let controller = OverlayStatusController(theme: strongSelf.presentationData.theme, type: .loading(cancelled: nil))
strongSelf.present(controller, in: .window(.root))
let signal = requestMessageSelectPollOption(account: strongSelf.context.account, messageId: id, opaqueIdentifier: nil)
let signal = requestMessageSelectPollOption(account: strongSelf.context.account, messageId: id, opaqueIdentifiers: [])
|> afterDisposed { [weak controller] in
Queue.mainQueue().async {
controller?.dismiss()

View File

@ -45,7 +45,7 @@ public enum ChatControllerInteractionLongTapAction {
}
struct ChatInterfacePollActionState: Equatable {
var pollMessageIdsInProgress: [MessageId: Data] = [:]
var pollMessageIdsInProgress: [MessageId: [Data]] = [:]
}
public final class ChatControllerInteraction {
@ -91,7 +91,8 @@ public final class ChatControllerInteraction {
let requestRedeliveryOfFailedMessages: (MessageId) -> Void
let addContact: (String) -> Void
let rateCall: (Message, CallId) -> Void
let requestSelectMessagePollOption: (MessageId, Data) -> Void
let requestSelectMessagePollOptions: (MessageId, [Data]) -> Void
let requestOpenMessagePollResults: (MessageId, MediaId) -> Void
let openAppStorePage: () -> Void
let displayMessageTooltip: (MessageId, String, ASDisplayNode?, CGRect?) -> Void
let seekToTimecode: (Message, Double, Bool) -> Void
@ -117,7 +118,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, TapLongTapOrDoubleTapGestureRecognizer?) -> 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, requestSelectMessagePollOption: @escaping (MessageId, Data) -> 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, 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, TapLongTapOrDoubleTapGestureRecognizer?) -> 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, requestMessageUpdate: @escaping (MessageId) -> Void, cancelInteractiveKeyboardGestures: @escaping () -> Void, automaticMediaDownloadSettings: MediaAutoDownloadSettings, pollActionState: ChatInterfacePollActionState, stickerSettings: ChatInterfaceStickerSettings) {
self.openMessage = openMessage
self.openPeer = openPeer
self.openPeerMention = openPeerMention
@ -160,7 +161,8 @@ public final class ChatControllerInteraction {
self.requestRedeliveryOfFailedMessages = requestRedeliveryOfFailedMessages
self.addContact = addContact
self.rateCall = rateCall
self.requestSelectMessagePollOption = requestSelectMessagePollOption
self.requestSelectMessagePollOptions = requestSelectMessagePollOptions
self.requestOpenMessagePollResults = requestOpenMessagePollResults
self.openAppStorePage = openAppStorePage
self.displayMessageTooltip = displayMessageTooltip
self.seekToTimecode = seekToTimecode
@ -198,7 +200,8 @@ public final class ChatControllerInteraction {
}, requestRedeliveryOfFailedMessages: { _ in
}, addContact: { _ in
}, rateCall: { _, _ in
}, requestSelectMessagePollOption: { _, _ in
}, requestSelectMessagePollOptions: { _, _ in
}, requestOpenMessagePollResults: { _, _ in
}, openAppStorePage: {
}, displayMessageTooltip: { _, _, _, _ in
}, seekToTimecode: { _, _, _ in

View File

@ -511,7 +511,7 @@ func contextMenuForChatPresentationIntefaceState(chatPresentationInterfaceState:
hasSelected = true
}
}
if hasSelected {
if hasSelected, case .poll = activePoll.kind {
actions.append(.action(ContextMenuActionItem(text: chatPresentationInterfaceState.strings.Conversation_UnvotePoll, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Unvote"), color: theme.actionSheet.primaryTextColor)
}, action: { _, f in
@ -539,7 +539,7 @@ func contextMenuForChatPresentationIntefaceState(chatPresentationInterfaceState:
}
}
if let _ = activePoll, messages[0].forwardInfo == nil {
if let activePoll = activePoll, messages[0].forwardInfo == nil {
var canStopPoll = false
if !messages[0].flags.contains(.Incoming) {
canStopPoll = true

View File

@ -7,8 +7,12 @@ import SyncCore
import Postbox
import TextFormat
import UrlEscaping
import CheckNode
import SwiftSignalKit
import AccountContext
import AvatarNode
struct PercentCounterItem: Comparable {
private struct PercentCounterItem: Comparable {
var index: Int = 0
var percent: Int = 0
var remainder: Int = 0
@ -24,7 +28,7 @@ struct PercentCounterItem: Comparable {
}
func adjustPercentCount(_ items: [PercentCounterItem], left: Int) -> [PercentCounterItem] {
private func adjustPercentCount(_ items: [PercentCounterItem], left: Int) -> [PercentCounterItem] {
var left = left
var items = items.sorted(by: <)
var i:Int = 0
@ -104,16 +108,26 @@ private final class ChatMessagePollOptionRadioNode: ASDisplayNode {
private(set) var isAnimating: Bool = false
private var startTime: Double?
private var checkNode: CheckNode?
private var displayLink: CADisplayLink?
private var shouldBeAnimating: Bool {
return self.isInHierarchyValue && self.isAnimating
}
var isChecked: Bool? {
return self.checkNode?.isChecked
}
func updateIsChecked(_ value: Bool, animated: Bool) {
self.checkNode?.setIsChecked(value, animated: animated)
}
override init() {
super.init()
self.isLayerBacked = true
self.isUserInteractionEnabled = false
self.isOpaque = false
}
@ -139,7 +153,7 @@ private final class ChatMessagePollOptionRadioNode: ASDisplayNode {
}
}
func update(staticColor: UIColor, animatedColor: UIColor, isAnimating: Bool) {
func update(staticColor: UIColor, animatedColor: UIColor, foregroundColor: UIColor, isSelectable: Bool, isAnimating: Bool) {
var updated = false
if !staticColor.isEqual(self.staticColor) {
self.staticColor = staticColor
@ -157,6 +171,20 @@ private final class ChatMessagePollOptionRadioNode: ASDisplayNode {
self.updateAnimating()
}
}
if isSelectable {
if self.checkNode == nil {
updated = true
let checkNode = CheckNode(strokeColor: staticColor, fillColor: animatedColor, foregroundColor: foregroundColor, style: .plain)
self.checkNode = checkNode
self.addSubnode(checkNode)
checkNode.isUserInteractionEnabled = false
checkNode.frame = CGRect(origin: CGPoint(x: -5.0, y: -5.0), size: CGSize(width: 32.0, height: 32.0))
}
} else if let checkNode = self.checkNode {
updated = true
self.checkNode = nil
checkNode.removeFromSupernode()
}
if updated {
self.setNeedsDisplay()
}
@ -197,7 +225,7 @@ private final class ChatMessagePollOptionRadioNode: ASDisplayNode {
if let startTime = self.startTime {
offset = CACurrentMediaTime() - startTime
}
return ChatMessagePollOptionRadioNodeParameters(staticColor: staticColor, animatedColor: animatedColor, offset: offset)
return ChatMessagePollOptionRadioNodeParameters(staticColor: self.checkNode == nil ? staticColor : .clear, animatedColor: animatedColor, offset: offset)
} else {
return nil
}
@ -311,19 +339,26 @@ private struct ChatMessagePollOptionResult: Equatable {
let count: Int32
}
private struct ChatMessagePollOptionSelection: Equatable {
var isSelected: Bool
var isCorrect: Bool
}
private final class ChatMessagePollOptionNode: ASDisplayNode {
private let highlightedBackgroundNode: ASDisplayNode
private var radioNode: ChatMessagePollOptionRadioNode?
private(set) var radioNode: ChatMessagePollOptionRadioNode?
private let percentageNode: ASDisplayNode
private var percentageImage: UIImage?
private var titleNode: TextNode?
private let buttonNode: HighlightTrackingButtonNode
private let separatorNode: ASDisplayNode
private let resultBarNode: ASImageNode
private let resultBarIconNode: ASImageNode
var option: TelegramMediaPollOption?
public private(set) var currentResult: ChatMessagePollOptionResult?
private(set) var currentResult: ChatMessagePollOptionResult?
private(set) var currentSelection: ChatMessagePollOptionSelection?
var pressed: (() -> Void)?
var selectionUpdated: (() -> Void)?
override init() {
self.highlightedBackgroundNode = ASDisplayNode()
@ -338,6 +373,9 @@ private final class ChatMessagePollOptionNode: ASDisplayNode {
self.resultBarNode.isLayerBacked = true
self.resultBarNode.alpha = 0.0
self.resultBarIconNode = ASImageNode()
self.resultBarIconNode.isLayerBacked = true
self.percentageNode = ASDisplayNode()
self.percentageNode.alpha = 0.0
self.percentageNode.isLayerBacked = true
@ -347,6 +385,7 @@ private final class ChatMessagePollOptionNode: ASDisplayNode {
self.addSubnode(self.highlightedBackgroundNode)
self.addSubnode(self.separatorNode)
self.addSubnode(self.resultBarNode)
self.addSubnode(self.resultBarIconNode)
self.addSubnode(self.percentageNode)
self.addSubnode(self.buttonNode)
@ -365,14 +404,20 @@ private final class ChatMessagePollOptionNode: ASDisplayNode {
}
@objc private func buttonPressed() {
self.pressed?()
if let radioNode = self.radioNode, let isChecked = radioNode.isChecked {
radioNode.updateIsChecked(!isChecked, animated: true)
self.selectionUpdated?()
} else {
self.pressed?()
}
}
static func asyncLayout(_ maybeNode: ChatMessagePollOptionNode?) -> (_ accountPeerId: PeerId, _ presentationData: ChatPresentationData, _ message: Message, _ option: TelegramMediaPollOption, _ optionResult: ChatMessagePollOptionResult?, _ constrainedWidth: CGFloat) -> (minimumWidth: CGFloat, layout: ((CGFloat) -> (CGSize, (Bool, Bool) -> ChatMessagePollOptionNode))) {
static func asyncLayout(_ maybeNode: ChatMessagePollOptionNode?) -> (_ accountPeerId: PeerId, _ presentationData: ChatPresentationData, _ message: Message, _ poll: TelegramMediaPoll, _ option: TelegramMediaPollOption, _ optionResult: ChatMessagePollOptionResult?, _ constrainedWidth: CGFloat) -> (minimumWidth: CGFloat, layout: ((CGFloat) -> (CGSize, (Bool, Bool) -> ChatMessagePollOptionNode))) {
let makeTitleLayout = TextNode.asyncLayout(maybeNode?.titleNode)
let currentResult = maybeNode?.currentResult
let currentSelection = maybeNode?.currentSelection
return { accountPeerId, presentationData, message, option, optionResult, constrainedWidth in
return { accountPeerId, presentationData, message, poll, option, optionResult, constrainedWidth in
let leftInset: CGFloat = 50.0
let rightInset: CGFloat = 12.0
@ -383,12 +428,84 @@ private final class ChatMessagePollOptionNode: ASDisplayNode {
let contentHeight: CGFloat = max(46.0, titleLayout.size.height + 22.0)
let shouldHaveRadioNode = optionResult == nil
let isSelectable: Bool
if shouldHaveRadioNode, case .poll(multipleAnswers: true) = poll.kind {
isSelectable = true
} else {
isSelectable = false
}
var updatedPercentageImage: UIImage?
if currentResult != optionResult {
updatedPercentageImage = generatePercentageImage(presentationData: presentationData, incoming: incoming, value: optionResult?.percent ?? 0)
}
var resultIcon: UIImage?
var updatedResultIcon = false
var selection: ChatMessagePollOptionSelection?
if optionResult != nil {
if let voters = poll.results.voters {
for voter in voters {
if voter.opaqueIdentifier == option.opaqueIdentifier {
if voter.selected || voter.isCorrect {
selection = ChatMessagePollOptionSelection(isSelected: voter.selected, isCorrect: voter.isCorrect)
}
break
}
}
}
}
if selection != currentSelection {
updatedResultIcon = true
if let selection = selection {
var isQuiz = false
if case .quiz = poll.kind {
isQuiz = true
}
resultIcon = generateImage(CGSize(width: 16.0, height: 16.0), rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
var isIncorrect = false
let fillColor: UIColor
if selection.isSelected {
if isQuiz {
if selection.isCorrect {
fillColor = incoming ? presentationData.theme.theme.chat.message.incoming.polls.barPositive : presentationData.theme.theme.chat.message.outgoing.polls.barPositive
} else {
fillColor = incoming ? presentationData.theme.theme.chat.message.incoming.polls.barNegative : presentationData.theme.theme.chat.message.outgoing.polls.barNegative
isIncorrect = true
}
} else {
fillColor = incoming ? presentationData.theme.theme.chat.message.incoming.polls.bar : presentationData.theme.theme.chat.message.outgoing.polls.bar
}
} else if isQuiz && selection.isCorrect {
fillColor = incoming ? presentationData.theme.theme.chat.message.incoming.polls.bar : presentationData.theme.theme.chat.message.outgoing.polls.bar
} else {
fillColor = incoming ? presentationData.theme.theme.chat.message.incoming.polls.bar : presentationData.theme.theme.chat.message.outgoing.polls.bar
}
context.setFillColor(fillColor.cgColor)
context.fillEllipse(in: CGRect(origin: CGPoint(), size: size))
let strokeColor = incoming ? presentationData.theme.theme.chat.message.incoming.polls.barIconForeground : presentationData.theme.theme.chat.message.outgoing.polls.barIconForeground
context.setStrokeColor(strokeColor.cgColor)
context.setLineWidth(1.5)
context.setLineJoin(.round)
context.setLineCap(.round)
if isIncorrect {
context.translateBy(x: 5.0, y: 5.0)
context.move(to: CGPoint(x: 0.0, y: 6.0))
context.addLine(to: CGPoint(x: 6.0, y: 0.0))
context.strokePath()
context.move(to: CGPoint(x: 0.0, y: 0.0))
context.addLine(to: CGPoint(x: 6.0, y: 6.0))
context.strokePath()
} else {
let _ = try? drawSvgPath(context, path: "M4,8.5 L6.44778395,10.9477839 C6.47662208,10.9766221 6.52452135,10.9754786 6.54754782,10.9524522 L12,5.5 S ")
}
})
}
}
return (titleLayout.size.width + leftInset + rightInset, { width in
return (CGSize(width: width, height: contentHeight), { animated, inProgress in
let node: ChatMessagePollOptionNode
@ -432,7 +549,7 @@ private final class ChatMessagePollOptionNode: ASDisplayNode {
}
let radioSize: CGFloat = 22.0
radioNode.frame = CGRect(origin: CGPoint(x: 12.0, y: 12.0), size: CGSize(width: radioSize, height: radioSize))
radioNode.update(staticColor: incoming ? presentationData.theme.theme.chat.message.incoming.polls.radioButton : presentationData.theme.theme.chat.message.outgoing.polls.radioButton, animatedColor: incoming ? presentationData.theme.theme.chat.message.incoming.polls.radioProgress : presentationData.theme.theme.chat.message.outgoing.polls.radioProgress, isAnimating: inProgress)
radioNode.update(staticColor: incoming ? presentationData.theme.theme.chat.message.incoming.polls.radioButton : presentationData.theme.theme.chat.message.outgoing.polls.radioButton, animatedColor: incoming ? presentationData.theme.theme.chat.message.incoming.polls.radioProgress : presentationData.theme.theme.chat.message.outgoing.polls.radioProgress, foregroundColor: presentationData.theme.theme.list.itemCheckColors.foregroundColor, isSelectable: isSelectable, isAnimating: inProgress)
} else if let radioNode = node.radioNode {
node.radioNode = nil
if animated {
@ -469,26 +586,60 @@ private final class ChatMessagePollOptionNode: ASDisplayNode {
node.separatorNode.backgroundColor = incoming ? presentationData.theme.theme.chat.message.incoming.polls.separator : presentationData.theme.theme.chat.message.outgoing.polls.separator
node.separatorNode.frame = CGRect(origin: CGPoint(x: leftInset, y: contentHeight - UIScreenPixel), size: CGSize(width: width - leftInset, height: UIScreenPixel))
if node.resultBarNode.image == nil {
node.resultBarNode.image = generateStretchableFilledCircleImage(diameter: 6.0, color: incoming ? presentationData.theme.theme.chat.message.incoming.polls.bar : presentationData.theme.theme.chat.message.outgoing.polls.bar)
if node.resultBarNode.image == nil || updatedResultIcon {
var isQuiz = false
if case .quiz = poll.kind {
isQuiz = true
}
let fillColor: UIColor
if let selection = selection {
if selection.isSelected {
if isQuiz {
if selection.isCorrect {
fillColor = incoming ? presentationData.theme.theme.chat.message.incoming.polls.barPositive : presentationData.theme.theme.chat.message.outgoing.polls.barPositive
} else {
fillColor = incoming ? presentationData.theme.theme.chat.message.incoming.polls.barNegative : presentationData.theme.theme.chat.message.outgoing.polls.barNegative
}
} else {
fillColor = incoming ? presentationData.theme.theme.chat.message.incoming.polls.bar : presentationData.theme.theme.chat.message.outgoing.polls.bar
}
} else if isQuiz && selection.isCorrect {
fillColor = incoming ? presentationData.theme.theme.chat.message.incoming.polls.bar : presentationData.theme.theme.chat.message.outgoing.polls.bar
} else {
fillColor = incoming ? presentationData.theme.theme.chat.message.incoming.polls.bar : presentationData.theme.theme.chat.message.outgoing.polls.bar
}
} else {
fillColor = incoming ? presentationData.theme.theme.chat.message.incoming.polls.bar : presentationData.theme.theme.chat.message.outgoing.polls.bar
}
node.resultBarNode.image = generateStretchableFilledCircleImage(diameter: 6.0, color: fillColor)
}
if updatedResultIcon {
node.resultBarIconNode.image = resultIcon
}
let minBarWidth: CGFloat = 6.0
let resultBarWidth = minBarWidth + floor((width - leftInset - rightInset - minBarWidth) * (optionResult?.normalized ?? 0.0))
node.resultBarNode.frame = CGRect(origin: CGPoint(x: leftInset, y: contentHeight - 6.0 - 1.0), size: CGSize(width: resultBarWidth, height: 6.0))
let barFrame = CGRect(origin: CGPoint(x: leftInset, y: contentHeight - 6.0 - 1.0), size: CGSize(width: resultBarWidth, height: 6.0))
node.resultBarNode.frame = barFrame
node.resultBarIconNode.frame = CGRect(origin: CGPoint(x: barFrame.minX - 6.0 - 16.0, y: barFrame.minY + floor((barFrame.height - 16.0) / 2.0)), size: CGSize(width: 16.0, height: 16.0))
node.resultBarNode.alpha = optionResult != nil ? 1.0 : 0.0
node.percentageNode.alpha = optionResult != nil ? 1.0 : 0.0
node.separatorNode.alpha = optionResult == nil ? 1.0 : 0.0
node.resultBarIconNode.alpha = optionResult != nil ? 1.0 : 0.0
if animated, currentResult != optionResult {
if (currentResult != nil) != (optionResult != nil) {
if optionResult != nil {
node.resultBarNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1)
node.percentageNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
node.separatorNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.08)
node.resultBarIconNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
} else {
node.resultBarNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.4)
node.percentageNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2)
node.separatorNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
node.resultBarIconNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2)
}
}
@ -513,10 +664,17 @@ private let labelsFont = Font.regular(14.0)
class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode {
private let textNode: TextNode
private let typeNode: TextNode
private let avatarsNode: MergedAvatarsNode
private let votersNode: TextNode
private let buttonSubmitInactiveTextNode: TextNode
private let buttonSubmitActiveTextNode: TextNode
private let buttonViewResultsTextNode: TextNode
private let buttonNode: HighlightableButtonNode
private let statusNode: ChatMessageDateAndStatusNode
private var optionNodes: [ChatMessagePollOptionNode] = []
private var poll: TelegramMediaPoll?
required init() {
self.textNode = TextNode()
self.textNode.isUserInteractionEnabled = false
@ -530,29 +688,100 @@ class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode {
self.typeNode.contentsScale = UIScreenScale
self.typeNode.displaysAsynchronously = true
self.avatarsNode = MergedAvatarsNode()
self.votersNode = TextNode()
self.votersNode.isUserInteractionEnabled = false
self.votersNode.contentMode = .topLeft
self.votersNode.contentsScale = UIScreenScale
self.votersNode.displaysAsynchronously = true
self.buttonSubmitInactiveTextNode = TextNode()
self.buttonSubmitInactiveTextNode.isUserInteractionEnabled = false
self.buttonSubmitInactiveTextNode.contentMode = .topLeft
self.buttonSubmitInactiveTextNode.contentsScale = UIScreenScale
self.buttonSubmitInactiveTextNode.displaysAsynchronously = true
self.buttonSubmitActiveTextNode = TextNode()
self.buttonSubmitActiveTextNode.isUserInteractionEnabled = false
self.buttonSubmitActiveTextNode.contentMode = .topLeft
self.buttonSubmitActiveTextNode.contentsScale = UIScreenScale
self.buttonSubmitActiveTextNode.displaysAsynchronously = true
self.buttonViewResultsTextNode = TextNode()
self.buttonViewResultsTextNode.isUserInteractionEnabled = false
self.buttonViewResultsTextNode.contentMode = .topLeft
self.buttonViewResultsTextNode.contentsScale = UIScreenScale
self.buttonViewResultsTextNode.displaysAsynchronously = true
self.buttonNode = HighlightableButtonNode()
self.statusNode = ChatMessageDateAndStatusNode()
super.init()
self.addSubnode(self.textNode)
self.addSubnode(self.typeNode)
self.addSubnode(self.avatarsNode)
self.addSubnode(self.votersNode)
self.addSubnode(self.buttonSubmitInactiveTextNode)
self.addSubnode(self.buttonSubmitActiveTextNode)
self.addSubnode(self.buttonViewResultsTextNode)
self.addSubnode(self.buttonNode)
self.buttonNode.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: .touchUpInside)
self.buttonNode.highligthedChanged = { [weak self] highlighted in
if let strongSelf = self {
if highlighted {
strongSelf.buttonSubmitActiveTextNode.layer.removeAnimation(forKey: "opacity")
strongSelf.buttonSubmitActiveTextNode.alpha = 0.6
strongSelf.buttonViewResultsTextNode.layer.removeAnimation(forKey: "opacity")
strongSelf.buttonViewResultsTextNode.alpha = 0.6
} else {
strongSelf.buttonSubmitActiveTextNode.alpha = 1.0
strongSelf.buttonSubmitActiveTextNode.layer.animateAlpha(from: 0.6, to: 1.0, duration: 0.3)
strongSelf.buttonViewResultsTextNode.alpha = 1.0
strongSelf.buttonViewResultsTextNode.layer.animateAlpha(from: 0.6, to: 1.0, duration: 0.3)
}
}
}
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
@objc private func buttonPressed() {
guard let item = self.item, let poll = self.poll, let pollId = poll.id else {
return
}
var hasSelection = false
var selectedOpaqueIdentifiers: [Data] = []
for optionNode in self.optionNodes {
if let option = optionNode.option {
if let isChecked = optionNode.radioNode?.isChecked {
hasSelection = true
if isChecked {
selectedOpaqueIdentifiers.append(option.opaqueIdentifier)
}
}
}
}
if !hasSelection {
item.controllerInteraction.requestOpenMessagePollResults(item.message.id, pollId)
} else if !selectedOpaqueIdentifiers.isEmpty {
item.controllerInteraction.requestSelectMessagePollOptions(item.message.id, selectedOpaqueIdentifiers)
}
}
override func asyncLayoutContent() -> (_ item: ChatMessageBubbleContentItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ preparePosition: ChatMessageBubblePreparePosition, _ messageSelection: Bool?, _ constrainedSize: CGSize) -> (ChatMessageBubbleContentProperties, CGSize?, CGFloat, (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool) -> Void))) {
let makeTextLayout = TextNode.asyncLayout(self.textNode)
let makeTypeLayout = TextNode.asyncLayout(self.typeNode)
let makeVotersLayout = TextNode.asyncLayout(self.votersNode)
let makeSubmitInactiveTextLayout = TextNode.asyncLayout(self.buttonSubmitInactiveTextNode)
let makeSubmitActiveTextLayout = TextNode.asyncLayout(self.buttonSubmitActiveTextNode)
let makeViewResultsTextLayout = TextNode.asyncLayout(self.buttonViewResultsTextNode)
let statusLayout = self.statusNode.asyncLayout()
var previousPoll: TelegramMediaPoll?
@ -564,11 +793,15 @@ class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode {
}
}
var previousOptionNodeLayouts: [Data: (_ accountPeerId: PeerId, _ presentationData: ChatPresentationData, _ message: Message, _ option: TelegramMediaPollOption, _ optionResult: ChatMessagePollOptionResult?, _ constrainedWidth: CGFloat) -> (minimumWidth: CGFloat, layout: ((CGFloat) -> (CGSize, (Bool, Bool) -> ChatMessagePollOptionNode)))] = [:]
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
@ -653,8 +886,35 @@ class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode {
let (textLayout, textApply) = makeTextLayout(TextNodeLayoutArguments(attributedString: attributedText, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: textConstrainedSize, alignment: .natural, cutout: nil, insets: textInsets))
let typeText: String
if let poll = poll, poll.isClosed {
var avatarPeers: [Peer] = []
if let poll = poll {
for peerId in poll.results.recentVoters {
if let peer = item.message.peers[peerId] {
avatarPeers.append(peer)
}
}
}
if let poll = poll, poll.isClosed, case .poll = poll.kind {
typeText = item.presentationData.strings.MessagePoll_LabelClosed
} else if let poll = poll {
switch poll.kind {
case .poll:
switch poll.publicity {
case .anonymous:
typeText = item.presentationData.strings.MessagePoll_LabelAnonymous
case .public:
typeText = item.presentationData.strings.MessagePoll_LabelPoll
}
case .quiz:
switch poll.publicity {
case .anonymous:
typeText = item.presentationData.strings.MessagePoll_LabelAnonymousQuiz
case .public:
typeText = item.presentationData.strings.MessagePoll_LabelQuiz
}
}
} else {
typeText = item.presentationData.strings.MessagePoll_LabelAnonymous
}
@ -672,6 +932,10 @@ class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode {
}
let (votersLayout, votersApply) = makeVotersLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: votersString, font: labelsFont, textColor: messageTheme.secondaryTextColor), backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: textConstrainedSize, alignment: .natural, cutout: nil, insets: textInsets))
let (buttonSubmitInactiveTextLayout, buttonSubmitInactiveTextApply) = makeSubmitInactiveTextLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.presentationData.strings.MessagePoll_SubmitVote, font: Font.regular(17.0), textColor: messageTheme.accentControlDisabledColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: textConstrainedSize, alignment: .natural, cutout: nil, insets: textInsets))
let (buttonSubmitActiveTextLayout, buttonSubmitActiveTextApply) = makeSubmitActiveTextLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.presentationData.strings.MessagePoll_SubmitVote, font: Font.regular(17.0), textColor: messageTheme.polls.bar), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: textConstrainedSize, alignment: .natural, cutout: nil, insets: textInsets))
let (buttonViewResultsTextLayout, buttonViewResultsTextApply) = makeViewResultsTextLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.presentationData.strings.MessagePoll_ViewResults, font: Font.regular(17.0), textColor: messageTheme.polls.bar), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: textConstrainedSize, alignment: .natural, cutout: nil, insets: textInsets))
var textFrame = CGRect(origin: CGPoint(x: -textInsets.left, y: -textInsets.top), size: textLayout.size)
var textFrameWithoutInsets = CGRect(origin: CGPoint(x: textFrame.origin.x + textInsets.left, y: textFrame.origin.y + textInsets.top), size: CGSize(width: textFrame.width - textInsets.left - textInsets.right, height: textFrame.height - textInsets.top - textInsets.bottom))
@ -687,6 +951,8 @@ class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode {
var boundingSize: CGSize = textFrameWithoutInsets.size
boundingSize.width = max(boundingSize.width, typeLayout.size.width)
boundingSize.width = max(boundingSize.width, votersLayout.size.width + 4.0 + (statusSize?.width ?? 0.0))
boundingSize.width = max(boundingSize.width, buttonSubmitInactiveTextLayout.size.width + 4.0 + (statusSize?.width ?? 0.0))
boundingSize.width = max(boundingSize.width, buttonViewResultsTextLayout.size.width + 4.0 + (statusSize?.width ?? 0.0))
boundingSize.width += layoutConstants.text.bubbleInsets.left + layoutConstants.text.bubbleInsets.right
boundingSize.height += layoutConstants.text.bubbleInsets.top + layoutConstants.text.bubbleInsets.bottom
@ -733,7 +999,7 @@ class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode {
for i in 0 ..< poll.options.count {
let option = poll.options[i]
let makeLayout: (_ accountPeerId: PeerId, _ presentationData: ChatPresentationData, _ message: Message, _ option: TelegramMediaPollOption, _ optionResult: ChatMessagePollOptionResult?, _ constrainedWidth: CGFloat) -> (minimumWidth: CGFloat, layout: ((CGFloat) -> (CGSize, (Bool, Bool) -> ChatMessagePollOptionNode)))
let makeLayout: (_ accountPeerId: PeerId, _ presentationData: ChatPresentationData, _ message: Message, _ poll: TelegramMediaPoll, _ option: TelegramMediaPollOption, _ optionResult: ChatMessagePollOptionResult?, _ constrainedWidth: CGFloat) -> (minimumWidth: CGFloat, layout: ((CGFloat) -> (CGSize, (Bool, Bool) -> ChatMessagePollOptionNode)))
if let previous = previousOptionNodeLayouts[option.opaqueIdentifier] {
makeLayout = previous
} else {
@ -749,7 +1015,7 @@ class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode {
} else if poll.isClosed {
optionResult = ChatMessagePollOptionResult(normalized: 0, percent: 0, count: 0)
}
let result = makeLayout(item.context.account.peerId, item.presentationData, item.message, option, optionResult, constrainedSize.width - layoutConstants.bubble.borderInset * 2.0)
let result = makeLayout(item.context.account.peerId, item.presentationData, item.message, poll, option, optionResult, constrainedSize.width - layoutConstants.bubble.borderInset * 2.0)
boundingSize.width = max(boundingSize.width, result.minimumWidth + layoutConstants.bubble.borderInset * 2.0)
pollOptionsFinalizeLayouts.append(result.1)
}
@ -792,7 +1058,8 @@ class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode {
}
let optionsVotersSpacing: CGFloat = 11.0
let votersBottomSpacing: CGFloat = 8.0
let optionsButtonSpacing: CGFloat = 8.0
let votersBottomSpacing: CGFloat = 11.0
resultSize.height += optionsVotersSpacing + votersLayout.size.height + votersBottomSpacing
var adjustedStatusFrame: CGRect?
@ -800,9 +1067,10 @@ class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode {
adjustedStatusFrame = CGRect(origin: CGPoint(x: boundingWidth - statusFrame.size.width - layoutConstants.text.bubbleInsets.right, y: resultSize.height - statusFrame.size.height - 6.0), size: statusFrame.size)
}
return (resultSize, { [weak self] animation, _ in
return (resultSize, { [weak self] animation, synchronousLoad in
if let strongSelf = self {
strongSelf.item = item
strongSelf.poll = poll
let cachedLayout = strongSelf.textNode.cachedLayout
@ -834,7 +1102,9 @@ class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode {
let (size, apply) = optionNodesSizesAndApply[i]
var isRequesting = false
if let poll = poll, i < poll.options.count {
isRequesting = item.controllerInteraction.pollActionState.pollMessageIdsInProgress[item.message.id] == poll.options[i].opaqueIdentifier
if let inProgressOpaqueIds = item.controllerInteraction.pollActionState.pollMessageIdsInProgress[item.message.id] {
isRequesting = inProgressOpaqueIds.contains(poll.options[i].opaqueIdentifier)
}
}
let optionNode = apply(animation.isAnimated, isRequesting)
if optionNode.supernode !== strongSelf {
@ -845,7 +1115,13 @@ class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode {
return
}
item.controllerInteraction.requestSelectMessagePollOption(item.message.id, option.opaqueIdentifier)
item.controllerInteraction.requestSelectMessagePollOptions(item.message.id, [option.opaqueIdentifier])
}
optionNode.selectionUpdated = {
guard let strongSelf = self else {
return
}
strongSelf.updateSelection()
}
}
optionNode.frame = CGRect(origin: CGPoint(x: layoutConstants.bubble.borderInset, y: verticalOffset), size: size)
@ -887,15 +1163,31 @@ class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode {
} else {
strongSelf.textNode.frame = textFrame
}
strongSelf.typeNode.frame = CGRect(origin: CGPoint(x: textFrame.minX, y: textFrame.maxY + titleTypeSpacing), size: typeLayout.size)
let typeFrame = CGRect(origin: CGPoint(x: textFrame.minX, y: textFrame.maxY + titleTypeSpacing), size: typeLayout.size)
strongSelf.typeNode.frame = typeFrame
strongSelf.avatarsNode.frame = CGRect(origin: CGPoint(x: typeFrame.maxX + 6.0, y: typeFrame.minY + floor((typeFrame.height - mergedImageSize) / 2.0)), size: CGSize(width: mergedImageSpacing * 3.0, height: mergedImageSize))
strongSelf.avatarsNode.update(context: item.context, peers: avatarPeers, synchronousLoad: synchronousLoad)
let _ = votersApply()
strongSelf.votersNode.frame = CGRect(origin: CGPoint(x: textFrame.minX, y: verticalOffset + optionsVotersSpacing), size: votersLayout.size)
strongSelf.votersNode.frame = CGRect(origin: CGPoint(x: floor((resultSize.width - votersLayout.size.width) / 2.0), y: verticalOffset + optionsVotersSpacing), size: votersLayout.size)
if animation.isAnimated, let previousPoll = previousPoll, let poll = poll {
if previousPoll.results.totalVoters == nil && poll.results.totalVoters != nil {
strongSelf.votersNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25)
}
}
let _ = buttonSubmitInactiveTextApply()
strongSelf.buttonSubmitInactiveTextNode.frame = CGRect(origin: CGPoint(x: floor((resultSize.width - buttonSubmitInactiveTextLayout.size.width) / 2.0), y: verticalOffset + optionsButtonSpacing), size: buttonSubmitInactiveTextLayout.size)
let _ = buttonSubmitActiveTextApply()
strongSelf.buttonSubmitActiveTextNode.frame = CGRect(origin: CGPoint(x: floor((resultSize.width - buttonSubmitActiveTextLayout.size.width) / 2.0), y: verticalOffset + optionsButtonSpacing), size: buttonSubmitActiveTextLayout.size)
let _ = buttonViewResultsTextApply()
strongSelf.buttonViewResultsTextNode.frame = CGRect(origin: CGPoint(x: floor((resultSize.width - buttonViewResultsTextLayout.size.width) / 2.0), y: verticalOffset + optionsButtonSpacing), size: buttonViewResultsTextLayout.size)
strongSelf.buttonNode.frame = CGRect(origin: CGPoint(x: 0.0, y: verticalOffset), size: CGSize(width: resultSize.width, height: 44.0))
strongSelf.updateSelection()
}
})
})
@ -903,6 +1195,43 @@ class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode {
}
}
private func updateSelection() {
guard let poll = self.poll else {
return
}
var hasSelection = false
var hasSelectedOptions = false
for optionNode in self.optionNodes {
if let isChecked = optionNode.radioNode?.isChecked {
hasSelection = true
if isChecked {
hasSelectedOptions = true
}
}
}
if hasSelection && poll.pollId.namespace == Namespaces.Media.CloudPoll {
self.votersNode.isHidden = true
self.buttonViewResultsTextNode.isHidden = true
self.buttonSubmitInactiveTextNode.isHidden = hasSelectedOptions
self.buttonSubmitActiveTextNode.isHidden = !hasSelectedOptions
self.buttonNode.isHidden = !hasSelectedOptions
} else {
if case .public = poll.publicity {
self.votersNode.isHidden = true
self.buttonViewResultsTextNode.isHidden = false
self.buttonNode.isHidden = false
} else {
self.votersNode.isHidden = false
self.buttonViewResultsTextNode.isHidden = true
self.buttonNode.isHidden = true
}
self.buttonSubmitInactiveTextNode.isHidden = true
self.buttonSubmitActiveTextNode.isHidden = true
}
}
override func animateInsertion(_ currentTimestamp: Double, duration: Double) {
self.textNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
self.statusNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
@ -954,6 +1283,9 @@ class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode {
}
}
}
if self.buttonNode.isUserInteractionEnabled, !self.buttonNode.isHidden, self.buttonNode.frame.contains(point) {
return .ignore
}
return .none
}
}
@ -965,3 +1297,167 @@ class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode {
return nil
}
}
private enum PeerAvatarReference: Equatable {
case letters(PeerId, [String])
case image(PeerReference, TelegramMediaImageRepresentation)
var peerId: PeerId {
switch self {
case let .letters(value, _):
return value
case let .image(value, _):
return value.id
}
}
}
private extension PeerAvatarReference {
init(peer: Peer) {
if let photo = peer.smallProfileImage, let peerReference = PeerReference(peer) {
self = .image(peerReference, photo)
} else {
self = .letters(peer.id, peer.displayLetters)
}
}
}
private final class MergedAvatarsNodeArguments: NSObject {
let peers: [PeerAvatarReference]
let images: [PeerId: UIImage]
init(peers: [PeerAvatarReference], images: [PeerId: UIImage]) {
self.peers = peers
self.images = images
}
}
private let mergedImageSize: CGFloat = 16.0
private let mergedImageSpacing: CGFloat = 15.0
private let avatarFont = avatarPlaceholderFont(size: 8.0)
private final class MergedAvatarsNode: ASDisplayNode {
private var peers: [PeerAvatarReference] = []
private var images: [PeerId: UIImage] = [:]
private var disposables: [PeerId: Disposable] = [:]
override init() {
super.init()
self.isOpaque = false
self.displaysAsynchronously = true
}
deinit {
for (_, disposable) in self.disposables {
disposable.dispose()
}
}
func update(context: AccountContext, peers: [Peer], synchronousLoad: Bool) {
var filteredPeers = peers.map(PeerAvatarReference.init)
if filteredPeers.count > 3 {
filteredPeers.dropLast(filteredPeers.count - 3)
}
if filteredPeers != self.peers {
self.peers = filteredPeers
var validImageIds: [PeerId] = []
for peer in filteredPeers {
if case .image = peer {
validImageIds.append(peer.peerId)
}
}
var removedImageIds: [PeerId] = []
for (id, _) in self.images {
if !validImageIds.contains(id) {
removedImageIds.append(id)
}
}
var removedDisposableIds: [PeerId] = []
for (id, disposable) in self.disposables {
if !validImageIds.contains(id) {
disposable.dispose()
removedDisposableIds.append(id)
}
}
for id in removedImageIds {
self.images.removeValue(forKey: id)
}
for id in removedDisposableIds {
self.disposables.removeValue(forKey: id)
}
for peer in filteredPeers {
switch peer {
case let .image(peerReference, representation):
if self.disposables[peer.peerId] == nil {
if let signal = peerAvatarImage(account: context.account, peerReference: peerReference, authorOfMessage: nil, representation: representation, displayDimensions: CGSize(width: mergedImageSize, height: mergedImageSize), synchronousLoad: synchronousLoad) {
let disposable = (signal
|> deliverOnMainQueue).start(next: { [weak self] image in
guard let strongSelf = self else {
return
}
if let image = image {
strongSelf.images[peer.peerId] = image
strongSelf.setNeedsDisplay()
}
})
self.disposables[peer.peerId] = disposable
}
}
case .letters:
break
}
}
self.setNeedsDisplay()
}
}
override func drawParameters(forAsyncLayer layer: _ASDisplayLayer) -> NSObjectProtocol {
return MergedAvatarsNodeArguments(peers: self.peers, images: self.images)
}
@objc override class func draw(_ bounds: CGRect, withParameters parameters: Any?, isCancelled: () -> Bool, isRasterizing: Bool) {
assertNotOnMainThread()
let context = UIGraphicsGetCurrentContext()!
if !isRasterizing {
context.setBlendMode(.copy)
context.setFillColor(UIColor.clear.cgColor)
context.fill(bounds)
}
guard let parameters = parameters as? MergedAvatarsNodeArguments else {
return
}
let imageOverlaySpacing: CGFloat = 1.0
context.setBlendMode(.copy)
var currentX = mergedImageSize + mergedImageSpacing * CGFloat(parameters.peers.count - 1) - mergedImageSize
for i in (0 ..< parameters.peers.count).reversed() {
let imageRect = CGRect(origin: CGPoint(x: currentX, y: 0.0), size: CGSize(width: mergedImageSize, height: mergedImageSize))
context.setFillColor(UIColor.clear.cgColor)
context.fillEllipse(in: imageRect.insetBy(dx: -1.0, dy: -1.0))
switch parameters.peers[i] {
case let .letters(peerId, letters):
context.saveGState()
context.translateBy(x: currentX, y: 0.0)
drawPeerAvatarLetters(context: context, size: CGSize(width: mergedImageSize, height: mergedImageSize), font: avatarFont, letters: letters, peerId: peerId)
context.restoreGState()
case let .image(reference):
if let image = parameters.images[parameters.peers[i].peerId] {
context.draw(image.cgImage!, in: imageRect)
} else {
context.setFillColor(UIColor.gray.cgColor)
context.fillEllipse(in: imageRect)
}
}
currentX -= mergedImageSpacing
}
}
}

View File

@ -403,7 +403,8 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode {
}, requestRedeliveryOfFailedMessages: { _ in
}, addContact: { _ in
}, rateCall: { _, _ in
}, requestSelectMessagePollOption: { _, _ in
}, requestSelectMessagePollOptions: { _, _ in
}, requestOpenMessagePollResults: { _, _ in
}, openAppStorePage: { [weak self] in
if let strongSelf = self {
strongSelf.context.sharedContext.applicationBindings.openAppStorePage()

View File

@ -107,7 +107,8 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, UIGestu
}, requestRedeliveryOfFailedMessages: { _ in
}, addContact: { _ in
}, rateCall: { _, _ in
}, requestSelectMessagePollOption: { _, _ in
}, requestSelectMessagePollOptions: { _, _ in
}, requestOpenMessagePollResults: { _, _ in
}, openAppStorePage: {
}, displayMessageTooltip: { _, _, _, _ in
}, seekToTimecode: { _, _, _ in

View File

@ -412,7 +412,8 @@ public class PeerMediaCollectionController: TelegramBaseController {
}, requestRedeliveryOfFailedMessages: { _ in
}, addContact: { _ in
}, rateCall: { _, _ in
}, requestSelectMessagePollOption: { _, _ in
}, requestSelectMessagePollOptions: { _, _ in
}, requestOpenMessagePollResults: { _, _ in
}, openAppStorePage: {
}, displayMessageTooltip: { _, _, _, _ in
}, seekToTimecode: { _, _, _ in

View File

@ -0,0 +1,313 @@
import Foundation
import Postbox
import SyncCore
import TelegramCore
import SwiftSignalKit
import TelegramPresentationData
import AccountContext
import ItemListUI
import Display
import ItemListPeerItem
import ItemListPeerActionItem
private let collapsedResultCount: Int = 10
private final class PollResultsControllerArguments {
let context: AccountContext
let collapseOption: (Data) -> Void
let expandOption: (Data) -> Void
let openPeer: (RenderedPeer) -> Void
init(context: AccountContext, collapseOption: @escaping (Data) -> Void, expandOption: @escaping (Data) -> Void, openPeer: @escaping (RenderedPeer) -> Void) {
self.context = context
self.collapseOption = collapseOption
self.expandOption = expandOption
self.openPeer = openPeer
}
}
private enum PollResultsSection {
case text
case option(Int)
var rawValue: Int32 {
switch self {
case .text:
return 0
case let .option(index):
return 1 + Int32(index)
}
}
}
private enum PollResultsEntryId: Hashable {
case text
case optionPeer(Int, Int)
case optionExpand(Int)
}
private enum PollResultsEntry: ItemListNodeEntry {
case text(String)
case optionPeer(optionId: Int, index: Int, peer: RenderedPeer, optionText: String, optionPercentage: Int, optionExpanded: Bool, opaqueIdentifier: Data)
case optionExpand(optionId: Int, opaqueIdentifier: Data, text: String)
var section: ItemListSectionId {
switch self {
case .text:
return PollResultsSection.text.rawValue
case let .optionPeer(optionPeer):
return PollResultsSection.option(optionPeer.optionId).rawValue
case let .optionExpand(optionExpand):
return PollResultsSection.option(optionExpand.optionId).rawValue
}
}
var stableId: PollResultsEntryId {
switch self {
case .text:
return .text
case let .optionPeer(optionPeer):
return .optionPeer(optionPeer.optionId, optionPeer.index)
case let .optionExpand(optionExpand):
return .optionExpand(optionExpand.optionId)
}
}
static func <(lhs: PollResultsEntry, rhs: PollResultsEntry) -> Bool {
switch lhs {
case .text:
switch rhs {
case .text:
return false
default:
return true
}
case let .optionPeer(lhsOptionPeer):
switch rhs {
case .text:
return false
case let .optionPeer(rhsOptionPeer):
if lhsOptionPeer.optionId == rhsOptionPeer.optionId {
return lhsOptionPeer.index < rhsOptionPeer.index
} else {
return lhsOptionPeer.optionId < rhsOptionPeer.optionId
}
case let .optionExpand(rhsOptionExpand):
if lhsOptionPeer.optionId == rhsOptionExpand.optionId {
return true
} else {
return lhsOptionPeer.optionId < rhsOptionExpand.optionId
}
}
case let .optionExpand(lhsOptionExpand):
switch rhs {
case .text:
return false
case let .optionPeer(rhsOptionPeer):
if lhsOptionExpand.optionId == rhsOptionPeer.optionId {
return false
} else {
return lhsOptionExpand.optionId < rhsOptionPeer.optionId
}
case let .optionExpand(rhsOptionExpand):
if lhsOptionExpand.optionId == rhsOptionExpand.optionId {
return false
} else {
return lhsOptionExpand.optionId < rhsOptionExpand.optionId
}
}
}
}
func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem {
let arguments = arguments as! PollResultsControllerArguments
switch self {
case let .text(text):
return ItemListTextItem(presentationData: presentationData, text: .large(text), sectionId: self.section)
case let .optionPeer(optionId, _, peer, optionText, optionPercentage, optionExpanded, opaqueIdentifier):
let header = ItemListPeerItemHeader(theme: presentationData.theme, strings: presentationData.strings, text: optionText, actionTitle: optionExpanded ? presentationData.strings.PollResults_Collapse : "\(optionPercentage)%", id: Int64(optionId), action: optionExpanded ? {
arguments.collapseOption(opaqueIdentifier)
} : nil)
return ItemListPeerItem(presentationData: presentationData, dateTimeFormat: PresentationDateTimeFormat(timeFormat: .regular, dateFormat: .dayFirst, dateSeparator: ".", decimalSeparator: ".", groupingSeparator: ""), nameDisplayOrder: .firstLast, context: arguments.context, peer: peer.peers[peer.peerId]!, presence: nil, text: .none, label: .none, editing: ItemListPeerItemEditing(editable: false, editing: false, revealed: false), switchValue: nil, enabled: true, selectable: true, sectionId: self.section, action: {
arguments.openPeer(peer)
}, setPeerIdWithRevealedOptions: { _, _ in
}, removePeer: { _ in
}, noInsets: true, header: header)
case let .optionExpand(_, opaqueIdentifier, text):
return ItemListPeerActionItem(presentationData: presentationData, icon: PresentationResourcesItemList.downArrowImage(presentationData.theme), title: text, sectionId: self.section, editing: false, action: {
arguments.expandOption(opaqueIdentifier)
})
}
}
}
private struct PollResultsControllerState: Equatable {
var expandedOptions = Set<Data>()
}
private func pollResultsControllerEntries(presentationData: PresentationData, poll: TelegramMediaPoll, state: PollResultsControllerState, resultsState: PollResultsState) -> [PollResultsEntry] {
var entries: [PollResultsEntry] = []
var isEmpty = false
for (_, optionState) in resultsState.options {
if !optionState.hasLoadedOnce {
isEmpty = true
break
}
}
entries.append(.text(poll.text))
if isEmpty {
return entries
}
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 {
for i in 0 ..< poll.options.count {
inner: for optionVoters in voters {
if optionVoters.opaqueIdentifier == poll.options[i].opaqueIdentifier {
optionVoterCount[i] = optionVoters.count
break inner
}
}
}
}
optionPercentage = countNicePercent(votes: (0 ..< poll.options.count).map({ Int(optionVoterCount[$0] ?? 0) }), total: Int(totalVoterCount))
}
for i in 0 ..< poll.options.count {
let option = poll.options[i]
if let optionState = resultsState.options[option.opaqueIdentifier], !optionState.peers.isEmpty {
let percentage = optionPercentage.count > i ? optionPercentage[i] : 0
var peerIndex = 0
var hasMore = false
let optionExpanded = state.expandedOptions.contains(option.opaqueIdentifier)
var peers = optionState.peers
var count = optionState.count
/*#if DEBUG
for _ in 0 ..< 10 {
peers += peers
}
count = max(count, peers.count)
#endif*/
inner: for peer in peers {
if !optionExpanded && peerIndex >= collapsedResultCount {
hasMore = true
break inner
}
entries.append(.optionPeer(optionId: i, index: peerIndex, peer: peer, optionText: option.text, optionPercentage: percentage, optionExpanded: optionExpanded, opaqueIdentifier: option.opaqueIdentifier))
peerIndex += 1
}
if hasMore {
let remainingCount = count - peerIndex
entries.append(.optionExpand(optionId: i, opaqueIdentifier: option.opaqueIdentifier, text: presentationData.strings.PollResults_ShowMore(Int32(remainingCount))))
}
}
}
return entries
}
public func pollResultsController(context: AccountContext, messageId: MessageId, poll: TelegramMediaPoll) -> ViewController {
let statePromise = ValuePromise(PollResultsControllerState(), ignoreRepeated: true)
let stateValue = Atomic(value: PollResultsControllerState())
let updateState: ((PollResultsControllerState) -> PollResultsControllerState) -> Void = { f in
statePromise.set(stateValue.modify { f($0) })
}
var pushControllerImpl: ((ViewController) -> Void)?
var dismissImpl: (() -> Void)?
let actionsDisposable = DisposableSet()
let resultsContext = PollResultsContext(account: context.account, messageId: messageId, poll: poll)
let arguments = PollResultsControllerArguments(context: context,
collapseOption: { optionId in
updateState { state in
var state = state
state.expandedOptions.remove(optionId)
return state
}
}, expandOption: { optionId in
updateState { state in
var state = state
state.expandedOptions.insert(optionId)
return state
}
let _ = (resultsContext.state
|> take(1)
|> deliverOnMainQueue).start(next: { [weak resultsContext] state in
if let optionState = state.options[optionId] {
if optionState.canLoadMore && optionState.peers.count <= collapsedResultCount {
resultsContext?.loadMore(optionOpaqueIdentifier: optionId)
}
}
})
}, openPeer: { peer in
if let peer = peer.peers[peer.peerId] {
if let controller = context.sharedContext.makePeerInfoController(context: context, peer: peer, mode: .generic) {
pushControllerImpl?(controller)
}
}
})
let previousWasEmpty = Atomic<Bool?>(value: nil)
let signal = combineLatest(queue: .mainQueue(),
context.sharedContext.presentationData,
statePromise.get(),
resultsContext.state
)
|> map { presentationData, state, resultsState -> (ItemListControllerState, (ItemListNodeState, Any)) in
let leftNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Close), style: .regular, enabled: true, action: {
dismissImpl?()
})
var isEmpty = false
for (_, optionState) in resultsState.options {
if !optionState.hasLoadedOnce {
isEmpty = true
break
}
}
var emptyStateItem: ItemListControllerEmptyStateItem?
if isEmpty {
emptyStateItem = ItemListLoadingIndicatorEmptyStateItem(theme: presentationData.theme)
}
let previousWasEmptyValue = previousWasEmpty.swap(isEmpty)
let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(presentationData.strings.PollResults_Title), leftNavigationButton: leftNavigationButton, rightNavigationButton: nil, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: false)
let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: pollResultsControllerEntries(presentationData: presentationData, poll: poll, state: state, resultsState: resultsState), style: .blocks, focusItemTag: nil, ensureVisibleItemTag: nil, emptyStateItem: emptyStateItem, crossfadeState: previousWasEmptyValue != nil && previousWasEmptyValue == true && isEmpty == false, animateChanges: false)
return (controllerState, (listState, arguments))
}
|> afterDisposed {
actionsDisposable.dispose()
}
let controller = ItemListController(context: context, state: signal)
controller.navigationPresentation = .modal
pushControllerImpl = { [weak controller] c in
controller?.push(c)
}
dismissImpl = { [weak controller] in
controller?.dismiss()
}
controller.isOpaqueWhenInOverlay = true
controller.blocksBackgroundWhenInOverlay = true
return controller
}

View File

@ -1116,7 +1116,8 @@ public final class SharedAccountContextImpl: SharedAccountContext {
}, requestRedeliveryOfFailedMessages: { _ in
}, addContact: { _ in
}, rateCall: { _, _ in
}, requestSelectMessagePollOption: { _, _ in
}, requestSelectMessagePollOptions: { _, _ in
}, requestOpenMessagePollResults: { _, _ in
}, openAppStorePage: {
}, displayMessageTooltip: { _, _, _, _ in
}, seekToTimecode: { _, _, _ in

View File

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

View File

@ -400,7 +400,7 @@ final class WatchMediaHandler: WatchRequestHandler {
}
} |> mapToSignal({ peer -> Signal<UIImage?, NoError> in
if let peer = peer, let representation = peer.smallProfileImage {
let imageData = peerAvatarImageData(account: context.account, peer: peer, authorOfMessage: nil, representation: representation, synchronousLoad: false)
let imageData = peerAvatarImageData(account: context.account, peerReference: PeerReference(peer), authorOfMessage: nil, representation: representation, synchronousLoad: false)
if let imageData = imageData {
return imageData
|> map { data -> UIImage? in