diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index fe6c025908..84ecce5b7b 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -8002,6 +8002,8 @@ Sorry for the inconvenience."; "PrivacySettings.LoginEmail" = "Login Email"; "PrivacySettings.LoginEmailInfo" = "Change your email address for Telegram login codes."; "Login.EmailChanged" = "Your email has been changed."; +"PrivacySettings.LoginEmailAlertText" = "This email address will be used every time you login to your Telegram account from a new device."; +"PrivacySettings.LoginEmailAlertChange" = "Change Email"; "Login.InvalidEmailAddressError" = "An error occurred. Please try again."; "Login.InvalidEmailError" = "Please enter a valid e-mail address."; diff --git a/submodules/AccountContext/Sources/AccountContext.swift b/submodules/AccountContext/Sources/AccountContext.swift index ab0e05b602..0e686c05f2 100644 --- a/submodules/AccountContext/Sources/AccountContext.swift +++ b/submodules/AccountContext/Sources/AccountContext.swift @@ -153,6 +153,7 @@ public struct ChatAvailableMessageActionOptions: OptionSet { public static let unsendPersonal = ChatAvailableMessageActionOptions(rawValue: 1 << 7) public static let sendScheduledNow = ChatAvailableMessageActionOptions(rawValue: 1 << 8) public static let editScheduledTime = ChatAvailableMessageActionOptions(rawValue: 1 << 9) + public static let externalShare = ChatAvailableMessageActionOptions(rawValue: 1 << 10) } public struct ChatAvailableMessageActions { diff --git a/submodules/ContextUI/Sources/ContextActionNode.swift b/submodules/ContextUI/Sources/ContextActionNode.swift index e354fe63e3..f9bc303629 100644 --- a/submodules/ContextUI/Sources/ContextActionNode.swift +++ b/submodules/ContextUI/Sources/ContextActionNode.swift @@ -245,6 +245,9 @@ public final class ContextActionNode: ASDisplayNode, ContextActionNodeProtocol { if !iconSize.width.isZero { rightTextInset = max(iconSize.width, standardIconWidth) + iconSideInset + sideInset } + if let iconSize = self.titleIconNode.image?.size { + rightTextInset += iconSize.width + 10.0 + } let badgeTextSize = self.badgeTextNode.updateLayout(CGSize(width: constrainedWidth, height: .greatestFiniteMagnitude)) let badgeInset: CGFloat = 4.0 diff --git a/submodules/GalleryUI/Sources/ChatItemGalleryFooterContentNode.swift b/submodules/GalleryUI/Sources/ChatItemGalleryFooterContentNode.swift index b6b5c442c0..69ad8c7418 100644 --- a/submodules/GalleryUI/Sources/ChatItemGalleryFooterContentNode.swift +++ b/submodules/GalleryUI/Sources/ChatItemGalleryFooterContentNode.swift @@ -692,6 +692,8 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, UIScroll hasCaption = true } else if let file = media as? TelegramMediaFile { hasCaption = file.mimeType.hasPrefix("image/") + } else if media is TelegramMediaInvoice { + hasCaption = true } } if hasCaption { @@ -1305,7 +1307,16 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, UIScroll } } } - let shareController = ShareController(context: strongSelf.context, subject: subject, preferredAction: preferredAction, forceTheme: forceTheme) + + var hasExternalShare = true + for media in currentMessage.media { + if let invoice = media as? TelegramMediaInvoice, let _ = invoice.extendedMedia { + hasExternalShare = false + break + } + } + + let shareController = ShareController(context: strongSelf.context, subject: subject, preferredAction: preferredAction, externalShare: hasExternalShare, forceTheme: forceTheme) shareController.dismissed = { [weak self] _ in self?.interacting?(false) } diff --git a/submodules/InstantPageUI/Sources/InstantPageControllerNode.swift b/submodules/InstantPageUI/Sources/InstantPageControllerNode.swift index e3bf4ff4cb..436f802fd0 100644 --- a/submodules/InstantPageUI/Sources/InstantPageControllerNode.swift +++ b/submodules/InstantPageUI/Sources/InstantPageControllerNode.swift @@ -148,6 +148,41 @@ final class InstantPageControllerNode: ASDisplayNode, UIScrollViewDelegate { strongSelf.present(UndoOverlayController(presentationData: presentationData, content: .linkCopied(text: presentationData.strings.Conversation_LinkCopied), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), nil) } } + shareController.completed = { [weak self] peerIds in + let _ = (context.engine.data.get( + EngineDataList( + peerIds.map(TelegramEngine.EngineData.Item.Peer.Peer.init) + ) + ) + |> deliverOnMainQueue).start(next: { [weak self] peerList in + if let strongSelf = self { + let peers = peerList.compactMap { $0 } + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + + let text: String + var savedMessages = false + if peerIds.count == 1, let peerId = peerIds.first, peerId == strongSelf.context.account.peerId { + text = presentationData.strings.Conversation_ForwardTooltip_SavedMessages_One + savedMessages = true + } else { + if peers.count == 1, let peer = peers.first { + let peerName = peer.id == strongSelf.context.account.peerId ? presentationData.strings.DialogList_SavedMessages : peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) + text = presentationData.strings.Conversation_ForwardTooltip_Chat_One(peerName).string + } else if peers.count == 2, let firstPeer = peers.first, let secondPeer = peers.last { + let firstPeerName = firstPeer.id == strongSelf.context.account.peerId ? presentationData.strings.DialogList_SavedMessages : firstPeer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) + let secondPeerName = secondPeer.id == strongSelf.context.account.peerId ? presentationData.strings.DialogList_SavedMessages : secondPeer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) + text = presentationData.strings.Conversation_ForwardTooltip_TwoChats_One(firstPeerName, secondPeerName).string + } else if let peer = peers.first { + let peerName = peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) + text = presentationData.strings.Conversation_ForwardTooltip_ManyChats_One(peerName, "\(peers.count - 1)").string + } else { + text = "" + } + } + strongSelf.present(UndoOverlayController(presentationData: presentationData, content: .forward(savedMessages: savedMessages, text: text), elevatedLayout: false, animateInAsReplacement: true, action: { _ in return false }), nil) + } + }) + } strongSelf.present(shareController, nil) } } diff --git a/submodules/ManagedAnimationNode/Sources/ManagedAnimationNode.swift b/submodules/ManagedAnimationNode/Sources/ManagedAnimationNode.swift index 9ab26c70e4..c68a5459f9 100644 --- a/submodules/ManagedAnimationNode/Sources/ManagedAnimationNode.swift +++ b/submodules/ManagedAnimationNode/Sources/ManagedAnimationNode.swift @@ -294,7 +294,10 @@ open class ManagedAnimationNode: ASDisplayNode { } } - public func trackTo(item: ManagedAnimationItem) { + public func trackTo(item: ManagedAnimationItem, immediately: Bool = false) { + if immediately { + self.trackStack.removeAll() + } self.trackStack.append(item) self.didTryAdvancingState = false self.updateAnimation() diff --git a/submodules/SettingsUI/Sources/Privacy and Security/PrivacyAndSecurityController.swift b/submodules/SettingsUI/Sources/Privacy and Security/PrivacyAndSecurityController.swift index 31c8b84f0f..e1598799b9 100644 --- a/submodules/SettingsUI/Sources/Privacy and Security/PrivacyAndSecurityController.swift +++ b/submodules/SettingsUI/Sources/Privacy and Security/PrivacyAndSecurityController.swift @@ -565,7 +565,7 @@ class PrivacyAndSecurityControllerImpl: ItemListController, ASAuthorizationContr } } -public func privacyAndSecurityController(context: AccountContext, initialSettings: AccountPrivacySettings? = nil, updatedSettings: ((AccountPrivacySettings?) -> Void)? = nil, updatedBlockedPeers: ((BlockedPeersContext?) -> Void)? = nil, updatedHasTwoStepAuth: ((Bool) -> Void)? = nil, focusOnItemTag: PrivacyAndSecurityEntryTag? = nil, activeSessionsContext: ActiveSessionsContext? = nil, webSessionsContext: WebSessionsContext? = nil, blockedPeersContext: BlockedPeersContext? = nil, hasTwoStepAuth: Bool? = nil, loginEmailPattern: Signal? = nil) -> ViewController { +public func privacyAndSecurityController(context: AccountContext, initialSettings: AccountPrivacySettings? = nil, updatedSettings: ((AccountPrivacySettings?) -> Void)? = nil, updatedBlockedPeers: ((BlockedPeersContext?) -> Void)? = nil, updatedHasTwoStepAuth: ((Bool) -> Void)? = nil, focusOnItemTag: PrivacyAndSecurityEntryTag? = nil, activeSessionsContext: ActiveSessionsContext? = nil, webSessionsContext: WebSessionsContext? = nil, blockedPeersContext: BlockedPeersContext? = nil, hasTwoStepAuth: Bool? = nil, loginEmailPattern: Signal? = nil, updatedTwoStepAuthData: (() -> Void)? = nil) -> ViewController { let statePromise = ValuePromise(PrivacyAndSecurityControllerState(), ignoreRepeated: true) let stateValue = Atomic(value: PrivacyAndSecurityControllerState()) let updateState: ((PrivacyAndSecurityControllerState) -> PrivacyAndSecurityControllerState) -> Void = { f in @@ -1005,8 +1005,8 @@ public func privacyAndSecurityController(context: AccountContext, initialSetting if let emailPattern = emailPattern { let presentationData = context.sharedContext.currentPresentationData.with { $0 } let controller = textAlertController( - context: context, title: emailPattern, text: "This email address will be used every time you login to your Telegram account from a new device.", actions: [ - TextAlertAction(type: .genericAction, title: "Change Email", action: { + context: context, title: emailPattern, text: presentationData.strings.PrivacySettings_LoginEmailAlertText, actions: [ + TextAlertAction(type: .genericAction, title: presentationData.strings.PrivacySettings_LoginEmailAlertChange, action: { setupEmailImpl?(emailPattern) }), TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_Cancel, action: { @@ -1092,8 +1092,10 @@ public func privacyAndSecurityController(context: AccountContext, initialSetting let emailChangeCompletion: (AuthorizationSequenceCodeEntryController?) -> Void = { codeController in let presentationData = context.sharedContext.currentPresentationData.with { $0 } - codeController?.animateSuccess() + + updatedTwoStepAuthData?() + Queue.mainQueue().after(0.75) { if let navigationController = getNavigationControllerImpl?() { let controllers = navigationController.viewControllers.filter { controller in @@ -1105,9 +1107,11 @@ public func privacyAndSecurityController(context: AccountContext, initialSetting } navigationController.setViewControllers(controllers, animated: true) - navigationController.presentOverlay(controller: UndoOverlayController(presentationData: presentationData, content: .emoji(name: "IntroLetter", text: presentationData.strings.Login_EmailChanged), elevatedLayout: false, animateInAsReplacement: false, action: { _ in - return false - })) + Queue.mainQueue().after(0.1, { + navigationController.presentOverlay(controller: UndoOverlayController(presentationData: presentationData, content: .emoji(name: "IntroLetter", text: presentationData.strings.Login_EmailChanged), elevatedLayout: false, animateInAsReplacement: false, action: { _ in + return false + })) + }) } } } @@ -1115,6 +1119,8 @@ public func privacyAndSecurityController(context: AccountContext, initialSetting setupEmailImpl = { emailPattern in let presentationData = context.sharedContext.currentPresentationData.with { $0 } var dismissEmailControllerImpl: (() -> Void)? + var presentControllerImpl: ((ViewController) -> Void)? + let emailController = AuthorizationSequenceEmailEntryController(presentationData: presentationData, mode: emailPattern != nil ? .change : .setup, back: { dismissEmailControllerImpl?() }) @@ -1124,9 +1130,15 @@ public func privacyAndSecurityController(context: AccountContext, initialSetting actionsDisposable.add((sendLoginEmailChangeCode(account: context.account, email: email) |> deliverOnMainQueue).start(next: { data in var dismissCodeControllerImpl: (() -> Void)? + var presentControllerImpl: ((ViewController) -> Void)? + let codeController = AuthorizationSequenceCodeEntryController(presentationData: presentationData, openUrl: { _ in }, back: { dismissCodeControllerImpl?() }) + + presentControllerImpl = { [weak codeController] c in + codeController?.present(c, in: .window(.root), with: nil) + } codeController.loginWithCode = { [weak codeController] code in actionsDisposable.add((verifyLoginEmailChange(account: context.account, code: .emailCode(code)) @@ -1248,6 +1260,10 @@ public func privacyAndSecurityController(context: AccountContext, initialSetting emailController.updateData(appleSignInAllowed: true) pushControllerImpl?(emailController, true) + presentControllerImpl = { [weak emailController] c in + emailController?.present(c, in: .window(.root), with: nil) + } + dismissEmailControllerImpl = { [weak emailController] in emailController?.dismiss() } diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index 8ac4be33a3..3b043443d8 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -8970,6 +8970,9 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G items.append(.custom(ChatSendAsPeerTitleContextItem(text: strongSelf.presentationInterfaceState.strings.Conversation_SendMesageAs.uppercased()), false)) items.append(.custom(ChatSendAsPeerListContextItem(context: strongSelf.context, chatPeerId: peerId, peers: peers, selectedPeerId: myPeerId, isPremium: isPremium, presentToast: { [weak self] peer in if let strongSelf = self { + let hapticFeedback = HapticFeedback() + hapticFeedback.impact() + strongSelf.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .invitedToVoiceChat(context: strongSelf.context, peer: peer, text: strongSelf.presentationData.strings.Conversation_SendMesageAsPremiumInfo, action: strongSelf.presentationData.strings.EmojiInput_PremiumEmojiToast_Action), elevatedLayout: false, action: { [weak self] action in guard let strongSelf = self else { return true diff --git a/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift b/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift index 8c0c36228d..4c5d04d298 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift @@ -1700,6 +1700,7 @@ func chatAvailableMessageActionsImpl(engine: TelegramEngine, accountPeerId: Peer var hadBanPeerId = false var disableDelete = false var isCopyProtected = false + var isShareProtected = false func getPeer(_ peerId: PeerId) -> Peer? { if let maybePeer = peerMap[peerId], let peer = maybePeer { @@ -1732,7 +1733,9 @@ func chatAvailableMessageActionsImpl(engine: TelegramEngine, accountPeerId: Peer isCopyProtected = true } for media in message.media { - if let file = media as? TelegramMediaFile, file.isSticker { + if let invoice = media as? TelegramMediaInvoice, let _ = invoice.extendedMedia { + isShareProtected = true + } else if let file = media as? TelegramMediaFile, file.isSticker { for case let .Sticker(_, packReference, _) in file.attributes { if let _ = packReference { optionsMap[id]!.insert(.viewStickerPack) @@ -1917,8 +1920,12 @@ func chatAvailableMessageActionsImpl(engine: TelegramEngine, accountPeerId: Peer optionsMap[id]!.insert(.deleteLocally) } } + + if !isShareProtected { + optionsMap[id]!.insert(.externalShare) + } } - + if !optionsMap.isEmpty { var reducedOptions = optionsMap.values.first! for value in optionsMap.values { diff --git a/submodules/TelegramUI/Sources/ChatMessageInteractiveMediaNode.swift b/submodules/TelegramUI/Sources/ChatMessageInteractiveMediaNode.swift index 0f5349e738..b44192655c 100644 --- a/submodules/TelegramUI/Sources/ChatMessageInteractiveMediaNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageInteractiveMediaNode.swift @@ -438,7 +438,11 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio } else if let fetchStatus = self.fetchStatus, case .Local = fetchStatus { var videoContentMatch = true if let content = self.videoContent, case let .message(stableId, mediaId) = content.nativeId { - videoContentMatch = self.message?.stableId == stableId && self.media?.id == mediaId + var media = self.media + if let invoice = media as? TelegramMediaInvoice, let extendedMedia = invoice.extendedMedia, case let .full(fullMedia) = extendedMedia { + media = fullMedia + } + videoContentMatch = self.message?.stableId == stableId && media?.id == mediaId } self.activateLocalContent((self.automaticPlayback ?? false) && videoContentMatch ? .automaticPlayback : .default) } else { @@ -1490,6 +1494,11 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio let formatting = DataSizeStringFormatting(strings: strings, decimalSeparator: decimalSeparator) + var media = self.media + if let invoice = media as? TelegramMediaInvoice, let extendedMedia = invoice.extendedMedia, case let .full(fullMedia) = extendedMedia { + media = fullMedia + } + switch fetchStatus { case let .Fetching(_, progress): let adjustedProgress = max(progress, 0.027) @@ -1502,8 +1511,8 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio } else { state = .progress(color: messageTheme.mediaOverlayControlColors.foregroundColor, lineWidth: nil, value: CGFloat(adjustedProgress), cancelEnabled: true, animateRotation: true) } - - if let file = self.media as? TelegramMediaFile { + + if let file = media as? TelegramMediaFile { if file.isVideoSticker { state = .none badgeContent = nil @@ -1617,7 +1626,7 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio } case .Remote, .Paused: state = .download(messageTheme.mediaOverlayControlColors.foregroundColor) - if let file = self.media as? TelegramMediaFile, !file.isVideoSticker { + if let file = media as? TelegramMediaFile, !file.isVideoSticker { do { let durationString = file.isAnimated ? gifTitle : stringForDuration(playerDuration > 0 ? playerDuration : (file.duration ?? 0), position: playerPosition) if wideLayout { diff --git a/submodules/TelegramUI/Sources/ChatMessageSelectionInputPanelNode.swift b/submodules/TelegramUI/Sources/ChatMessageSelectionInputPanelNode.swift index 6282292a44..b72a27bf36 100644 --- a/submodules/TelegramUI/Sources/ChatMessageSelectionInputPanelNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageSelectionInputPanelNode.swift @@ -172,7 +172,7 @@ final class ChatMessageSelectionInputPanelNode: ChatInputPanelNode { } else { self.deleteButton.isEnabled = !actions.disableDelete } - self.shareButton.isImplicitlyDisabled = actions.options.intersection([.forward]).isEmpty + self.shareButton.isImplicitlyDisabled = actions.options.intersection(.forward).isEmpty || actions.options.intersection(.externalShare).isEmpty self.reportButton.isEnabled = !actions.options.intersection([.report]).isEmpty if self.peerMedia { diff --git a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift index 36ac1507d8..2b94ac8a67 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift @@ -6414,7 +6414,17 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate self?.blockedPeers.set(.single(blockedPeersContext)) }, updatedHasTwoStepAuth: { [weak self] hasTwoStepAuthValue in self?.hasTwoStepAuth.set(.single(hasTwoStepAuthValue)) - }, focusOnItemTag: nil, activeSessionsContext: settings.activeSessionsContext, webSessionsContext: settings.webSessionsContext, blockedPeersContext: blockedPeersContext, hasTwoStepAuth: hasTwoStepAuth, loginEmailPattern: loginEmailPattern)) + }, focusOnItemTag: nil, activeSessionsContext: settings.activeSessionsContext, webSessionsContext: settings.webSessionsContext, blockedPeersContext: blockedPeersContext, hasTwoStepAuth: hasTwoStepAuth, loginEmailPattern: loginEmailPattern, updatedTwoStepAuthData: { [weak self] in + if let strongSelf = self { + strongSelf.twoStepAuthData.set( + strongSelf.context.engine.auth.twoStepAuthData() + |> map(Optional.init) + |> `catch` { _ -> Signal in + return .single(nil) + } + ) + } + })) } }) }