Web app improvements

This commit is contained in:
Ilya Laktyushin 2023-10-27 03:21:21 +04:00
parent bc09798555
commit e3866ea65b
10 changed files with 168 additions and 65 deletions

View File

@ -10411,3 +10411,5 @@ Sorry for the inconvenience.";
"ChannelBoost.EnableColors" = "Enable Colors"; "ChannelBoost.EnableColors" = "Enable Colors";
"ChannelBoost.EnableColorsText" = "Your channel needs %1$@ to change channel color.\n\nAsk your **Premium** subscribers to boost your channel with this link:"; "ChannelBoost.EnableColorsText" = "Your channel needs %1$@ to change channel color.\n\nAsk your **Premium** subscribers to boost your channel with this link:";
"ChannelBoost.BoostAgain" = "Boost Again"; "ChannelBoost.BoostAgain" = "Boost Again";
"Settings.New" = "NEW";

View File

@ -2421,6 +2421,25 @@ public func chatMessageImageFile(account: Account, userLocation: MediaResourceUs
} }
} }
public func preloadedBotIcon(account: Account, fileReference: FileMediaReference) -> Signal<Bool, NoError> {
let signal = Signal<Bool, NoError> { subscriber in
let fetched = fetchedMediaResource(mediaBox: account.postbox.mediaBox, userLocation: .other, userContentType: MediaResourceUserContentType(file: fileReference.media), reference: fileReference.resourceReference(fileReference.media.resource)).start()
let dataDisposable = account.postbox.mediaBox.resourceData(fileReference.media.resource, option: .incremental(waitUntilFetchStatus: false)).start(next: { data in
if data.complete {
subscriber.putNext(true)
subscriber.putCompletion()
} else {
subscriber.putNext(false)
}
})
return ActionDisposable {
fetched.dispose()
dataDisposable.dispose()
}
}
return signal
}
public func instantPageImageFile(account: Account, userLocation: MediaResourceUserLocation, fileReference: FileMediaReference, fetched: Bool = false) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { public func instantPageImageFile(account: Account, userLocation: MediaResourceUserLocation, fileReference: FileMediaReference, fetched: Bool = false) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> {
return chatMessageFileDatas(account: account, userLocation: userLocation, fileReference: fileReference, progressive: false, fetched: fetched) return chatMessageFileDatas(account: account, userLocation: userLocation, fileReference: fileReference, progressive: false, fetched: fetched)
|> map { value in |> map { value in

View File

@ -471,7 +471,7 @@ func _internal_acceptAttachMenuBotDisclaimer(postbox: Postbox, botId: PeerId) ->
} |> ignoreValues } |> ignoreValues
} }
public struct AttachMenuBot { public struct AttachMenuBot: Equatable {
public let peer: EnginePeer public let peer: EnginePeer
public let shortName: String public let shortName: String
public let icons: [AttachMenuBots.Bot.IconName: TelegramMediaFile] public let icons: [AttachMenuBots.Bot.IconName: TelegramMediaFile]

View File

@ -13551,6 +13551,26 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
} }
completion(controller, controller.mediaPickerContext) completion(controller, controller.mediaPickerContext)
strongSelf.controllerNavigationDisposable.set(nil) strongSelf.controllerNavigationDisposable.set(nil)
if bot.flags.contains(.notActivated) {
let alertController = webAppTermsAlertController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, bot: bot, completion: { [weak self] allowWrite in
guard let self else {
return
}
if bot.flags.contains(.showInSettingsDisclaimer) {
let _ = self.context.engine.messages.acceptAttachMenuBotDisclaimer(botId: bot.peer.id).startStandalone()
}
let _ = (self.context.engine.messages.addBotToAttachMenu(botId: bot.peer.id, allowWrite: allowWrite)
|> deliverOnMainQueue).startStandalone(error: { _ in
}, completed: { [weak controller] in
controller?.refresh()
})
},
dismissed: {
strongSelf.attachmentController?.dismiss(animated: true)
})
strongSelf.present(alertController, in: .window(.root))
}
default: default:
break break
} }

View File

@ -459,23 +459,12 @@ class ContactMultiselectionControllerImpl: ViewController, ContactMultiselection
} }
chatsNode.updateState { state in chatsNode.updateState { state in
var state = state var state = state
if "".isEmpty { if state.selectedAdditionalCategoryIds.contains(id) {
if !state.selectedAdditionalCategoryIds.contains(id) { state.selectedAdditionalCategoryIds.remove(id)
for id in state.selectedAdditionalCategoryIds { removedTokenIds.append(id)
removedTokenIds.append(id)
state.selectedAdditionalCategoryIds.remove(id)
}
state.selectedAdditionalCategoryIds.insert(id)
addedToken = categoryToken
}
} else { } else {
if state.selectedAdditionalCategoryIds.contains(id) { state.selectedAdditionalCategoryIds.insert(id)
state.selectedAdditionalCategoryIds.remove(id) addedToken = categoryToken
removedTokenIds.append(id)
} else {
state.selectedAdditionalCategoryIds.insert(id)
addedToken = categoryToken
}
} }
return state return state

View File

@ -9,12 +9,13 @@ final class PeerInfoScreenDisclosureItem: PeerInfoScreenItem {
case text(String) case text(String)
case badge(String, UIColor) case badge(String, UIColor)
case semitransparentBadge(String, UIColor) case semitransparentBadge(String, UIColor)
case titleBadge(String, UIColor)
var text: String { var text: String {
switch self { switch self {
case .none: case .none:
return "" return ""
case let .text(text), let .badge(text, _), let .semitransparentBadge(text, _): case let .text(text), let .badge(text, _), let .semitransparentBadge(text, _), let .titleBadge(text, _):
return text return text
} }
} }
@ -23,7 +24,7 @@ final class PeerInfoScreenDisclosureItem: PeerInfoScreenItem {
switch self { switch self {
case .none, .text: case .none, .text:
return nil return nil
case let .badge(_, color), let .semitransparentBadge(_, color): case let .badge(_, color), let .semitransparentBadge(_, color), let .titleBadge(_, color):
return color return color
} }
} }
@ -146,6 +147,9 @@ private final class PeerInfoScreenDisclosureItemNode: PeerInfoScreenItemNode {
} else if case .badge = item.label { } else if case .badge = item.label {
labelColorValue = presentationData.theme.list.itemCheckColors.foregroundColor labelColorValue = presentationData.theme.list.itemCheckColors.foregroundColor
labelFont = Font.regular(15.0) labelFont = Font.regular(15.0)
} else if case .titleBadge = item.label {
labelColorValue = presentationData.theme.list.itemCheckColors.foregroundColor
labelFont = Font.medium(11.0)
} else { } else {
labelColorValue = presentationData.theme.list.itemSecondaryTextColor labelColorValue = presentationData.theme.list.itemSecondaryTextColor
labelFont = titleFont labelFont = titleFont
@ -178,7 +182,7 @@ private final class PeerInfoScreenDisclosureItemNode: PeerInfoScreenItemNode {
if previousItem?.text != item.text { if previousItem?.text != item.text {
self.iconNode.image = nil self.iconNode.image = nil
self.iconDisposable.set((iconSignal self.iconDisposable.set((iconSignal
|> deliverOnMainQueue).startStrict(next: { [weak self] icon in |> deliverOnMainQueue).startStrict(next: { [weak self] icon in
if let self { if let self {
self.iconNode.image = icon self.iconNode.image = icon
} }
@ -217,6 +221,13 @@ private final class PeerInfoScreenDisclosureItemNode: PeerInfoScreenItemNode {
if self.labelBadgeNode.supernode == nil { if self.labelBadgeNode.supernode == nil {
self.insertSubnode(self.labelBadgeNode, belowSubnode: self.labelNode) self.insertSubnode(self.labelBadgeNode, belowSubnode: self.labelNode)
} }
} else if case let .titleBadge(text, badgeColor) = item.label, !text.isEmpty {
if previousItem?.label.badgeColor != badgeColor {
self.labelBadgeNode.image = generateFilledRoundedRectImage(size: CGSize(width: 16.0, height: 16.0), cornerRadius: 5.0, color: badgeColor)?.stretchableImage(withLeftCapWidth: 6, topCapHeight: 6)
}
if self.labelBadgeNode.supernode == nil {
self.insertSubnode(self.labelBadgeNode, belowSubnode: self.labelNode)
}
} else { } else {
self.labelBadgeNode.removeFromSupernode() self.labelBadgeNode.removeFromSupernode()
} }
@ -230,11 +241,18 @@ private final class PeerInfoScreenDisclosureItemNode: PeerInfoScreenItemNode {
labelFrame = CGRect(origin: CGPoint(x: width - rightInset - badgeWidth + (badgeWidth - labelSize.width) / 2.0, y: floor((height - labelSize.height) / 2.0)), size: labelSize) labelFrame = CGRect(origin: CGPoint(x: width - rightInset - badgeWidth + (badgeWidth - labelSize.width) / 2.0, y: floor((height - labelSize.height) / 2.0)), size: labelSize)
} else if case .badge = item.label { } else if case .badge = item.label {
labelFrame = CGRect(origin: CGPoint(x: width - rightInset - badgeWidth + (badgeWidth - labelSize.width) / 2.0, y: floor((height - labelSize.height) / 2.0)), size: labelSize) labelFrame = CGRect(origin: CGPoint(x: width - rightInset - badgeWidth + (badgeWidth - labelSize.width) / 2.0, y: floor((height - labelSize.height) / 2.0)), size: labelSize)
} else if case .titleBadge = item.label {
labelFrame = CGRect(origin: CGPoint(x: textFrame.maxX + 10.0, y: floor((height - labelSize.height) / 2.0) + 1.0), size: labelSize)
} else { } else {
labelFrame = CGRect(origin: CGPoint(x: width - rightInset - labelSize.width, y: 12.0), size: labelSize) labelFrame = CGRect(origin: CGPoint(x: width - rightInset - labelSize.width, y: 12.0), size: labelSize)
} }
let labelBadgeNodeFrame = CGRect(origin: CGPoint(x: width - rightInset - badgeWidth, y: floorToScreenPixels(labelFrame.midY - badgeDiameter / 2.0)), size: CGSize(width: badgeWidth, height: badgeDiameter)) let labelBadgeNodeFrame: CGRect
if case .titleBadge = item.label {
labelBadgeNodeFrame = labelFrame.insetBy(dx: -4.0, dy: -2.0 + UIScreenPixel)
} else {
labelBadgeNodeFrame = CGRect(origin: CGPoint(x: width - rightInset - badgeWidth, y: floorToScreenPixels(labelFrame.midY - badgeDiameter / 2.0)), size: CGSize(width: badgeWidth, height: badgeDiameter))
}
self.activateArea.accessibilityLabel = item.text self.activateArea.accessibilityLabel = item.text
self.activateArea.accessibilityValue = item.label.text self.activateArea.accessibilityValue = item.label.text

View File

@ -13,6 +13,7 @@ import TelegramNotices
import AccountUtils import AccountUtils
import DeviceAccess import DeviceAccess
import PeerInfoVisualMediaPaneNode import PeerInfoVisualMediaPaneNode
import PhotoResources
enum PeerInfoUpdatingAvatar { enum PeerInfoUpdatingAvatar {
case none case none
@ -494,20 +495,56 @@ func peerInfoScreenSettingsData(context: AccountContext, peerId: EnginePeer.Id,
let botsKey = ValueBoxKey(length: 8) let botsKey = ValueBoxKey(length: 8)
botsKey.setInt64(0, value: 0) botsKey.setInt64(0, value: 0)
var iconLoaded: [EnginePeer.Id: Bool] = [:]
let bots = context.engine.data.subscribe(TelegramEngine.EngineData.Item.ItemCache.Item(collectionId: Namespaces.CachedItemCollection.attachMenuBots, id: botsKey)) let bots = context.engine.data.subscribe(TelegramEngine.EngineData.Item.ItemCache.Item(collectionId: Namespaces.CachedItemCollection.attachMenuBots, id: botsKey))
|> mapToSignal { entry -> Signal<[AttachMenuBot], NoError> in |> mapToSignal { entry -> Signal<[AttachMenuBot], NoError> in
let bots: [AttachMenuBots.Bot] = entry?.get(AttachMenuBots.self)?.bots ?? [] let bots: [AttachMenuBots.Bot] = entry?.get(AttachMenuBots.self)?.bots ?? []
return context.engine.data.subscribe( return context.engine.data.subscribe(
EngineDataMap(bots.map(\.peerId).map(TelegramEngine.EngineData.Item.Peer.Peer.init)) EngineDataMap(bots.map(\.peerId).map(TelegramEngine.EngineData.Item.Peer.Peer.init))
) )
|> map { peersMap -> [AttachMenuBot] in |> mapToSignal { peersMap -> Signal<[AttachMenuBot], NoError> in
var result: [AttachMenuBot] = [] var result: [Signal<AttachMenuBot?, NoError>] = []
for bot in bots { for bot in bots {
if let maybePeer = peersMap[bot.peerId], let peer = maybePeer { if let maybePeer = peersMap[bot.peerId], let peer = maybePeer {
result.append(AttachMenuBot(peer: peer, shortName: bot.name, icons: bot.icons, peerTypes: bot.peerTypes, flags: bot.flags)) let resultBot = AttachMenuBot(peer: peer, shortName: bot.name, icons: bot.icons, peerTypes: bot.peerTypes, flags: bot.flags)
if bot.flags.contains(.showInSettings) {
if let peer = PeerReference(peer._asPeer()), let icon = bot.icons[.iOSSettingsStatic] {
let fileReference: FileMediaReference = .attachBot(peer: peer, media: icon)
let signal: Signal<AttachMenuBot?, NoError>
if let _ = iconLoaded[peer.id] {
signal = .single(resultBot)
} else {
signal = .single(nil)
|> then(
preloadedBotIcon(account: context.account, fileReference: fileReference)
|> filter { $0 }
|> map { _ -> AttachMenuBot? in
return resultBot
}
|> afterNext { _ in
iconLoaded[peer.id] = true
}
)
}
result.append(signal)
} else {
result.append(.single(resultBot))
}
}
} }
} }
return result return combineLatest(result)
|> map { bots in
var result: [AttachMenuBot] = []
for bot in bots {
if let bot {
result.append(bot)
}
}
return result
}
|> distinctUntilChanged
} }
} }

View File

@ -818,25 +818,24 @@ private func settingsItems(data: PeerInfoScreenData?, context: AccountContext, p
var appIndex = 1000 var appIndex = 1000
if let settings = data.globalSettings { if let settings = data.globalSettings {
for bot in settings.bots { for bot in settings.bots {
if bot.flags.contains(.showInSettings) { let iconSignal: Signal<UIImage?, NoError>
let iconSignal: Signal<UIImage?, NoError> if let peer = PeerReference(bot.peer._asPeer()), let icon = bot.icons[.iOSSettingsStatic] {
if let peer = PeerReference(bot.peer._asPeer()), let icon = bot.icons[.iOSSettingsStatic] { let fileReference: FileMediaReference = .attachBot(peer: peer, media: icon)
let fileReference: FileMediaReference = .attachBot(peer: peer, media: icon) iconSignal = instantPageImageFile(account: context.account, userLocation: .other, fileReference: fileReference, fetched: true)
iconSignal = instantPageImageFile(account: context.account, userLocation: .other, fileReference: fileReference, fetched: true) |> map { generator -> UIImage? in
|> map { generator -> UIImage? in let size = CGSize(width: 29.0, height: 29.0)
let size = CGSize(width: 29.0, height: 29.0) let context = generator(TransformImageArguments(corners: ImageCorners(), imageSize: size, boundingSize: size, intrinsicInsets: .zero))
let context = generator(TransformImageArguments(corners: ImageCorners(), imageSize: size, boundingSize: size, intrinsicInsets: .zero)) return context?.generateImage()
return context?.generateImage()
}
let _ = freeMediaFileInteractiveFetched(account: context.account, userLocation: .other, fileReference: fileReference).startStandalone()
} else {
iconSignal = .single(UIImage(bundleImageName: "Settings/Menu/Websites")!)
} }
items[.apps]!.append(PeerInfoScreenDisclosureItem(id: bot.peer.id.id._internalGetInt64Value(), text: bot.shortName, icon: nil, iconSignal: iconSignal, action: { let _ = freeMediaFileInteractiveFetched(account: context.account, userLocation: .other, fileReference: fileReference).startStandalone()
interaction.openBotApp(bot) } else {
})) iconSignal = .single(UIImage(bundleImageName: "Settings/Menu/Websites")!)
appIndex += 1
} }
let label: PeerInfoScreenDisclosureItem.Label = bot.flags.contains(.notActivated) || bot.flags.contains(.showInSettingsDisclaimer) ? .titleBadge(presentationData.strings.Settings_New, presentationData.theme.list.itemAccentColor) : .none
items[.apps]!.append(PeerInfoScreenDisclosureItem(id: bot.peer.id.id._internalGetInt64Value(), label: label, text: bot.shortName, icon: nil, iconSignal: iconSignal, action: {
interaction.openBotApp(bot)
}))
appIndex += 1
} }
} }
@ -4756,6 +4755,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
guard let self else { guard let self else {
return return
} }
let showInstalledTooltip = !bot.flags.contains(.showInSettingsDisclaimer)
if bot.flags.contains(.showInSettingsDisclaimer) { if bot.flags.contains(.showInSettingsDisclaimer) {
let _ = self.context.engine.messages.acceptAttachMenuBotDisclaimer(botId: bot.peer.id).startStandalone() let _ = self.context.engine.messages.acceptAttachMenuBotDisclaimer(botId: bot.peer.id).startStandalone()
} }
@ -4763,7 +4763,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
let _ = (self.context.engine.messages.addBotToAttachMenu(botId: bot.peer.id, allowWrite: allowWrite) let _ = (self.context.engine.messages.addBotToAttachMenu(botId: bot.peer.id, allowWrite: allowWrite)
|> deliverOnMainQueue).startStandalone(error: { _ in |> deliverOnMainQueue).startStandalone(error: { _ in
}, completed: { }, completed: {
proceed(true) proceed(showInstalledTooltip)
}) })
} else { } else {
proceed(false) proceed(false)

View File

@ -413,6 +413,32 @@ public final class WebAppController: ViewController, AttachmentContainable {
}) })
}) })
self.setupWebView()
}
deinit {
self.placeholderDisposable?.dispose()
self.iconDisposable?.dispose()
self.keepAliveDisposable?.dispose()
self.paymentDisposable?.dispose()
self.webView?.removeObserver(self, forKeyPath: #keyPath(WKWebView.estimatedProgress))
}
override func didLoad() {
super.didLoad()
guard let webView = self.webView else {
return
}
self.view.addSubview(webView)
webView.scrollView.insertSubview(self.topOverscrollNode.view, at: 0)
}
func setupWebView() {
guard let controller = self.controller else {
return
}
if let url = controller.url, controller.source != .menu { if let url = controller.url, controller.source != .menu {
self.queryId = controller.queryId self.queryId = controller.queryId
if let parsedUrl = URL(string: url) { if let parsedUrl = URL(string: url) {
@ -433,7 +459,7 @@ public final class WebAppController: ViewController, AttachmentContainable {
} }
} else { } else {
if controller.source.isSimple { if controller.source.isSimple {
let _ = (context.engine.messages.requestSimpleWebView(botId: controller.botId, url: nil, source: .settings, themeParams: generateWebAppThemeParams(presentationData.theme)) let _ = (self.context.engine.messages.requestSimpleWebView(botId: controller.botId, url: nil, source: .settings, themeParams: generateWebAppThemeParams(presentationData.theme))
|> deliverOnMainQueue).start(next: { [weak self] result in |> deliverOnMainQueue).start(next: { [weak self] result in
guard let strongSelf = self else { guard let strongSelf = self else {
return return
@ -443,7 +469,7 @@ public final class WebAppController: ViewController, AttachmentContainable {
} }
}) })
} else { } else {
let _ = (context.engine.messages.requestWebView(peerId: controller.peerId, botId: controller.botId, url: controller.url, payload: controller.payload, themeParams: generateWebAppThemeParams(presentationData.theme), fromMenu: controller.source == .menu, replyToMessageId: controller.replyToMessageId, threadId: controller.threadId) let _ = (self.context.engine.messages.requestWebView(peerId: controller.peerId, botId: controller.botId, url: controller.url, payload: controller.payload, themeParams: generateWebAppThemeParams(presentationData.theme), fromMenu: controller.source == .menu, replyToMessageId: controller.replyToMessageId, threadId: controller.threadId)
|> deliverOnMainQueue).start(next: { [weak self] result in |> deliverOnMainQueue).start(next: { [weak self] result in
guard let strongSelf = self else { guard let strongSelf = self else {
return return
@ -469,25 +495,6 @@ public final class WebAppController: ViewController, AttachmentContainable {
} }
} }
deinit {
self.placeholderDisposable?.dispose()
self.iconDisposable?.dispose()
self.keepAliveDisposable?.dispose()
self.paymentDisposable?.dispose()
self.webView?.removeObserver(self, forKeyPath: #keyPath(WKWebView.estimatedProgress))
}
override func didLoad() {
super.didLoad()
guard let webView = self.webView else {
return
}
self.view.addSubview(webView)
webView.scrollView.insertSubview(self.topOverscrollNode.view, at: 0)
}
@objc fileprivate func mainButtonPressed() { @objc fileprivate func mainButtonPressed() {
if let mainButtonState = self.mainButtonState, !mainButtonState.isVisible || !mainButtonState.isEnabled { if let mainButtonState = self.mainButtonState, !mainButtonState.isVisible || !mainButtonState.isEnabled {
return return
@ -1624,6 +1631,10 @@ public final class WebAppController: ViewController, AttachmentContainable {
self.updateTabBarAlpha(1.0, .immediate) self.updateTabBarAlpha(1.0, .immediate)
} }
public func refresh() {
self.controllerNode.setupWebView()
}
public func requestDismiss(completion: @escaping () -> Void) { public func requestDismiss(completion: @escaping () -> Void) {
if self.controllerNode.needDismissConfirmation { if self.controllerNode.needDismissConfirmation {
let actionSheet = ActionSheetController(presentationData: self.presentationData) let actionSheet = ActionSheetController(presentationData: self.presentationData)

View File

@ -353,7 +353,8 @@ public func webAppTermsAlertController(
context: AccountContext, context: AccountContext,
updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)?, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)?,
bot: AttachMenuBot, bot: AttachMenuBot,
completion: @escaping (Bool) -> Void completion: @escaping (Bool) -> Void,
dismissed: @escaping () -> Void = {}
) -> AlertController { ) -> AlertController {
let theme = defaultDarkColorPresentationTheme let theme = defaultDarkColorPresentationTheme
let presentationData: PresentationData let presentationData: PresentationData
@ -369,6 +370,7 @@ public func webAppTermsAlertController(
completion(true) completion(true)
dismissImpl?(true) dismissImpl?(true)
}), TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: { }), TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {
dismissed()
dismissImpl?(true) dismissImpl?(true)
})] })]
@ -382,6 +384,11 @@ public func webAppTermsAlertController(
}) })
} }
let controller = AlertController(theme: AlertControllerTheme(presentationData: presentationData), contentNode: contentNode) let controller = AlertController(theme: AlertControllerTheme(presentationData: presentationData), contentNode: contentNode)
controller.dismissed = { outside in
if outside {
dismissed()
}
}
dismissImpl = { [weak controller] animated in dismissImpl = { [weak controller] animated in
if animated { if animated {
controller?.dismissAnimated() controller?.dismissAnimated()