mirror of
https://github.com/TelegramMessenger/Telegram-iOS.git
synced 2026-07-06 03:33:41 +02:00
[WIP] Polls
This commit is contained in:
parent
1a08c3f0d0
commit
6519b5c84d
43 changed files with 960 additions and 330 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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];
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 ?? "")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ public enum PeerInfoPaneKey: Int32 {
|
|||
case voice
|
||||
case links
|
||||
case gifs
|
||||
case polls
|
||||
case groupsInCommon
|
||||
case similarChannels
|
||||
case similarBots
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 ?? []
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 |
BIN
submodules/TelegramUI/Images.xcassets/Chat List/MentionBadgeIcon.imageset/mentionslist.pdf
vendored
Normal file
BIN
submodules/TelegramUI/Images.xcassets/Chat List/MentionBadgeIcon.imageset/mentionslist.pdf
vendored
Normal file
Binary file not shown.
12
submodules/TelegramUI/Images.xcassets/Chat List/PollBadgeIcon.imageset/Contents.json
vendored
Normal file
12
submodules/TelegramUI/Images.xcassets/Chat List/PollBadgeIcon.imageset/Contents.json
vendored
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "polllist.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
BIN
submodules/TelegramUI/Images.xcassets/Chat List/PollBadgeIcon.imageset/polllist.pdf
vendored
Normal file
BIN
submodules/TelegramUI/Images.xcassets/Chat List/PollBadgeIcon.imageset/polllist.pdf
vendored
Normal file
Binary file not shown.
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "reactionchatsbadge.pdf",
|
||||
"filename" : "reactionlist.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
|
|
|
|||
|
|
@ -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
|
||||
BIN
submodules/TelegramUI/Images.xcassets/Chat List/ReactionsBadgeIcon.imageset/reactionlist.pdf
vendored
Normal file
BIN
submodules/TelegramUI/Images.xcassets/Chat List/ReactionsBadgeIcon.imageset/reactionlist.pdf
vendored
Normal file
Binary file not shown.
12
submodules/TelegramUI/Images.xcassets/Item List/AttachIcon.imageset/Contents.json
vendored
Normal file
12
submodules/TelegramUI/Images.xcassets/Item List/AttachIcon.imageset/Contents.json
vendored
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "attach.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
BIN
submodules/TelegramUI/Images.xcassets/Item List/AttachIcon.imageset/attach.pdf
vendored
Normal file
BIN
submodules/TelegramUI/Images.xcassets/Item List/AttachIcon.imageset/attach.pdf
vendored
Normal file
Binary file not shown.
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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?()
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue