diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index 3c13a0edfa..4b54f5fb5d 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -15988,7 +15988,14 @@ Error: %8$@"; "Chat.PolOptionAddedTimestamp.TodayAt" = "Added by %1$@ today at %2$@"; "Chat.PolOptionAddedTimestamp.YesterdayAt" = "Added by %1$@ yesterday at %2$@"; - "Chat.PolOptionAddedTimestampYou.Date" = "Added by you %1$@"; "Chat.PolOptionAddedTimestampYou.TodayAt" = "Added by you today at %1$@"; "Chat.PolOptionAddedTimestampYou.YesterdayAt" = "Added by you yesterday at %1$@"; + +"MessagePoll.EndsIn" = "ends in %@"; +"MessagePoll.EndsInDays_1" = "ends in %@ day"; +"MessagePoll.EndsInDays_any" = "ends in %@ days"; + +"MessagePoll.ResultsIn" = "results in %@"; +"MessagePoll.ResultsInDays_1" = "results in %@ day"; +"MessagePoll.ResultsInDays_any" = "results in %@ days"; diff --git a/submodules/GalleryData/Sources/GalleryData.swift b/submodules/GalleryData/Sources/GalleryData.swift index 7c3cc05adc..12d7c199eb 100644 --- a/submodules/GalleryData/Sources/GalleryData.swift +++ b/submodules/GalleryData/Sources/GalleryData.swift @@ -116,7 +116,7 @@ public func chatMessageGalleryControllerData(context: AccountContext, chatLocati if let poll = media as? TelegramMediaPoll, let attachedMedia = poll.attachedMedia { if attachedMedia is TelegramMediaImage { galleryMedia = attachedMedia - } else if let file = attachedMedia as? TelegramMediaFile, file.isVideo || file.isMusic { + } else if let _ = attachedMedia as? TelegramMediaFile { galleryMedia = attachedMedia } else if attachedMedia is TelegramMediaMap { galleryMedia = attachedMedia diff --git a/submodules/LegacyComponents/Sources/TGMediaAssetsController.m b/submodules/LegacyComponents/Sources/TGMediaAssetsController.m index 5d4ee533a8..cb8298c2fc 100644 --- a/submodules/LegacyComponents/Sources/TGMediaAssetsController.m +++ b/submodules/LegacyComponents/Sources/TGMediaAssetsController.m @@ -976,9 +976,9 @@ if (adjustments.paintingData.hasAnimation) { grouping = false; } - if ([editingContext livePhotoModeForItem:asset] != TGMediaLivePhotoModeOff) { - grouping = false; - } +// if ([editingContext livePhotoModeForItem:asset] != TGMediaLivePhotoModeOff) { +// grouping = false; +// } } } @@ -1544,9 +1544,9 @@ if (adjustments.paintingData.hasAnimation) { grouping = false; } - if ([editingContext livePhotoModeForItem:asset] != TGMediaLivePhotoModeOff) { - grouping = false; - } +// if ([editingContext livePhotoModeForItem:asset] != TGMediaLivePhotoModeOff) { +// grouping = false; +// } } } diff --git a/submodules/LegacyComponents/Sources/TGMediaPickerGalleryInterfaceView.m b/submodules/LegacyComponents/Sources/TGMediaPickerGalleryInterfaceView.m index 6f3d8df1c1..3d46c2d8ca 100644 --- a/submodules/LegacyComponents/Sources/TGMediaPickerGalleryInterfaceView.m +++ b/submodules/LegacyComponents/Sources/TGMediaPickerGalleryInterfaceView.m @@ -411,6 +411,8 @@ id galleryEditableItem = (id)strongSelf->_currentItem; [strongSelf->_editingContext setLivePhotoMode:(TGMediaLivePhotoMode)mode forItem:galleryEditableItem.editableMediaItem]; + + [strongSelf->_selectionContext setItem:(id)galleryEditableItem.editableMediaItem selected:true animated:true sender:nil]; }; _captionMixin.stickersContext = stickersContext; diff --git a/submodules/LocationUI/Sources/LocationMapHeaderNode.swift b/submodules/LocationUI/Sources/LocationMapHeaderNode.swift index 3ed3657b9a..34d88662f9 100644 --- a/submodules/LocationUI/Sources/LocationMapHeaderNode.swift +++ b/submodules/LocationUI/Sources/LocationMapHeaderNode.swift @@ -74,6 +74,7 @@ public final class LocationMapHeaderNode: ASDisplayNode { public init( presentationData: PresentationData, glass: Bool, + isPreview: Bool = false, toggleMapModeSelection: @escaping () -> Void, updateMapMode: @escaping (LocationMapMode) -> Void, goToUserLocation: @escaping () -> Void, @@ -157,21 +158,23 @@ public final class LocationMapHeaderNode: ASDisplayNode { self.addSubnode(self.mapNode) - if glass { - if let placesBackgroundView = self.placesBackgroundView { - self.placesBackgroundNode.view.addSubview(placesBackgroundView) + if !isPreview { + if glass { + if let placesBackgroundView = self.placesBackgroundView { + self.placesBackgroundNode.view.addSubview(placesBackgroundView) + } + } else { + if let optionsBackgroundView = self.optionsBackgroundView { + self.optionsSeparatorNode.isHidden = true + self.view.addSubview(optionsBackgroundView) + } + self.addSubnode(self.optionsBackgroundNode) + self.optionsBackgroundNode.addSubnode(self.optionsSeparatorNode) + self.optionsBackgroundNode.addSubnode(self.optionsSecondSeparatorNode) + self.optionsBackgroundNode.addSubnode(self.infoButtonNode) + self.optionsBackgroundNode.addSubnode(self.locationButtonNode) + self.optionsBackgroundNode.addSubnode(self.notificationButtonNode) } - } else { - if let optionsBackgroundView = self.optionsBackgroundView { - self.optionsSeparatorNode.isHidden = true - self.view.addSubview(optionsBackgroundView) - } - self.addSubnode(self.optionsBackgroundNode) - self.optionsBackgroundNode.addSubnode(self.optionsSeparatorNode) - self.optionsBackgroundNode.addSubnode(self.optionsSecondSeparatorNode) - self.optionsBackgroundNode.addSubnode(self.infoButtonNode) - self.optionsBackgroundNode.addSubnode(self.locationButtonNode) - self.optionsBackgroundNode.addSubnode(self.notificationButtonNode) } self.addSubnode(self.placesBackgroundNode) diff --git a/submodules/LocationUI/Sources/LocationPickerControllerNode.swift b/submodules/LocationUI/Sources/LocationPickerControllerNode.swift index 1059850d20..8e8f4496a5 100644 --- a/submodules/LocationUI/Sources/LocationPickerControllerNode.swift +++ b/submodules/LocationUI/Sources/LocationPickerControllerNode.swift @@ -1416,7 +1416,7 @@ final class LocationPickerControllerNode: ViewControllerTracingNode, CLLocationM ) ), environment: {}, - containerSize: CGSize(width: 200.0, height: 40.0) + containerSize: CGSize(width: 230.0, height: 40.0) ) combinedTitleHeight += subtitleSize.height + titleSpacing diff --git a/submodules/LocationUI/Sources/LocationViewController.swift b/submodules/LocationUI/Sources/LocationViewController.swift index 1a19c949e0..519c96a91d 100644 --- a/submodules/LocationUI/Sources/LocationViewController.swift +++ b/submodules/LocationUI/Sources/LocationViewController.swift @@ -78,6 +78,7 @@ public final class LocationViewController: ViewController { private var presentationDataDisposable: Disposable? private var showAll: Bool private let isStoryLocation: Bool + private let isPreview: Bool private let locationManager = LocationManager() @@ -87,20 +88,30 @@ public final class LocationViewController: ViewController { public var dismissed: () -> Void = {} - public init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, subject: EngineMessage, isStoryLocation: Bool = false, params: LocationViewParams) { + public init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, subject: EngineMessage, isStoryLocation: Bool = false, isPreview: Bool = false, params: LocationViewParams) { self.context = context self.subject = subject self.showAll = params.showAll self.isStoryLocation = isStoryLocation + self.isPreview = isPreview self.presentationData = updatedPresentationData?.initial ?? context.sharedContext.currentPresentationData.with { $0 } - super.init(navigationBarPresentationData: NavigationBarPresentationData(theme: NavigationBarTheme(rootControllerTheme: self.presentationData.theme).withUpdatedSeparatorColor(.clear), strings: NavigationBarStrings(presentationStrings: self.presentationData.strings))) + let navigationBarPresentationData: NavigationBarPresentationData? + if !isPreview { + navigationBarPresentationData = NavigationBarPresentationData(theme: NavigationBarTheme(rootControllerTheme: self.presentationData.theme).withUpdatedSeparatorColor(.clear), strings: NavigationBarStrings(presentationStrings: self.presentationData.strings)) + } else { + navigationBarPresentationData = nil + } + + super.init(navigationBarPresentationData: navigationBarPresentationData) self.navigationPresentation = .modal - self.title = self.presentationData.strings.Map_LocationTitle - self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Close, style: .plain, target: self, action: #selector(self.cancelPressed)) + if !self.isPreview { + self.title = self.presentationData.strings.Map_LocationTitle + self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Close, style: .plain, target: self, action: #selector(self.cancelPressed)) + } self.presentationDataDisposable = ((updatedPresentationData?.signal ?? context.sharedContext.presentationData) |> deliverOnMainQueue).start(next: { [weak self] presentationData in @@ -504,7 +515,7 @@ public final class LocationViewController: ViewController { return } - self.displayNode = LocationViewControllerNode(context: self.context, presentationData: self.presentationData, subject: self.subject, interaction: interaction, locationManager: self.locationManager, isStoryLocation: self.isStoryLocation) + self.displayNode = LocationViewControllerNode(context: self.context, presentationData: self.presentationData, subject: self.subject, interaction: interaction, locationManager: self.locationManager, isStoryLocation: self.isStoryLocation, isPreview: self.isPreview) self.displayNodeDidLoad() self.controllerNode.onAnnotationsReady = { [weak self] in @@ -518,6 +529,9 @@ public final class LocationViewController: ViewController { } private func updateRightBarButton() { + guard !self.isPreview else { + return + } switch self.rightBarButtonAction { case .none: self.navigationItem.rightBarButtonItem = nil diff --git a/submodules/LocationUI/Sources/LocationViewControllerNode.swift b/submodules/LocationUI/Sources/LocationViewControllerNode.swift index 82c3bf8097..f8c143d244 100644 --- a/submodules/LocationUI/Sources/LocationViewControllerNode.swift +++ b/submodules/LocationUI/Sources/LocationViewControllerNode.swift @@ -274,6 +274,7 @@ final class LocationViewControllerNode: ViewControllerTracingNode, CLLocationMan private let interaction: LocationViewInteraction private let locationManager: LocationManager private let isStoryLocation: Bool + private let isPreview: Bool private let listNode: ListView let headerNode: LocationMapHeaderNode @@ -301,7 +302,7 @@ final class LocationViewControllerNode: ViewControllerTracingNode, CLLocationMan } private let travelTimesPromise = Promise<[EngineMessage.Id: (Double, ExpectedTravelTime, ExpectedTravelTime, ExpectedTravelTime)]>([:]) - init(context: AccountContext, presentationData: PresentationData, subject: EngineMessage, interaction: LocationViewInteraction, locationManager: LocationManager, isStoryLocation: Bool) { + init(context: AccountContext, presentationData: PresentationData, subject: EngineMessage, interaction: LocationViewInteraction, locationManager: LocationManager, isStoryLocation: Bool, isPreview: Bool) { self.context = context self.presentationData = presentationData self.presentationDataPromise = Promise(presentationData) @@ -309,6 +310,7 @@ final class LocationViewControllerNode: ViewControllerTracingNode, CLLocationMan self.interaction = interaction self.locationManager = locationManager self.isStoryLocation = isStoryLocation + self.isPreview = isPreview self.state = LocationViewState() self.statePromise = Promise(self.state) @@ -325,6 +327,7 @@ final class LocationViewControllerNode: ViewControllerTracingNode, CLLocationMan self.headerNode = LocationMapHeaderNode( presentationData: presentationData, glass: false, + isPreview: true, toggleMapModeSelection: interaction.toggleMapModeSelection, updateMapMode: interaction.updateMapMode, goToUserLocation: interaction.toggleTrackingMode, @@ -339,9 +342,13 @@ final class LocationViewControllerNode: ViewControllerTracingNode, CLLocationMan self.backgroundColor = self.presentationData.theme.list.plainBackgroundColor - self.addSubnode(self.listNode) + if !self.isPreview { + self.addSubnode(self.listNode) + } self.addSubnode(self.headerNode) - self.addSubnode(self.optionsNode) + if !self.isPreview { + self.addSubnode(self.optionsNode) + } let userLocation: Signal = .single(nil) |> then( @@ -716,15 +723,17 @@ final class LocationViewControllerNode: ViewControllerTracingNode, CLLocationMan } }) - self.listNode.updateFloatingHeaderOffset = { [weak self] offset, listTransition in - guard let strongSelf = self, let (layout, navigationBarHeight) = strongSelf.validLayout, strongSelf.listNode.scrollEnabled else { - return + if !isPreview { + self.listNode.updateFloatingHeaderOffset = { [weak self] offset, listTransition in + guard let strongSelf = self, let (layout, navigationBarHeight) = strongSelf.validLayout, strongSelf.listNode.scrollEnabled else { + return + } + let overlap: CGFloat = 0.0 + strongSelf.listOffset = max(0.0, offset) + let headerFrame = CGRect(origin: CGPoint(), size: CGSize(width: layout.size.width, height: max(0.0, offset + overlap))) + listTransition.updateFrame(node: strongSelf.headerNode, frame: headerFrame) + strongSelf.headerNode.updateLayout(layout: layout, navigationBarHeight: navigationBarHeight, topPadding: strongSelf.state.displayingMapModeOptions ? 38.0 : 0.0, controlsTopPadding: strongSelf.state.displayingMapModeOptions ? 38.0 : 0.0, controlsBottomPadding: 0.0, offset: 0.0, size: headerFrame.size, transition: listTransition) } - let overlap: CGFloat = 0.0 - strongSelf.listOffset = max(0.0, offset) - let headerFrame = CGRect(origin: CGPoint(), size: CGSize(width: layout.size.width, height: max(0.0, offset + overlap))) - listTransition.updateFrame(node: strongSelf.headerNode, frame: headerFrame) - strongSelf.headerNode.updateLayout(layout: layout, navigationBarHeight: navigationBarHeight, topPadding: strongSelf.state.displayingMapModeOptions ? 38.0 : 0.0, controlsTopPadding: strongSelf.state.displayingMapModeOptions ? 38.0 : 0.0, controlsBottomPadding: 0.0, offset: 0.0, size: headerFrame.size, transition: listTransition) } self.listNode.beganInteractiveDragging = { [weak self] _ in @@ -954,7 +963,9 @@ final class LocationViewControllerNode: ViewControllerTracingNode, CLLocationMan } let headerHeight: CGFloat - if let listOffset = self.listOffset { + if self.isPreview { + headerHeight = layout.size.height + } else if let listOffset = self.listOffset { headerHeight = max(0.0, listOffset + overlap) } else { headerHeight = topInset + overlap diff --git a/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift b/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift index d3a3a940fa..fd120adc0d 100644 --- a/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift +++ b/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift @@ -1333,7 +1333,7 @@ public final class MediaPickerScreenImpl: ViewController, MediaPickerScreen, Att self.openingMedia = true - self.currentGalleryController = presentLegacyMediaPickerGallery(context: controller.context, peer: controller.peer, threadTitle: controller.threadTitle, chatLocation: controller.chatLocation, isScheduledMessages: controller.isScheduledMessages, presentationData: self.presentationData, source: .fetchResult(fetchResult: fetchResult, index: index, reversed: reversed), immediateThumbnail: immediateThumbnail, selectionContext: interaction.selectionState, editingContext: interaction.editingState, hasSilentPosting: true, hasSchedule: hasSchedule, hasTimer: hasTimer, updateHiddenMedia: { [weak self] id in + self.currentGalleryController = presentLegacyMediaPickerGallery(context: controller.context, peer: controller.peer, threadTitle: controller.threadTitle, chatLocation: controller.chatLocation, isScheduledMessages: controller.isScheduledMessages, presentationData: self.presentationData, source: .fetchResult(fetchResult: fetchResult, index: index, reversed: reversed), immediateThumbnail: immediateThumbnail, selectionContext: interaction.selectionState?.selectionLimit == 1 ? nil : interaction.selectionState, editingContext: interaction.editingState, hasSilentPosting: true, hasSchedule: hasSchedule, hasTimer: hasTimer, updateHiddenMedia: { [weak self] id in self?.hiddenMediaId.set(.single(id)) }, initialLayout: layout, transitionHostView: { [weak self] in return self?.gridNode.view diff --git a/submodules/TelegramCore/Sources/PendingMessages/PendingMessageUploadedContent.swift b/submodules/TelegramCore/Sources/PendingMessages/PendingMessageUploadedContent.swift index 91ba458a1d..033e570f0a 100644 --- a/submodules/TelegramCore/Sources/PendingMessages/PendingMessageUploadedContent.swift +++ b/submodules/TelegramCore/Sources/PendingMessages/PendingMessageUploadedContent.swift @@ -774,7 +774,7 @@ private func uploadedMediaImageContent(network: Network, postbox: Postbox, trans |> mapError { _ -> PendingMessageUploadError in } |> mapToSignal { inputPeer -> Signal in if let inputPeer = inputPeer { - if autoclearMessageAttribute != nil || photoVideo != nil { + if autoclearMessageAttribute != nil || (photoVideo != nil && !isGrouped) { return .single(.content(PendingMessageUploadedContentAndReuploadInfo(content: .media(.inputMediaUploadedPhoto(.init(flags: flags, file: file, stickers: stickers, ttlSeconds: ttlSeconds, video: video)), text), reuploadInfo: nil, cacheReferenceKey: nil))) } return network.request(Api.functions.messages.uploadMedia(flags: 0, businessConnectionId: nil, peer: inputPeer, media: Api.InputMedia.inputMediaUploadedPhoto(.init(flags: flags, file: file, stickers: stickers, ttlSeconds: ttlSeconds, video: video)))) @@ -793,8 +793,11 @@ private func uploadedMediaImageContent(network: Network, postbox: Postbox, trans if hasSpoiler { flags |= 1 << 1 } + if let _ = video { + flags |= 1 << 2 + } - let result: PendingMessageUploadedContentResult = .content(PendingMessageUploadedContentAndReuploadInfo(content: .media(.inputMediaPhoto(.init(flags: flags, id: .inputPhoto(.init(id: id, accessHash: accessHash, fileReference: Buffer(data: fileReference))), ttlSeconds: ttlSeconds, video: nil)), text), reuploadInfo: nil, cacheReferenceKey: nil)) + let result: PendingMessageUploadedContentResult = .content(PendingMessageUploadedContentAndReuploadInfo(content: .media(.inputMediaPhoto(.init(flags: flags, id: .inputPhoto(.init(id: id, accessHash: accessHash, fileReference: Buffer(data: fileReference))), ttlSeconds: ttlSeconds, video: video)), text), reuploadInfo: nil, cacheReferenceKey: nil)) if let _ = ttlSeconds { return .single(result) } else { diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift index c5713170fd..19fc2d759f 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift @@ -132,6 +132,7 @@ private func contentNodeMessagesAndClassesForItem(_ item: ChatMessageItem) -> ([ var addedPriceInfo = false var addedPollMedia = false + var addedQuizAnswer = false outer: for (message, itemAttributes) in item.content { for attribute in message.attributes { @@ -287,6 +288,7 @@ private func contentNodeMessagesAndClassesForItem(_ item: ChatMessageItem) -> ([ } else if let poll = media as? TelegramMediaPoll { if item.controllerInteraction.currentPollMessageWithTooltip == item.message.id, let _ = poll.results.solution { result.append((message, ChatMessageQuizAnswerBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .media, neighborSpacing: .default))) + addedQuizAnswer = true } if let attachedMedia = poll.attachedMedia { @@ -343,7 +345,14 @@ private func contentNodeMessagesAndClassesForItem(_ item: ChatMessageItem) -> ([ } if isMediaInverted { - result.insert((message, ChatMessageTextBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: isFile ? .condensed : .default)), at: addedPriceInfo || addedPollMedia ? 1 : 0) + var targetIndex = 0 + if addedPriceInfo || addedPollMedia { + targetIndex += 1 + } + if addedQuizAnswer { + targetIndex += 1 + } + result.insert((message, ChatMessageTextBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: isFile ? .condensed : .default)), at: targetIndex) } else { result.append((message, ChatMessageTextBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: isFile ? .condensed : .default))) needReactions = false diff --git a/submodules/TelegramUI/Components/Chat/ChatMessagePollBubbleContentNode/Sources/ChatMessagePollBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessagePollBubbleContentNode/Sources/ChatMessagePollBubbleContentNode.swift index 3b78db8d2f..ef2a6b4368 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessagePollBubbleContentNode/Sources/ChatMessagePollBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessagePollBubbleContentNode/Sources/ChatMessagePollBubbleContentNode.swift @@ -605,8 +605,10 @@ private final class ChatMessagePollOptionNode: ASDisplayNode { strongSelf.highlightedBackgroundNode.alpha = 0.0 strongSelf.highlightedBackgroundNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, completion: { finished in if finished && strongSelf.highlightedBackgroundNode.supernode != strongSelf { + let highlightOffset: CGFloat = strongSelf.currentResult != nil ? 7.0 : 0.0 + strongSelf.highlightedBackgroundNode.layer.compositingFilter = nil - strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: strongSelf.highlightedBackgroundNode.frame.size) + strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel + highlightOffset), size: strongSelf.highlightedBackgroundNode.frame.size) strongSelf.insertSubnode(strongSelf.highlightedBackgroundNode, at: 0) } }) @@ -1250,7 +1252,8 @@ private final class ChatMessagePollOptionNode: ASDisplayNode { node.buttonNode.frame = CGRect(origin: CGPoint(x: 1.0, y: 0.0), size: CGSize(width: width - 2.0, height: contentHeight)) if node.highlightedBackgroundNode.supernode == node { - node.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: width, height: contentHeight + UIScreenPixel)) + let highlightOffset: CGFloat = node.currentResult != nil ? 7.0 : 0.0 + node.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel + highlightOffset), size: CGSize(width: width, height: contentHeight + UIScreenPixel - highlightOffset)) } node.separatorNode.backgroundColor = incoming ? presentationData.theme.theme.chat.message.incoming.polls.separator : presentationData.theme.theme.chat.message.outgoing.polls.separator node.separatorNode.frame = CGRect(origin: CGPoint(x: leftInset, y: contentLayoutHeight - UIScreenPixel), size: CGSize(width: width - leftInset - 10.0, height: UIScreenPixel)) @@ -1358,6 +1361,7 @@ private final class ChatMessagePollAddOptionNode: ASDisplayNode { private final class StateBridge: ComponentState { } + private let measureTextNode = ImmediateTextNode() private let textField = ComponentView() private let textFieldState = StateBridge() private let leftAccessoryButton: HighlightableButtonNode @@ -1399,7 +1403,6 @@ private final class ChatMessagePollAddOptionNode: ASDisplayNode { var modeSelectorPressed: (() -> Void)? var requestSave: (() -> Void)? - static let characterLimit = 100 private static let leftInset: CGFloat = 50.0 private static let rightInset: CGFloat = 10.0 private static let verticalInset: CGFloat = 15.0 @@ -1461,58 +1464,52 @@ private final class ChatMessagePollAddOptionNode: ASDisplayNode { private var textFieldView: TextFieldComponent.View? { return self.textField.view as? TextFieldComponent.View } - - private func makeTextFieldComponent() -> TextFieldComponent? { - guard let context = self.currentContext, let theme = self.currentTheme, let strings = self.currentStrings, let currentTextColor = self.currentTextColor, let currentPlaceholderColor = self.currentPlaceholderColor, let currentTintColor = self.currentTintColor, let font = self.currentFont else { - return nil - } - - return TextFieldComponent( - context: context, - theme: theme, - strings: strings, - externalState: self._textFieldExternalState, - fontSize: font.pointSize, - textColor: currentTextColor, - accentColor: currentTintColor, - insets: UIEdgeInsets(top: ChatMessagePollAddOptionNode.verticalInset, left: 8.0, bottom: ChatMessagePollAddOptionNode.verticalInset, right: 8.0), - hideKeyboard: self.currentFocusedTextInputIsMedia, - customInputView: nil, - placeholder: NSAttributedString(string: strings.CreatePoll_AddOption, font: font, textColor: currentPlaceholderColor), - placeholderVerticalOffset: 1.0 + UIScreenPixel, - resetText: nil, - isOneLineWhenUnfocused: false, - characterLimit: ChatMessagePollAddOptionNode.characterLimit, - enableInlineAnimations: true, - emptyLineHandling: .notAllowed, - formatMenuAvailability: .none, - returnKeyType: .done, - lockedFormatAction: { - }, - present: { _ in - }, - paste: { _ in - }, - returnKeyAction: { [weak self] in - self?.requestSave?() - }, - backspaceKeyAction: { - - } - ) - } - + private func updateTextFieldLayout(size: CGSize, forceUpdate: Bool) { - guard let component = self.makeTextFieldComponent() else { + guard let context = self.currentContext, let theme = self.currentTheme, let strings = self.currentStrings, let currentTextColor = self.currentTextColor, let currentPlaceholderColor = self.currentPlaceholderColor, let currentTintColor = self.currentTintColor, let font = self.currentFont else { return } - + let textFieldSize = self.textField.update( transition: .immediate, - component: AnyComponent(component), + component: AnyComponent( + TextFieldComponent( + context: context, + theme: theme, + strings: strings, + externalState: self._textFieldExternalState, + fontSize: font.pointSize, + textColor: currentTextColor, + accentColor: currentTintColor, + insets: UIEdgeInsets(top: ChatMessagePollAddOptionNode.verticalInset, left: 8.0, bottom: ChatMessagePollAddOptionNode.verticalInset, right: 8.0), + hideKeyboard: self.currentFocusedTextInputIsMedia, + customInputView: nil, + placeholder: NSAttributedString(string: strings.CreatePoll_AddOption, font: font, textColor: currentPlaceholderColor), + placeholderVerticalOffset: 1.0 + UIScreenPixel, + resetText: nil, + isOneLineWhenUnfocused: false, + characterLimit: 100, + enableInlineAnimations: true, + emptyLineHandling: .notAllowed, + formatMenuAvailability: .none, + returnKeyType: .done, + lockedFormatAction: { + }, + present: { _ in + }, + paste: { _ in + }, + returnKeyAction: { [weak self] in + self?.requestSave?() + }, + backspaceKeyAction: { + + } + ) + ), environment: {}, forceUpdate: forceUpdate, - containerSize: CGSize(width: self.currentTextWidth + 16.0, height: 1000.0) + containerSize: CGSize(width: self.currentTextWidth + 16.0 + 16.0, height: 1000.0) ) if let textFieldView = self.textField.view { @@ -1529,7 +1526,7 @@ private final class ChatMessagePollAddOptionNode: ASDisplayNode { } let updatedText = self._textFieldExternalState.text - let updatedMetrics = ChatMessagePollAddOptionNode.layoutMetrics(constrainedWidth: self.currentSize.width, text: updatedText, attachment: self.currentAttachment, font: font) + let updatedMetrics = ChatMessagePollAddOptionNode.layoutMetrics(measureTextNode: self.measureTextNode, constrainedWidth: self.currentSize.width, text: updatedText, font: font) let previousText = self.currentTextValue let previousIsEditing = self.currentIsEditing @@ -1559,20 +1556,21 @@ private final class ChatMessagePollAddOptionNode: ASDisplayNode { self.updateAttachmentLayout(size: self.currentSize, tintColor: secondaryTextColor) } - private static func layoutMetrics(constrainedWidth: CGFloat, text: NSAttributedString, attachment: Attachment?, font: UIFont) -> (textWidth: CGFloat, contentHeight: CGFloat) { - let constrainedWidth = min(268.0, constrainedWidth) - let hasTrailingAccessory = attachment?.media != nil || !text.string.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty - let textWidth = max(1.0, constrainedWidth - ChatMessagePollAddOptionNode.leftInset - ChatMessagePollAddOptionNode.rightInset - (hasTrailingAccessory ? ChatMessagePollAddOptionNode.attachmentInset : 0.0)) + private static func layoutMetrics(measureTextNode: ImmediateTextNode?, constrainedWidth: CGFloat, text: NSAttributedString, font: UIFont) -> (textWidth: CGFloat, contentHeight: CGFloat) { + let textWidth = max(1.0, constrainedWidth - ChatMessagePollAddOptionNode.leftInset - ChatMessagePollAddOptionNode.rightInset - ChatMessagePollAddOptionNode.attachmentInset) + let measureTextNode = measureTextNode ?? ImmediateTextNode() + var measureText = text.string if measureText.hasSuffix("\n") || measureText.isEmpty { measureText += "|" } - let measureTextNode = ImmediateTextNode() + measureTextNode.maximumNumberOfLines = 0 + measureTextNode.insets = UIEdgeInsets(top: 0.0, left: 8.0, bottom: 0.0, right: 8.0) measureTextNode.attributedText = NSAttributedString(string: measureText, font: font, textColor: .black) let measureSize = measureTextNode.updateLayout(CGSize(width: textWidth, height: .greatestFiniteMagnitude)) - + return ( textWidth: textWidth, contentHeight: max(ChatMessagePollAddOptionNode.minHeight, measureSize.height + ChatMessagePollAddOptionNode.verticalInset * 2.0) @@ -1580,6 +1578,7 @@ private final class ChatMessagePollAddOptionNode: ASDisplayNode { } static func asyncLayout(_ maybeNode: ChatMessagePollAddOptionNode?) -> (_ context: AccountContext, _ presentationData: ChatPresentationData, _ strings: PresentationStrings, _ incoming: Bool, _ focusedTextInputIsMedia: Bool, _ text: NSAttributedString, _ attachment: Attachment?, _ constrainedWidth: CGFloat) -> (minimumWidth: CGFloat, layout: ((CGFloat) -> (CGSize, (Bool, Bool) -> ChatMessagePollAddOptionNode))) { + let currentMeasureTextNode = maybeNode?.measureTextNode return { context, presentationData, strings, incoming, focusedTextInputIsMedia, text, attachment, constrainedWidth in let font = presentationData.messageFont let textColor = incoming ? presentationData.theme.theme.chat.message.incoming.primaryTextColor : presentationData.theme.theme.chat.message.outgoing.primaryTextColor @@ -1587,10 +1586,10 @@ private final class ChatMessagePollAddOptionNode: ASDisplayNode { let placeholderColor = (incoming ? presentationData.theme.theme.chat.message.incoming.secondaryTextColor : presentationData.theme.theme.chat.message.outgoing.secondaryTextColor).withMultipliedAlpha(0.7) let tintColor = incoming ? presentationData.theme.theme.chat.message.incoming.polls.bar : presentationData.theme.theme.chat.message.outgoing.polls.bar - let constrainedWidth = min(268.0, constrainedWidth) - let metrics = ChatMessagePollAddOptionNode.layoutMetrics(constrainedWidth: constrainedWidth, text: text, attachment: attachment, font: font) - + let constrainedWidth = min(260.0, constrainedWidth) + return (constrainedWidth, { width in + let metrics = ChatMessagePollAddOptionNode.layoutMetrics(measureTextNode: currentMeasureTextNode, constrainedWidth: width, text: text, font: font) let size = CGSize(width: width, height: metrics.contentHeight) return (size, { _, _ in let node = maybeNode ?? ChatMessagePollAddOptionNode() @@ -1630,7 +1629,11 @@ private final class ChatMessagePollAddOptionNode: ASDisplayNode { } @objc private func leftAccessoryPressed() { - self.modeSelectorPressed?() + if let _ = self.modeSelector?.view { + self.modeSelectorPressed?() + } else { + self.textFieldView?.activateInput() + } } @objc private func imageButtonPressed() { @@ -1651,7 +1654,7 @@ private final class ChatMessagePollAddOptionNode: ASDisplayNode { self.addIconNode.frame = CGRect(origin: CGPoint(x: floor((ChatMessagePollAddOptionNode.leftInset - addIconSize.width) * 0.5) - 2.0, y: floor((size.height - addIconSize.height) * 0.5)), size: addIconSize) let displaySelector = self.currentIsEditing - self.addIconNode.isHidden = displaySelector + self.addIconNode.alpha = displaySelector ? 0.0 : 1.0 if displaySelector { var playAnimation = false @@ -2770,7 +2773,11 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode { pollOptionsFinalizeLayouts.append((optionResult != nil, result.1)) } - let displayAddOption = poll.openAnswers && !isClosed && poll.pollId.namespace == Namespaces.Media.CloudPoll + var maxPollOptions: Int = 20 + if let data = item.context.currentAppConfiguration.with({ $0 }).data, let value = data["poll_answers_max"] as? Double { + maxPollOptions = Int(value) + } + let displayAddOption = poll.openAnswers && !isClosed && poll.pollId.namespace == Namespaces.Media.CloudPoll && orderedPollOptions.count < maxPollOptions if displayAddOption { let addOptionResult = makeAddOptionLayout(item.context, item.presentationData, item.presentationData.strings, incoming, item.controllerInteraction.focusedTextInputIsMedia, currentNewOptionText, currentNewOptionAttachment, constrainedSize.width - layoutConstants.bubble.borderInset * 2.0) boundingSize.width = max(boundingSize.width, addOptionResult.minimumWidth + layoutConstants.bubble.borderInset * 2.0) @@ -3120,7 +3127,7 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode { timerNode.alpha = 0.0 } } - timerNode.update(size: resultSize, color: messageTheme.secondaryTextColor, deadlineTimeout: endDate, resultsHidden: poll.hideResultsUntilClose) + timerNode.update(size: resultSize, color: messageTheme.secondaryTextColor, deadlineTimeout: endDate, resultsHidden: poll.hideResultsUntilClose, strings: item.presentationData.strings) timerNode.frame = CGRect(origin: CGPoint(x: 0.0, y: verticalOffset + 31.0), size: CGSize(width: resultSize.width, height: 20.0)) statusOffset += 6.0 } else if let timerNode = strongSelf.deadlineTimerNode { @@ -3379,7 +3386,7 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode { self.buttonSubmitActiveTextNode.isHidden = true } - let canDisplayNewOption = poll.openAnswers && !isClosed && !hasVoted && poll.pollId.namespace == Namespaces.Media.CloudPoll + let canDisplayNewOption = poll.openAnswers && !isClosed && poll.pollId.namespace == Namespaces.Media.CloudPoll let canSubmitNewOption = !self.currentNewOptionText.string.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty && !(self.currentNewOptionMedia?.requiresUpload ?? false) if canDisplayNewOption && canSubmitNewOption { self.votersNode.isHidden = true @@ -3652,17 +3659,22 @@ private struct ArbitraryRandomNumberGenerator : RandomNumberGenerator { func next() -> UInt64 { return UInt64(drand48() * Double(UInt64.max)) } } -private func stringForRemainingTime(_ duration: Int32) -> String { +private func stringForRemainingTime(_ duration: Int32, strings: PresentationStrings, results: Bool) -> String { + let days = duration / (3600 * 24) let hours = duration / 3600 let minutes = duration / 60 % 60 let seconds = duration % 60 - let durationString: String - if hours > 0 { - durationString = String(format: "%d:%02d:%02d", hours, minutes, seconds) + if days > 0 { + return results ? strings.MessagePoll_ResultsInDays(days) : strings.MessagePoll_EndsInDays(days) } else { - durationString = String(format: "%02d:%02d", minutes, seconds) + let durationString: String + if hours > 0 { + durationString = String(format: "%d:%02d:%02d", hours, minutes, seconds) + } else { + durationString = String(format: "%02d:%02d", minutes, seconds) + } + return results ? strings.MessagePoll_ResultsIn(durationString).string : strings.MessagePoll_EndsIn(durationString).string } - return durationString } @@ -3670,7 +3682,7 @@ private class DeadlineTimerNode: ASDisplayNode { private let textNode: ImmediateTextNode private var timer: SwiftSignalKit.Timer? - private var params: (size: CGSize, color: UIColor, deadlineTimeout: Int32, resultsHidden: Bool)? + private var params: (size: CGSize, color: UIColor, deadlineTimeout: Int32, resultsHidden: Bool, strings: PresentationStrings)? override init() { self.textNode = ImmediateTextNode() @@ -3685,12 +3697,9 @@ private class DeadlineTimerNode: ASDisplayNode { self.timer?.invalidate() } - func update(size: CGSize, color: UIColor, deadlineTimeout: Int32, resultsHidden: Bool) { - self.params = (size, color, deadlineTimeout, resultsHidden) - - //TODO:localize - let prefix: String = resultsHidden ? "results in" : "ends in" - + func update(size: CGSize, color: UIColor, deadlineTimeout: Int32, resultsHidden: Bool, strings: PresentationStrings) { + self.params = (size, color, deadlineTimeout, resultsHidden, strings) + let currentTime = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970) let duration = max(0, deadlineTimeout - currentTime) @@ -3700,7 +3709,7 @@ private class DeadlineTimerNode: ASDisplayNode { guard let self, let params = self.params else { return } - self.update(size: params.size, color: params.color, deadlineTimeout: params.deadlineTimeout, resultsHidden: params.resultsHidden) + self.update(size: params.size, color: params.color, deadlineTimeout: params.deadlineTimeout, resultsHidden: params.resultsHidden, strings: params.strings) }, queue: Queue.mainQueue()) self.timer?.start() } @@ -3713,7 +3722,7 @@ private class DeadlineTimerNode: ASDisplayNode { if duration == 0 { text = "" } else { - text = prefix + " " + stringForRemainingTime(duration) + text = stringForRemainingTime(duration, strings: strings, results: resultsHidden) } self.textNode.attributedText = NSAttributedString(string: text, font: Font.with(size: 11.0, traits: .monospacedNumbers), textColor: color) let textSize = self.textNode.updateLayout(size) diff --git a/submodules/TelegramUI/Components/ChatScheduleTimeController/Sources/ChatScheduleTimeScreen.swift b/submodules/TelegramUI/Components/ChatScheduleTimeController/Sources/ChatScheduleTimeScreen.swift index 506638a6d4..7d4fead1a9 100644 --- a/submodules/TelegramUI/Components/ChatScheduleTimeController/Sources/ChatScheduleTimeScreen.swift +++ b/submodules/TelegramUI/Components/ChatScheduleTimeController/Sources/ChatScheduleTimeScreen.swift @@ -644,7 +644,17 @@ private final class ChatScheduleTimeSheetContentComponent: Component { guard let self else { return } - self.date = value + var date = value + if let minDate = self.minDate, date < minDate { + let timeZone = TimeZone(secondsFromGMT: 0)! + var calendar = Calendar(identifier: .gregorian) + calendar.timeZone = timeZone + if let nextDayDate = calendar.date(byAdding: .day, value: 1, to: date) { + date = nextDayDate + } + } + self.date = date + self.datePicker?.date = date self.state?.updated() } )), diff --git a/submodules/TelegramUI/Components/ComposePollScreen/Sources/ComposePollScreen.swift b/submodules/TelegramUI/Components/ComposePollScreen/Sources/ComposePollScreen.swift index c55c56f87a..863014057c 100644 --- a/submodules/TelegramUI/Components/ComposePollScreen/Sources/ComposePollScreen.swift +++ b/submodules/TelegramUI/Components/ComposePollScreen/Sources/ComposePollScreen.swift @@ -6,6 +6,7 @@ import TelegramCore import Postbox import SwiftSignalKit import TelegramPresentationData +import PresentationDataUtils import TelegramStringFormatting import ComponentFlow import ComponentDisplayAdapters @@ -33,6 +34,7 @@ import ChatScheduleTimeController import ContextUI import StickerPeekUI import EdgeEffect +import LocationUI public final class ComposedPoll { public struct Text { @@ -457,6 +459,30 @@ final class ComposePollScreenComponent: Component { case isUploading } + var hasAnyData: Bool { + if self.pollTextInputState.hasText { + return true + } + if self.pollDescriptionInputState.hasText { + return true + } + if self.pollDescriptionMedia != nil { + return true + } + for pollOption in self.pollOptions { + if pollOption.textInputState.text.length > 0 { + return true + } + } + if self.quizAnswerTextInputState.hasText { + return true + } + if self.quizAnswerMedia != nil { + return true + } + return false + } + func validatedInput() -> ValidatedInput? { if self.pollTextInputState.text.length == 0 { return nil @@ -613,7 +639,7 @@ final class ComposePollScreenComponent: Component { } func scrollViewWillBeginDragging(_ scrollView: UIScrollView) { - self.endEditing(true) + self.deactivateInput() } func isPanGestureEnabled() -> Bool { @@ -645,11 +671,13 @@ final class ComposePollScreenComponent: Component { let needsInputActivation: Bool = !"".isEmpty var height: CGFloat = 0.0 - if case .emoji = self.currentInputMode, let inputData = self.inputMediaNodeData { + if case .emoji = self.currentInputMode, var inputData = self.inputMediaNodeData { if let updatedTag = self.collectTextInputStates().first(where: { $1.isEditing })?.view.currentTag { self.inputMediaNodeTargetTag = updatedTag } + inputData.stickers = nil + let inputMediaNode: ChatEntityKeyboardInputNode var inputMediaNodeTransition = transition var animateIn = false @@ -661,10 +689,15 @@ final class ComposePollScreenComponent: Component { inputMediaNode = ChatEntityKeyboardInputNode( context: component.context, currentInputData: inputData, - updatedInputData: self.inputMediaNodeDataPromise.get(), + updatedInputData: self.inputMediaNodeDataPromise.get() + |> map { inputData in + var inputData = inputData + inputData.stickers = nil + return inputData + }, defaultToEmojiTab: true, opaqueTopPanelBackground: false, - useOpaqueTheme: true, + useOpaqueTheme: false, //true, interaction: self.inputMediaInteraction, chatPeerId: nil, stateContext: self.inputMediaNodeStateContext @@ -708,7 +741,7 @@ final class ComposePollScreenComponent: Component { businessIntro: nil ) - self.inputMediaNodeBackground.backgroundColor = presentationData.theme.rootController.navigationBar.opaqueBackgroundColor.cgColor + //self.inputMediaNodeBackground.backgroundColor = presentationData.theme.rootController.navigationBar.opaqueBackgroundColor.cgColor let heightAndOverflow = inputMediaNode.updateLayout(width: availableSize.width, leftInset: 0.0, rightInset: 0.0, bottomInset: bottomInset, standardInputHeight: deviceMetrics.standardInputHeight(inLandscape: false), inputHeight: inputHeight < 100.0 ? inputHeight - bottomContainerInset : inputHeight, maximumHeight: availableSize.height, inputPanelHeight: 0.0, transition: .immediate, interfaceState: presentationInterfaceState, layoutMetrics: metrics, deviceMetrics: deviceMetrics, isVisible: true, isExpanded: false) let inputNodeHeight = heightAndOverflow.0 @@ -849,7 +882,7 @@ final class ComposePollScreenComponent: Component { return } - self.endEditing(true) + self.deactivateInput() guard replace || !self.openAttachMediaContextMenu(subject: subject) else { return @@ -1061,9 +1094,26 @@ final class ComposePollScreenComponent: Component { }))) let message = Message(stableId: 0, stableVersion: 0, id: MessageId(peerId: PeerId(namespace: Namespaces.Peer.CloudUser, id: ._internalFromInt64Value(0)), namespace: Namespaces.Message.Local, id: 0), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 0, flags: [], tags: [], globalTags: [], localTags: [], customTags: [], forwardInfo: nil, author: nil, text: "", attributes: [], media: [media.media.media], peers: SimpleDictionary(), associatedMessages: SimpleDictionary(), associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) - let gallery = component.context.sharedContext.makeGalleryController(context: component.context, source: .standaloneMessage(message, nil), streamSingleVideo: true, isPreview: true) - let source: ContextContentSource = .controller(ComposePollContextControllerContentSource(controller: gallery, sourceView: nil, sourceRect: .zero)) + let source: ContextContentSource + + if let _ = media.media.media as? TelegramMediaMap { + let controller = LocationViewController( + context: component.context, + subject: EngineMessage(message), + isPreview: true, + params: LocationViewParams( + sendLiveLocation: { _ in }, + stopLiveLocation: { _ in }, + openUrl: { _ in }, + openPeer: { _ in } + ) + ) + source = .controller(ComposePollContextControllerContentSource(controller: controller, sourceView: nil, sourceRect: .zero)) + } else { + let gallery = component.context.sharedContext.makeGalleryController(context: component.context, source: .standaloneMessage(message, nil), streamSingleVideo: true, isPreview: true) + source = .controller(ComposePollContextControllerContentSource(controller: gallery, sourceView: nil, sourceRect: .zero)) + } let contextController = makeContextController( presentationData: presentationData, @@ -1166,6 +1216,15 @@ final class ComposePollScreenComponent: Component { (self.environment?.controller() as? ComposePollScreen)?.parentController()?.push(controller) } + func deactivateInput() { + self.currentInputMode = .keyboard + if hasFirstResponder(self) { + self.endEditing(true) + } else { + self.state?.updated(transition: .spring(duration: 0.4).withUserData(TextFieldComponent.AnimationHint(view: nil, kind: .textFocusChanged(isFocused: false)))) + } + } + func update(component: ComposePollScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { self.isUpdating = true defer { @@ -1221,7 +1280,7 @@ final class ComposePollScreenComponent: Component { hasStickers: true, hasGifs: false, hideBackground: true, - maskEdge: .fade, + maskEdge: .clip, sendGif: nil ) ) @@ -1433,7 +1492,7 @@ final class ComposePollScreenComponent: Component { strings: environment.strings, resetText: nil, assumeIsEditing: self.inputMediaNodeTargetTag === self.pollDescriptionFieldTag, - characterLimit: 1024, //TODO + characterLimit: 1024, attachment: pollDescriptionAttachment, emptyLineHandling: .allowed, returnKeyType: .default, @@ -1564,7 +1623,7 @@ final class ComposePollScreenComponent: Component { } if let index = self.pollOptions.firstIndex(where: { $0.id == optionId }) { if index == self.pollOptions.count - 1 { - self.endEditing(true) + self.deactivateInput() } else { if let pollOptionView = self.pollOptionsSectionContainer.itemViews[self.pollOptions[index + 1].id] { if let pollOptionComponentView = pollOptionView.contents.view as? ListComposePollOptionComponent.View { @@ -1865,6 +1924,9 @@ final class ComposePollScreenComponent: Component { return } self.isAnonymous = !self.isAnonymous + if self.isAnonymous { + self.canAddOptions = false + } self.state?.updated(transition: .spring(duration: 0.4)) })), action: nil @@ -2271,7 +2333,7 @@ final class ComposePollScreenComponent: Component { guard let self else { return } - self.endEditing(true) + self.deactivateInput() }, backspaceKeyAction: nil, selection: nil, @@ -2614,7 +2676,7 @@ final class ComposePollScreenComponent: Component { } )), environment: {}, - containerSize: barButtonSize + containerSize: CGSize(width: 120.0, height: barButtonSize.height) ) let doneButtonFrame = CGRect(origin: CGPoint(x: availableSize.width - environment.safeInsets.right - 16.0 - doneButtonSize.width, y: 16.0), size: doneButtonSize) if let doneButtonView = self.doneButton.view { @@ -2865,11 +2927,32 @@ public class ComposePollScreen: ViewControllerComponentContainer, AttachmentCont } public func requestDismiss(completion: @escaping () -> Void) { - completion() + guard let componentView = self.node.hostView.componentView as? ComposePollScreenComponent.View else { + return + } + let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } + if componentView.hasAnyData { + //TODO:localize + let text = "Discard poll?" + let controller = textAlertController(context: self.context, title: nil, text: text, actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Attachment_CancelSelectionAlertNo, action: { + }), TextAlertAction(type: .defaultAction, title: presentationData.strings.Attachment_CancelSelectionAlertYes, action: { + completion() + })]) + self.present(controller, in: .window(.root)) + } else { + completion() + } } public func shouldDismissImmediately() -> Bool { - return true + guard let componentView = self.node.hostView.componentView as? ComposePollScreenComponent.View else { + return true + } + if componentView.hasAnyData { + return false + } else { + return true + } } } @@ -2918,3 +3001,15 @@ private final class ComposePollContextControllerContentSource: ContextController } } } + +private func hasFirstResponder(_ view: UIView) -> Bool { + if view.isFirstResponder { + return true + } + for subview in view.subviews { + if hasFirstResponder(subview) { + return true + } + } + return false +} diff --git a/submodules/TelegramUI/Components/ComposePollScreen/Sources/PollAttachmentScreen.swift b/submodules/TelegramUI/Components/ComposePollScreen/Sources/PollAttachmentScreen.swift index 901afc8961..eecfb977b3 100644 --- a/submodules/TelegramUI/Components/ComposePollScreen/Sources/PollAttachmentScreen.swift +++ b/submodules/TelegramUI/Components/ComposePollScreen/Sources/PollAttachmentScreen.swift @@ -28,7 +28,7 @@ public func presentPollAttachmentScreen( updatedPresentationData: (initial: PresentationData, signal: Signal)?, subject: PollAttachmentSubject, availableButtons: [AttachmentButtonType], - inputMediaNodeData: Signal = .single(nil), + inputMediaNodeData: Signal? = nil, present: @escaping (ViewController, Bool) -> Void, completion: @escaping (AnyMediaReference) -> Void ) { @@ -44,6 +44,27 @@ public func presentPollAttachmentScreen( return nil } ) + let inputMediaNodeDataPromise = Promise(nil) + if let inputMediaNodeData { + inputMediaNodeDataPromise.set(inputMediaNodeData) + } else if availableButtons.contains(.sticker) || availableButtons.contains(.emoji) { + inputMediaNodeDataPromise.set(.single(nil) |> then( + ChatEntityKeyboardInputNode.inputData( + context: context, + chatPeerId: nil, + areCustomEmojiEnabled: true, + hasTrending: false, + hasSearch: true, + hasStickers: true, + hasGifs: false, + hideBackground: true, + maskEdge: .fade, + sendGif: nil + ) + |> map(Optional.init) + )) + } + attachmentController.requestController = { [weak attachmentController] type, controllerCompletion in let mediaPickerPollSubject: MediaPickerScreenImpl.Subject.AssetsMode.PollMode let filePickerPollSubject: AttachmentFileControllerSource.PollMode @@ -178,7 +199,8 @@ public func presentPollAttachmentScreen( controllerCompletion(controller, controller.mediaPickerContext) return true case .sticker: - let _ = (inputMediaNodeData + let _ = (inputMediaNodeDataPromise.get() + |> filter { $0 != nil } |> take(1) |> deliverOnMainQueue).start(next: { content in guard let content = content?.stickers else { @@ -196,7 +218,8 @@ public func presentPollAttachmentScreen( }) return true case .emoji: - let _ = (inputMediaNodeData + let _ = (inputMediaNodeDataPromise.get() + |> filter { $0 != nil } |> take(1) |> deliverOnMainQueue).start(next: { content in guard let content = content?.emoji else { diff --git a/submodules/TelegramUI/Sources/Chat/ChatControllerOpenPollContextMenu.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerOpenPollContextMenu.swift index 032ba7e222..0ac8d00db9 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatControllerOpenPollContextMenu.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerOpenPollContextMenu.swift @@ -133,7 +133,16 @@ extension ChatControllerImpl { guard let self, let link else { return } - UIPasteboard.general.string = link + "?option=\(pollOptionIndex)" + let encodeBase64URL: (Data) -> String = { data in + var string = data.base64EncodedString() + string = string + .replacingOccurrences(of: "+", with: "-") + .replacingOccurrences(of: "/", with: "_") + string = string.replacingOccurrences(of: "=", with: "") + return string + } + let optionId = encodeBase64URL(pollOption.opaqueIdentifier) + UIPasteboard.general.string = link + "?option=\(optionId)" let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } @@ -156,15 +165,17 @@ extension ChatControllerImpl { } if let addedByPeer, let date = pollOption.date { - //TODO:localize - items.append(.action(ContextMenuActionItem(text: "Remove", textColor: .destructive, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.contextMenu.destructiveColor) }, action: { [weak self] _, f in - f(.default) - - guard let self else { - return - } - let _ = self.context.engine.messages.deletePollOption(messageId: message.id, opaqueIdentifier: pollOption.opaqueIdentifier).start() - }))) + if poll.isCreator || addedByPeer.id == self.context.account.peerId { + //TODO:localize + items.append(.action(ContextMenuActionItem(text: "Remove", textColor: .destructive, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.contextMenu.destructiveColor) }, action: { [weak self] _, f in + f(.default) + + guard let self else { + return + } + let _ = self.context.engine.messages.deletePollOption(messageId: message.id, opaqueIdentifier: pollOption.opaqueIdentifier).start() + }))) + } items.append(.separator) diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index ae3c8f751d..3af4620ada 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -3692,39 +3692,50 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G strongSelf.selectPollOptionFeedback?.success() case .quiz: if let voters = resultPoll.results.voters { + var selectedCount = 0 + var correctCount = 0 + var selectedCorrectCount = 0 + for voter in voters { if voter.selected { - if voter.isCorrect { + selectedCount += 1 + } + if voter.isCorrect { + correctCount += 1 + if voter.selected { + selectedCorrectCount += 1 + } + } + } + + if correctCount == selectedCount && correctCount == selectedCorrectCount { + if strongSelf.selectPollOptionFeedback == nil { + strongSelf.selectPollOptionFeedback = HapticFeedback() + } + strongSelf.selectPollOptionFeedback?.success() + + strongSelf.chatDisplayNode.playConfettiAnimation() + } else { + var found = false + strongSelf.chatDisplayNode.historyNode.forEachVisibleItemNode { itemNode in + if !found, let itemNode = itemNode as? ChatMessageBubbleItemNode, itemNode.item?.message.id == id { + found = true if strongSelf.selectPollOptionFeedback == nil { strongSelf.selectPollOptionFeedback = HapticFeedback() } - strongSelf.selectPollOptionFeedback?.success() + strongSelf.selectPollOptionFeedback?.error() - strongSelf.chatDisplayNode.playConfettiAnimation() - } else { - var found = false - strongSelf.chatDisplayNode.historyNode.forEachVisibleItemNode { itemNode in - if !found, let itemNode = itemNode as? ChatMessageBubbleItemNode, itemNode.item?.message.id == id { - found = true - if strongSelf.selectPollOptionFeedback == nil { - strongSelf.selectPollOptionFeedback = HapticFeedback() - } - strongSelf.selectPollOptionFeedback?.error() - - itemNode.animateQuizInvalidOptionSelected() - - if let solution = resultPoll.results.solution { - for contentNode in itemNode.contentNodes { - if let contentNode = contentNode as? ChatMessagePollBubbleContentNode { - let sourceNode = contentNode.solutionTipSourceNode - strongSelf.displayPollSolution(solution: solution, sourceNode: sourceNode, isAutomatic: true) - } - } + itemNode.animateQuizInvalidOptionSelected() + + if let solution = resultPoll.results.solution { + for contentNode in itemNode.contentNodes { + if let contentNode = contentNode as? ChatMessagePollBubbleContentNode { + let sourceNode = contentNode.solutionTipSourceNode + strongSelf.displayPollSolution(solution: solution, sourceNode: sourceNode, isAutomatic: true) } } } } - break } } } diff --git a/submodules/UrlHandling/Sources/UrlHandling.swift b/submodules/UrlHandling/Sources/UrlHandling.swift index 9277a6a1fb..8e54a2d406 100644 --- a/submodules/UrlHandling/Sources/UrlHandling.swift +++ b/submodules/UrlHandling/Sources/UrlHandling.swift @@ -97,9 +97,15 @@ public enum ParsedInternalUrl { case id(PeerId) } + public enum UrlMessageSubject { + case timecode(Double) + case todoItem(Int32) + case pollOption(String) + } + case peer(UrlPeerReference, ParsedInternalPeerUrlParameter?) case peerId(PeerId) - case privateMessage(messageId: MessageId, threadId: Int32?, timecode: Double?, taskId: Int32?) + case privateMessage(messageId: MessageId, threadId: Int32?, subject: UrlMessageSubject?) case stickerPack(name: String, type: StickerPackUrlType) case invoice(String) case join(String) @@ -611,8 +617,7 @@ public func parseInternalUrl(sharedContext: SharedAccountContext, context: Accou } else if pathComponents.count == 3 && pathComponents[0] == "c" { if let channelId = Int64(pathComponents[1]), let messageId = Int32(pathComponents[2]), channelId > 0 { var threadId: Int32? - var timecode: Double? - var taskId: Int32? + var subject: ParsedInternalUrl.UrlMessageSubject? if let queryItems = components.queryItems { for queryItem in queryItems { if let value = queryItem.value { @@ -623,17 +628,19 @@ public func parseInternalUrl(sharedContext: SharedAccountContext, context: Accou } else if queryItem.name == "t" { let timestampValue = hTmeParseDuration(value) if timestampValue != 0 { - timecode = Double(timestampValue) + subject = .timecode(Double(timestampValue)) } } else if queryItem.name == "task" { if let intValue = Int32(value) { - taskId = intValue + subject = .todoItem(intValue) } + } else if queryItem.name == "option" { + subject = .pollOption(value) } } } } - return .privateMessage(messageId: MessageId(peerId: PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value(channelId)), namespace: Namespaces.Message.Cloud, id: messageId), threadId: threadId, timecode: timecode, taskId: taskId) + return .privateMessage(messageId: MessageId(peerId: PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value(channelId)), namespace: Namespaces.Message.Cloud, id: messageId), threadId: threadId, subject: subject) } else { return nil } @@ -653,25 +660,26 @@ public func parseInternalUrl(sharedContext: SharedAccountContext, context: Accou } } else if pathComponents.count == 4 && pathComponents[0] == "c" { if let channelId = Int64(pathComponents[1]), let threadId = Int32(pathComponents[2]), let messageId = Int32(pathComponents[3]), channelId > 0 { - var timecode: Double? - var taskId: Int32? + var subject: ParsedInternalUrl.UrlMessageSubject? if let queryItems = components.queryItems { for queryItem in queryItems { if let value = queryItem.value { if queryItem.name == "t" { let timestampValue = hTmeParseDuration(value) if timestampValue != 0 { - timecode = Double(timestampValue) + subject = .timecode(Double(timestampValue)) } } else if queryItem.name == "task" { if let intValue = Int32(value) { - taskId = intValue + subject = .todoItem(intValue) } + } else if queryItem.name == "option" { + subject = .pollOption(value) } } } } - return .privateMessage(messageId: MessageId(peerId: PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value(channelId)), namespace: Namespaces.Message.Cloud, id: messageId), threadId: threadId, timecode: timecode, taskId: taskId) + return .privateMessage(messageId: MessageId(peerId: PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value(channelId)), namespace: Namespaces.Message.Cloud, id: messageId), threadId: threadId, subject: subject) } else { return nil } @@ -1073,7 +1081,7 @@ private func resolveInternalUrl(context: AccountContext, url: ParsedInternalUrl) return .single(.result(.peer(nil, .info(nil)))) } }) - case let .privateMessage(messageId, threadId, timecode, taskId): + case let .privateMessage(messageId, threadId, subject): return context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: messageId.peerId)) |> mapToSignal { peer -> Signal in let foundPeer: Signal @@ -1151,12 +1159,39 @@ private func resolveInternalUrl(context: AccountContext, url: ParsedInternalUrl) } |> map { result -> ResolveInternalUrlResult in guard let result = result else { + var timecode: Double? + if case let .timecode(timecodeValue) = subject { + timecode = timecodeValue + } return .result(.channelMessage(peer: foundPeer._asPeer(), messageId: replyThreadMessageId, timecode: timecode)) } return .result(.replyThreadMessage(replyThreadMessage: result, messageId: messageId)) }) } else { - return .single(.result(.peer(foundPeer._asPeer(), .chat(textInputState: nil, subject: .message(id: .id(messageId), highlight: ChatControllerSubject.MessageHighlight(quote: nil, subject: taskId.flatMap { .todoItem($0) }), timecode: timecode, setupReply: false), peekData: nil)))) + var timecode: Double? + if case let .timecode(timecodeValue) = subject { + timecode = timecodeValue + } + var highlightSubject: EngineMessageReplyInnerSubject? + switch subject { + case let .todoItem(todoItemId): + highlightSubject = .todoItem(todoItemId) + case let .pollOption(pollOption): + let decodeBase64: (String) -> Data? = { string in + var string = string.replacingOccurrences(of: "-", with: "+") + .replacingOccurrences(of: "_", with: "/") + while string.count % 4 != 0 { + string.append("=") + } + return Data(base64Encoded: string) + } + if let opaqueIdentifier = decodeBase64(pollOption) { + highlightSubject = .pollOption(opaqueIdentifier) + } + default: + break + } + return .single(.result(.peer(foundPeer._asPeer(), .chat(textInputState: nil, subject: .message(id: .id(messageId), highlight: ChatControllerSubject.MessageHighlight(quote: nil, subject: highlightSubject), timecode: timecode, setupReply: false), peekData: nil)))) } } else { return .single(.result(.inaccessiblePeer))