Poll improvements

This commit is contained in:
Ali 2020-01-17 12:39:58 +04:00
parent 7ab49435b7
commit 479689bc1a
20 changed files with 2600 additions and 2442 deletions

View file

@ -32,6 +32,7 @@
<string>merchant.sberbank.test.ph.telegra.Telegraph</string>
<string>merchant.privatbank.test.telergramios</string>
<string>merchant.privatbank.prod.telergram</string>
<string>merchant.telegram.tranzzo.test</string>
</array>
<key>com.apple.developer.pushkit.unrestricted-voip</key>
<true/>

View file

@ -5246,6 +5246,8 @@ Any member of this group will be able to see messages in the channel.";
"Map.NoPlacesNearby" = "There are no known places nearby.\nTry a different location.";
"CreatePoll.QuizTitle" = "New Quiz";
"CreatePoll.QuizOptionsHeader" = "QUIZ OPTIONS";
"CreatePoll.Anonymous" = "Anonymous Voting";
"CreatePoll.MultipleChoice" = "Multiple Choice";
"CreatePoll.MultipleChoiceQuizAlert" = "A quiz has one correct answer.";

View file

@ -382,7 +382,13 @@ private func createPollControllerEntries(presentationData: PresentationData, pee
}
entries.append(.textHeader(presentationData.strings.CreatePoll_TextHeader, textLimitText))
entries.append(.text(presentationData.strings.CreatePoll_TextPlaceholder, state.text, Int(limitsConfiguration.maxMediaCaptionLength)))
entries.append(.optionsHeader(presentationData.strings.CreatePoll_OptionsHeader))
let optionsHeaderTitle: String
if let defaultIsQuiz = defaultIsQuiz, defaultIsQuiz {
optionsHeaderTitle = presentationData.strings.CreatePoll_QuizOptionsHeader
} else {
optionsHeaderTitle = presentationData.strings.CreatePoll_OptionsHeader
}
entries.append(.optionsHeader(optionsHeaderTitle))
for i in 0 ..< state.options.count {
let isSecondLast = state.options.count == 2 && i == 0
let isLast = i == state.options.count - 1
@ -748,7 +754,14 @@ public func createPollController(context: AccountContext, peer: Peer, isQuiz: Bo
ensureVisibleItemTag = focusItemTag
}
let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(presentationData.strings.CreatePoll_Title), leftNavigationButton: leftNavigationButton, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: false)
let title: String
if let isQuiz = isQuiz, isQuiz {
title = presentationData.strings.CreatePoll_QuizTitle
} else {
title = presentationData.strings.CreatePoll_Title
}
let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(title), leftNavigationButton: leftNavigationButton, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: false)
let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: createPollControllerEntries(presentationData: presentationData, peer: peer, state: state, limitsConfiguration: limitsConfiguration, defaultIsQuiz: isQuiz), style: .blocks, focusItemTag: focusItemTag, ensureVisibleItemTag: ensureVisibleItemTag, animateChanges: previousIds != nil)
return (controllerState, (listState, arguments))

View file

@ -182,6 +182,7 @@ class CreatePollOptionItemNode: ItemListRevealOptionsItemNode, ItemListItemNode,
}
func editableTextNodeDidFinishEditing(_ editableTextNode: ASEditableTextNode) {
self.editableTextNodeDidUpdateText(editableTextNode)
self.item?.focused(false)
}

View file

@ -18,7 +18,7 @@
- (void)appendVideoPixelBuffer:(CVPixelBufferRef)pixelBuffer withPresentationTime:(CMTime)presentationTime;
- (void)appendAudioSampleBuffer:(CMSampleBufferRef)sampleBuffer;
- (void)finishRecording;
- (void)finishRecording:(void(^)())completed;
- (NSTimeInterval)videoDuration;

View file

@ -105,7 +105,7 @@ typedef enum {
if (_status != TGMovieRecorderStatusIdle)
return;
[self transitionToStatus:TGMovieRecorderStatusPreparingToRecord error:nil];
[self transitionToStatus:TGMovieRecorderStatusPreparingToRecord error:nil completed:nil];
}
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), ^
@ -138,9 +138,9 @@ typedef enum {
@synchronized (self)
{
if (error || !succeed)
[self transitionToStatus:TGMovieRecorderStatusFailed error:error];
[self transitionToStatus:TGMovieRecorderStatusFailed error:error completed:nil];
else
[self transitionToStatus:TGMovieRecorderStatusRecording error:nil];
[self transitionToStatus:TGMovieRecorderStatusRecording error:nil completed:nil];
}
}
} );
@ -169,8 +169,9 @@ typedef enum {
[self appendSampleBuffer:sampleBuffer ofMediaType:AVMediaTypeAudio];
}
- (void)finishRecording
- (void)finishRecording:(void(^)())completed
{
printf("finishRecording %d\n", _status);
@synchronized (self)
{
bool shouldFinishRecording = false;
@ -190,9 +191,10 @@ typedef enum {
}
if (shouldFinishRecording)
[self transitionToStatus:TGMovieRecorderStatusFinishingWaiting error:nil];
else
[self transitionToStatus:TGMovieRecorderStatusFinishingWaiting error:nil completed:completed];
else {
return;
}
}
dispatch_async(_writingQueue, ^
@ -201,10 +203,14 @@ typedef enum {
{
@synchronized (self)
{
if (_status != TGMovieRecorderStatusFinishingWaiting)
if (_status != TGMovieRecorderStatusFinishingWaiting) {
if (completed) {
completed();
}
return;
}
[self transitionToStatus:TGMovieRecorderStatusFinishingCommiting error:nil];
[self transitionToStatus:TGMovieRecorderStatusFinishingCommiting error:nil completed:nil];
}
[_assetWriter finishWritingWithCompletionHandler:^
@ -213,9 +219,9 @@ typedef enum {
{
NSError *error = _assetWriter.error;
if (error)
[self transitionToStatus:TGMovieRecorderStatusFailed error:error];
[self transitionToStatus:TGMovieRecorderStatusFailed error:error completed:completed];
else
[self transitionToStatus:TGMovieRecorderStatusFinished error:nil];
[self transitionToStatus:TGMovieRecorderStatusFinished error:nil completed:completed];
}
}];
}
@ -340,7 +346,7 @@ typedef enum {
NSError *error = _assetWriter.error;
@synchronized (self)
{
[self transitionToStatus:TGMovieRecorderStatusFailed error:error];
[self transitionToStatus:TGMovieRecorderStatusFailed error:error completed:nil];
}
}
}
@ -349,8 +355,10 @@ typedef enum {
});
}
- (void)transitionToStatus:(TGMovieRecorderStatus)newStatus error:(NSError *)error
- (void)transitionToStatus:(TGMovieRecorderStatus)newStatus error:(NSError *)error completed:(void(^)())completed
{
printf("recorder transitionToStatus %d\n", newStatus);
bool shouldNotifyDelegate = false;
if (newStatus != _status)
@ -389,6 +397,7 @@ typedef enum {
break;
case TGMovieRecorderStatusFinished:
printf("TGMovieRecorderStatusFinished _delegate == nil = %d\n", (int)(_delegate == nil));
[_delegate movieRecorderDidFinishRecording:self];
break;
@ -399,9 +408,16 @@ typedef enum {
default:
break;
}
if (completed) {
completed();
}
}
});
}
} else {
if (completed) {
completed();
}
}
}
- (bool)setupAssetWriterAudioInputWithSourceFormatDescription:(CMFormatDescriptionRef)audioFormatDescription settings:(NSDictionary *)audioSettings

View file

@ -21,7 +21,7 @@
- (void)stopRunning;
- (void)startRecording:(NSURL *)url preset:(TGMediaVideoConversionPreset)preset liveUpload:(bool)liveUpload;
- (void)stopRecording;
- (void)stopRecording:(void (^)())completed;
- (CGAffineTransform)transformForOrientation:(AVCaptureVideoOrientation)orientation;

View file

@ -111,6 +111,7 @@ const NSInteger TGVideoCameraRetainedBufferCount = 16;
- (void)dealloc
{
printf("Camera pipeline dealloc\n");
[self destroyCaptureSession];
}
@ -134,7 +135,7 @@ const NSInteger TGVideoCameraRetainedBufferCount = 16;
{
_running = false;
[self stopRecording];
[self stopRecording:^{}];
[_captureSession stopRunning];
[self captureSessionDidStopRunning];
@ -285,7 +286,7 @@ const NSInteger TGVideoCameraRetainedBufferCount = 16;
- (void)captureSessionDidStopRunning
{
[self stopRecording];
[self stopRecording:^{}];
[self destroyVideoPipeline];
}
@ -684,20 +685,29 @@ const NSInteger TGVideoCameraRetainedBufferCount = 16;
[recorder prepareToRecord];
}
- (void)stopRecording
- (void)stopRecording:(void (^)())completed
{
[[TGVideoCameraPipeline cameraQueue] dispatch:^
{
@synchronized (self)
{
if (_recordingStatus != TGVideoCameraRecordingStatusRecording)
if (_recordingStatus != TGVideoCameraRecordingStatusRecording) {
if (completed) {
completed();
}
return;
}
[self transitionToRecordingStatus:TGVideoCameraRecordingStatusStoppingRecording error:nil];
}
_resultDuration = _recorder.videoDuration;
[_recorder finishRecording];
[_recorder finishRecording:^{
__unused __auto_type description = [self description];
if (completed) {
completed();
}
}];
}];
}
@ -734,6 +744,8 @@ const NSInteger TGVideoCameraRetainedBufferCount = 16;
- (void)movieRecorderDidFinishRecording:(TGVideoCameraMovieRecorder *)__unused recorder
{
printf("movieRecorderDidFinishRecording\n");
@synchronized (self)
{
if (_recordingStatus != TGVideoCameraRecordingStatusStoppingRecording)
@ -750,6 +762,8 @@ const NSInteger TGVideoCameraRetainedBufferCount = 16;
- (void)transitionToRecordingStatus:(TGVideoCameraRecordingStatus)newStatus error:(NSError *)error
{
printf("transitionToRecordingStatus %d\n", newStatus);
TGVideoCameraRecordingStatus oldStatus = _recordingStatus;
_recordingStatus = newStatus;
@ -763,12 +777,16 @@ const NSInteger TGVideoCameraRetainedBufferCount = 16;
}
else
{
__strong id<TGVideoCameraPipelineDelegate> delegate = _delegate;
if ((oldStatus == TGVideoCameraRecordingStatusStartingRecording) && (newStatus == TGVideoCameraRecordingStatusRecording))
delegateCallbackBlock = ^{ [_delegate capturePipelineRecordingDidStart:self]; };
delegateCallbackBlock = ^{ [delegate capturePipelineRecordingDidStart:self]; };
else if ((oldStatus == TGVideoCameraRecordingStatusRecording) && (newStatus == TGVideoCameraRecordingStatusStoppingRecording))
delegateCallbackBlock = ^{ [_delegate capturePipelineRecordingWillStop:self]; };
delegateCallbackBlock = ^{ [delegate capturePipelineRecordingWillStop:self]; };
else if ((oldStatus == TGVideoCameraRecordingStatusStoppingRecording) && (newStatus == TGVideoCameraRecordingStatusIdle))
delegateCallbackBlock = ^{ [_delegate capturePipelineRecordingDidStop:self duration:_resultDuration liveUploadData:_liveUploadData thumbnailImage:_recordingThumbnail thumbnails:_thumbnails]; };
delegateCallbackBlock = ^{
printf("transitionToRecordingStatus delegateCallbackBlock _delegate == nil = %d\n", (int)(delegate == nil));
[delegate capturePipelineRecordingDidStop:self duration:_resultDuration liveUploadData:_liveUploadData thumbnailImage:_recordingThumbnail thumbnails:_thumbnails];
};
}
if (delegateCallbackBlock != nil)

View file

@ -201,6 +201,7 @@ typedef enum
- (void)dealloc
{
printf("Video controller dealloc\n");
[_thumbnailsDisposable dispose];
[[NSNotificationCenter defaultCenter] removeObserver:_didEnterBackgroundObserver];
[_activityDisposable dispose];
@ -649,9 +650,11 @@ typedef enum
return;
[_activityDisposable dispose];
[self stopRecording];
[self dismiss:false];
[self stopRecording:^{
TGDispatchOnMainThread(^{
[self dismiss:false];
});
}];
}
- (void)buttonInteractionUpdate:(CGPoint)value
@ -684,7 +687,7 @@ typedef enum
_switchButton.userInteractionEnabled = false;
[_activityDisposable dispose];
[self stopRecording];
[self stopRecording:^{}];
return true;
}
@ -939,9 +942,9 @@ typedef enum
[self startRecordingTimer];
}
- (void)stopRecording
- (void)stopRecording:(void (^)())completed
{
[_capturePipeline stopRecording];
[_capturePipeline stopRecording:completed];
[_buttonHandler ignoreEventsFor:1.0f andDisable:true];
}

View file

@ -301,6 +301,8 @@ public func legacyAttachmentMenu(context: AccountContext, peer: Peer, editMediaO
peerSupportsPolls = true
} else if let user = peer as? TelegramUser, let _ = user.botInfo {
peerSupportsPolls = true
} else if peer.id == context.account.peerId {
peerSupportsPolls = true
}
if peerSupportsPolls && canSendMessagesToPeer(peer) && canSendPolls {
let pollItem = TGMenuSheetButtonItemView(title: presentationData.strings.AttachmentMenu_Poll, type: TGMenuSheetButtonTypeDefault, fontSize: fontSize, action: { [weak controller] in

View file

@ -87,12 +87,13 @@ private class SearchBarTextField: UITextField {
}
var rect = bounds.insetBy(dx: 4.0, dy: 4.0)
let prefixSize = self.prefixLabel.measure(bounds.size)
let prefixSize = self.prefixLabel.measure(CGSize(width: floor(bounds.size.width * 0.7), height: bounds.size.height))
if !prefixSize.width.isZero {
let prefixOffset = prefixSize.width
rect.origin.x += prefixOffset
rect.size.width -= prefixOffset
}
rect.size.width = max(rect.size.width, 10.0)
return rect
}
@ -117,7 +118,7 @@ private class SearchBarTextField: UITextField {
let labelSize = self.placeholderLabel.measure(textRect.size)
self.placeholderLabel.frame = CGRect(origin: CGPoint(x: textRect.minX, y: textRect.minY + textOffset), size: labelSize)
let prefixSize = self.prefixLabel.measure(bounds.size)
let prefixSize = self.prefixLabel.measure(CGSize(width: floor(bounds.size.width * 0.7), height: bounds.size.height))
let prefixBounds = bounds.insetBy(dx: 4.0, dy: 4.0)
self.prefixLabel.frame = CGRect(origin: CGPoint(x: prefixBounds.minX, y: prefixBounds.minY + textOffset), size: prefixSize)
}

View file

@ -1559,7 +1559,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
}
return;
}
if true {
if false {
if strongSelf.selectPollOptionFeedback == nil {
strongSelf.selectPollOptionFeedback = HapticFeedback()
}
@ -6708,7 +6708,13 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
}
public func navigateToMessage(messageLocation: NavigateToMessageLocation, animated: Bool, forceInCurrentChat: Bool = false, completion: (() -> Void)? = nil, customPresentProgress: ((ViewController, Any?) -> Void)? = nil) {
self.navigateToMessage(from: nil, to: messageLocation, rememberInStack: false, forceInCurrentChat: forceInCurrentChat, animated: animated, completion: completion, customPresentProgress: customPresentProgress)
let scrollPosition: ListViewScrollPosition
if case .upperBound = messageLocation {
scrollPosition = .top(0.0)
} else {
scrollPosition = .center(.bottom)
}
self.navigateToMessage(from: nil, to: messageLocation, scrollPosition: scrollPosition, rememberInStack: false, forceInCurrentChat: forceInCurrentChat, animated: animated, completion: completion, customPresentProgress: customPresentProgress)
}
private func navigateToMessage(from fromId: MessageId?, to messageLocation: NavigateToMessageLocation, scrollPosition: ListViewScrollPosition = .center(.bottom), rememberInStack: Bool = true, forceInCurrentChat: Bool = false, animated: Bool = true, completion: (() -> Void)? = nil, customPresentProgress: ((ViewController, Any?) -> Void)? = nil) {

View file

@ -11,6 +11,7 @@ import CheckNode
import SwiftSignalKit
import AccountContext
import AvatarNode
import TelegramPresentationData
private struct PercentCounterItem: Comparable {
var index: Int = 0
@ -373,6 +374,7 @@ private final class ChatMessagePollOptionNode: ASDisplayNode {
private(set) var currentSelection: ChatMessagePollOptionSelection?
var pressed: (() -> Void)?
var selectionUpdated: (() -> Void)?
private var theme: PresentationTheme?
override init() {
self.highlightedBackgroundNode = ASDisplayNode()
@ -430,6 +432,7 @@ private final class ChatMessagePollOptionNode: ASDisplayNode {
let makeTitleLayout = TextNode.asyncLayout(maybeNode?.titleNode)
let currentResult = maybeNode?.currentResult
let currentSelection = maybeNode?.currentSelection
let currentTheme = maybeNode?.theme
return { accountPeerId, presentationData, message, poll, option, optionResult, constrainedWidth in
let leftInset: CGFloat = 50.0
@ -449,8 +452,10 @@ private final class ChatMessagePollOptionNode: ASDisplayNode {
isSelectable = false
}
let themeUpdated = presentationData.theme.theme !== currentTheme
var updatedPercentageImage: UIImage?
if currentResult != optionResult {
if currentResult != optionResult || themeUpdated {
let value = optionResult?.percent ?? 0
updatedPercentageImage = generatePercentageImage(presentationData: presentationData, incoming: incoming, value: value, targetValue: value)
}
@ -471,7 +476,7 @@ private final class ChatMessagePollOptionNode: ASDisplayNode {
}
}
}
if selection != currentSelection {
if selection != currentSelection || themeUpdated {
updatedResultIcon = true
if let selection = selection {
var isQuiz = false
@ -537,6 +542,7 @@ private final class ChatMessagePollOptionNode: ASDisplayNode {
let previousResult = node.currentResult
node.currentResult = optionResult
node.currentSelection = selection
node.theme = presentationData.theme.theme
node.highlightedBackgroundNode.backgroundColor = incoming ? presentationData.theme.theme.chat.message.incoming.polls.highlight : presentationData.theme.theme.chat.message.outgoing.polls.highlight
@ -568,7 +574,7 @@ private final class ChatMessagePollOptionNode: ASDisplayNode {
}
let radioSize: CGFloat = 22.0
radioNode.frame = CGRect(origin: CGPoint(x: 12.0, y: 12.0), size: CGSize(width: radioSize, height: radioSize))
radioNode.update(staticColor: incoming ? presentationData.theme.theme.chat.message.incoming.polls.radioButton : presentationData.theme.theme.chat.message.outgoing.polls.radioButton, animatedColor: incoming ? presentationData.theme.theme.chat.message.incoming.polls.radioProgress : presentationData.theme.theme.chat.message.outgoing.polls.radioProgress, foregroundColor: presentationData.theme.theme.list.itemCheckColors.foregroundColor, isSelectable: isSelectable, isAnimating: inProgress)
radioNode.update(staticColor: incoming ? presentationData.theme.theme.chat.message.incoming.polls.radioButton : presentationData.theme.theme.chat.message.outgoing.polls.radioButton, animatedColor: incoming ? presentationData.theme.theme.chat.message.incoming.polls.radioProgress : presentationData.theme.theme.chat.message.outgoing.polls.radioProgress, foregroundColor: incoming ? presentationData.theme.theme.chat.message.incoming.polls.barIconForeground : presentationData.theme.theme.chat.message.outgoing.polls.barIconForeground, isSelectable: isSelectable, isAnimating: inProgress)
} else if let radioNode = node.radioNode {
node.radioNode = nil
if animated {
@ -1071,7 +1077,7 @@ class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode {
}
}
if Namespaces.Message.allScheduled.contains(item.message.id.namespace) {
canVote = true
canVote = false
}
return (boundingSize.width, { boundingWidth in
@ -1275,6 +1281,10 @@ class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode {
var hasResults = false
if poll.isClosed {
hasResults = true
hasSelection = false
if let totalVoters = poll.results.totalVoters, totalVoters == 0 {
hasResults = false
}
} else {
if let totalVoters = poll.results.totalVoters, totalVoters != 0 {
if let voters = poll.results.voters {
@ -1351,12 +1361,43 @@ class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode {
if optionNode.frame.contains(point), case .tap = gesture {
if optionNode.isUserInteractionEnabled {
return .ignore
} else if let result = optionNode.currentResult, let item = self.item, let poll = self.poll, case .public = poll.publicity, let option = optionNode.option {
if !isEstimating {
item.controllerInteraction.openMessagePollResults(item.message.id, option.opaqueIdentifier)
return .ignore
} else if let result = optionNode.currentResult, let item = self.item, let poll = self.poll, let option = optionNode.option {
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 .tooltip(string, optionNode, optionNode.bounds.offsetBy(dx: 0.0, dy: 10.0))
case .public:
var hasNonZeroVoters = false
if let voters = poll.results.voters {
for voter in voters {
if voter.count != 0 {
hasNonZeroVoters = true
break
}
}
}
if hasNonZeroVoters {
if !isEstimating {
item.controllerInteraction.openMessagePollResults(item.message.id, option.opaqueIdentifier)
return .ignore
}
return .openMessage
}
}
return .openMessage
}
}
}

View file

@ -19,11 +19,14 @@ private final class ParticleLayer: CALayer {
var velocity: Vector2
var angularVelocity: Float
var rotationAngle: Float = 0.0
var localTime: Float = 0.0
var type: Int
init(image: CGImage, size: CGSize, position: CGPoint, mass: Float, velocity: Vector2, angularVelocity: Float) {
init(image: CGImage, size: CGSize, position: CGPoint, mass: Float, velocity: Vector2, angularVelocity: Float, type: Int) {
self.mass = mass
self.velocity = velocity
self.angularVelocity = angularVelocity
self.type = type
super.init()
@ -84,38 +87,49 @@ final class ConfettiView: UIView {
let velocityYRange = Float(3.0) ..< Float(5.0)
let angularVelocityRange = Float(1.0) ..< Float(6.0)
let sizeVariation = Float(0.8) ..< Float(1.6)
let topDelayRange = Float(0.0) ..< Float(0.0)
for i in 0 ..< 70 {
let (image, size) = images[i % imageCount]
let sizeScale = CGFloat(Float.random(in: sizeVariation))
let particle = ParticleLayer(image: image, size: CGSize(width: size.width * sizeScale, height: size.height * sizeScale), position: CGPoint(x: CGFloat(Int.random(in: originXRange)), y: CGFloat(Int.random(in: originYRange))), mass: Float.random(in: topMassRange), velocity: Vector2(x: 0.0, y: Float.random(in: velocityYRange)), angularVelocity: Float.random(in: angularVelocityRange))
let particle = ParticleLayer(image: image, size: CGSize(width: size.width * sizeScale, height: size.height * sizeScale), position: CGPoint(x: CGFloat(Int.random(in: originXRange)), y: CGFloat(Int.random(in: originYRange))), mass: Float.random(in: topMassRange), velocity: Vector2(x: 0.0, y: Float.random(in: velocityYRange)), angularVelocity: Float.random(in: angularVelocityRange), type: 0)
self.particles.append(particle)
self.layer.addSublayer(particle)
}
let sideMassRange: Range<Float> = 100.0 ..< 110.0
let sideOriginYBase: Float = Float(frame.size.height * 8.5 / 10.0)
let sideMassRange: Range<Float> = 110.0 ..< 120.0
let sideOriginYBase: Float = Float(frame.size.height * 9.0 / 10.0)
let sideOriginYVariation: Float = Float(frame.size.height / 12.0)
let sideOriginYRange = Float(sideOriginYBase - sideOriginYVariation) ..< Float(sideOriginYBase + sideOriginYVariation)
let sideOriginXRange = Float(0.0) ..< Float(100.0)
let sideOriginVelocityValueRange = Float(1.1) ..< Float(1.6)
let sideOriginVelocityValueScaling: Float = 1200.0
let sideOriginVelocityValueRange = Float(1.1) ..< Float(1.3)
let sideOriginVelocityValueScaling: Float = 2400.0 * Float(frame.height) / 896.0
let sideOriginVelocityBase: Float = Float.pi / 2.0 + atanf(Float(CGFloat(sideOriginYBase) / (frame.size.width * 0.8)))
let sideOriginVelocityVariation: Float = 0.2
let sideOriginVelocityVariation: Float = 0.09
let sideOriginVelocityAngleRange = Float(sideOriginVelocityBase - sideOriginVelocityVariation) ..< Float(sideOriginVelocityBase + sideOriginVelocityVariation)
let originAngleRange = Float(0.0) ..< (Float.pi * 2.0)
let originAmplitudeDiameter: CGFloat = 230.0
let originAmplitudeRange = Float(0.0) ..< Float(originAmplitudeDiameter / 2.0)
let sideTypes: [Int] = [0, 1, 2]
for sideIndex in 0 ..< 2 {
let sideSign: Float = sideIndex == 0 ? 1.0 : -1.0
let originX: CGFloat = sideIndex == 0 ? -5.0 : (frame.width + 5.0)
let baseOriginX: CGFloat = sideIndex == 0 ? -originAmplitudeDiameter / 2.0 : (frame.width + originAmplitudeDiameter / 2.0)
for i in 0 ..< 40 {
let offsetX = CGFloat(Float.random(in: sideOriginXRange) * (-sideSign))
let originAngle = Float.random(in: originAngleRange)
let originAmplitude = Float.random(in: originAmplitudeRange)
let originX = baseOriginX + CGFloat(cosf(originAngle) * originAmplitude)
let originY = CGFloat(sideOriginYBase + sinf(originAngle) * originAmplitude)
let velocityValue = Float.random(in: sideOriginVelocityValueRange) * sideOriginVelocityValueScaling
let velocityAngle = Float.random(in: sideOriginVelocityAngleRange)
let velocityX = sideSign * velocityValue * sinf(velocityAngle)
let velocityY = velocityValue * cosf(velocityAngle)
let (image, size) = images[i % imageCount]
let sizeScale = CGFloat(Float.random(in: sizeVariation))
let particle = ParticleLayer(image: image, size: CGSize(width: size.width * sizeScale, height: size.height * sizeScale), position: CGPoint(x: originX + offsetX, y: CGFloat(Float.random(in: sideOriginYRange))), mass: Float.random(in: sideMassRange), velocity: Vector2(x: velocityX, y: velocityY), angularVelocity: Float.random(in: angularVelocityRange))
let particle = ParticleLayer(image: image, size: CGSize(width: size.width * sizeScale, height: size.height * sizeScale), position: CGPoint(x: originX, y: originY), mass: Float.random(in: sideMassRange), velocity: Vector2(x: velocityX, y: velocityY), angularVelocity: Float.random(in: angularVelocityRange), type: sideTypes[i % 3])
self.particles.append(particle)
self.layer.addSublayer(particle)
}
@ -132,57 +146,89 @@ final class ConfettiView: UIView {
fatalError("init(coder:) has not been implemented")
}
private var slowdownStartTimestamps: [Float?] = [nil, nil, nil]
private func step() {
self.slowdownStartTimestamps[0] = 0.33
var haveParticlesAboveGround = false
let minPositionY: CGFloat = 0.0
let maxPositionY = self.bounds.height + 30.0
let minDampingX: CGFloat = 40.0
let maxDampingX: CGFloat = self.bounds.width - 40.0
let centerX: CGFloat = self.bounds.width / 2.0
let currentTime = self.localTime
let dt: Float = 1.0 / 60.0
let slowdownDt: Float
let slowdownStart: Float = 0.27
let slowdownDuration: Float = 0.9
let damping: Float
if currentTime >= slowdownStart && currentTime <= slowdownStart + slowdownDuration {
let slowdownTimestamp: Float = currentTime - slowdownStart
let slowdownRampInDuration: Float = 0.15
let slowdownRampOutDuration: Float = 0.5
let slowdownTransition: Float
if slowdownTimestamp < slowdownRampInDuration {
slowdownTransition = slowdownTimestamp / slowdownRampInDuration
} else if slowdownTimestamp >= slowdownDuration - slowdownRampOutDuration {
let reverseTransition = (slowdownTimestamp - (slowdownDuration - slowdownRampOutDuration)) / slowdownRampOutDuration
slowdownTransition = 1.0 - reverseTransition
let dt: Float = 1.0 * 1.0 / 60.0
let typeDelays: [Float] = [0.0, 0.01, 0.08]
var dtAndDamping: [(Float, Float)] = []
for i in 0 ..< 3 {
let typeDelay = typeDelays[i]
let currentTime = self.localTime - typeDelay
if currentTime < 0.0 {
dtAndDamping.append((0.0, 1.0))
} else if let slowdownStart = self.slowdownStartTimestamps[i] {
let slowdownDt: Float
let slowdownDuration: Float = 0.5
let damping: Float
if currentTime >= slowdownStart && currentTime <= slowdownStart + slowdownDuration {
let slowdownTimestamp: Float = currentTime - slowdownStart
let slowdownRampInDuration: Float = 0.05
let slowdownRampOutDuration: Float = 0.2
let rawSlowdownT: Float
if slowdownTimestamp < slowdownRampInDuration {
rawSlowdownT = slowdownTimestamp / slowdownRampInDuration
} else if slowdownTimestamp >= slowdownDuration - slowdownRampOutDuration {
let reverseTransition = (slowdownTimestamp - (slowdownDuration - slowdownRampOutDuration)) / slowdownRampOutDuration
rawSlowdownT = 1.0 - reverseTransition
} else {
rawSlowdownT = 1.0
}
let slowdownTransition = rawSlowdownT * rawSlowdownT
let slowdownFactor: Float = 0.8 * slowdownTransition + 1.0 * (1.0 - slowdownTransition)
slowdownDt = dt * slowdownFactor
let dampingFactor: Float = 0.937 * slowdownTransition + 1.0 * (1.0 - slowdownTransition)
damping = dampingFactor
} else {
slowdownDt = dt
damping = 1.0
}
if i == 1 {
//print("type 1 dt = \(slowdownDt), slowdownStart = \(slowdownStart), currentTime = \(currentTime)")
}
dtAndDamping.append((slowdownDt, damping))
} else {
slowdownTransition = 1.0
dtAndDamping.append((dt, 1.0))
}
let slowdownFactor: Float = 0.3 * slowdownTransition + 1.0 * (1.0 - slowdownTransition)
slowdownDt = dt * slowdownFactor
let dampingFactor: Float = 0.94 * slowdownTransition + 1.0 * (1.0 - slowdownTransition)
damping = dampingFactor
} else {
slowdownDt = dt
damping = 1.0
}
self.localTime += 1.0 / 60.0
self.localTime += dt
let g: Vector2 = Vector2(x: 0.0, y: 9.8)
CATransaction.begin()
CATransaction.setDisableActions(true)
var turbulenceVariation: [Float] = []
for _ in 0 ..< 20 {
turbulenceVariation.append(Float.random(in: -9.0 ..< 9.0))
turbulenceVariation.append(Float.random(in: -16.0 ..< 16.0) * 60.0)
}
let turbulenceVariationCount = turbulenceVariation.count
var index = 0
var typesWithPositiveVelocity: [Bool] = [false, false, false]
for particle in self.particles {
var position = particle.position
let (localDt, damping_) = dtAndDamping[particle.type]
if localDt.isZero {
continue
}
let damping: Float = 0.93
let localDt: Float = slowdownDt
particle.localTime += localDt
var position = particle.position
position.x += CGFloat(particle.velocity.x * localDt)
position.y += CGFloat(particle.velocity.y * localDt)
@ -196,10 +242,12 @@ final class ConfettiView: UIView {
var velocity = particle.velocity
velocity.x += acceleration.x * particle.mass * localDt
velocity.y += acceleration.y * particle.mass * localDt
velocity.x += turbulenceVariation[index % turbulenceVariationCount]
if position.y > minPositionY {
if velocity.y < 0.0 {
velocity.x *= damping
velocity.y *= damping
} else {
velocity.x += turbulenceVariation[index % turbulenceVariationCount] * localDt
typesWithPositiveVelocity[particle.type] = true
}
particle.velocity = velocity
@ -209,6 +257,11 @@ final class ConfettiView: UIView {
haveParticlesAboveGround = true
}
}
for i in 0 ..< 3 {
if typesWithPositiveVelocity[i] && self.slowdownStartTimestamps[i] == nil {
self.slowdownStartTimestamps[i] = max(0.0, self.localTime - typeDelays[i])
}
}
CATransaction.commit()
if !haveParticlesAboveGround {
self.displayLink?.isPaused = true

View file

@ -135,16 +135,11 @@ func legacyInstantVideoController(theme: PresentationTheme, panelFrame: CGRect,
done?(time)
}
}
controller.finishedWithVideo = { [weak controller] videoUrl, previewImage, _, duration, dimensions, liveUploadData, adjustments, isSilent, scheduleTimestamp in
controller.finishedWithVideo = { videoUrl, previewImage, _, duration, dimensions, liveUploadData, adjustments, isSilent, scheduleTimestamp in
guard let videoUrl = videoUrl else {
return
}
let strongController = controller
Queue.mainQueue().after(4.0, {
strongController?.resignFirstResponder()
})
var finalDimensions: CGSize = dimensions
var finalDuration: Double = duration

View file

@ -224,7 +224,11 @@ private func pollResultsControllerEntries(presentationData: PresentationData, po
if peers.count > collapsedInitialLimit {
displayCount = collapsedResultCount
} else {
displayCount = peers.count
if optionState.isLoadingMore && peers.count <= collapsedInitialLimit + 1 {
displayCount = collapsedResultCount
} else {
displayCount = peers.count
}
}
var peerIndex = 0
@ -278,7 +282,7 @@ public func pollResultsController(context: AccountContext, messageId: MessageId,
|> take(1)
|> deliverOnMainQueue).start(next: { [weak resultsContext] state in
if let optionState = state.options[optionId] {
if optionState.canLoadMore && optionState.peers.count <= collapsedResultCount {
if optionState.canLoadMore {
resultsContext?.loadMore(optionOpaqueIdentifier: optionId)
}
}

View file

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