[WIP] Polls

This commit is contained in:
Ilya Laktyushin 2026-03-17 11:12:58 +01:00
parent 1a08c3f0d0
commit 6519b5c84d
43 changed files with 960 additions and 330 deletions

View file

@ -799,6 +799,7 @@ public enum ChatControllerSubject: Equatable {
}
}
case tag(MessageTags)
case message(id: MessageSubject, highlight: MessageHighlight?, timecode: Double?, setupReply: Bool)
case scheduledMessages
case pinnedMessages(id: EngineMessage.Id?)
@ -807,6 +808,12 @@ public enum ChatControllerSubject: Equatable {
public static func ==(lhs: ChatControllerSubject, rhs: ChatControllerSubject) -> Bool {
switch lhs {
case let .tag(lhsTag):
if case let .tag(rhsTag) = rhs, lhsTag == rhsTag {
return true
} else {
return false
}
case let .message(lhsId, lhsHighlight, lhsTimecode, lhsSetupReply):
if case let .message(rhsId, rhsHighlight, rhsTimecode, rhsSetupReply) = rhs, lhsId == rhsId && lhsHighlight == rhsHighlight && lhsTimecode == rhsTimecode && lhsSetupReply == rhsSetupReply {
return true

View file

@ -34,6 +34,7 @@ private let smallGlassButtonSize = CGSize(width: 72.0, height: 62.0)
private let smallButtonWidth: CGFloat = 69.0
private let iconSize = CGSize(width: 30.0, height: 30.0)
private let glassPanelSideInset: CGFloat = 20.0
private let smallPanelWidth: CGFloat = 240.0
private final class IconComponent: Component {
public let account: Account
@ -1657,7 +1658,11 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate, ASGestureRecog
return
}
let width = layout.size.width
var width = layout.size.width
if self.buttons.count == 3 {
width = smallPanelWidth
}
var panelSideInset: CGFloat
switch self.panelStyle {
case .glass:
@ -1667,6 +1672,9 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate, ASGestureRecog
}
var distanceBetweenNodes = floorToScreenPixels((width - panelSideInset * 2.0 - self.buttonSize.width) / CGFloat(max(1, self.buttons.count - 1)))
if self.buttons.count == 3 {
distanceBetweenNodes = floorToScreenPixels((width - panelSideInset * 2.0 - 32.0) / CGFloat(max(1, self.buttons.count - 1)))
}
let internalWidth = distanceBetweenNodes * CGFloat(self.buttons.count - 1)
var buttonWidth = self.buttonSize.width
var leftNodeOriginX: CGFloat
@ -1680,6 +1688,9 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate, ASGestureRecog
case .legacy:
leftNodeOriginX = (width - internalWidth) / 2.0
}
if self.buttons.count == 3 {
leftNodeOriginX = floor((layout.size.width - width) / 2.0) + 16.0
}
if self.buttons.count > maxButtonsToFit && layout.size.width < layout.size.height {
switch self.panelStyle {
@ -2159,7 +2170,15 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate, ASGestureRecog
self.tabSelectionRecognizer = tabSelectionRecognizer
self.scrollNode.view.addGestureRecognizer(tabSelectionRecognizer)
}
let panelSize = CGSize(width: isSelecting ? textPanelWidth : layout.size.width - layout.safeInsets.left - layout.safeInsets.right - panelSideInset * 2.0, height: isSelecting ? textPanelHeight - 11.0 : glassPanelHeight)
let buttonsPanelWidth: CGFloat
if buttons.count == 3 {
buttonsPanelWidth = smallPanelWidth
} else {
buttonsPanelWidth = layout.size.width - layout.safeInsets.left - layout.safeInsets.right - panelSideInset * 2.0
}
let panelSize = CGSize(width: isSelecting ? textPanelWidth : buttonsPanelWidth, height: isSelecting ? textPanelHeight - 11.0 : glassPanelHeight)
let cornerRadius: CGFloat = isSelecting ? min(20.0, panelSize.height * 0.5) : panelSize.height * 0.5
let backgroundOriginX: CGFloat = isSelecting ? panelSideInset : floorToScreenPixels((layout.size.width - panelSize.width) / 2.0)

View file

@ -2174,7 +2174,7 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
let textFont = Font.regular(floor(item.presentationData.fontSize.itemListBaseFontSize * 15.0 / 17.0))
let italicTextFont = Font.italic(floor(item.presentationData.fontSize.itemListBaseFontSize * 15.0 / 17.0))
let dateFont = Font.regular(floor(item.presentationData.fontSize.itemListBaseFontSize * 14.0 / 17.0))
let badgeFont = Font.with(size: floor(item.presentationData.fontSize.itemListBaseFontSize * 14.0 / 17.0), design: .regular, weight: .regular, traits: [.monospacedNumbers])
let badgeFont = Font.with(size: floor(item.presentationData.fontSize.itemListBaseFontSize * 12.0 / 17.0), design: .regular, weight: .semibold, traits: [.monospacedNumbers])
let avatarBadgeFont = Font.with(size: floor(item.presentationData.fontSize.itemListBaseFontSize * 16.0 / 17.0), design: .regular, weight: .regular, traits: [.monospacedNumbers])
let account = item.context.account

View file

@ -30,10 +30,11 @@ static const void *positionChangedKey = &positionChangedKey;
@interface TGIconSwitchView () {
UIImageView *_offIconView;
UIImageView *_onIconView;
UIColor *_positiveContentColor;
UIColor *_negativeContentColor;
bool _stateIsOn;
bool _isLocked;
NSNumber *_isLocked;
}
@end
@ -103,7 +104,7 @@ static const void *positionChangedKey = &positionChangedKey;
offset = CGPointMake(-7.0, -3.0);
}
if (_isLocked) {
if (_isLocked.boolValue) {
_offIconView.frame = CGRectOffset(_offIconView.bounds, TGScreenPixelFloor(21.5f) + offset.x, TGScreenPixelFloor(12.5f) + offset.y);
} else {
_offIconView.frame = CGRectOffset(_offIconView.bounds, TGScreenPixelFloor(21.5f) + offset.x, TGScreenPixelFloor(14.5f) + offset.y);
@ -141,22 +142,49 @@ static const void *positionChangedKey = &positionChangedKey;
}
- (void)setPositiveContentColor:(UIColor *)color {
_positiveContentColor = color;
_onIconView.image = TGTintedImage(TGComponentsImageNamed(@"PermissionSwitchOn.png"), color);
if ([color isEqual:[UIColor clearColor]]) {
self.tintColor = nil;
self.backgroundColor = nil;
if (_isLocked.boolValue) {
_offIconView.hidden = false;
} else {
_offIconView.hidden = true;
}
} else {
self.tintColor = [UIColor redColor];
self.backgroundColor = [UIColor redColor];
}
}
- (void)setNegativeContentColor:(UIColor *)color {
_negativeContentColor = color;
if (_isLocked) {
if (_isLocked.boolValue) {
_offIconView.image = TGTintedImage(TGComponentsImageNamed(@"Item List/SwitchLockIcon"), color);
_offIconView.frame = CGRectMake(_offIconView.frame.origin.x, _offIconView.frame.origin.y, _offIconView.image.size.width, _offIconView.image.size.height);
} else {
_offIconView.image = TGTintedImage(TGComponentsImageNamed(@"PermissionSwitchOff.png"), color);
}
_offIconView.frame = CGRectMake(_offIconView.frame.origin.x, _offIconView.frame.origin.y, _offIconView.image.size.width, _offIconView.image.size.height);
}
- (void)updateIsLocked:(bool)isLocked {
if (_isLocked != isLocked) {
_isLocked = isLocked;
if (_isLocked == nil || _isLocked.boolValue != isLocked) {
_isLocked = @(isLocked);
if ([_positiveContentColor isEqual:[UIColor clearColor]]) {
if (!isLocked) {
_offIconView.hidden = true;
} else {
_offIconView.hidden = false;
}
_onIconView.hidden = true;
} else {
_offIconView.hidden = false;
_onIconView.hidden = false;
}
if (_negativeContentColor) {
[self setNegativeContentColor:_negativeContentColor];

View file

@ -454,7 +454,7 @@ func textMediaAndExpirationTimerFromApiMedia(_ media: Api.MessageMedia?, _ peerI
let (poll, results) = (messageMediaPollData.poll, messageMediaPollData.results)
switch poll {
case let .poll(pollData):
let (id, flags, question, answers, closePeriod) = (pollData.id, pollData.flags, pollData.question, pollData.answers, pollData.closePeriod)
let (id, flags, question, answers, closePeriod, closeDate) = (pollData.id, pollData.flags, pollData.question, pollData.answers, pollData.closePeriod, pollData.closeDate)
let publicity: TelegramMediaPollPublicity
if (flags & (1 << 1)) != 0 {
publicity = .public
@ -486,7 +486,7 @@ func textMediaAndExpirationTimerFromApiMedia(_ media: Api.MessageMedia?, _ peerI
if let apiAttachedMedia = messageMediaPollData.attachedMedia {
parsedAttachedMedia = textMediaAndExpirationTimerFromApiMedia(apiAttachedMedia, peerId).media
}
return (TelegramMediaPoll(pollId: MediaId(namespace: Namespaces.Media.CloudPoll, id: id), publicity: publicity, kind: kind, text: questionText, textEntities: questionEntities, options: answers.map(TelegramMediaPollOption.init(apiOption:)), correctAnswers: nil, results: TelegramMediaPollResults(apiResults: results), isClosed: (flags & (1 << 0)) != 0, deadlineTimeout: closePeriod, openAnswers: openAnswers, revotingDisabled: revotingDisabled, shuffleAnswers: shuffleAnswers, hideResultsUntilClose: hideResultsUntilClose, attachedMedia: parsedAttachedMedia), nil, nil, nil, nil, nil)
return (TelegramMediaPoll(pollId: MediaId(namespace: Namespaces.Media.CloudPoll, id: id), publicity: publicity, kind: kind, text: questionText, textEntities: questionEntities, options: answers.map(TelegramMediaPollOption.init(apiOption:)), correctAnswers: nil, results: TelegramMediaPollResults(apiResults: results), isClosed: (flags & (1 << 0)) != 0, deadlineTimeout: closePeriod, deadlineDate: closeDate, openAnswers: openAnswers, revotingDisabled: revotingDisabled, shuffleAnswers: shuffleAnswers, hideResultsUntilClose: hideResultsUntilClose, attachedMedia: parsedAttachedMedia), nil, nil, nil, nil, nil)
}
case let .messageMediaToDo(messageMediaToDoData):
let (todo, completions) = (messageMediaToDoData.todo, messageMediaToDoData.completions)

View file

@ -332,6 +332,9 @@ func mediaContentToUpload(accountPeerId: PeerId, network: Network, postbox: Post
if poll.deadlineTimeout != nil {
pollFlags |= 1 << 4
}
if poll.deadlineDate != nil {
pollFlags |= 1 << 5
}
if poll.openAnswers { pollFlags |= 1 << 6 }
if poll.revotingDisabled { pollFlags |= 1 << 7 }
if poll.shuffleAnswers { pollFlags |= 1 << 8 }
@ -383,10 +386,10 @@ func mediaContentToUpload(accountPeerId: PeerId, network: Network, postbox: Post
pollMediaFlags |= 1 << 3
if let solMedia = poll.results.solution?.media, let sm = cloudMediaToInputMedia(solMedia) {
pollMediaFlags |= 1 << 2
let inputPoll = Api.InputMedia.inputMediaPoll(.init(flags: pollMediaFlags, poll: Api.Poll.poll(.init(id: 0, flags: pollFlags, question: .textWithEntities(.init(text: poll.text, entities: apiEntitiesFromMessageTextEntities(poll.textEntities, associatedPeers: SimpleDictionary()))), answers: apiAnswers, closePeriod: poll.deadlineTimeout, closeDate: nil, hash: 0)), correctAnswers: correctAnswers, attachedMedia: im, solution: mappedSolution, solutionEntities: mappedSolutionEntities, solutionMedia: sm))
let inputPoll = Api.InputMedia.inputMediaPoll(.init(flags: pollMediaFlags, poll: Api.Poll.poll(.init(id: 0, flags: pollFlags, question: .textWithEntities(.init(text: poll.text, entities: apiEntitiesFromMessageTextEntities(poll.textEntities, associatedPeers: SimpleDictionary()))), answers: apiAnswers, closePeriod: poll.deadlineTimeout, closeDate: poll.deadlineDate, hash: 0)), correctAnswers: correctAnswers, attachedMedia: im, solution: mappedSolution, solutionEntities: mappedSolutionEntities, solutionMedia: sm))
return .single(.content(PendingMessageUploadedContentAndReuploadInfo(content: .media(inputPoll, text), reuploadInfo: nil, cacheReferenceKey: nil)))
} else {
let inputPoll = Api.InputMedia.inputMediaPoll(.init(flags: pollMediaFlags, poll: Api.Poll.poll(.init(id: 0, flags: pollFlags, question: .textWithEntities(.init(text: poll.text, entities: apiEntitiesFromMessageTextEntities(poll.textEntities, associatedPeers: SimpleDictionary()))), answers: apiAnswers, closePeriod: poll.deadlineTimeout, closeDate: nil, hash: 0)), correctAnswers: correctAnswers, attachedMedia: im, solution: mappedSolution, solutionEntities: mappedSolutionEntities, solutionMedia: nil))
let inputPoll = Api.InputMedia.inputMediaPoll(.init(flags: pollMediaFlags, poll: Api.Poll.poll(.init(id: 0, flags: pollFlags, question: .textWithEntities(.init(text: poll.text, entities: apiEntitiesFromMessageTextEntities(poll.textEntities, associatedPeers: SimpleDictionary()))), answers: apiAnswers, closePeriod: poll.deadlineTimeout, closeDate: poll.deadlineDate, hash: 0)), correctAnswers: correctAnswers, attachedMedia: im, solution: mappedSolution, solutionEntities: mappedSolutionEntities, solutionMedia: nil))
return .single(.content(PendingMessageUploadedContentAndReuploadInfo(content: .media(inputPoll, text), reuploadInfo: nil, cacheReferenceKey: nil)))
}
} else {
@ -395,7 +398,7 @@ func mediaContentToUpload(accountPeerId: PeerId, network: Network, postbox: Post
apiSolutionMedia = sm
pollMediaFlags |= 1 << 2
}
let inputPoll = Api.InputMedia.inputMediaPoll(.init(flags: pollMediaFlags, poll: Api.Poll.poll(.init(id: 0, flags: pollFlags, question: .textWithEntities(.init(text: poll.text, entities: apiEntitiesFromMessageTextEntities(poll.textEntities, associatedPeers: SimpleDictionary()))), answers: apiAnswers, closePeriod: poll.deadlineTimeout, closeDate: nil, hash: 0)), correctAnswers: correctAnswers, attachedMedia: nil, solution: mappedSolution, solutionEntities: mappedSolutionEntities, solutionMedia: apiSolutionMedia))
let inputPoll = Api.InputMedia.inputMediaPoll(.init(flags: pollMediaFlags, poll: Api.Poll.poll(.init(id: 0, flags: pollFlags, question: .textWithEntities(.init(text: poll.text, entities: apiEntitiesFromMessageTextEntities(poll.textEntities, associatedPeers: SimpleDictionary()))), answers: apiAnswers, closePeriod: poll.deadlineTimeout, closeDate: poll.deadlineDate, hash: 0)), correctAnswers: correctAnswers, attachedMedia: nil, solution: mappedSolution, solutionEntities: mappedSolutionEntities, solutionMedia: apiSolutionMedia))
return .single(.content(PendingMessageUploadedContentAndReuploadInfo(content: .media(inputPoll, text), reuploadInfo: nil, cacheReferenceKey: nil)))
}
} else if let todo = media as? TelegramMediaTodo {

View file

@ -4500,7 +4500,7 @@ func replayFinalState(
if let apiPoll = apiPoll {
switch apiPoll {
case let .poll(pollData):
let (id, flags, question, answers, closePeriod) = (pollData.id, pollData.flags, pollData.question, pollData.answers, pollData.closePeriod)
let (id, flags, question, answers, closePeriod, closeDate) = (pollData.id, pollData.flags, pollData.question, pollData.answers, pollData.closePeriod, pollData.closeDate)
let publicity: TelegramMediaPollPublicity
if (flags & (1 << 1)) != 0 {
publicity = .public
@ -4527,7 +4527,7 @@ func replayFinalState(
questionEntities = messageTextEntitiesFromApiEntities(entities)
}
updatedPoll = TelegramMediaPoll(pollId: MediaId(namespace: Namespaces.Media.CloudPoll, id: id), publicity: publicity, kind: kind, text: questionText, textEntities: questionEntities, options: answers.map(TelegramMediaPollOption.init(apiOption:)), correctAnswers: nil, results: poll.results, isClosed: (flags & (1 << 0)) != 0, deadlineTimeout: closePeriod, openAnswers: openAnswers, revotingDisabled: revotingDisabled, shuffleAnswers: shuffleAnswers, hideResultsUntilClose: hideResultsUntilClose, attachedMedia: poll.attachedMedia)
updatedPoll = TelegramMediaPoll(pollId: MediaId(namespace: Namespaces.Media.CloudPoll, id: id), publicity: publicity, kind: kind, text: questionText, textEntities: questionEntities, options: answers.map(TelegramMediaPollOption.init(apiOption:)), correctAnswers: nil, results: poll.results, isClosed: (flags & (1 << 0)) != 0, deadlineTimeout: closePeriod, deadlineDate: closeDate, openAnswers: openAnswers, revotingDisabled: revotingDisabled, shuffleAnswers: shuffleAnswers, hideResultsUntilClose: hideResultsUntilClose, attachedMedia: poll.attachedMedia)
}
}
updatedPoll = updatedPoll.withUpdatedResults(TelegramMediaPollResults(apiResults: results), min: resultsMin)

View file

@ -221,6 +221,7 @@ public final class TelegramMediaPoll: Media, Equatable {
public let results: TelegramMediaPollResults
public let isClosed: Bool
public let deadlineTimeout: Int32?
public let deadlineDate: Int32?
public let openAnswers: Bool
public let revotingDisabled: Bool
@ -228,7 +229,7 @@ public final class TelegramMediaPoll: Media, Equatable {
public let hideResultsUntilClose: Bool
public let attachedMedia: Media?
public init(pollId: MediaId, publicity: TelegramMediaPollPublicity, kind: TelegramMediaPollKind, text: String, textEntities: [MessageTextEntity], options: [TelegramMediaPollOption], correctAnswers: [Data]?, results: TelegramMediaPollResults, isClosed: Bool, deadlineTimeout: Int32?, openAnswers: Bool = false, revotingDisabled: Bool = false, shuffleAnswers: Bool = false, hideResultsUntilClose: Bool = false, attachedMedia: Media? = nil) {
public init(pollId: MediaId, publicity: TelegramMediaPollPublicity, kind: TelegramMediaPollKind, text: String, textEntities: [MessageTextEntity], options: [TelegramMediaPollOption], correctAnswers: [Data]?, results: TelegramMediaPollResults, isClosed: Bool, deadlineTimeout: Int32?, deadlineDate: Int32?, openAnswers: Bool = false, revotingDisabled: Bool = false, shuffleAnswers: Bool = false, hideResultsUntilClose: Bool = false, attachedMedia: Media? = nil) {
self.pollId = pollId
self.publicity = publicity
self.kind = kind
@ -239,6 +240,7 @@ public final class TelegramMediaPoll: Media, Equatable {
self.results = results
self.isClosed = isClosed
self.deadlineTimeout = deadlineTimeout
self.deadlineDate = deadlineDate
self.openAnswers = openAnswers
self.revotingDisabled = revotingDisabled
self.shuffleAnswers = shuffleAnswers
@ -261,6 +263,7 @@ public final class TelegramMediaPoll: Media, Equatable {
self.results = decoder.decodeObjectForKey("rs", decoder: { TelegramMediaPollResults(decoder: $0) }) as? TelegramMediaPollResults ?? TelegramMediaPollResults(voters: nil, totalVoters: nil, recentVoters: [], solution: nil)
self.isClosed = decoder.decodeInt32ForKey("ic", orElse: 0) != 0
self.deadlineTimeout = decoder.decodeOptionalInt32ForKey("dt")
self.deadlineDate = decoder.decodeOptionalInt32ForKey("dd")
self.openAnswers = decoder.decodeInt32ForKey("oa", orElse: 0) != 0
self.revotingDisabled = decoder.decodeInt32ForKey("rd", orElse: 0) != 0
self.shuffleAnswers = decoder.decodeInt32ForKey("sa", orElse: 0) != 0
@ -289,6 +292,11 @@ public final class TelegramMediaPoll: Media, Equatable {
} else {
encoder.encodeNil(forKey: "dt")
}
if let deadlineDate = self.deadlineDate {
encoder.encodeInt32(deadlineDate, forKey: "dd")
} else {
encoder.encodeNil(forKey: "dd")
}
encoder.encodeInt32(self.openAnswers ? 1 : 0, forKey: "oa")
encoder.encodeInt32(self.revotingDisabled ? 1 : 0, forKey: "rd")
encoder.encodeInt32(self.shuffleAnswers ? 1 : 0, forKey: "sa")
@ -342,6 +350,9 @@ public final class TelegramMediaPoll: Media, Equatable {
if lhs.deadlineTimeout != rhs.deadlineTimeout {
return false
}
if lhs.deadlineDate != rhs.deadlineDate {
return false
}
if lhs.openAnswers != rhs.openAnswers {
return false
}
@ -387,6 +398,6 @@ public final class TelegramMediaPoll: Media, Equatable {
} else {
updatedResults = results
}
return TelegramMediaPoll(pollId: self.pollId, publicity: self.publicity, kind: self.kind, text: self.text, textEntities: self.textEntities, options: self.options, correctAnswers: self.correctAnswers, results: updatedResults, isClosed: self.isClosed, deadlineTimeout: self.deadlineTimeout, openAnswers: self.openAnswers, revotingDisabled: self.revotingDisabled, shuffleAnswers: self.shuffleAnswers, hideResultsUntilClose: self.hideResultsUntilClose, attachedMedia: self.attachedMedia)
return TelegramMediaPoll(pollId: self.pollId, publicity: self.publicity, kind: self.kind, text: self.text, textEntities: self.textEntities, options: self.options, correctAnswers: self.correctAnswers, results: updatedResults, isClosed: self.isClosed, deadlineTimeout: self.deadlineTimeout, deadlineDate: self.deadlineDate, openAnswers: self.openAnswers, revotingDisabled: self.revotingDisabled, shuffleAnswers: self.shuffleAnswers, hideResultsUntilClose: self.hideResultsUntilClose, attachedMedia: self.attachedMedia)
}
}

View file

@ -34,7 +34,7 @@ func _internal_requestMessageSelectPollOption(account: Account, messageId: Messa
if let poll = poll {
switch poll {
case let .poll(pollData):
let (flags, question, answers, closePeriod) = (pollData.flags, pollData.question, pollData.answers, pollData.closePeriod)
let (flags, question, answers, closePeriod, closeDate) = (pollData.flags, pollData.question, pollData.answers, pollData.closePeriod, pollData.closeDate)
let publicity: TelegramMediaPollPublicity
if (flags & (1 << 1)) != 0 {
publicity = .public
@ -59,7 +59,7 @@ func _internal_requestMessageSelectPollOption(account: Account, messageId: Messa
questionText = text
questionEntities = messageTextEntitiesFromApiEntities(entities)
}
resultPoll = TelegramMediaPoll(pollId: pollId, publicity: publicity, kind: kind, text: questionText, textEntities: questionEntities, options: answers.map(TelegramMediaPollOption.init(apiOption:)), correctAnswers: nil, results: TelegramMediaPollResults(apiResults: results), isClosed: (flags & (1 << 0)) != 0, deadlineTimeout: closePeriod, openAnswers: openAnswers, revotingDisabled: revotingDisabled, shuffleAnswers: shuffleAnswers, hideResultsUntilClose: hideResultsUntilClose, attachedMedia: resultPoll?.attachedMedia)
resultPoll = TelegramMediaPoll(pollId: pollId, publicity: publicity, kind: kind, text: questionText, textEntities: questionEntities, options: answers.map(TelegramMediaPollOption.init(apiOption:)), correctAnswers: nil, results: TelegramMediaPollResults(apiResults: results), isClosed: (flags & (1 << 0)) != 0, deadlineTimeout: closePeriod, deadlineDate: closeDate, openAnswers: openAnswers, revotingDisabled: revotingDisabled, shuffleAnswers: shuffleAnswers, hideResultsUntilClose: hideResultsUntilClose, attachedMedia: resultPoll?.attachedMedia)
}
}

View file

@ -180,6 +180,11 @@ public enum PresentationResourceKey: Int32 {
case chatBubbleTodoCheckIncomingIcon
case chatBubbleTodoCheckOutgoingIcon
case chatBubblePollChevronLeftIncomingIcon
case chatBubblePollChevronLeftOutgoingIcon
case chatBubblePollChevronRightIncomingIcon
case chatBubblePollChevronRightOutgoingIcon
case chatServiceMessageTodoCompletedIcon
case chatServiceMessageTodoIncompletedIcon
case chatServiceMessageTodoAppendedIcon

View file

@ -1479,6 +1479,52 @@ public struct PresentationResourcesChat {
})
}
public static func chatBubblePollChevronLeftIncomingIcon(_ theme: PresentationTheme) -> UIImage? {
return theme.image(PresentationResourceKey.chatBubblePollChevronLeftIncomingIcon.rawValue, { theme in
if let image = UIImage(bundleImageName: "Settings/TextArrowRight") {
return generateImage(image.size, contextGenerator: { size, context in
context.clear(CGRect(origin: .zero, size: size))
context.translateBy(x: size.width / 2.0, y: size.height / 2.0)
context.scaleBy(x: -1.0, y: -1.0)
context.translateBy(x: -size.width / 2.0, y: -size.height / 2.0)
if let image = generateTintedImage(image: UIImage(bundleImageName: "Settings/TextArrowRight"), color: theme.chat.message.incoming.accentTextColor), let cgImage = image.cgImage {
context.draw(cgImage, in: CGRect(origin: .zero, size: image.size))
}
})
}
return nil
})
}
public static func chatBubblePollChevronLeftOutgoingIcon(_ theme: PresentationTheme) -> UIImage? {
return theme.image(PresentationResourceKey.chatBubblePollChevronLeftOutgoingIcon.rawValue, { theme in
if let image = UIImage(bundleImageName: "Settings/TextArrowRight") {
return generateImage(image.size, contextGenerator: { size, context in
context.clear(CGRect(origin: .zero, size: size))
context.translateBy(x: size.width / 2.0, y: size.height / 2.0)
context.scaleBy(x: -1.0, y: -1.0)
context.translateBy(x: -size.width / 2.0, y: -size.height / 2.0)
if let image = generateTintedImage(image: UIImage(bundleImageName: "Settings/TextArrowRight"), color: theme.chat.message.outgoing.accentTextColor), let cgImage = image.cgImage {
context.draw(cgImage, in: CGRect(origin: .zero, size: image.size))
}
})
}
return nil
})
}
public static func chatBubblePollChevronRightIncomingIcon(_ theme: PresentationTheme) -> UIImage? {
return theme.image(PresentationResourceKey.chatBubblePollChevronRightIncomingIcon.rawValue, { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Settings/TextArrowRight"), color: theme.chat.message.outgoing.accentTextColor)
})
}
public static func chatBubblePollChevronRightOutgoingIcon(_ theme: PresentationTheme) -> UIImage? {
return theme.image(PresentationResourceKey.chatBubblePollChevronRightOutgoingIcon.rawValue, { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Settings/TextArrowRight"), color: theme.chat.message.outgoing.accentTextColor)
})
}
public static func messageButtonsPostReject(_ theme: PresentationTheme) -> UIImage? {
return theme.image(PresentationResourceKey.messageButtonsPostReject.rawValue, { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/SuggestPostDecline"), color: .white)

View file

@ -207,25 +207,26 @@ public struct PresentationResourcesChatList {
public static func badgeBackgroundMention(_ theme: PresentationTheme, diameter: CGFloat) -> UIImage? {
return theme.image(PresentationResourceParameterKey.chatListBadgeBackgroundMention(diameter), { theme in
return generateBadgeBackgroundImage(theme: theme, diameter: diameter, active: true, reaction: false, provisional: false, icon: generateTintedImage(image: UIImage(bundleImageName: "Chat List/MentionBadgeIcon"), color: theme.chatList.unreadBadgeActiveTextColor))
//return generateScaledImage(image: generateTintedImage(image: UIImage(bundleImageName: "Chat List/MentionBadgeIcon"), color: theme.chatList.unreadBadgeActiveBackgroundColor), size: CGSize(width: diameter, height: diameter))
return generateTintedImage(image: UIImage(bundleImageName: "Chat List/MentionBadgeIcon"), color: theme.chatList.unreadBadgeActiveBackgroundColor)
})
}
public static func badgeBackgroundInactiveMention(_ theme: PresentationTheme, diameter: CGFloat) -> UIImage? {
return theme.image(PresentationResourceParameterKey.chatListBadgeBackgroundInactiveMention(diameter), { theme in
return generateBadgeBackgroundImage(theme: theme, diameter: diameter, active: false, reaction: false, provisional: false, icon: generateTintedImage(image: UIImage(bundleImageName: "Chat List/MentionBadgeIcon"), color: theme.chatList.unreadBadgeInactiveTextColor))
return generateTintedImage(image: UIImage(bundleImageName: "Chat List/MentionBadgeIcon"), color: theme.chatList.unreadBadgeInactiveBackgroundColor)
})
}
public static func badgeBackgroundReactions(_ theme: PresentationTheme, diameter: CGFloat) -> UIImage? {
return theme.image(PresentationResourceParameterKey.badgeBackgroundReactions(diameter), { theme in
return generateBadgeBackgroundImage(theme: theme, diameter: diameter, active: true, reaction: true, provisional: false, icon: generateTintedImage(image: UIImage(bundleImageName: "Chat List/ReactionsBadgeIcon"), color: theme.chatList.unreadBadgeActiveTextColor))
return generateTintedImage(image: UIImage(bundleImageName: "Chat List/ReactionsBadgeIcon"), color: theme.chatList.reactionBadgeActiveBackgroundColor)
})
}
public static func badgeBackgroundInactiveReactions(_ theme: PresentationTheme, diameter: CGFloat) -> UIImage? {
return theme.image(PresentationResourceParameterKey.badgeBackgroundInactiveReactions(diameter), { theme in
return generateBadgeBackgroundImage(theme: theme, diameter: diameter, active: false, reaction: false, provisional: false, icon: generateTintedImage(image: UIImage(bundleImageName: "Chat List/ReactionsBadgeIcon"), color: theme.chatList.unreadBadgeInactiveTextColor))
return generateTintedImage(image: UIImage(bundleImageName: "Chat List/ReactionsBadgeIcon"), color: theme.chatList.unreadBadgeInactiveBackgroundColor)
})
}

View file

@ -1163,7 +1163,7 @@ private final class AdminUserActionsSheetComponent: Component {
theme: environment.theme,
title: itemTitle,
accessory: .toggle(ListActionItemComponent.Toggle(
style: isEnabled ? .icons : .lock,
style: isEnabled ? .icons : .lock(isLocked: true),
isOn: itemValue,
isInteractive: isEnabled,
action: isEnabled ? { [weak self] _ in

View file

@ -38,7 +38,6 @@ swift_library(
"//submodules/TelegramUI/Components/Chat/ChatMessageInteractiveMediaNode",
"//submodules/TelegramUI/Components/WallpaperPreviewMedia",
"//submodules/TelegramUI/Components/Chat/ChatMessageAttachedContentButtonNode",
"//submodules/TelegramUI/Components/Chat/ChatMessagePollBubbleContentNode",
"//submodules/TelegramUI/Components/Chat/MessageInlineBlockBackgroundView",
"//submodules/ComponentFlow",
"//submodules/TelegramUI/Components/PlainButtonComponent",

View file

@ -285,6 +285,10 @@ private func contentNodeMessagesAndClassesForItem(_ item: ChatMessageItem) -> ([
result.append((message, ChatMessageActionBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: .default)))
return (result, false, true)
} else if let poll = media as? TelegramMediaPoll {
if item.controllerInteraction.currentPollMessageWithTooltip == item.message.id, let _ = poll.results.solution {
result.append((message, ChatMessageQuizAnswerBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .media, neighborSpacing: .default)))
}
if let attachedMedia = poll.attachedMedia {
if let _ = attachedMedia as? TelegramMediaImage {
result.append((message, ChatMessageMediaBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .media, neighborSpacing: .default)))
@ -6437,6 +6441,8 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
contentNode.updateQuoteTextHighlightState(text: nil, offset: nil, color: .clear, animated: true)
} else if let contentNode = contentNode as? ChatMessageTodoBubbleContentNode {
contentNode.updateTaskHighlightState(id: nil, color: .clear, animated: true)
} else if let contentNode = contentNode as? ChatMessagePollBubbleContentNode {
contentNode.updateOptionHighlightState(id: nil, color: .clear, animated: true)
}
}

View file

@ -26,6 +26,7 @@ swift_library(
"//submodules/TelegramUI/Components/Chat/ChatMessageItemCommon",
"//submodules/TelegramUI/Components/Chat/PollBubbleTimerNode",
"//submodules/TelegramUI/Components/Chat/MergedAvatarsNode",
"//submodules/TelegramUI/Components/Chat/ChatMessageAttachedContentNode",
"//submodules/TelegramUI/Components/TextNodeWithEntities",
"//submodules/TelegramUI/Components/Chat/ShimmeringLinkNode",
"//submodules/TelegramUI/Components/EmojiTextAttachmentView",

View file

@ -450,6 +450,8 @@ private final class ChatMessagePollOptionNode: ASDisplayNode {
private static let avatarsSpacing: CGFloat = 6.0
private static let countSpacing: CGFloat = 6.0
private var backgroundWallpaperNode: ChatMessageBubbleBackdrop?
private var backgroundNode: ChatMessageBackground?
private var extractedContainerView: UIView?
fileprivate let contextSourceNode: ContextExtractedContentContainingNode
@ -479,7 +481,11 @@ private final class ChatMessagePollOptionNode: ASDisplayNode {
var pressed: (() -> Void)?
var selectionUpdated: (() -> Void)?
var longTapped: (() -> Void)?
var context: AccountContext?
var message: Message?
private var theme: PresentationTheme?
private var presentationData: ChatPresentationData?
private var presentationContext: ChatPresentationContext?
weak var previousOptionNode: ChatMessagePollOptionNode?
@ -598,12 +604,59 @@ private final class ChatMessagePollOptionNode: ASDisplayNode {
}
}
self.contextSourceNode.willUpdateIsExtractedToContextPreview = { [weak self] isExtractedToContextPreview, _ in
self.contextSourceNode.willUpdateIsExtractedToContextPreview = { [weak self] isExtractedToContextPreview, transition in
guard let self else {
return
}
if isExtractedToContextPreview {
self.buttonNode.highligthedChanged(false)
var offset: CGFloat = 0.0
var inset: CGFloat = 0.0
var type: ChatMessageBackgroundType
var incoming = false
if let context = self.context, let message = self.message {
incoming = message.effectivelyIncoming(context.account.peerId)
}
if incoming {
type = .incoming(.Extracted)
offset = -5.0
inset = 5.0
} else {
type = .outgoing(.Extracted)
inset = 5.0
}
if let _ = self.backgroundNode {
} else if let presentationData = self.presentationData, let presentationContext = self.presentationContext {
let graphics = PresentationResourcesChat.principalGraphics(theme: presentationData.theme.theme, wallpaper: presentationData.theme.wallpaper, bubbleCorners: presentationData.chatBubbleCorners)
let backgroundWallpaperNode = ChatMessageBubbleBackdrop()
backgroundWallpaperNode.alpha = 0.0
let backgroundNode = ChatMessageBackground()
backgroundNode.alpha = 0.0
self.contextSourceNode.contentNode.insertSubnode(backgroundNode, at: 0)
self.contextSourceNode.contentNode.insertSubnode(backgroundWallpaperNode, at: 0)
self.backgroundWallpaperNode = backgroundWallpaperNode
self.backgroundNode = backgroundNode
transition.updateAlpha(node: backgroundNode, alpha: 1.0)
transition.updateAlpha(node: backgroundWallpaperNode, alpha: 1.0)
backgroundNode.setType(type: type, highlighted: false, graphics: graphics, maskMode: true, hasWallpaper: presentationData.theme.wallpaper.hasWallpaper, transition: .immediate, backgroundNode: presentationContext.backgroundNode)
backgroundWallpaperNode.setType(type: type, theme: presentationData.theme, essentialGraphics: graphics, maskMode: true, backgroundNode: presentationContext.backgroundNode)
}
let backgroundFrame = CGRect(x: offset, y: 0.0, width: self.bounds.width + inset, height: self.bounds.height)
self.backgroundNode?.updateLayout(size: backgroundFrame.size, transition: .immediate)
self.backgroundNode?.frame = backgroundFrame
self.backgroundWallpaperNode?.frame = backgroundFrame
self.extractedContainerView?.removeFromSuperview()
self.extractedContainerView = nil
@ -612,6 +665,19 @@ private final class ChatMessagePollOptionNode: ASDisplayNode {
self.contextSourceNode.contentNode.view.addSubview(extractedContainerView)
self.extractedContainerView = extractedContainerView
}
} else {
if let backgroundNode = self.backgroundNode {
self.backgroundNode = nil
transition.updateAlpha(node: backgroundNode, alpha: 0.0, completion: { [weak backgroundNode] _ in
backgroundNode?.removeFromSupernode()
})
}
if let backgroundWallpaperNode = self.backgroundWallpaperNode {
self.backgroundWallpaperNode = nil
transition.updateAlpha(node: backgroundWallpaperNode, alpha: 0.0, completion: { [weak backgroundWallpaperNode] _ in
backgroundWallpaperNode?.removeFromSupernode()
})
}
}
}
@ -669,22 +735,15 @@ private final class ChatMessagePollOptionNode: ASDisplayNode {
})
}
static func asyncLayout(_ maybeNode: ChatMessagePollOptionNode?) -> (_ context: AccountContext, _ presentationData: ChatPresentationData, _ message: Message, _ poll: TelegramMediaPoll, _ option: TelegramMediaPollOption, _ translation: TranslationMessageAttribute.Additional?, _ optionResult: ChatMessagePollOptionResult?, _ hasAnyMedia: Bool, _ constrainedWidth: CGFloat) -> (minimumWidth: CGFloat, layout: ((CGFloat) -> (CGSize, (Bool, Bool, Bool) -> ChatMessagePollOptionNode))) {
static func asyncLayout(_ maybeNode: ChatMessagePollOptionNode?) -> (_ context: AccountContext, _ presentationData: ChatPresentationData, _ presentationContext: ChatPresentationContext, _ message: Message, _ poll: TelegramMediaPoll, _ option: TelegramMediaPollOption, _ translation: TranslationMessageAttribute.Additional?, _ optionResult: ChatMessagePollOptionResult?, _ hasAnyMedia: Bool, _ constrainedWidth: CGFloat) -> (minimumWidth: CGFloat, layout: ((CGFloat) -> (CGSize, (Bool, Bool, Bool) -> ChatMessagePollOptionNode))) {
let makeTitleLayout = TextNodeWithEntities.asyncLayout(maybeNode?.titleNode)
let currentResult = maybeNode?.currentResult
let currentSelection = maybeNode?.currentSelection
let currentTheme = maybeNode?.theme
return { context, presentationData, message, poll, option, translation, optionResult, hasAnyMedia, constrainedWidth in
return { context, presentationData, presentationContext, message, poll, option, translation, optionResult, hasAnyMedia, constrainedWidth in
let leftInset: CGFloat = 50.0
let media = option.media
// let hasRecentVoterAvatars = !(optionResult?.recentVoterPeerIds.isEmpty ?? true)
// let avatarsInset: CGFloat
// if hasRecentVoterAvatars {
// avatarsInset = ChatMessagePollOptionNode.avatarsSize.width + ChatMessagePollOptionNode.avatarsSpacing
// } else {
// avatarsInset = 0.0
// }
let mediaInset: CGFloat
if hasAnyMedia {
mediaInset = ChatMessagePollOptionNode.mediaSize.width + ChatMessagePollOptionNode.mediaSpacing + ChatMessagePollOptionNode.mediaRightInset
@ -724,7 +783,7 @@ private final class ChatMessagePollOptionNode: ASDisplayNode {
} else {
isSelectable = false
}
let themeUpdated = presentationData.theme.theme !== currentTheme
var updatedPercentageImage: UIImage?
@ -837,6 +896,8 @@ private final class ChatMessagePollOptionNode: ASDisplayNode {
}
node.option = option
node.context = context
node.message = message
let previousMedia = node.currentMedia
node.currentMedia = media
node.currentRecentVoterPeerIds = optionResult?.recentVoterPeerIds ?? []
@ -844,6 +905,8 @@ private final class ChatMessagePollOptionNode: ASDisplayNode {
node.currentResult = optionResult
node.currentSelection = selection
node.theme = presentationData.theme.theme
node.presentationData = presentationData
node.presentationContext = presentationContext
node.highlightedBackgroundNode.backgroundColor = incoming ? presentationData.theme.theme.chat.message.incoming.polls.highlight : presentationData.theme.theme.chat.message.outgoing.polls.highlight
@ -1225,6 +1288,7 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode {
private let solutionButtonNode: SolutionButtonNode
private let avatarsNode: MergedAvatarsNode
private let votersNode: TextNode
private var deadlineTimerNode: DeadlineTimerNode?
private let buttonSubmitInactiveTextNode: TextNode
private let buttonSubmitActiveTextNode: TextNode
private let buttonViewResultsTextNode: TextNode
@ -1236,6 +1300,8 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode {
private let temporaryHiddenMediaDisposable = MetaDisposable()
private var poll: TelegramMediaPoll?
private var isPreviewingResults = false
public var solutionTipSourceNode: ASDisplayNode {
return self.solutionButtonNode
@ -1367,36 +1433,6 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode {
return nil
}
private func resolvedOptionOrder(for poll: TelegramMediaPoll) -> (orderedOptions: [(Int, TelegramMediaPollOption)], shuffledOpaqueIdentifiers: [Data]?) {
let defaultOrderedOptions = Array(poll.options.enumerated()).map { ($0.offset, $0.element) }
guard poll.shuffleAnswers else {
return (defaultOrderedOptions, nil)
}
let currentOpaqueIdentifiers = poll.options.map(\.opaqueIdentifier)
let resolvedOpaqueIdentifiers: [Data]
if let shuffledOptionOpaqueIdentifiers = self.shuffledOptionOpaqueIdentifiers, shuffledOptionOpaqueIdentifiers.count == currentOpaqueIdentifiers.count, Set(shuffledOptionOpaqueIdentifiers) == Set(currentOpaqueIdentifiers) {
resolvedOpaqueIdentifiers = shuffledOptionOpaqueIdentifiers
} else {
var generatedOpaqueIdentifiers = currentOpaqueIdentifiers
generatedOpaqueIdentifiers.shuffle()
resolvedOpaqueIdentifiers = generatedOpaqueIdentifiers
}
let indicesByOpaqueIdentifier = Dictionary(uniqueKeysWithValues: poll.options.enumerated().map { ($0.element.opaqueIdentifier, $0.offset) })
let orderedOptions = resolvedOpaqueIdentifiers.compactMap { opaqueIdentifier -> (Int, TelegramMediaPollOption)? in
guard let index = indicesByOpaqueIdentifier[opaqueIdentifier] else {
return nil
}
return (index, poll.options[index])
}
if orderedOptions.count == poll.options.count {
return (orderedOptions, resolvedOpaqueIdentifiers)
} else {
return (defaultOrderedOptions, currentOpaqueIdentifiers)
}
}
private func openOptionMedia(_ media: Media) {
guard let item = self.item else {
@ -1505,9 +1541,30 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode {
}
}
}
if !hasSelection {
var canAlwaysViewResults = false
if let peer = item.message.peers[item.message.id.peerId] {
if let group = peer as? TelegramGroup {
switch group.role {
case .creator, .admin:
canAlwaysViewResults = true
default:
break
}
} else if let channel = peer as? TelegramChannel, let _ = channel.adminRights {
canAlwaysViewResults = true
}
}
if !hasSelection || (canAlwaysViewResults && selectedOpaqueIdentifiers.isEmpty) {
if !Namespaces.Message.allNonRegular.contains(item.message.id.namespace) {
item.controllerInteraction.requestOpenMessagePollResults(item.message.id, pollId)
switch poll.publicity {
case .public:
item.controllerInteraction.requestOpenMessagePollResults(item.message.id, pollId)
case .anonymous:
self.isPreviewingResults = !self.isPreviewingResults
item.controllerInteraction.requestMessageUpdate(item.message.id, false)
}
}
} else if !selectedOpaqueIdentifiers.isEmpty {
item.controllerInteraction.requestSelectMessagePollOptions(item.message.id, selectedOpaqueIdentifiers)
@ -1532,12 +1589,14 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode {
}
}
var previousOptionNodeLayouts: [Data: (_ contet: AccountContext, _ presentationData: ChatPresentationData, _ message: Message, _ poll: TelegramMediaPoll, _ option: TelegramMediaPollOption, _ translation: TranslationMessageAttribute.Additional?, _ optionResult: ChatMessagePollOptionResult?, _ hasAnyMedia: Bool, _ constrainedWidth: CGFloat) -> (minimumWidth: CGFloat, layout: ((CGFloat) -> (CGSize, (Bool, Bool, Bool) -> ChatMessagePollOptionNode)))] = [:]
var previousOptionNodeLayouts: [Data: (_ contet: AccountContext, _ presentationData: ChatPresentationData, _ presentationContext: ChatPresentationContext, _ message: Message, _ poll: TelegramMediaPoll, _ option: TelegramMediaPollOption, _ translation: TranslationMessageAttribute.Additional?, _ optionResult: ChatMessagePollOptionResult?, _ hasAnyMedia: Bool, _ constrainedWidth: CGFloat) -> (minimumWidth: CGFloat, layout: ((CGFloat) -> (CGSize, (Bool, Bool, Bool) -> ChatMessagePollOptionNode)))] = [:]
for optionNode in self.optionNodes {
if let option = optionNode.option {
previousOptionNodeLayouts[option.opaqueIdentifier] = ChatMessagePollOptionNode.asyncLayout(optionNode)
}
}
let isPreviewingResults = self.isPreviewingResults
return { item, layoutConstants, _, _, _, _ in
let contentProperties = ChatMessageBubbleContentProperties(hidesSimpleAuthorHeader: false, headerSpacing: 0.0, hidesBackground: .never, forceFullCorners: false, forceAlignment: .none)
@ -1656,7 +1715,6 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode {
let messageTheme = incoming ? item.presentationData.theme.theme.chat.message.incoming : item.presentationData.theme.theme.chat.message.outgoing
var pollTitleText = poll?.text ?? ""
var pollTitleEntities = poll?.textEntities ?? []
var pollOptions: [TranslationMessageAttribute.Additional] = []
@ -1751,18 +1809,49 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode {
votersString = " "
}
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))
var hasVoted = false
if let poll, let voters = poll.results.voters {
for voter in voters {
if voter.selected {
hasVoted = true
break
}
}
}
let viewResultsString: String
if let poll = poll, let totalVoters = poll.results.totalVoters {
if let poll, let totalVoters = poll.results.totalVoters {
//TODO:localize
viewResultsString = "View Votes (\(totalVoters))"
if case .public = poll.publicity {
viewResultsString = "View Votes (\(totalVoters))"
} else {
if isPreviewingResults {
viewResultsString = "< Vote"
} else {
if totalVoters == 1 {
viewResultsString = "\(totalVoters) vote >"
} else {
viewResultsString = "\(totalVoters) votes >"
}
}
}
} else {
viewResultsString = item.presentationData.strings.MessagePoll_ViewResults
}
let viewResultsAttributedString = NSMutableAttributedString(string: viewResultsString, font: Font.regular(17.0), textColor: messageTheme.polls.bar)
if let range = viewResultsAttributedString.string.range(of: "<") {
let chevronImage = incoming ? PresentationResourcesChat.chatBubblePollChevronLeftIncomingIcon(item.presentationData.theme.theme) : PresentationResourcesChat.chatBubblePollChevronLeftOutgoingIcon(item.presentationData.theme.theme)
viewResultsAttributedString.addAttribute(.attachment, value: chevronImage!, range: NSRange(range, in: viewResultsAttributedString.string))
}
if let range = viewResultsAttributedString.string.range(of: ">") {
let chevronImage = incoming ? PresentationResourcesChat.chatBubblePollChevronRightIncomingIcon(item.presentationData.theme.theme) : PresentationResourcesChat.chatBubblePollChevronRightOutgoingIcon(item.presentationData.theme.theme)
viewResultsAttributedString.addAttribute(.attachment, value: chevronImage!, range: NSRange(range, in: viewResultsAttributedString.string))
}
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: viewResultsString, 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: viewResultsAttributedString, 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))
@ -1795,7 +1884,7 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode {
var orderedPollOptions: [(Int, TelegramMediaPollOption)] = []
var shuffledOptionOpaqueIdentifiers: [Data]?
if let poll = poll {
let resolvedOptionOrder = self.resolvedOptionOrder(for: poll)
let resolvedOptionOrder = resolvedOptionOrder(for: item)
orderedPollOptions = resolvedOptionOrder.orderedOptions
shuffledOptionOpaqueIdentifiers = resolvedOptionOrder.shuffledOpaqueIdentifiers
@ -1816,7 +1905,7 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode {
}
}
totalVoterCount = totalVoters
if didVote || isClosed {
if didVote || isClosed || isPreviewingResults {
for i in 0 ..< poll.options.count {
inner: for optionVoters in voters {
if optionVoters.opaqueIdentifier == poll.options[i].opaqueIdentifier {
@ -1840,8 +1929,7 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode {
let hasAnyOptionMedia = orderedPollOptions.contains(where: { $0.1.media != nil })
for (i, option) in orderedPollOptions {
let makeLayout: (_ context: AccountContext, _ presentationData: ChatPresentationData, _ message: Message, _ poll: TelegramMediaPoll, _ option: TelegramMediaPollOption, _ translation: TranslationMessageAttribute.Additional?, _ optionResult: ChatMessagePollOptionResult?, _ hasAnyMedia: Bool, _ constrainedWidth: CGFloat) -> (minimumWidth: CGFloat, layout: ((CGFloat) -> (CGSize, (Bool, Bool, Bool) -> ChatMessagePollOptionNode)))
let makeLayout: (_ context: AccountContext, _ presentationData: ChatPresentationData, _ presentationContext: ChatPresentationContext, _ message: Message, _ poll: TelegramMediaPoll, _ option: TelegramMediaPollOption, _ translation: TranslationMessageAttribute.Additional?, _ optionResult: ChatMessagePollOptionResult?, _ hasAnyMedia: Bool, _ constrainedWidth: CGFloat) -> (minimumWidth: CGFloat, layout: ((CGFloat) -> (CGSize, (Bool, Bool, Bool) -> ChatMessagePollOptionNode)))
if let previous = previousOptionNodeLayouts[option.opaqueIdentifier] {
makeLayout = previous
} else {
@ -1870,7 +1958,7 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode {
translation = pollOptions[i]
}
let result = makeLayout(item.context, item.presentationData, item.message, poll, option, translation, optionResult, hasAnyOptionMedia, constrainedSize.width - layoutConstants.bubble.borderInset * 2.0)
let result = makeLayout(item.context, item.presentationData, item.controllerInteraction.presentationContext, item.message, poll, option, translation, optionResult, hasAnyOptionMedia, constrainedSize.width - layoutConstants.bubble.borderInset * 2.0)
boundingSize.width = max(boundingSize.width, result.minimumWidth + layoutConstants.bubble.borderInset * 2.0)
pollOptionsFinalizeLayouts.append(result.1)
}
@ -1880,19 +1968,12 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode {
var canVote = false
if (item.message.id.namespace == Namespaces.Message.Cloud || Namespaces.Message.allNonRegular.contains(item.message.id.namespace)), let poll = poll, poll.pollId.namespace == Namespaces.Media.CloudPoll, !isClosed {
var hasVoted = false
if let voters = poll.results.voters {
for voter in voters {
if voter.selected {
hasVoted = true
break
}
}
}
if !hasVoted {
canVote = true
}
}
let _ = canVote
return (boundingSize.width, { boundingWidth in
var resultSize = CGSize(width: max(boundingSize.width, boundingWidth), height: boundingSize.height)
@ -1917,6 +1998,10 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode {
} else {
resultSize.height += 26.0
}
if let poll, case .poll = poll.kind, !poll.isClosed, let _ = poll.deadlineTimeout {
resultSize.height += 6.0
}
var statusSizeAndApply: (CGSize, (ListViewItemUpdateAnimation) -> Void)?
if let statusSuggestedWidthAndContinue = statusSuggestedWidthAndContinue {
@ -2005,7 +2090,7 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode {
verticalOffset += size.height
updatedOptionNodes.append(optionNode)
optionNode.isUserInteractionEnabled = canVote && item.controllerInteraction.pollActionState.pollMessageIdsInProgress[item.message.id] == nil
optionNode.isUserInteractionEnabled = item.controllerInteraction.pollActionState.pollMessageIdsInProgress[item.message.id] == nil
if i > 0 {
optionNode.previousOptionNode = updatedOptionNodes[i - 1]
@ -2028,34 +2113,33 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode {
animation.animator.updateFrame(layer: strongSelf.typeNode.layer, frame: typeFrame, completion: nil)
let deadlineTimeout = poll?.deadlineTimeout
var displayDeadline = true
var displayDeadlineTimer = true
var hasSelected = false
if let poll = poll {
if let poll {
if let voters = poll.results.voters {
for voter in voters {
if voter.selected {
displayDeadline = false
displayDeadlineTimer = false
hasSelected = true
break
}
}
}
}
if let deadlineTimeout = deadlineTimeout, !isClosed {
var endDate: Int32?
if message.id.namespace == Namespaces.Message.Cloud {
let startDate: Int32
if let forwardInfo = message.forwardInfo {
startDate = forwardInfo.date
} else {
startDate = message.timestamp
}
endDate = startDate + deadlineTimeout
var endDate: Int32?
if let deadlineTimeout, message.id.namespace == Namespaces.Message.Cloud {
let startDate: Int32
if let forwardInfo = message.forwardInfo {
startDate = forwardInfo.date
} else {
startDate = message.timestamp
}
endDate = startDate + deadlineTimeout
}
if let poll, case .quiz = poll.kind, let deadlineTimeout, !isClosed {
let timerNode: PollBubbleTimerNode
if let current = strongSelf.timerNode {
timerNode = current
@ -2065,7 +2149,7 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode {
} else {
timerTransition = .immediate
}
if displayDeadline {
if displayDeadlineTimer {
timerTransition.updateAlpha(node: timerNode, alpha: 1.0)
} else {
timerTransition.updateAlpha(node: timerNode, alpha: 0.0)
@ -2087,7 +2171,7 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode {
} else {
timerTransition = .immediate
}
if displayDeadline {
if displayDeadlineTimer {
timerNode.alpha = 0.0
timerTransition.updateAlpha(node: timerNode, alpha: 1.0)
} else {
@ -2110,12 +2194,64 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode {
})
timerTransition.updateTransformScale(node: timerNode, scale: 0.1)
}
var statusOffset: CGFloat = 0.0
if let poll, case .poll = poll.kind, let endDate, !isClosed {
let timerNode: DeadlineTimerNode
if let current = strongSelf.deadlineTimerNode {
timerNode = current
let timerTransition: ContainedViewLayoutTransition
if animation.isAnimated {
timerTransition = .animated(duration: 0.25, curve: .easeInOut)
} else {
timerTransition = .immediate
}
if displayDeadlineTimer {
timerTransition.updateAlpha(node: timerNode, alpha: 1.0)
} else {
timerTransition.updateAlpha(node: timerNode, alpha: 0.0)
}
} else {
timerNode = DeadlineTimerNode()
strongSelf.deadlineTimerNode = timerNode
strongSelf.addSubnode(timerNode)
let timerTransition: ContainedViewLayoutTransition
if animation.isAnimated {
timerTransition = .animated(duration: 0.25, curve: .easeInOut)
} else {
timerTransition = .immediate
}
if displayDeadlineTimer {
timerNode.alpha = 0.0
timerTransition.updateAlpha(node: timerNode, alpha: 1.0)
} else {
timerNode.alpha = 0.0
}
}
timerNode.update(size: resultSize, color: messageTheme.secondaryTextColor, deadlineTimeout: endDate, resultsHidden: poll.hideResultsUntilClose)
timerNode.frame = CGRect(origin: CGPoint(x: 0.0, y: verticalOffset + 30.0), size: CGSize(width: resultSize.width, height: 20.0))
statusOffset += 6.0
} else if let timerNode = strongSelf.deadlineTimerNode {
strongSelf.deadlineTimerNode = nil
let timerTransition: ContainedViewLayoutTransition
if animation.isAnimated {
timerTransition = .animated(duration: 0.25, curve: .easeInOut)
} else {
timerTransition = .immediate
}
timerTransition.updateAlpha(node: timerNode, alpha: 0.0, completion: { [weak timerNode] _ in
timerNode?.removeFromSupernode()
})
timerTransition.updateTransformScale(node: timerNode, scale: 0.1)
}
let solutionButtonSize = CGSize(width: 32.0, height: 32.0)
let solutionButtonFrame = CGRect(origin: CGPoint(x: resultSize.width - layoutConstants.text.bubbleInsets.right - solutionButtonSize.width + 5.0, y: typeFrame.minY - 16.0), size: solutionButtonSize)
strongSelf.solutionButtonNode.frame = solutionButtonFrame
if (strongSelf.timerNode == nil || !displayDeadline), let poll = poll, case .quiz = poll.kind, let _ = poll.results.solution, (isClosed || hasSelected) {
if (strongSelf.timerNode == nil || !displayDeadlineTimer), let poll = poll, case .quiz = poll.kind, let _ = poll.results.solution, (isClosed || hasSelected) {
if strongSelf.solutionButtonNode.alpha.isZero {
let timerTransition: ContainedViewLayoutTransition
if animation.isAnimated {
@ -2160,7 +2296,7 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode {
}
if let statusSizeAndApply = statusSizeAndApply {
let statusFrame = CGRect(origin: CGPoint(x: resultSize.width - statusSizeAndApply.0.width - layoutConstants.text.bubbleInsets.right, y: votersFrame.maxY), size: statusSizeAndApply.0)
let statusFrame = CGRect(origin: CGPoint(x: resultSize.width - statusSizeAndApply.0.width - layoutConstants.text.bubbleInsets.right, y: votersFrame.maxY + statusOffset), size: statusSizeAndApply.0)
if strongSelf.statusNode.supernode == nil {
statusSizeAndApply.1(.None)
@ -2265,9 +2401,34 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode {
}
}
}
var hasVoted = false
if let voters = poll.results.voters {
for voter in voters {
if voter.selected {
hasVoted = true
break
}
}
}
let isClosed = isPollEffectivelyClosed(message: item.message, poll: poll)
var canAlwaysViewResults = false
if let peer = item.message.peers[item.message.id.peerId] {
if let group = peer as? TelegramGroup {
switch group.role {
case .creator, .admin:
canAlwaysViewResults = true
default:
break
}
} else if let channel = peer as? TelegramChannel, let _ = channel.adminRights {
canAlwaysViewResults = true
}
}
var hasAnyVotes = false
var hasResults = false
if isClosed {
hasResults = true
@ -2277,6 +2438,7 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode {
}
} else {
if let totalVoters = poll.results.totalVoters, totalVoters != 0 {
hasAnyVotes = true
if let voters = poll.results.voters {
for voter in voters {
if voter.selected {
@ -2288,7 +2450,7 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode {
}
}
if !disableAllActions && hasSelection && !hasResults && poll.pollId.namespace == Namespaces.Media.CloudPoll {
if !disableAllActions && hasSelection && !hasResults && (!canAlwaysViewResults || hasSelectedOptions) && poll.pollId.namespace == Namespaces.Media.CloudPoll {
self.votersNode.isHidden = true
self.buttonViewResultsTextNode.isHidden = true
self.buttonSubmitInactiveTextNode.isHidden = hasSelectedOptions
@ -2296,7 +2458,15 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode {
self.buttonNode.isHidden = !hasSelectedOptions
self.buttonNode.isUserInteractionEnabled = true
} else {
if case .public = poll.publicity, hasResults, !disableAllActions {
let shouldShowViewResultsButton: Bool
switch poll.publicity {
case .public:
shouldShowViewResultsButton = hasResults || (canAlwaysViewResults && hasAnyVotes)
case .anonymous:
shouldShowViewResultsButton = canAlwaysViewResults && hasAnyVotes && !hasVoted
}
if shouldShowViewResultsButton, !disableAllActions {
self.votersNode.isHidden = true
if isBotChat {
@ -2375,25 +2545,10 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode {
}
if optionNode.isUserInteractionEnabled {
return ChatMessageBubbleContentTapAction(content: .ignore)
} else if let result = optionNode.currentResult, let item = self.item, !Namespaces.Message.allNonRegular.contains(item.message.id.namespace), let poll = self.poll, let option = optionNode.option, !isBotChat {
} else if let item = self.item, !Namespaces.Message.allNonRegular.contains(item.message.id.namespace), let poll = self.poll, let option = optionNode.option, !isBotChat {
switch poll.publicity {
case .anonymous:
let string: String
switch poll.kind {
case .poll:
if result.count == 0 {
string = item.presentationData.strings.MessagePoll_NoVotes
} else {
string = item.presentationData.strings.MessagePoll_VotedCount(result.count)
}
case .quiz:
if result.count == 0 {
string = item.presentationData.strings.MessagePoll_QuizNoUsers
} else {
string = item.presentationData.strings.MessagePoll_QuizCount(result.count)
}
}
return ChatMessageBubbleContentTapAction(content: .tooltip(string, optionNode, optionNode.bounds.offsetBy(dx: 0.0, dy: 10.0)))
return ChatMessageBubbleContentTapAction(content: .none)
case .public:
var hasNonZeroVoters = false
if let voters = poll.results.voters {
@ -2574,3 +2729,105 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode {
return nil
}
}
private struct ArbitraryRandomNumberGenerator : RandomNumberGenerator {
init(seed: Int) { srand48(seed) }
func next() -> UInt64 { return UInt64(drand48() * Double(UInt64.max)) }
}
private func stringForRemainingTime(_ duration: Int32) -> String {
let hours = duration / 3600
let minutes = duration / 60 % 60
let seconds = duration % 60
let durationString: String
if hours > 0 {
durationString = String(format: "%d:%02d:%02d", hours, minutes, seconds)
} else {
durationString = String(format: "%02d:%02d", minutes, seconds)
}
return durationString
}
private class DeadlineTimerNode: ASDisplayNode {
private let textNode: ImmediateTextNode
private var timer: SwiftSignalKit.Timer?
private var params: (size: CGSize, color: UIColor, deadlineTimeout: Int32, resultsHidden: Bool)?
override init() {
self.textNode = ImmediateTextNode()
self.textNode.textAlignment = .center
super.init()
self.addSubnode(self.textNode)
}
deinit {
self.timer?.invalidate()
}
func update(size: CGSize, color: UIColor, deadlineTimeout: Int32, resultsHidden: Bool) {
self.params = (size, color, deadlineTimeout, resultsHidden)
let prefix: String = resultsHidden ? "results in" : "ends in"
let currentTime = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970)
let duration = max(0, deadlineTimeout - currentTime)
if duration < 60 * 60 * 24 {
if self.timer == nil {
self.timer = SwiftSignalKit.Timer(timeout: 1.0, repeat: true, completion: { [weak self] in
guard let self, let params = self.params else {
return
}
self.update(size: params.size, color: params.color, deadlineTimeout: params.deadlineTimeout, resultsHidden: params.resultsHidden)
}, queue: Queue.mainQueue())
self.timer?.start()
}
} else {
self.timer?.invalidate()
self.timer = nil
}
let text = prefix + " " + stringForRemainingTime(duration)
self.textNode.attributedText = NSAttributedString(string: text, font: Font.with(size: 11.0, traits: .monospacedNumbers), textColor: color)
let textSize = self.textNode.updateLayout(size)
self.textNode.frame = CGRect(origin: CGPoint(x: floor((size.width - textSize.width) / 2.0), y: 0.0), size: textSize)
}
}
private func resolvedOptionOrder(for item: ChatMessageBubbleContentItem) -> (orderedOptions: [(Int, TelegramMediaPollOption)], shuffledOpaqueIdentifiers: [Data]?) {
guard let poll = item.message.media.first(where: { $0 is TelegramMediaPoll }) as? TelegramMediaPoll else {
return ([], nil)
}
let defaultOrderedOptions = Array(poll.options.enumerated()).map { ($0.offset, $0.element) }
guard poll.shuffleAnswers else {
return (defaultOrderedOptions, nil)
}
let currentOpaqueIdentifiers = poll.options.map(\.opaqueIdentifier)
let messageSeed = UInt64(bitPattern: Int64(item.message.stableId))
let peerSeed = UInt64(bitPattern: item.context.account.peerId.toInt64())
let seed = Int(truncatingIfNeeded: (messageSeed &* 6364136223846793005) ^ peerSeed)
var resolvedOpaqueIdentifiers = currentOpaqueIdentifiers
var randomNumberGenerator = ArbitraryRandomNumberGenerator(seed: seed)
resolvedOpaqueIdentifiers.shuffle(using: &randomNumberGenerator)
let indicesByOpaqueIdentifier = Dictionary(uniqueKeysWithValues: poll.options.enumerated().map { ($0.element.opaqueIdentifier, $0.offset) })
let orderedOptions = resolvedOpaqueIdentifiers.compactMap { opaqueIdentifier -> (Int, TelegramMediaPollOption)? in
guard let index = indicesByOpaqueIdentifier[opaqueIdentifier] else {
return nil
}
return (index, poll.options[index])
}
if orderedOptions.count == poll.options.count {
return (orderedOptions, resolvedOpaqueIdentifiers)
} else {
return (defaultOrderedOptions, currentOpaqueIdentifiers)
}
}

View file

@ -0,0 +1,112 @@
import Foundation
import UIKit
import Postbox
import Display
import AsyncDisplayKit
import SwiftSignalKit
import TelegramCore
import ChatMessageBubbleContentNode
import ChatMessageItemCommon
import ChatMessageAttachedContentNode
public final class ChatMessageQuizAnswerBubbleContentNode: ChatMessageBubbleContentNode {
private let contentNode: ChatMessageAttachedContentNode
override public var visibility: ListViewItemNodeVisibility {
didSet {
self.contentNode.visibility = visibility
}
}
required public init() {
self.contentNode = ChatMessageAttachedContentNode()
super.init()
self.addSubnode(self.contentNode)
}
required public init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override public func asyncLayoutContent() -> (_ item: ChatMessageBubbleContentItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ preparePosition: ChatMessageBubblePreparePosition, _ messageSelection: Bool?, _ constrainedSize: CGSize, _ avatarInset: CGFloat) -> (ChatMessageBubbleContentProperties, CGSize?, CGFloat, (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool, ListViewItemApply?) -> Void))) {
let contentNodeLayout = self.contentNode.asyncLayout()
return { item, layoutConstants, preparePosition, _, constrainedSize, _ in
//TODO:localize
let title: String = "Explanation"
var text: String = ""
var entities: [MessageTextEntity] = []
var mediaAndFlags: ([Media], ChatMessageAttachedContentNodeMediaFlags)? = nil
if let poll = item.message.media.first(where: { $0 is TelegramMediaPoll }) as? TelegramMediaPoll, let solution = poll.results.solution {
text = solution.text
entities = solution.entities
mediaAndFlags = solution.media.flatMap { ([$0], []) }
}
let (initialWidth, continueLayout) = contentNodeLayout(item.presentationData, item.controllerInteraction.automaticMediaDownloadSettings, item.associatedData, item.attributes, item.context, item.controllerInteraction, item.message, true, .peer(id: item.message.id.peerId), title, nil, nil, text, entities, mediaAndFlags, nil, nil, nil, true, layoutConstants, preparePosition, constrainedSize, item.controllerInteraction.presentationContext.animationCache, item.controllerInteraction.presentationContext.animationRenderer)
let contentProperties = ChatMessageBubbleContentProperties(hidesSimpleAuthorHeader: false, headerSpacing: 8.0, hidesBackground: .never, forceFullCorners: false, forceAlignment: .none)
return (contentProperties, nil, initialWidth, { constrainedSize, position in
let (refinedWidth, finalizeLayout) = continueLayout(constrainedSize, position)
return (refinedWidth, { boundingWidth in
let (size, apply) = finalizeLayout(boundingWidth)
return (size, { [weak self] animation, synchronousLoads, applyInfo in
if let strongSelf = self {
strongSelf.item = item
apply(animation, synchronousLoads, applyInfo)
strongSelf.contentNode.frame = CGRect(origin: CGPoint(), size: size)
}
})
})
})
}
}
override public func animateInsertion(_ currentTimestamp: Double, duration: Double) {
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25)
}
override public func animateAdded(_ currentTimestamp: Double, duration: Double) {
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25)
}
override public func animateRemoved(_ currentTimestamp: Double, duration: Double) {
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false)
}
override public func animateInsertionIntoBubble(_ duration: Double) {
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25)
}
override public func tapActionAtPoint(_ point: CGPoint, gesture: TapLongTapOrDoubleTapGesture, isEstimating: Bool) -> ChatMessageBubbleContentTapAction {
if self.bounds.contains(point) {
let contentNodeFrame = self.contentNode.frame
return self.contentNode.tapActionAtPoint(point.offsetBy(dx: -contentNodeFrame.minX, dy: -contentNodeFrame.minY), gesture: gesture, isEstimating: isEstimating)
}
return ChatMessageBubbleContentTapAction(content: .none)
}
override public func updateTouchesAtPoint(_ point: CGPoint?) {
let contentNodeFrame = self.contentNode.frame
self.contentNode.updateTouchesAtPoint(point.flatMap { $0.offsetBy(dx: -contentNodeFrame.minX, dy: -contentNodeFrame.minY) })
}
override public func updateHiddenMedia(_ media: [Media]?) -> Bool {
return self.contentNode.updateHiddenMedia(media)
}
override public func transitionNode(messageId: MessageId, media: Media, adjustRect: Bool) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? {
if self.item?.message.id != messageId {
return nil
}
return self.contentNode.transitionNode(media: media)
}
}

View file

@ -449,7 +449,6 @@ public class ChatMessageReplyInfoNode: ASDisplayNode {
if case let .pollOption(optionId) = arguments.innerSubject, let poll = arguments.message?.media.first(where: { $0 is TelegramMediaPoll }) as? TelegramMediaPoll, let pollOption = poll.options.first(where: { $0.opaqueIdentifier == optionId }) {
messageText = stringWithAppliedEntities(pollOption.text, entities: pollOption.entities, baseColor: textColor, linkColor: textColor, baseFont: textFont, linkFont: textFont, boldFont: textFont, italicFont: textFont, boldItalicFont: textFont, fixedFont: textFont, blockQuoteFont: textFont, underlineLinks: false, message: nil)
textLeftInset += 16.0
} else if case let .todoItem(todoItemId) = arguments.innerSubject, let todo = arguments.message?.media.first(where: { $0 is TelegramMediaTodo }) as? TelegramMediaTodo, let todoItem = todo.items.first(where: { $0.id == todoItemId }) {
messageText = stringWithAppliedEntities(todoItem.text, entities: todoItem.entities, baseColor: textColor, linkColor: textColor, baseFont: textFont, linkFont: textFont, boldFont: textFont, italicFont: textFont, boldItalicFont: textFont, fixedFont: textFont, blockQuoteFont: textFont, underlineLinks: false, message: nil)
textLeftInset += 16.0
@ -520,7 +519,25 @@ public class ChatMessageReplyInfoNode: ASDisplayNode {
var updatedMediaReference: AnyMediaReference?
var imageDimensions: CGSize?
var hasRoundImage = false
if let message = arguments.message, !message.containsSecretMedia {
if case let .pollOption(optionId) = arguments.innerSubject, let poll = arguments.message?.media.first(where: { $0 is TelegramMediaPoll }) as? TelegramMediaPoll, let pollOption = poll.options.first(where: { $0.opaqueIdentifier == optionId }), let media = pollOption.media {
if let image = media as? TelegramMediaImage {
updatedMediaReference = .message(message: MessageReference(arguments.parentMessage), media: image)
if let representation = largestRepresentationForPhoto(image) {
imageDimensions = representation.dimensions.cgSize
}
} else if let file = media as? TelegramMediaFile, file.isVideo && !file.isVideoSticker {
updatedMediaReference = .message(message: MessageReference(arguments.parentMessage), media: file)
if let dimensions = file.dimensions {
imageDimensions = dimensions.cgSize
} else if let representation = largestImageRepresentation(file.previewRepresentations), !file.isSticker {
imageDimensions = representation.dimensions.cgSize
}
if file.isInstantVideo {
hasRoundImage = true
}
}
} else if let message = arguments.message, !message.containsSecretMedia {
for media in message.media {
if let image = media as? TelegramMediaImage {
updatedMediaReference = .message(message: MessageReference(message), media: image)

View file

@ -124,7 +124,7 @@ public final class PollBubbleTimerNode: ASDisplayNode {
let isTimer = timeout <= Int(timerInterval)
let color = isProximity ? params.proximityColor : params.regularColor
self.textNode.attributedText = NSAttributedString(string: textForTimeout(value: timeout), font: Font.regular(14.0), textColor: color)
self.textNode.attributedText = NSAttributedString(string: textForTimeout(value: timeout), font: Font.with(size: 14.0, traits: .monospacedNumbers), textColor: color)
let textSize = textNode.updateLayout(CGSize(width: 100.0, height: 100.0))
self.textNode.frame = CGRect(origin: CGPoint(x: -22.0 - textSize.width, y: 0.0), size: textSize)

View file

@ -58,6 +58,7 @@ public final class ComposedPoll {
public let correctAnswers: [Data]?
public let results: TelegramMediaPollResults
public let deadlineTimeout: Int32?
public let deadlineDate: Int32?
public let usedCustomEmojiFiles: [Int64: TelegramMediaFile]
public init(
@ -74,6 +75,7 @@ public final class ComposedPoll {
correctAnswers: [Data]?,
results: TelegramMediaPollResults,
deadlineTimeout: Int32?,
deadlineDate: Int32?,
usedCustomEmojiFiles: [Int64: TelegramMediaFile]
) {
self.publicity = publicity
@ -89,6 +91,7 @@ public final class ComposedPoll {
self.correctAnswers = correctAnswers
self.results = results
self.deadlineTimeout = deadlineTimeout
self.deadlineDate = deadlineDate
self.usedCustomEmojiFiles = usedCustomEmojiFiles
}
}
@ -537,12 +540,13 @@ final class ComposePollScreenComponent: Component {
let usedCustomEmojiFiles: [Int64: TelegramMediaFile] = [:]
var deadlineTimeout: Int32?
var deadlineDate: Int32?
if self.limitDuration {
switch self.timeLimit {
case let .duration(duration):
deadlineTimeout = Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970) + duration
deadlineTimeout = duration
case let .deadline(deadline):
deadlineTimeout = deadline
deadlineDate = deadline
}
}
@ -575,6 +579,7 @@ final class ComposePollScreenComponent: Component {
}
),
deadlineTimeout: deadlineTimeout,
deadlineDate: deadlineDate,
usedCustomEmojiFiles: usedCustomEmojiFiles
)
}
@ -1812,7 +1817,7 @@ final class ComposePollScreenComponent: Component {
leftIcon: .custom(AnyComponentWithIdentity(id: 0, component: AnyComponent(
Image(image: self.cachedAddIcon, size: CGSize(width: 30.0, height: 30.0))
)), false),
accessory: .toggle(ListActionItemComponent.Toggle(style: .regular, isOn: self.canAddOptions, action: { [weak self] _ in
accessory: .toggle(ListActionItemComponent.Toggle(style: .lock(isLocked: self.isQuiz), isOn: self.canAddOptions, action: { [weak self] _ in
guard let self else {
return
}
@ -1928,6 +1933,9 @@ final class ComposePollScreenComponent: Component {
return
}
self.isQuiz = !self.isQuiz
if self.isQuiz && self.canAddOptions {
self.canAddOptions = false
}
self.state?.updated(transition: .spring(duration: 0.4))
})),
action: nil

View file

@ -13,10 +13,10 @@ public final class ListActionItemComponent: Component {
case legacy
}
public enum ToggleStyle {
public enum ToggleStyle: Equatable {
case regular
case icons
case lock
case lock(isLocked: Bool)
}
public struct Toggle: Equatable {
@ -831,9 +831,12 @@ public final class ListActionItemComponent: Component {
let switchNode: IconSwitchNode
var switchTransition = transition
var updateSwitchTheme = themeUpdated
if case let .toggle(previousToggle) = previousComponent?.accessory, previousToggle.style != toggle.style {
updateSwitchTheme = true
}
if let current = self.iconSwitchNode {
switchNode = current
switchNode.updateIsLocked(toggle.style == .lock)
switchNode.updateIsLocked(toggle.style == .lock(isLocked: true))
if switchNode.isOn != toggle.isOn {
switchNode.setOn(toggle.isOn, animated: !transition.animation.isImmediate)
}
@ -841,7 +844,7 @@ public final class ListActionItemComponent: Component {
switchTransition = switchTransition.withAnimation(.none)
updateSwitchTheme = true
switchNode = IconSwitchNode()
switchNode.updateIsLocked(toggle.style == .lock)
switchNode.updateIsLocked(toggle.style == .lock(isLocked: true))
switchNode.setOn(toggle.isOn, animated: false)
self.iconSwitchNode = switchNode
self.button.addSubview(switchNode.view)
@ -863,8 +866,13 @@ public final class ListActionItemComponent: Component {
switchNode.frameColor = component.theme.list.itemSwitchColors.frameColor
switchNode.contentColor = component.theme.list.itemSwitchColors.contentColor
switchNode.handleColor = component.theme.list.itemSwitchColors.handleColor
switchNode.positiveContentColor = component.theme.list.itemSwitchColors.positiveColor
switchNode.negativeContentColor = component.theme.list.itemSwitchColors.negativeColor
if case .icons = toggle.style {
switchNode.positiveContentColor = component.theme.list.itemSwitchColors.positiveColor
switchNode.negativeContentColor = component.theme.list.itemSwitchColors.negativeColor
} else {
switchNode.positiveContentColor = .clear
switchNode.negativeContentColor = component.theme.list.itemSecondaryTextColor
}
}
var switchSize = CGSize(width: 51.0, height: 31.0)

View file

@ -468,6 +468,7 @@ public final class ListComposePollOptionComponent: Component {
private var imageNode: TransformImageNode?
private var statusNode: SemanticStatusNode?
private var animationLayer: InlineStickerItemLayer?
private var videoIconView: UIImageView?
private let imageButton = HighlightTrackingButton()
private var checkView: CheckView?
@ -907,9 +908,8 @@ public final class ListComposePollOptionComponent: Component {
transition: attachButtonTransition,
component: AnyComponent(PlainButtonComponent(
content: AnyComponent(BundleIconComponent(
name: "Chat/Input/Text/IconAttachment",
tintColor: component.theme.chat.inputPanel.inputControlColor.blitOver(component.theme.list.itemBlocksBackgroundColor, alpha: 1.0),
maxSize: CGSize(width: 25.0, height: 25.0)
name: "Item List/AttachIcon",
tintColor: component.theme.chat.inputPanel.inputControlColor.blitOver(component.theme.list.itemBlocksBackgroundColor, alpha: 1.0)
)),
effectAlignment: .center,
action: { [weak self] in
@ -1034,6 +1034,8 @@ public final class ListComposePollOptionComponent: Component {
self.appliedMedia = media
updateMedia = true
}
var isVideo = false
if let image = media.media as? TelegramMediaImage, let largest = largestImageRepresentation(image.representations), let photoReference = media.concrete(TelegramMediaImage.self) {
imageSize = largest.dimensions.cgSize.aspectFilled(imageNodeSize)
@ -1048,10 +1050,29 @@ public final class ListComposePollOptionComponent: Component {
if updateMedia {
imageNode.setSignal(instantPageImageFile(account: component.context.account, userLocation: .other, fileReference: fileReference, fetched: true))
}
} else {
} else if file.isVideo {
if updateMedia {
imageNode.setSignal(chatMessageVideo(postbox: component.context.account.postbox, userLocation: .other, videoReference: fileReference))
}
isVideo = true
} else {
let fileName: String = file.fileName ?? "File"
var fileExtension: String?
if let range = fileName.range(of: ".", options: [.backwards]) {
fileExtension = fileName[range.upperBound...].lowercased()
}
imageNode.setSignal(.single({ arguments in
let size = arguments.imageSize
let context = DrawingContext(size: size)!
context.withFlippedContext { context in
context.clear(CGRect(origin: .zero, size: size))
if let image = extensionImage(fileExtension: fileExtension), let cgImage = image.cgImage {
context.draw(cgImage, in: CGRect(origin: .zero, size: size))
}
}
return context
}))
}
} else if let map = media.media as? TelegramMediaMap {
imageSize = CGSize(width: 40.0, height: 40.0)
@ -1085,6 +1106,8 @@ public final class ListComposePollOptionComponent: Component {
let progressFrame = imageNodeFrame.insetBy(dx: 6.0, dy: 6.0)
statusNode.frame = progressFrame
statusNode.transitionToState(.progress(value: max(0.027, min(1.0, progress)), cancelEnabled: true, appearance: SemanticStatusNodeState.ProgressAppearance(inset: 1.0, lineWidth: 2.0), animateRotation: false), updateCutout: false)
isVideo = false
} else if let statusNode = self.statusNode {
self.statusNode = nil
if !transition.animation.isImmediate {
@ -1097,6 +1120,31 @@ public final class ListComposePollOptionComponent: Component {
statusNode.view.removeFromSuperview()
}
}
if isVideo {
let videoIconView: UIImageView
if let current = self.videoIconView {
videoIconView = current
} else {
videoIconView = UIImageView(image: UIImage(bundleImageName: "Media Gallery/PlayButton")?.withRenderingMode(.alwaysTemplate))
videoIconView.tintColor = .white
self.addSubview(videoIconView)
self.videoIconView = videoIconView
}
let videoIconFrame = CGRect(origin: CGPoint(x: imageNodeFrame.center.x - 15.0, y: imageNodeFrame.center.y - 15.0), size: CGSize(width: 30.0, height: 30.0))
videoIconView.frame = videoIconFrame
} else if let videoIconView = self.videoIconView {
self.videoIconView = nil
if !transition.animation.isImmediate {
let alphaTransition: ComponentTransition = .easeInOut(duration: 0.2)
alphaTransition.setAlpha(view: videoIconView, alpha: 0.0, completion: { [weak videoIconView] _ in
videoIconView?.removeFromSuperview()
})
alphaTransition.setScale(view: videoIconView, scale: 0.001)
} else {
videoIconView.removeFromSuperview()
}
}
} else if let imageNode = self.imageNode {
self.imageNode = nil
if !transition.animation.isImmediate {
@ -1387,3 +1435,89 @@ final class RevealOptionsGestureRecognizer: UIPanGestureRecognizer {
}
}
}
private let extensionFont = Font.with(size: 15.0, design: .round, weight: .bold)
private let mediumExtensionFont = Font.with(size: 14.0, design: .round, weight: .bold)
private let smallExtensionFont = Font.with(size: 12.0, design: .round, weight: .bold)
private let redColors: (UInt32, UInt32) = (0xff875f, 0xff5069)
private let greenColors: (UInt32, UInt32) = (0x99de6f, 0x5fb84f)
private let blueColors: (UInt32, UInt32) = (0x72d5fd, 0x2a9ef1)
private let yellowColors: (UInt32, UInt32) = (0xffa24b, 0xed705c)
private let extensionColorsMap: [String: (UInt32, UInt32)] = [
"ppt": redColors,
"pptx": redColors,
"pdf": redColors,
"key": redColors,
"xls": greenColors,
"xlsx": greenColors,
"csv": greenColors,
"zip": yellowColors,
"rar": yellowColors,
"gzip": yellowColors,
"ai": yellowColors
]
private func generateExtensionImage(colors: (UInt32, UInt32), fileExtension: String) -> UIImage? {
return generateImage(CGSize(width: 40.0, height: 40.0), rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
context.saveGState()
context.beginPath()
let _ = try? drawSvgPath(context, path: "M6,0 L26.7573593,0 C27.5530088,-8.52837125e-16 28.3160705,0.316070521 28.8786797,0.878679656 L39.1213203,11.1213203 C39.6839295,11.6839295 40,12.4469912 40,13.2426407 L40,34 C40,37.3137085 37.3137085,40 34,40 L6,40 C2.6862915,40 4.05812251e-16,37.3137085 0,34 L0,6 C-4.05812251e-16,2.6862915 2.6862915,6.08718376e-16 6,0 ")
context.clip()
let gradientColors = [UIColor(rgb: colors.0).cgColor, UIColor(rgb: colors.1).cgColor] as CFArray
var locations: [CGFloat] = [0.0, 1.0]
let colorSpace = CGColorSpaceCreateDeviceRGB()
let gradient = CGGradient(colorsSpace: colorSpace, colors: gradientColors, locations: &locations)!
context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: 0.0, y: size.height), options: CGGradientDrawingOptions())
context.restoreGState()
context.saveGState()
let rounded = UIBezierPath(roundedRect: CGRect(origin: .zero, size: size), cornerRadius: 13).cgPath
let full = UIBezierPath(rect: CGRect(origin: .zero, size: size)).cgPath
context.addPath(full)
context.addPath(rounded)
context.setBlendMode(.destinationOut)
context.drawPath(using: .eoFill)
context.setBlendMode(.normal)
context.restoreGState()
context.saveGState()
context.beginPath()
let _ = try? drawSvgPath(context, path: "M6,0 L26.7573593,0 C27.5530088,-8.52837125e-16 28.3160705,0.316070521 28.8786797,0.878679656 L39.1213203,11.1213203 C39.6839295,11.6839295 40,12.4469912 40,13.2426407 L40,34 C40,37.3137085 37.3137085,40 34,40 L6,40 C2.6862915,40 4.05812251e-16,37.3137085 0,34 L0,6 C-4.05812251e-16,2.6862915 2.6862915,6.08718376e-16 6,0 ")
context.clip()
context.setBlendMode(.overlay)
context.setFillColor(UIColor(rgb: 0xffffff, alpha: 0.5).cgColor)
context.translateBy(x: 40.0 - 14.0, y: 0.0)
let _ = try? drawSvgPath(context, path: "M-1,0 L14,0 L14,15 L14,14 C14,12.8954305 13.1045695,12 12,12 L4,12 C2.8954305,12 2,11.1045695 2,10 L2,2 C2,0.8954305 1.1045695,-2.02906125e-16 0,0 L-1,0 L-1,0 Z ")
context.restoreGState()
UIGraphicsPushContext(context)
let extensionText = NSAttributedString(string: fileExtension, font: fileExtension.count > 3 ? mediumExtensionFont : extensionFont, textColor: .white, paragraphAlignment: .center)
extensionText.draw(in: CGRect(origin: CGPoint(x: 0.0, y: 15.0), size: size))
UIGraphicsPopContext()
})
}
private func extensionImage(fileExtension: String?) -> UIImage? {
let colors: (UInt32, UInt32)
if let fileExtension = fileExtension {
if let extensionColors = extensionColorsMap[fileExtension] {
colors = extensionColors
} else {
colors = blueColors
}
} else {
colors = blueColors
}
return generateExtensionImage(colors: colors, fileExtension: fileExtension ?? "")
}

View file

@ -92,7 +92,6 @@ private final class SearchNavigationContentNode: ASDisplayNode, PeerInfoPanelNod
public final class PeerInfoChatPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScrollViewDelegate, ASGestureRecognizerDelegate {
private let context: AccountContext
private let peerId: EnginePeer.Id
private let navigationController: () -> NavigationController?
private let chatController: ChatController
@ -142,15 +141,14 @@ public final class PeerInfoChatPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScro
private var presentationData: PresentationData
private var presentationDataDisposable: Disposable?
public init(context: AccountContext, peerId: EnginePeer.Id, navigationController: @escaping () -> NavigationController?) {
public init(context: AccountContext, chatLocation: ChatLocation, tag: MessageTags?, navigationController: @escaping () -> NavigationController?) {
self.context = context
self.peerId = peerId
self.navigationController = navigationController
self.presentationData = context.sharedContext.currentPresentationData.with { $0 }
self.coveringView = UIView()
self.chatController = context.sharedContext.makeChatController(context: context, chatLocation: .replyThread(message: ChatReplyThreadMessage(peerId: context.account.peerId, threadId: peerId.toInt64(), channelMessageId: nil, isChannelPost: false, isForumPost: false, isMonoforumPost: false, maxMessage: nil, maxReadIncomingMessageId: nil, maxReadOutgoingMessageId: nil, unreadCount: 0, initialFilledHoles: IndexSet(), initialAnchor: .automatic, isNotAvailable: false)), subject: nil, botStart: nil, mode: .standard(.embedded(invertDirection: true)), params: nil)
self.chatController = context.sharedContext.makeChatController(context: context, chatLocation: chatLocation, subject: tag.flatMap { .tag($0) }, botStart: nil, mode: .standard(.embedded(invertDirection: true)), params: nil)
self.chatController.navigation_setNavigationController(navigationController())
super.init()
@ -165,13 +163,30 @@ public final class PeerInfoChatPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScro
self.presentationData = presentationData
})
var peerId: EnginePeer.Id
var threadId: Int64?
switch chatLocation {
case let .peer(peerIdValue):
peerId = peerIdValue
case let .replyThread(message):
peerId = self.context.account.peerId
threadId = message.threadId
default:
fatalError()
}
let strings = self.presentationData.strings
self.statusPromise.set(self.context.engine.data.subscribe(
TelegramEngine.EngineData.Item.Messages.MessageCount(peerId: self.context.account.peerId, threadId: peerId.toInt64(), tag: [])
TelegramEngine.EngineData.Item.Messages.MessageCount(peerId: peerId, threadId: threadId, tag: tag ?? [])
)
|> map { count in
if let count {
return PeerInfoStatusData(text: strings.Conversation_Messages(Int32(count)), isActivity: false, key: .savedMessages)
if tag == .polls {
//TODO:localize
return PeerInfoStatusData(text: "\(count) polls", isActivity: false, key: .polls)
} else {
return PeerInfoStatusData(text: strings.Conversation_Messages(Int32(count)), isActivity: false, key: .savedMessages)
}
} else {
return nil
}

View file

@ -19,6 +19,7 @@ public enum PeerInfoPaneKey: Int32 {
case voice
case links
case gifs
case polls
case groupsInCommon
case similarChannels
case similarBots

View file

@ -174,6 +174,9 @@ final class PeerInfoListPaneNode: ASDisplayNode, PeerInfoPaneNode {
return PeerInfoStatusData(text: presentationData.strings.SharedMedia_VoiceMessageCount(Int32(count)), isActivity: false, key: .voice)
case MessageTags.webPage:
return PeerInfoStatusData(text: presentationData.strings.SharedMedia_LinkCount(Int32(count)), isActivity: false, key: .links)
case MessageTags.polls:
//TODO:localize
return PeerInfoStatusData(text: "\(count) poll", isActivity: false, key: .links)
default:
return nil
}

View file

@ -599,7 +599,8 @@ private func peerInfoAvailableMediaPanes(context: AccountContext, peerId: PeerId
(.music, .music),
(.voiceOrInstantVideo, .voice),
(.webPage, .links),
(.gif, .gifs)
(.gif, .gifs),
(.polls, .polls)
]
}
enum PaneState {
@ -882,7 +883,6 @@ func peerInfoScreenSettingsData(context: AccountContext, peerId: EnginePeer.Id,
let botsKey = ValueBoxKey(length: 8)
botsKey.setInt64(0, value: 0)
//let iconLoaded = Atomic<[EnginePeer.Id: Bool]>(value: [:])
let bots = context.engine.data.subscribe(TelegramEngine.EngineData.Item.ItemCache.Item(collectionId: Namespaces.CachedItemCollection.attachMenuBots, id: botsKey))
|> mapToSignal { entry -> Signal<[AttachMenuBot], NoError> in
let bots: [AttachMenuBots.Bot] = entry?.get(AttachMenuBots.self)?.bots ?? []

View file

@ -560,7 +560,9 @@ private final class PeerInfoPendingPane {
case .savedMessagesChats:
paneNode = PeerInfoChatListPaneNode(context: context, navigationController: chatControllerInteraction.navigationController)
case .savedMessages:
paneNode = PeerInfoChatPaneNode(context: context, peerId: peerId, navigationController: chatControllerInteraction.navigationController)
paneNode = PeerInfoChatPaneNode(context: context, chatLocation: .replyThread(message: ChatReplyThreadMessage(peerId: context.account.peerId, threadId: peerId.toInt64(), channelMessageId: nil, isChannelPost: false, isForumPost: false, isMonoforumPost: false, maxMessage: nil, maxReadIncomingMessageId: nil, maxReadOutgoingMessageId: nil, unreadCount: 0, initialFilledHoles: IndexSet(), initialAnchor: .automatic, isNotAvailable: false)), tag: nil, navigationController: chatControllerInteraction.navigationController)
case .polls:
paneNode = PeerInfoChatPaneNode(context: context, chatLocation: .peer(id: peerId), tag: .polls, navigationController: chatControllerInteraction.navigationController)
}
paneNode.externalDataUpdated = externalDataUpdated
paneNode.parentController = parentController
@ -1312,6 +1314,9 @@ final class PeerInfoPaneContainerNode: ASDisplayNode, ASGestureRecognizerDelegat
GiftsTabItemComponent(context: self.context, icons: icons, title: presentationData.strings.PeerInfo_PaneGifts, theme: presentationData.theme)
))
canReorder = true
case .polls:
//TODO:localize
content = .title(HorizontalTabsComponent.Tab.Title(text: "Polls", entities: [], enableAnimations: false))
}
return HorizontalTabsComponent.Tab(

View file

@ -2489,7 +2489,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
}
})
self.refreshMessageTagStatsDisposable = context.engine.messages.refreshMessageTagStats(peerId: peerId, threadId: chatLocation.threadId, tags: [.video, .photo, .gif, .music, .voiceOrInstantVideo, .webPage, .file]).startStrict()
self.refreshMessageTagStatsDisposable = context.engine.messages.refreshMessageTagStats(peerId: peerId, threadId: chatLocation.threadId, tags: [.video, .photo, .gif, .music, .voiceOrInstantVideo, .webPage, .file, .polls]).startStrict()
if peerId.namespace == Namespaces.Peer.CloudChannel {
self.translationStateDisposable = (chatTranslationState(context: context, peerId: peerId, threadId: nil)

View file

@ -1,22 +1,12 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"filename" : "ic_mention2@2x.png",
"scale" : "2x"
},
{
"idiom" : "universal",
"filename" : "ic_mention2@3x.png",
"scale" : "3x"
"filename" : "mentionslist.pdf",
"idiom" : "universal"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
"author" : "xcode",
"version" : 1
}
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 819 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

View file

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

View file

@ -1,7 +1,7 @@
{
"images" : [
{
"filename" : "reactionchatsbadge.pdf",
"filename" : "reactionlist.pdf",
"idiom" : "universal"
}
],

View file

@ -1,75 +0,0 @@
%PDF-1.7
1 0 obj
<< >>
endobj
2 0 obj
<< /Length 3 0 R >>
stream
/DeviceRGB CS
/DeviceRGB cs
q
1.000000 0.000000 -0.000000 1.000000 4.570068 4.225903 cm
0.000000 0.000000 0.000000 scn
5.424805 0.000000 m
5.564453 0.000000 5.763184 0.102051 5.924316 0.204102 c
8.926758 2.137695 10.849609 4.404297 10.849609 6.703125 c
10.849609 8.674316 9.490723 10.049316 7.777344 10.049316 c
6.708496 10.049316 5.908203 9.458496 5.424805 8.572266 c
4.952148 9.453125 4.146484 10.049316 3.077637 10.049316 c
1.364258 10.049316 0.000000 8.674316 0.000000 6.703125 c
0.000000 4.404297 1.922852 2.137695 4.925293 0.204102 c
5.091797 0.102051 5.290527 0.000000 5.424805 0.000000 c
h
f
n
Q
endstream
endobj
3 0 obj
604
endobj
4 0 obj
<< /Annots []
/Type /Page
/MediaBox [ 0.000000 0.000000 20.000000 20.000000 ]
/Resources 1 0 R
/Contents 2 0 R
/Parent 5 0 R
>>
endobj
5 0 obj
<< /Kids [ 4 0 R ]
/Count 1
/Type /Pages
>>
endobj
6 0 obj
<< /Pages 5 0 R
/Type /Catalog
>>
endobj
xref
0 7
0000000000 65535 f
0000000010 00000 n
0000000034 00000 n
0000000694 00000 n
0000000716 00000 n
0000000889 00000 n
0000000963 00000 n
trailer
<< /ID [ (some) (id) ]
/Root 6 0 R
/Size 7
>>
startxref
1022
%%EOF

View file

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

View file

@ -7917,93 +7917,94 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
return
}
var found = false
self.forEachController({ controller in
if let controller = controller as? TooltipScreen {
if controller.text == .entities(text: solution.text, entities: solution.entities) {
found = true
controller.dismiss()
return false
}
}
return true
})
if found {
return
}
let tooltipScreen = TooltipScreen(context: self.context, account: self.context.account, sharedContext: self.context.sharedContext, text: .entities(text: solution.text, entities: solution.entities), icon: .animation(name: "anim_infotip", delay: 0.2, tintColor: nil), location: .top, shouldDismissOnTouch: { point, _ in
return .ignore
}, openActiveTextItem: { [weak self] item, action in
guard let strongSelf = self else {
return
}
switch item {
case let .url(url, concealed):
switch action {
case .tap:
strongSelf.openUrl(url, concealed: concealed)
case .longTap:
strongSelf.controllerInteraction?.longTap(.url(url), nil)
}
case let .mention(peerId, mention):
switch action {
case .tap:
let _ = (strongSelf.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId))
|> deliverOnMainQueue).startStandalone(next: { peer in
if let strongSelf = self, let peer = peer {
strongSelf.controllerInteraction?.openPeer(peer, .default, nil, .default)
}
})
case .longTap:
strongSelf.controllerInteraction?.longTap(.peerMention(peerId, mention), nil)
}
case let .textMention(mention):
switch action {
case .tap:
strongSelf.controllerInteraction?.openPeerMention(mention, nil)
case .longTap:
strongSelf.controllerInteraction?.longTap(.mention(mention), nil)
}
case let .botCommand(command):
switch action {
case .tap:
strongSelf.controllerInteraction?.sendBotCommand(nil, command)
case .longTap:
strongSelf.controllerInteraction?.longTap(.command(command), nil)
}
case let .hashtag(hashtag):
switch action {
case .tap:
strongSelf.controllerInteraction?.openHashtag(nil, hashtag)
case .longTap:
strongSelf.controllerInteraction?.longTap(.hashtag(hashtag), nil)
}
}
})
// var found = false
// self.forEachController({ controller in
// if let controller = controller as? TooltipScreen {
// if controller.text == .entities(text: solution.text, entities: solution.entities) {
// found = true
// controller.dismiss()
// return false
// }
// }
// return true
// })
// if found {
// return
// }
// let tooltipScreen = TooltipScreen(context: self.context, account: self.context.account, sharedContext: self.context.sharedContext, text: .entities(text: solution.text, entities: solution.entities), icon: .animation(name: "anim_infotip", delay: 0.2, tintColor: nil), location: .top, shouldDismissOnTouch: { point, _ in
// return .ignore
// }, openActiveTextItem: { [weak self] item, action in
// guard let strongSelf = self else {
// return
// }
// switch item {
// case let .url(url, concealed):
// switch action {
// case .tap:
// strongSelf.openUrl(url, concealed: concealed)
// case .longTap:
// strongSelf.controllerInteraction?.longTap(.url(url), nil)
// }
// case let .mention(peerId, mention):
// switch action {
// case .tap:
// let _ = (strongSelf.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId))
// |> deliverOnMainQueue).startStandalone(next: { peer in
// if let strongSelf = self, let peer = peer {
// strongSelf.controllerInteraction?.openPeer(peer, .default, nil, .default)
// }
// })
// case .longTap:
// strongSelf.controllerInteraction?.longTap(.peerMention(peerId, mention), nil)
// }
// case let .textMention(mention):
// switch action {
// case .tap:
// strongSelf.controllerInteraction?.openPeerMention(mention, nil)
// case .longTap:
// strongSelf.controllerInteraction?.longTap(.mention(mention), nil)
// }
// case let .botCommand(command):
// switch action {
// case .tap:
// strongSelf.controllerInteraction?.sendBotCommand(nil, command)
// case .longTap:
// strongSelf.controllerInteraction?.longTap(.command(command), nil)
// }
// case let .hashtag(hashtag):
// switch action {
// case .tap:
// strongSelf.controllerInteraction?.openHashtag(nil, hashtag)
// case .longTap:
// strongSelf.controllerInteraction?.longTap(.hashtag(hashtag), nil)
// }
// }
// })
//
let messageId = item.message.id
self.controllerInteraction?.currentPollMessageWithTooltip = messageId
self.updatePollTooltipMessageState(animated: !isAutomatic)
self.controllerInteraction?.requestMessageUpdate(messageId, false)
//self.updatePollTooltipMessageState(animated: !isAutomatic)
tooltipScreen.willBecomeDismissed = { [weak self] tooltipScreen in
guard let strongSelf = self else {
return
}
if strongSelf.controllerInteraction?.currentPollMessageWithTooltip == messageId {
strongSelf.controllerInteraction?.currentPollMessageWithTooltip = nil
strongSelf.updatePollTooltipMessageState(animated: true)
}
}
self.forEachController({ controller in
if let controller = controller as? TooltipScreen {
controller.dismiss()
}
return true
})
self.present(tooltipScreen, in: .current)
// tooltipScreen.willBecomeDismissed = { [weak self] tooltipScreen in
// guard let strongSelf = self else {
// return
// }
// if strongSelf.controllerInteraction?.currentPollMessageWithTooltip == messageId {
// strongSelf.controllerInteraction?.currentPollMessageWithTooltip = nil
// strongSelf.updatePollTooltipMessageState(animated: true)
// }
// }
//
// self.forEachController({ controller in
// if let controller = controller as? TooltipScreen {
// controller.dismiss()
// }
// return true
// })
//
// self.present(tooltipScreen, in: .current)
}
public func displayPromoAnnouncement(text: String) {

View file

@ -548,7 +548,7 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate {
}
if let poll = media as? TelegramMediaPoll {
var updatedMedia = message.media.filter { !($0 is TelegramMediaPoll) }
updatedMedia.append(TelegramMediaPoll(pollId: poll.pollId, publicity: poll.publicity, kind: poll.kind, text: poll.text, textEntities: poll.textEntities, options: poll.options, correctAnswers: poll.correctAnswers, results: TelegramMediaPollResults(voters: nil, totalVoters: nil, recentVoters: [], solution: nil), isClosed: false, deadlineTimeout: nil))
updatedMedia.append(TelegramMediaPoll(pollId: poll.pollId, publicity: poll.publicity, kind: poll.kind, text: poll.text, textEntities: poll.textEntities, options: poll.options, correctAnswers: poll.correctAnswers, results: TelegramMediaPollResults(voters: nil, totalVoters: nil, recentVoters: [], solution: nil), isClosed: false, deadlineTimeout: nil, deadlineDate: nil))
messageMedia = updatedMedia
}
if let _ = media as? TelegramMediaDice {
@ -716,8 +716,11 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate {
self.controllerInteraction.chatIsRotated = historyNodeRotated
var displayAdPeer: PeerId?
var tag: MessageTags?
if !isChatPreview {
switch subject {
case let .tag(tagValue):
tag = tagValue
case .none, .message:
if case let .peer(peerId) = chatLocation {
displayAdPeer = peerId
@ -733,7 +736,7 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate {
}
var getMessageTransitionNode: (() -> ChatMessageTransitionNodeImpl?)?
self.historyNode = ChatHistoryListNodeImpl(context: context, updatedPresentationData: controller?.updatedPresentationData ?? (context.sharedContext.currentPresentationData.with({ $0 }), context.sharedContext.presentationData), chatLocation: chatLocation, chatLocationContextHolder: chatLocationContextHolder, adMessagesContext: self.adMessagesContext, tag: nil, source: source, subject: subject, controllerInteraction: controllerInteraction, selectedMessages: self.selectedMessagesPromise.get(), rotated: historyNodeRotated, isChatPreview: isChatPreview, messageTransitionNode: {
self.historyNode = ChatHistoryListNodeImpl(context: context, updatedPresentationData: controller?.updatedPresentationData ?? (context.sharedContext.currentPresentationData.with({ $0 }), context.sharedContext.presentationData), chatLocation: chatLocation, chatLocationContextHolder: chatLocationContextHolder, adMessagesContext: self.adMessagesContext, tag: tag.flatMap { .tag($0) }, source: source, subject: subject, controllerInteraction: controllerInteraction, selectedMessages: self.selectedMessagesPromise.get(), rotated: historyNodeRotated, isChatPreview: isChatPreview, messageTransitionNode: {
return getMessageTransitionNode?()
})

View file

@ -2050,6 +2050,7 @@ extension ChatControllerImpl {
results: poll.results,
isClosed: false,
deadlineTimeout: poll.deadlineTimeout,
deadlineDate: poll.deadlineDate,
openAnswers: poll.openAnswers,
revotingDisabled: poll.revotingDisabled,
shuffleAnswers: poll.shuffleAnswers,