diff --git a/submodules/ChatListUI/Sources/ChatListController.swift b/submodules/ChatListUI/Sources/ChatListController.swift index 539b4065b3..cd77da62c5 100644 --- a/submodules/ChatListUI/Sources/ChatListController.swift +++ b/submodules/ChatListUI/Sources/ChatListController.swift @@ -2356,7 +2356,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController let location = CGRect(origin: CGPoint(x: absoluteFrame.midX, y: absoluteFrame.minY - 8.0), size: CGSize()) - parentController.present(TooltipScreen(account: strongSelf.context.account, sharedContext: strongSelf.context.sharedContext, text: .plain(text: text), icon: .animation(name: "ChatListFoldersTooltip", delay: 0.6), location: .point(location, .bottom), shouldDismissOnTouch: { point in + parentController.present(TooltipScreen(account: strongSelf.context.account, sharedContext: strongSelf.context.sharedContext, text: .plain(text: text), icon: .animation(name: "ChatListFoldersTooltip", delay: 0.6, tintColor: nil), location: .point(location, .bottom), shouldDismissOnTouch: { point in guard let strongSelf = self, let parentController = strongSelf.parent as? TabBarController else { return .dismiss(consume: false) } @@ -2640,7 +2640,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController }))) let isMuted = notificationSettings.storiesMuted == true - items.append(.action(ContextMenuActionItem(text: isMuted ? "Notify" : "Not Notify", icon: { theme in + items.append(.action(ContextMenuActionItem(text: isMuted ? "Notify" : "Don't Notify", icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: isMuted ? "Chat/Context Menu/Unmute" : "Chat/Context Menu/Muted"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in f(.default) @@ -2683,7 +2683,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController } }))) - items.append(.action(ContextMenuActionItem(text: "Move to Contacts", icon: { theme in + items.append(.action(ContextMenuActionItem(text: "Hide \(peer.compactDisplayTitle)", icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/MoveToContacts"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in f(.dismissWithoutContent) diff --git a/submodules/ChatListUI/Sources/ChatListControllerNode.swift b/submodules/ChatListUI/Sources/ChatListControllerNode.swift index 6fa08b2bc6..d828653729 100644 --- a/submodules/ChatListUI/Sources/ChatListControllerNode.swift +++ b/submodules/ChatListUI/Sources/ChatListControllerNode.swift @@ -1295,39 +1295,38 @@ public final class ChatListContainerNode: ASDisplayNode, UIGestureRecognizerDele let hasStoryCameraTransition = self.controller?.hasStoryCameraTransition ?? false if hasStoryCameraTransition { self.controller?.storyCameraPanGestureEnded(transitionFraction: translation.x / layout.size.width, velocity: velocity.x) - } else { - var applyNodeAsCurrent: ChatListFilterTabEntryId? - - if let directionIsToRight = directionIsToRight { - var updatedIndex = selectedIndex - if directionIsToRight { - updatedIndex = min(updatedIndex + 1, maxFilterIndex) - } else { - updatedIndex = max(updatedIndex - 1, 0) - } - let switchToId = self.availableFilters[updatedIndex].id - if switchToId != self.selectedId, let itemNode = self.itemNodes[switchToId] { - let _ = itemNode - self.selectedId = switchToId - applyNodeAsCurrent = switchToId - } - } - self.transitionFraction = 0.0 - let transition: ContainedViewLayoutTransition = .animated(duration: 0.45, curve: .spring) - self.disableItemNodeOperationsWhileAnimating = true - self.update(layout: layout, navigationBarHeight: navigationBarHeight, visualNavigationHeight: visualNavigationHeight, originalNavigationHeight: originalNavigationHeight, cleanNavigationBarHeight: cleanNavigationBarHeight, insets: insets, isReorderingFilters: isReorderingFilters, isEditing: isEditing, inlineNavigationLocation: inlineNavigationLocation, inlineNavigationTransitionFraction: inlineNavigationTransitionFraction, storiesInset: storiesInset, transition: transition) - DispatchQueue.main.async { - self.disableItemNodeOperationsWhileAnimating = false - if let (layout, navigationBarHeight, visualNavigationHeight, originalNavigationHeight, cleanNavigationBarHeight, insets, isReorderingFilters, isEditing, inlineNavigationLocation, inlineNavigationTransitionFraction, storiesInset) = self.validLayout { - self.update(layout: layout, navigationBarHeight: navigationBarHeight, visualNavigationHeight: visualNavigationHeight, originalNavigationHeight: originalNavigationHeight, cleanNavigationBarHeight: cleanNavigationBarHeight, insets: insets, isReorderingFilters: isReorderingFilters, isEditing: isEditing, inlineNavigationLocation: inlineNavigationLocation, inlineNavigationTransitionFraction: inlineNavigationTransitionFraction, storiesInset: storiesInset, transition: .immediate) - } - } - - if let switchToId = applyNodeAsCurrent, let itemNode = self.itemNodes[switchToId] { - self.applyItemNodeAsCurrent(id: switchToId, itemNode: itemNode) - } - self.currentItemFilterUpdated?(self.currentItemFilter, self.transitionFraction, transition, false) } + var applyNodeAsCurrent: ChatListFilterTabEntryId? + + if let directionIsToRight = directionIsToRight { + var updatedIndex = selectedIndex + if directionIsToRight { + updatedIndex = min(updatedIndex + 1, maxFilterIndex) + } else { + updatedIndex = max(updatedIndex - 1, 0) + } + let switchToId = self.availableFilters[updatedIndex].id + if switchToId != self.selectedId, let itemNode = self.itemNodes[switchToId] { + let _ = itemNode + self.selectedId = switchToId + applyNodeAsCurrent = switchToId + } + } + self.transitionFraction = 0.0 + let transition: ContainedViewLayoutTransition = .animated(duration: 0.45, curve: .spring) + self.disableItemNodeOperationsWhileAnimating = true + self.update(layout: layout, navigationBarHeight: navigationBarHeight, visualNavigationHeight: visualNavigationHeight, originalNavigationHeight: originalNavigationHeight, cleanNavigationBarHeight: cleanNavigationBarHeight, insets: insets, isReorderingFilters: isReorderingFilters, isEditing: isEditing, inlineNavigationLocation: inlineNavigationLocation, inlineNavigationTransitionFraction: inlineNavigationTransitionFraction, storiesInset: storiesInset, transition: transition) + DispatchQueue.main.async { + self.disableItemNodeOperationsWhileAnimating = false + if let (layout, navigationBarHeight, visualNavigationHeight, originalNavigationHeight, cleanNavigationBarHeight, insets, isReorderingFilters, isEditing, inlineNavigationLocation, inlineNavigationTransitionFraction, storiesInset) = self.validLayout { + self.update(layout: layout, navigationBarHeight: navigationBarHeight, visualNavigationHeight: visualNavigationHeight, originalNavigationHeight: originalNavigationHeight, cleanNavigationBarHeight: cleanNavigationBarHeight, insets: insets, isReorderingFilters: isReorderingFilters, isEditing: isEditing, inlineNavigationLocation: inlineNavigationLocation, inlineNavigationTransitionFraction: inlineNavigationTransitionFraction, storiesInset: storiesInset, transition: .immediate) + } + } + + if let switchToId = applyNodeAsCurrent, let itemNode = self.itemNodes[switchToId] { + self.applyItemNodeAsCurrent(id: switchToId, itemNode: itemNode) + } + self.currentItemFilterUpdated?(self.currentItemFilter, self.transitionFraction, transition, false) } default: break diff --git a/submodules/Components/LottieAnimationComponent/Sources/LottieAnimationComponent.swift b/submodules/Components/LottieAnimationComponent/Sources/LottieAnimationComponent.swift index 30eee8d303..9fc9e1447e 100644 --- a/submodules/Components/LottieAnimationComponent/Sources/LottieAnimationComponent.swift +++ b/submodules/Components/LottieAnimationComponent/Sources/LottieAnimationComponent.swift @@ -128,14 +128,21 @@ public final class LottieAnimationComponent: Component { } public func playOnce() { - guard let animationView = self.animationView else { + guard let animationView = self.animationView, let component = self.component else { return } animationView.stop() animationView.loopMode = .playOnce - animationView.play { [weak self] _ in - self?.currentCompletion?() + + if let range = component.animation.range { + animationView.play(fromProgress: range.0, toProgress: range.1, completion: { [weak self] _ in + self?.currentCompletion?() + }) + } else { + animationView.play { [weak self] _ in + self?.currentCompletion?() + } } } diff --git a/submodules/ContactListUI/Sources/ContactContextMenus.swift b/submodules/ContactListUI/Sources/ContactContextMenus.swift index 982d46e5ef..2c499de1e9 100644 --- a/submodules/ContactListUI/Sources/ContactContextMenus.swift +++ b/submodules/ContactListUI/Sources/ContactContextMenus.swift @@ -43,7 +43,7 @@ func contactContextMenuItems(context: AccountContext, peerId: EnginePeer.Id, con }))) let isMuted = notificationSettings.storiesMuted == true - items.append(.action(ContextMenuActionItem(text: isMuted ? "Notify" : "Not Notify", icon: { theme in + items.append(.action(ContextMenuActionItem(text: isMuted ? "Notify" : "Don't Notify", icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: isMuted ? "Chat/Context Menu/Unmute" : "Chat/Context Menu/Muted"), color: theme.contextMenu.primaryColor) }, action: { _, f in f(.default) @@ -85,7 +85,7 @@ func contactContextMenuItems(context: AccountContext, peerId: EnginePeer.Id, con } }))) - items.append(.action(ContextMenuActionItem(text: "Move to chats", icon: { theme in + items.append(.action(ContextMenuActionItem(text: "Move to Chats", icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/MoveToChats"), color: theme.contextMenu.primaryColor) }, action: { _, f in f(.default) @@ -102,7 +102,7 @@ func contactContextMenuItems(context: AccountContext, peerId: EnginePeer.Id, con context: context, account: context.account, sharedContext: context.sharedContext, - text: .markdown(text: "Stories from \(peer.compactDisplayTitle) will now be shown in Chats, not Contacts."), + text: .markdown(text: "Stories from **\(peer.compactDisplayTitle)** will now be shown in Chats, not Contacts."), icon: .peer(peer: peer, isStory: true), action: TooltipScreen.Action( title: "Undo", diff --git a/submodules/DrawingUI/Sources/DrawingBubbleEntity.swift b/submodules/DrawingUI/Sources/DrawingBubbleEntity.swift index 585b913720..8a3e30e153 100644 --- a/submodules/DrawingUI/Sources/DrawingBubbleEntity.swift +++ b/submodules/DrawingUI/Sources/DrawingBubbleEntity.swift @@ -330,6 +330,10 @@ final class DrawingBubbleEntititySelectionView: DrawingEntitySelectionView, UIGe case .changed: rotation = gestureRecognizer.rotation updatedRotation += rotation + + updatedRotation = self.snapTool.update(entityView: entityView, velocity: velocity, delta: rotation, updatedRotation: updatedRotation) + entity.rotation = updatedRotation + entityView.update() gestureRecognizer.rotation = 0.0 case .ended, .cancelled: @@ -338,11 +342,7 @@ final class DrawingBubbleEntititySelectionView: DrawingEntitySelectionView, UIGe default: break } - - updatedRotation = self.snapTool.update(entityView: entityView, velocity: velocity, delta: rotation, updatedRotation: updatedRotation) - entity.rotation = updatedRotation - entityView.update() - + entityView.onPositionUpdated(entity.position) } diff --git a/submodules/DrawingUI/Sources/DrawingEntitiesView.swift b/submodules/DrawingUI/Sources/DrawingEntitiesView.swift index b2cdd7680a..5d56284be7 100644 --- a/submodules/DrawingUI/Sources/DrawingEntitiesView.swift +++ b/submodules/DrawingUI/Sources/DrawingEntitiesView.swift @@ -350,6 +350,7 @@ public final class DrawingEntitiesView: UIView, TGPhotoDrawingEntitiesView { } } + let isMediaEntity = entity is DrawingMediaEntity view.onSnapUpdated = { [weak self, weak view] type, snapped in guard let self else { return @@ -380,6 +381,7 @@ public final class DrawingEntitiesView: UIView, TGPhotoDrawingEntitiesView { self.hapticFeedback.impact(.light) } transition.updateAlpha(layer: self.angleLayer, alpha: 1.0) + self.angleLayer.isHidden = isMediaEntity } else { transition.updateAlpha(layer: self.angleLayer, alpha: 0.0) } diff --git a/submodules/DrawingUI/Sources/DrawingMediaEntity.swift b/submodules/DrawingUI/Sources/DrawingMediaEntity.swift index 9cb0d183ed..fdb4f60717 100644 --- a/submodules/DrawingUI/Sources/DrawingMediaEntity.swift +++ b/submodules/DrawingUI/Sources/DrawingMediaEntity.swift @@ -49,6 +49,12 @@ public final class DrawingMediaEntityView: DrawingEntityView, DrawingEntityMedia init(context: AccountContext, entity: DrawingMediaEntity) { super.init(context: context, entity: entity) + + self.snapTool.onSnapUpdated = { [weak self] type, snapped in + if let self { + self.onSnapUpdated(type, snapped) + } + } } required init?(coder: NSCoder) { @@ -196,15 +202,15 @@ public final class DrawingMediaEntityView: DrawingEntityView, DrawingEntityMedia self.beganRotating = true gestureRecognizer.rotation = 0.0 } + + updatedRotation = self.snapTool.update(entityView: self, velocity: velocity, delta: rotation, updatedRotation: updatedRotation) + self.mediaEntity.rotation = updatedRotation + self.update(animated: false) case .ended, .cancelled: self.snapTool.rotationReset() self.beganRotating = false default: break } - - updatedRotation = self.snapTool.update(entityView: self, velocity: velocity, delta: rotation, updatedRotation: updatedRotation) - self.mediaEntity.rotation = updatedRotation - self.update(animated: false) } } diff --git a/submodules/DrawingUI/Sources/DrawingScreen.swift b/submodules/DrawingUI/Sources/DrawingScreen.swift index 8c6d230640..17c831abfb 100644 --- a/submodules/DrawingUI/Sources/DrawingScreen.swift +++ b/submodules/DrawingUI/Sources/DrawingScreen.swift @@ -485,6 +485,7 @@ private let color6Tag = GenericComponentViewTag() private let color7Tag = GenericComponentViewTag() private let color8Tag = GenericComponentViewTag() private let colorTags = [color1Tag, color2Tag, color3Tag, color4Tag, color5Tag, color6Tag, color7Tag, color8Tag] +private let cancelButtonTag = GenericComponentViewTag() private let doneButtonTag = GenericComponentViewTag() private final class DrawingScreenComponent: CombinedComponent { @@ -2074,42 +2075,46 @@ private final class DrawingScreenComponent: CombinedComponent { animatingOut = true } - let backButton = backButton.update( - component: Button( - content: AnyComponent( - LottieAnimationComponent( - animation: LottieAnimationComponent.AnimationItem( - name: "media_backToCancel", - mode: .animating(loop: false), - range: animatingOut || component.isAvatar ? (0.5, 1.0) : (0.0, 0.5) - ), - colors: ["__allcolors__": .white], - size: CGSize(width: 33.0, height: 33.0) - ) - ), - action: { [weak state] in - if let state = state { - dismissEyedropper.invoke(Void()) - state.saveToolState() - dismiss.invoke(Void()) + if animatingOut && component.sourceHint == .storyEditor { + + } else { + let backButton = backButton.update( + component: Button( + content: AnyComponent( + LottieAnimationComponent( + animation: LottieAnimationComponent.AnimationItem( + name: "media_backToCancel", + mode: .animating(loop: false), + range: animatingOut || component.isAvatar ? (0.5, 1.0) : (0.0, 0.5) + ), + colors: ["__allcolors__": .white], + size: CGSize(width: 33.0, height: 33.0) + ) + ), + action: { [weak state] in + if let state = state { + dismissEyedropper.invoke(Void()) + state.saveToolState() + dismiss.invoke(Void()) + } } + ).minSize(CGSize(width: 44.0, height: 44.0)).tagged(cancelButtonTag), + availableSize: CGSize(width: 33.0, height: 33.0), + transition: .immediate + ) + var backButtonPosition = CGPoint(x: environment.safeInsets.left + backButton.size.width / 2.0 + 3.0, y: context.availableSize.height - environment.safeInsets.bottom - backButton.size.height / 2.0 - 2.0 - UIScreenPixel) + if component.sourceHint == .storyEditor { + backButtonPosition.x = backButtonPosition.x + 2.0 + if case .regular = environment.metrics.widthClass { + backButtonPosition.x += 20.0 } - ).minSize(CGSize(width: 44.0, height: 44.0)), - availableSize: CGSize(width: 33.0, height: 33.0), - transition: .immediate - ) - var backButtonPosition = CGPoint(x: environment.safeInsets.left + backButton.size.width / 2.0 + 3.0, y: context.availableSize.height - environment.safeInsets.bottom - backButton.size.height / 2.0 - 2.0 - UIScreenPixel) - if component.sourceHint == .storyEditor { - backButtonPosition.x = backButtonPosition.x + 2.0 - if case .regular = environment.metrics.widthClass { - backButtonPosition.x += 20.0 + backButtonPosition.y = floorToScreenPixels(context.availableSize.height - previewBottomInset + 3.0 + backButton.size.height / 2.0) } - backButtonPosition.y = floorToScreenPixels(context.availableSize.height - previewBottomInset + 3.0 + backButton.size.height / 2.0) + context.add(backButton + .position(backButtonPosition) + .opacity(controlsAreVisible ? 1.0 : 0.0) + ) } - context.add(backButton - .position(backButtonPosition) - .opacity(controlsAreVisible ? 1.0 : 0.0) - ) return context.availableSize } @@ -2497,6 +2502,9 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController, U view.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false) } if let view = self.componentHost.findTaggedView(tag: bottomGradientTag) { + if self.controller?.sourceHint == .storyEditor { + view.isHidden = true + } view.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false) } if let buttonView = self.componentHost.findTaggedView(tag: undoButtonTag) { @@ -3031,6 +3039,10 @@ public final class DrawingToolsInteraction { } } + guard !isVideo else { + return + } + let presentationData = self.context.sharedContext.currentPresentationData.with { $0 }.withUpdated(theme: defaultDarkPresentationTheme) var actions: [ContextMenuAction] = [] actions.append(ContextMenuAction(content: .text(title: presentationData.strings.Paint_Delete, accessibilityLabel: presentationData.strings.Paint_Delete), action: { [weak self, weak entityView] in diff --git a/submodules/DrawingUI/Sources/DrawingSimpleShapeEntity.swift b/submodules/DrawingUI/Sources/DrawingSimpleShapeEntity.swift index 74a0c722bc..f8be53dfdf 100644 --- a/submodules/DrawingUI/Sources/DrawingSimpleShapeEntity.swift +++ b/submodules/DrawingUI/Sources/DrawingSimpleShapeEntity.swift @@ -383,6 +383,10 @@ final class DrawingSimpleShapeEntititySelectionView: DrawingEntitySelectionView, case .changed: rotation = gestureRecognizer.rotation updatedRotation += rotation + + updatedRotation = self.snapTool.update(entityView: entityView, velocity: velocity, delta: rotation, updatedRotation: updatedRotation) + entity.rotation = updatedRotation + entityView.update() gestureRecognizer.rotation = 0.0 case .ended, .cancelled: @@ -391,11 +395,7 @@ final class DrawingSimpleShapeEntititySelectionView: DrawingEntitySelectionView, default: break } - - updatedRotation = self.snapTool.update(entityView: entityView, velocity: velocity, delta: rotation, updatedRotation: updatedRotation) - entity.rotation = updatedRotation - entityView.update() - + entityView.onPositionUpdated(entity.position) } diff --git a/submodules/DrawingUI/Sources/DrawingStickerEntity.swift b/submodules/DrawingUI/Sources/DrawingStickerEntity.swift index 4d4370a1ca..065b1280a2 100644 --- a/submodules/DrawingUI/Sources/DrawingStickerEntity.swift +++ b/submodules/DrawingUI/Sources/DrawingStickerEntity.swift @@ -583,6 +583,10 @@ final class DrawingStickerEntititySelectionView: DrawingEntitySelectionView, UIG case .changed: rotation = gestureRecognizer.rotation updatedRotation += rotation + + updatedRotation = self.snapTool.update(entityView: entityView, velocity: velocity, delta: rotation, updatedRotation: updatedRotation) + entity.rotation = updatedRotation + entityView.update() gestureRecognizer.rotation = 0.0 case .ended, .cancelled: @@ -591,11 +595,7 @@ final class DrawingStickerEntititySelectionView: DrawingEntitySelectionView, UIG default: break } - - updatedRotation = self.snapTool.update(entityView: entityView, velocity: velocity, delta: rotation, updatedRotation: updatedRotation) - entity.rotation = updatedRotation - entityView.update() - + entityView.onPositionUpdated(entity.position) } @@ -967,9 +967,9 @@ class DrawingEntitySnapTool { let currentTimestamp = CACurrentMediaTime() - let snapDelta: CGFloat = 0.02 - let snapVelocity: CGFloat = snapDelta * 8.0 - let snapSkipRotation: CGFloat = snapDelta * 5.0 + let snapDelta: CGFloat = 0.01 + let snapVelocity: CGFloat = snapDelta * 35.0 + let snapSkipRotation: CGFloat = snapDelta * 40.0 if abs(velocity) < snapVelocity || self.rotationState?.waitForLeave == true { if let (snapRotation, skipped, waitForLeave) = self.rotationState { diff --git a/submodules/DrawingUI/Sources/DrawingTextEntity.swift b/submodules/DrawingUI/Sources/DrawingTextEntity.swift index 410e1bb362..5f8bb676c9 100644 --- a/submodules/DrawingUI/Sources/DrawingTextEntity.swift +++ b/submodules/DrawingUI/Sources/DrawingTextEntity.swift @@ -842,6 +842,10 @@ final class DrawingTextEntititySelectionView: DrawingEntitySelectionView, UIGest rotation = gestureRecognizer.rotation updatedRotation += rotation + updatedRotation = self.snapTool.update(entityView: entityView, velocity: velocity, delta: rotation, updatedRotation: updatedRotation) + entity.rotation = updatedRotation + entityView.update() + gestureRecognizer.rotation = 0.0 case .ended, .cancelled: self.snapTool.rotationReset() @@ -850,10 +854,6 @@ final class DrawingTextEntititySelectionView: DrawingEntitySelectionView, UIGest break } - updatedRotation = self.snapTool.update(entityView: entityView, velocity: velocity, delta: rotation, updatedRotation: updatedRotation) - entity.rotation = updatedRotation - entityView.update() - entityView.onPositionUpdated(entity.position) } diff --git a/submodules/SettingsUI/Sources/Data and Storage/SaveIncomingMediaController.swift b/submodules/SettingsUI/Sources/Data and Storage/SaveIncomingMediaController.swift index 53eb0764d9..9f4dca4e0e 100644 --- a/submodules/SettingsUI/Sources/Data and Storage/SaveIncomingMediaController.swift +++ b/submodules/SettingsUI/Sources/Data and Storage/SaveIncomingMediaController.swift @@ -206,7 +206,7 @@ private enum SaveIncomingMediaEntry: ItemListNodeEntry { return ItemListSectionHeaderItem(presentationData: presentationData, text: title, sectionId: self.section) case let .addException(title): let icon: UIImage? = PresentationResourcesItemList.createGroupIcon(presentationData.theme) - return ItemListPeerActionItem(presentationData: presentationData, icon: icon, title: title, alwaysPlain: false, sectionId: self.section, editing: false, action: { + return ItemListPeerActionItem(presentationData: presentationData, icon: icon, title: title, alwaysPlain: false, sectionId: self.section, height: .generic, editing: false, action: { arguments.openAddException() }) case let .exceptionItem(_, peer, label): diff --git a/submodules/TelegramAudio/Sources/ManagedAudioSession.swift b/submodules/TelegramAudio/Sources/ManagedAudioSession.swift index ea5c8f4765..31a2bd5c58 100644 --- a/submodules/TelegramAudio/Sources/ManagedAudioSession.swift +++ b/submodules/TelegramAudio/Sources/ManagedAudioSession.swift @@ -778,21 +778,21 @@ public final class ManagedAudioSession: NSObject { options.insert(.allowBluetooth) options.insert(.allowBluetoothA2DP) options.insert(.mixWithOthers) - case .record, .recordWithOthers: + case .record: options.insert(.allowBluetooth) + case .recordWithOthers: + options.insert(.allowBluetoothA2DP) + options.insert(.mixWithOthers) } managedAudioSessionLog("ManagedAudioSession setting category and options") let mode: AVAudioSession.Mode switch type { case .voiceCall: mode = .voiceChat - options.insert(.mixWithOthers) case .videoCall: mode = .videoChat - options.insert(.mixWithOthers) case .recordWithOthers: mode = .videoRecording - options.insert(.mixWithOthers) default: mode = .default } diff --git a/submodules/TelegramUI/Components/CameraScreen/Sources/CameraScreen.swift b/submodules/TelegramUI/Components/CameraScreen/Sources/CameraScreen.swift index 52f69752d3..892b747940 100644 --- a/submodules/TelegramUI/Components/CameraScreen/Sources/CameraScreen.swift +++ b/submodules/TelegramUI/Components/CameraScreen/Sources/CameraScreen.swift @@ -374,7 +374,12 @@ private final class CameraScreenComponent: CombinedComponent { self.updated(transition: .easeInOut(duration: 0.2)) } + private var isTakingPhoto = false func takePhoto() { + guard !self.isTakingPhoto else { + return + } + self.isTakingPhoto = true let takePhoto = self.camera.takePhoto() |> mapToSignal { value -> Signal in switch value { @@ -387,9 +392,15 @@ private final class CameraScreenComponent: CombinedComponent { } } self.completion.invoke(takePhoto) + Queue.mainQueue().after(1.0) { + self.isTakingPhoto = false + } } func startVideoRecording(pressing: Bool) { + guard case .none = self.cameraState.recording else { + return + } self.cameraState = self.cameraState.updatedDuration(0.0).updatedRecording(pressing ? .holding : .handsFree) self.resultDisposable.set((self.camera.startRecording() |> deliverOnMainQueue).start(next: { [weak self] duration in @@ -413,7 +424,7 @@ private final class CameraScreenComponent: CombinedComponent { } })) self.isTransitioning = true - Queue.mainQueue().after(0.8, { + Queue.mainQueue().after(1.25, { self.isTransitioning = false self.updated(transition: .immediate) }) @@ -469,6 +480,14 @@ private final class CameraScreenComponent: CombinedComponent { let smallPanelWidth = min(component.panelWidth, 88.0) let panelWidth = min(component.panelWidth, 185.0) + var controlsBottomInset: CGFloat = 0.0 + if !isTablet { + let previewHeight = floorToScreenPixels(availableSize.width * 1.77778) + if availableSize.height < previewHeight + 30.0 { + controlsBottomInset = -48.0 + } + } + let topControlInset: CGFloat = 20.0 if case .none = state.cameraState.recording, !state.isTransitioning { let cancelButton = cancelButton.update( @@ -493,7 +512,7 @@ private final class CameraScreenComponent: CombinedComponent { transition: .immediate ) context.add(cancelButton - .position(CGPoint(x: isTablet ? smallPanelWidth / 2.0 : topControlInset + cancelButton.size.width / 2.0, y: environment.safeInsets.top + topControlInset + cancelButton.size.height / 2.0)) + .position(CGPoint(x: isTablet ? smallPanelWidth / 2.0 : topControlInset + cancelButton.size.width / 2.0, y: max(environment.statusBarHeight + 5.0, environment.safeInsets.top + topControlInset) + cancelButton.size.height / 2.0)) .appear(.default(scale: true)) .disappear(.default(scale: true)) ) @@ -553,7 +572,7 @@ private final class CameraScreenComponent: CombinedComponent { transition: .immediate ) context.add(flashButton - .position(CGPoint(x: isTablet ? availableSize.width - smallPanelWidth / 2.0 : availableSize.width - topControlInset - flashButton.size.width / 2.0 - 5.0, y: environment.safeInsets.top + topControlInset + flashButton.size.height / 2.0)) + .position(CGPoint(x: isTablet ? availableSize.width - smallPanelWidth / 2.0 : availableSize.width - topControlInset - flashButton.size.width / 2.0 - 5.0, y: max(environment.statusBarHeight + 5.0, environment.safeInsets.top + topControlInset) + flashButton.size.height / 2.0)) .appear(.default(scale: true)) .disappear(.default(scale: true)) ) @@ -578,7 +597,7 @@ private final class CameraScreenComponent: CombinedComponent { transition: .immediate ) context.add(dualButton - .position(CGPoint(x: availableSize.width - topControlInset - flashButton.size.width / 2.0 - 52.0, y: environment.safeInsets.top + topControlInset + dualButton.size.height / 2.0 + 1.0)) + .position(CGPoint(x: availableSize.width - topControlInset - flashButton.size.width / 2.0 - 52.0, y: max(environment.statusBarHeight + 5.0, environment.safeInsets.top + topControlInset) + dualButton.size.height / 2.0 + 1.0)) .appear(.default(scale: true)) .disappear(.default(scale: true)) ) @@ -702,7 +721,7 @@ private final class CameraScreenComponent: CombinedComponent { if isTablet { captureControlsPosition = CGPoint(x: availableSize.width - panelWidth / 2.0, y: availableSize.height / 2.0) } else { - captureControlsPosition = CGPoint(x: availableSize.width / 2.0, y: availableSize.height - captureControls.size.height / 2.0 - environment.safeInsets.bottom - 5.0) + captureControlsPosition = CGPoint(x: availableSize.width / 2.0, y: availableSize.height - captureControls.size.height / 2.0 - environment.safeInsets.bottom - 5.0 + floorToScreenPixels(controlsBottomInset * 0.66)) } context.add(captureControls .position(captureControlsPosition) @@ -834,7 +853,7 @@ private final class CameraScreenComponent: CombinedComponent { if isTablet { modeControlPosition = CGPoint(x: availableSize.width - panelWidth / 2.0, y: availableSize.height / 2.0 + modeControl.size.height + 26.0) } else { - modeControlPosition = CGPoint(x: availableSize.width / 2.0, y: availableSize.height - environment.safeInsets.bottom + modeControl.size.height / 2.0) + modeControlPosition = CGPoint(x: availableSize.width / 2.0, y: availableSize.height - environment.safeInsets.bottom + modeControl.size.height / 2.0 + controlsBottomInset) } context.add(modeControl .clipsToBounds(true) @@ -1289,7 +1308,7 @@ public class CameraScreen: ViewController { gestureRecognizer.isEnabled = true } } - case .ended: + case .ended, .cancelled: let velocity = gestureRecognizer.velocity(in: self.view) let transitionFraction = 1.0 - max(0.0, translation.x * -1.0) / self.frame.width controller.completeWithTransitionProgress(transitionFraction, velocity: abs(velocity.x), dismissing: true) @@ -1658,13 +1677,16 @@ public class CameraScreen: ViewController { isTablet = false } + var topInset: CGFloat = (layout.statusBarHeight ?? 0.0) + 5.0 let previewSize: CGSize if isTablet { previewSize = CGSize(width: floorToScreenPixels(layout.size.height / 1.77778), height: layout.size.height) } else { previewSize = CGSize(width: layout.size.width, height: floorToScreenPixels(layout.size.width * 1.77778)) + if layout.size.height < previewSize.height + 30.0 { + topInset = 0.0 + } } - let topInset: CGFloat = (layout.statusBarHeight ?? 0.0) + 5.0 let bottomInset = layout.size.height - previewSize.height - topInset let panelWidth: CGFloat @@ -1936,10 +1958,6 @@ public class CameraScreen: ViewController { self.navigationPresentation = .flatModal self.requestAudioSession() - - if #available(iOS 13.0, *) { - try? AVAudioSession.sharedInstance().setAllowHapticsAndSystemSoundsDuringRecording(true) - } } required public init(coder: NSCoder) { @@ -1960,7 +1978,11 @@ public class CameraScreen: ViewController { } private func requestAudioSession() { - self.audioSessionDisposable = self.context.sharedContext.mediaManager.audioSession.push(audioSessionType: .recordWithOthers, activate: { _ in }, deactivate: { _ in + self.audioSessionDisposable = self.context.sharedContext.mediaManager.audioSession.push(audioSessionType: .recordWithOthers, activate: { _ in + if #available(iOS 13.0, *) { + try? AVAudioSession.sharedInstance().setAllowHapticsAndSystemSoundsDuringRecording(true) + } + }, deactivate: { _ in return .single(Void()) }) } diff --git a/submodules/TelegramUI/Components/ForumCreateTopicScreen/BUILD b/submodules/TelegramUI/Components/ForumCreateTopicScreen/BUILD index d00b21fc02..bb370549e6 100644 --- a/submodules/TelegramUI/Components/ForumCreateTopicScreen/BUILD +++ b/submodules/TelegramUI/Components/ForumCreateTopicScreen/BUILD @@ -32,6 +32,7 @@ swift_library( "//submodules/Components/PagerComponent:PagerComponent", "//submodules/PremiumUI", "//submodules/ProgressNavigationButtonNode", + "//submodules/TelegramUI/Components/SwitchComponent", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/ForumCreateTopicScreen/Sources/ForumCreateTopicScreen.swift b/submodules/TelegramUI/Components/ForumCreateTopicScreen/Sources/ForumCreateTopicScreen.swift index 508ae2bda0..6a2be5e358 100644 --- a/submodules/TelegramUI/Components/ForumCreateTopicScreen/Sources/ForumCreateTopicScreen.swift +++ b/submodules/TelegramUI/Components/ForumCreateTopicScreen/Sources/ForumCreateTopicScreen.swift @@ -16,72 +16,7 @@ import EmojiStatusComponent import PremiumUI import ProgressNavigationButtonNode import Postbox - -private final class SwitchComponent: Component { - typealias EnvironmentType = Empty - - let value: Bool - let valueUpdated: (Bool) -> Void - - init( - value: Bool, - valueUpdated: @escaping (Bool) -> Void - ) { - self.value = value - self.valueUpdated = valueUpdated - } - - static func ==(lhs: SwitchComponent, rhs: SwitchComponent) -> Bool { - if lhs.value != rhs.value { - return false - } - return true - } - - final class View: UIView { - private let switchView: UISwitch - - private var component: SwitchComponent? - - override init(frame: CGRect) { - self.switchView = UISwitch() - - super.init(frame: frame) - - self.addSubview(self.switchView) - - self.switchView.addTarget(self, action: #selector(self.valueChanged(_:)), for: .valueChanged) - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - @objc func valueChanged(_ sender: Any) { - self.component?.valueUpdated(self.switchView.isOn) - } - - func update(component: SwitchComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { - self.component = component - - self.switchView.setOn(component.value, animated: !transition.animation.isImmediate) - - self.switchView.sizeToFit() - self.switchView.frame = CGRect(origin: .zero, size: self.switchView.frame.size) - - return self.switchView.frame.size - } - } - - public func makeView() -> View { - return View(frame: CGRect()) - } - - public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { - return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) - } -} - +import SwitchComponent private final class TitleFieldComponent: Component { typealias EnvironmentType = Empty diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorDraft.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorDraft.swift index 9c65dcf5a6..c2ed9ea2b2 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorDraft.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorDraft.swift @@ -9,25 +9,27 @@ import AccountContext public struct MediaEditorResultPrivacy: Codable, Equatable { private enum CodingKeys: String, CodingKey { - case type case privacy - case peers case timeout + case disableForwarding case archive } public let privacy: EngineStoryPrivacy public let timeout: Int - public let archive: Bool + public let isForwardingDisabled: Bool + public let pin: Bool public init( privacy: EngineStoryPrivacy, timeout: Int, - archive: Bool + isForwardingDisabled: Bool, + pin: Bool ) { self.privacy = privacy self.timeout = timeout - self.archive = archive + self.isForwardingDisabled = isForwardingDisabled + self.pin = pin } public init(from decoder: Decoder) throws { @@ -35,7 +37,8 @@ public struct MediaEditorResultPrivacy: Codable, Equatable { self.privacy = try container.decode(EngineStoryPrivacy.self, forKey: .privacy) self.timeout = Int(try container.decode(Int32.self, forKey: .timeout)) - self.archive = try container.decode(Bool.self, forKey: .archive) + self.isForwardingDisabled = try container.decodeIfPresent(Bool.self, forKey: .disableForwarding) ?? false + self.pin = try container.decode(Bool.self, forKey: .archive) } public func encode(to encoder: Encoder) throws { @@ -43,7 +46,8 @@ public struct MediaEditorResultPrivacy: Codable, Equatable { try container.encode(self.privacy, forKey: .privacy) try container.encode(Int32(self.timeout), forKey: .timeout) - try container.encode(self.archive, forKey: .archive) + try container.encode(self.isForwardingDisabled, forKey: .disableForwarding) + try container.encode(self.pin, forKey: .archive) } } diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorRenderer.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorRenderer.swift index d1b9e79cd7..5b120f5477 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorRenderer.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorRenderer.swift @@ -304,7 +304,10 @@ final class MediaEditorRenderer: TextureConsumer { self.willRenderFrame() self.currentPixelBuffer = pixelBuffer - self.currentAdditionalPixelBuffer = additionalPixelBuffer + if additionalPixelBuffer == nil && self.currentAdditionalPixelBuffer != nil { + } else { + self.currentAdditionalPixelBuffer = additionalPixelBuffer + } if render { if self.previousPresentationTimestamp == pixelBuffer.timestamp { self.didRenderFrame() diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorVideoExport.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorVideoExport.swift index 56318cd4dc..87ccdacd09 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorVideoExport.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorVideoExport.swift @@ -518,7 +518,7 @@ public final class MediaEditorVideoExport { let exportDuration = end - self.startTimestamp print("video processing took \(exportDuration)s") if duration.seconds > 0 { - print("\(exportDuration / duration.seconds) speed") + Logger.shared.log("VideoExport", "Video processing took \(exportDuration / duration.seconds)") } }) } @@ -534,7 +534,7 @@ public final class MediaEditorVideoExport { let duration: Double = 5.0 let frameRate: Double = Double(self.configuration.frameRate) - var position: CMTime = CMTime(value: 0, timescale: Int32(self.configuration.frameRate)) + var position: CMTime = CMTime(value: 0, timescale: Int32(frameRate)) var appendFailed = false while writer.isReadyForMoreVideoData { @@ -542,6 +542,7 @@ public final class MediaEditorVideoExport { return false } if writer.status != .writing { + Logger.shared.log("VideoExport", "Video finished") writer.markVideoAsFinished() return false } @@ -552,9 +553,12 @@ public final class MediaEditorVideoExport { composer.processImage(inputImage: image, pool: writer.pixelBufferPool, time: position, completion: { pixelBuffer, timestamp in if let pixelBuffer { if !writer.appendPixelBuffer(pixelBuffer, at: timestamp) { + Logger.shared.log("VideoExport", "Failed to append pixelbuffer") writer.markVideoAsFinished() appendFailed = true } + } else { + Logger.shared.log("VideoExport", "No pixelbuffer from composer") } Thread.sleep(forTimeInterval: 0.001) self.semaphore.signal() @@ -563,6 +567,7 @@ public final class MediaEditorVideoExport { position = position + CMTime(value: 1, timescale: Int32(frameRate)) if position.seconds >= duration { + Logger.shared.log("VideoExport", "Video finished") writer.markVideoAsFinished() return false } diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/VideoTextureSource.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/VideoTextureSource.swift index f8eca7ee51..fb017e0585 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/VideoTextureSource.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/VideoTextureSource.swift @@ -378,7 +378,7 @@ private func verticesData( pos: simd_float4(x: Float(rect.maxX) * 2.0, y: Float(rect.maxY) * 2.0, z: z, w: 1), texCoord: bottomRight, localPos: simd_float2(1.0, 1.0) - ), + ) ] } diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift index ec9e7cffb7..8192a29b2e 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift @@ -562,6 +562,10 @@ final class MediaEditorScreenComponent: Component { view.alpha = 1.0 } + if let buttonView = self.cancelButton.view as? Button.View, let view = buttonView.content as? LottieAnimationComponent.View { + view.playOnce() + } + let buttons = [ self.drawButton, self.textButton, @@ -637,8 +641,9 @@ final class MediaEditorScreenComponent: Component { let buttonSideInset: CGFloat let buttonBottomInset: CGFloat = 8.0 + var controlsBottomInset: CGFloat = 0.0 let previewSize: CGSize - let topInset: CGFloat = environment.statusBarHeight + 5.0 + var topInset: CGFloat = environment.statusBarHeight + 5.0 if isTablet { let previewHeight = availableSize.height - topInset - 75.0 previewSize = CGSize(width: floorToScreenPixels(previewHeight / 1.77778), height: previewHeight) @@ -646,6 +651,10 @@ final class MediaEditorScreenComponent: Component { } else { previewSize = CGSize(width: availableSize.width, height: floorToScreenPixels(availableSize.width * 1.77778)) buttonSideInset = 10.0 + if availableSize.height < previewSize.height + 30.0 { + topInset = 0.0 + controlsBottomInset = -50.0 + } } let cancelButtonSize = self.cancelButton.update( @@ -655,8 +664,8 @@ final class MediaEditorScreenComponent: Component { LottieAnimationComponent( animation: LottieAnimationComponent.AnimationItem( name: "media_backToCancel", - mode: .still(position: .begin), - range: nil + mode: .still(position: .end), + range: (0.5, 1.0) ), colors: ["__allcolors__": .white], size: CGSize(width: 33.0, height: 33.0) @@ -673,7 +682,7 @@ final class MediaEditorScreenComponent: Component { containerSize: CGSize(width: 44.0, height: 44.0) ) let cancelButtonFrame = CGRect( - origin: CGPoint(x: buttonSideInset, y: availableSize.height - environment.safeInsets.bottom + buttonBottomInset), + origin: CGPoint(x: buttonSideInset, y: availableSize.height - environment.safeInsets.bottom + buttonBottomInset + controlsBottomInset), size: cancelButtonSize ) if let cancelButtonView = self.cancelButton.view { @@ -688,24 +697,28 @@ final class MediaEditorScreenComponent: Component { let doneButtonSize = self.doneButton.update( transition: transition, component: AnyComponent(Button( - content: AnyComponent(Image( - image: state.image(.done), - size: CGSize(width: 33.0, height: 33.0) - )), + content: AnyComponent(DoneButtonComponent( + backgroundColor: UIColor(rgb: 0x007aff), + icon: UIImage(bundleImageName: "Media Editor/Next")!, + title: "NEXT")), action: { guard let controller = environment.controller() as? MediaEditorScreen else { return } - controller.openPrivacySettings(completion: { [weak controller] in - controller?.requestCompletion(animated: true) - }) + if controller.isEditingStory { + controller.requestCompletion(animated: true) + } else { + controller.openPrivacySettings(completion: { [weak controller] in + controller?.requestCompletion(animated: true) + }) + } } )), environment: {}, - containerSize: CGSize(width: 44.0, height: 44.0) + containerSize: CGSize(width: availableSize.width, height: 44.0) ) let doneButtonFrame = CGRect( - origin: CGPoint(x: availableSize.width - buttonSideInset - doneButtonSize.width, y: availableSize.height - environment.safeInsets.bottom + buttonBottomInset), + origin: CGPoint(x: availableSize.width - buttonSideInset - doneButtonSize.width, y: availableSize.height - environment.safeInsets.bottom + buttonBottomInset + controlsBottomInset), size: doneButtonSize ) if let doneButtonView = self.doneButton.view { @@ -723,8 +736,8 @@ final class MediaEditorScreenComponent: Component { buttonsAvailableWidth = previewSize.width + 260.0 buttonsLeftOffset = floorToScreenPixels((availableSize.width - buttonsAvailableWidth) / 2.0) } else { - buttonsAvailableWidth = availableSize.width - buttonsLeftOffset = 0.0 + buttonsAvailableWidth = floor(availableSize.width - cancelButtonSize.width * 0.66 - (doneButtonSize.width - cancelButtonSize.width * 0.33) - buttonSideInset * 2.0) + buttonsLeftOffset = floorToScreenPixels(buttonSideInset + cancelButtonSize.width * 0.66) } let drawButtonSize = self.drawButton.update( @@ -742,7 +755,7 @@ final class MediaEditorScreenComponent: Component { containerSize: CGSize(width: 40.0, height: 40.0) ) let drawButtonFrame = CGRect( - origin: CGPoint(x: buttonsLeftOffset + floorToScreenPixels(buttonsAvailableWidth / 4.0 - 3.0 - drawButtonSize.width / 2.0), y: availableSize.height - environment.safeInsets.bottom + buttonBottomInset + 1.0), + origin: CGPoint(x: buttonsLeftOffset + floorToScreenPixels(buttonsAvailableWidth / 5.0 - drawButtonSize.width / 2.0 - 3.0), y: availableSize.height - environment.safeInsets.bottom + buttonBottomInset + controlsBottomInset + 1.0), size: drawButtonSize ) if let drawButtonView = self.drawButton.view { @@ -771,7 +784,7 @@ final class MediaEditorScreenComponent: Component { containerSize: CGSize(width: 40.0, height: 40.0) ) let textButtonFrame = CGRect( - origin: CGPoint(x: buttonsLeftOffset + floorToScreenPixels(buttonsAvailableWidth / 2.5 + 5.0 - textButtonSize.width / 2.0), y: availableSize.height - environment.safeInsets.bottom + buttonBottomInset + 1.0), + origin: CGPoint(x: buttonsLeftOffset + floorToScreenPixels(buttonsAvailableWidth / 5.0 * 2.0 - textButtonSize.width / 2.0), y: availableSize.height - environment.safeInsets.bottom + buttonBottomInset + controlsBottomInset + 2.0), size: textButtonSize ) if let textButtonView = self.textButton.view { @@ -800,7 +813,7 @@ final class MediaEditorScreenComponent: Component { containerSize: CGSize(width: 40.0, height: 40.0) ) let stickerButtonFrame = CGRect( - origin: CGPoint(x: floorToScreenPixels(availableSize.width - buttonsLeftOffset - buttonsAvailableWidth / 2.5 - 5.0 - stickerButtonSize.width / 2.0), y: availableSize.height - environment.safeInsets.bottom + buttonBottomInset + 1.0), + origin: CGPoint(x: buttonsLeftOffset + floorToScreenPixels(buttonsAvailableWidth / 5.0 * 3.0 - stickerButtonSize.width / 2.0), y: availableSize.height - environment.safeInsets.bottom + buttonBottomInset + controlsBottomInset + 2.0), size: stickerButtonSize ) if let stickerButtonView = self.stickerButton.view { @@ -829,7 +842,7 @@ final class MediaEditorScreenComponent: Component { containerSize: CGSize(width: 40.0, height: 40.0) ) let toolsButtonFrame = CGRect( - origin: CGPoint(x: buttonsLeftOffset + floorToScreenPixels(buttonsAvailableWidth / 4.0 * 3.0 + 3.0 - toolsButtonSize.width / 2.0), y: availableSize.height - environment.safeInsets.bottom + buttonBottomInset + 1.0), + origin: CGPoint(x: buttonsLeftOffset + floorToScreenPixels(buttonsAvailableWidth / 5.0 * 4.0 - toolsButtonSize.width / 2.0 + 3.0), y: availableSize.height - environment.safeInsets.bottom + buttonBottomInset + controlsBottomInset + 1.0), size: toolsButtonSize ) if let toolsButtonView = self.toolsButton.view { @@ -884,10 +897,14 @@ final class MediaEditorScreenComponent: Component { containerSize: CGSize(width: availableSize.width - scrubberInset * 2.0, height: availableSize.height) ) - let scrubberFrame = CGRect(origin: CGPoint(x: scrubberInset, y: availableSize.height - environment.safeInsets.bottom - scrubberSize.height - 8.0), size: scrubberSize) + let scrubberFrame = CGRect(origin: CGPoint(x: scrubberInset, y: availableSize.height - environment.safeInsets.bottom - scrubberSize.height - 8.0 + controlsBottomInset), size: scrubberSize) if let scrubberView = self.scrubber.view { if scrubberView.superview == nil { - self.addSubview(scrubberView) + if let inputPanelBackgroundView = self.inputPanelBackground.view, inputPanelBackgroundView.superview != nil { + self.insertSubview(scrubberView, belowSubview: inputPanelBackgroundView) + } else { + self.addSubview(scrubberView) + } } transition.setFrame(view: scrubberView, frame: scrubberFrame) if !self.animatingButtons { @@ -914,9 +931,6 @@ final class MediaEditorScreenComponent: Component { default: timeoutValue = "24" } - if component.privacy.archive { - timeoutValue = "∞" - } timeoutSelected = false var inputPanelAvailableWidth = previewSize.width @@ -1075,7 +1089,7 @@ final class MediaEditorScreenComponent: Component { sizeValue = textEntity.fontSize } - var inputPanelBottomInset: CGFloat = scrubberBottomInset + var inputPanelBottomInset: CGFloat = scrubberBottomInset - controlsBottomInset if inputHeight > 0.0 { inputPanelBottomInset = inputHeight - environment.safeInsets.bottom } @@ -1197,7 +1211,7 @@ final class MediaEditorScreenComponent: Component { containerSize: CGSize(width: 44.0, height: 44.0) ) let saveButtonFrame = CGRect( - origin: CGPoint(x: availableSize.width - 20.0 - saveButtonSize.width, y: environment.safeInsets.top + 20.0), + origin: CGPoint(x: availableSize.width - 20.0 - saveButtonSize.width, y: max(environment.statusBarHeight + 10.0, environment.safeInsets.top + 20.0)), size: saveButtonSize ) if let saveButtonView = self.saveButton.view { @@ -1218,117 +1232,91 @@ final class MediaEditorScreenComponent: Component { transition.setAlpha(view: saveButtonView, alpha: displayTopButtons && !component.isDismissing && !component.isInteractingWithEntities ? saveButtonAlpha : 0.0) } - if let playerState = state.playerState, playerState.hasAudio { - let isVideoMuted = mediaEditor?.values.videoIsMuted ?? false - - let muteContentComponent: AnyComponentWithIdentity - if component.hasAppeared { - muteContentComponent = AnyComponentWithIdentity( - id: "animatedIcon", - component: AnyComponent( - LottieAnimationComponent( - animation: LottieAnimationComponent.AnimationItem( - name: "anim_storymute", - mode: state.muteDidChange ? .animating(loop: false) : .still(position: .begin), - range: isVideoMuted ? (0.0, 0.5) : (0.5, 1.0) - ), - colors: ["__allcolors__": .white], - size: CGSize(width: 30.0, height: 30.0) - ).tagged(muteButtonTag) - ) - ) - } else { - muteContentComponent = AnyComponentWithIdentity( - id: "staticIcon", - component: AnyComponent( - BundleIconComponent( - name: "Media Editor/MuteIcon", - tintColor: nil + if let playerState = state.playerState { + if playerState.hasAudio { + let isVideoMuted = mediaEditor?.values.videoIsMuted ?? false + + let muteContentComponent: AnyComponentWithIdentity + if component.hasAppeared { + muteContentComponent = AnyComponentWithIdentity( + id: "animatedIcon", + component: AnyComponent( + LottieAnimationComponent( + animation: LottieAnimationComponent.AnimationItem( + name: "anim_storymute", + mode: state.muteDidChange ? .animating(loop: false) : .still(position: .begin), + range: isVideoMuted ? (0.0, 0.5) : (0.5, 1.0) + ), + colors: ["__allcolors__": .white], + size: CGSize(width: 30.0, height: 30.0) + ).tagged(muteButtonTag) ) ) - ) - } - - let muteButtonSize = self.muteButton.update( - transition: transition, - component: AnyComponent(CameraButton( - content: muteContentComponent, - action: { [weak state, weak mediaEditor] in - if let mediaEditor { - state?.muteDidChange = true - let isMuted = !mediaEditor.values.videoIsMuted - mediaEditor.setVideoIsMuted(isMuted) - state?.updated() - - if let controller = environment.controller() as? MediaEditorScreen { - controller.node.presentMutedTooltip() + } else { + muteContentComponent = AnyComponentWithIdentity( + id: "staticIcon", + component: AnyComponent( + BundleIconComponent( + name: "Media Editor/MuteIcon", + tintColor: nil + ) + ) + ) + } + + let muteButtonSize = self.muteButton.update( + transition: transition, + component: AnyComponent(CameraButton( + content: muteContentComponent, + action: { [weak state, weak mediaEditor] in + if let mediaEditor { + state?.muteDidChange = true + let isMuted = !mediaEditor.values.videoIsMuted + mediaEditor.setVideoIsMuted(isMuted) + state?.updated() + + if let controller = environment.controller() as? MediaEditorScreen { + controller.node.presentMutedTooltip() + } } } - } - )), - environment: {}, - containerSize: CGSize(width: 44.0, height: 44.0) - ) - let muteButtonFrame = CGRect( - origin: CGPoint(x: availableSize.width - 20.0 - muteButtonSize.width - 50.0, y: environment.safeInsets.top + 20.0), - size: muteButtonSize - ) - if let muteButtonView = self.muteButton.view { - if muteButtonView.superview == nil { - muteButtonView.layer.shadowOffset = CGSize(width: 0.0, height: 0.0) - muteButtonView.layer.shadowRadius = 2.0 - muteButtonView.layer.shadowColor = UIColor.black.cgColor - muteButtonView.layer.shadowOpacity = 0.35 - self.addSubview(muteButtonView) - - if self.animatingButtons { - muteButtonView.layer.animateAlpha(from: 0.0, to: muteButtonView.alpha, duration: 0.1) - muteButtonView.layer.animateScale(from: 0.4, to: 1.0, duration: 0.1) - } - } - transition.setPosition(view: muteButtonView, position: muteButtonFrame.center) - transition.setBounds(view: muteButtonView, bounds: CGRect(origin: .zero, size: muteButtonFrame.size)) - transition.setScale(view: muteButtonView, scale: displayTopButtons ? 1.0 : 0.01) - transition.setAlpha(view: muteButtonView, alpha: displayTopButtons && !component.isDismissing && !component.isInteractingWithEntities ? 1.0 : 0.0) - } - } - - if let _ = state.playerState { - let settingsButtonSize = self.settingsButton.update( - transition: transition, - component: AnyComponent(Button( - content: AnyComponent( - BundleIconComponent( - name: "Chat/Input/Media/EntityInputSettingsIcon", - tintColor: UIColor(rgb: 0xffffff) - ) - ), - action: { - if let controller = environment.controller() as? MediaEditorScreen { - controller.requestSettings() + )), + environment: {}, + containerSize: CGSize(width: 44.0, height: 44.0) + ) + let muteButtonFrame = CGRect( + origin: CGPoint(x: availableSize.width - 20.0 - muteButtonSize.width - 50.0, y: max(environment.statusBarHeight + 10.0, environment.safeInsets.top + 20.0)), + size: muteButtonSize + ) + if let muteButtonView = self.muteButton.view { + if muteButtonView.superview == nil { + muteButtonView.layer.shadowOffset = CGSize(width: 0.0, height: 0.0) + muteButtonView.layer.shadowRadius = 2.0 + muteButtonView.layer.shadowColor = UIColor.black.cgColor + muteButtonView.layer.shadowOpacity = 0.35 + self.addSubview(muteButtonView) + + if self.animatingButtons { + muteButtonView.layer.animateAlpha(from: 0.0, to: muteButtonView.alpha, duration: 0.1) + muteButtonView.layer.animateScale(from: 0.4, to: 1.0, duration: 0.1) } } - )), - environment: {}, - containerSize: CGSize(width: 44.0, height: 44.0) - ) - let settingsButtonFrame = CGRect( - origin: CGPoint(x: floorToScreenPixels((availableSize.width - settingsButtonSize.width) / 2.0), y: environment.safeInsets.top + 20.0), - size: settingsButtonSize - ) - if let settingsButtonView = self.settingsButton.view { - if settingsButtonView.superview == nil { - settingsButtonView.layer.shadowOffset = CGSize(width: 0.0, height: 0.0) - settingsButtonView.layer.shadowRadius = 2.0 - settingsButtonView.layer.shadowColor = UIColor.black.cgColor - settingsButtonView.layer.shadowOpacity = 0.35 - //self.addSubview(settingsButtonView) + transition.setPosition(view: muteButtonView, position: muteButtonFrame.center) + transition.setBounds(view: muteButtonView, bounds: CGRect(origin: .zero, size: muteButtonFrame.size)) + transition.setScale(view: muteButtonView, scale: displayTopButtons ? 1.0 : 0.01) + transition.setAlpha(view: muteButtonView, alpha: displayTopButtons && !component.isDismissing && !component.isInteractingWithEntities ? 1.0 : 0.0) } - transition.setPosition(view: settingsButtonView, position: settingsButtonFrame.center) - transition.setBounds(view: settingsButtonView, bounds: CGRect(origin: .zero, size: settingsButtonFrame.size)) - transition.setScale(view: settingsButtonView, scale: displayTopButtons ? 1.0 : 0.01) - transition.setAlpha(view: settingsButtonView, alpha: displayTopButtons && !component.isDismissing && !component.isInteractingWithEntities ? 1.0 : 0.0) + } else if let muteButtonView = self.muteButton.view, muteButtonView.superview != nil { + muteButtonView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak muteButtonView] _ in + muteButtonView?.removeFromSuperview() + }) + muteButtonView.layer.animateScale(from: 1.0, to: 0.01, duration: 0.2, removeOnCompletion: false) } + } else if let muteButtonView = self.muteButton.view, muteButtonView.superview != nil { + muteButtonView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak muteButtonView] _ in + muteButtonView?.removeFromSuperview() + }) + muteButtonView.layer.animateScale(from: 1.0, to: 0.01, duration: 0.2, removeOnCompletion: false) } let textCancelButtonSize = self.textCancelButton.update( @@ -1347,7 +1335,7 @@ final class MediaEditorScreenComponent: Component { containerSize: CGSize(width: 100.0, height: 30.0) ) let textCancelButtonFrame = CGRect( - origin: CGPoint(x: 13.0, y: environment.statusBarHeight + 20.0), + origin: CGPoint(x: 13.0, y: max(environment.statusBarHeight + 10.0, environment.safeInsets.top + 20.0)), size: textCancelButtonSize ) if let textCancelButtonView = self.textCancelButton.view { @@ -1376,7 +1364,7 @@ final class MediaEditorScreenComponent: Component { containerSize: CGSize(width: 100.0, height: 30.0) ) let textDoneButtonFrame = CGRect( - origin: CGPoint(x: availableSize.width - textDoneButtonSize.width - 13.0, y: environment.statusBarHeight + 20.0), + origin: CGPoint(x: availableSize.width - textDoneButtonSize.width - 13.0, y: max(environment.statusBarHeight + 10.0, environment.safeInsets.top + 20.0)), size: textDoneButtonSize ) if let textDoneButtonView = self.textDoneButton.view { @@ -1405,9 +1393,10 @@ final class MediaEditorScreenComponent: Component { environment: {}, containerSize: CGSize(width: 30.0, height: 240.0) ) + let textSizeTopInset = max(environment.safeInsets.top, environment.statusBarHeight) let bottomInset: CGFloat = inputHeight > 0.0 ? inputHeight : environment.safeInsets.bottom let textSizeFrame = CGRect( - origin: CGPoint(x: 0.0, y: environment.safeInsets.top + (availableSize.height - environment.safeInsets.top - bottomInset) / 2.0 - textSizeSize.height / 2.0), + origin: CGPoint(x: 0.0, y: textSizeTopInset + (availableSize.height - textSizeTopInset - bottomInset) / 2.0 - textSizeSize.height / 2.0), size: textSizeSize ) if let textSizeView = self.textSize.view { @@ -1546,7 +1535,12 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate } struct State { - var privacy: MediaEditorResultPrivacy = MediaEditorResultPrivacy(privacy: EngineStoryPrivacy(base: .everyone, additionallyIncludePeers: []), timeout: 86400, archive: false) + var privacy: MediaEditorResultPrivacy = MediaEditorResultPrivacy( + privacy: EngineStoryPrivacy(base: .everyone, additionallyIncludePeers: []), + timeout: 86400, + isForwardingDisabled: false, + pin: false + ) } var state = State() { @@ -1862,8 +1856,9 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate } } } - } else if case let .asset(asset) = subject, asset.mediaType == .video { + } //#if DEBUG +// if case let .asset(asset) = subject, asset.mediaType == .video { // let videoEntity = DrawingStickerEntity(content: .dualVideoReference) // videoEntity.referenceDrawingSize = storyDimensions // videoEntity.scale = 1.49 @@ -1879,8 +1874,8 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate // } // } // } +// } //#endif - } self.gradientColorsDisposable = mediaEditor.gradientColors.start(next: { [weak self] colors in if let self, let colors { @@ -1898,6 +1893,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate self.previewContainerView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25, completion: { _ in self.previewContainerView.layer.allowsGroupOpacity = false self.previewContainerView.alpha = 1.0 + self.backgroundDimView.isHidden = false }) } else { self.backgroundDimView.isHidden = false @@ -1942,63 +1938,8 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate } } } - - if case .video = subject { - self.playbackPositionDisposable = (mediaEditor.position - |> deliverOnMainQueue).start(next: { [weak self] position in - if let self { - self.updateVideoPlaybackPosition(position: position) - } - }) - } } - - private var additionalIsMainstage = false - private func updateVideoPlaybackPosition(position: CGFloat) { - guard let subject = self.subject, case let .video(_, _, _, _, _, _, _, timestamps, _) = subject, !timestamps.isEmpty else { - return - } - var currentIsFront = false - for (isFront, timestamp) in timestamps { - if position < timestamp { - break - } - currentIsFront = isFront - } - - self.additionalIsMainstage = currentIsFront - self.updateMainStageVideo() - } - - private func updateMainStageVideo() { - guard let mainEntityView = self.entitiesView.getView(where: { $0 is DrawingMediaEntityView }) as? DrawingMediaEntityView, let mainEntity = mainEntityView.entity as? DrawingMediaEntity else { - return - } - - let additionalEntityView = self.entitiesView.getView(where: { view in - if let stickerEntity = view.entity as? DrawingStickerEntity, case .video = stickerEntity.content { - return true - } else { - return false - } - }) as? DrawingStickerEntityView - - var animated = true - if mainEntity.scale != 1.0 || mainEntity.rotation != 0.0 || mainEntity.position != CGPoint(x: storyDimensions.width / 2.0, y: storyDimensions.height / 2.0) { - animated = false - } - - let _ = animated - - if self.additionalIsMainstage { - mainEntityView.additionalView = additionalEntityView?.videoView - additionalEntityView?.mainView = mainEntityView.previewView - } else { - mainEntityView.additionalView = nil - additionalEntityView?.mainView = nil - } - } - + override func didLoad() { super.didLoad() @@ -2641,7 +2582,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate let location = CGRect(origin: CGPoint(x: absoluteFrame.midX, y: absoluteFrame.minY - 5.0), size: CGSize()) let text: String - if controller.state.privacy.archive { + if controller.state.privacy.pin { text = "Story will be kept on your page." } else { text = "Story will disappear in 24 hours." @@ -2703,15 +2644,18 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate isTablet = false } - let topInset: CGFloat = (layout.statusBarHeight ?? 0.0) + 5.0 + var topInset: CGFloat = (layout.statusBarHeight ?? 0.0) + 5.0 let previewSize: CGSize if isTablet { let previewHeight = layout.size.height - topInset - 75.0 previewSize = CGSize(width: floorToScreenPixels(previewHeight / 1.77778), height: previewHeight) } else { previewSize = CGSize(width: layout.size.width, height: floorToScreenPixels(layout.size.width * 1.77778)) + if layout.size.height < previewSize.height + 30.0 { + topInset = 0.0 + } } - let bottomInset = layout.size.height - previewSize.height - topInset + let bottomInset = max(0.0, layout.size.height - previewSize.height - topInset) var layoutInputHeight = layout.inputHeight ?? 0.0 if self.stickerScreen != nil { @@ -3026,6 +2970,10 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate } } + var isPhoto: Bool { + return !self.isVideo + } + var isVideo: Bool { switch self { case .image: @@ -3103,7 +3051,12 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate if isEditing { if let initialPrivacy { - self.state.privacy = MediaEditorResultPrivacy(privacy: initialPrivacy, timeout: 86400, archive: false) + self.state.privacy = MediaEditorResultPrivacy( + privacy: initialPrivacy, + timeout: 86400, + isForwardingDisabled: false, + pin: false + ) } } else { let _ = combineLatest( @@ -3113,7 +3066,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate ).start(next: { [weak self] state, peer in if let self, var privacy = state?.privacy { if case let .user(user) = peer, !user.isPremium && privacy.timeout != 86400 { - privacy = MediaEditorResultPrivacy(privacy: privacy.privacy, timeout: 86400, archive: false) + privacy = MediaEditorResultPrivacy(privacy: privacy.privacy, timeout: 86400, isForwardingDisabled: privacy.isForwardingDisabled, pin: privacy.pin) } self.state.privacy = privacy } @@ -3143,36 +3096,42 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate let privacy = privacy ?? self.state.privacy - let stateContext = ShareWithPeersScreen.StateContext(context: self.context, subject: .stories, initialPeerIds: Set(privacy.privacy.additionallyIncludePeers)) + let stateContext = ShareWithPeersScreen.StateContext(context: self.context, subject: .stories(editing: false), initialPeerIds: Set(privacy.privacy.additionallyIncludePeers)) let _ = (stateContext.ready |> filter { $0 } |> take(1) |> deliverOnMainQueue).start(next: { [weak self] _ in guard let self else { return } let initialPrivacy = privacy.privacy let timeout = privacy.timeout - let archive = privacy.archive self.push( ShareWithPeersScreen( context: self.context, initialPrivacy: initialPrivacy, - timeout: timeout, + allowScreenshots: !privacy.isForwardingDisabled, + pin: privacy.pin, + timeout: privacy.timeout, stateContext: stateContext, - completion: { [weak self] privacy in + completion: { [weak self] privacy, allowScreenshots, pin in guard let self else { return } - self.state.privacy = MediaEditorResultPrivacy(privacy: privacy, timeout: timeout, archive: archive) + self.state.privacy = MediaEditorResultPrivacy(privacy: privacy, timeout: timeout, isForwardingDisabled: !allowScreenshots, pin: pin) completion() }, - editCategory: { [weak self] privacy in + editCategory: { [weak self] privacy, allowScreenshots, pin in guard let self else { return } - self.openEditCategory(privacy: privacy, completion: { [weak self] privacy in + self.openEditCategory(privacy: privacy, isForwardingDisabled: !allowScreenshots, pin: pin, completion: { [weak self] privacy in guard let self else { return } - self.openPrivacySettings(MediaEditorResultPrivacy(privacy: privacy, timeout: timeout, archive: archive), completion: completion) + self.openPrivacySettings(MediaEditorResultPrivacy( + privacy: privacy, + timeout: timeout, + isForwardingDisabled: !allowScreenshots, + pin: pin + ), completion: completion) }) } ) @@ -3180,7 +3139,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate }) } - private func openEditCategory(privacy: EngineStoryPrivacy, completion: @escaping (EngineStoryPrivacy) -> Void) { + private func openEditCategory(privacy: EngineStoryPrivacy, isForwardingDisabled: Bool, pin: Bool, completion: @escaping (EngineStoryPrivacy) -> Void) { let stateContext = ShareWithPeersScreen.StateContext(context: self.context, subject: .contacts(privacy.base), initialPeerIds: Set(privacy.additionallyIncludePeers)) let _ = (stateContext.ready |> filter { $0 } |> take(1) |> deliverOnMainQueue).start(next: { [weak self] _ in guard let self else { @@ -3191,9 +3150,10 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate ShareWithPeersScreen( context: self.context, initialPrivacy: privacy, - timeout: 0, + allowScreenshots: !isForwardingDisabled, + pin: pin, stateContext: stateContext, - completion: { [weak self] result in + completion: { [weak self] result, isForwardingDisabled, pin in guard let self else { return } @@ -3204,7 +3164,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate completion(result) } }, - editCategory: { _ in } + editCategory: { _, _, _ in } ) ) }) @@ -3215,16 +3175,16 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate var items: [ContextMenuItem] = [] - let updateTimeout: (Int?, Bool) -> Void = { [weak self] timeout, archive in + let updateTimeout: (Int?) -> Void = { [weak self] timeout in guard let self else { return } - self.state.privacy = MediaEditorResultPrivacy(privacy: self.state.privacy.privacy, timeout: timeout ?? 86400, archive: archive) + self.state.privacy = MediaEditorResultPrivacy(privacy: self.state.privacy.privacy, timeout: timeout ?? 86400, isForwardingDisabled: self.state.privacy.isForwardingDisabled, pin: self.state.privacy.pin) } let title = "Choose how long the story will be visible." let currentValue = self.state.privacy.timeout - let currentArchived = self.state.privacy.archive + let currentArchived = self.state.privacy.pin let emptyAction: ((ContextMenuActionItem.Action) -> Void)? = nil items.append(.action(ContextMenuActionItem(text: title, textLayout: .multiline, textFont: .small, icon: { _ in return nil }, action: emptyAction))) @@ -3239,7 +3199,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate a(.default) if hasPremium { - updateTimeout(3600 * 6, false) + updateTimeout(3600 * 6) } else { self?.presentTimeoutPremiumSuggestion(3600 * 6) } @@ -3254,7 +3214,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate a(.default) if hasPremium { - updateTimeout(3600 * 12, false) + updateTimeout(3600 * 12) } else { self?.presentTimeoutPremiumSuggestion(3600 * 12) } @@ -3264,7 +3224,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate }, action: { _, a in a(.default) - updateTimeout(86400, false) + updateTimeout(86400) }))) items.append(.action(ContextMenuActionItem(text: "48 Hours", icon: { theme in if !hasPremium { @@ -3276,7 +3236,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate a(.default) if hasPremium { - updateTimeout(86400 * 2, false) + updateTimeout(86400 * 2) } else { self?.presentTimeoutPremiumSuggestion(86400 * 2) } @@ -3936,7 +3896,7 @@ final class PrivacyButtonComponent: CombinedComponent { let text = text.update( component: Text( - text: "\(context.component.text)", + text: context.component.text, font: Font.medium(14.0), color: .white ), @@ -3970,6 +3930,92 @@ final class PrivacyButtonComponent: CombinedComponent { } } +final class DoneButtonComponent: CombinedComponent { + let backgroundColor: UIColor + let icon: UIImage + let title: String? + + init( + backgroundColor: UIColor, + icon: UIImage, + title: String? + ) { + self.backgroundColor = backgroundColor + self.icon = icon + self.title = title + } + + static func ==(lhs: DoneButtonComponent, rhs: DoneButtonComponent) -> Bool { + if lhs.backgroundColor != rhs.backgroundColor { + return false + } + if lhs.title != rhs.title { + return false + } + return true + } + + static var body: Body { + let background = Child(RoundedRectangle.self) + let icon = Child(Image.self) + let text = Child(Text.self) + + return { context in + let icon = icon.update( + component: Image(image: context.component.icon, tintColor: .white, size: CGSize(width: 10.0, height: 16.0)), + availableSize: CGSize(width: 180.0, height: 100.0), + transition: .immediate + ) + + let backgroundHeight: CGFloat = 33.0 + + var textWidth: CGFloat = 0.0 + var title: _UpdatedChildComponent? + if let titleText = context.component.title { + title = text.update( + component: Text( + text: titleText, + font: Font.with(size: 16.0, design: .round, weight: .semibold), + color: .white + ), + availableSize: CGSize(width: 180.0, height: 100.0), + transition: .immediate + ) + textWidth = title!.size.width + } + + var backgroundSize = CGSize(width: 33.0, height: backgroundHeight) + if !textWidth.isZero { + backgroundSize.width += textWidth + 7.0 + } + + let background = background.update( + component: RoundedRectangle(color: context.component.backgroundColor, cornerRadius: backgroundHeight / 2.0), + availableSize: backgroundSize, + transition: .immediate + ) + context.add(background + .position(CGPoint(x: backgroundSize.width / 2.0, y: backgroundSize.height / 2.0)) + .cornerRadius(min(backgroundSize.width, backgroundSize.height) / 2.0) + .clipsToBounds(true) + ) + + if let title { + context.add(title + .position(CGPoint(x: title.size.width / 2.0 + 15.0, y: backgroundHeight / 2.0)) + ) + } + + context.add(icon + .position(CGPoint(x: background.size.width - 16.0, y: backgroundSize.height / 2.0)) + ) + + return backgroundSize + } + } +} + + private final class HeaderContextReferenceContentSource: ContextReferenceContentSource { private let controller: ViewController private let sourceView: UIView diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaToolsScreen.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaToolsScreen.swift index bc3fc31ec2..89ab6c0c68 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaToolsScreen.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaToolsScreen.swift @@ -284,6 +284,8 @@ private final class MediaToolsScreenComponent: Component { func animateOutToEditor(completion: @escaping () -> Void) { self.animatingOut = true + self.cancelButton.view?.isHidden = true + let buttons = [ self.adjustmentsButton, self.tintButton, diff --git a/submodules/TelegramUI/Components/ShareWithPeersScreen/BUILD b/submodules/TelegramUI/Components/ShareWithPeersScreen/BUILD index c6f08dae1a..2655843936 100644 --- a/submodules/TelegramUI/Components/ShareWithPeersScreen/BUILD +++ b/submodules/TelegramUI/Components/ShareWithPeersScreen/BUILD @@ -35,6 +35,8 @@ swift_library( "//submodules/LocalizedPeerData", "//submodules/TelegramUI/Components/Stories/PeerListItemComponent", "//submodules/TelegramUI/Components/LottieComponent", + "//submodules/TelegramUI/Components/SwitchComponent", + "//submodules/TooltipUI", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/CategoryListItemComponent.swift b/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/CategoryListItemComponent.swift index 8c38a9b484..a1f23de673 100644 --- a/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/CategoryListItemComponent.swift +++ b/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/CategoryListItemComponent.swift @@ -20,7 +20,6 @@ final class CategoryListItemComponent: Component { let context: AccountContext let theme: PresentationTheme - let sideInset: CGFloat let title: String let color: ShareWithPeersScreenComponent.CategoryColor let iconName: String? @@ -33,7 +32,6 @@ final class CategoryListItemComponent: Component { init( context: AccountContext, theme: PresentationTheme, - sideInset: CGFloat, title: String, color: ShareWithPeersScreenComponent.CategoryColor, iconName: String?, @@ -45,7 +43,6 @@ final class CategoryListItemComponent: Component { ) { self.context = context self.theme = theme - self.sideInset = sideInset self.title = title self.color = color self.iconName = iconName @@ -63,9 +60,6 @@ final class CategoryListItemComponent: Component { if lhs.theme !== rhs.theme { return false } - if lhs.sideInset != rhs.sideInset { - return false - } if lhs.title != rhs.title { return false } @@ -179,11 +173,11 @@ final class CategoryListItemComponent: Component { let contextInset: CGFloat = 0.0 - let height: CGFloat = 60.0 - let verticalInset: CGFloat = 1.0 - var leftInset: CGFloat = 62.0 + component.sideInset - let rightInset: CGFloat = contextInset * 2.0 + 8.0 + component.sideInset - var avatarLeftInset: CGFloat = component.sideInset + 10.0 + let height: CGFloat = 56.0 + let verticalInset: CGFloat = 0.0 + var leftInset: CGFloat = 62.0 + let rightInset: CGFloat = contextInset * 2.0 + 8.0 + var avatarLeftInset: CGFloat = 10.0 if case let .editing(isSelected, isTinted) = component.selectionState { leftInset += 44.0 @@ -310,7 +304,7 @@ final class CategoryListItemComponent: Component { labelArrowView.isUserInteractionEnabled = false self.containerButton.addSubview(labelArrowView) } - transition.setFrame(view: labelArrowView, frame: CGRect(origin: CGPoint(x: titleFrame.minX + labelSize.width + 5.0, y: titleFrame.maxY + titleSpacing + floorToScreenPixels(labelSize.height / 2.0 - labelArrowSize.height / 2.0)), size: labelArrowSize)) + transition.setFrame(view: labelArrowView, frame: CGRect(origin: CGPoint(x: titleFrame.minX + labelSize.width + 5.0, y: titleFrame.maxY + titleSpacing + floorToScreenPixels(labelSize.height / 2.0 - labelArrowSize.height / 2.0) + 1.0 ), size: labelArrowSize)) } if themeUpdated { diff --git a/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/OptionListItemComponent.swift b/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/OptionListItemComponent.swift new file mode 100644 index 0000000000..77bcb6a0c5 --- /dev/null +++ b/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/OptionListItemComponent.swift @@ -0,0 +1,157 @@ +import Foundation +import UIKit +import Display +import ComponentFlow +import MultilineTextComponent +import TelegramPresentationData +import SwitchComponent + +final class OptionListItemComponent: Component { + enum SelectionState: Equatable { + case none + case editing(isSelected: Bool, isTinted: Bool) + } + + let theme: PresentationTheme + let title: String + let hasNext: Bool + let selected: Bool + let selectionChanged: (Bool) -> Void + + init( + theme: PresentationTheme, + title: String, + hasNext: Bool, + selected: Bool, + selectionChanged: @escaping (Bool) -> Void + ) { + self.theme = theme + self.title = title + self.hasNext = hasNext + self.selected = selected + self.selectionChanged = selectionChanged + } + + static func ==(lhs: OptionListItemComponent, rhs: OptionListItemComponent) -> Bool { + if lhs.theme !== rhs.theme { + return false + } + if lhs.title != rhs.title { + return false + } + if lhs.selected != rhs.selected { + return false + } + if lhs.hasNext != rhs.hasNext { + return false + } + return true + } + + final class View: UIView { + private let containerButton: HighlightTrackingButton + + private let title = ComponentView() + private let switchComponent = ComponentView() + private let separatorLayer: SimpleLayer + + private var component: OptionListItemComponent? + private weak var state: EmptyComponentState? + + override init(frame: CGRect) { + self.separatorLayer = SimpleLayer() + + self.containerButton = HighlightTrackingButton() + + super.init(frame: frame) + + self.layer.addSublayer(self.separatorLayer) + self.addSubview(self.containerButton) + + self.containerButton.addTarget(self, action: #selector(self.pressed), for: .touchUpInside) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + @objc private func pressed() { +// guard let component = self.component else { +// return +// } +// if case .editing(true, _) = component.selectionState { +// component.secondaryAction() +// } else { +// component.action() +// } + } + + func update(component: OptionListItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + let themeUpdated = self.component?.theme !== component.theme + + self.component = component + self.state = state + + let height: CGFloat = 44.0 + let verticalInset: CGFloat = 0.0 + let leftInset: CGFloat = 16.0 + let rightInset: CGFloat = 16.0 + + let switchSize = self.switchComponent.update( + transition: .immediate, + component: AnyComponent(SwitchComponent( + tintColor: nil, + value: component.selected, + valueUpdated: { selected in + component.selectionChanged(selected) + } + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - leftInset - rightInset, height: 100.0) + ) + + let titleSize = self.title.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: component.title, font: Font.regular(17.0), textColor: component.theme.list.itemPrimaryTextColor)) + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - leftInset - rightInset, height: 100.0) + ) + + let titleFrame = CGRect(origin: CGPoint(x: leftInset, y: floorToScreenPixels((height - titleSize.height) / 2.0)), size: titleSize) + if let titleView = self.title.view { + if titleView.superview == nil { + titleView.isUserInteractionEnabled = false + self.containerButton.addSubview(titleView) + } + titleView.frame = titleFrame + } + if let switchView = self.switchComponent.view { + if switchView.superview == nil { + self.containerButton.addSubview(switchView) + } + transition.setFrame(view: switchView, frame: CGRect(origin: CGPoint(x: availableSize.width - rightInset - switchSize.width, y: floorToScreenPixels((height - switchSize.height) / 2.0)), size: switchSize)) + } + + if themeUpdated { + self.separatorLayer.backgroundColor = component.theme.list.itemPlainSeparatorColor.cgColor + } + transition.setFrame(layer: self.separatorLayer, frame: CGRect(origin: CGPoint(x: leftInset, y: height), size: CGSize(width: availableSize.width - leftInset, height: UIScreenPixel))) + self.separatorLayer.isHidden = !component.hasNext + + let containerFrame = CGRect(origin: CGPoint(x: 0.0, y: verticalInset), size: CGSize(width: availableSize.width, height: height - verticalInset * 2.0)) + transition.setFrame(view: self.containerButton, frame: containerFrame) + + return CGSize(width: availableSize.width, height: height) + } + } + + func makeView() -> View { + return View(frame: CGRect()) + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} diff --git a/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/PeerListItemComponent.swift b/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/PeerListItemComponent.swift deleted file mode 100644 index fdafc66196..0000000000 --- a/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/PeerListItemComponent.swift +++ /dev/null @@ -1,344 +0,0 @@ -//import Foundation -//import UIKit -//import Display -//import AsyncDisplayKit -//import ComponentFlow -//import SwiftSignalKit -//import AccountContext -//import TelegramCore -//import MultilineTextComponent -//import AvatarNode -//import TelegramPresentationData -//import CheckNode -//import TelegramStringFormatting -//import PeerPresenceStatusManager -// -//private let avatarFont = avatarPlaceholderFont(size: 15.0) -// -//final class PeerListItemComponent: Component { -// enum SelectionState: Equatable { -// case none -// case editing(isSelected: Bool, isTinted: Bool) -// } -// -// let context: AccountContext -// let theme: PresentationTheme -// let strings: PresentationStrings -// let sideInset: CGFloat -// let title: String -// let peer: EnginePeer? -// let subtitle: String? -// let presence: EnginePeer.Presence? -// let selectionState: SelectionState -// let hasNext: Bool -// let action: (EnginePeer) -> Void -// -// init( -// context: AccountContext, -// theme: PresentationTheme, -// strings: PresentationStrings, -// sideInset: CGFloat, -// title: String, -// peer: EnginePeer?, -// subtitle: String?, -// presence: EnginePeer.Presence?, -// selectionState: SelectionState, -// hasNext: Bool, -// action: @escaping (EnginePeer) -> Void -// ) { -// self.context = context -// self.theme = theme -// self.strings = strings -// self.sideInset = sideInset -// self.title = title -// self.peer = peer -// self.subtitle = subtitle -// self.presence = presence -// self.selectionState = selectionState -// self.hasNext = hasNext -// self.action = action -// } -// -// static func ==(lhs: PeerListItemComponent, rhs: PeerListItemComponent) -> Bool { -// if lhs.context !== rhs.context { -// return false -// } -// if lhs.theme !== rhs.theme { -// return false -// } -// if lhs.strings !== rhs.strings { -// return false -// } -// if lhs.sideInset != rhs.sideInset { -// return false -// } -// if lhs.title != rhs.title { -// return false -// } -// if lhs.peer != rhs.peer { -// return false -// } -// if lhs.subtitle != rhs.subtitle { -// return false -// } -// if lhs.presence != rhs.presence { -// return false -// } -// if lhs.selectionState != rhs.selectionState { -// return false -// } -// if lhs.hasNext != rhs.hasNext { -// return false -// } -// return true -// } -// -// final class View: UIView { -// private let containerButton: HighlightTrackingButton -// -// private let title = ComponentView() -// private let label = ComponentView() -// private let separatorLayer: SimpleLayer -// private let avatarNode: AvatarNode -// -// private var checkLayer: CheckLayer? -// -// private var component: PeerListItemComponent? -// private weak var state: EmptyComponentState? -// -// private var presenceManager: PeerPresenceStatusManager? -// -// override init(frame: CGRect) { -// self.separatorLayer = SimpleLayer() -// -// self.containerButton = HighlightTrackingButton() -// -// self.avatarNode = AvatarNode(font: avatarFont) -// self.avatarNode.isLayerBacked = true -// -// super.init(frame: frame) -// -// self.layer.addSublayer(self.separatorLayer) -// self.addSubview(self.containerButton) -// self.containerButton.layer.addSublayer(self.avatarNode.layer) -// -// self.containerButton.addTarget(self, action: #selector(self.pressed), for: .touchUpInside) -// } -// -// required init?(coder: NSCoder) { -// fatalError("init(coder:) has not been implemented") -// } -// -// @objc private func pressed() { -// guard let component = self.component, let peer = component.peer else { -// return -// } -// component.action(peer) -// } -// -// func update(component: PeerListItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { -// let animationHint = transition.userData(ShareWithPeersScreenComponent.AnimationHint.self) -// var synchronousLoad = false -// if let animationHint, animationHint.contentReloaded { -// synchronousLoad = true -// } -// -// let themeUpdated = self.component?.theme !== component.theme -// -// var hasSelectionUpdated = false -// if let previousComponent = self.component { -// switch previousComponent.selectionState { -// case .none: -// if case .none = component.selectionState { -// } else { -// hasSelectionUpdated = true -// } -// case .editing: -// if case .editing = component.selectionState { -// } else { -// hasSelectionUpdated = true -// } -// } -// } -// -// if let presence = component.presence { -// let presenceManager: PeerPresenceStatusManager -// if let current = self.presenceManager { -// presenceManager = current -// } else { -// presenceManager = PeerPresenceStatusManager(update: { [weak self] in -// self?.state?.updated(transition: .immediate) -// }) -// self.presenceManager = presenceManager -// } -// presenceManager.reset(presence: presence) -// } else { -// if self.presenceManager != nil { -// self.presenceManager = nil -// } -// } -// -// self.component = component -// self.state = state -// -// let contextInset: CGFloat = 0.0 -// -// let height: CGFloat = 60.0 -// let verticalInset: CGFloat = 1.0 -// var leftInset: CGFloat = 62.0 + component.sideInset -// let rightInset: CGFloat = contextInset * 2.0 + 8.0 + component.sideInset -// var avatarLeftInset: CGFloat = component.sideInset + 10.0 -// -// if case let .editing(isSelected, isTinted) = component.selectionState { -// leftInset += 44.0 -// avatarLeftInset += 44.0 -// let checkSize: CGFloat = 22.0 -// -// let checkLayer: CheckLayer -// if let current = self.checkLayer { -// checkLayer = current -// if themeUpdated { -// var theme = CheckNodeTheme(theme: component.theme, style: .plain) -// if isTinted { -// theme.backgroundColor = theme.backgroundColor.mixedWith(component.theme.list.itemBlocksBackgroundColor, alpha: 0.5) -// } -// checkLayer.theme = theme -// } -// checkLayer.setSelected(isSelected, animated: !transition.animation.isImmediate) -// } else { -// var theme = CheckNodeTheme(theme: component.theme, style: .plain) -// if isTinted { -// theme.backgroundColor = theme.backgroundColor.mixedWith(component.theme.list.itemBlocksBackgroundColor, alpha: 0.5) -// } -// checkLayer = CheckLayer(theme: theme) -// self.checkLayer = checkLayer -// self.containerButton.layer.addSublayer(checkLayer) -// checkLayer.frame = CGRect(origin: CGPoint(x: -checkSize, y: floor((height - verticalInset * 2.0 - checkSize) / 2.0)), size: CGSize(width: checkSize, height: checkSize)) -// checkLayer.setSelected(isSelected, animated: false) -// checkLayer.setNeedsDisplay() -// } -// transition.setFrame(layer: checkLayer, frame: CGRect(origin: CGPoint(x: floor((54.0 - checkSize) * 0.5), y: floor((height - verticalInset * 2.0 - checkSize) / 2.0)), size: CGSize(width: checkSize, height: checkSize))) -// } else { -// if let checkLayer = self.checkLayer { -// self.checkLayer = nil -// transition.setPosition(layer: checkLayer, position: CGPoint(x: -checkLayer.bounds.width * 0.5, y: checkLayer.position.y), completion: { [weak checkLayer] _ in -// checkLayer?.removeFromSuperlayer() -// }) -// } -// } -// -// let avatarSize: CGFloat = 40.0 -// -// let avatarFrame = CGRect(origin: CGPoint(x: avatarLeftInset, y: floor((height - verticalInset * 2.0 - avatarSize) / 2.0)), size: CGSize(width: avatarSize, height: avatarSize)) -// if self.avatarNode.bounds.isEmpty { -// self.avatarNode.frame = avatarFrame -// } else { -// transition.setFrame(layer: self.avatarNode.layer, frame: avatarFrame) -// } -// if let peer = component.peer { -// let clipStyle: AvatarNodeClipStyle -// if case let .channel(channel) = peer, channel.flags.contains(.isForum) { -// clipStyle = .roundedRect -// } else { -// clipStyle = .round -// } -// self.avatarNode.setPeer(context: component.context, theme: component.theme, peer: peer, clipStyle: clipStyle, synchronousLoad: synchronousLoad, displayDimensions: CGSize(width: avatarSize, height: avatarSize)) -// } -// -// let labelData: (String, Bool) -// -// if let presence = component.presence { -// let timestamp = CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970 -// labelData = stringAndActivityForUserPresence(strings: component.strings, dateTimeFormat: PresentationDateTimeFormat(), presence: presence, relativeTo: Int32(timestamp)) -// } else if let subtitle = component.subtitle { -// labelData = (subtitle, false) -// } else if case .legacyGroup = component.peer { -// labelData = (component.strings.Group_Status, false) -// } else if case let .channel(channel) = component.peer { -// if case .group = channel.info { -// labelData = (component.strings.Group_Status, false) -// } else { -// labelData = (component.strings.Channel_Status, false) -// } -// } else { -// labelData = (component.strings.Group_Status, false) -// } -// -// let labelSize = self.label.update( -// transition: .immediate, -// component: AnyComponent(MultilineTextComponent( -// text: .plain(NSAttributedString(string: labelData.0, font: Font.regular(15.0), textColor: labelData.1 ? component.theme.list.itemAccentColor : component.theme.list.itemSecondaryTextColor)) -// )), -// environment: {}, -// containerSize: CGSize(width: availableSize.width - leftInset - rightInset, height: 100.0) -// ) -// -// let previousTitleFrame = self.title.view?.frame -// var previousTitleContents: UIView? -// if hasSelectionUpdated && !"".isEmpty { -// previousTitleContents = self.title.view?.snapshotView(afterScreenUpdates: false) -// } -// -// let titleSize = self.title.update( -// transition: .immediate, -// component: AnyComponent(MultilineTextComponent( -// text: .plain(NSAttributedString(string: component.title, font: Font.semibold(17.0), textColor: component.theme.list.itemPrimaryTextColor)) -// )), -// environment: {}, -// containerSize: CGSize(width: availableSize.width - leftInset - rightInset, height: 100.0) -// ) -// -// let titleSpacing: CGFloat = 1.0 -// let centralContentHeight: CGFloat = titleSize.height + labelSize.height + titleSpacing -// -// let titleFrame = CGRect(origin: CGPoint(x: leftInset, y: floor((height - verticalInset * 2.0 - centralContentHeight) / 2.0)), size: titleSize) -// if let titleView = self.title.view { -// if titleView.superview == nil { -// titleView.isUserInteractionEnabled = false -// self.containerButton.addSubview(titleView) -// } -// titleView.frame = titleFrame -// if let previousTitleFrame, previousTitleFrame.origin.x != titleFrame.origin.x { -// transition.animatePosition(view: titleView, from: CGPoint(x: previousTitleFrame.origin.x - titleFrame.origin.x, y: 0.0), to: CGPoint(), additive: true) -// } -// -// if let previousTitleFrame, let previousTitleContents, previousTitleFrame.size != titleSize { -// previousTitleContents.frame = CGRect(origin: previousTitleFrame.origin, size: previousTitleFrame.size) -// self.addSubview(previousTitleContents) -// -// transition.setFrame(view: previousTitleContents, frame: CGRect(origin: titleFrame.origin, size: previousTitleFrame.size)) -// transition.setAlpha(view: previousTitleContents, alpha: 0.0, completion: { [weak previousTitleContents] _ in -// previousTitleContents?.removeFromSuperview() -// }) -// transition.animateAlpha(view: titleView, from: 0.0, to: 1.0) -// } -// } -// if let labelView = self.label.view { -// if labelView.superview == nil { -// labelView.isUserInteractionEnabled = false -// self.containerButton.addSubview(labelView) -// } -// transition.setFrame(view: labelView, frame: CGRect(origin: CGPoint(x: titleFrame.minX, y: titleFrame.maxY + titleSpacing), size: labelSize)) -// } -// -// if themeUpdated { -// self.separatorLayer.backgroundColor = component.theme.list.itemPlainSeparatorColor.cgColor -// } -// transition.setFrame(layer: self.separatorLayer, frame: CGRect(origin: CGPoint(x: leftInset, y: height), size: CGSize(width: availableSize.width - leftInset, height: UIScreenPixel))) -// self.separatorLayer.isHidden = !component.hasNext -// -// let containerFrame = CGRect(origin: CGPoint(x: contextInset, y: verticalInset), size: CGSize(width: availableSize.width - contextInset * 2.0, height: height - verticalInset * 2.0)) -// transition.setFrame(view: self.containerButton, frame: containerFrame) -// -// return CGSize(width: availableSize.width, height: height) -// } -// } -// -// func makeView() -> View { -// return View(frame: CGRect()) -// } -// -// func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { -// return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) -// } -//} diff --git a/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/SectionHeaderComponent.swift b/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/SectionHeaderComponent.swift index 5bb71c4073..53589d0d8a 100644 --- a/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/SectionHeaderComponent.swift +++ b/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/SectionHeaderComponent.swift @@ -7,16 +7,16 @@ import MultilineTextComponent final class SectionHeaderComponent: Component { let theme: PresentationTheme - let sideInset: CGFloat + let style: ShareWithPeersScreenComponent.Style let title: String init( theme: PresentationTheme, - sideInset: CGFloat, + style: ShareWithPeersScreenComponent.Style, title: String ) { self.theme = theme - self.sideInset = sideInset + self.style = style self.title = title } @@ -24,7 +24,7 @@ final class SectionHeaderComponent: Component { if lhs.theme !== rhs.theme { return false } - if lhs.sideInset != rhs.sideInset { + if lhs.style != rhs.style { return false } if lhs.title != rhs.title { @@ -59,13 +59,19 @@ final class SectionHeaderComponent: Component { self.state = state let height: CGFloat = 28.0 - let leftInset: CGFloat = component.sideInset - let rightInset: CGFloat = component.sideInset + let leftInset: CGFloat = 16.0 + let rightInset: CGFloat = 0.0 let previousTitleFrame = self.title.view?.frame if themeUpdated { - self.backgroundView.updateColor(color: component.theme.rootController.navigationBar.blurredBackgroundColor, transition: .immediate) + switch component.style { + case .plain: + self.backgroundView.isHidden = false + self.backgroundView.updateColor(color: component.theme.rootController.navigationBar.blurredBackgroundColor, transition: .immediate) + case .blocks: + self.backgroundView.isHidden = true + } } let titleSize = self.title.update( diff --git a/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/ShareWithPeersScreen.swift b/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/ShareWithPeersScreen.swift index 62e3f89687..d351b5ea87 100644 --- a/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/ShareWithPeersScreen.swift +++ b/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/ShareWithPeersScreen.swift @@ -20,6 +20,7 @@ import AvatarNode import LocalizedPeerData import PeerListItemComponent import LottieComponent +import TooltipUI final class ShareWithPeersScreenComponent: Component { typealias EnvironmentType = ViewControllerComponentContainer.Environment @@ -27,30 +28,36 @@ final class ShareWithPeersScreenComponent: Component { let context: AccountContext let stateContext: ShareWithPeersScreen.StateContext let initialPrivacy: EngineStoryPrivacy + let screenshot: Bool + let pin: Bool let timeout: Int let categoryItems: [CategoryItem] - let completion: (EngineStoryPrivacy) -> Void - let editCategory: (EngineStoryPrivacy) -> Void - let secondaryAction: () -> Void + let optionItems: [OptionItem] + let completion: (EngineStoryPrivacy, Bool, Bool) -> Void + let editCategory: (EngineStoryPrivacy, Bool, Bool) -> Void init( context: AccountContext, stateContext: ShareWithPeersScreen.StateContext, initialPrivacy: EngineStoryPrivacy, + screenshot: Bool, + pin: Bool, timeout: Int, categoryItems: [CategoryItem], - completion: @escaping (EngineStoryPrivacy) -> Void, - editCategory: @escaping (EngineStoryPrivacy) -> Void, - secondaryAction: @escaping () -> Void + optionItems: [OptionItem], + completion: @escaping (EngineStoryPrivacy, Bool, Bool) -> Void, + editCategory: @escaping (EngineStoryPrivacy, Bool, Bool) -> Void ) { self.context = context self.stateContext = stateContext self.initialPrivacy = initialPrivacy + self.screenshot = screenshot + self.pin = pin self.timeout = timeout self.categoryItems = categoryItems + self.optionItems = optionItems self.completion = completion self.editCategory = editCategory - self.secondaryAction = secondaryAction } static func ==(lhs: ShareWithPeersScreenComponent, rhs: ShareWithPeersScreenComponent) -> Bool { @@ -63,16 +70,29 @@ final class ShareWithPeersScreenComponent: Component { if lhs.initialPrivacy != rhs.initialPrivacy { return false } + if lhs.screenshot != rhs.screenshot { + return false + } + if lhs.pin != rhs.pin { + return false + } if lhs.timeout != rhs.timeout { return false } if lhs.categoryItems != rhs.categoryItems { return false } - + if lhs.optionItems != rhs.optionItems { + return false + } return true } + enum Style { + case plain + case blocks + } + private struct ItemLayout: Equatable { struct Section: Equatable { var id: Int @@ -97,6 +117,7 @@ final class ShareWithPeersScreenComponent: Component { } } + var style: ShareWithPeersScreenComponent.Style var containerSize: CGSize var containerInset: CGFloat var bottomInset: CGFloat @@ -107,7 +128,8 @@ final class ShareWithPeersScreenComponent: Component { var contentHeight: CGFloat - init(containerSize: CGSize, containerInset: CGFloat, bottomInset: CGFloat, topInset: CGFloat, sideInset: CGFloat, navigationHeight: CGFloat, sections: [Section]) { + init(style: ShareWithPeersScreenComponent.Style, containerSize: CGSize, containerInset: CGFloat, bottomInset: CGFloat, topInset: CGFloat, sideInset: CGFloat, navigationHeight: CGFloat, sections: [Section]) { + self.style = style self.containerSize = containerSize self.containerInset = containerInset self.bottomInset = bottomInset @@ -155,6 +177,13 @@ final class ShareWithPeersScreenComponent: Component { case violet } + enum CategoryId: Int, Hashable { + case everyone = 0 + case contacts = 1 + case closeFriends = 2 + case selectedContacts = 3 + } + final class CategoryItem: Equatable { let id: CategoryId let title: String @@ -204,15 +233,34 @@ final class ShareWithPeersScreenComponent: Component { } } - enum CategoryId: Int, Hashable { - case everyone = 0 - case contacts = 1 - case closeFriends = 2 - case selectedContacts = 3 + enum OptionId: Int, Hashable { + case screenshot = 0 + case pin = 1 } + final class OptionItem: Equatable { + let id: OptionId + let title: String + + init( + id: OptionId, + title: String + ) { + self.id = id + self.title = title + } + + static func ==(lhs: OptionItem, rhs: OptionItem) -> Bool { + if lhs === rhs { + return true + } + return false + } + } + final class View: UIView, UIScrollViewDelegate { private let dimView: UIView + private let containerView: UIView private let backgroundView: UIImageView private let navigationContainerView: UIView @@ -239,17 +287,20 @@ final class ShareWithPeersScreenComponent: Component { private let categoryTemplateItem = ComponentView() private let peerTemplateItem = ComponentView() + private let optionTemplateItem = ComponentView() private let itemContainerView: UIView private var visibleSectionHeaders: [Int: ComponentView] = [:] private var visibleItems: [AnyHashable: ComponentView] = [:] + private var visibleSectionBackgrounds: [Int: UIView] = [:] + private var visibleSectionFooters: [Int: ComponentView] = [:] private var ignoreScrolling: Bool = false private var isDismissed: Bool = false private var selectedPeers: [EnginePeer.Id] = [] private var selectedCategories = Set() - private var selectedPeersByCategory: [CategoryId: [EnginePeer.Id]] = [:] + private var selectedOptions = Set() private var component: ShareWithPeersScreenComponent? private weak var state: EmptyComponentState? @@ -268,8 +319,20 @@ final class ShareWithPeersScreenComponent: Component { return self.searchStateContext?.stateValue ?? self.defaultStateValue } + private struct DismissPanState: Equatable { + var translation: CGFloat + + init(translation: CGFloat) { + self.translation = translation + } + } + + private var dismissPanGesture: UIPanGestureRecognizer? + private var dismissPanState: DismissPanState? + override init(frame: CGRect) { self.dimView = UIView() + self.containerView = SparseContainerView() self.backgroundView = UIImageView() @@ -295,7 +358,8 @@ final class ShareWithPeersScreenComponent: Component { super.init(frame: frame) self.addSubview(self.dimView) - self.addSubview(self.backgroundView) + self.addSubview(self.containerView) + self.containerView.addSubview(self.backgroundView) self.scrollView.delaysContentTouches = true self.scrollView.canCancelContentTouches = true @@ -314,19 +378,23 @@ final class ShareWithPeersScreenComponent: Component { self.scrollView.delegate = self self.scrollView.clipsToBounds = true - self.addSubview(self.scrollContentClippingView) + self.containerView.addSubview(self.scrollContentClippingView) self.scrollContentClippingView.addSubview(self.scrollView) self.scrollView.addSubview(self.scrollContentView) self.scrollContentView.addSubview(self.itemContainerView) - self.addSubview(self.navigationContainerView) + self.containerView.addSubview(self.navigationContainerView) self.navigationContainerView.addSubview(self.navigationBackgroundView) self.navigationContainerView.layer.addSublayer(self.navigationSeparatorLayer) - self.addSubview(self.bottomBackgroundView) - self.layer.addSublayer(self.bottomSeparatorLayer) + self.containerView.addSubview(self.bottomBackgroundView) + self.containerView.layer.addSublayer(self.bottomSeparatorLayer) + + let dismissPanGesture = UIPanGestureRecognizer(target: self, action: #selector(self.dismissPanGesture(_:))) + self.containerView.addGestureRecognizer(dismissPanGesture) + self.dismissPanGesture = dismissPanGesture self.dimView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dimTapGesture(_:)))) } @@ -389,8 +457,99 @@ final class ShareWithPeersScreenComponent: Component { } } + @objc private func dismissPanGesture(_ recognizer: UIPanGestureRecognizer) { + guard let controller = self.environment?.controller() as? ShareWithPeersScreen else { + return + } + switch recognizer.state { + case .began: + controller.dismissAllTooltips() + + self.dismissPanState = DismissPanState(translation: 0.0) + self.state?.updated(transition: .immediate) + case .changed: + let translation = recognizer.translation(in: self) + self.dismissPanState = DismissPanState(translation: translation.y) + self.state?.updated(transition: .immediate) + case .cancelled, .ended: + if self.dismissPanState != nil { + let translation = recognizer.translation(in: self) + let velocity = recognizer.velocity(in: self) + + self.dismissPanState = nil + + if translation.y > 100.0 || velocity.y > 10.0 { + controller.dismiss() + } else { + self.state?.updated(transition: Transition(animation: .curve(duration: 0.3, curve: .spring))) + } + } + default: + break + } + } + + private func presentOptionsTooltip(optionId: OptionId) { + guard let component = self.component, let controller = self.environment?.controller() else { + return + } + let animationName: String + let text: String + + switch optionId { + case .screenshot: + if self.selectedOptions.contains(.screenshot) { + if self.selectedCategories.contains(.everyone) { + animationName = "anim_savemedia" + text = "Downloading, sharing and taking screenshots will be enabled for this story." + } else { + animationName = "anim_savemedia" + text = "Downloading and taking screenshots will be enabled for this story." + } + } else { + if self.selectedCategories.contains(.everyone) { + animationName = "premium_unlock" + text = "Downloading, sharing and taking screenshots will be disabled for this story." + } else { + animationName = "premium_unlock" + text = "Downloading and taking screenshots will be disabled for this story." + } + } + case .pin: + if self.selectedOptions.contains(.pin) { + animationName = "anim_profileadd" + text = "Users allowed to view your story will see it on your page event after it expires." + } else { + animationName = "anim_autoremove_on" + text = "The story will disappear after it expires." + } + } + + let tooltipScreen = TooltipScreen( + context: component.context, + account: component.context.account, + sharedContext: component.context.sharedContext, + text: .markdown(text: text), + style: .wide, + icon: .animation(name: animationName, delay: 0.0, tintColor: .white), + location: .top, + displayDuration: .custom(4.0), + shouldDismissOnTouch: { point in + return .ignore + } + ) + + controller.window?.forEachController({ controller in + if let controller = controller as? TooltipScreen { + controller.dismiss(inPlace: true) + } + }) + + controller.present(tooltipScreen, in: .window(.root)) + } + private func updateScrolling(transition: Transition) { - guard let component = self.component, let environment = self.environment, let controller = environment.controller(), let itemLayout = self.itemLayout else { + guard let component = self.component, let environment = self.environment, let itemLayout = self.itemLayout else { return } guard let stateValue = self.effectiveStateValue else { @@ -412,9 +571,7 @@ final class ShareWithPeersScreenComponent: Component { var topOffsetFraction = topOffset / topOffsetDistance topOffsetFraction = max(0.0, min(1.0, topOffsetFraction)) - let transitionFactor: CGFloat = 1.0 - topOffsetFraction - let _ = transitionFactor - let _ = controller + //let transitionFactor: CGFloat = 1.0 - topOffsetFraction //controller.updateModalStyleOverlayTransitionFactor(transitionFactor, transition: transition.containedViewLayoutTransition) var visibleBounds = self.scrollView.bounds @@ -427,14 +584,42 @@ final class ShareWithPeersScreenComponent: Component { var validIds: [AnyHashable] = [] var validSectionHeaders: [AnyHashable] = [] + var validSectionBackgrounds: [AnyHashable] = [] var sectionOffset: CGFloat = itemLayout.navigationHeight for sectionIndex in 0 ..< itemLayout.sections.count { let section = itemLayout.sections[sectionIndex] + + if case .blocks = itemLayout.style { + let sectionBackgroundFrame = CGRect(origin: CGPoint(x: itemLayout.sideInset, y: sectionOffset + section.insets.top), size: CGSize(width: itemLayout.containerSize.width, height: section.totalHeight - section.insets.top)) + + if visibleFrame.intersects(sectionBackgroundFrame) { + validSectionBackgrounds.append(section.id) + + var sectionBackground: UIView + var sectionBackgroundTransition = transition + if let current = self.visibleSectionBackgrounds[section.id] { + sectionBackground = current + } else { + if !transition.animation.isImmediate { + sectionBackgroundTransition = .immediate + } + sectionBackground = UIView() + sectionBackground.backgroundColor = environment.theme.list.itemModalBlocksBackgroundColor + sectionBackground.layer.cornerRadius = 10.0 + self.visibleSectionBackgrounds[section.id] = sectionBackground + } + + if sectionBackground.superview == nil { + sectionBackground.isUserInteractionEnabled = false + self.itemContainerView.addSubview(sectionBackground) + } + sectionBackgroundTransition.setFrame(view: sectionBackground, frame: sectionBackgroundFrame) + } + } var minSectionHeader: UIView? - do { - var sectionHeaderFrame = CGRect(origin: CGPoint(x: visibleFrame.minX, y: itemLayout.containerInset + sectionOffset - self.scrollView.bounds.minY + itemLayout.topInset), size: CGSize(width: itemLayout.containerSize.width, height: section.insets.top)) + var sectionHeaderFrame = CGRect(origin: CGPoint(x: itemLayout.sideInset, y: itemLayout.containerInset + sectionOffset - self.scrollView.bounds.minY + itemLayout.topInset), size: CGSize(width: itemLayout.containerSize.width, height: section.insets.top)) let sectionHeaderMinY = topOffset + itemLayout.containerInset + itemLayout.navigationHeight let sectionHeaderMaxY = itemLayout.containerInset + sectionOffset - self.scrollView.bounds.minY + itemLayout.topInset + section.totalHeight - 28.0 @@ -459,19 +644,17 @@ final class ShareWithPeersScreenComponent: Component { let sectionTitle: String if section.id == 0 { sectionTitle = "WHO CAN VIEW" + } else if section.id == 1 { + sectionTitle = "CONTACTS" } else { - if case .chats = component.stateContext.subject { - sectionTitle = "CHATS" - } else { - sectionTitle = "CONTACTS" - } + sectionTitle = "" } let _ = sectionHeader.update( transition: sectionHeaderTransition, component: AnyComponent(SectionHeaderComponent( theme: environment.theme, - sideInset: 16.0, + style: itemLayout.style, title: sectionTitle )), environment: {}, @@ -492,7 +675,7 @@ final class ShareWithPeersScreenComponent: Component { if section.id == 0 { for i in 0 ..< component.categoryItems.count { - let itemFrame = CGRect(origin: CGPoint(x: 0.0, y: sectionOffset + section.insets.top + CGFloat(i) * section.itemHeight), size: CGSize(width: itemLayout.containerSize.width, height: section.itemHeight)) + let itemFrame = CGRect(origin: CGPoint(x: itemLayout.sideInset, y: sectionOffset + section.insets.top + CGFloat(i) * section.itemHeight), size: CGSize(width: itemLayout.containerSize.width, height: section.itemHeight)) if !visibleBounds.intersects(itemFrame) { continue } @@ -519,7 +702,6 @@ final class ShareWithPeersScreenComponent: Component { component: AnyComponent(CategoryListItemComponent( context: component.context, theme: environment.theme, - sideInset: itemLayout.sideInset, title: item.title, color: item.iconColor, iconName: item.icon, @@ -527,7 +709,7 @@ final class ShareWithPeersScreenComponent: Component { selectionState: .editing(isSelected: self.selectedCategories.contains(item.id), isTinted: false), hasNext: i != component.categoryItems.count - 1, action: { [weak self] in - guard let self else { + guard let self, let environment = self.environment, let controller = environment.controller() as? ShareWithPeersScreen else { return } if self.selectedCategories.contains(categoryId) { @@ -538,14 +720,19 @@ final class ShareWithPeersScreenComponent: Component { self.selectedCategories.insert(categoryId) if self.selectedPeers.isEmpty && categoryId == .selectedContacts { - component.editCategory(EngineStoryPrivacy(base: .nobody, additionallyIncludePeers: [])) + component.editCategory( + EngineStoryPrivacy(base: .nobody, additionallyIncludePeers: []), + self.selectedOptions.contains(.screenshot), + self.selectedOptions.contains(.pin) + ) + controller.dismissAllTooltips() controller.dismiss() } } self.state?.updated(transition: Transition(animation: .curve(duration: 0.35, curve: .spring))) }, secondaryAction: { [weak self] in - guard let self, let environment = self.environment, let controller = environment.controller() else { + guard let self, let environment = self.environment, let controller = environment.controller() as? ShareWithPeersScreen else { return } let base: EngineStoryPrivacy.Base? @@ -560,7 +747,12 @@ final class ShareWithPeersScreenComponent: Component { base = .nobody } if let base { - component.editCategory(EngineStoryPrivacy(base: base, additionallyIncludePeers: self.selectedPeers)) + component.editCategory( + EngineStoryPrivacy(base: base, additionallyIncludePeers: self.selectedPeers), + self.selectedOptions.contains(.screenshot), + self.selectedOptions.contains(.pin) + ) + controller.dismissAllTooltips() controller.dismiss() } } @@ -581,7 +773,7 @@ final class ShareWithPeersScreenComponent: Component { } } else if section.id == 1 { for i in 0 ..< stateValue.peers.count { - let itemFrame = CGRect(origin: CGPoint(x: 0.0, y: sectionOffset + section.insets.top + CGFloat(i) * section.itemHeight), size: CGSize(width: itemLayout.containerSize.width, height: section.itemHeight)) + let itemFrame = CGRect(origin: CGPoint(x: itemLayout.sideInset, y: sectionOffset + section.insets.top + CGFloat(i) * section.itemHeight), size: CGSize(width: itemLayout.containerSize.width, height: section.itemHeight)) if !visibleBounds.intersects(itemFrame) { continue } @@ -647,6 +839,95 @@ final class ShareWithPeersScreenComponent: Component { itemTransition.setFrame(view: itemView, frame: itemFrame) } } + } else if section.id == 2 { + for i in 0 ..< component.optionItems.count { + let itemFrame = CGRect(origin: CGPoint(x: itemLayout.sideInset, y: sectionOffset + section.insets.top + CGFloat(i) * section.itemHeight), size: CGSize(width: itemLayout.containerSize.width, height: section.itemHeight)) + if !visibleBounds.intersects(itemFrame) { + continue + } + + let item = component.optionItems[i] + let optionId = item.id + let itemId = AnyHashable(item.id) + validIds.append(itemId) + + var itemTransition = transition + let visibleItem: ComponentView + if let current = self.visibleItems[itemId] { + visibleItem = current + } else { + visibleItem = ComponentView() + if !transition.animation.isImmediate { + itemTransition = .immediate + } + self.visibleItems[itemId] = visibleItem + } + + let _ = visibleItem.update( + transition: itemTransition, + component: AnyComponent(OptionListItemComponent( + theme: environment.theme, + title: item.title, + hasNext: i != component.optionItems.count - 1, + selected: self.selectedOptions.contains(item.id), + selectionChanged: { [weak self] selected in + if let self { + if selected { + self.selectedOptions.insert(optionId) + } else { + self.selectedOptions.remove(optionId) + } + let transition = Transition(animation: .curve(duration: 0.35, curve: .spring)) + self.state?.updated(transition: transition) + + self.presentOptionsTooltip(optionId: optionId) + } + } + )), + environment: {}, + containerSize: itemFrame.size + ) + if let itemView = visibleItem.view { + if itemView.superview == nil { + if let minSectionHeader { + self.itemContainerView.insertSubview(itemView, belowSubview: minSectionHeader) + } else { + self.itemContainerView.addSubview(itemView) + } + } + itemTransition.setFrame(view: itemView, frame: itemFrame) + } + } + + let sectionFooter: ComponentView + var sectionFooterTransition = transition + if let current = self.visibleSectionFooters[section.id] { + sectionFooter = current + } else { + if !transition.animation.isImmediate { + sectionFooterTransition = .immediate + } + sectionFooter = ComponentView() + self.visibleSectionFooters[section.id] = sectionFooter + } + + let footerSize = sectionFooter.update( + transition: sectionFooterTransition, + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: "Keep this story on your page even after it expires in \( component.timeout / 3600 ) hours. Privacy settings will apply.", font: Font.regular(13.0), textColor: environment.theme.list.freeTextColor)), + maximumNumberOfLines: 0, + lineSpacing: 0.2 + )), + environment: {}, + containerSize: CGSize(width: itemLayout.containerSize.width - 16.0 * 2.0, height: itemLayout.contentHeight) + ) + let footerFrame = CGRect(origin: CGPoint(x: itemLayout.sideInset + 16.0, y: sectionOffset + section.totalHeight + 7.0), size: footerSize) + if let footerView = sectionFooter.view { + if footerView.superview == nil { + self.itemContainerView.addSubview(footerView) + } + sectionFooterTransition.setFrame(view: footerView, frame: footerFrame) + } } sectionOffset += section.totalHeight @@ -678,6 +959,17 @@ final class ShareWithPeersScreenComponent: Component { self.visibleSectionHeaders.removeValue(forKey: id) } + var removeSectionBackgroundIds: [Int] = [] + for (id, item) in self.visibleSectionBackgrounds { + if !validSectionBackgrounds.contains(id) { + removeSectionBackgroundIds.append(id) + item.removeFromSuperview() + } + } + for id in removeSectionBackgroundIds { + self.visibleSectionBackgrounds.removeValue(forKey: id) + } + let fadeTransition = Transition.easeInOut(duration: 0.25) if let searchStateContext = self.searchStateContext, case let .search(query) = searchStateContext.subject, let value = searchStateContext.stateValue, value.peers.isEmpty { let sideInset: CGFloat = 44.0 @@ -830,7 +1122,16 @@ final class ShareWithPeersScreenComponent: Component { let resetScrolling = self.scrollView.bounds.width != availableSize.width - let sideInset: CGFloat = 0.0 + var sideInset: CGFloat = 0.0 + if case .stories = component.stateContext.subject { + sideInset = 16.0 + self.scrollView.bounces = false + self.dismissPanGesture?.isEnabled = true + } else { + self.scrollView.bounces = true + self.dismissPanGesture?.isEnabled = false + } + let containerWidth: CGFloat if case .regular = environment.metrics.widthClass { containerWidth = 390.0 @@ -851,6 +1152,13 @@ final class ShareWithPeersScreenComponent: Component { self.selectedCategories.insert(.selectedContacts) } + if component.screenshot { + self.selectedOptions.insert(.screenshot) + } + if component.pin { + self.selectedOptions.insert(.pin) + } + var applyState = false self.defaultStateValue = component.stateContext.stateValue self.selectedPeers = Array(component.stateContext.initialPeerIds) @@ -880,23 +1188,41 @@ final class ShareWithPeersScreenComponent: Component { self.backgroundView.image = generateImage(CGSize(width: 20.0, height: 20.0), rotatedContext: { size, context in context.clear(CGRect(origin: CGPoint(), size: size)) - context.setFillColor(environment.theme.list.plainBackgroundColor.cgColor) + if case .stories = component.stateContext.subject { + context.setFillColor(environment.theme.list.modalBlocksBackgroundColor.cgColor) + } else { + context.setFillColor(environment.theme.list.plainBackgroundColor.cgColor) + } context.fillEllipse(in: CGRect(origin: CGPoint(), size: size)) context.fill(CGRect(origin: CGPoint(x: 0.0, y: size.height * 0.5), size: CGSize(width: size.width, height: size.height * 0.5))) })?.stretchableImage(withLeftCapWidth: 10, topCapHeight: 19) - self.navigationBackgroundView.updateColor(color: environment.theme.rootController.navigationBar.blurredBackgroundColor, transition: .immediate) - self.navigationSeparatorLayer.backgroundColor = environment.theme.rootController.navigationBar.separatorColor.cgColor - self.textFieldSeparatorLayer.backgroundColor = environment.theme.rootController.navigationBar.separatorColor.cgColor + if case .stories = component.stateContext.subject { + self.navigationBackgroundView.updateColor(color: environment.theme.list.modalBlocksBackgroundColor, transition: .immediate) + self.navigationSeparatorLayer.backgroundColor = UIColor.clear.cgColor + self.bottomBackgroundView.updateColor(color: environment.theme.list.modalBlocksBackgroundColor, transition: .immediate) + self.bottomSeparatorLayer.backgroundColor = UIColor.clear.cgColor + } else { + self.navigationBackgroundView.updateColor(color: environment.theme.rootController.navigationBar.blurredBackgroundColor, transition: .immediate) + self.navigationSeparatorLayer.backgroundColor = environment.theme.rootController.navigationBar.separatorColor.cgColor + self.bottomBackgroundView.updateColor(color: environment.theme.rootController.navigationBar.blurredBackgroundColor, transition: .immediate) + self.bottomSeparatorLayer.backgroundColor = environment.theme.rootController.navigationBar.separatorColor.cgColor + } - self.bottomBackgroundView.updateColor(color: environment.theme.rootController.navigationBar.blurredBackgroundColor, transition: .immediate) - self.bottomSeparatorLayer.backgroundColor = environment.theme.rootController.navigationBar.separatorColor.cgColor + self.textFieldSeparatorLayer.backgroundColor = environment.theme.rootController.navigationBar.separatorColor.cgColor } + let itemLayoutStyle: ShareWithPeersScreenComponent.Style + let itemsContainerWidth: CGFloat let navigationTextFieldSize: CGSize if case .stories = component.stateContext.subject { + itemLayoutStyle = .blocks + itemsContainerWidth = containerWidth - sideInset * 2.0 navigationTextFieldSize = .zero } else { + itemLayoutStyle = .plain + itemsContainerWidth = containerWidth + var tokens: [TokenListTextField.Token] = [] for peerId in self.selectedPeers { guard let stateValue = self.defaultStateValue, let peer = stateValue.peers.first(where: { $0.id == peerId }) else { @@ -974,12 +1300,12 @@ final class ShareWithPeersScreenComponent: Component { transition.setFrame(view: self.dimView, frame: CGRect(origin: CGPoint(), size: availableSize)) + let categoryItemSize = self.categoryTemplateItem.update( transition: .immediate, component: AnyComponent(CategoryListItemComponent( context: component.context, theme: environment.theme, - sideInset: sideInset, title: "Title", color: .blue, iconName: nil, @@ -990,7 +1316,7 @@ final class ShareWithPeersScreenComponent: Component { secondaryAction: {} )), environment: {}, - containerSize: CGSize(width: containerWidth, height: 1000.0) + containerSize: CGSize(width: itemsContainerWidth, height: 1000.0) ) let peerItemSize = self.peerTemplateItem.update( transition: transition, @@ -1011,7 +1337,19 @@ final class ShareWithPeersScreenComponent: Component { } )), environment: {}, - containerSize: CGSize(width: containerWidth, height: 1000.0) + containerSize: CGSize(width: itemsContainerWidth, height: 1000.0) + ) + let optionItemSize = self.optionTemplateItem.update( + transition: transition, + component: AnyComponent(OptionListItemComponent( + theme: environment.theme, + title: "Title", + hasNext: true, + selected: false, + selectionChanged: { _ in } + )), + environment: {}, + containerSize: CGSize(width: itemsContainerWidth, height: 1000.0) ) var sections: [ItemLayout.Section] = [] @@ -1023,6 +1361,12 @@ final class ShareWithPeersScreenComponent: Component { itemHeight: categoryItemSize.height, itemCount: component.categoryItems.count )) + sections.append(ItemLayout.Section( + id: 2, + insets: UIEdgeInsets(top: 28.0, left: 0.0, bottom: 0.0, right: 0.0), + itemHeight: optionItemSize.height, + itemCount: component.optionItems.count + )) } else { sections.append(ItemLayout.Section( id: 1, @@ -1065,9 +1409,13 @@ final class ShareWithPeersScreenComponent: Component { var actionButtonTitle = "Save Settings" let title: String switch component.stateContext.subject { - case .stories: - title = "Share Story" - actionButtonTitle = "Post Story" + case let .stories(editing): + if editing { + title = "Edit Story" + } else { + title = "Share Story" + actionButtonTitle = "Post Story" + } case .chats: title = "Send as a Message" case let .contacts(category): @@ -1110,15 +1458,25 @@ final class ShareWithPeersScreenComponent: Component { } navigationHeight += navigationTextFieldFrame.height + if case .stories = component.stateContext.subject { + navigationHeight += 16.0 + } + let topInset: CGFloat if environment.inputHeight != 0.0 || !self.navigationTextFieldState.text.isEmpty { topInset = 0.0 } else { - if case .stories = component.stateContext.subject { - topInset = max(0.0, availableSize.height - containerInset - 427.0) + let inset: CGFloat + if case let .stories(editing) = component.stateContext.subject { + if editing { + inset = 430.0 + } else { + inset = 605.0 + } } else { - topInset = max(0.0, availableSize.height - containerInset - 600.0) + inset = 600.0 } + topInset = max(0.0, availableSize.height - containerInset - inset) } self.navigationBackgroundView.update(size: CGSize(width: containerWidth, height: navigationHeight), cornerRadius: 10.0, maskedCorners: [.layerMinXMinYCorner, .layerMaxXMinYCorner], transition: transition.containedViewLayoutTransition) @@ -1147,7 +1505,7 @@ final class ShareWithPeersScreenComponent: Component { isEnabled: true, displaysProgress: false, action: { [weak self] in - guard let self, let component = self.component, let controller = self.environment?.controller() else { + guard let self, let component = self.component, let controller = self.environment?.controller() as? ShareWithPeersScreen else { return } @@ -1164,11 +1522,16 @@ final class ShareWithPeersScreenComponent: Component { base = .nobody } - component.completion(EngineStoryPrivacy( - base: base, - additionallyIncludePeers: self.selectedPeers - )) + component.completion( + EngineStoryPrivacy( + base: base, + additionallyIncludePeers: self.selectedPeers + ), + self.selectedOptions.contains(.screenshot), + self.selectedOptions.contains(.pin) + ) + controller.dismissAllTooltips() controller.dismiss() } )), @@ -1185,7 +1548,7 @@ final class ShareWithPeersScreenComponent: Component { let actionButtonFrame = CGRect(origin: CGPoint(x: containerSideInset + navigationSideInset, y: availableSize.height - bottomPanelHeight), size: actionButtonSize) if let actionButtonView = self.actionButton.view { if actionButtonView.superview == nil { - self.addSubview(actionButtonView) + self.containerView.addSubview(actionButtonView) } transition.setFrame(view: actionButtonView, frame: actionButtonFrame) } @@ -1194,12 +1557,12 @@ final class ShareWithPeersScreenComponent: Component { self.bottomBackgroundView.update(size: self.bottomBackgroundView.bounds.size, transition: transition.containedViewLayoutTransition) transition.setFrame(layer: self.bottomSeparatorLayer, frame: CGRect(origin: CGPoint(x: containerSideInset + sideInset, y: availableSize.height - bottomPanelHeight - 8.0 - UIScreenPixel), size: CGSize(width: containerWidth, height: UIScreenPixel))) - let itemContainerSize = CGSize(width: containerWidth, height: availableSize.height) - let itemLayout = ItemLayout(containerSize: itemContainerSize, containerInset: containerInset, bottomInset: bottomPanelHeight, topInset: topInset, sideInset: sideInset, navigationHeight: navigationHeight, sections: sections) + let itemContainerSize = CGSize(width: itemsContainerWidth, height: availableSize.height) + let itemLayout = ItemLayout(style: itemLayoutStyle, containerSize: itemContainerSize, containerInset: containerInset, bottomInset: bottomPanelHeight, topInset: topInset, sideInset: sideInset, navigationHeight: navigationHeight, sections: sections) let previousItemLayout = self.itemLayout self.itemLayout = itemLayout - contentTransition.setFrame(view: self.itemContainerView, frame: CGRect(origin: CGPoint(x: sideInset, y: 0.0), size: CGSize(width: containerWidth, height: itemLayout.contentHeight))) + contentTransition.setFrame(view: self.itemContainerView, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: containerWidth, height: itemLayout.contentHeight))) let scrollContentHeight = max(topInset + itemLayout.contentHeight + containerInset, availableSize.height - containerInset) @@ -1208,10 +1571,16 @@ final class ShareWithPeersScreenComponent: Component { transition.setPosition(view: self.backgroundView, position: CGPoint(x: availableSize.width / 2.0, y: availableSize.height / 2.0)) transition.setBounds(view: self.backgroundView, bounds: CGRect(origin: CGPoint(x: containerSideInset, y: 0.0), size: CGSize(width: containerWidth, height: availableSize.height))) - let scrollClippingFrame = CGRect(origin: CGPoint(x: sideInset, y: containerInset + 10.0), size: CGSize(width: availableSize.width, height: availableSize.height - 10.0)) + let scrollClippingFrame = CGRect(origin: CGPoint(x: 0.0, y: containerInset + 10.0), size: CGSize(width: availableSize.width, height: availableSize.height - 10.0)) transition.setPosition(view: self.scrollContentClippingView, position: scrollClippingFrame.center) transition.setBounds(view: self.scrollContentClippingView, bounds: CGRect(origin: CGPoint(x: scrollClippingFrame.minX, y: scrollClippingFrame.minY), size: scrollClippingFrame.size)) + var dismissOffset: CGFloat = 0.0 + if let dismissPanState = self.dismissPanState { + dismissOffset = max(0.0, dismissPanState.translation) + } + transition.setFrame(view: self.containerView, frame: CGRect(origin: CGPoint(x: 0.0, y: dismissOffset), size: availableSize)) + self.ignoreScrolling = true transition.setFrame(view: self.scrollView, frame: CGRect(origin: CGPoint(x: containerSideInset, y: 0.0), size: CGSize(width: containerWidth, height: availableSize.height))) let contentSize = CGSize(width: containerWidth, height: scrollContentHeight) @@ -1265,7 +1634,7 @@ public class ShareWithPeersScreen: ViewControllerComponentContainer { public final class StateContext { public enum Subject: Equatable { - case stories + case stories(editing: Bool) case chats case contacts(EngineStoryPrivacy.Base) case search(String) @@ -1430,11 +1799,21 @@ public class ShareWithPeersScreen: ViewControllerComponentContainer { public var dismissed: () -> Void = {} - public init(context: AccountContext, initialPrivacy: EngineStoryPrivacy, timeout: Int, stateContext: StateContext, completion: @escaping (EngineStoryPrivacy) -> Void, editCategory: @escaping (EngineStoryPrivacy) -> Void, secondaryAction: @escaping () -> Void = {}) { + public init( + context: AccountContext, + initialPrivacy: EngineStoryPrivacy, + allowScreenshots: Bool = true, + pin: Bool = false, + timeout: Int = 0, + stateContext: StateContext, + completion: @escaping (EngineStoryPrivacy, Bool, Bool) -> Void, + editCategory: @escaping (EngineStoryPrivacy, Bool, Bool) -> Void + ) { self.context = context var categoryItems: [ShareWithPeersScreenComponent.CategoryItem] = [] - if case .stories = stateContext.subject { + var optionItems: [ShareWithPeersScreenComponent.OptionItem] = [] + if case let .stories(editing) = stateContext.subject { categoryItems.append(ShareWithPeersScreenComponent.CategoryItem( id: .everyone, title: "Everyone", @@ -1482,17 +1861,31 @@ public class ShareWithPeersScreen: ViewControllerComponentContainer { iconColor: .violet, actionTitle: selectedContactsSubtitle )) + + if !editing { + optionItems.append(ShareWithPeersScreenComponent.OptionItem( + id: .screenshot, + title: "Allow Screenshots" + )) + + optionItems.append(ShareWithPeersScreenComponent.OptionItem( + id: .pin, + title: "Keep on My Page" + )) + } } super.init(context: context, component: ShareWithPeersScreenComponent( context: context, stateContext: stateContext, initialPrivacy: initialPrivacy, + screenshot: allowScreenshots, + pin: pin, timeout: timeout, categoryItems: categoryItems, + optionItems: optionItems, completion: completion, - editCategory: editCategory, - secondaryAction: secondaryAction + editCategory: editCategory ), navigationBarAppearance: .none, theme: .dark) self.statusBar.statusBarStyle = .Ignore @@ -1523,7 +1916,22 @@ public class ShareWithPeersScreen: ViewControllerComponentContainer { } } + fileprivate func dismissAllTooltips() { + self.window?.forEachController { controller in + if let controller = controller as? TooltipScreen { + controller.dismiss() + } + } + self.forEachController { controller in + if let controller = controller as? TooltipScreen { + controller.dismiss() + } + return true + } + } + func requestDismiss() { + self.dismissAllTooltips() self.dismissed() self.dismiss() } diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContainerScreen.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContainerScreen.swift index 778b4d537f..ee826080c4 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContainerScreen.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContainerScreen.swift @@ -17,6 +17,7 @@ import AsyncDisplayKit import AttachmentUI import simd import VolumeButtons +import TooltipUI func hasFirstResponder(_ view: UIView) -> Bool { if view.isFirstResponder { @@ -827,7 +828,7 @@ private final class StoryContainerScreenComponent: Component { guard let self, let environment = self.environment else { return } - if c is UndoOverlayController { + if c is UndoOverlayController || c is TooltipScreen { environment.controller()?.present(c, in: .current) } else { environment.controller()?.present(c, in: .window(.root), with: a) diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift index 25b2123226..7e9e93d07d 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift @@ -31,6 +31,7 @@ import SaveToCameraRoll import BundleIconComponent import PeerListItemComponent import PremiumUI +import AttachmentUI public final class StoryItemSetContainerComponent: Component { public final class ExternalState { @@ -755,7 +756,7 @@ public final class StoryItemSetContainerComponent: Component { } if let navigationController = component.controller()?.navigationController as? NavigationController { let topViewController = navigationController.topViewController - if !(topViewController is StoryContainerScreen) && !(topViewController is MediaEditorScreen) && !(topViewController is ShareWithPeersScreen) { + if !(topViewController is StoryContainerScreen) && !(topViewController is MediaEditorScreen) && !(topViewController is ShareWithPeersScreen) && !(topViewController is AttachmentController) { return true } } @@ -2585,6 +2586,36 @@ public final class StoryItemSetContainerComponent: Component { return contentSize } + private func presentPrivacyTooltip(privacy: EngineStoryPrivacy) { + guard let component = self.component else { + return + } + + let text: String + if privacy.base == .contacts { + text = "This story is shown to all your contacts." + } else if privacy.base == .closeFriends { + text = "This story is shown to your close friends." + } else if privacy.base == .nobody { + if !privacy.additionallyIncludePeers.isEmpty { + text = "This story is shown to selected contacts." + } else { + text = "This story is shown only to you." + } + } else { + text = "This story is shown to everyone." + } + + let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } + self.component?.presentController(UndoOverlayController( + presentationData: presentationData, + content: .info(title: nil, text: text, timeout: nil), + elevatedLayout: false, + animateInAsReplacement: false, + action: { _ in return false } + ), nil) + } + private func openItemPrivacySettings(initialPrivacy: EngineStoryPrivacy? = nil) { guard let context = self.component?.context else { return @@ -2595,7 +2626,7 @@ public final class StoryItemSetContainerComponent: Component { return } - let stateContext = ShareWithPeersScreen.StateContext(context: context, subject: .stories, initialPeerIds: Set(privacy.additionallyIncludePeers)) + let stateContext = ShareWithPeersScreen.StateContext(context: context, subject: .stories(editing: true), initialPeerIds: Set(privacy.additionallyIncludePeers)) let _ = (stateContext.ready |> filter { $0 } |> take(1) |> deliverOnMainQueue).start(next: { [weak self] _ in guard let self else { return @@ -2603,18 +2634,19 @@ public final class StoryItemSetContainerComponent: Component { let controller = ShareWithPeersScreen( context: context, initialPrivacy: privacy, - timeout: 86400, stateContext: stateContext, - completion: { [weak self] privacy in + completion: { [weak self] privacy, _, _ in guard let self, let component = self.component else { return } let _ = component.context.engine.messages.editStoryPrivacy(id: component.slice.item.storyItem.id, privacy: privacy).start() + self.presentPrivacyTooltip(privacy: privacy) + self.privacyController = nil self.updateIsProgressPaused() }, - editCategory: { [weak self] privacy in + editCategory: { [weak self] privacy, _, _ in guard let self else { return } @@ -2652,9 +2684,8 @@ public final class StoryItemSetContainerComponent: Component { let controller = ShareWithPeersScreen( context: context, initialPrivacy: privacy, - timeout: 86400, stateContext: stateContext, - completion: { result in + completion: { result, _, _ in if case .closeFriends = privacy.base { let _ = context.engine.privacy.updateCloseFriends(peerIds: result.additionallyIncludePeers).start() completion(EngineStoryPrivacy(base: .closeFriends, additionallyIncludePeers: [])) @@ -2662,7 +2693,7 @@ public final class StoryItemSetContainerComponent: Component { completion(result) } }, - editCategory: { _ in } + editCategory: { _, _, _ in } ) controller.dismissed = { [weak self] in if let self { @@ -2687,17 +2718,32 @@ public final class StoryItemSetContainerComponent: Component { guard let navigationController = controller.navigationController as? NavigationController else { return } - guard let chatController = component.context.sharedContext.makePeerInfoController(context: component.context, updatedPresentationData: nil, peer: peer._asPeer(), mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) else { - return - } - - var viewControllers = navigationController.viewControllers - if let index = viewControllers.firstIndex(where: { $0 === controller }) { - viewControllers.insert(chatController, at: index) + if let messageId { + component.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: component.context, chatLocation: .peer(peer), subject: .message(id: .id(messageId), highlight: false, timecode: nil), keepStack: .always, animated: true, pushController: { [weak controller, weak navigationController] chatController, animated, completion in + guard let controller, let navigationController else { + return + } + var viewControllers = navigationController.viewControllers + if let index = viewControllers.firstIndex(where: { $0 === controller }) { + viewControllers.insert(chatController, at: index) + } else { + viewControllers.append(chatController) + } + navigationController.setViewControllers(viewControllers, animated: animated) + })) } else { - viewControllers.append(chatController) + guard let chatController = component.context.sharedContext.makePeerInfoController(context: component.context, updatedPresentationData: nil, peer: peer._asPeer(), mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) else { + return + } + + var viewControllers = navigationController.viewControllers + if let index = viewControllers.firstIndex(where: { $0 === controller }) { + viewControllers.insert(chatController, at: index) + } else { + viewControllers.append(chatController) + } + navigationController.setViewControllers(viewControllers, animated: true) } - navigationController.setViewControllers(viewControllers, animated: true) controller.dismissWithoutTransitionOut() } @@ -2934,6 +2980,8 @@ public final class StoryItemSetContainerComponent: Component { component.controller()?.forEachController { c in if let c = c as? UndoOverlayController { c.dismiss() + } else if let c = c as? TooltipScreen { + c.dismiss() } return true } @@ -2990,7 +3038,7 @@ public final class StoryItemSetContainerComponent: Component { items.append(.separator) - items.append(.action(ContextMenuActionItem(text: component.slice.item.storyItem.isPinned ? "Remove from profile" : "Save to profile", icon: { theme in + items.append(.action(ContextMenuActionItem(text: component.slice.item.storyItem.isPinned ? "Remove from Profile" : "Save to Profile", icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: component.slice.item.storyItem.isPinned ? "Chat/Context Menu/Check" : "Chat/Context Menu/Add"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, a in a(.default) @@ -3114,7 +3162,7 @@ public final class StoryItemSetContainerComponent: Component { var items: [ContextMenuItem] = [] let isMuted = settings.storiesMuted == true - items.append(.action(ContextMenuActionItem(text: isMuted ? "Notify" : "Not Notify", icon: { theme in + items.append(.action(ContextMenuActionItem(text: isMuted ? "Notify" : "Don't Notify", icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: component.slice.additionalPeerData.isMuted ? "Chat/Context Menu/Unmute" : "Chat/Context Menu/Muted"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, a in a(.default) @@ -3173,6 +3221,34 @@ public final class StoryItemSetContainerComponent: Component { } let _ = component.context.engine.peers.updatePeerStoriesHidden(id: component.slice.peer.id, isHidden: !isHidden) + + let text = isHidden ? "Stories from **\(component.slice.peer.compactDisplayTitle)** will now be shown in Chats, not Contacts." : "Stories from **\(component.slice.peer.compactDisplayTitle)** will now be shown in Contacts, not Chats." + let tooltipScreen = TooltipScreen( + context: component.context, + account: component.context.account, + sharedContext: component.context.sharedContext, + text: .markdown(text: text), + style: .customBlur(UIColor(rgb: 0x1c1c1c)), + icon: .peer(peer: component.slice.peer, isStory: true), + action: TooltipScreen.Action( + title: "Undo", + action: { + component.context.engine.peers.updatePeerStoriesHidden(id: component.slice.peer.id, isHidden: isHidden) + } + ), + location: .bottom, + shouldDismissOnTouch: { _ in return .dismiss(consume: false) } + ) + tooltipScreen.willBecomeDismissed = { [weak self] _ in + guard let self else { + return + } + self.sendMessageContext.tooltipScreen = nil + self.updateIsProgressPaused() + } + self.sendMessageContext.tooltipScreen = tooltipScreen + self.updateIsProgressPaused() + component.controller()?.present(tooltipScreen, in: .current) }))) items.append(.action(ContextMenuActionItem(text: "Report", icon: { theme in diff --git a/submodules/TelegramUI/Components/SwitchComponent/BUILD b/submodules/TelegramUI/Components/SwitchComponent/BUILD new file mode 100644 index 0000000000..6bbae84901 --- /dev/null +++ b/submodules/TelegramUI/Components/SwitchComponent/BUILD @@ -0,0 +1,23 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "SwitchComponent", + module_name = "SwitchComponent", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit", + "//submodules/AsyncDisplayKit:AsyncDisplayKit", + "//submodules/Display:Display", + "//submodules/ContextUI:ContextUI", + "//submodules/TelegramPresentationData:TelegramPresentationData", + "//submodules/ComponentFlow:ComponentFlow", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Components/SwitchComponent/Sources/SwitchComponent.swift b/submodules/TelegramUI/Components/SwitchComponent/Sources/SwitchComponent.swift new file mode 100644 index 0000000000..305628999b --- /dev/null +++ b/submodules/TelegramUI/Components/SwitchComponent/Sources/SwitchComponent.swift @@ -0,0 +1,78 @@ +import Foundation +import UIKit +import Display +import AsyncDisplayKit +import ComponentFlow +import TelegramPresentationData + +public final class SwitchComponent: Component { + public typealias EnvironmentType = Empty + + let tintColor: UIColor? + let value: Bool + let valueUpdated: (Bool) -> Void + + public init( + tintColor: UIColor? = nil, + value: Bool, + valueUpdated: @escaping (Bool) -> Void + ) { + self.tintColor = tintColor + self.value = value + self.valueUpdated = valueUpdated + } + + public static func ==(lhs: SwitchComponent, rhs: SwitchComponent) -> Bool { + if lhs.tintColor != rhs.tintColor { + return false + } + if lhs.value != rhs.value { + return false + } + return true + } + + public final class View: UIView { + private let switchView: UISwitch + + private var component: SwitchComponent? + + override init(frame: CGRect) { + self.switchView = UISwitch() + + super.init(frame: frame) + + self.addSubview(self.switchView) + + self.switchView.addTarget(self, action: #selector(self.valueChanged(_:)), for: .valueChanged) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + @objc func valueChanged(_ sender: Any) { + self.component?.valueUpdated(self.switchView.isOn) + } + + func update(component: SwitchComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + self.component = component + + self.switchView.tintColor = component.tintColor + self.switchView.setOn(component.value, animated: !transition.animation.isImmediate) + + self.switchView.sizeToFit() + self.switchView.frame = CGRect(origin: .zero, size: self.switchView.frame.size) + + return self.switchView.frame.size + } + } + + public func makeView() -> View { + return View(frame: CGRect()) + } + + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Media Editor/Next.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Media Editor/Next.imageset/Contents.json new file mode 100644 index 0000000000..ffc2f05f85 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Media Editor/Next.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "arrow_left.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Media Editor/Tools.imageset/ic_editor_tools.pdf b/submodules/TelegramUI/Images.xcassets/Media Editor/Next.imageset/arrow_left.pdf similarity index 76% rename from submodules/TelegramUI/Images.xcassets/Media Editor/Tools.imageset/ic_editor_tools.pdf rename to submodules/TelegramUI/Images.xcassets/Media Editor/Next.imageset/arrow_left.pdf index e7db29f833..7b20434672 100644 Binary files a/submodules/TelegramUI/Images.xcassets/Media Editor/Tools.imageset/ic_editor_tools.pdf and b/submodules/TelegramUI/Images.xcassets/Media Editor/Next.imageset/arrow_left.pdf differ diff --git a/submodules/TelegramUI/Images.xcassets/Media Editor/Pencil.imageset/pencil_30.pdf b/submodules/TelegramUI/Images.xcassets/Media Editor/Pencil.imageset/pencil_30.pdf index 9cb855fa62..20a9307e93 100644 --- a/submodules/TelegramUI/Images.xcassets/Media Editor/Pencil.imageset/pencil_30.pdf +++ b/submodules/TelegramUI/Images.xcassets/Media Editor/Pencil.imageset/pencil_30.pdf @@ -10,35 +10,35 @@ stream /DeviceRGB CS /DeviceRGB cs q -1.000000 0.000000 -0.000000 1.000000 4.335022 4.334961 cm +1.000000 0.000000 -0.000000 1.000000 4.170021 4.169979 cm 1.000000 1.000000 1.000000 scn -1.330000 10.665017 m -1.330000 15.820595 5.509422 20.000017 10.665000 20.000017 c -15.820578 20.000017 20.000000 15.820595 20.000000 10.665017 c -20.000000 7.511573 18.436378 4.723331 16.042154 3.033316 c -14.314164 10.809276 l -14.246551 11.113539 13.976685 11.330017 13.665000 11.330017 c -13.443334 11.330017 l -11.613684 16.818966 l -11.309785 17.730663 10.020216 17.730665 9.716317 16.818970 c -7.886667 11.330017 l -7.665000 11.330017 l -7.353315 11.330017 7.083449 11.113539 7.015836 10.809276 c -5.287845 3.033316 l -2.893622 4.723331 1.330000 7.511574 1.330000 10.665017 c +1.660000 10.830000 m +1.660000 15.894451 5.765549 20.000000 10.830000 20.000000 c +15.894451 20.000000 20.000000 15.894451 20.000000 10.830000 c +20.000000 7.821688 18.551388 5.151716 16.313671 3.479595 c +14.640235 11.010053 l +14.555845 11.389809 14.219020 11.660000 13.830000 11.660000 c +13.553333 11.660000 l +11.778684 16.983950 l +11.474785 17.895645 10.185216 17.895649 9.881317 16.983952 c +8.106667 11.660000 l +7.830000 11.660000 l +7.440980 11.660000 7.104155 11.389809 7.019765 11.010053 c +5.346330 3.479595 l +3.108612 5.151716 1.660000 7.821688 1.660000 10.830000 c h -13.131556 10.000017 m -14.839726 2.313250 l -13.583508 1.684090 12.165603 1.330017 10.665000 1.330017 c -9.164397 1.330017 7.746492 1.684090 6.490273 2.313250 c -8.198444 10.000017 l -13.131556 10.000017 l +13.164198 10.000000 m +14.815517 2.569059 l +13.610337 1.986547 12.258304 1.660000 10.830000 1.660000 c +9.401696 1.660000 8.049662 1.986547 6.844482 2.569059 c +8.495803 10.000000 l +13.164198 10.000000 l h -10.665000 21.330017 m -4.774883 21.330017 0.000000 16.555134 0.000000 10.665017 c -0.000000 4.774900 4.774883 0.000015 10.665000 0.000015 c -16.555117 0.000015 21.330002 4.774900 21.330002 10.665017 c -21.330002 16.555134 16.555117 21.330017 10.665000 21.330017 c +10.830000 21.660000 m +4.848756 21.660000 0.000000 16.811243 0.000000 10.830000 c +0.000000 4.848755 4.848756 0.000000 10.830000 0.000000 c +16.811245 0.000000 21.660000 4.848755 21.660000 10.830000 c +21.660000 16.811243 16.811245 21.660000 10.830000 21.660000 c h f* n diff --git a/submodules/TelegramUI/Images.xcassets/Media Editor/Tools.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Media Editor/Tools.imageset/Contents.json index 3dbd2aa827..cb8e30c043 100644 --- a/submodules/TelegramUI/Images.xcassets/Media Editor/Tools.imageset/Contents.json +++ b/submodules/TelegramUI/Images.xcassets/Media Editor/Tools.imageset/Contents.json @@ -1,7 +1,7 @@ { "images" : [ { - "filename" : "ic_editor_tools.pdf", + "filename" : "tools_30.pdf", "idiom" : "universal" } ], diff --git a/submodules/TelegramUI/Images.xcassets/Media Editor/Tools.imageset/tools_30.pdf b/submodules/TelegramUI/Images.xcassets/Media Editor/Tools.imageset/tools_30.pdf new file mode 100644 index 0000000000..25301318c7 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Media Editor/Tools.imageset/tools_30.pdf @@ -0,0 +1,101 @@ +%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.170021 4.169979 cm +1.000000 1.000000 1.000000 scn +5.830000 21.660000 m +6.288396 21.660000 6.660000 21.288397 6.660000 20.830000 c +6.660000 17.660000 l +20.830000 17.660000 l +21.288397 17.660000 21.660000 17.288397 21.660000 16.830000 c +21.660000 16.371603 21.288397 16.000000 20.830000 16.000000 c +6.660000 16.000000 l +6.660000 12.830000 l +6.660000 12.371604 6.288396 12.000000 5.830000 12.000000 c +5.371603 12.000000 5.000000 12.371604 5.000000 12.830000 c +5.000000 16.000000 l +0.830000 16.000000 l +0.371603 16.000000 0.000000 16.371603 0.000000 16.830000 c +0.000000 17.288397 0.371603 17.660000 0.830000 17.660000 c +5.000000 17.660000 l +5.000000 20.830000 l +5.000000 21.288397 5.371603 21.660000 5.830000 21.660000 c +h +20.830000 3.999998 m +21.288397 3.999998 21.660000 4.371601 21.660000 4.829998 c +21.660000 5.288395 21.288397 5.659998 20.830000 5.659998 c +16.660000 5.659998 l +16.660000 8.830000 l +16.660000 9.288396 16.288397 9.660000 15.830000 9.660000 c +15.371604 9.660000 15.000000 9.288396 15.000000 8.830000 c +15.000000 5.659998 l +0.830000 5.659998 l +0.371603 5.659998 0.000000 5.288395 0.000000 4.829998 c +0.000000 4.371601 0.371603 3.999998 0.830000 3.999998 c +15.000000 3.999998 l +15.000000 0.829998 l +15.000000 0.371603 15.371604 0.000000 15.830000 0.000000 c +16.288397 0.000000 16.660000 0.371603 16.660000 0.829998 c +16.660000 3.999998 l +20.830000 3.999998 l +h +f* +n +Q + +endstream +endobj + +3 0 obj + 1452 +endobj + +4 0 obj + << /Annots [] + /Type /Page + /MediaBox [ 0.000000 0.000000 30.000000 30.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 +0000001542 00000 n +0000001565 00000 n +0000001738 00000 n +0000001812 00000 n +trailer +<< /ID [ (some) (id) ] + /Root 6 0 R + /Size 7 +>> +startxref +1871 +%%EOF \ No newline at end of file diff --git a/submodules/TelegramUI/Resources/Animations/anim_profileadd.tgs b/submodules/TelegramUI/Resources/Animations/anim_profileadd.tgs new file mode 100644 index 0000000000..8b28b004bc Binary files /dev/null and b/submodules/TelegramUI/Resources/Animations/anim_profileadd.tgs differ diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index 670cb1e452..af41d6ff98 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -14502,7 +14502,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G return } - let tooltipScreen = TooltipScreen(account: self.context.account, sharedContext: self.context.sharedContext, text: .entities(text: solution.text, entities: solution.entities), icon: .animation(name: "anim_infotip", delay: 0.2), location: .top, shouldDismissOnTouch: { point in + let tooltipScreen = TooltipScreen(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 { @@ -14595,7 +14595,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G return } - let tooltipScreen = TooltipScreen(account: self.context.account, sharedContext: self.context.sharedContext, text: .entities(text: psaText, entities: psaEntities), icon: .animation(name: "anim_infotip", delay: 0.2), location: .top, displayDuration: .custom(10.0), shouldDismissOnTouch: { point in + let tooltipScreen = TooltipScreen(account: self.context.account, sharedContext: self.context.sharedContext, text: .entities(text: psaText, entities: psaEntities), icon: .animation(name: "anim_infotip", delay: 0.2, tintColor: nil), location: .top, displayDuration: .custom(10.0), shouldDismissOnTouch: { point in return .ignore }, openActiveTextItem: { [weak self] item, action in guard let strongSelf = self else { @@ -14709,7 +14709,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G return } - let tooltipScreen = TooltipScreen(account: self.context.account, sharedContext: self.context.sharedContext, text: .entities(text: psaText, entities: psaEntities), icon: .animation(name: "anim_infotip", delay: 0.2), location: .top, displayDuration: .custom(10.0), shouldDismissOnTouch: { point in + let tooltipScreen = TooltipScreen(account: self.context.account, sharedContext: self.context.sharedContext, text: .entities(text: psaText, entities: psaEntities), icon: .animation(name: "anim_infotip", delay: 0.2, tintColor: nil), location: .top, displayDuration: .custom(10.0), shouldDismissOnTouch: { point in return .ignore }, openActiveTextItem: { [weak self] item, action in guard let strongSelf = self else { diff --git a/submodules/TelegramUI/Sources/TelegramRootController.swift b/submodules/TelegramUI/Sources/TelegramRootController.swift index d694126b59..b5d2010422 100644 --- a/submodules/TelegramUI/Sources/TelegramRootController.swift +++ b/submodules/TelegramUI/Sources/TelegramRootController.swift @@ -24,10 +24,8 @@ import LegacyMediaPickerUI import LegacyCamera import AvatarNode import LocalMediaResources -import ShareWithPeersScreen import ImageCompression import TextFormat -import UndoUI private class DetailsChatPlaceholderNode: ASDisplayNode, NavigationDetailsPlaceholderNode { private var presentationData: PresentationData @@ -363,13 +361,19 @@ public final class TelegramRootController: NavigationController, TelegramRootCon return } + if let rootTabController = self.rootTabController { + if let index = rootTabController.controllers.firstIndex(where: { $0 is ChatListController}) { + rootTabController.selectedIndex = index + } + } + if let chatListController = self.chatListController as? ChatListControllerImpl { chatListController.scrollToStories() switch mediaResult { case let .image(image, dimensions): if let imageData = compressImageToJPEG(image, quality: 0.7) { let entities = generateChatInputTextEntities(caption) - self.context.engine.messages.uploadStory(media: .image(dimensions: dimensions, data: imageData), text: caption.string, entities: entities, pin: privacy.archive, privacy: privacy.privacy, isForwardingDisabled: false, period: privacy.timeout, randomId: randomId) + self.context.engine.messages.uploadStory(media: .image(dimensions: dimensions, data: imageData), text: caption.string, entities: entities, pin: privacy.pin, privacy: privacy.privacy, isForwardingDisabled: privacy.isForwardingDisabled, period: privacy.timeout, randomId: randomId) Queue.mainQueue().justDispatch { commit({}) } @@ -392,7 +396,7 @@ public final class TelegramRootController: NavigationController, TelegramRootCon } let imageData = firstFrameImage.flatMap { compressImageToJPEG($0, quality: 0.6) } let entities = generateChatInputTextEntities(caption) - self.context.engine.messages.uploadStory(media: .video(dimensions: dimensions, duration: duration, resource: resource, firstFrameImageData: imageData), text: caption.string, entities: entities, pin: privacy.archive, privacy: privacy.privacy, isForwardingDisabled: false, period: privacy.timeout, randomId: randomId) + self.context.engine.messages.uploadStory(media: .video(dimensions: dimensions, duration: duration, resource: resource, firstFrameImageData: imageData), text: caption.string, entities: entities, pin: privacy.pin, privacy: privacy.privacy, isForwardingDisabled: privacy.isForwardingDisabled, period: privacy.timeout, randomId: randomId) Queue.mainQueue().justDispatch { commit({}) } diff --git a/submodules/TooltipUI/Sources/TooltipScreen.swift b/submodules/TooltipUI/Sources/TooltipScreen.swift index 86d94718dd..3952b1a394 100644 --- a/submodules/TooltipUI/Sources/TooltipScreen.swift +++ b/submodules/TooltipUI/Sources/TooltipScreen.swift @@ -132,7 +132,7 @@ private final class TooltipScreenNode: ViewControllerTracingNode { private var arrowGradientNode: ASDisplayNode? private let arrowNode: ASImageNode private let arrowContainer: ASDisplayNode - private let animatedStickerNode: AnimatedStickerNode + private let animatedStickerNode: DefaultAnimatedStickerNodeImpl private var downArrowsNode: DownArrowsIconNode? private var avatarNode: AvatarNode? private var avatarStoryIndicator: ComponentView? @@ -157,7 +157,7 @@ private final class TooltipScreenNode: ViewControllerTracingNode { action: TooltipScreen.Action? = nil, location: TooltipScreen.Location, displayDuration: TooltipScreen.DisplayDuration, - inset: CGFloat = 13.0, + inset: CGFloat = 12.0, cornerRadius: CGFloat? = nil, shouldDismissOnTouch: @escaping (CGPoint) -> TooltipScreen.DismissOnTouch, requestDismiss: @escaping () -> Void, openActiveTextItem: ((TooltipActiveTextItem, TooltipActiveTextAction) -> Void)?) { @@ -229,18 +229,31 @@ private final class TooltipScreenNode: ViewControllerTracingNode { self.arrowContainer = ASDisplayNode() - let fontSize: CGFloat + var hasArrow = true if case .top = location { + hasArrow = false + } else if case .bottom = location { + hasArrow = false + } + + let fontSize: CGFloat + if !hasArrow { let backgroundColor: UIColor - if theme.overallDarkAppearance { - backgroundColor = theme.rootController.navigationBar.blurredBackgroundColor + var enableSaturation = true + if case let .customBlur(color) = style { + backgroundColor = color + enableSaturation = false } else { - backgroundColor = UIColor(rgb: 0x000000, alpha: 0.6) + if theme.overallDarkAppearance { + backgroundColor = theme.rootController.navigationBar.blurredBackgroundColor + } else { + backgroundColor = UIColor(rgb: 0x000000, alpha: 0.6) + } } - self.effectNode = NavigationBackgroundNode(color: backgroundColor) + self.effectNode = NavigationBackgroundNode(color: backgroundColor, enableSaturation: enableSaturation) self.backgroundMaskNode.addSubnode(self.backgroundClipNode) self.backgroundClipNode.clipsToBounds = true - if case let .point(_, arrowPosition) = location, case .right = arrowPosition { + if case .bottom = location { self.backgroundClipNode.cornerRadius = 8.5 } else { self.backgroundClipNode.cornerRadius = 14.0 @@ -248,7 +261,6 @@ private final class TooltipScreenNode: ViewControllerTracingNode { if #available(iOS 13.0, *) { self.backgroundClipNode.layer.cornerCurve = .continuous } - fontSize = 14.0 } else if case let .gradient(leftColor, rightColor) = style { self.gradientNode = ASDisplayNode() @@ -362,9 +374,10 @@ private final class TooltipScreenNode: ViewControllerTracingNode { switch icon { case .none: break - case let .animation(animationName, _): + case let .animation(animationName, _, animationTintColor): self.animatedStickerNode.setup(source: AnimatedStickerNodeLocalFileSource(name: animationName), width: Int(70 * UIScreenScale), height: Int(70 * UIScreenScale), playbackMode: .once, mode: .direct(cachePathPrefix: nil)) self.animatedStickerNode.automaticallyLoadFirstFrame = true + self.animatedStickerNode.dynamicColor = animationTintColor case .downArrows: self.downArrowsNode = DownArrowsIconNode() case let .peer(peer, _): @@ -498,21 +511,25 @@ private final class TooltipScreenNode: ViewControllerTracingNode { let contentInset: CGFloat = 11.0 let contentVerticalInset: CGFloat = 8.0 let animationSize: CGSize - let animationInset: CGFloat - let animationSpacing: CGFloat + var animationInset: CGFloat = 0.0 + var animationSpacing: CGFloat = 0.0 + var animationOffset: CGFloat = 0.0 switch self.icon { case .none: animationSize = CGSize() - animationInset = 0.0 - animationSpacing = 0.0 case .downArrows: animationSize = CGSize(width: 24.0, height: 32.0) animationInset = (40.0 - animationSize.width) / 2.0 - animationSpacing = 8.0 - case let .animation(animationName, _): - animationSize = CGSize(width: 32.0, height: 32.0) - if animationName == "ChatListFoldersTooltip" { + case let .animation(animationName, _, _): + if animationName == "premium_unlock" { + animationSize = CGSize(width: 34.0, height: 34.0) + } else { + animationSize = CGSize(width: 32.0, height: 32.0) + } + if animationName == "anim_autoremove_on" { + animationOffset = -3.0 + } else if animationName == "ChatListFoldersTooltip" { animationInset = (70.0 - animationSize.width) / 2.0 } else { animationInset = 0.0 @@ -543,14 +560,16 @@ private final class TooltipScreenNode: ViewControllerTracingNode { var backgroundHeight: CGFloat switch self.tooltipStyle { - case .default, .gradient, .customBlur: + case .default, .gradient, .customBlur, .wide: backgroundHeight = max(animationSize.height, textSize.height) + contentVerticalInset * 2.0 - if self.actionButtonNode != nil { - backgroundHeight += 2.0 - } case .light: backgroundHeight = max(28.0, max(animationSize.height, textSize.height) + 4.0 * 2.0) } + if case .wide = self.tooltipStyle { + backgroundHeight += 4.0 + } else if self.actionButtonNode != nil { + backgroundHeight += 4.0 + } var invertArrow = false switch self.location { @@ -587,6 +606,9 @@ private final class TooltipScreenNode: ViewControllerTracingNode { case .top: let backgroundWidth = containerWidth backgroundFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - backgroundWidth) / 2.0), y: layout.insets(options: [.statusBar]).top + 13.0), size: CGSize(width: backgroundWidth, height: backgroundHeight)) + case .bottom: + let backgroundWidth = containerWidth + backgroundFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - backgroundWidth) / 2.0), y: layout.size.height - layout.insets(options: []).bottom - 12.0 - backgroundHeight), size: CGSize(width: backgroundWidth, height: backgroundHeight)) } transition.updateFrame(node: self.containerNode, frame: backgroundFrame) @@ -653,7 +675,7 @@ private final class TooltipScreenNode: ViewControllerTracingNode { transition.updateFrame(node: actionButtonNode, frame: CGRect(origin: CGPoint(x: backgroundFrame.width - actionSize.width - 16.0, y: floor((backgroundHeight - actionSize.height) / 2.0)), size: actionSize)) } - let animationFrame = CGRect(origin: CGPoint(x: contentInset - animationInset, y: floorToScreenPixels((backgroundHeight - animationSize.height - animationInset * 2.0) / 2.0)), size: CGSize(width: animationSize.width + animationInset * 2.0, height: animationSize.height + animationInset * 2.0)) + let animationFrame = CGRect(origin: CGPoint(x: contentInset - animationInset, y: floorToScreenPixels((backgroundHeight - animationSize.height - animationInset * 2.0) / 2.0) + animationOffset), size: CGSize(width: animationSize.width + animationInset * 2.0, height: animationSize.height + animationInset * 2.0)) transition.updateFrame(node: self.animatedStickerNode, frame: animationFrame) self.animatedStickerNode.updateLayout(size: CGSize(width: animationSize.width + animationInset * 2.0, height: animationSize.height + animationInset * 2.0)) @@ -756,11 +778,18 @@ private final class TooltipScreenNode: ViewControllerTracingNode { func animateIn() { switch self.location { - case .top: - self.containerNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + case .top, .bottom: + self.containerNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) self.containerNode.layer.animateScale(from: 0.96, to: 1.0, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring) - if let _ = self.validLayout { - self.containerNode.layer.animatePosition(from: CGPoint(x: 0.0, y: -13.0 - self.backgroundContainerNode.frame.height), to: CGPoint(), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, additive: true) + + if let _ = self.validLayout, case .top = self.location { + let offset: CGFloat + if case .top = self.location { + offset = -13.0 - self.backgroundContainerNode.frame.height + } else { + offset = 13.0 + self.backgroundContainerNode.frame.height + } + self.containerNode.layer.animatePosition(from: CGPoint(x: 0.0, y: offset), to: CGPoint(), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, additive: true) } case let .point(_, arrowPosition): self.containerNode.layer.animateSpring(from: NSNumber(value: Float(0.01)), to: NSNumber(value: Float(1.0)), keyPath: "transform.scale", duration: 0.4, damping: 105.0) @@ -780,7 +809,7 @@ private final class TooltipScreenNode: ViewControllerTracingNode { let animationDelay: Double switch self.icon { - case let .animation(_, delay): + case let .animation(_, delay, _): animationDelay = delay case .none, .downArrows: animationDelay = 0.0 @@ -793,15 +822,21 @@ private final class TooltipScreenNode: ViewControllerTracingNode { }) } - func animateOut(completion: @escaping () -> Void) { + func animateOut(inPlace: Bool, completion: @escaping () -> Void) { switch self.location { - case .top: - self.containerNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { _ in + case .top, .bottom: + self.containerNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, timingFunction: CAMediaTimingFunctionName.easeOut.rawValue, removeOnCompletion: false, completion: { _ in completion() }) self.containerNode.layer.animateScale(from: 1.0, to: 0.96, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false) - if let _ = self.validLayout { - self.containerNode.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: -13.0 - self.backgroundContainerNode.frame.height), duration: 0.3, removeOnCompletion: false, additive: true) + if let _ = self.validLayout, case .top = self.location, !inPlace { + let offset: CGFloat + if case .top = self.location { + offset = -13.0 - self.backgroundContainerNode.frame.height + } else { + offset = 13.0 + self.backgroundContainerNode.frame.height + } + self.containerNode.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: offset), duration: 0.3, removeOnCompletion: false, additive: true) } case let .point(_, arrowPosition): self.containerNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { _ in @@ -856,7 +891,7 @@ public final class TooltipScreen: ViewController { } public enum Icon { - case animation(name: String, delay: Double) + case animation(name: String, delay: Double, tintColor: UIColor?) case peer(peer: EnginePeer, isStory: Bool) case downArrows } @@ -875,6 +910,7 @@ public final class TooltipScreen: ViewController { public enum Location { case point(CGRect, ArrowPosition) case top + case bottom } public enum DisplayDuration { @@ -889,6 +925,7 @@ public final class TooltipScreen: ViewController { case light case customBlur(UIColor) case gradient(UIColor, UIColor) + case wide } public enum Alignment { @@ -942,7 +979,7 @@ public final class TooltipScreen: ViewController { action: TooltipScreen.Action? = nil, location: TooltipScreen.Location, displayDuration: DisplayDuration = .default, - inset: CGFloat = 13.0, + inset: CGFloat = 12.0, cornerRadius: CGFloat? = nil, shouldDismissOnTouch: @escaping (CGPoint) -> TooltipScreen.DismissOnTouch, openActiveTextItem: ((TooltipActiveTextItem, TooltipActiveTextAction) -> Void)? = nil @@ -1044,13 +1081,13 @@ public final class TooltipScreen: ViewController { self.controllerNode.addRelativeScrollingOffset(value, transition: transition) } - override public func dismiss(completion: (() -> Void)? = nil) { + public func dismiss(inPlace: Bool, completion: (() -> Void)? = nil) { if self.isDismissed { return } self.isDismissed = true self.willBecomeDismissed?(self) - self.controllerNode.animateOut(completion: { [weak self] in + self.controllerNode.animateOut(inPlace: inPlace, completion: { [weak self] in guard let strongSelf = self else { return } @@ -1059,4 +1096,8 @@ public final class TooltipScreen: ViewController { becameDismissed?(strongSelf) }) } + + override public func dismiss(completion: (() -> Void)? = nil) { + self.dismiss(inPlace: false, completion: completion) + } } diff --git a/submodules/UndoUI/Sources/UndoOverlayControllerNode.swift b/submodules/UndoUI/Sources/UndoOverlayControllerNode.swift index 3387014347..20d8699e14 100644 --- a/submodules/UndoUI/Sources/UndoOverlayControllerNode.swift +++ b/submodules/UndoUI/Sources/UndoOverlayControllerNode.swift @@ -1374,6 +1374,9 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode { } else { self.panelNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) self.panelWrapperNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) + + self.panelNode.layer.animateScale(from: 0.96, to: 1.0, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring) + self.panelWrapperNode.layer.animateScale(from: 0.96, to: 1.0, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring) } if let iconCheckNode = self.iconCheckNode, self.iconNode != nil { @@ -1403,6 +1406,8 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode { self.panelWrapperNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, delay: 0.0, timingFunction: CAMediaTimingFunctionName.easeOut.rawValue, removeOnCompletion: false) { _ in completion() } + self.panelNode.layer.animateScale(from: 1.0, to: 0.96, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false) + self.panelWrapperNode.layer.animateScale(from: 1.0, to: 0.96, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false) } func animateOutWithReplacement(completion: @escaping () -> Void) {