diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index aa08eb69ae..930473e8b5 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -7525,9 +7525,6 @@ Sorry for the inconvenience."; "WebApp.OpenWebViewAlertTitle" = "Open Web App"; "WebApp.OpenWebViewAlertText" = "**%@** would like to open its web app to proceed.\n\nIt will be able to access your **IP address** and basic device info."; -"WebApp.MessagePreview" = "Message Preview"; -"WebApp.Send" = "Send"; - "WebApp.RemoveConfirmationTitle" = "Remove Bot"; "WebApp.RemoveConfirmationText" = "Remove **%@** from the attachment menu?"; @@ -13242,3 +13239,50 @@ Sorry for the inconvenience."; "PrivacySettings.ValueBotsPlus" = "Mini Apps +%@"; "PrivacySettings.CategoryBots" = "Mini Apps"; + +"PeerInfo.Permissions.Title" = "ALLOW ACCESS TO"; +"PeerInfo.Permissions.EmojiStatus" = "Emoji Status"; +"PeerInfo.Permissions.Geolocation" = "Geolocation"; +"PeerInfo.Permissions.Biometry" = "Biometry"; + +"Stars.Transaction.Subscription" = "Subscription"; +"Stars.Transaction.Subscription.Bot" = "Bot"; +"Stars.Transaction.Subscription.Business" = "Business"; + +"Stars.Transfer.BotSubscribeInfo" = "Do you want to subscribe to **%1$@** in **%2$@** for **%3$@** per month?"; +"Stars.Transfer.BotSubscribeInfo.Stars_1" = "%@ Star"; +"Stars.Transfer.BotSubscribeInfo.Stars_any" = "%@ Stars"; +"Stars.Transfer.SubscribeFor" = "Subscribe for"; +"Stars.Transfer.SubscribePerMonth" = "/ month"; + +"WebApp.AddToHomeScreen" = "Add to Home Screen"; + +"WebApp.Emoji.Title" = "Set Emoji Status"; +"WebApp.Emoji.Text" = "Do you want to set this emoji status suggested by **%@**?"; +"WebApp.Emoji.DurationText" = "Do you want to set this emoji status suggested by **%1$@** for **%2$@**?"; +"WebApp.Emoji.Confirm" = "Confirm"; + +"WebApp.Emoji.Succeed" = "Your emoji status updated."; +"WebApp.Emoji.DurationSucceed" = "Your emoji status set for %@."; + +"WebApp.EmojiPermission.Text" = "**%1$@** requests access to set your **emoji status**. You will be able to revoke this access in the profile page of **%2$@**."; +"WebApp.EmojiPermission.Allow" = "Allow"; +"WebApp.EmojiPermission.Decline" = "Decline"; +"WebApp.EmojiPermission.Succeed" = "**%@** can now set your emoji status anytime."; +"WebApp.EmojiPermission.Undo" = "Undo"; + +"WebApp.LocationPermission.Text" = "**%1$@** requests access to your **location**. You will be able to revoke this access in the profile page of **%2$@**."; +"WebApp.LocationPermission.Allow" = "Allow"; +"WebApp.LocationPermission.Decline" = "Decline"; +"WebApp.LocationPermission.Succeed" = "**%@** can now have access to your location."; +"WebApp.LocationPermission.Undo" = "Undo"; + +"WebApp.Download.Photo" = "Download Photo"; +"WebApp.Download.Video" = "Download Video"; +"WebApp.Download.Document" = "Download Document"; +"WebApp.Download.Text" = "**%1$@** suggests you to download **%2$@**%3$@."; +"WebApp.Download.Download" = "Download"; +"WebApp.Download.Cancel" = "Cancel"; + +"WebApp.Download.SavedToPhotos" = "Saved to Photos."; +"WebApp.Download.SavedToFiles" = "Saved to Files."; diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/PreparedInlineMessages.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/PreparedInlineMessages.swift index 014525d9bf..b28358fc3e 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/PreparedInlineMessages.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/PreparedInlineMessages.swift @@ -44,3 +44,27 @@ func _internal_getPreparedInlineMessage(account: Account, botId: EnginePeer.Id, } } } + +func _internal_checkBotDownload(account: Account, botId: EnginePeer.Id, fileName: String, url: String) -> Signal { + return account.postbox.transaction { transaction -> Api.InputUser? in + return transaction.getPeer(botId).flatMap(apiInputUser) + } + |> mapToSignal { inputBot -> Signal in + guard let inputBot else { + return .single(false) + } + return account.network.request(Api.functions.bots.checkDownloadFileParams(bot: inputBot, fileName: fileName, url: url)) + |> `catch` { _ -> Signal in + return .single(.boolFalse) + } + |> map { value in + switch value { + case .boolTrue: + return true + case .boolFalse: + return false + } + } + } +} + diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift index 10f0ff60d1..1973674532 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift @@ -410,6 +410,10 @@ public extension TelegramEngine { public func getPreparedInlineMessage(botId: EnginePeer.Id, id: String) -> Signal { return _internal_getPreparedInlineMessage(account: self.account, botId: botId, id: id) } + + public func checkBotDownload(botId: EnginePeer.Id, fileName: String, url: String) -> Signal { + return _internal_checkBotDownload(account: self.account, botId: botId, fileName: fileName, url: url) + } public func requestCancelLiveLocation(ids: [MessageId]) -> Signal { return self.account.postbox.transaction { transaction -> Void in diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift index 34dc31be09..c101a966e1 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift @@ -1544,14 +1544,12 @@ private func infoItems(data: PeerInfoScreenData?, context: AccountContext, prese } if let _ = user.botInfo { - //TODO:localize var canManageEmojiStatus = false if let cachedData = data.cachedData as? CachedUserData, cachedData.flags.contains(.botCanManageEmojiStatus) { canManageEmojiStatus = true } - if canManageEmojiStatus || data.webAppPermissions?.emojiStatus?.isRequested == true { - items[.permissions]!.append(PeerInfoScreenSwitchItem(id: 31, text: "Emoji Status", value: canManageEmojiStatus, icon: UIImage(bundleImageName: "Chat/Info/Status"), isLocked: false, toggled: { value in + items[.permissions]!.append(PeerInfoScreenSwitchItem(id: 31, text: presentationData.strings.PeerInfo_Permissions_EmojiStatus, value: canManageEmojiStatus, icon: UIImage(bundleImageName: "Chat/Info/Status"), isLocked: false, toggled: { value in let _ = (context.engine.peers.toggleBotEmojiStatusAccess(peerId: user.id, enabled: value) |> deliverOnMainQueue).startStandalone() @@ -1560,17 +1558,21 @@ private func infoItems(data: PeerInfoScreenData?, context: AccountContext, prese }.startStandalone() })) } - if data.webAppPermissions?.location?.isRequested == true || data.webAppPermissions?.location?.isAllowed == true { - items[.permissions]!.append(PeerInfoScreenSwitchItem(id: 32, text: "Geolocation", value: data.webAppPermissions?.location?.isAllowed ?? false, icon: UIImage(bundleImageName: "Chat/Info/Location"), isLocked: false, toggled: { value in + items[.permissions]!.append(PeerInfoScreenSwitchItem(id: 32, text: presentationData.strings.PeerInfo_Permissions_Geolocation, value: data.webAppPermissions?.location?.isAllowed ?? false, icon: UIImage(bundleImageName: "Chat/Info/Location"), isLocked: false, toggled: { value in let _ = updateWebAppPermissionsStateInteractively(context: context, peerId: user.id) { current in return WebAppPermissionsState(location: WebAppPermissionsState.Location(isRequested: true, isAllowed: value), emojiStatus: current?.emojiStatus) }.startStandalone() })) } + if !"".isEmpty { + items[.permissions]!.append(PeerInfoScreenSwitchItem(id: 33, text: presentationData.strings.PeerInfo_Permissions_Biometry, value: true, icon: UIImage(bundleImageName: "Settings/Menu/TouchId"), isLocked: false, toggled: { value in + + })) + } if !items[.permissions]!.isEmpty { - items[.permissions]!.insert(PeerInfoScreenHeaderItem(id: 30, text: "ALLOW ACCESS TO"), at: 0) + items[.permissions]!.insert(PeerInfoScreenHeaderItem(id: 30, text: presentationData.strings.PeerInfo_Permissions_Title), at: 0) } } diff --git a/submodules/TelegramUI/Components/PeerSelectionController/Sources/PeerSelectionControllerNode.swift b/submodules/TelegramUI/Components/PeerSelectionController/Sources/PeerSelectionControllerNode.swift index 94e30c8f79..7b32c110a3 100644 --- a/submodules/TelegramUI/Components/PeerSelectionController/Sources/PeerSelectionControllerNode.swift +++ b/submodules/TelegramUI/Components/PeerSelectionController/Sources/PeerSelectionControllerNode.swift @@ -1792,8 +1792,7 @@ private final class PeersCountPanelNode: ASDisplayNode { var count: Int = 0 { didSet { if self.count != oldValue && self.count > 0 { - //TODO:localize - self.button.title = "Send" + self.button.title = self.strings.ShareMenu_Send self.button.badge = "\(self.count)" if let (width, sideInset, bottomInset) = self.validLayout { diff --git a/submodules/TelegramUI/Components/Stars/StarsTransactionScreen/Sources/StarsTransactionScreen.swift b/submodules/TelegramUI/Components/Stars/StarsTransactionScreen/Sources/StarsTransactionScreen.swift index a6e87df6d9..60cd6fb195 100644 --- a/submodules/TelegramUI/Components/Stars/StarsTransactionScreen/Sources/StarsTransactionScreen.swift +++ b/submodules/TelegramUI/Components/Stars/StarsTransactionScreen/Sources/StarsTransactionScreen.swift @@ -725,10 +725,9 @@ private final class StarsTransactionSheetContent: CombinedComponent { let title: String if isSubscription { if isBotSubscription { - //TODO:localize - title = "Bot" + title = strings.Stars_Transaction_Subscription_Bot } else if isBusinessSubscription { - title = "Business" + title = strings.Stars_Transaction_Subscription_Business } else { title = strings.Stars_Transaction_Subscription_Subscription } @@ -766,10 +765,9 @@ private final class StarsTransactionSheetContent: CombinedComponent { ) )) if case let .subscription(subscription) = component.subject, let title = subscription.title { - //TODO:localize tableItems.append(.init( id: "subscription", - title: "Subscription", + title: strings.Stars_Transaction_Subscription, component: AnyComponent( MultilineTextComponent(text: .plain(NSAttributedString(string: title, font: tableFont, textColor: tableTextColor))) ) diff --git a/submodules/TelegramUI/Components/Stars/StarsTransferScreen/Sources/StarsTransferScreen.swift b/submodules/TelegramUI/Components/Stars/StarsTransferScreen/Sources/StarsTransferScreen.swift index f4ba1e0a17..6e65e2b10b 100644 --- a/submodules/TelegramUI/Components/Stars/StarsTransferScreen/Sources/StarsTransferScreen.swift +++ b/submodules/TelegramUI/Components/Stars/StarsTransferScreen/Sources/StarsTransferScreen.swift @@ -362,7 +362,7 @@ private final class SheetContent: CombinedComponent { let titleString: String if isSubscription { if isBot { - titleString = "Subscription Name" + titleString = component.invoice.title } else { titleString = strings.Stars_Transfer_Subscribe_Channel_Title } @@ -412,7 +412,7 @@ private final class SheetContent: CombinedComponent { if case .starsChatSubscription = context.component.source { infoText = strings.Stars_Transfer_SubscribeInfo(state.botPeer?.compactDisplayTitle ?? "", strings.Stars_Transfer_Info_Stars(Int32(amount))).string } else if let _ = component.invoice.subscriptionPeriod { - infoText = "Do you want to subscribe to **\(component.invoice.title)** in **\(state.botPeer?.compactDisplayTitle ?? "")** for **\(strings.Stars_Transfer_Info_Stars(Int32(amount)))** per month?" + infoText = strings.Stars_Transfer_BotSubscribeInfo(component.invoice.title, state.botPeer?.compactDisplayTitle ?? "", strings.Stars_Transfer_BotSubscribeInfo_Stars(Int32(amount))).string } else if !component.extendedMedia.isEmpty { var description: String = "" var photoCount: Int32 = 0 @@ -534,11 +534,10 @@ private final class SheetContent: CombinedComponent { let amountString = presentationStringsFormattedNumber(Int32(amount), presentationData.dateTimeFormat.groupingSeparator) let buttonAttributedString: NSMutableAttributedString if case .starsChatSubscription = component.source { - //TODO:localize - buttonAttributedString = NSMutableAttributedString(string: "Subscribe for # \(amountString) / month", font: Font.semibold(17.0), textColor: theme.list.itemCheckColors.foregroundColor, paragraphAlignment: .center) - //buttonAttributedString = NSMutableAttributedString(string: strings.Stars_Transfer_Subscribe, font: Font.semibold(17.0), textColor: theme.list.itemCheckColors.foregroundColor, paragraphAlignment: .center) + buttonAttributedString = NSMutableAttributedString(string: "\(strings.Stars_Transfer_SubscribeFor) # \(amountString) \(strings.Stars_Transfer_SubscribePerMonth)", font: Font.semibold(17.0), textColor: theme.list.itemCheckColors.foregroundColor, paragraphAlignment: .center) + } else if let _ = component.invoice.subscriptionPeriod { - buttonAttributedString = NSMutableAttributedString(string: "Subscribe for # \(amountString) / month", font: Font.semibold(17.0), textColor: theme.list.itemCheckColors.foregroundColor, paragraphAlignment: .center) + buttonAttributedString = NSMutableAttributedString(string: "\(strings.Stars_Transfer_SubscribeFor) # \(amountString) \(strings.Stars_Transfer_SubscribePerMonth)", font: Font.semibold(17.0), textColor: theme.list.itemCheckColors.foregroundColor, paragraphAlignment: .center) } else { buttonAttributedString = NSMutableAttributedString(string: "\(strings.Stars_Transfer_Pay) # \(amountString)", font: Font.semibold(17.0), textColor: theme.list.itemCheckColors.foregroundColor, paragraphAlignment: .center) } diff --git a/submodules/WebUI/BUILD b/submodules/WebUI/BUILD index 182a8df853..3dbe57365e 100644 --- a/submodules/WebUI/BUILD +++ b/submodules/WebUI/BUILD @@ -49,6 +49,7 @@ swift_library( "//submodules/TelegramUI/Components/ListSectionComponent", "//submodules/TelegramUI/Components/Chat/ChatMessageItemImpl", "//submodules/DeviceLocationManager", + "//submodules/DeviceAccess", "//submodules/TelegramUI/Components/Utils/GenerateStickerPlaceholderImage", ], visibility = [ diff --git a/submodules/WebUI/Sources/WebAppController.swift b/submodules/WebUI/Sources/WebAppController.swift index 17a721c5cb..b87f142c91 100644 --- a/submodules/WebUI/Sources/WebAppController.swift +++ b/submodules/WebUI/Sources/WebAppController.swift @@ -33,6 +33,7 @@ import AvatarNode import OverlayStatusController import TelegramUIPreferences import CoreMotion +import DeviceAccess import DeviceLocationManager import LegacyMediaPickerUI import GenerateStickerPlaceholderImage @@ -1205,7 +1206,7 @@ public final class WebAppController: ViewController, AttachmentContainable { } } case "web_app_open_popup": - if let json = json, let message = json["message"] as? String, let buttons = json["buttons"] as? [Any] { + if let json, let message = json["message"] as? String, let buttons = json["buttons"] as? [Any] { let presentationData = self.presentationData let title = json["title"] as? String @@ -1262,12 +1263,12 @@ public final class WebAppController: ViewController, AttachmentContainable { self.controller?.present(alertController, in: .window(.root)) } case "web_app_setup_closing_behavior": - if let json = json, let needConfirmation = json["need_confirmation"] as? Bool { + if let json, let needConfirmation = json["need_confirmation"] as? Bool { self.needDismissConfirmation = needConfirmation } case "web_app_open_scan_qr_popup": var info: String = "" - if let json = json, let text = json["text"] as? String { + if let json, let text = json["text"] as? String { info = text } let controller = QrCodeScanScreen(context: self.context, subject: .custom(info: info)) @@ -1288,7 +1289,7 @@ public final class WebAppController: ViewController, AttachmentContainable { controller.dismissAnimated() } case "web_app_read_text_from_clipboard": - if let json = json, let requestId = json["req_id"] as? String { + if let json, let requestId = json["req_id"] as? String { let botId = controller.botId let isAttachMenu = controller.url == nil @@ -1327,7 +1328,7 @@ public final class WebAppController: ViewController, AttachmentContainable { self.invokeCustomMethod(requestId: requestId, method: method, params: paramsString ?? "{}") } case "web_app_setup_settings_button": - if let json = json, let isVisible = json["is_visible"] as? Bool { + if let json, let isVisible = json["is_visible"] as? Bool { self.controller?.hasSettings = isVisible } case "web_app_biometry_get_info": @@ -1353,11 +1354,11 @@ public final class WebAppController: ViewController, AttachmentContainable { self.openBotSettings() } case "web_app_setup_swipe_behavior": - if let json = json, let isPanGestureEnabled = json["allow_vertical_swipe"] as? Bool { + if let json, let isPanGestureEnabled = json["allow_vertical_swipe"] as? Bool { self.controller?._isPanGestureEnabled = isPanGestureEnabled } case "web_app_share_to_story": - if let json = json, let mediaUrl = json["media_url"] as? String { + if let json, let mediaUrl = json["media_url"] as? String { let text = json["text"] as? String let link = json["widget_link"] as? [String: Any] @@ -1441,25 +1442,29 @@ public final class WebAppController: ViewController, AttachmentContainable { case "web_app_exit_fullscreen": self.setIsFullscreen(false) case "web_app_start_accelerometer": - if let json = json, let refreshRate = json["refresh_rate"] as? Double { + if let json { + let refreshRate = json["refresh_rate"] as? Double self.setIsAccelerometerActive(true, refreshRate: refreshRate) } case "web_app_stop_accelerometer": self.setIsAccelerometerActive(false) case "web_app_start_device_orientation": - if let json = json, let refreshRate = json["refresh_rate"] as? Double { - self.setIsDeviceOrientationActive(true, refreshRate: refreshRate) + if let json { + let refreshRate = json["refresh_rate"] as? Double + let absolute = (json["need_absolute"] as? Bool) == true + self.setIsDeviceOrientationActive(true, refreshRate: refreshRate, absolute: absolute) } case "web_app_stop_device_orientation": self.setIsDeviceOrientationActive(false) case "web_app_start_gyroscope": - if let json = json, let refreshRate = json["refresh_rate"] as? Double { + if let json { + let refreshRate = json["refresh_rate"] as? Double self.setIsGyroscopeActive(true, refreshRate: refreshRate) } case "web_app_stop_gyroscope": self.setIsGyroscopeActive(false) case "web_app_set_emoji_status": - if let json = json, let emojiIdString = json["custom_emoji_id"] as? String, let emojiId = Int64(emojiIdString) { + if let json, let emojiIdString = json["custom_emoji_id"] as? String, let emojiId = Int64(emojiIdString) { let duration = json["duration"] as? Double self.setEmojiStatus(emojiId, duration: duration.flatMap { Int32($0) }) } @@ -1479,17 +1484,17 @@ public final class WebAppController: ViewController, AttachmentContainable { self.openLocationSettings() } case "web_app_send_prepared_message": - if let json = json, let id = json["id"] as? String { + if let json, let id = json["id"] as? String { self.sendPreparedMessage(id: id) } case "web_app_request_emoji_status_access": self.requestEmojiStatusAccess() case "web_app_request_file_download": - if let json = json, let url = json["url"] as? String, let fileName = json["file_name"] as? String { + if let json, let url = json["url"] as? String, let fileName = json["file_name"] as? String { self.downloadFile(url: url, fileName: fileName) } case "web_app_toggle_orientation_lock": - if let json = json, let lock = json["locked"] as? Bool { + if let json, let lock = json["locked"] as? Bool { controller.parentController()?.lockOrientation = lock } default: @@ -2194,6 +2199,8 @@ public final class WebAppController: ViewController, AttachmentContainable { if let refreshRate { self.motionManager.accelerometerUpdateInterval = refreshRate * 0.001 + } else { + self.motionManager.accelerometerUpdateInterval = 1.0 } self.motionManager.startAccelerometerUpdates(to: OperationQueue.main) { [weak self] data, error in guard let self, let data else { @@ -2214,7 +2221,7 @@ public final class WebAppController: ViewController, AttachmentContainable { } private var isDeviceOrientationActive = false - fileprivate func setIsDeviceOrientationActive(_ isActive: Bool, refreshRate: Double? = nil) { + fileprivate func setIsDeviceOrientationActive(_ isActive: Bool, refreshRate: Double? = nil, absolute: Bool = false) { guard self.motionManager.isDeviceMotionAvailable else { self.webView?.sendEvent(name: "device_orientation_failed", data: "{error: \"UNSUPPORTED\"}") return @@ -2228,16 +2235,39 @@ public final class WebAppController: ViewController, AttachmentContainable { if let refreshRate { self.motionManager.deviceMotionUpdateInterval = refreshRate * 0.001 + } else { + self.motionManager.deviceMotionUpdateInterval = 1.0 } - self.motionManager.startDeviceMotionUpdates(using: .xTrueNorthZVertical, to: OperationQueue.main) { [weak self] data, error in + + let referenceFrame: CMAttitudeReferenceFrame + if absolute && CMMotionManager.availableAttitudeReferenceFrames().contains(.xMagneticNorthZVertical) { + referenceFrame = .xMagneticNorthZVertical + } else { + if CMMotionManager.availableAttitudeReferenceFrames().contains(.xArbitraryCorrectedZVertical) { + referenceFrame = .xArbitraryCorrectedZVertical + } else { + referenceFrame = .xArbitraryZVertical + } + } + self.motionManager.startDeviceMotionUpdates(using: referenceFrame, to: OperationQueue.main) { [weak self] data, error in guard let self, let data else { return } + var alpha: Double + if absolute { + alpha = data.heading * .pi / 180.0 + if alpha > .pi { + alpha -= 2.0 * .pi + } else if alpha < -.pi { + alpha += 2.0 * .pi + } + } else { + alpha = data.attitude.yaw + } self.webView?.sendEvent( name: "device_orientation_changed", - data: "{alpha: \(data.attitude.yaw), beta: \(data.attitude.pitch), gamma: \(data.attitude.roll)}" + data: "{absolute: true, alpha: \(alpha), beta: \(data.attitude.pitch), gamma: \(data.attitude.roll)}" ) - print("{alpha: \(data.attitude.yaw), beta: \(data.attitude.pitch), gamma: \(data.attitude.roll)}") } } else { if self.motionManager.isDeviceMotionActive { @@ -2262,6 +2292,8 @@ public final class WebAppController: ViewController, AttachmentContainable { if let refreshRate { self.motionManager.gyroUpdateInterval = refreshRate * 0.001 + } else { + self.motionManager.gyroUpdateInterval = 1.0 } self.motionManager.startGyroUpdates(to: OperationQueue.main) { [weak self] data, error in guard let self, let data else { @@ -2309,47 +2341,52 @@ public final class WebAppController: ViewController, AttachmentContainable { guard let controller = self.controller else { return } - var isMedia = true + var isMedia = false var title: String? let photoExtensions = [".jpg", ".png", ".gif", ".tiff"] let videoExtensions = [".mp4", ".mov"] let lowercasedFilename = fileName.lowercased() for ext in photoExtensions { if lowercasedFilename.hasSuffix(ext) { - title = "Download Photo" + title = self.presentationData.strings.WebApp_Download_Photo + isMedia = true break } } if title == nil { for ext in videoExtensions { if lowercasedFilename.hasSuffix(ext) { - title = "Download Video" + title = self.presentationData.strings.WebApp_Download_Video break } } } if title == nil { - title = "Download Document" - isMedia = false + title = self.presentationData.strings.WebApp_Download_Document } - let _ = (FileDownload.getFileSize(url: url) - |> deliverOnMainQueue).start(next: { [weak self] fileSize in + let _ = combineLatest(queue: Queue.mainQueue(), + FileDownload.getFileSize(url: url), + self.context.engine.messages.checkBotDownload(botId: controller.botId, fileName: fileName, url: url) + ).start(next: { [weak self] fileSize, canDownload in guard let self else { return } - + guard canDownload else { + self.webView?.sendEvent(name: "file_download_requested", data: "{status: \"cancelled\"}") + return + } var fileSizeString = "" if let fileSize { fileSizeString = " (\(dataSizeString(fileSize, formatting: DataSizeStringFormatting(presentationData: self.presentationData))))" } - let text: String = "**\(controller.botName)** suggests you to download **\(fileName)**\(fileSizeString)." + let text: String = self.presentationData.strings.WebApp_Download_Text(controller.botName, fileName, fileSizeString).string let alertController = standardTextAlertController(theme: AlertControllerTheme(presentationData: self.presentationData), title: title, text: text, actions: [ TextAlertAction(type: .genericAction, title: self.presentationData.strings.Common_Cancel, action: { [weak self] in self?.webView?.sendEvent(name: "file_download_requested", data: "{status: \"cancelled\"}") }), - TextAlertAction(type: .defaultAction, title: "Download", action: { [weak self] in + TextAlertAction(type: .defaultAction, title: self.presentationData.strings.WebApp_Download_Download, action: { [weak self] in self?.startDownload(url: url, fileName: fileName, fileSize: fileSize, isMedia: isMedia) }) ], parseMarkdown: true) @@ -2387,12 +2424,12 @@ public final class WebAppController: ViewController, AttachmentContainable { progress: progress, title: fileName, text: text, - undoText: "Cancel" + undoText: self.presentationData.strings.WebApp_Download_Cancel ) }, completion: { [weak self] resultUrl, _ in if let resultUrl, let self { - let tooltipContent: UndoOverlayContent = .actionSucceeded(title: fileName, text: isMedia ? "Saved to Photos" : "Saved to Files", cancel: nil, destructive: false) + let tooltipContent: UndoOverlayContent = .actionSucceeded(title: fileName, text: isMedia ? self.presentationData.strings.WebApp_Download_SavedToPhotos : self.presentationData.strings.WebApp_Download_SavedToFiles, cancel: nil, destructive: false) if isMedia { let saveToPhotos: (URL, Bool) -> Void = { url, isVideo in var fileExtension = (resultUrl.absoluteString as NSString).pathExtension @@ -2470,7 +2507,7 @@ public final class WebAppController: ViewController, AttachmentContainable { progress: 0.0, title: fileName, text: text, - undoText: "Cancel" + undoText: self.presentationData.strings.WebApp_Download_Cancel ), elevatedLayout: false, position: .top, @@ -2534,12 +2571,11 @@ public final class WebAppController: ViewController, AttachmentContainable { |> deliverOnMainQueue).startStandalone(completed: { [weak self] in self?.webView?.sendEvent(name: "emoji_status_access_requested", data: "{status: \"allowed\"}") }) - - //TODO:localize + if let botPeer { let resultController = UndoOverlayController( - presentationData: presentationData, - content: .invitedToVoiceChat(context: self.context, peer: botPeer, title: nil, text: "**\(controller.botName)** can now set your emoji status anytime.", action: "Undo", duration: 5.0), + presentationData: self.presentationData, + content: .invitedToVoiceChat(context: self.context, peer: botPeer, title: nil, text: self.presentationData.strings.WebApp_EmojiPermission_Succeed(controller.botName).string, action: self.presentationData.strings.WebApp_EmojiPermission_Undo, duration: 5.0), elevatedLayout: true, action: { action in if case .undo = action { @@ -2618,10 +2654,16 @@ public final class WebAppController: ViewController, AttachmentContainable { |> deliverOnMainQueue).start(completed: { [weak self] in self?.webView?.sendEvent(name: "emoji_status_set", data: nil) }) - //TODO:localize + let text: String + if let duration { + let durationString = scheduledTimeIntervalString(strings: self.presentationData.strings, value: duration) + text = self.presentationData.strings.WebApp_Emoji_DurationSucceed(durationString).string + } else { + text = self.presentationData.strings.WebApp_Emoji_Succeed + } let resultController = UndoOverlayController( presentationData: self.presentationData, - content: .sticker(context: context, file: file, loop: false, title: nil, text: "Your emoji status updated.", undoText: nil, customAction: nil), + content: .sticker(context: context, file: file, loop: false, title: nil, text: text, undoText: nil, customAction: nil), elevatedLayout: true, action: { action in if case .undo = action { @@ -2709,108 +2751,116 @@ public final class WebAppController: ViewController, AttachmentContainable { }) } + private let locationManager = LocationManager() fileprivate func requestLocation() { - guard let controller = self.controller else { - return - } - let context = controller.context - let botId = controller.botId - let _ = (webAppPermissionsState(context: self.context, peerId: botId) - |> take(1) - |> deliverOnMainQueue).start(next: { [weak self, weak controller] state in - guard let self else { + let context = self.context + DeviceAccess.authorizeAccess(to: .location(.send), locationManager: self.locationManager, presentationData: self.presentationData, present: { [weak self] c, a in + self?.controller?.present(c, in: .window(.root), with: a) + }, openSettings: { + context.sharedContext.applicationBindings.openSettings() + }, { [weak self, weak controller] authorized in + guard let controller else { return } - - var shouldRequest = false - if let location = state?.location { - if location.isRequested { - if location.isAllowed { - let locationCoordinates = Signal { subscriber in - return context.sharedContext.locationManager!.push(mode: DeviceLocationMode.preciseForeground, updated: { location, _ in - subscriber.putNext(location) - subscriber.putCompletion() + let context = controller.context + let botId = controller.botId + let _ = (webAppPermissionsState(context: context, peerId: botId) + |> take(1) + |> deliverOnMainQueue).start(next: { [weak self, weak controller] state in + guard let self else { + return + } + + var shouldRequest = false + if let location = state?.location { + if location.isRequested { + if location.isAllowed { + let locationCoordinates = Signal { subscriber in + return context.sharedContext.locationManager!.push(mode: DeviceLocationMode.preciseForeground, updated: { location, _ in + subscriber.putNext(location) + subscriber.putCompletion() + }) + } |> deliverOnMainQueue + let _ = locationCoordinates.startStandalone(next: { location in + var data: [String: Any] = [:] + data["available"] = true + data["latitude"] = location.coordinate.latitude + data["longitude"] = location.coordinate.longitude + data["altitude"] = location.altitude + data["course"] = location.course + data["speed"] = location.speed + data["horizontal_accuracy"] = location.horizontalAccuracy + data["vertical_accuracy"] = location.verticalAccuracy + if #available(iOS 13.4, *) { + data["course_accuracy"] = location.courseAccuracy + } else { + data["course_accuracy"] = NSNull() + } + data["speed_accuracy"] = location.speedAccuracy + if let serializedData = JSON(dictionary: data)?.string { + self.webView?.sendEvent(name: "location_requested", data: serializedData) + } }) - } |> deliverOnMainQueue - let _ = locationCoordinates.startStandalone(next: { location in + } else { var data: [String: Any] = [:] - data["available"] = true - data["latitude"] = location.coordinate.latitude - data["longitude"] = location.coordinate.longitude - data["altitude"] = location.altitude - data["course"] = location.course - data["speed"] = location.speed - data["horizontal_accuracy"] = location.horizontalAccuracy - data["vertical_accuracy"] = location.verticalAccuracy - if #available(iOS 13.4, *) { - data["course_accuracy"] = location.courseAccuracy - } else { - data["course_accuracy"] = NSNull() - } - data["speed_accuracy"] = location.speedAccuracy - if let serializedData = JSON(dictionary: data)?.string { - self.webView?.sendEvent(name: "location_requested", data: serializedData) - } - }) + data["available"] = false + self.webView?.sendEvent(name: "location_requested", data: JSON(dictionary: data)?.string) + } } else { - var data: [String: Any] = [:] - data["available"] = false - self.webView?.sendEvent(name: "location_requested", data: JSON(dictionary: data)?.string) + shouldRequest = true } } else { shouldRequest = true } - } else { - shouldRequest = true - } - - if shouldRequest { - let _ = (context.engine.data.get( - TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId), - TelegramEngine.EngineData.Item.Peer.Peer(id: botId) - ) - |> deliverOnMainQueue).start(next: { [weak self, weak controller] accountPeer, botPeer in - guard let accountPeer, let botPeer, let controller else { - return - } - let alertController = webAppLocationAlertController( - context: controller.context, - accountPeer: accountPeer, - botPeer: botPeer, - completion: { [weak self, weak controller] result in - guard let self, let controller else { - return - } - if result { - let resultController = UndoOverlayController( - presentationData: self.presentationData, - content: .invitedToVoiceChat(context: context, peer: botPeer, title: nil, text: "**\(botPeer.compactDisplayTitle)** can now have access to your location.", action: "Undo", duration: 5.0), - elevatedLayout: true, - action: { action in - if case .undo = action { - - } - return true - } - ) - controller.present(resultController, in: .window(.root)) - - Queue.mainQueue().after(0.1, { - self.requestLocation() - }) - } else { - var data: [String: Any] = [:] - data["available"] = false - self.webView?.sendEvent(name: "location_requested", data: JSON(dictionary: data)?.string) - } - let _ = updateWebAppPermissionsStateInteractively(context: context, peerId: botId) { current in - return WebAppPermissionsState(location: WebAppPermissionsState.Location(isRequested: true, isAllowed: result), emojiStatus: current?.emojiStatus) - }.start() - } + + if shouldRequest { + let _ = (context.engine.data.get( + TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId), + TelegramEngine.EngineData.Item.Peer.Peer(id: botId) ) - controller.present(alertController, in: .window(.root)) - }) - } + |> deliverOnMainQueue).start(next: { [weak self, weak controller] accountPeer, botPeer in + guard let accountPeer, let botPeer, let controller else { + return + } + let alertController = webAppLocationAlertController( + context: controller.context, + accountPeer: accountPeer, + botPeer: botPeer, + completion: { [weak self, weak controller] result in + guard let self, let controller else { + return + } + if result { + let resultController = UndoOverlayController( + presentationData: self.presentationData, + content: .invitedToVoiceChat(context: context, peer: botPeer, title: nil, text: self.presentationData.strings.WebApp_LocationPermission_Succeed(botPeer.compactDisplayTitle).string, action: self.presentationData.strings.WebApp_LocationPermission_Undo, duration: 5.0), + elevatedLayout: true, + action: { action in + if case .undo = action { + + } + return true + } + ) + controller.present(resultController, in: .window(.root)) + + Queue.mainQueue().after(0.1, { + self.requestLocation() + }) + } else { + var data: [String: Any] = [:] + data["available"] = false + self.webView?.sendEvent(name: "location_requested", data: JSON(dictionary: data)?.string) + } + let _ = updateWebAppPermissionsStateInteractively(context: context, peerId: botId) { current in + return WebAppPermissionsState(location: WebAppPermissionsState.Location(isRequested: true, isAllowed: result), emojiStatus: current?.emojiStatus) + }.start() + } + ) + controller.present(alertController, in: .window(.root)) + }) + } + }) }) } } @@ -3094,9 +3144,8 @@ public final class WebAppController: ViewController, AttachmentContainable { self?.controllerNode.webView?.reload() }))) - //TODO:localize if let _ = self?.appName { - items.append(.action(ContextMenuActionItem(text: "Add to Home Screen", icon: { theme in + items.append(.action(ContextMenuActionItem(text: presentationData.strings.WebApp_AddToHomeScreen, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/AddSquare"), color: theme.contextMenu.primaryColor) }, action: { [weak self] c, _ in c?.dismiss(completion: nil) diff --git a/submodules/WebUI/Sources/WebAppEmojiStatusAlertController.swift b/submodules/WebUI/Sources/WebAppEmojiStatusAlertController.swift index b0ada96f4a..9c461b0dc7 100644 --- a/submodules/WebUI/Sources/WebAppEmojiStatusAlertController.swift +++ b/submodules/WebUI/Sources/WebAppEmojiStatusAlertController.swift @@ -192,8 +192,7 @@ private final class WebAppEmojiStatusAlertContentNode: AlertContentNode { } override func updateTheme(_ theme: AlertControllerTheme) { - //TODO:localize - let string = "**\(self.botName)** requests access to set your **emoji status**. You will be able to revoke this access in the profile page of **\(self.botName)**." + let string = self.strings.WebApp_EmojiPermission_Text(self.botName, self.botName).string let attributedText = parseMarkdownIntoAttributedString(string, attributes: MarkdownAttributes( body: MarkdownAttributeSet(font: Font.regular(13.0), textColor: theme.primaryColor), bold: MarkdownAttributeSet(font: Font.semibold(13.0), textColor: theme.primaryColor), @@ -340,11 +339,11 @@ func webAppEmojiStatusAlertController( var dismissImpl: ((Bool) -> Void)? var contentNode: WebAppEmojiStatusAlertContentNode? - let actions: [TextAlertAction] = [TextAlertAction(type: .genericAction, title: "Decline", action: { + let actions: [TextAlertAction] = [TextAlertAction(type: .genericAction, title: strings.WebApp_EmojiPermission_Decline, action: { dismissImpl?(true) completion(false) - }), TextAlertAction(type: .defaultAction, title: "Allow", action: { + }), TextAlertAction(type: .defaultAction, title: strings.WebApp_EmojiPermission_Allow, action: { dismissImpl?(true) completion(true) diff --git a/submodules/WebUI/Sources/WebAppLocationAlertController.swift b/submodules/WebUI/Sources/WebAppLocationAlertController.swift index 9f4500321a..867841fc99 100644 --- a/submodules/WebUI/Sources/WebAppLocationAlertController.swift +++ b/submodules/WebUI/Sources/WebAppLocationAlertController.swift @@ -265,16 +265,15 @@ private final class WebAppLocationAlertContentNode: AlertContentNode { func webAppLocationAlertController(context: AccountContext, accountPeer: EnginePeer, botPeer: EnginePeer, completion: @escaping (Bool) -> Void) -> AlertController { let presentationData = context.sharedContext.currentPresentationData.with { $0 } let strings = presentationData.strings - - //TODO:localize - let text = "**\(botPeer.compactDisplayTitle)** requests access to your **location**. You will be able to revoke this access in the profile page of **\(botPeer.compactDisplayTitle)**." - + + let text = strings.WebApp_LocationPermission_Text(botPeer.compactDisplayTitle, botPeer.compactDisplayTitle).string + var dismissImpl: ((Bool) -> Void)? var contentNode: WebAppLocationAlertContentNode? - let actions: [TextAlertAction] = [TextAlertAction(type: .genericAction, title: "Decline", action: { + let actions: [TextAlertAction] = [TextAlertAction(type: .genericAction, title: strings.WebApp_LocationPermission_Decline, action: { dismissImpl?(true) completion(false) - }), TextAlertAction(type: .defaultAction, title: "Allow", action: { + }), TextAlertAction(type: .defaultAction, title: strings.WebApp_LocationPermission_Allow, action: { dismissImpl?(true) completion(true) })] diff --git a/submodules/WebUI/Sources/WebAppSetEmojiStatusScreen.swift b/submodules/WebUI/Sources/WebAppSetEmojiStatusScreen.swift index 7dfbf453f2..71cdcd0042 100644 --- a/submodules/WebUI/Sources/WebAppSetEmojiStatusScreen.swift +++ b/submodules/WebUI/Sources/WebAppSetEmojiStatusScreen.swift @@ -83,6 +83,7 @@ private final class SheetContent: CombinedComponent { let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } let theme = presentationData.theme + let strings = presentationData.strings var contentSize = CGSize(width: context.availableSize.width, height: 18.0) @@ -134,7 +135,7 @@ private final class SheetContent: CombinedComponent { contentSize.height += 128.0 let title = title.update( - component: Text(text: "Set Emoji Status", font: Font.bold(24.0), color: theme.list.itemPrimaryTextColor), + component: Text(text: strings.WebApp_Emoji_Title, font: Font.bold(24.0), color: theme.list.itemPrimaryTextColor), availableSize: CGSize(width: constrainedTitleWidth, height: context.availableSize.height), transition: .immediate ) @@ -153,11 +154,11 @@ private final class SheetContent: CombinedComponent { }) var textString: String - if let _ = component.duration { - //TODO:localize - textString = "Do you want to set this emoji status suggested by **\(component.botName)** for **5 minutes**?" + if let duration = component.duration { + let durationString = scheduledTimeIntervalString(strings: strings, value: duration) + textString = strings.WebApp_Emoji_DurationText(component.botName, durationString).string } else { - textString = "Do you want to set this emoji status suggested by **\(component.botName)**?" + textString = strings.WebApp_Emoji_Text(component.botName).string } let text = text.update( @@ -207,7 +208,7 @@ private final class SheetContent: CombinedComponent { ), content: AnyComponentWithIdentity( id: AnyHashable(0), - component: AnyComponent(MultilineTextComponent(text: .plain(NSMutableAttributedString(string: "Confirm", font: Font.semibold(17.0), textColor: theme.list.itemCheckColors.foregroundColor, paragraphAlignment: .center)))) + component: AnyComponent(MultilineTextComponent(text: .plain(NSMutableAttributedString(string: strings.WebApp_Emoji_Confirm, font: Font.semibold(17.0), textColor: theme.list.itemCheckColors.foregroundColor, paragraphAlignment: .center)))) ), isEnabled: true, displaysProgress: false, diff --git a/versions.json b/versions.json index 2dc865d46f..dc9b8e2579 100644 --- a/versions.json +++ b/versions.json @@ -1,5 +1,5 @@ { - "app": "11.3.1", + "app": "11.4", "xcode": "16.0", "bazel": "7.3.1:981f82a470bad1349322b6f51c9c6ffa0aa291dab1014fac411543c12e661dff", "macos": "15.0"