mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-10-08 19:10:53 +00:00
Updated polls
This commit is contained in:
parent
54f46ccac3
commit
b87c84a17d
@ -3,7 +3,7 @@
|
||||
@implementation Serialization
|
||||
|
||||
- (NSUInteger)currentLayer {
|
||||
return 108;
|
||||
return 109;
|
||||
}
|
||||
|
||||
- (id _Nullable)parseMessage:(NSData * _Nullable)data {
|
||||
|
@ -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";
|
||||
|
@ -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]
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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 {
|
||||
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -4,6 +4,7 @@ import AsyncDisplayKit
|
||||
|
||||
public enum ListViewItemHeaderStickDirection {
|
||||
case top
|
||||
case topEdge
|
||||
case bottom
|
||||
}
|
||||
|
||||
|
@ -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))
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -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];
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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:
|
||||
|
@ -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()
|
||||
|
@ -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)
|
||||
|
@ -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>) {
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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):
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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! {
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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) }
|
||||
} ?? [])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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),
|
||||
|
@ -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,
|
||||
|
@ -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)),
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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"),
|
||||
|
@ -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()
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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()
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
313
submodules/TelegramUI/TelegramUI/PollResultsController.swift
Normal file
313
submodules/TelegramUI/TelegramUI/PollResultsController.swift
Normal 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
|
||||
}
|
||||
|
Binary file not shown.
@ -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
|
||||
|
Binary file not shown.
@ -448,12 +448,12 @@ public final class WalletStrings: Equatable {
|
||||
public var Wallet_SecureStorageReset_Title: String { return self._s[218]! }
|
||||
public var Wallet_Receive_CommentHeader: String { return self._s[219]! }
|
||||
public var Wallet_Info_ReceiveGrams: String { return self._s[220]! }
|
||||
public func Wallet_Updated_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)
|
||||
|
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user