diff --git a/Nicegram/NGLottie/Sources/LottieViewImpl.swift b/Nicegram/NGLottie/Sources/LottieViewImpl.swift index 2d0428e9f57..ed8fc94457c 100644 --- a/Nicegram/NGLottie/Sources/LottieViewImpl.swift +++ b/Nicegram/NGLottie/Sources/LottieViewImpl.swift @@ -40,6 +40,19 @@ extension LottieViewImpl: LottieViewProtocol { ) } + public func setImageProvider(_ imageProvider: ImageProvider?) { + if let imageProvider { + animationView.imageProvider = AnonymousImageProvider( + provider: imageProvider + ) + } else { + animationView.imageProvider = BundleImageProvider( + bundle: .main, + searchPath: nil + ) + } + } + public func setLoopMode(_ loopMode: LoopMode) { let mode: LottieLoopMode switch loopMode { @@ -62,3 +75,17 @@ extension LottieViewImpl: LottieViewProtocol { animationView.stop() } } + +private class AnonymousImageProvider { + let provider: LottieViewProtocol.ImageProvider + + init(provider: LottieViewProtocol.ImageProvider) { + self.provider = provider + } +} + +extension AnonymousImageProvider: AnimationImageProvider { + func imageForAsset(asset: ImageAsset) -> CGImage? { + provider.image(asset.id) + } +} diff --git a/Nicegram/NGStats/BUILD b/Nicegram/NGStats/BUILD index 5cca9fcaa4f..cece22c3d14 100644 --- a/Nicegram/NGStats/BUILD +++ b/Nicegram/NGStats/BUILD @@ -7,10 +7,8 @@ swift_library( "Sources/**/*.swift", ]), deps = [ - "@swiftpkg_nicegram_assistant_ios//:NGApi", + "@swiftpkg_nicegram_assistant_ios//:FeatNicegramHub", "//submodules/AccountContext:AccountContext", - "//Nicegram/NGData:NGData", - "//Nicegram/NGRemoteConfig:NGRemoteConfig", "//Nicegram/NGUtils:NGUtils", ], visibility = [ diff --git a/Nicegram/NGStats/Sources/ChatsSharing.swift b/Nicegram/NGStats/Sources/ChatsSharing.swift new file mode 100644 index 00000000000..74898560026 --- /dev/null +++ b/Nicegram/NGStats/Sources/ChatsSharing.swift @@ -0,0 +1,278 @@ +import AccountContext +import FeatNicegramHub +import Foundation +import NGUtils +import Postbox +import SwiftSignalKit +import TelegramCore + +@available(iOS 13.0, *) +public func sharePeerData( + peerId: PeerId, + context: AccountContext +) { + _ = (context.account.viewTracker.peerView(peerId, updateData: true) + |> take(1)) + .start(next: { peerView in + if let peer = peerView.peers[peerView.peerId] { + Task { + await sharePeerData( + peer: peer, + cachedData: peerView.cachedData, + context: context + ) + } + } + }) +} + +@available(iOS 13.0, *) +private func sharePeerData( + peer: Peer, + cachedData: CachedPeerData?, + context: AccountContext +) async { + let peerId = extractPeerId(peer: peer) + + let type: PeerType + switch EnginePeer(peer) { + case let .channel(channel): + switch channel.info { + case .group: + type = .group + case .broadcast: + type = .channel + } + case .legacyGroup: + type = .group + case .user(let user): + if user.botInfo != nil { + type = .bot + } else { + return + } + default: + return + } + + let sharePeerDataUseCase = NicegramHubContainer.shared.sharePeerDataUseCase() + + await sharePeerDataUseCase( + id: peerId, + participantsCount: extractParticipantsCount( + peer: peer, + cachedData: cachedData + ), + type: type, + peerDataProvider: { + await withCheckedContinuation { continuation in + let avatarImageSignal = fetchAvatarImage(peer: peer, context: context) + let inviteLinksSignal = context.engine.peers.direct_peerExportedInvitations(peerId: peer.id, revoked: false) + let interlocutorLanguageSignal = wrapped_detectInterlocutorLanguage(forChatWith: peer.id, context: context) + + _ = (combineLatest(avatarImageSignal, inviteLinksSignal, interlocutorLanguageSignal) + |> take(1)).start(next: { avatarImageData, inviteLinks, interlocutorLanguage in + let peerData = PeerData( + avatarImageData: avatarImageData?.base64EncodedString(), + id: peerId, + inviteLinks: extractInviteLinks(inviteLinks), + payload: extractPayload( + peer: peer, + cachedData: cachedData, + interlocutorLanguage: interlocutorLanguage + ), + type: type + ) + continuation.resume(returning: peerData) + }) + } + } + ) +} + +private func extractPayload( + peer: Peer, + cachedData: CachedPeerData?, + interlocutorLanguage: String? +) -> PeerPayload? { + switch EnginePeer(peer) { + case let .legacyGroup(group): + PeerPayload.legacyGroup( + extractPayload( + group: group, + cachedData: cachedData as? CachedGroupData, + lastMessageLanguageCode: interlocutorLanguage + ) + ) + case let .channel(channel): + PeerPayload.channel( + extractPayload( + channel: channel, + cachedData: cachedData as? CachedChannelData, + lastMessageLanguageCode: interlocutorLanguage + ) + ) + case let .user(user): + if let botInfo = user.botInfo { + PeerPayload.user( + extractPayload( + bot: user, + botInfo: botInfo, + lastMessageLanguageCode: interlocutorLanguage + ) + ) + } else { + nil + } + default: + nil + } +} + +private func extractPayload(group: TelegramGroup, cachedData: CachedGroupData?, lastMessageLanguageCode: String?) -> LegacyGroupPayload { + LegacyGroupPayload( + deactivated: group.flags.contains(.deactivated), + title: group.title, + participantsCount: group.participantCount, + date: group.creationDate, + migratedTo: group.migrationReference?.peerId.id._internalGetInt64Value(), + photo: extractChatPhoto(peer: group), + lastMessageLang: lastMessageLanguageCode, + about: cachedData?.about + ) +} + +private func extractPayload(channel: TelegramChannel, cachedData: CachedChannelData?, lastMessageLanguageCode: String?) -> ChannelPayload { + ChannelPayload( + verified: channel.isVerified, + scam: channel.isScam, + hasGeo: channel.flags.contains(.hasGeo), + fake: channel.isFake, + gigagroup: channel.flags.contains(.isGigagroup), + title: channel.title, + username: channel.username, + date: channel.creationDate, + restrictions: extractRestrictions(restrictionInfo: channel.restrictionInfo), + participantsCount: cachedData?.participantsSummary.memberCount, + photo: extractChatPhoto(peer: channel), + lastMessageLang: lastMessageLanguageCode, + about: cachedData?.about, + geoLocation: cachedData?.peerGeoLocation.map { + extractGeoLocation($0) + } + ) +} + +private func extractPayload( + bot: TelegramUser, + botInfo: BotUserInfo, + lastMessageLanguageCode: String? +) -> UserPayload { + let botFlags = botInfo.flags + let userFlags = bot.flags + + return UserPayload( + deleted: bot.isDeleted, + bot: true, + botChatHistory: botFlags.contains(.hasAccessToChatHistory), + botNochats: !botFlags.contains(.worksWithGroups), + verified: bot.isVerified, + restricted: bot.restrictionInfo != nil, + botInlineGeo: botFlags.contains(.requiresGeolocationForInlineRequests), + support: userFlags.contains(.isSupport), + scam: bot.isScam, + fake: bot.isFake, + botAttachMenu: botFlags.contains(.canBeAddedToAttachMenu), + premium: bot.isPremium, + botCanEdit: botFlags.contains(.canEdit), + firstName: bot.firstName, + lastName: bot.lastName, + username: bot.username, + phone: bot.phone, + photo: extractUserProfilePhoto(peer: bot), + restrictionReason: extractRestrictions(restrictionInfo: bot.restrictionInfo), + botInlinePlaceholder: botInfo.inlinePlaceholder, + langCode: lastMessageLanguageCode, + usernames: bot.usernames.map { + .init( + editable: $0.flags.contains(.isEditable), + active: $0.isActive, + username: $0.username + ) + } + ) +} + +private func extractChatPhoto( + peer: Peer +) -> ChatPhoto? { + guard let imageRepresentation = peer.profileImageRepresentations.first else { + return nil + } + guard let resource = imageRepresentation.resource as? CloudPeerPhotoSizeMediaResource else { + return nil + } + + return ChatPhoto(mediaResourceId: resource.id.stringRepresentation, datacenterId: resource.datacenterId, photoId: resource.photoId, volumeId: resource.volumeId, localId: resource.localId, sizeSpec: resource.sizeSpec.rawValue) +} + +private func extractUserProfilePhoto( + peer: Peer +) -> UserProfilePhoto? { + guard let imageRepresentation = peer.profileImageRepresentations.first else { + return nil + } + guard let resource = imageRepresentation.resource as? CloudPeerPhotoSizeMediaResource else { + return nil + } + + return UserProfilePhoto( + hasVideo: imageRepresentation.hasVideo, + personal: imageRepresentation.isPersonal, + photoId: resource.photoId, + dcId: resource.datacenterId + ) +} + +func extractParticipantsCount(peer: Peer, cachedData: CachedPeerData?) -> Int { + switch EnginePeer(peer) { + case .user: + return 2 + case .channel: + let channelData = cachedData as? CachedChannelData + return Int(channelData?.participantsSummary.memberCount ?? 0) + case let .legacyGroup(group): + return group.participantCount + case .secretChat: + return 0 + } +} + +private func extractRestrictions( + restrictionInfo: PeerAccessRestrictionInfo? +) -> [FeatNicegramHub.RestrictionRule] { + restrictionInfo?.rules.map { + FeatNicegramHub.RestrictionRule( + platform: $0.platform, + reason: $0.reason, + text: $0.text + ) + } ?? [] +} + +private func extractGeoLocation( + _ geo: PeerGeoLocation +) -> GeoLocation { + GeoLocation(latitude: geo.latitude, longitude: geo.longitude, address: geo.address) +} + +private func extractInviteLinks(_ links: ExportedInvitations?) -> [InviteLink]? { + links?.list?.compactMap { link in + switch link { + case let .link(link, title, isPermanent, requestApproval, isRevoked, adminId, date, startDate, expireDate, usageLimit, count, requestedCount): + InviteLink(link: link, title: title, isPermanent: isPermanent, requestApproval: requestApproval, isRevoked: isRevoked, adminId: adminId.id._internalGetInt64Value(), date: date, startDate: startDate, expireDate: expireDate, usageLimit: usageLimit, count: count, requestedCount: requestedCount) + case .publicJoinRequest: + nil + } + } +} diff --git a/Nicegram/NGStats/Sources/DTO.swift b/Nicegram/NGStats/Sources/DTO.swift deleted file mode 100644 index ff68c394315..00000000000 --- a/Nicegram/NGStats/Sources/DTO.swift +++ /dev/null @@ -1,137 +0,0 @@ -import TelegramCore -import Foundation - -struct ProfileInfoBody: Encodable { - let id: Int64 - let type: ProfileTypeDTO - let inviteLinks: [InviteLinkDTO] - let icon: String? - @Stringified var payload: AnyProfilePayload -} - -protocol ProfilePayload: Encodable {} -struct AnyProfilePayload: ProfilePayload { - let wrapped: ProfilePayload - - func encode(to encoder: Encoder) throws { - try wrapped.encode(to: encoder) - } -} - -struct GroupPayload: ProfilePayload { - let deactivated: Bool - let title: String - let participantsCount: Int - let date: Int32 - let migratedTo: Int64? - let photo: ProfileImageDTO? - let lastMessageLang: String? - let about: String? -} - -struct ChannelPayload: ProfilePayload { - let verified: Bool - let scam: Bool - let hasGeo: Bool - let fake: Bool - let gigagroup: Bool - let title: String - let username: String? - let date: Int32 - let restrictions: [RestrictionRuleDTO] - let participantsCount: Int32? - let photo: ProfileImageDTO? - let lastMessageLang: String? - let about: String? - let geoLocation: GeoLocationDTO? -} - -enum ProfileTypeDTO: String, Encodable { - case channel - case group -} - -struct ProfileImageDTO: Encodable { - let mediaResourceId: String - let datacenterId: Int - let photoId: Int64? - let volumeId: Int64? - let localId: Int32? - let sizeSpec: Int32 -} - -struct RestrictionRuleDTO: Encodable { - let platform: String - let reason: String - let text: String -} - -struct GeoLocationDTO: Encodable { - let latitude: Double - let longitude: Double - let address: String -} - -struct InviteLinkDTO: Encodable { - let link: String - let title: String? - let isPermanent: Bool - let requestApproval: Bool - let isRevoked: Bool - let adminId: Int64 - let date: Int32 - let startDate: Int32? - let expireDate: Int32? - let usageLimit: Int32? - let count: Int32? - let requestedCount: Int32? -} - -@propertyWrapper -struct Stringified: Encodable { - - var wrappedValue: T - - init(wrappedValue: T) { - self.wrappedValue = wrappedValue - } - - func encode(to encoder: Encoder) throws { - var container = encoder.singleValueContainer() - - let data = try JSONEncoder().encode(wrappedValue) - let string = String(data: data, encoding: .utf8) - try container.encode(string) - } -} - -// MARK: - Mapping - -extension ProfileImageDTO { - init(cloudPeerPhotoSizeMediaResource resource: CloudPeerPhotoSizeMediaResource) { - self.init(mediaResourceId: resource.id.stringRepresentation, datacenterId: resource.datacenterId, photoId: resource.photoId, volumeId: resource.volumeId, localId: resource.localId, sizeSpec: resource.sizeSpec.rawValue) - } -} - -extension RestrictionRuleDTO { - init(restrictionRule rule: RestrictionRule) { - self.init(platform: rule.platform, reason: rule.reason, text: rule.text) - } -} - -extension GeoLocationDTO { - init(peerGeoLocation geo: PeerGeoLocation) { - self.init(latitude: geo.latitude, longitude: geo.longitude, address: geo.address) - } -} - -extension InviteLinkDTO { - init?(exportedInvitation: ExportedInvitation) { - switch exportedInvitation { - case let .link(link, title, isPermanent, requestApproval, isRevoked, adminId, date, startDate, expireDate, usageLimit, count, requestedCount): - self.init(link: link, title: title, isPermanent: isPermanent, requestApproval: requestApproval, isRevoked: isRevoked, adminId: adminId.id._internalGetInt64Value(), date: date, startDate: startDate, expireDate: expireDate, usageLimit: usageLimit, count: count, requestedCount: requestedCount) - case .publicJoinRequest: - return nil - } - } -} diff --git a/Nicegram/NGStats/Sources/MetaStorage.swift b/Nicegram/NGStats/Sources/MetaStorage.swift deleted file mode 100644 index e720f5df01e..00000000000 --- a/Nicegram/NGStats/Sources/MetaStorage.swift +++ /dev/null @@ -1,41 +0,0 @@ -import NGCore -import Foundation - -struct ChatStatsMeta: Codable { - let id: Int64 - let sharedAt: Date -} - -class ChatStatsMetaStorage { - - // MARK: - Dependencies - - private let storage = FileStorage<[Int64: ChatStatsMeta]>(path: "chat-stats-meta") - - // MARK: - Lifecycle - - init() {} - - // MARK: - Public Functions - - func getSharedAt(peerId: Int64) -> Date? { - var dict = storage.read() ?? [:] - return dict[peerId]?.sharedAt - } - - func setSharedAt(_ sharedAt: Date, peerId: Int64) { - var dict = storage.read() ?? [:] - dict[peerId] = ChatStatsMeta(id: peerId, sharedAt: sharedAt) - storage.save(dict) - } - - public func removeItems(whereSharedAt: (Date) -> Bool) { - var dict = storage.read() ?? [:] - for (id, meta) in dict { - if whereSharedAt(meta.sharedAt) { - dict.removeValue(forKey: id) - } - } - storage.save(dict) - } -} diff --git a/Nicegram/NGStats/Sources/NGStats.swift b/Nicegram/NGStats/Sources/NGStats.swift deleted file mode 100644 index 3f5b169717e..00000000000 --- a/Nicegram/NGStats/Sources/NGStats.swift +++ /dev/null @@ -1,182 +0,0 @@ -import AccountContext -import NGApi -import NGData -import NGEnv -import NGUtils -import Postbox -import SwiftSignalKit -import TelegramCore -import Foundation - -private let thresholdGroupMemebrsCount: Int = 1000 - -private let apiClient = EsimApiClient( - baseUrl: URL(string: NGENV.esim_api_url)!, - apiKey: NGENV.esim_api_key, - mobileIdentifier: "" -) -private let throttlingService = ChatStatsThrottlingService() - -public func isShareChannelsInfoEnabled() -> Bool { - return NGSettings.shareChannelsInfo -} - -public func setShareChannelsInfo(enabled: Bool) { - NGSettings.shareChannelsInfo = enabled -} - -public func shareChannelInfo(peerId: PeerId, context: AccountContext) { - if !isShareChannelsInfoEnabled() { - return - } - - if throttlingService.shouldSkipShare(peerId: peerId) { - return - } - - _ = (context.account.viewTracker.peerView(peerId, updateData: true) - |> take(1)) - .start(next: { peerView in - if let peer = peerView.peers[peerView.peerId] { - shareChannelInfo(peer: peer, cachedData: peerView.cachedData, context: context) - } - }) -} - -private func shareChannelInfo(peer: Peer, cachedData: CachedPeerData?, context: AccountContext) { - if isGroup(peer: peer) { - let participantsCount = extractParticipantsCount(peer: peer, cachedData: cachedData) - if participantsCount < thresholdGroupMemebrsCount { - return - } - } else if !isChannel(peer: peer) { - return - } - - let avatarImageSignal = fetchAvatarImage(peer: peer, context: context) - let inviteLinksSignal = context.engine.peers.direct_peerExportedInvitations(peerId: peer.id, revoked: false) - let interlocutorLanguageSignal = wrapped_detectInterlocutorLanguage(forChatWith: peer.id, context: context) - - _ = (combineLatest(avatarImageSignal, inviteLinksSignal, interlocutorLanguageSignal) - |> take(1)).start(next: { avatarImageData, inviteLinks, interlocutorLanguage in - shareChannelInfo(peer: peer, cachedData: cachedData, avatarImageData: avatarImageData, inviteLinks: inviteLinks, interlocutorLanguage: interlocutorLanguage) - }) -} - -private func shareChannelInfo(peer: Peer, cachedData: CachedPeerData?, avatarImageData: Data?, inviteLinks: ExportedInvitations?, interlocutorLanguage: String?) { - let id = extractPeerId(peer: peer) - - let type: ProfileTypeDTO - let payload: ProfilePayload - switch EnginePeer(peer) { - case let .legacyGroup(group): - type = .group - payload = extractPayload( - group: group, - cachedData: cachedData as? CachedGroupData, - lastMessageLanguageCode: interlocutorLanguage - ) - case let .channel(channel): - switch channel.info { - case .broadcast: - type = .channel - case .group: - type = .group - } - payload = extractPayload( - channel: channel, - cachedData: cachedData as? CachedChannelData, - lastMessageLanguageCode: interlocutorLanguage - ) - default: - return - } - - let inviteLinks = inviteLinks?.list?.compactMap({ InviteLinkDTO(exportedInvitation: $0) }) ?? [] - - let profileImageBase64 = avatarImageData?.base64EncodedString() - - let body = ProfileInfoBody(id: id, type: type, inviteLinks: inviteLinks, icon: profileImageBase64, payload: AnyProfilePayload(wrapped: payload)) - - throttlingService.markAsShared(peerId: peer.id) - apiClient.send(.post(path: "telegram/chat", body: body), completion: nil) -} - -private func extractPayload(group: TelegramGroup, cachedData: CachedGroupData?, lastMessageLanguageCode: String?) -> ProfilePayload { - let isDeactivated = group.flags.contains(.deactivated) - let title = group.title - let participantsCount = group.participantCount - let date = group.creationDate - let migratedTo = group.migrationReference?.peerId.id._internalGetInt64Value() - let photo = extractProfileImageDTO(peer: group) - let about = cachedData?.about - - return GroupPayload(deactivated: isDeactivated, title: title, participantsCount: participantsCount, date: date, migratedTo: migratedTo, photo: photo, lastMessageLang: lastMessageLanguageCode, about: about) -} - -private func extractPayload(channel: TelegramChannel, cachedData: CachedChannelData?, lastMessageLanguageCode: String?) -> ProfilePayload { - let isVerified = channel.isVerified - let isScam = channel.isScam - let hasGeo = channel.flags.contains(.hasGeo) - let isFake = channel.isFake - let isGigagroup = channel.flags.contains(.isGigagroup) - let title = channel.title - let username = channel.username - let date = channel.creationDate - let restrictions = channel.restrictionInfo?.rules.map({ RestrictionRuleDTO(restrictionRule: $0) }) ?? [] - let participantsCount = cachedData?.participantsSummary.memberCount - let photo = extractProfileImageDTO(peer: channel) - let about = cachedData?.about - let geoLocation = cachedData?.peerGeoLocation.flatMap({ GeoLocationDTO(peerGeoLocation: $0) }) - - return ChannelPayload(verified: isVerified, scam: isScam, hasGeo: hasGeo, fake: isFake, gigagroup: isGigagroup, title: title, username: username, date: date, restrictions: restrictions, participantsCount: participantsCount, photo: photo, lastMessageLang: lastMessageLanguageCode, about: about, geoLocation: geoLocation) -} - -private func extractProfileImageDTO(peer: Peer) -> ProfileImageDTO? { - guard let imageRepresentation = peer.profileImageRepresentations.first else { - return nil - } - guard let resource = imageRepresentation.resource as? CloudPeerPhotoSizeMediaResource else { - return nil - } - return ProfileImageDTO(cloudPeerPhotoSizeMediaResource: resource) -} - -private func isChannel(peer: Peer) -> Bool { - if case let .channel(channel) = EnginePeer(peer), - case .broadcast = channel.info { - return true - } else { - return false - } -} - -public func isGroup(peer: Peer) -> Bool { - switch EnginePeer(peer) { - case let .channel(channel): - switch channel.info { - case .group: - return true - case .broadcast: - return false - } - case .legacyGroup: - return true - default: - return false - } -} - -func extractParticipantsCount(peer: Peer, cachedData: CachedPeerData?) -> Int { - switch EnginePeer(peer) { - case .user: - return 2 - case .channel: - let channelData = cachedData as? CachedChannelData - return Int(channelData?.participantsSummary.memberCount ?? 0) - case let .legacyGroup(group): - return group.participantCount - case .secretChat: - return 0 - } -} diff --git a/Nicegram/NGStats/Sources/StickersSharing.swift b/Nicegram/NGStats/Sources/StickersSharing.swift new file mode 100644 index 00000000000..faafca6a954 --- /dev/null +++ b/Nicegram/NGStats/Sources/StickersSharing.swift @@ -0,0 +1,60 @@ +import AccountContext +import FeatNicegramHub +import TelegramCore + +@available(iOS 13.0, *) +public class StickersDataProviderImpl { + + // MARK: - Dependencies + + private let context: AccountContext + + // MARK: - Lifecycle + + public init(context: AccountContext) { + self.context = context + } +} + +@available(iOS 13.0, *) +extension StickersDataProviderImpl: StickersDataProvider { + public func getStickersData() async -> StickersData? { + await withCheckedContinuation { continuation in + _ = context.account.postbox.transaction { transaction in + transaction + .getItemCollectionsInfos( + namespace: Namespaces.ItemCollection.CloudStickerPacks + ).compactMap { + $0.1 as? StickerPackCollectionInfo + }.map { pack in + let resource = pack.thumbnail?.resource as? CloudStickerPackThumbnailMediaResource + + let flags = pack.flags + + return StickerSet( + archived: false, + channelEmojiStatus: flags.contains(.isAvailableAsChannelStatus), + official: flags.contains(.isOfficial), + masks: flags.contains(.isMasks), + animated: flags.contains(.isAnimated), + videos: flags.contains(.isVideo), + emojis: flags.contains(.isEmoji), + id: pack.id.id, + title: pack.title, + shortName: pack.shortName, + thumbDcId: resource?.datacenterId, + thumbVersion: resource?.thumbVersion, + thumbDocumentId: pack.thumbnailFileId, + count: pack.count + ) + } + }.start(next: { stickerSets in + continuation.resume( + returning: StickersData( + stickerSets: stickerSets + ) + ) + }) + } + } +} diff --git a/Nicegram/NGStats/Sources/Throttling.swift b/Nicegram/NGStats/Sources/Throttling.swift deleted file mode 100644 index 47db7c6d4e5..00000000000 --- a/Nicegram/NGStats/Sources/Throttling.swift +++ /dev/null @@ -1,53 +0,0 @@ -import Foundation -import NGRemoteConfig -import Postbox - -class ChatStatsThrottlingService { - - // MARK: - Dependencies - - private let remoteConfig = RemoteConfigServiceImpl.shared - private let metaStorage = ChatStatsMetaStorage() - - // MARK: - Lifecycle - - init() { - metaStorage.removeItems { !self.shouldSkipShare(sharedAt: $0) } - } - - // MARK: - Public Functions - - func shouldSkipShare(peerId: PeerId) -> Bool { - let peerId = peerId.id._internalGetInt64Value() - - let sharedAt = metaStorage.getSharedAt(peerId: peerId) - return shouldSkipShare(sharedAt: sharedAt) - } - - func markAsShared(peerId: PeerId) { - let peerId = peerId.id._internalGetInt64Value() - - metaStorage.setSharedAt(Date(), peerId: peerId) - } - - // MARK: - Private Functions - - private func getShareChannelsConfig() -> ShareChannelsConfig { - let remoteValue = RemoteConfigServiceImpl.shared.get(ShareChannelsConfig.self, byKey: "shareChannelsConfig") - let defaultValue = ShareChannelsConfig(throttlingInterval: 86400) - return remoteValue ?? defaultValue - } - - private func shouldSkipShare(sharedAt: Date?) -> Bool { - let sharedAt = sharedAt ?? .distantPast - let currentDate = Date() - let throttlingInterval = getShareChannelsConfig().throttlingInterval - - return (sharedAt.addingTimeInterval(throttlingInterval) > currentDate) - } - -} - -private struct ShareChannelsConfig: Decodable { - let throttlingInterval: TimeInterval -} diff --git a/Nicegram/NGUI/BUILD b/Nicegram/NGUI/BUILD index 47093e25d0d..7cbfa09d22b 100644 --- a/Nicegram/NGUI/BUILD +++ b/Nicegram/NGUI/BUILD @@ -42,6 +42,8 @@ swift_library( "//Nicegram/NGStealthMode:NGStealthMode", "@swiftpkg_nicegram_assistant_ios//:NGAiChatUI", "@swiftpkg_nicegram_assistant_ios//:FeatImagesHubUI", + "@swiftpkg_nicegram_assistant_ios//:FeatNicegramHub", + "@swiftpkg_nicegram_assistant_ios//:FeatPinnedChats", "@swiftpkg_nicegram_assistant_ios//:FeatSpeechToText", ], visibility = [ diff --git a/Nicegram/NGUI/Sources/NicegramSettingsController.swift b/Nicegram/NGUI/Sources/NicegramSettingsController.swift index 071928c9a7e..0f32b58e427 100644 --- a/Nicegram/NGUI/Sources/NicegramSettingsController.swift +++ b/Nicegram/NGUI/Sources/NicegramSettingsController.swift @@ -11,6 +11,7 @@ import AccountContext import Display import FeatImagesHubUI +import FeatNicegramHub import FeatPinnedChats import Foundation import ItemListUI @@ -70,7 +71,7 @@ private enum NicegramSettingsControllerSection: Int32 { case Account case Other case QuickReplies - case ShareChannelsInfo + case ShareData case PinnedChats } @@ -120,8 +121,10 @@ private enum NicegramSettingsControllerEntry: ItemListNodeEntry { case secretMenu(String) - case shareChannelsInfoToggle(String, Bool) - case shareChannelsInfoNote(String) + case shareBotsData(String, Bool) + case shareChannelsData(String, Bool) + case shareStickersData(String, Bool) + case shareDataNote(String) // MARK: Section @@ -143,8 +146,8 @@ private enum NicegramSettingsControllerEntry: ItemListNodeEntry { return NicegramSettingsControllerSection.Account.rawValue case .secretMenu: return NicegramSettingsControllerSection.SecretMenu.rawValue - case .shareChannelsInfoToggle, .shareChannelsInfoNote: - return NicegramSettingsControllerSection.ShareChannelsInfo.rawValue + case .shareBotsData, .shareChannelsData, .shareStickersData, .shareDataNote: + return NicegramSettingsControllerSection.ShareData.rawValue case .pinnedChatsHeader, .pinnedChat: return NicegramSettingsControllerSection.PinnedChats.rawValue } @@ -223,10 +226,14 @@ private enum NicegramSettingsControllerEntry: ItemListNodeEntry { case let .easyToggle(index, _, _, _): return 5000 + Int32(index) - case .shareChannelsInfoToggle: + case .shareBotsData: return 6000 - case .shareChannelsInfoNote: + case .shareChannelsData: return 6001 + case .shareStickersData: + return 6002 + case .shareDataNote: + return 6010 } } @@ -373,14 +380,26 @@ private enum NicegramSettingsControllerEntry: ItemListNodeEntry { } else { return false } - case let .shareChannelsInfoToggle(lhsText, lhsValue): - if case let .shareChannelsInfoToggle(rhsText, rhsValue) = rhs, lhsText == rhsText, lhsValue == rhsValue { + case let .shareBotsData(lhsText, lhsValue): + if case let .shareBotsData(rhsText, rhsValue) = rhs, lhsText == rhsText, lhsValue == rhsValue { return true } else { return false } - case let .shareChannelsInfoNote(lhsText): - if case let .shareChannelsInfoNote(rhsText) = rhs, lhsText == rhsText { + case let .shareChannelsData(lhsText, lhsValue): + if case let .shareChannelsData(rhsText, rhsValue) = rhs, lhsText == rhsText, lhsValue == rhsValue { + return true + } else { + return false + } + case let .shareStickersData(lhsText, lhsValue): + if case let .shareStickersData(rhsText, rhsValue) = rhs, lhsText == rhsText, lhsValue == rhsValue { + return true + } else { + return false + } + case let .shareDataNote(lhsText): + if case let .shareDataNote(rhsText) = rhs, lhsText == rhsText { return true } else { return false @@ -530,11 +549,43 @@ private enum NicegramSettingsControllerEntry: ItemListNodeEntry { return ItemListActionItem(presentationData: presentationData, title: text, kind: .neutral, alignment: .natural, sectionId: section, style: .blocks) { arguments.pushController(secretMenuController(context: arguments.context)) } - case let .shareChannelsInfoToggle(text, value): + case let .shareBotsData(text, value): + return ItemListSwitchItem(presentationData: presentationData, title: text, value: value, enabled: true, sectionId: section, style: .blocks, updated: { value in + if #available(iOS 13.0, *) { + Task { + let updateSharingSettingsUseCase = NicegramHubContainer.shared.updateSharingSettingsUseCase() + + await updateSharingSettingsUseCase { + $0.with(\.shareBotsData, value) + } + } + } + }) + case let .shareChannelsData(text, value): + return ItemListSwitchItem(presentationData: presentationData, title: text, value: value, enabled: true, sectionId: section, style: .blocks, updated: { value in + if #available(iOS 13.0, *) { + Task { + let updateSharingSettingsUseCase = NicegramHubContainer.shared.updateSharingSettingsUseCase() + + await updateSharingSettingsUseCase { + $0.with(\.shareChannelsData, value) + } + } + } + }) + case let .shareStickersData(text, value): return ItemListSwitchItem(presentationData: presentationData, title: text, value: value, enabled: true, sectionId: section, style: .blocks, updated: { value in - setShareChannelsInfo(enabled: value) + if #available(iOS 13.0, *) { + Task { + let updateSharingSettingsUseCase = NicegramHubContainer.shared.updateSharingSettingsUseCase() + + await updateSharingSettingsUseCase { + $0.with(\.shareStickersData, value) + } + } + } }) - case let .shareChannelsInfoNote(text): + case let .shareDataNote(text): return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: section) case .pinnedChatsHeader: return ItemListSectionHeaderItem( @@ -570,7 +621,7 @@ private enum NicegramSettingsControllerEntry: ItemListNodeEntry { // MARK: Entries list -private func nicegramSettingsControllerEntries(presentationData: PresentationData, experimentalSettings: ExperimentalUISettings, showCalls: Bool, pinnedChats: [PinnedChat], context: AccountContext) -> [NicegramSettingsControllerEntry] { +private func nicegramSettingsControllerEntries(presentationData: PresentationData, experimentalSettings: ExperimentalUISettings, showCalls: Bool, pinnedChats: [PinnedChat], sharingSettings: SharingSettings?, context: AccountContext) -> [NicegramSettingsControllerEntry] { var entries: [NicegramSettingsControllerEntry] = [] if canOpenSecretMenu(context: context) { @@ -666,8 +717,29 @@ private func nicegramSettingsControllerEntries(presentationData: PresentationDat entries.append(.easyToggle(toggleIndex, .hideStories, l("NicegramSettings.HideStories"), NGSettings.hideStories)) toggleIndex += 1 - entries.append(.shareChannelsInfoToggle(l("NicegramSettings.ShareChannelsInfoToggle"), isShareChannelsInfoEnabled())) - entries.append(.shareChannelsInfoNote(l("NicegramSettings.ShareChannelsInfoToggle.Note"))) + if let sharingSettings { + entries.append( + .shareBotsData( + l("NicegramSettings.ShareBotsToggle"), + sharingSettings.shareBotsData + ) + ) + entries.append( + .shareChannelsData( + l("NicegramSettings.ShareChannelsToggle"), + sharingSettings.shareChannelsData + ) + ) + entries.append( + .shareStickersData( + l("NicegramSettings.ShareStickersToggle"), + sharingSettings.shareStickersData + ) + ) + entries.append( + .shareDataNote(l("NicegramSettings.ShareData.Note")) + ) + } return entries } @@ -715,8 +787,19 @@ public func nicegramSettingsController(context: AccountContext, accountsContexts } else { pinnedChatsSignal = .single([]) } + + let sharingSettingsSignal: Signal + if #available(iOS 13.0, *) { + sharingSettingsSignal = NicegramHubContainer.shared.getSharingSettingsUseCase() + .publisher() + .map { Optional($0) } + .toSignal() + .skipError() + } else { + sharingSettingsSignal = .single(nil) + } - let signal = combineLatest(context.sharedContext.presentationData, sharedDataSignal, showCallsTab, pinnedChatsSignal) |> map { presentationData, sharedData, showCalls, pinnedChats -> (ItemListControllerState, (ItemListNodeState, Any)) in + let signal = combineLatest(context.sharedContext.presentationData, sharedDataSignal, showCallsTab, pinnedChatsSignal, sharingSettingsSignal) |> map { presentationData, sharedData, showCalls, pinnedChats, sharingSettings -> (ItemListControllerState, (ItemListNodeState, Any)) in let experimentalSettings: ExperimentalUISettings = sharedData.entries[ApplicationSpecificSharedDataKeys.experimentalUISettings]?.get(ExperimentalUISettings.self) ?? ExperimentalUISettings.defaultSettings @@ -727,7 +810,7 @@ public func nicegramSettingsController(context: AccountContext, accountsContexts }) } - let entries = nicegramSettingsControllerEntries(presentationData: presentationData, experimentalSettings: experimentalSettings, showCalls: showCalls, pinnedChats: pinnedChats, context: context) + let entries = nicegramSettingsControllerEntries(presentationData: presentationData, experimentalSettings: experimentalSettings, showCalls: showCalls, pinnedChats: pinnedChats, sharingSettings: sharingSettings, context: context) let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(l("AppName")), leftNavigationButton: leftNavigationButton, rightNavigationButton: nil, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back)) let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: entries, style: .blocks) diff --git a/Package.resolved b/Package.resolved index df872f4878b..a44a6d54870 100644 --- a/Package.resolved +++ b/Package.resolved @@ -14,8 +14,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/scenee/FloatingPanel", "state" : { - "revision" : "5b33d3d5ff1f50f4a2d64158ccfe8c07b5a3e649", - "version" : "2.8.1" + "revision" : "8f2be39bf49b4d5e22bbf7bdde69d5b76d0ecd2a", + "version" : "2.8.2" } }, { @@ -36,13 +36,22 @@ "version" : "1.2.0" } }, + { + "identity" : "navigation-stack-backport", + "kind" : "remoteSourceControl", + "location" : "https://github.com/denis15yo/navigation-stack-backport.git", + "state" : { + "branch" : "main", + "revision" : "66716ce9c31198931c2275a0b69de2fdaa687e74" + } + }, { "identity" : "nicegram-assistant-ios", "kind" : "remoteSourceControl", "location" : "git@bitbucket.org:mobyrix/nicegram-assistant-ios.git", "state" : { "branch" : "develop", - "revision" : "123eb001dcc9477f9087e40ff1b0e7f012182d45" + "revision" : "9d2bc90674d3fa38fdd2e6f8f069c67d296d2bde" } }, { @@ -59,8 +68,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/SDWebImage/SDWebImage.git", "state" : { - "revision" : "b11493f76481dff17ac8f45274a6b698ba0d3af5", - "version" : "5.18.11" + "revision" : "73b9397cfbd902f606572964055464903b1d84c6", + "version" : "5.19.0" } }, { diff --git a/Telegram/SiriIntents/IntentMessages.swift b/Telegram/SiriIntents/IntentMessages.swift index 935c3a1db4d..37360288e34 100644 --- a/Telegram/SiriIntents/IntentMessages.swift +++ b/Telegram/SiriIntents/IntentMessages.swift @@ -53,7 +53,7 @@ func unreadMessages(account: Account) -> Signal<[INMessage], NoError> { } if !isMuted && hasUnread { - signals.append(account.postbox.aroundMessageHistoryViewForLocation(.peer(peerId: index.messageIndex.id.peerId, threadId: nil), anchor: .upperBound, ignoreMessagesInTimestampRange: nil, count: 10, fixedCombinedReadStates: fixedCombinedReadStates, topTaggedMessageIdNamespaces: Set(), tag: nil, appendMessagesFromTheSameGroup: false, namespaces: .not(Namespaces.Message.allScheduled), orderStatistics: .combinedLocation) + signals.append(account.postbox.aroundMessageHistoryViewForLocation(.peer(peerId: index.messageIndex.id.peerId, threadId: nil), anchor: .upperBound, ignoreMessagesInTimestampRange: nil, count: 10, fixedCombinedReadStates: fixedCombinedReadStates, topTaggedMessageIdNamespaces: Set(), tag: nil, appendMessagesFromTheSameGroup: false, namespaces: .not(Namespaces.Message.allNonRegular), orderStatistics: .combinedLocation) |> take(1) |> map { view -> [INMessage] in var messages: [INMessage] = [] diff --git a/Telegram/Telegram-iOS/ar.lproj/NiceLocalizable.strings b/Telegram/Telegram-iOS/ar.lproj/NiceLocalizable.strings index 7481159fd2f..dca106d3d3f 100644 --- a/Telegram/Telegram-iOS/ar.lproj/NiceLocalizable.strings +++ b/Telegram/Telegram-iOS/ar.lproj/NiceLocalizable.strings @@ -162,8 +162,6 @@ "NicegramSettings.Other.hideReactions" = "إخفاء ردود الفعل"; "NicegramSettings.RoundVideos.DownloadVideos" = "نزّل الفيديوهات إلى المعرض"; "Premium.OnetapTranslate.UseIgnoreLanguages" = "تجاهل اللغات"; -"NicegramSettings.ShareChannelsInfoToggle" = "شارك معلومات القناة"; -"NicegramSettings.ShareChannelsInfoToggle.Note" = "ساهم في موسوعة ويكيبيديا الأكثر شمولاً لقنوات ومجموعات تيليجرام عن طريق إرسال معلومات القناة تلقائياً والارتباط بقاعدة البيانات الخاصة بنا. نحن لا نربط ملفك الشخصي وقناتك على تيليجرام ولا نشارك بياناتك الشخصية."; "Premium.OnetapTranslate.UseIgnoreLanguages.Note" = "التطبيق يحدد لغات كل رسالة في جهازك. ويتطلب اداء عالي من الجهاز و بالتالي إستهلاك البطارية."; "Premium.rememberFolderOnExit" = "تذكر المجلد الحالي عند الخروج"; "NicegramSettings.HideStories" = "إخفاء الستوري"; @@ -221,3 +219,9 @@ "ChatContextMenu.Hide" = "إخفاء"; "ChatContextMenu.Unhide" = "إظهار"; "HiddenChatsTooltip" = "اضغط مع الاستمرار على شعار 'N' لكشف الدردشات السرية المخفية"; + +/*Data Sharing*/ +"NicegramSettings.ShareBotsToggle" = "مشاركة معلومات البوتات"; +"NicegramSettings.ShareChannelsToggle" = "مشاركة معلومات القناة"; +"NicegramSettings.ShareStickersToggle" = "مشاركة معلومات الملصقات"; +"NicegramSettings.ShareData.Note" = "ساهم في أكثر ويكيبيديا شمولية لقنوات ومجموعات تليغرام من خلال إرسال معلومات القناة والرابط تلقائيًا إلى قاعدة بياناتنا. نحن لا نربط ملفك الشخصي في تليغرام والقناة ولا نشارك بياناتك الشخصية."; diff --git a/Telegram/Telegram-iOS/de.lproj/NiceLocalizable.strings b/Telegram/Telegram-iOS/de.lproj/NiceLocalizable.strings index bca5c528f4e..07cac4b05a3 100644 --- a/Telegram/Telegram-iOS/de.lproj/NiceLocalizable.strings +++ b/Telegram/Telegram-iOS/de.lproj/NiceLocalizable.strings @@ -162,8 +162,6 @@ "NicegramSettings.Other.hideReactions" = "Reaktionen verbergen"; "NicegramSettings.RoundVideos.DownloadVideos" = "Videos in die Galerie herunterladen"; "Premium.OnetapTranslate.UseIgnoreLanguages" = "Sprachen ignorieren"; -"NicegramSettings.ShareChannelsInfoToggle" = "Kanalinformationen teilen"; -"NicegramSettings.ShareChannelsInfoToggle.Note" = "Unterstützen Sie die umfassendste Wikipedia von Telegrammkanälen und Gruppen, indem Sie automatisch Kanal-Informationen und Links zu unserer Datenbank senden. Wir verbinden Ihr Telegramm-Profil und Ihren Kanal nicht und teilen Ihre persönlichen Daten nicht."; "Premium.OnetapTranslate.UseIgnoreLanguages.Note" = "Die App erkennt die Sprache jeder Nachricht auf deinem Gerät. Das ist eine Aufgabe mit hoher Priorität, die sich auf die Akkulaufzeit auswirken kann."; "Premium.rememberFolderOnExit" = "Aktuellen Ordner beim Beenden merken"; "NicegramSettings.HideStories" = "Stories verbergen"; @@ -221,3 +219,9 @@ "ChatContextMenu.Hide" = "Verbergen"; "ChatContextMenu.Unhide" = "Einblenden"; "HiddenChatsTooltip" = "Drücken und halten Sie das 'N'-Logo, um versteckte Geheimchats zu enthüllen"; + +/*Data Sharing*/ +"NicegramSettings.ShareBotsToggle" = "Teilen Sie Bot-Informationen"; +"NicegramSettings.ShareChannelsToggle" = "Teilen Sie Kanal-Informationen"; +"NicegramSettings.ShareStickersToggle" = "Teilen Sie Sticker-Informationen"; +"NicegramSettings.ShareData.Note" = "Tragen Sie zur umfassendsten Wikipedia von Telegram-Kanälen und -Gruppen bei, indem Sie automatisch Kanalinformationen und Links zu unserer Datenbank übermitteln. Wir verbinden Ihr Telegram-Profil und Ihren Kanal nicht und teilen Ihre persönlichen Daten nicht."; diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index 934fe2e4221..f75375eb404 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -11218,6 +11218,7 @@ Sorry for the inconvenience."; "GroupBoost.AdditionalFeatures" = "Additional Features"; "GroupBoost.AdditionalFeaturesText" = "By gaining **boosts**, your group reaches higher levels and unlocks more features."; +"ChannelBoost.AdditionalFeaturesText" = "By gaining **boosts**, your channel reaches higher levels and unlocks more features."; "Stats.Boosts.Group.NoBoostersYet" = "No users currently boost your group"; "Stats.Boosts.Group.BoostersInfo" = "Your group is currently boosted by these members."; @@ -11315,3 +11316,281 @@ Sorry for the inconvenience."; "ChannelBoost.Header.Giveaway" = "giveaway"; "ChannelBoost.Header.Features" = "features"; + +"GroupBoost.Table.Group.VoiceToText" = "Voice-to-Text Conversion"; +"GroupBoost.Table.Group.EmojiPack" = "Custom Emojipack"; + +"ChannelBoost.Header.Boost" = "boost"; +"ChannelBoost.Header.Giveaway" = "giveaway"; +"ChannelBoost.Header.Features" = "features"; + +"Premium.FolderTags" = "Folder Tags"; +"Premium.FolderTagsInfo" = "Add colorful labels to chats for faster access with Telegram Premium."; +"Premium.FolderTagsStandaloneInfo" = "Add colorful labels to chats for faster access with Telegram Premium."; +"Premium.FolderTags.Proceed" = "About Telegram Premium"; + +"Premium.Business" = "Telegram Business"; +"Premium.BusinessInfo" = "Upgrade your account with business features such as location, opening hours and quick replies."; + +"Premium.Business.Location.Title" = "Location"; +"Premium.Business.Location.Text" = "Display the location of your business on your account."; + +"Premium.Business.Hours.Title" = "Opening Hours"; +"Premium.Business.Hours.Text" = "Show to your customers when you are open for business."; + +"Premium.Business.Replies.Title" = "Quick Replies"; +"Premium.Business.Replies.Text" = "Set up shortcuts with rich text and media to respond to messages faster."; + +"Premium.Business.Greetings.Title" = "Greeting Messages"; +"Premium.Business.Greetings.Text" = "Create greetings that will be automatically sent to new customers."; + +"Premium.Business.Away.Title" = "Away Messages"; +"Premium.Business.Away.Text" = "Define messages that are automatically sent when you are off."; + +"Premium.Business.Chatbots.Title" = "Chatbots"; +"Premium.Business.Chatbots.Text" = "Add any third party chatbots that will process customer interactions."; + +"Business.Title" = "Telegram Business"; +"Business.Description" = "Turn your account into a **business page** with these additional features."; +"Business.SubscribedDescription" = "You have now unlocked these additional business features."; + +"Business.Location" = "Location"; +"Business.OpeningHours" = "Opening Hours"; +"Business.QuickReplies" = "Quick Replies"; +"Business.GreetingMessages" = "Greeting Messages"; +"Business.AwayMessages" = "Away Messages"; +"Business.Chatbots" = "Chatbots"; + +"Business.LocationInfo" = "Display the location of your business on your account."; +"Business.OpeningHoursInfo" = "Show to your customers when you are open for business."; +"Business.QuickRepliesInfo" = "Set up shortcuts with rich text and media to respond to messages faster."; +"Business.GreetingMessagesInfo" = "Create greetings that will be automatically sent to new customers."; +"Business.AwayMessagesInfo" = "Define messages that are automatically sent when you are off."; +"Business.ChatbotsInfo" = "Add any third-party chatbots that will process customer interactions."; + +"Business.MoreFeaturesTitle" = "MORE BUSINESS FEATURES"; +"Business.MoreFeaturesInfo" = "Check this section later for new business features."; + +"Business.SetEmojiStatus" = "Set Emoji Status"; +"Business.SetEmojiStatusInfo" = "Display the current status of your business next to your name."; + +"Business.TagYourChats" = "Tag Your Chats"; +"Business.TagYourChatsInfo" = "Add colorful labels to chats for faster access in chat list."; + +"Business.AddPost" = "Add a Post"; +"Business.AddPostInfo" = "Publish photos and videos of your goods or services on your page."; + +"StoryList.SavedEmptyPosts.Title" = "No posts yet..."; +"StoryList.SavedEmptyPosts.Text" = "Publish photos and videos to display on your profile page."; +"StoryList.SavedAddAction" = "Add a Post"; + +"PeerInfo.Channel.Boost" = "Boost Channel"; + +"ChannelBoost.Title" = "Boost Channel"; +"ChannelBoost.Info" = "Subscribers of your channel can **boost** it so that it **levels up** and gets **exclusive features**."; + +"Conversation.BoostToUnrestrictText" = "Boost this group to send messages"; + +"Settings.Business" = "Telegram Business"; + +"Story.Editor.DiscardText" = "If you go back now, you will lose any changes you made."; + +"Premium.Business.Description" = "Turn your account to a business page with these additional features."; + +"Attachment.Reply" = "Reply"; + +"ChatList.ItemMoreMessagesFormat_1" = "+1 MORE"; +"ChatList.ItemMoreMessagesFormat_any" = "+%d MORE"; +"ChatList.ItemMenuEdit" = "Edit"; +"ChatList.ItemMenuDelete" = "Delete"; +"ChatList.PeerTypeNonContact" = "non-contact"; + +"ChatListFilter.TagLabelNoTag" = "NO TAG"; +"ChatListFilter.TagLabelPremiumExpired" = "PREMIUM EXPIRED"; +"ChatListFilter.TagSectionTitle" = "FOLDER COLOR"; +"ChatListFilter.TagSectionFooter" = "This color will be used for the folder's tag in the chat list"; +"ChatListFilter.TagPremiumRequiredTooltipText" = "Subscribe to **Telegram Premium** to select folder color."; +"ChatListFilterList.ShowTags" = "Show Folder Tags"; +"ChatListFilterList.ShowTagsFooter" = "Display folder names for each chat in the chat list."; + +"PeerInfo.BusinessHours.DayOpen24h" = "open 24 hours"; +"PeerInfo.BusinessHours.DayClosed" = "closed"; +"PeerInfo.BusinessHours.StatusOpen" = "Open"; +"PeerInfo.BusinessHours.StatusClosed" = "Closed"; +"PeerInfo.BusinessHours.StatusOpensInMinutes_1" = "Opens in one minute"; +"PeerInfo.BusinessHours.StatusOpensInMinutes_any" = "Opens in %d minutes"; +"PeerInfo.BusinessHours.StatusOpensInHours_1" = "Opens in one hour"; +"PeerInfo.BusinessHours.StatusOpensInHours_any" = "Opens in %d hours"; +"PeerInfo.BusinessHours.StatusOpensOnDate" = "Opens %@"; +"PeerInfo.BusinessHours.StatusOpensTodayAt" = "Opens today at %@"; +"PeerInfo.BusinessHours.StatusOpensTomorrowAt" = "Opens tomorrow at %@"; +"PeerInfo.BusinessHours.TimezoneSwitchMy" = "my time"; +"PeerInfo.BusinessHours.TimezoneSwitchBusiness" = "local time"; +"PeerInfo.BusinessHours.Label" = "business hours"; +"PeerInfo.Location.Label" = "location"; + +"QuickReplies.EmptyState.Title" = "No Quick Replies"; +"QuickReplies.EmptyState.Text" = "Set up shortcuts with rich text and media to respond to messages faster."; +"QuickReplies.EmptyState.AddButton" = "Add Quick Reply"; + +"Chat.CommandList.EditQuickReplies" = "Edit Quick Replies"; + +"Conversation.EditingQuickReplyPanelTitle" = "Edit Quick Reply"; + +"Chat.Placeholder.QuickReply" = "Add quick reply.."; +"Chat.Placeholder.GreetingMessage" = "Add greeting message..."; +"Chat.Placeholder.AwayMessage" = "Add away message..."; + +"Chat.QuickReplyMessageLimitReachedText_1" = "Limit of %d message reached"; +"Chat.QuickReplyMessageLimitReachedText_any" = "Limit of %d messages reached"; + +"Chat.EmptyState.QuickReply.Title" = "New Quick Reply"; +"Chat.EmptyState.QuickReply.Text1" = "· Enter a message below that will be sent in chats when you type \"**/%@\"**."; +"Chat.EmptyState.QuickReply.Text2" = "· You can access Quick Replies in any chat by typing \"/\" or using the Attachment menu."; +"EmptyState.GreetingMessage.Title" = "New Greeting Message"; +"EmptyState.GreetingMessage.Text" = "Create greetings that will be automatically sent to new customers"; +"EmptyState.AwayMessage.Title" = "New Away Message"; +"EmptyState.AwayMessage.Text" = "Add messages that are automatically sent when you are off."; + +"QuickReply.Title" = "Quick Replies"; +"QuickReply.SelectedTitle_1" = "%d Selected"; +"QuickReply.SelectedTitle_any" = "%d Selected"; + +"QuickReply.ShortcutPlaceholder" = "Shortcut"; +"QuickReply.CreateShortcutTitle" = "New Quick Reply"; +"QuickReply.CreateShortcutText" = "Add a shortcut for your quick reply."; +"QuickReply.EditShortcutTitle" = "Edit Shortcut"; +"QuickReply.EditShortcutText" = "Add a new name for your shortcut."; +"QuickReply.ShortcutExistsInlineError" = "Shortcut with that name already exists"; + +"QuickReply.ChatRemoveGeneric.Title" = "Remove Shortcut"; +"QuickReply.ChatRemoveGeneric.Text" = "You didn't create a quick reply message. Exiting will remove the shortcut."; +"QuickReply.ChatRemoveGreetingMessage.Title" = "Remove Greeting Message"; +"QuickReply.ChatRemoveGreetingMessage.Text" = "You didn't create a greeting message. Exiting will remove it."; +"QuickReply.ChatRemoveAwayMessage.Title" = "Remove Away Message"; +"QuickReply.ChatRemoveAwayMessage.Text" = "You didn't create an away message. Exiting will remove it."; +"QuickReply.ChatRemoveGeneric.DeleteAction" = "Delete"; +"QuickReply.TitleGreetingMessage" = "Greeting Message"; +"QuickReply.TitleAwayMessage" = "Away Message"; + +"QuickReply.InlineCreateAction" = "New Quick Reply"; +"QuickReply.DeleteConfirmationSingle" = "Delete Quick Reply"; +"QuickReply.DeleteConfirmationMultiple" = "Delete Quick Replies"; +"QuickReply.DeleteAction_1" = "Delete 1 Quick Reply"; +"QuickReply.DeleteAction_any" = "Delete %d Quick Replies"; + +"TimeZoneSelection.Title" = "Time Zone"; + +"BusinessMessageSetup.TitleGreetingMessage" = "Greeting Message"; +"BusinessMessageSetup.TextGreetingMessage" = "Greet customers when they message you the first time or after a period of no activity."; +"BusinessMessageSetup.TitleAwayMessage" = "Away Message"; +"BusinessMessageSetup.TextAwayMessage" = "Automatically reply with a message when you are away."; +"BusinessMessageSetup.ToggleGreetingMessage" = "Send Greeting Message"; +"BusinessMessageSetup.ToggleAwayMessage" = "Send Away Message"; +"BusinessMessageSetup.CreateGreetingMessage" = "Create a Greeting Message"; +"BusinessMessageSetup.CreateAwayMessage" = "Create an Away Message"; +"BusinessMessageSetup.GreetingMessageSectionHeader" = "GREETING MESSAGE"; +"BusinessMessageSetup.AwayMessageSectionHeader" = "AWAY MESSAGE"; + +"BusinessMessageSetup.ScheduleSectionHeader" = "SCHEDULE"; +"BusinessMessageSetup.ScheduleAlways" = "Always Send"; +"BusinessMessageSetup.ScheduleOutsideBusinessHours" = "Outside of Business Hours"; +"BusinessMessageSetup.ScheduleCustom" = "Custom Schedule"; +"BusinessMessageSetup.ScheduleStartTime" = "Start Time"; +"BusinessMessageSetup.ScheduleEndTime" = "End Time"; +"BusinessMessageSetup.ScheduleTimePlaceholder" = "Set"; + +"BusinessMessageSetup.SendWhenOffline" = "Only if Offline"; +"BusinessMessageSetup.SendWhenOfflineFooter" = "Don't send the away message if you've recently been online."; + +"BusinessMessageSetup.ErrorNoRecipients.Text" = "No recipients selected. Reset?"; +"BusinessMessageSetup.ErrorNoRecipients.ResetAction" = "Reset"; +"BusinessMessageSetup.ErrorScheduleEndTimeBeforeStartTime.Text" = "Custom schedule end time must be larger than start time."; +"BusinessMessageSetup.ErrorScheduleTimeMissing.Text" = "Custom schedule time is missing."; +"BusinessMessageSetup.ErrorScheduleStartTimeMissing.Text" = "Custom schedule start time is missing."; +"BusinessMessageSetup.ErrorScheduleEndTimeMissing.Text" = "Custom schedule end time is missing."; +"BusinessMessageSetup.ErrorScheduleTime.ResetAction" = "Reset"; + +"BusinessMessageSetup.RecipientsSectionHeader" = "RECIPIENTS"; +"BusinessMessageSetup.RecipientsOptionAllExcept" = "All 1-to-1 Chats Except..."; +"BusinessMessageSetup.RecipientsOptionOnly" = "Only Selected Chats"; + +"BusinessMessageSetup.Recipients.AddExclude" = "Exclude Chats..."; +"BusinessMessageSetup.Recipients.AddInclude" = "Include Chats..."; + +"BusinessMessageSetup.Recipients.CategoryExistingChats" = "Existing Chats"; +"BusinessMessageSetup.Recipients.CategoryNewChats" = "New Chats"; +"BusinessMessageSetup.Recipients.CategoryContacts" = "Contacts"; +"BusinessMessageSetup.Recipients.CategoryNonContacts" = "Non-Contacts"; +"BusinessMessageSetup.Recipients.IncludeSearchTitle" = "Include Chats"; +"BusinessMessageSetup.Recipients.ExcludeSearchTitle" = "Exclude Chats"; + +"BusinessMessageSetup.Recipients.IncludedSectionHeader" = "INCLUDED CHATS"; +"BusinessMessageSetup.Recipients.ExcludedSectionHeader" = "EXCLUDED CHATS"; +"BusinessMessageSetup.Recipients.GreetingMessageFooter" = "Choose chats or entire chat categories for sending a greeting message."; +"BusinessMessageSetup.Recipients.AwayMessageFooter" = "Choose chats or entire chat categories for sending an away message."; + +"BusinessMessageSetup.InactivitySectionHeader" = "PERIOD OF NO ACTIVITY"; +"BusinessMessageSetup.InactivitySectionFooter" = "Choose how many days should pass after your last interaction with a recipient to send them the greeting in response to their message."; + +"BusinessHoursSetup.Title" = "Business Hours"; +"BusinessHoursSetup.Text" = "Turn this on to show your opening hours schedule to your customers."; + +"BusinessHoursSetup.MainToggle" = "Show Business Hours"; + +"BusinessHoursSetup.DaySwitch" = "Open On This Day"; +"BusinessHoursSetup.DayIntervalStart" = "Opening time"; +"BusinessHoursSetup.DayIntervalEnd" = "Closing time"; +"BusinessHoursSetup.DayIntervalRemove" = "Remove"; +"BusinessHoursSetup.AddSectionFooter" = "Specify your working hours during the day."; +"BusinessHoursSetup.AddAction" = "Add a Set of Hours"; + +"BusinessHoursSetup.ErrorIntersectingHours.Text" = "Business hours are intersecting. Reset?"; +"BusinessHoursSetup.ErrorIntersectingHours.ResetAction" = "Reset"; + +"BusinessHoursSetup.ErrorIntersectingDays.Text" = "Business hours are intersecting. Reset?"; +"BusinessHoursSetup.ErrorIntersectingDays.ResetAction" = "Reset"; + +"BusinessHoursSetup.DayOpen24h" = "Open 24 Hours"; +"BusinessHoursSetup.DayClosed" = "Closed"; +"BusinessHoursSetup.DaysSectionTitle" = "BUSINESS HOURS"; + +"BusinessHoursSetup.TimeZone" = "Time Zone"; + +"BusinessLocationSetup.Title" = "Location"; +"BusinessLocationSetup.Text" = "Display the location of your business on your account."; + +"BusinessLocationSetup.AddressPlaceholder" = "Enter Address"; +"BusinessLocationSetup.SetLocationOnMap" = "Set Location on Map"; +"BusinessLocationSetup.DeleteLocation" = "Delete Location"; + +"BusinessLocationSetup.ErrorAddressEmpty.Text" = "Address can't be empty."; +"BusinessLocationSetup.ErrorAddressEmpty.ResetAction" = "Delete"; + +"BusinessLocationSetup.AlertUnsavedChanges.Text" = "You have unsaved changes."; +"BusinessLocationSetup.AlertUnsavedChanges.ResetAction" = "Revert"; + +"ChatbotSetup.Title" = "Chatbots"; +"ChatbotSetup.Text" = "Add a bot to your account to help you automatically process and respond to the messages you receive. [Learn More >]()"; +"ChatbotSetup.TextLink" = "https://telegram.org"; + +"ChatbotSetup.BotSearchPlaceholder" = "Bot Username"; +"ChatbotSetup.BotSectionFooter" = "Enter the username or URL of the Telegram bot that you want to automatically process your chats."; + +"ChatbotSetup.RecipientsSectionHeader" = "CHATS ACCESSIBLE FOR THE BOT"; + +"ChatbotSetup.Recipients.ExcludedSectionFooter" = "Select chats or entire chat categories which the bot **WILL NOT** have access to."; +"ChatbotSetup.Recipients.IncludedSectionFooter" = "Select chats or entire chat categories which the bot **WILL** have access to."; + +"ChatbotSetup.PermissionsSectionHeader" = "BOT PERMISSIONS"; +"ChatbotSetup.PermissionsSectionFooter" = "The bot will be able to view all new incoming messages, but not the messages that had been sent before you added the bot."; +"ChatbotSetup.Permission.ReplyToMessages" = "Reply to Messages"; + +"ChatbotSetup.BotAddAction" = "ADD"; +"ChatbotSetup.BotNotFoundStatus" = "Chatbot not found"; + +"Chat.QuickReply.ServiceHeader1" = "To edit or delete your quick reply, tap an hold on it."; +"Chat.QuickReply.ServiceHeader2" = "To use this quick reply in a chat, type / and select the shortcut from the list."; + +"Chat.QuickReplyMediaMessageLimitReachedText_1" = "There can be at most %d message in this chat."; +"Chat.QuickReplyMediaMessageLimitReachedText_any" = "There can be at most %d messages in this chat."; diff --git a/Telegram/Telegram-iOS/en.lproj/NiceLocalizable.strings b/Telegram/Telegram-iOS/en.lproj/NiceLocalizable.strings index 771b089e502..e74c084ce00 100644 --- a/Telegram/Telegram-iOS/en.lproj/NiceLocalizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/NiceLocalizable.strings @@ -162,8 +162,6 @@ "NicegramSettings.Other.hideReactions" = "Hide Reactions"; "NicegramSettings.RoundVideos.DownloadVideos" = "Download videos to Gallery"; "Premium.OnetapTranslate.UseIgnoreLanguages" = "Ignore Languages"; -"NicegramSettings.ShareChannelsInfoToggle" = "Share channel information"; -"NicegramSettings.ShareChannelsInfoToggle.Note" = "Contribute to the most comprehensive wikipedia of Telegram channels and groups by automatically submitting channel information and link to our database. We do not connect your telegram profile and channel and do not share your personal data."; "Premium.OnetapTranslate.UseIgnoreLanguages.Note" = "App detects language of each message on your device. It's a high-performance task which can affect your battery life."; "Premium.rememberFolderOnExit" = "Remember current folder on exit"; "NicegramSettings.HideStories" = "Hide Stories"; @@ -221,3 +219,9 @@ "ChatContextMenu.Hide" = "Hide"; "ChatContextMenu.Unhide" = "Unhide"; "HiddenChatsTooltip" = "Press and hold the 'N' logo to reveal hidden secret chats"; + +/*Data Sharing*/ +"NicegramSettings.ShareBotsToggle" = "Share bots information"; +"NicegramSettings.ShareChannelsToggle" = "Share channel information"; +"NicegramSettings.ShareStickersToggle" = "Share stickers information"; +"NicegramSettings.ShareData.Note" = "Contribute to the most comprehensive wikipedia of Telegram channels and groups by automatically submitting channel information and link to our database. We do not connect your telegram profile and channel and do not share your personal data."; diff --git a/Telegram/Telegram-iOS/es.lproj/NiceLocalizable.strings b/Telegram/Telegram-iOS/es.lproj/NiceLocalizable.strings index a7e44c19dc1..cbf6e73784d 100644 --- a/Telegram/Telegram-iOS/es.lproj/NiceLocalizable.strings +++ b/Telegram/Telegram-iOS/es.lproj/NiceLocalizable.strings @@ -162,8 +162,6 @@ "NicegramSettings.Other.hideReactions" = "Ocultar reacciones"; "NicegramSettings.RoundVideos.DownloadVideos" = "Descargar vídeos en la galería"; "Premium.OnetapTranslate.UseIgnoreLanguages" = "Ignorar idiomas"; -"NicegramSettings.ShareChannelsInfoToggle" = "Comparta información del canal"; -"NicegramSettings.ShareChannelsInfoToggle.Note" = "Contribuya a la Wikipedia más completa de canales y grupos de Telegram enviando automáticamente la información del canal y el enlace a nuestra base de datos. No conectamos su perfil y canal de Telegram y no compartimos sus datos personales."; "Premium.OnetapTranslate.UseIgnoreLanguages.Note" = "La aplicación detecta el idioma de cada mensaje en su aparato. Es una tarea de alta predeterminación que puede afectar la duración de su batería."; "Premium.rememberFolderOnExit" = "Recordar la carpeta actual a la salida"; "NicegramSettings.HideStories" = "Ocultar Historias"; @@ -221,3 +219,9 @@ "ChatContextMenu.Hide" = "Esconder"; "ChatContextMenu.Unhide" = "Mostrar"; "HiddenChatsTooltip" = "Mantén presionado el logo 'N' para revelar chats secretos ocultos"; + +/*Data Sharing*/ +"NicegramSettings.ShareBotsToggle" = "Compartir información de bots"; +"NicegramSettings.ShareChannelsToggle" = "Compartir información de canales"; +"NicegramSettings.ShareStickersToggle" = "Compartir información de stickers"; +"NicegramSettings.ShareData.Note" = "Contribuye a la wikipedia más completa de canales y grupos de Telegram enviando automáticamente información y enlaces de canales a nuestra base de datos. No conectamos tu perfil de telegram con el canal y no compartimos tus datos personales."; diff --git a/Telegram/Telegram-iOS/fr.lproj/NiceLocalizable.strings b/Telegram/Telegram-iOS/fr.lproj/NiceLocalizable.strings index b247a1e6232..cc9d348f72d 100644 --- a/Telegram/Telegram-iOS/fr.lproj/NiceLocalizable.strings +++ b/Telegram/Telegram-iOS/fr.lproj/NiceLocalizable.strings @@ -162,8 +162,6 @@ "NicegramSettings.Other.hideReactions" = "Masquer les réactions"; "NicegramSettings.RoundVideos.DownloadVideos" = "Télécharger des vidéos dans la galerie"; "Premium.OnetapTranslate.UseIgnoreLanguages" = "Ignorer les langues"; -"NicegramSettings.ShareChannelsInfoToggle" = "Partager les informations du canal"; -"NicegramSettings.ShareChannelsInfoToggle.Note" = "Contribuez au Wikipédia le plus complet de chaînes et de groupes Telegram en soumettant automatiquement des informations sur les chaînes et un lien vers notre base de données. Nous ne connectons pas votre profil de télégramme et votre canal et ne partageons pas vos données personnelles."; "Premium.OnetapTranslate.UseIgnoreLanguages.Note" = "L'application détecte la langue de chaque message sur votre appareil. C'est une tâche de haute performance qui peut affecter l'autonomie de votre batterie."; "Premium.rememberFolderOnExit" = "Mémoriser le dossier actuel en quittant"; "NicegramSettings.HideStories" = "Cacher les histoires"; @@ -221,3 +219,9 @@ "ChatContextMenu.Hide" = "Masquer"; "ChatContextMenu.Unhide" = "Révéler"; "HiddenChatsTooltip" = "Appuyez et maintenez le logo 'N' pour révéler les chats secrets cachés"; + +/*Data Sharing*/ +"NicegramSettings.ShareBotsToggle" = "Partager les informations des bots"; +"NicegramSettings.ShareChannelsToggle" = "Partager les informations du canal"; +"NicegramSettings.ShareStickersToggle" = "Partager des informations sur les stickers"; +"NicegramSettings.ShareData.Note" = "Contribuez au Wikipédia le plus complet de chaînes et de groupes Telegram en soumettant automatiquement des informations sur les chaînes et un lien vers notre base de données. Nous ne connectons pas votre profil de télégramme et votre canal et ne partageons pas vos données personnelles."; diff --git a/Telegram/Telegram-iOS/it.lproj/NiceLocalizable.strings b/Telegram/Telegram-iOS/it.lproj/NiceLocalizable.strings index 9e172601316..ebf6aef7acc 100644 --- a/Telegram/Telegram-iOS/it.lproj/NiceLocalizable.strings +++ b/Telegram/Telegram-iOS/it.lproj/NiceLocalizable.strings @@ -162,8 +162,6 @@ "NicegramSettings.Other.hideReactions" = "Nascondi Reazioni"; "NicegramSettings.RoundVideos.DownloadVideos" = "Scarica i video nella Galleria"; "Premium.OnetapTranslate.UseIgnoreLanguages" = "Ignora Lingue"; -"NicegramSettings.ShareChannelsInfoToggle" = "Condividi informazioni sul canale"; -"NicegramSettings.ShareChannelsInfoToggle.Note" = "Contribuisci alla wikipedia più completa di canali e gruppi di Telegram inviando automaticamente le informazioni e i link dei canali al nostro database. Non colleghiamo il tuo profilo e canale Telegram e non condividiamo i tuoi dati personali."; "Premium.OnetapTranslate.UseIgnoreLanguages.Note" = "L'app rileva la lingua di ogni messaggio sul tuo dispositivo. È un'attività che consuma tante risorse e che può influire sulla durata della batteria."; "Premium.rememberFolderOnExit" = "Ricorda la cartella corrente all'uscita"; "NicegramSettings.HideStories" = "Nascondi Storie"; @@ -221,3 +219,9 @@ "ChatContextMenu.Hide" = "Nascondi"; "ChatContextMenu.Unhide" = "Rivelare"; "HiddenChatsTooltip" = "Premi e tieni premuto il logo 'N' per rivelare chat segrete nascoste"; + +/*Data Sharing*/ +"NicegramSettings.ShareBotsToggle" = "Condividi informazioni sui bot"; +"NicegramSettings.ShareChannelsToggle" = "Condividi informazioni sul canale"; +"NicegramSettings.ShareStickersToggle" = "Condividi informazioni sugli sticker"; +"NicegramSettings.ShareData.Note" = "Contribuisci all'enciclopedia più completa di canali e gruppi Telegram inviando automaticamente informazioni e link sul canale al nostro database. Non collegiamo il tuo profilo Telegram e il canale e non condividiamo i tuoi dati personali."; diff --git a/Telegram/Telegram-iOS/km.lproj/NiceLocalizable.strings b/Telegram/Telegram-iOS/km.lproj/NiceLocalizable.strings index 9afc7c4d916..ed58fa4dda2 100644 --- a/Telegram/Telegram-iOS/km.lproj/NiceLocalizable.strings +++ b/Telegram/Telegram-iOS/km.lproj/NiceLocalizable.strings @@ -44,7 +44,7 @@ "Common.Later" = "ពេលក្រោយ"; "Common.RestartRequired" = "តម្រូវ​ឲ្យ​ចាប់ផ្ដើម​ឡើង​វិញ!"; "NiceFeatures.Tabs.ShowNames" = "បង្ហាញឈ្មោះថេប"; -"Chat.ForwardAsCopy" = "បញ្ជូនបន្តជាចម្លង"; +"Chat.ForwardAsCopy" = "បញ្ជូនបន្តជាច្បាប់ចម្លង"; /*Folder*/ "Folder.DefaultName" = "ថត"; @@ -87,3 +87,13 @@ "IAP.Premium.Title" = "ព្រីមៀរ"; "IAP.Premium.Subtitle" = "លក្ខណៈពិសេសប្លែក ដែលអ្នកមិនអាចបដិសេធ!"; "IAP.Premium.Features" = "អ្នកបកប្រែសាររហ័ស\n\nចងចាំថតដែលបានជ្រើសនៅពេលចេញ"; + +/*Registration Date*/ +"NiceFeatures.RegDate.OlderThan" = "ចាស់ជាង"; +"NiceFeatures.RegDate.Approximately" = "ប្រហែល"; +"NiceFeatures.RegDate.NewerThan" = "ថ្មោងថ្មីជាង"; +"NicegramOnboarding.1.Title" = "អតិថូបករណ៍ №1 សម្រាប់កម្មវិធីសារតេឡេក្រាម"; +"NicegramOnboarding.1.Desc" = "អ្នកប្រើប្រាស់ Nicegram ច្រើនជាង 2 លាននាក់ និងអាចអាក់សេសអតិថូបករណ៍តេឡេក្រាមដ៏មានឥទ្ធិពល និងសុវត្ថិភាពបំផុតសម្រាប់អាជីវកម្ម។"; +"NicegramOnboarding.2.Title" = "បទពិសោធន៍ផ្ញើសារកម្រិតខ្ពស់"; +"NicegramOnboarding.3.Title" = "ប្រូហ្វាលអត្ថជនបន្ថែម"; +"NicegramOnboarding.4.Title" = "ផ្នែកបន្ថែមអាជីវកម្មតែមួយគត់"; diff --git a/Telegram/Telegram-iOS/ko.lproj/NiceLocalizable.strings b/Telegram/Telegram-iOS/ko.lproj/NiceLocalizable.strings index ffbe637fe54..f93eb79f46a 100644 --- a/Telegram/Telegram-iOS/ko.lproj/NiceLocalizable.strings +++ b/Telegram/Telegram-iOS/ko.lproj/NiceLocalizable.strings @@ -162,8 +162,6 @@ "NicegramSettings.Other.hideReactions" = "반응 숨기기"; "NicegramSettings.RoundVideos.DownloadVideos" = "사진앨범으로 동영상 다운로드"; "Premium.OnetapTranslate.UseIgnoreLanguages" = "언어 무시하기"; -"NicegramSettings.ShareChannelsInfoToggle" = "채널 정보 공유"; -"NicegramSettings.ShareChannelsInfoToggle.Note" = "저희 데이터 베이스에 자동으로 채널 정보와 링크를 제출함으로써 텔레그램(Telegram) 채널 및 그룹의 가장 포괄적인 위키피디아에 기여하세요. 저희는 귀하의 텔레그램 프로필과 채널을 연결하지 않으며 귀하의 개인 정보도 공유하지 않습니다."; "Premium.OnetapTranslate.UseIgnoreLanguages.Note" = "앱은 기기에서 각 메시지의 언어를 감지합니다. 배터리 수명에 영향을 줄 수 있는 고성능 작업입니다."; "Premium.rememberFolderOnExit" = "종료시 현재 폴더 기억하기"; "NicegramSettings.HideStories" = "스토리 숨기기"; @@ -221,3 +219,9 @@ "ChatContextMenu.Hide" = "숨기기"; "ChatContextMenu.Unhide" = "숨김 해제"; "HiddenChatsTooltip" = "숨겨진 비밀 채팅을 보려면 'N' 로고를 누르고 있으세요"; + +/*Data Sharing*/ +"NicegramSettings.ShareBotsToggle" = "봇 정보 공유하기"; +"NicegramSettings.ShareChannelsToggle" = "채널 정보 공유하기"; +"NicegramSettings.ShareStickersToggle" = "스티커 정보 공유하기"; +"NicegramSettings.ShareData.Note" = "자동으로 채널 정보와 링크를 제출함으로써 Telegram 채널과 그룹들의 가장 포괄적인 위키백과에 기여하세요. 우리는 여러분의 Telegram 프로필과 채널을 연결하거나 개인 데이터를 공유하지 않습니다."; diff --git a/Telegram/Telegram-iOS/pl.lproj/NiceLocalizable.strings b/Telegram/Telegram-iOS/pl.lproj/NiceLocalizable.strings index 1e5f394ed54..da62bd6b7bf 100644 --- a/Telegram/Telegram-iOS/pl.lproj/NiceLocalizable.strings +++ b/Telegram/Telegram-iOS/pl.lproj/NiceLocalizable.strings @@ -162,8 +162,6 @@ "NicegramSettings.Other.hideReactions" = "Ukryj reakcje"; "NicegramSettings.RoundVideos.DownloadVideos" = "Pobierz filmy do galerii"; "Premium.OnetapTranslate.UseIgnoreLanguages" = "Ignorowane języki"; -"NicegramSettings.ShareChannelsInfoToggle" = "Udostępnij informacje o kanale"; -"NicegramSettings.ShareChannelsInfoToggle.Note" = "Przyczyniaj się do najbardziej wszechstronnej bazy danych kanałów i grup Telegram, automatycznie przesyłając informacje o kanałach i linki do naszej bazy danych. Nie łączymy twojego profilu Telegram z kanałem i nie udostępniamy twoich danych osobowych."; "Premium.OnetapTranslate.UseIgnoreLanguages.Note" = "Aplikacja wykrywa język każdej wiadomości na urządzeniu. Jest to zadanie o wysokim stopniu zaawansowania, które może mieć wpływ na żywotność baterii."; "Premium.rememberFolderOnExit" = "Zapamiętaj bieżący folder przy wyjściu"; "NicegramSettings.HideStories" = "Ukryj Historie"; @@ -221,3 +219,9 @@ "ChatContextMenu.Hide" = "Ukryj"; "ChatContextMenu.Unhide" = "Odkryj"; "HiddenChatsTooltip" = "Naciśnij i przytrzymaj logo 'N', aby odkryć ukryte sekretne czaty"; + +/*Data Sharing*/ +"NicegramSettings.ShareBotsToggle" = "Udostępnij informacje o botach"; +"NicegramSettings.ShareChannelsToggle" = "Udostępnij informacje o kanale"; +"NicegramSettings.ShareStickersToggle" = "Udostępnij informacje o naklejkach"; +"NicegramSettings.ShareData.Note" = "Przyczyn się do najbardziej kompleksowej wikipedii kanałów i grup Telegrama, automatycznie przesyłając informacje o kanale i link do naszej bazy danych. Nie łączymy Twojego profilu na Telegramie z kanałem i nie udostępniamy Twoich danych osobowych."; diff --git a/Telegram/Telegram-iOS/pt.lproj/NiceLocalizable.strings b/Telegram/Telegram-iOS/pt.lproj/NiceLocalizable.strings index d527d5285b2..7ea9e485fef 100644 --- a/Telegram/Telegram-iOS/pt.lproj/NiceLocalizable.strings +++ b/Telegram/Telegram-iOS/pt.lproj/NiceLocalizable.strings @@ -162,8 +162,6 @@ "NicegramSettings.Other.hideReactions" = "Ocultar Reações"; "NicegramSettings.RoundVideos.DownloadVideos" = "Baixar vídeos para a Galeria"; "Premium.OnetapTranslate.UseIgnoreLanguages" = "Ignorar idiomas"; -"NicegramSettings.ShareChannelsInfoToggle" = "Compartilhar informações do canal"; -"NicegramSettings.ShareChannelsInfoToggle.Note" = "Contribua para a wikipedia mais abrangente de canais e grupos do Telegram enviando automaticamente informações do canal e link para o nosso banco de dados. Não conectamos o seu perfil e canal do Telegram e não compartilhamos os seus dados pessoais."; "Premium.OnetapTranslate.UseIgnoreLanguages.Note" = "O aplicativo detecta o idioma de cada mensagem em seu dispositivo. É uma tarefa de alto desempenho que pode afetar a vida útil da bateria."; "Premium.rememberFolderOnExit" = "Lembrar pasta atual antes de sair"; "NicegramSettings.HideStories" = "Ocultar Stories"; @@ -221,3 +219,9 @@ "ChatContextMenu.Hide" = "Esconder"; "ChatContextMenu.Unhide" = "Revelar"; "HiddenChatsTooltip" = "Pressione e segure o logo 'N' para revelar chats secretos ocultos"; + +/*Data Sharing*/ +"NicegramSettings.ShareBotsToggle" = "Partilhar informação de bots"; +"NicegramSettings.ShareChannelsToggle" = "Partilhar informação de canais"; +"NicegramSettings.ShareStickersToggle" = "Partilhar informação de stickers"; +"NicegramSettings.ShareData.Note" = "Contribua para a wikipedia mais abrangente de canais e grupos do Telegram, submetendo automaticamente informações e links de canais à nossa base de dados. Não associamos o seu perfil do Telegram ao canal e não partilhamos os seus dados pessoais."; diff --git a/Telegram/Telegram-iOS/ru.lproj/NiceLocalizable.strings b/Telegram/Telegram-iOS/ru.lproj/NiceLocalizable.strings index d0bf6285af4..aa1a46a937b 100644 --- a/Telegram/Telegram-iOS/ru.lproj/NiceLocalizable.strings +++ b/Telegram/Telegram-iOS/ru.lproj/NiceLocalizable.strings @@ -162,8 +162,6 @@ "NicegramSettings.Other.hideReactions" = "Скрыть реакции"; "NicegramSettings.RoundVideos.DownloadVideos" = "Скачивать видео в галерею"; "Premium.OnetapTranslate.UseIgnoreLanguages" = "Игнорировать языки"; -"NicegramSettings.ShareChannelsInfoToggle" = "Поделиться информацией канала"; -"NicegramSettings.ShareChannelsInfoToggle.Note" = "Внесите свой вклад в самую полную Википедию каналов и групп Telegram, автоматически отправляя информацию о каналах и ссылки в нашу базу данных. Мы не связываем ваш телеграм-профиль и канал, а также никому не передаем ваши личные данные."; "Premium.OnetapTranslate.UseIgnoreLanguages.Note" = "Приложение определяет язык для каждого сообщения на Вашем устройстве. Поскольку это высокопроизводительная задача, время работы аккумулятора может уменьшиться."; "Premium.rememberFolderOnExit" = "Запомнить текущую папку при выходе"; "NicegramSettings.HideStories" = "Скрыть истории"; @@ -221,3 +219,9 @@ "ChatContextMenu.Hide" = "Скрыть"; "ChatContextMenu.Unhide" = "Показать"; "HiddenChatsTooltip" = "Нажмите и удерживайте логотип 'N', чтобы показать скрытые секретные чаты"; + +/*Data Sharing*/ +"NicegramSettings.ShareBotsToggle" = "Поделиться информацией о ботах"; +"NicegramSettings.ShareChannelsToggle" = "Поделиться информацией о канале"; +"NicegramSettings.ShareStickersToggle" = "Поделиться информацией о стикерах"; +"NicegramSettings.ShareData.Note" = "Внесите свой вклад в самую полную википедию каналов и групп Telegram, автоматически отправляя информацию о канале и ссылку в нашу базу данных. Мы не связываем ваш профиль Telegram и канал и не делимся вашими личными данными."; diff --git a/Telegram/Telegram-iOS/tr.lproj/NiceLocalizable.strings b/Telegram/Telegram-iOS/tr.lproj/NiceLocalizable.strings index fff7b6c8eec..f8d40272807 100644 --- a/Telegram/Telegram-iOS/tr.lproj/NiceLocalizable.strings +++ b/Telegram/Telegram-iOS/tr.lproj/NiceLocalizable.strings @@ -162,8 +162,6 @@ "NicegramSettings.Other.hideReactions" = "Tepkileri Gizle"; "NicegramSettings.RoundVideos.DownloadVideos" = "Videoları galeriye indir"; "Premium.OnetapTranslate.UseIgnoreLanguages" = "Dilleri görmezden gel"; -"NicegramSettings.ShareChannelsInfoToggle" = "Kanal bilgilerini paylaş"; -"NicegramSettings.ShareChannelsInfoToggle.Note" = "Kanal bilgilerini ve bağlantısını veri tabanımıza göndererek en kapsamlı Telegram kanalı wikipedia'sına katkıda bulunun. Telegram profilinizi ve kanalınızı bağlamayız ve kişisel verilerinizi paylaşmayız."; "Premium.OnetapTranslate.UseIgnoreLanguages.Note" = "Uygulama, cihazınızdaki her mesajın dilini algılar. Bu, pil ömrünüzü etkileyebilecek yüksek öncelikli bir görevdir."; "Premium.rememberFolderOnExit" = "Çıkışta şu anki klasörü hatırla"; "NicegramSettings.HideStories" = "Hikayeleri Gizle"; @@ -221,3 +219,9 @@ "ChatContextMenu.Hide" = "Gizle"; "ChatContextMenu.Unhide" = "Göster"; "HiddenChatsTooltip" = "Gizli gizli sohbetleri görmek için 'N' logosuna basılı tutun"; + +/*Data Sharing*/ +"NicegramSettings.ShareBotsToggle" = "Bot bilgilerini paylaş"; +"NicegramSettings.ShareChannelsToggle" = "Kanal bilgilerini paylaş"; +"NicegramSettings.ShareStickersToggle" = "Sticker bilgilerini paylaş"; +"NicegramSettings.ShareData.Note" = "Kanal bilgileri ve bağlantısını otomatik olarak göndererek Telegram kanalları ve gruplarının en kapsamlı vikipedisine katkıda bulunun. Telegram profilinizle ve kanalınızla bağlantı kurmuyoruz ve kişisel verilerinizi paylaşmıyoruz."; diff --git a/Telegram/Telegram-iOS/uk.lproj/NiceLocalizable.strings b/Telegram/Telegram-iOS/uk.lproj/NiceLocalizable.strings index 11421761fb2..79d2240183f 100644 --- a/Telegram/Telegram-iOS/uk.lproj/NiceLocalizable.strings +++ b/Telegram/Telegram-iOS/uk.lproj/NiceLocalizable.strings @@ -162,8 +162,6 @@ "NicegramSettings.Other.hideReactions" = "Приховати реакції"; "NicegramSettings.RoundVideos.DownloadVideos" = "Завантажити відео у Галерею"; "Premium.OnetapTranslate.UseIgnoreLanguages" = "Ігнорувати мови"; -"NicegramSettings.ShareChannelsInfoToggle" = "Поділитися інформацією про канал"; -"NicegramSettings.ShareChannelsInfoToggle.Note" = "Приєднуйтесь до найбільш всеосяжної Вікіпедії каналів і груп Telegram, автоматично надсилаючи інформацію про канал і посилання на нашу базу даних. Ми не зв'язуємо ваш профіль у Telegram і канал і не поширюємо ваші персональні дані."; "Premium.OnetapTranslate.UseIgnoreLanguages.Note" = "Додаток виявляє мову кожного повідомлення на вашому пристрої. Це високопродуктивне завдання, яке може вплинути на життя батареї."; "Premium.rememberFolderOnExit" = "Запам’ятати поточну теку при виході"; diff --git a/Telegram/Telegram-iOS/vi.lproj/NiceLocalizable.strings b/Telegram/Telegram-iOS/vi.lproj/NiceLocalizable.strings index 449d34446dc..5a4506401c2 100644 --- a/Telegram/Telegram-iOS/vi.lproj/NiceLocalizable.strings +++ b/Telegram/Telegram-iOS/vi.lproj/NiceLocalizable.strings @@ -124,6 +124,40 @@ "NGLab.RegDate.Title" = "Registration Date"; "NGLab.RegDate.Notice" = "This is an approximate date"; "NGLab.RegDate.MenuItem" = "registered"; +"NGLab.RegDate.FetchError" = "."; +"NGLab.BadDeviceToken" = ""; + +/*Backup Settings*/ +"NiceFeatures.BackupIcloud" = ""; +"NiceFeatures.BackupSettings" = "Backup Settings & Folders"; +"NiceFeatures.BackupSettings.Notice" = "."; +"NiceFeatures.BackupSettings.Done" = ""; +"NiceFeatures.BackupSettings.Error" = ""; + +"NiceFeatures.RestoreSettings.Confirm" = ""; +"NiceFeatures.RestoreSettings.Done" = ""; +"NiceFeatures.RestoreSettings.Error" = ""; + +/*Preview Mode*/ +"Gmod.Restricted" = "."; +"Gmod.Unavailable" = ""; +"Gmod.Disable.Notice" = ""; +"Gmod" = ""; +"Gmod.Enable" = "?"; +"Gmod.Disable" = "?"; +"Gmod.Notice" = ""; + +"SendWithKb" = ""; +"NiceFeatures.ShowGmodIcon" = ""; +"Gmod.OpenChatQ" = "?"; +"Gmod.OpenChatNotice" = ""; +"Gmod.OpenChatBtn" = ""; +"Gmod.DisableBtn" = ""; + +"NicegramSettings.Other.hidePhoneInSettingsNotice" = "."; +"NicegramSettings.Other.showProfileId" = ""; +"NicegramSettings.Other.showRegDate" = ""; +"NicegramSettings.Unblock.Header" = ""; "DoubleBottom.Description" = "To enable this feature, you should have more than one account and switch on Passcode Lock (go to Settings → Privacy & Security → Passcode Lock)"; "DoubleBottom.Enabled.Title" = "Double Bottom is enabled"; "DoubleBottom.Enabled.Description" = "Please remember the passcode you’ve just set and restart the app for Double Bottom to work well"; diff --git a/Telegram/Telegram-iOS/zh-hans.lproj/NiceLocalizable.strings b/Telegram/Telegram-iOS/zh-hans.lproj/NiceLocalizable.strings index 02b08e3b734..bff18cd0737 100644 --- a/Telegram/Telegram-iOS/zh-hans.lproj/NiceLocalizable.strings +++ b/Telegram/Telegram-iOS/zh-hans.lproj/NiceLocalizable.strings @@ -162,8 +162,6 @@ "NicegramSettings.Other.hideReactions" = "隐藏反应"; "NicegramSettings.RoundVideos.DownloadVideos" = "将视频下载到图库"; "Premium.OnetapTranslate.UseIgnoreLanguages" = "忽略语言"; -"NicegramSettings.ShareChannelsInfoToggle" = "分享频道信息"; -"NicegramSettings.ShareChannelsInfoToggle.Note" = "通过自动提交频道信息和链接至我们的数据库,向最全面的 Telegram 频道和群组百科贡献力量。我们不会关联您的 Telegram 个人资料和频道,不会分享您的个人数据。"; "Premium.OnetapTranslate.UseIgnoreLanguages.Note" = "应用会检测您设备上每条消息使用的语言。这一功能需要很高的性能,可能会影响电池寿命。"; "Premium.rememberFolderOnExit" = "退出时记住当前分组"; "NicegramSettings.HideStories" = "隐藏故事"; @@ -221,3 +219,9 @@ "ChatContextMenu.Hide" = "隐藏"; "ChatContextMenu.Unhide" = "取消隐藏"; "HiddenChatsTooltip" = "长按 'N' 标志以显示隐藏的秘密聊天"; + +/*Data Sharing*/ +"NicegramSettings.ShareBotsToggle" = "分享机器人信息"; +"NicegramSettings.ShareChannelsToggle" = "分享频道信息"; +"NicegramSettings.ShareStickersToggle" = "分享贴纸信息"; +"NicegramSettings.ShareData.Note" = "通过自动提交频道信息和链接到我们的数据库,为最全面的Telegram频道和群组维基百科做出贡献。我们不会连接您的Telegram个人资料和频道,也不会分享您的个人数据。"; diff --git a/Telegram/Telegram-iOS/zh-hant.lproj/NiceLocalizable.strings b/Telegram/Telegram-iOS/zh-hant.lproj/NiceLocalizable.strings index f1137df846b..78c1b13372e 100644 --- a/Telegram/Telegram-iOS/zh-hant.lproj/NiceLocalizable.strings +++ b/Telegram/Telegram-iOS/zh-hant.lproj/NiceLocalizable.strings @@ -162,8 +162,6 @@ "NicegramSettings.Other.hideReactions" = "隱藏心情回應"; "NicegramSettings.RoundVideos.DownloadVideos" = "將影片下載至圖片庫"; "Premium.OnetapTranslate.UseIgnoreLanguages" = "忽略語言"; -"NicegramSettings.ShareChannelsInfoToggle" = "分享頻道資訊"; -"NicegramSettings.ShareChannelsInfoToggle.Note" = "將頻道資訊與連結自動提交給我們的資料庫,來為最全面的 Telegram 頻道與群組維基百科做出貢獻。我們不會連接您的 Telegram 個人檔案與頻道,也不會分享您的個人資料。"; "Premium.OnetapTranslate.UseIgnoreLanguages.Note" = "App 會檢測您設備上每則訊息的語言。這是一項高耗電的功能,會影響您的電池壽命。"; "Premium.rememberFolderOnExit" = "記下應用程式關閉時所在的資料夾"; "NicegramSettings.HideStories" = "隱藏故事"; @@ -221,3 +219,9 @@ "ChatContextMenu.Hide" = "隱藏"; "ChatContextMenu.Unhide" = "取消隱藏"; "HiddenChatsTooltip" = "長按 'N' 標誌以顯示隱藏的秘密聊天"; + +/*Data Sharing*/ +"NicegramSettings.ShareBotsToggle" = "分享機器人資訊"; +"NicegramSettings.ShareChannelsToggle" = "分享頻道資訊"; +"NicegramSettings.ShareStickersToggle" = "分享貼圖資訊"; +"NicegramSettings.ShareData.Note" = "通過自動提交頻道資訊和連結到我們的數據庫,貢獻給最全面的Telegram頻道和群組維基百科。我們不會連接您的Telegram個人檔案和頻道,也不會分享您的個人資料。"; diff --git a/submodules/AccountContext/Sources/AccountContext.swift b/submodules/AccountContext/Sources/AccountContext.swift index d1987030db6..db396754e82 100644 --- a/submodules/AccountContext/Sources/AccountContext.swift +++ b/submodules/AccountContext/Sources/AccountContext.swift @@ -395,13 +395,13 @@ public enum ChatSearchDomain: Equatable { public enum ChatLocation: Equatable { case peer(id: PeerId) case replyThread(message: ChatReplyThreadMessage) - case feed(id: Int32) + case customChatContents } public extension ChatLocation { var normalized: ChatLocation { switch self { - case .peer, .feed: + case .peer, .customChatContents: return self case let .replyThread(message): return .replyThread(message: message.normalized) @@ -851,6 +851,15 @@ public protocol TelegramRootControllerInterface: NavigationController { func openSettings() } +public protocol QuickReplySetupScreenInitialData: AnyObject { +} + +public protocol AutomaticBusinessMessageSetupScreenInitialData: AnyObject { +} + +public protocol ChatbotSetupScreenInitialData: AnyObject { +} + public protocol SharedAccountContext: AnyObject { var sharedContainerPath: String { get } var basePath: String { get } @@ -938,8 +947,16 @@ public protocol SharedAccountContext: AnyObject { func makeHashtagSearchController(context: AccountContext, peer: EnginePeer?, query: String, all: Bool) -> ViewController func makeMyStoriesController(context: AccountContext, isArchive: Bool) -> ViewController func makeArchiveSettingsController(context: AccountContext) -> ViewController + func makeFilterSettingsController(context: AccountContext, modal: Bool, scrollToTags: Bool, dismissed: (() -> Void)?) -> ViewController func makeBusinessSetupScreen(context: AccountContext) -> ViewController - func makeChatbotSetupScreen(context: AccountContext) -> ViewController + func makeChatbotSetupScreen(context: AccountContext, initialData: ChatbotSetupScreenInitialData) -> ViewController + func makeChatbotSetupScreenInitialData(context: AccountContext) -> Signal + func makeBusinessLocationSetupScreen(context: AccountContext, initialValue: TelegramBusinessLocation?, completion: @escaping (TelegramBusinessLocation?) -> Void) -> ViewController + func makeBusinessHoursSetupScreen(context: AccountContext, initialValue: TelegramBusinessHours?, completion: @escaping (TelegramBusinessHours?) -> Void) -> ViewController + func makeAutomaticBusinessMessageSetupScreen(context: AccountContext, initialData: AutomaticBusinessMessageSetupScreenInitialData, isAwayMode: Bool) -> ViewController + func makeAutomaticBusinessMessageSetupScreenInitialData(context: AccountContext) -> Signal + func makeQuickReplySetupScreen(context: AccountContext, initialData: QuickReplySetupScreenInitialData) -> ViewController + func makeQuickReplySetupScreenInitialData(context: AccountContext) -> Signal func navigateToChatController(_ params: NavigateToChatControllerParams) func navigateToForumChannel(context: AccountContext, peerId: EnginePeer.Id, navigationController: NavigationController) func navigateToForumThread(context: AccountContext, peerId: EnginePeer.Id, threadId: Int64, messageId: EngineMessage.Id?, navigationController: NavigationController, activateInput: ChatControllerActivateInput?, keepStack: NavigateToChatKeepStack) -> Signal diff --git a/submodules/AccountContext/Sources/ChatController.swift b/submodules/AccountContext/Sources/ChatController.swift index 8773f184871..f8941880428 100644 --- a/submodules/AccountContext/Sources/ChatController.swift +++ b/submodules/AccountContext/Sources/ChatController.swift @@ -744,6 +744,7 @@ public enum ChatControllerSubject: Equatable { case scheduledMessages case pinnedMessages(id: EngineMessage.Id?) case messageOptions(peerIds: [EnginePeer.Id], ids: [EngineMessage.Id], info: MessageOptionsInfo) + case customChatContents(contents: ChatCustomContentsProtocol) public static func ==(lhs: ChatControllerSubject, rhs: ChatControllerSubject) -> Bool { switch lhs { @@ -771,6 +772,12 @@ public enum ChatControllerSubject: Equatable { } else { return false } + case let .customChatContents(lhsValue): + if case let .customChatContents(rhsValue) = rhs, lhsValue === rhsValue { + return true + } else { + return false + } } } @@ -796,6 +803,25 @@ public enum ChatControllerPresentationMode: Equatable { case inline(NavigationController?) } +public enum ChatInputTextCommand: Equatable { + case command(PeerCommand) + case shortcut(ShortcutMessageList.Item) +} + +public struct ChatInputQueryCommandsResult: Equatable { + public var commands: [ChatInputTextCommand] + public var accountPeer: EnginePeer? + public var hasShortcuts: Bool + public var query: String + + public init(commands: [ChatInputTextCommand], accountPeer: EnginePeer?, hasShortcuts: Bool, query: String) { + self.commands = commands + self.accountPeer = accountPeer + self.hasShortcuts = hasShortcuts + self.query = query + } +} + public enum ChatPresentationInputQueryResult: Equatable { // MARK: Nicegram QuickReplies case quickReplies([String]) @@ -803,7 +829,7 @@ public enum ChatPresentationInputQueryResult: Equatable { case stickers([FoundStickerItem]) case hashtags([String]) case mentions([EnginePeer]) - case commands([PeerCommand]) + case commands(ChatInputQueryCommandsResult) case emojis([(String, TelegramMediaFile?, String)], NSRange) case contextRequestResult(EnginePeer?, ChatContextResultCollection?) @@ -1061,7 +1087,30 @@ public enum ChatHistoryListSource { } case `default` - case custom(messages: Signal<([Message], Int32, Bool), NoError>, messageId: MessageId, quote: Quote?, loadMore: (() -> Void)?) + case custom(messages: Signal<([Message], Int32, Bool), NoError>, messageId: MessageId?, quote: Quote?, loadMore: (() -> Void)?) + case customView(historyView: Signal<(MessageHistoryView, ViewUpdateType), NoError>) +} + +public enum ChatQuickReplyShortcutType { + case generic + case greeting + case away +} + +public enum ChatCustomContentsKind: Equatable { + case quickReplyMessageInput(shortcut: String, shortcutType: ChatQuickReplyShortcutType) +} + +public protocol ChatCustomContentsProtocol: AnyObject { + var kind: ChatCustomContentsKind { get } + var historyView: Signal<(MessageHistoryView, ViewUpdateType), NoError> { get } + var messageLimit: Int? { get } + + func enqueueMessages(messages: [EnqueueMessage]) + func deleteMessages(ids: [EngineMessage.Id]) + func editMessage(id: EngineMessage.Id, text: String, media: RequestEditMessageMedia, entities: TextEntitiesMessageAttribute?, webpagePreviewAttribute: WebpagePreviewMessageAttribute?, disableUrlPreview: Bool) + + func quickReplyUpdateShortcut(value: String) } public enum ChatHistoryListDisplayHeaders { @@ -1080,7 +1129,7 @@ public protocol ChatControllerInteractionProtocol: AnyObject { public enum ChatHistoryNodeHistoryState: Equatable { case loading - case loaded(isEmpty: Bool) + case loaded(isEmpty: Bool, hasReachedLimits: Bool) } public protocol ChatHistoryListNode: ListView { diff --git a/submodules/AccountContext/Sources/ContactMultiselectionController.swift b/submodules/AccountContext/Sources/ContactMultiselectionController.swift index 6d601745281..45cbfa89ff7 100644 --- a/submodules/AccountContext/Sources/ContactMultiselectionController.swift +++ b/submodules/AccountContext/Sources/ContactMultiselectionController.swift @@ -45,6 +45,7 @@ public enum ContactMultiselectionControllerMode { public var chatListFilters: [ChatListFilter]? public var displayAutoremoveTimeout: Bool public var displayPresence: Bool + public var onlyUsers: Bool public init( title: String, @@ -53,7 +54,8 @@ public enum ContactMultiselectionControllerMode { additionalCategories: ContactMultiselectionControllerAdditionalCategories?, chatListFilters: [ChatListFilter]?, displayAutoremoveTimeout: Bool = false, - displayPresence: Bool = false + displayPresence: Bool = false, + onlyUsers: Bool = false ) { self.title = title self.searchPlaceholder = searchPlaceholder @@ -62,6 +64,7 @@ public enum ContactMultiselectionControllerMode { self.chatListFilters = chatListFilters self.displayAutoremoveTimeout = displayAutoremoveTimeout self.displayPresence = displayPresence + self.onlyUsers = onlyUsers } } diff --git a/submodules/AccountContext/Sources/MediaManager.swift b/submodules/AccountContext/Sources/MediaManager.swift index 02cda6c109f..092fc4c52ba 100644 --- a/submodules/AccountContext/Sources/MediaManager.swift +++ b/submodules/AccountContext/Sources/MediaManager.swift @@ -36,8 +36,8 @@ public enum PeerMessagesPlaylistLocation: Equatable, SharedMediaPlaylistLocation return .peer(peerId) case let .replyThread(replyThreaMessage): return .peer(replyThreaMessage.peerId) - case let .feed(id): - return .feed(id) + case .customChatContents: + return .custom } case let .singleMessage(id): return .peer(id.peerId) diff --git a/submodules/AccountContext/Sources/PeerNameColors.swift b/submodules/AccountContext/Sources/PeerNameColors.swift index 0bca8e23461..1966a168ea3 100644 --- a/submodules/AccountContext/Sources/PeerNameColors.swift +++ b/submodules/AccountContext/Sources/PeerNameColors.swift @@ -80,6 +80,7 @@ public class PeerNameColors: Equatable { colors: defaultSingleColors, darkColors: [:], displayOrder: [5, 3, 1, 0, 2, 4, 6], + chatFolderTagDisplayOrder: [5, 3, 1, 0, 2, 4, 6], profileColors: [:], profileDarkColors: [:], profilePaletteColors: [:], @@ -97,6 +98,8 @@ public class PeerNameColors: Equatable { public let darkColors: [Int32: Colors] public let displayOrder: [Int32] + public let chatFolderTagDisplayOrder: [Int32] + public let profileColors: [Int32: Colors] public let profileDarkColors: [Int32: Colors] public let profilePaletteColors: [Int32: Colors] @@ -119,6 +122,16 @@ public class PeerNameColors: Equatable { } } + public func getChatFolderTag(_ color: PeerNameColor, dark: Bool = false) -> Colors { + if dark, let colors = self.darkColors[color.rawValue] { + return colors + } else if let colors = self.colors[color.rawValue] { + return colors + } else { + return PeerNameColors.defaultSingleColors[5]! + } + } + public func getProfile(_ color: PeerNameColor, dark: Bool = false, subject: Subject = .background) -> Colors { switch subject { case .background: @@ -152,6 +165,7 @@ public class PeerNameColors: Equatable { colors: [Int32: Colors], darkColors: [Int32: Colors], displayOrder: [Int32], + chatFolderTagDisplayOrder: [Int32], profileColors: [Int32: Colors], profileDarkColors: [Int32: Colors], profilePaletteColors: [Int32: Colors], @@ -166,6 +180,7 @@ public class PeerNameColors: Equatable { self.colors = colors self.darkColors = darkColors self.displayOrder = displayOrder + self.chatFolderTagDisplayOrder = chatFolderTagDisplayOrder self.profileColors = profileColors self.profileDarkColors = profileDarkColors self.profilePaletteColors = profilePaletteColors @@ -257,6 +272,7 @@ public class PeerNameColors: Equatable { colors: colors, darkColors: darkColors, displayOrder: displayOrder, + chatFolderTagDisplayOrder: PeerNameColors.defaultValue.chatFolderTagDisplayOrder, profileColors: profileColors, profileDarkColors: profileDarkColors, profilePaletteColors: profilePaletteColors, @@ -280,6 +296,9 @@ public class PeerNameColors: Equatable { if lhs.displayOrder != rhs.displayOrder { return false } + if lhs.chatFolderTagDisplayOrder != rhs.chatFolderTagDisplayOrder { + return false + } if lhs.profileColors != rhs.profileColors { return false } diff --git a/submodules/AccountContext/Sources/Premium.swift b/submodules/AccountContext/Sources/Premium.swift index a8a5e153852..c327bc55978 100644 --- a/submodules/AccountContext/Sources/Premium.swift +++ b/submodules/AccountContext/Sources/Premium.swift @@ -38,6 +38,7 @@ public enum PremiumIntroSource { case presence case readTime case messageTags + case folderTags } public enum PremiumGiftSource: Equatable { @@ -70,6 +71,14 @@ public enum PremiumDemoSubject { case messageTags case lastSeen case messagePrivacy + case folderTags + + case businessLocation + case businessHours + case businessGreetingMessage + case businessQuickReplies + case businessAwayMessage + case businessChatBots } public enum PremiumLimitSubject { diff --git a/submodules/AttachmentUI/Sources/AttachmentController.swift b/submodules/AttachmentUI/Sources/AttachmentController.swift index 0fe0add1fa6..f1ec3c9d1f7 100644 --- a/submodules/AttachmentUI/Sources/AttachmentController.swift +++ b/submodules/AttachmentUI/Sources/AttachmentController.swift @@ -19,6 +19,7 @@ public enum AttachmentButtonType: Equatable { case gallery case file case location + case quickReply case contact case poll case app(AttachMenuBot) @@ -27,54 +28,60 @@ public enum AttachmentButtonType: Equatable { public static func ==(lhs: AttachmentButtonType, rhs: AttachmentButtonType) -> Bool { switch lhs { - case .gallery: - if case .gallery = rhs { - return true - } else { - return false - } - case .file: - if case .file = rhs { - return true - } else { - return false - } - case .location: - if case .location = rhs { - return true - } else { - return false - } - case .contact: - if case .contact = rhs { - return true - } else { - return false - } - case .poll: - if case .poll = rhs { - return true - } else { - return false - } - case let .app(lhsBot): - if case let .app(rhsBot) = rhs, lhsBot.peer.id == rhsBot.peer.id { - return true - } else { - return false - } - case .gift: - if case .gift = rhs { - return true - } else { - return false - } - case .standalone: - if case .standalone = rhs { - return true - } else { - return false - } + case .gallery: + if case .gallery = rhs { + return true + } else { + return false + } + case .file: + if case .file = rhs { + return true + } else { + return false + } + case .location: + if case .location = rhs { + return true + } else { + return false + } + case .quickReply: + if case .quickReply = rhs { + return true + } else { + return false + } + case .contact: + if case .contact = rhs { + return true + } else { + return false + } + case .poll: + if case .poll = rhs { + return true + } else { + return false + } + case let .app(lhsBot): + if case let .app(rhsBot) = rhs, lhsBot.peer.id == rhsBot.peer.id { + return true + } else { + return false + } + case .gift: + if case .gift = rhs { + return true + } else { + return false + } + case .standalone: + if case .standalone = rhs { + return true + } else { + return false + } } } } diff --git a/submodules/AttachmentUI/Sources/AttachmentPanel.swift b/submodules/AttachmentUI/Sources/AttachmentPanel.swift index e8c6db3c67b..6e9f7ef17d6 100644 --- a/submodules/AttachmentUI/Sources/AttachmentPanel.swift +++ b/submodules/AttachmentUI/Sources/AttachmentPanel.swift @@ -217,6 +217,9 @@ private final class AttachButtonComponent: CombinedComponent { name = "" imageName = "" imageFile = nil + case .quickReply: + name = strings.Attachment_Reply + imageName = "Chat/Attach Menu/Reply" } let tintColor = component.isSelected ? component.theme.rootController.tabBar.selectedIconColor : component.theme.rootController.tabBar.iconColor @@ -824,6 +827,8 @@ final class AttachmentPanel: ASDisplayNode, UIScrollViewDelegate { }, sendContextResult: { _, _, _, _ in return false }, sendBotCommand: { _, _ in + }, sendShortcut: { _ in + }, openEditShortcuts: { }, sendBotStart: { _ in }, botSwitchChatWithPayload: { _, _ in }, beginMediaRecording: { _ in @@ -1184,6 +1189,8 @@ final class AttachmentPanel: ASDisplayNode, UIScrollViewDelegate { accessibilityTitle = bot.shortName case .standalone: accessibilityTitle = "" + case .quickReply: + accessibilityTitle = self.presentationData.strings.Attachment_Reply } buttonView.isAccessibilityElement = true buttonView.accessibilityLabel = accessibilityTitle diff --git a/submodules/AuthorizationUI/Sources/AuthorizationSequenceController.swift b/submodules/AuthorizationUI/Sources/AuthorizationSequenceController.swift index bdc89f11928..dd9f8e0154c 100644 --- a/submodules/AuthorizationUI/Sources/AuthorizationSequenceController.swift +++ b/submodules/AuthorizationUI/Sources/AuthorizationSequenceController.swift @@ -1305,18 +1305,9 @@ public final class AuthorizationSequenceController: NavigationController, ASAuth } public static func defaultCountryCode() -> Int32 { - var countryId: String? = nil - let networkInfo = CTTelephonyNetworkInfo() - if let carrier = networkInfo.serviceSubscriberCellularProviders?.values.first { - countryId = carrier.isoCountryCode - } - - if countryId == nil { - countryId = (Locale.current as NSLocale).object(forKey: .countryCode) as? String - } - + let countryId = (Locale.current as NSLocale).object(forKey: .countryCode) as? String + var countryCode: Int32 = 1 - if let countryId = countryId { let normalizedId = countryId.uppercased() for (code, idAndName) in countryCodeToIdAndName { diff --git a/submodules/BrowserUI/Sources/BrowserScreen.swift b/submodules/BrowserUI/Sources/BrowserScreen.swift index 95e06060e34..1bfe441e3b3 100644 --- a/submodules/BrowserUI/Sources/BrowserScreen.swift +++ b/submodules/BrowserUI/Sources/BrowserScreen.swift @@ -618,6 +618,7 @@ public class BrowserScreen: ViewController { bottom: layout.intrinsicInsets.bottom + layout.safeInsets.bottom, right: layout.safeInsets.right ), + additionalInsets: layout.additionalInsets, inputHeight: layout.inputHeight ?? 0.0, metrics: layout.metrics, deviceMetrics: layout.deviceMetrics, diff --git a/submodules/ChatListUI/BUILD b/submodules/ChatListUI/BUILD index 0efe2ce08ad..da10aaf47c4 100644 --- a/submodules/ChatListUI/BUILD +++ b/submodules/ChatListUI/BUILD @@ -118,6 +118,7 @@ swift_library( "//submodules/TelegramUI/Components/Settings/ArchiveInfoScreen", "//submodules/TelegramUI/Components/Settings/NewSessionInfoScreen", "//submodules/TelegramUI/Components/Settings/PeerNameColorItem", + "//submodules/Components/MultilineTextComponent", ], visibility = [ "//visibility:public", diff --git a/submodules/ChatListUI/Sources/ChatListController.swift b/submodules/ChatListUI/Sources/ChatListController.swift index 223096241e8..6133450f963 100644 --- a/submodules/ChatListUI/Sources/ChatListController.swift +++ b/submodules/ChatListUI/Sources/ChatListController.swift @@ -5782,9 +5782,10 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController private func openFilterSettings() { self.chatListDisplayNode.mainContainerNode.updateEnableAdjacentFilterLoading(false) if let navigationController = self.context.sharedContext.mainWindow?.viewController as? NavigationController { - navigationController.pushViewController(chatListFilterPresetListController(context: self.context, mode: .modal, dismissed: { [weak self] in + let controller = self.context.sharedContext.makeFilterSettingsController(context: self.context, modal: true, scrollToTags: false, dismissed: { [weak self] in self?.chatListDisplayNode.mainContainerNode.updateEnableAdjacentFilterLoading(true) - })) + }) + navigationController.pushViewController(controller) } } diff --git a/submodules/ChatListUI/Sources/ChatListControllerNode.swift b/submodules/ChatListUI/Sources/ChatListControllerNode.swift index 5bb05f7d6f1..9abab9038d3 100644 --- a/submodules/ChatListUI/Sources/ChatListControllerNode.swift +++ b/submodules/ChatListUI/Sources/ChatListControllerNode.swift @@ -1342,6 +1342,7 @@ final class ChatListControllerNode: ASDisplayNode, UIGestureRecognizerDelegate { statusBarHeight: layout.statusBarHeight ?? 0.0, sideInset: layout.safeInsets.left, isSearchActive: self.isSearchDisplayControllerActive, + isSearchEnabled: true, primaryContent: headerContent?.primaryContent, secondaryContent: headerContent?.secondaryContent, secondaryTransition: self.inlineStackContainerTransitionFraction, diff --git a/submodules/ChatListUI/Sources/ChatListFilterPresetController.swift b/submodules/ChatListUI/Sources/ChatListFilterPresetController.swift index 3e6973ec32d..662f8cfe9cc 100644 --- a/submodules/ChatListUI/Sources/ChatListFilterPresetController.swift +++ b/submodules/ChatListUI/Sources/ChatListFilterPresetController.swift @@ -44,6 +44,7 @@ private final class ChatListFilterPresetControllerArguments { let linkContextAction: (ExportedChatFolderLink?, ASDisplayNode, ContextGesture?) -> Void let peerContextAction: (EnginePeer, ASDisplayNode, ContextGesture?, CGPoint?) -> Void let updateTagColor: (PeerNameColor?) -> Void + let openTagColorPremium: () -> Void init( context: AccountContext, @@ -63,7 +64,8 @@ private final class ChatListFilterPresetControllerArguments { removeLink: @escaping (ExportedChatFolderLink) -> Void, linkContextAction: @escaping (ExportedChatFolderLink?, ASDisplayNode, ContextGesture?) -> Void, peerContextAction: @escaping (EnginePeer, ASDisplayNode, ContextGesture?, CGPoint?) -> Void, - updateTagColor: @escaping (PeerNameColor?) -> Void + updateTagColor: @escaping (PeerNameColor?) -> Void, + openTagColorPremium: @escaping () -> Void ) { self.context = context self.updateState = updateState @@ -83,6 +85,7 @@ private final class ChatListFilterPresetControllerArguments { self.linkContextAction = linkContextAction self.peerContextAction = peerContextAction self.updateTagColor = updateTagColor + self.openTagColorPremium = openTagColorPremium } } @@ -231,8 +234,8 @@ private enum ChatListFilterPresetEntry: ItemListNodeEntry { case inviteLinkCreate(hasLinks: Bool) case inviteLink(Int, ExportedChatFolderLink) case inviteLinkInfo(text: String) - case tagColorHeader(name: String, color: PeerNameColors.Colors) - case tagColor(colors: PeerNameColors, currentColor: PeerNameColor?) + case tagColorHeader(name: String, color: PeerNameColors.Colors?, isPremium: Bool) + case tagColor(colors: PeerNameColors, currentColor: PeerNameColor?, isPremium: Bool) case tagColorFooter var section: ItemListSectionId { @@ -458,26 +461,43 @@ private enum ChatListFilterPresetEntry: ItemListNodeEntry { return ItemListPeerActionItem(presentationData: presentationData, icon: PresentationResourcesItemList.downArrowImage(presentationData.theme), title: text, sectionId: self.section, editing: false, action: { arguments.expandSection(.exclude) }) - case let .tagColorHeader(name, color): - //TODO:localize - return ItemListSectionHeaderItem(presentationData: presentationData, text: "FOLDER COLOR", badge: name.uppercased(), badgeStyle: ItemListSectionHeaderItem.BadgeStyle( - background: color.main.withMultipliedAlpha(0.1), - foreground: color.main - ), sectionId: self.section) - case let .tagColor(colors, color): + case let .tagColorHeader(name, color, isPremium): + var badge: String? + var badgeStyle: ItemListSectionHeaderItem.BadgeStyle? + var accessoryText: ItemListSectionHeaderAccessoryText? + if isPremium { + if let color { + badge = name.uppercased() + badgeStyle = ItemListSectionHeaderItem.BadgeStyle( + background: color.main.withMultipliedAlpha(0.1), + foreground: color.main + ) + } else { + accessoryText = ItemListSectionHeaderAccessoryText(value: presentationData.strings.ChatListFilter_TagLabelNoTag, color: .generic) + } + } else if color != nil { + accessoryText = ItemListSectionHeaderAccessoryText(value: presentationData.strings.ChatListFilter_TagLabelPremiumExpired, color: .generic) + } + return ItemListSectionHeaderItem(presentationData: presentationData, text: presentationData.strings.ChatListFilter_TagSectionTitle, badge: badge, badgeStyle: badgeStyle, accessoryText: accessoryText, sectionId: self.section) + case let .tagColor(colors, color, isPremium): return PeerNameColorItem( theme: presentationData.theme, colors: colors, - isProfile: true, - currentColor: color, + mode: .folderTag, + displayEmptyColor: true, + currentColor: isPremium ? color : nil, + isLocked: !isPremium, updated: { color in - arguments.updateTagColor(color) + if isPremium { + arguments.updateTagColor(color) + } else { + arguments.openTagColorPremium() + } }, sectionId: self.section ) case .tagColorFooter: - //TODO:localize - return ItemListTextItem(presentationData: presentationData, text: .plain("This color will be used for the folder's tag in the chat list"), sectionId: self.section) + return ItemListTextItem(presentationData: presentationData, text: .plain(presentationData.strings.ChatListFilter_TagSectionFooter), sectionId: self.section) case .inviteLinkHeader: return ItemListSectionHeaderItem(presentationData: presentationData, text: presentationData.strings.ChatListFilter_SectionShare, badge: nil, sectionId: self.section) case let .inviteLinkCreate(hasLinks): @@ -502,6 +522,7 @@ private struct ChatListFilterPresetControllerState: Equatable { var name: String var changedName: Bool var color: PeerNameColor? + var colorUpdated: Bool = false var includeCategories: ChatListFilterPeerCategories var excludeMuted: Bool var excludeRead: Bool @@ -616,12 +637,20 @@ private func chatListFilterPresetControllerEntries(context: AccountContext, pres entries.append(.excludePeerInfo(presentationData.strings.ChatListFolder_ExcludeSectionInfo)) } - /*let tagColor = state.color ?? .blue - let resolvedColor = context.peerNameColors.getProfile(tagColor, dark: presentationData.theme.overallDarkAppearance, subject: .palette) + let tagColor: PeerNameColor? + if state.colorUpdated { + tagColor = state.color + } else { + tagColor = state.color ?? .blue + } + var resolvedColor: PeerNameColors.Colors? + if let tagColor { + resolvedColor = context.peerNameColors.getChatFolderTag(tagColor, dark: presentationData.theme.overallDarkAppearance) + } - entries.append(.tagColorHeader(name: state.name, color: resolvedColor)) - entries.append(.tagColor(colors: context.peerNameColors, currentColor: tagColor)) - entries.append(.tagColorFooter)*/ + entries.append(.tagColorHeader(name: state.name, color: resolvedColor, isPremium: isPremium)) + entries.append(.tagColor(colors: context.peerNameColors, currentColor: tagColor, isPremium: isPremium)) + entries.append(.tagColorFooter) var hasLinks = false if let inviteLinks, !inviteLinks.isEmpty { @@ -1018,7 +1047,8 @@ func chatListFilterPresetController(context: AccountContext, currentPreset initi } else { initialName = "" } - let initialState = ChatListFilterPresetControllerState(name: initialName, changedName: initialPreset != nil, color: initialPreset?.data?.color, includeCategories: initialPreset?.data?.categories ?? [], excludeMuted: initialPreset?.data?.excludeMuted ?? false, excludeRead: initialPreset?.data?.excludeRead ?? false, excludeArchived: initialPreset?.data?.excludeArchived ?? false, additionallyIncludePeers: initialPreset?.data?.includePeers.peers ?? [], additionallyExcludePeers: initialPreset?.data?.excludePeers ?? [], expandedSections: []) + var initialState = ChatListFilterPresetControllerState(name: initialName, changedName: initialPreset != nil, color: initialPreset?.data?.color, includeCategories: initialPreset?.data?.categories ?? [], excludeMuted: initialPreset?.data?.excludeMuted ?? false, excludeRead: initialPreset?.data?.excludeRead ?? false, excludeArchived: initialPreset?.data?.excludeArchived ?? false, additionallyIncludePeers: initialPreset?.data?.includePeers.peers ?? [], additionallyExcludePeers: initialPreset?.data?.excludePeers ?? [], expandedSections: []) + initialState.colorUpdated = true let updatedCurrentPreset: Signal if let initialPreset { @@ -1543,8 +1573,20 @@ func chatListFilterPresetController(context: AccountContext, currentPreset initi updateState { state in var state = state state.color = color + state.colorUpdated = true return state } + }, + openTagColorPremium: { + var replaceImpl: ((ViewController) -> Void)? + let controller = context.sharedContext.makePremiumDemoController(context: context, subject: .folderTags, action: { + let controller = context.sharedContext.makePremiumIntroController(context: context, source: .folderTags, forceDark: false, dismissed: nil) + replaceImpl?(controller) + }) + replaceImpl = { [weak controller] c in + controller?.replace(with: c) + } + pushControllerImpl?(controller) } ) diff --git a/submodules/ChatListUI/Sources/ChatListFilterPresetListController.swift b/submodules/ChatListUI/Sources/ChatListFilterPresetListController.swift index c41c81ded51..c398146a213 100644 --- a/submodules/ChatListUI/Sources/ChatListFilterPresetListController.swift +++ b/submodules/ChatListUI/Sources/ChatListFilterPresetListController.swift @@ -22,8 +22,9 @@ private final class ChatListFilterPresetListControllerArguments { let setItemWithRevealedOptions: (Int32?, Int32?) -> Void let removePreset: (Int32) -> Void let updateDisplayTags: (Bool) -> Void + let updateDisplayTagsLocked: () -> Void - init(context: AccountContext, addSuggestedPressed: @escaping (String, ChatListFilterData) -> Void, openPreset: @escaping (ChatListFilter) -> Void, addNew: @escaping () -> Void, setItemWithRevealedOptions: @escaping (Int32?, Int32?) -> Void, removePreset: @escaping (Int32) -> Void, updateDisplayTags: @escaping (Bool) -> Void) { + init(context: AccountContext, addSuggestedPressed: @escaping (String, ChatListFilterData) -> Void, openPreset: @escaping (ChatListFilter) -> Void, addNew: @escaping () -> Void, setItemWithRevealedOptions: @escaping (Int32?, Int32?) -> Void, removePreset: @escaping (Int32) -> Void, updateDisplayTags: @escaping (Bool) -> Void, updateDisplayTagsLocked: @escaping () -> Void) { self.context = context self.addSuggestedPressed = addSuggestedPressed self.openPreset = openPreset @@ -31,6 +32,7 @@ private final class ChatListFilterPresetListControllerArguments { self.setItemWithRevealedOptions = setItemWithRevealedOptions self.removePreset = removePreset self.updateDisplayTags = updateDisplayTags + self.updateDisplayTagsLocked = updateDisplayTagsLocked } } @@ -41,6 +43,19 @@ private enum ChatListFilterPresetListSection: Int32 { case tags } +public enum ChatListFilterPresetListEntryTag: ItemListItemTag { + case displayTags + + public func isEqual(to other: ItemListItemTag) -> Bool { + if let other = other as? ChatListFilterPresetListEntryTag, self == other { + return true + } else { + return false + } + } +} + + private func stringForUserCount(_ peers: [EnginePeer.Id: SelectivePrivacyPeer], strings: PresentationStrings) -> String { if peers.isEmpty { return strings.PrivacyLastSeenSettings_EmpryUsersPlaceholder @@ -80,10 +95,10 @@ private enum ChatListFilterPresetListEntry: ItemListNodeEntry { case suggestedPreset(index: PresetIndex, title: String, label: String, preset: ChatListFilterData) case suggestedAddCustom(String) case listHeader(String) - case preset(index: PresetIndex, title: String, label: String, preset: ChatListFilter, canBeReordered: Bool, canBeDeleted: Bool, isEditing: Bool, isAllChats: Bool, isDisabled: Bool) + case preset(index: PresetIndex, title: String, label: String, preset: ChatListFilter, canBeReordered: Bool, canBeDeleted: Bool, isEditing: Bool, isAllChats: Bool, isDisabled: Bool, displayTags: Bool) case addItem(text: String, isEditing: Bool) case listFooter(String) - case displayTags(Bool) + case displayTags(Bool?) case displayTagsFooter var section: ItemListSectionId { @@ -107,7 +122,7 @@ private enum ChatListFilterPresetListEntry: ItemListNodeEntry { return 100 case .addItem: return 101 - case let .preset(index, _, _, _, _, _, _, _, _): + case let .preset(index, _, _, _, _, _, _, _, _, _): return 102 + index.value case .listFooter: return 1001 @@ -136,7 +151,7 @@ private enum ChatListFilterPresetListEntry: ItemListNodeEntry { return .suggestedAddCustom case .listHeader: return .listHeader - case let .preset(_, _, _, preset, _, _, _, _, _): + case let .preset(_, _, _, preset, _, _, _, _, _, _): return .preset(preset.id) case .addItem: return .addItem @@ -170,8 +185,16 @@ private enum ChatListFilterPresetListEntry: ItemListNodeEntry { }) case let .listHeader(text): return ItemListSectionHeaderItem(presentationData: presentationData, text: text, multiline: true, sectionId: self.section) - case let .preset(_, title, label, preset, canBeReordered, canBeDeleted, isEditing, isAllChats, isDisabled): - return ChatListFilterPresetListItem(presentationData: presentationData, preset: preset, title: title, label: label, editing: ChatListFilterPresetListItemEditing(editable: true, editing: isEditing, revealed: false), canBeReordered: canBeReordered, canBeDeleted: canBeDeleted, isAllChats: isAllChats, isDisabled: isDisabled, sectionId: self.section, action: { + case let .preset(_, title, label, preset, canBeReordered, canBeDeleted, isEditing, isAllChats, isDisabled, displayTags): + var resolvedColor: UIColor? + if displayTags, case let .filter(_, _, _, data) = preset { + let tagColor = data.color + if let tagColor { + resolvedColor = arguments.context.peerNameColors.getChatFolderTag(tagColor, dark: presentationData.theme.overallDarkAppearance).main + } + } + + return ChatListFilterPresetListItem(presentationData: presentationData, preset: preset, title: title, label: label, tagColor: resolvedColor, editing: ChatListFilterPresetListItemEditing(editable: true, editing: isEditing, revealed: false), canBeReordered: canBeReordered, canBeDeleted: canBeDeleted, isAllChats: isAllChats, isDisabled: isDisabled, sectionId: self.section, action: { if isDisabled { arguments.addNew() } else { @@ -189,13 +212,17 @@ private enum ChatListFilterPresetListEntry: ItemListNodeEntry { case let .listFooter(text): return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section) case let .displayTags(value): - //TODO:localize - return ItemListSwitchItem(presentationData: presentationData, title: "Show Folder Tags", value: value, sectionId: self.section, style: .blocks, updated: { value in - arguments.updateDisplayTags(value) - }) + return ItemListSwitchItem(presentationData: presentationData, title: presentationData.strings.ChatListFilterList_ShowTags, value: value == true, enableInteractiveChanges: value != nil, enabled: true, displayLocked: value == nil, sectionId: self.section, style: .blocks, updated: { updatedValue in + if value != nil { + arguments.updateDisplayTags(updatedValue) + } else { + arguments.updateDisplayTagsLocked() + } + }, activatedWhileDisabled: { + arguments.updateDisplayTagsLocked() + }, tag: ChatListFilterPresetListEntryTag.displayTags) case .displayTagsFooter: - //TODO:localize - return ItemListTextItem(presentationData: presentationData, text: .plain("Display folder names for each chat in the chat list."), sectionId: self.section) + return ItemListTextItem(presentationData: presentationData, text: .plain(presentationData.strings.ChatListFilterList_ShowTagsFooter), sectionId: self.section) } } } @@ -249,15 +276,20 @@ private func chatListFilterPresetListControllerEntries(presentationData: Present entries.append(.addItem(text: presentationData.strings.ChatListFilterList_CreateFolder, isEditing: state.isEditing)) + var effectiveDisplayTags: Bool? + if isPremium { + effectiveDisplayTags = displayTags + } + if !filters.isEmpty || suggestedFilters.isEmpty { var folderCount = 0 for (filter, chatCount) in filtersWithAppliedOrder(filters: filters, order: updatedFilterOrder) { if case .allChats = filter { - entries.append(.preset(index: PresetIndex(value: entries.count), title: "", label: "", preset: filter, canBeReordered: filters.count > 1, canBeDeleted: false, isEditing: state.isEditing, isAllChats: true, isDisabled: false)) + entries.append(.preset(index: PresetIndex(value: entries.count), title: "", label: "", preset: filter, canBeReordered: filters.count > 1, canBeDeleted: false, isEditing: state.isEditing, isAllChats: true, isDisabled: false, displayTags: effectiveDisplayTags == true)) } if case let .filter(_, title, _, _) = filter { folderCount += 1 - entries.append(.preset(index: PresetIndex(value: entries.count), title: title, label: chatCount == 0 ? "" : "\(chatCount)", preset: filter, canBeReordered: filters.count > 1, canBeDeleted: true, isEditing: state.isEditing, isAllChats: false, isDisabled: !isPremium && folderCount > limits.maxFoldersCount)) + entries.append(.preset(index: PresetIndex(value: entries.count), title: title, label: chatCount == 0 ? "" : "\(chatCount)", preset: filter, canBeReordered: filters.count > 1, canBeDeleted: true, isEditing: state.isEditing, isAllChats: false, isDisabled: !isPremium && folderCount > limits.maxFoldersCount, displayTags: effectiveDisplayTags == true)) } } @@ -274,8 +306,8 @@ private func chatListFilterPresetListControllerEntries(presentationData: Present } } - /*entries.append(.displayTags(displayTags)) - entries.append(.displayTagsFooter)*/ + entries.append(.displayTags(effectiveDisplayTags)) + entries.append(.displayTagsFooter) return entries } @@ -285,7 +317,7 @@ public enum ChatListFilterPresetListControllerMode { case modal } -public func chatListFilterPresetListController(context: AccountContext, mode: ChatListFilterPresetListControllerMode, dismissed: (() -> Void)? = nil) -> ViewController { +public func chatListFilterPresetListController(context: AccountContext, mode: ChatListFilterPresetListControllerMode, scrollToTags: Bool = false, dismissed: (() -> Void)? = nil) -> ViewController { let initialState = ChatListFilterPresetListControllerState() let statePromise = ValuePromise(initialState, ignoreRepeated: true) let stateValue = Atomic(value: initialState) @@ -308,6 +340,8 @@ public func chatListFilterPresetListController(context: AccountContext, mode: Ch let filtersWithCounts = Promise<[(ChatListFilter, Int)]>() filtersWithCounts.set(filtersWithCountsSignal) + let animateNextShowHideTagsTransition = Atomic(value: nil) + let arguments = ChatListFilterPresetListControllerArguments(context: context, addSuggestedPressed: { title, data in let _ = combineLatest( @@ -523,6 +557,16 @@ public func chatListFilterPresetListController(context: AccountContext, mode: Ch }) }, updateDisplayTags: { value in context.engine.peers.updateChatListFiltersDisplayTags(isEnabled: value) + }, updateDisplayTagsLocked: { + var replaceImpl: ((ViewController) -> Void)? + let controller = context.sharedContext.makePremiumDemoController(context: context, subject: .folderTags, action: { + let controller = context.sharedContext.makePremiumIntroController(context: context, source: .folderTags, forceDark: false, dismissed: nil) + replaceImpl?(controller) + }) + replaceImpl = { [weak controller] c in + controller?.replace(with: c) + } + pushControllerImpl?(controller) }) let featuredFilters = context.account.postbox.preferencesView(keys: [PreferencesKeys.chatListFiltersFeaturedState]) @@ -538,6 +582,8 @@ public func chatListFilterPresetListController(context: AccountContext, mode: Ch let preferences = context.account.postbox.preferencesView(keys: [ApplicationSpecificPreferencesKeys.chatListFilterSettings]) + let previousDisplayTags = Atomic(value: nil) + let limits = context.engine.data.get( TelegramEngine.EngineData.Item.Configuration.UserLimits(isPremium: false), TelegramEngine.EngineData.Item.Configuration.UserLimits(isPremium: true) @@ -626,8 +672,14 @@ public func chatListFilterPresetListController(context: AccountContext, mode: Ch rightNavigationButton = nil } + let previousDisplayTagsValue = previousDisplayTags.swap(displayTags) + if let previousDisplayTagsValue, previousDisplayTagsValue != displayTags { + let _ = animateNextShowHideTagsTransition.swap(displayTags) + } + let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(presentationData.strings.ChatListFolderSettings_Title), leftNavigationButton: leftNavigationButton, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: false) - let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: chatListFilterPresetListControllerEntries(presentationData: presentationData, state: state, filters: filtersWithCountsValue, updatedFilterOrder: updatedFilterOrderValue, suggestedFilters: suggestedFilters, displayTags: displayTags, isPremium: isPremium, limits: limits, premiumLimits: premiumLimits), style: .blocks, animateChanges: true) + let entries = chatListFilterPresetListControllerEntries(presentationData: presentationData, state: state, filters: filtersWithCountsValue, updatedFilterOrder: updatedFilterOrderValue, suggestedFilters: suggestedFilters, displayTags: displayTags, isPremium: isPremium, limits: limits, premiumLimits: premiumLimits) + let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: entries, style: .blocks, initialScrollToItem: scrollToTags ? ListViewScrollToItem(index: entries.count - 1, position: .center(.bottom), animated: true, curve: .Spring(duration: 0.4), directionHint: .Down) : nil, animateChanges: true) return (controllerState, (listState, arguments)) } @@ -659,7 +711,7 @@ public func chatListFilterPresetListController(context: AccountContext, mode: Ch } controller.setReorderEntry({ (fromIndex: Int, toIndex: Int, entries: [ChatListFilterPresetListEntry]) -> Signal in let fromEntry = entries[fromIndex] - guard case let .preset(_, _, _, fromPreset, _, _, _, _, _) = fromEntry else { + guard case let .preset(_, _, _, fromPreset, _, _, _, _, _, _) = fromEntry else { return .single(false) } var referenceFilter: ChatListFilter? @@ -667,7 +719,7 @@ public func chatListFilterPresetListController(context: AccountContext, mode: Ch var afterAll = false if toIndex < entries.count { switch entries[toIndex] { - case let .preset(_, _, _, preset, _, _, _, _, _): + case let .preset(_, _, _, preset, _, _, _, _, _, _): referenceFilter = preset default: if entries[toIndex] < fromEntry { @@ -744,6 +796,29 @@ public func chatListFilterPresetListController(context: AccountContext, mode: Ch } }) }) + controller.afterTransactionCompleted = { [weak controller] in + guard let toggleDirection = animateNextShowHideTagsTransition.swap(nil) else { + return + } + + guard let controller else { + return + } + var presetItemNodes: [ChatListFilterPresetListItemNode] = [] + controller.forEachItemNode { itemNode in + if let itemNode = itemNode as? ChatListFilterPresetListItemNode { + presetItemNodes.append(itemNode) + } + } + + var delay: Double = 0.0 + for itemNode in presetItemNodes.reversed() { + if toggleDirection { + itemNode.animateTagColorIn(delay: delay) + } + delay += 0.02 + } + } return controller } diff --git a/submodules/ChatListUI/Sources/ChatListFilterPresetListItem.swift b/submodules/ChatListUI/Sources/ChatListFilterPresetListItem.swift index 609c9ae333e..4af495c8a96 100644 --- a/submodules/ChatListUI/Sources/ChatListFilterPresetListItem.swift +++ b/submodules/ChatListUI/Sources/ChatListFilterPresetListItem.swift @@ -19,6 +19,7 @@ final class ChatListFilterPresetListItem: ListViewItem, ItemListItem { let preset: ChatListFilter let title: String let label: String + let tagColor: UIColor? let editing: ChatListFilterPresetListItemEditing let canBeReordered: Bool let canBeDeleted: Bool @@ -34,6 +35,7 @@ final class ChatListFilterPresetListItem: ListViewItem, ItemListItem { preset: ChatListFilter, title: String, label: String, + tagColor: UIColor?, editing: ChatListFilterPresetListItemEditing, canBeReordered: Bool, canBeDeleted: Bool, @@ -48,6 +50,7 @@ final class ChatListFilterPresetListItem: ListViewItem, ItemListItem { self.preset = preset self.title = title self.label = label + self.tagColor = tagColor self.editing = editing self.canBeReordered = canBeReordered self.canBeDeleted = canBeDeleted @@ -109,7 +112,7 @@ final class ChatListFilterPresetListItem: ListViewItem, ItemListItem { private let titleFont = Font.regular(17.0) -private final class ChatListFilterPresetListItemNode: ItemListRevealOptionsItemNode { +final class ChatListFilterPresetListItemNode: ItemListRevealOptionsItemNode { private let backgroundNode: ASDisplayNode private let topStripeNode: ASDisplayNode private let bottomStripeNode: ASDisplayNode @@ -125,6 +128,7 @@ private final class ChatListFilterPresetListItemNode: ItemListRevealOptionsItemN private let labelNode: TextNode private let arrowNode: ASImageNode private let sharedIconNode: ASImageNode + private var tagIconView: UIImageView? private let activateArea: AccessibilityAreaNode @@ -173,7 +177,7 @@ private final class ChatListFilterPresetListItemNode: ItemListRevealOptionsItemN self.sharedIconNode.displayWithoutProcessing = true self.sharedIconNode.displaysAsynchronously = false self.sharedIconNode.isLayerBacked = true - + self.activateArea = AccessibilityAreaNode() self.highlightedBackgroundNode = ASDisplayNode() @@ -214,6 +218,7 @@ private final class ChatListFilterPresetListItemNode: ItemListRevealOptionsItemN } else { updateArrowImage = PresentationResourcesItemList.disclosureArrowImage(item.presentationData.theme) } + updatedSharedIconImage = generateTintedImage(image: UIImage(bundleImageName: "Chat List/SharedFolderListIcon"), color: item.presentationData.theme.list.disclosureArrowColor) } @@ -406,7 +411,15 @@ private final class ChatListFilterPresetListItemNode: ItemListRevealOptionsItemN strongSelf.arrowNode.isHidden = item.isAllChats if let sharedIconImage = strongSelf.sharedIconNode.image { - strongSelf.sharedIconNode.frame = CGRect(origin: CGPoint(x: strongSelf.arrowNode.frame.minX + 2.0 - sharedIconImage.size.width, y: floorToScreenPixels((layout.contentSize.height - sharedIconImage.size.height) / 2.0) + 1.0), size: sharedIconImage.size) + var sharedIconFrame = CGRect(origin: CGPoint(x: strongSelf.arrowNode.frame.minX + 2.0 - sharedIconImage.size.width, y: floorToScreenPixels((layout.contentSize.height - sharedIconImage.size.height) / 2.0) + 1.0), size: sharedIconImage.size) + if item.tagColor != nil { + sharedIconFrame.origin.x -= 34.0 + } + if strongSelf.sharedIconNode.bounds.isEmpty { + strongSelf.sharedIconNode.frame = sharedIconFrame + } else { + transition.updateFrame(node: strongSelf.sharedIconNode, frame: sharedIconFrame) + } } var isShared = false if case let .filter(_, _, _, data) = item.preset, data.isShared { @@ -414,6 +427,33 @@ private final class ChatListFilterPresetListItemNode: ItemListRevealOptionsItemN } strongSelf.sharedIconNode.isHidden = !isShared + if let tagColor = item.tagColor { + let tagIconView: UIImageView + var tagIconTransition = transition + if let current = strongSelf.tagIconView { + tagIconView = current + } else { + tagIconTransition = .immediate + tagIconView = UIImageView(image: generateStretchableFilledCircleImage(diameter: 24.0, color: .white)?.withRenderingMode(.alwaysTemplate)) + strongSelf.tagIconView = tagIconView + strongSelf.containerNode.view.addSubview(tagIconView) + } + tagIconView.tintColor = tagColor + + let tagIconFrame = CGRect(origin: CGPoint(x: strongSelf.arrowNode.frame.minX - 2.0 - 24.0, y: floorToScreenPixels((layout.contentSize.height - 24.0) / 2.0)), size: CGSize(width: 24.0, height: 24.0)) + + tagIconTransition.updateAlpha(layer: tagIconView.layer, alpha: reorderControlSizeAndApply != nil ? 0.0 : 1.0) + tagIconTransition.updateFrame(view: tagIconView, frame: tagIconFrame) + } else { + if let tagIconView = strongSelf.tagIconView { + strongSelf.tagIconView = nil + transition.updateAlpha(layer: tagIconView.layer, alpha: 0.0, completion: { [weak tagIconView] _ in + tagIconView?.removeFromSuperview() + }) + transition.updateTransformScale(layer: tagIconView.layer, scale: 0.001) + } + } + strongSelf.activateArea.frame = CGRect(origin: CGPoint(x: leftInset + revealOffset + editingOffset, y: 0.0), size: CGSize(width: params.width - params.rightInset - 56.0 - (leftInset + revealOffset + editingOffset), height: layout.contentSize.height)) strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: params.width, height: layout.contentSize.height + UIScreenPixel + UIScreenPixel)) @@ -427,6 +467,13 @@ private final class ChatListFilterPresetListItemNode: ItemListRevealOptionsItemN } } + func animateTagColorIn(delay: Double) { + if let tagIconView = self.tagIconView { + tagIconView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.12, delay: delay) + tagIconView.layer.animateSpring(from: 0.001 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.4, delay: delay) + } + } + override func setHighlighted(_ highlighted: Bool, at point: CGPoint, animated: Bool) { super.setHighlighted(highlighted, at: point, animated: animated) @@ -507,7 +554,16 @@ private final class ChatListFilterPresetListItemNode: ItemListRevealOptionsItemN var sharedIconFrame = self.sharedIconNode.frame sharedIconFrame.origin.x = arrowFrame.minX + 2.0 - sharedIconFrame.width + if self.item?.tagColor != nil { + sharedIconFrame.origin.x -= 34.0 + } transition.updateFrame(node: self.sharedIconNode, frame: sharedIconFrame) + + if let tagIconView = self.tagIconView { + var tagIconFrame = tagIconView.frame + tagIconFrame.origin.x = arrowFrame.minX - 2.0 - tagIconFrame.width + transition.updateFrame(view: tagIconView, frame: tagIconFrame) + } } override func revealOptionsInteractivelyOpened() { diff --git a/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift b/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift index 2a6b2cc4f5c..0ac6bf755af 100644 --- a/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift +++ b/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift @@ -2324,6 +2324,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { self.interaction.openStories?(id, sourceNode.avatarNode) } }, dismissNotice: { _ in + }, editPeer: { _ in }) chatListInteraction.isSearchMode = true @@ -3689,6 +3690,7 @@ public final class ChatListSearchShimmerNode: ASDisplayNode { }, openChatFolderUpdates: {}, hideChatFolderUpdates: { }, openStories: { _, _ in }, dismissNotice: { _ in + }, editPeer: { _ in }) var isInlineMode = false if case .topics = key { diff --git a/submodules/ChatListUI/Sources/ChatListShimmerNode.swift b/submodules/ChatListUI/Sources/ChatListShimmerNode.swift index 760ec065918..109edbc4db1 100644 --- a/submodules/ChatListUI/Sources/ChatListShimmerNode.swift +++ b/submodules/ChatListUI/Sources/ChatListShimmerNode.swift @@ -157,6 +157,7 @@ public final class ChatListShimmerNode: ASDisplayNode { }, messageSelected: { _, _, _, _ in}, groupSelected: { _ in }, addContact: { _ in }, setPeerIdWithRevealedOptions: { _, _ in }, setItemPinned: { _, _ in }, setPeerMuted: { _, _ in }, setPeerThreadMuted: { _, _, _ in }, deletePeer: { _, _ in }, deletePeerThread: { _, _ in }, setPeerThreadStopped: { _, _, _ in }, setPeerThreadPinned: { _, _, _ in }, setPeerThreadHidden: { _, _, _ in }, updatePeerGrouping: { _, _ in }, togglePeerMarkedUnread: { _, _ in}, toggleArchivedFolderHiddenByDefault: {}, toggleThreadsSelection: { _, _ in }, hidePsa: { _ in }, activateChatPreview: { _, _, _, gesture, _ in gesture?.cancel() }, present: { _ in }, openForumThread: { _, _ in }, openStorageManagement: {}, openPasswordSetup: {}, openPremiumIntro: {}, openPremiumGift: {}, openActiveSessions: {}, performActiveSessionAction: { _, _ in }, openChatFolderUpdates: {}, hideChatFolderUpdates: {}, openStories: { _, _ in }, dismissNotice: { _ in + }, editPeer: { _ in }) interaction.isInlineMode = isInlineMode diff --git a/submodules/ChatListUI/Sources/Node/ChatListItem.swift b/submodules/ChatListUI/Sources/Node/ChatListItem.swift index 6949eac27f8..5b2ffa3c124 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListItem.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListItem.swift @@ -30,6 +30,8 @@ import TextNodeWithEntities import ComponentFlow import EmojiStatusComponent import AvatarVideoNode +import AppBundle +import MultilineTextComponent public enum ChatListItemContent { public struct ThreadInfo: Equatable { @@ -93,6 +95,20 @@ public enum ChatListItemContent { } } + public struct CustomMessageListData: Equatable { + public var commandPrefix: String? + public var searchQuery: String? + public var messageCount: Int? + public var hideSeparator: Bool + + public init(commandPrefix: String?, searchQuery: String?, messageCount: Int?, hideSeparator: Bool) { + self.commandPrefix = commandPrefix + self.searchQuery = searchQuery + self.messageCount = messageCount + self.hideSeparator = hideSeparator + } + } + public struct PeerData { public var messages: [EngineMessage] public var peer: EngineRenderedPeer @@ -116,6 +132,7 @@ public enum ChatListItemContent { public var requiresPremiumForMessaging: Bool public var displayAsTopicList: Bool public var tags: [Tag] + public var customMessageListData: CustomMessageListData? public init( messages: [EngineMessage], @@ -139,7 +156,8 @@ public enum ChatListItemContent { storyState: StoryState?, requiresPremiumForMessaging: Bool, displayAsTopicList: Bool, - tags: [Tag] + tags: [Tag], + customMessageListData: CustomMessageListData? = nil ) { self.messages = messages self.peer = peer @@ -163,6 +181,7 @@ public enum ChatListItemContent { self.requiresPremiumForMessaging = requiresPremiumForMessaging self.displayAsTopicList = displayAsTopicList self.tags = tags + self.customMessageListData = customMessageListData } } @@ -212,15 +231,18 @@ private final class ChatListItemTagListComponent: Component { let context: AccountContext let tags: [ChatListItemContent.Tag] let theme: PresentationTheme + let sizeFactor: CGFloat init( context: AccountContext, tags: [ChatListItemContent.Tag], - theme: PresentationTheme + theme: PresentationTheme, + sizeFactor: CGFloat ) { self.context = context self.tags = tags self.theme = theme + self.sizeFactor = sizeFactor } static func ==(lhs: ChatListItemTagListComponent, rhs: ChatListItemTagListComponent) -> Bool { @@ -233,6 +255,9 @@ private final class ChatListItemTagListComponent: Component { if lhs.theme !== rhs.theme { return false } + if lhs.sizeFactor != rhs.sizeFactor { + return false + } return true } @@ -252,16 +277,18 @@ private final class ChatListItemTagListComponent: Component { preconditionFailure() } - func update(context: AccountContext, title: String, backgroundColor: UIColor, foregroundColor: UIColor) -> CGSize { + func update(context: AccountContext, title: String, backgroundColor: UIColor, foregroundColor: UIColor, sizeFactor: CGFloat) -> CGSize { let titleSize = self.title.update( transition: .immediate, - component: AnyComponent(Text(text: title.isEmpty ? " " : title, font: Font.semibold(11.0), color: foregroundColor)), + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: title.isEmpty ? " " : title, font: Font.semibold(floor(11.0 * sizeFactor)), textColor: foregroundColor)) + )), environment: {}, containerSize: CGSize(width: 100.0, height: 100.0) ) - let backgroundSideInset: CGFloat = 4.0 - let backgroundVerticalInset: CGFloat = 2.0 + let backgroundSideInset: CGFloat = floorToScreenPixels(4.0 * sizeFactor) + let backgroundVerticalInset: CGFloat = floorToScreenPixels(2.0 * sizeFactor) let backgroundSize = CGSize(width: titleSize.width + backgroundSideInset * 2.0, height: titleSize.height + backgroundVerticalInset * 2.0) let backgroundFrame = CGRect(origin: CGPoint(), size: backgroundSize) @@ -293,7 +320,7 @@ private final class ChatListItemTagListComponent: Component { func update(component: ChatListItemTagListComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { var validIds: [Int32] = [] - let spacing: CGFloat = 5.0 + let spacing: CGFloat = floorToScreenPixels(5.0 * component.sizeFactor) var nextX: CGFloat = 0.0 for tag in component.tags { if nextX != 0.0 { @@ -314,7 +341,7 @@ private final class ChatListItemTagListComponent: Component { itemId = tag.id let tagColor = PeerNameColor(rawValue: tag.colorId) - let resolvedColor = component.context.peerNameColors.getProfile(tagColor, dark: component.theme.overallDarkAppearance, subject: .palette) + let resolvedColor = component.context.peerNameColors.getChatFolderTag(tagColor, dark: component.theme.overallDarkAppearance) itemTitle = tag.title.uppercased() itemBackgroundColor = resolvedColor.main.withMultipliedAlpha(0.1) @@ -330,7 +357,7 @@ private final class ChatListItemTagListComponent: Component { self.addSubview(itemView) } - let itemSize = itemView.update(context: component.context, title: itemTitle, backgroundColor: itemBackgroundColor, foregroundColor: itemForegroundColor) + let itemSize = itemView.update(context: component.context, title: itemTitle, backgroundColor: itemBackgroundColor, foregroundColor: itemForegroundColor, sizeFactor: component.sizeFactor) let itemFrame = CGRect(origin: CGPoint(x: nextX, y: 0.0), size: itemSize) itemView.frame = itemFrame @@ -566,6 +593,7 @@ private enum RevealOptionKey: Int32 { case hidePsa case open case close + case edit } private func canArchivePeer(id: EnginePeer.Id, accountPeerId: EnginePeer.Id) -> Bool { @@ -806,8 +834,8 @@ private let playIconImage = UIImage(bundleImageName: "Chat List/MiniThumbnailPla private final class ChatListMediaPreviewNode: ASDisplayNode { private let context: AccountContext - private let message: EngineMessage - private let media: EngineMedia + let message: EngineMessage + let media: EngineMedia private let imageNode: TransformImageNode private let playIcon: ASImageNode @@ -868,10 +896,14 @@ private final class ChatListMediaPreviewNode: ASDisplayNode { if file.isInstantVideo { isRound = true } - if file.isAnimated { + if file.isSticker || file.isAnimatedSticker { self.playIcon.isHidden = true } else { - self.playIcon.isHidden = false + if file.isAnimated { + self.playIcon.isHidden = true + } else { + self.playIcon.isHidden = false + } } if let mediaDimensions = file.dimensions { dimensions = mediaDimensions.cgSize @@ -883,9 +915,18 @@ private final class ChatListMediaPreviewNode: ASDisplayNode { } } + let radius: CGFloat + if isRound { + radius = size.width / 2.0 + } else if size.width >= 30.0 { + radius = 8.0 + } else { + radius = 2.0 + } + let makeLayout = self.imageNode.asyncLayout() self.imageNode.frame = CGRect(origin: CGPoint(), size: size) - let apply = makeLayout(TransformImageArguments(corners: ImageCorners(radius: isRound ? size.width / 2.0 : 2.0), imageSize: dimensions.aspectFilled(size), boundingSize: size, intrinsicInsets: UIEdgeInsets())) + let apply = makeLayout(TransformImageArguments(corners: ImageCorners(radius: radius), imageSize: dimensions.aspectFilled(size), boundingSize: size, intrinsicInsets: UIEdgeInsets())) apply() } } @@ -1148,6 +1189,18 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { } } + private struct ContentImageSpec { + var message: EngineMessage + var media: EngineMedia + var size: CGSize + + init(message: EngineMessage, media: EngineMedia, size: CGSize) { + self.message = message + self.media = media + self.size = size + } + } + var item: ChatListItem? private let backgroundNode: ASDisplayNode @@ -1162,6 +1215,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { var avatarIconComponent: EmojiStatusComponent? var avatarVideoNode: AvatarVideoNode? var avatarTapRecognizer: UITapGestureRecognizer? + private var avatarMediaNode: ChatListMediaPreviewNode? private var inlineNavigationMarkLayer: SimpleLayer? @@ -1174,10 +1228,13 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { private var currentItemHeight: CGFloat? let forwardedIconNode: ASImageNode let textNode: TextNodeWithEntities + var trailingTextBadgeNode: TextNode? + var trailingTextBadgeBackground: UIImageView? var dustNode: InvisibleInkDustNode? let inputActivitiesNode: ChatListInputActivitiesNode let dateNode: TextNode var dateStatusIconNode: ASImageNode? + var dateDisclosureIconView: UIImageView? let separatorNode: ASDisplayNode let statusNode: ChatListStatusNode let badgeNode: ChatListBadgeNode @@ -1199,7 +1256,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { private var cachedDataDisposable = MetaDisposable() private var currentTextLeftCutout: CGFloat = 0.0 - private var currentMediaPreviewSpecs: [(message: EngineMessage, media: EngineMedia, size: CGSize)] = [] + private var currentMediaPreviewSpecs: [ContentImageSpec] = [] private var mediaPreviewNodes: [EngineMedia.Id: ChatListMediaPreviewNode] = [:] var selectableControlNode: ItemListSelectableControlNode? @@ -1444,6 +1501,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { self.textNode = TextNodeWithEntities() self.textNode.textNode.isUserInteractionEnabled = false self.textNode.textNode.displaysAsynchronously = true + self.textNode.textNode.anchorPoint = CGPoint() self.inputActivitiesNode = ChatListInputActivitiesNode() self.inputActivitiesNode.isUserInteractionEnabled = false @@ -1624,7 +1682,8 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { if let peer = peer { var overrideImage: AvatarNodeImageOverride? - if peer.id.isReplies { + if case let .peer(peerData) = item.content, peerData.customMessageListData != nil { + } else if peer.id.isReplies { overrideImage = .repliesIcon } else if peer.id.isAnonymousSavedMessages { overrideImage = .anonymousSavedMessagesIcon @@ -1646,7 +1705,19 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { isForumAvatar = true } } + + var avatarDiameter = min(60.0, floor(item.presentationData.fontSize.baseDisplaySize * 60.0 / 17.0)) + + if case let .peer(peerData) = item.content, let customMessageListData = peerData.customMessageListData, customMessageListData.commandPrefix != nil { + avatarDiameter = 40.0 + } + if avatarDiameter != 60.0 { + let avatarFontSize = floor(avatarDiameter * 26.0 / 60.0) + if self.avatarNode.font.pointSize != avatarFontSize { + self.avatarNode.font = avatarPlaceholderFont(size: avatarFontSize) + } + } self.avatarNode.setPeer(context: item.context, theme: item.presentationData.theme, peer: peer, overrideImage: overrideImage, emptyColor: item.presentationData.theme.list.mediaPlaceholderColor, clipStyle: isForumAvatar ? .roundedRect : .round, synchronousLoad: synchronousLoads, displayDimensions: CGSize(width: 60.0, height: 60.0)) if peer.isPremium && peer.id != item.context.account.peerId { @@ -1842,6 +1913,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { func asyncLayout() -> (_ item: ChatListItem, _ params: ListViewItemLayoutParams, _ first: Bool, _ last: Bool, _ firstWithHeader: Bool, _ nextIsPinned: Bool) -> (ListViewItemNodeLayout, (Bool, Bool) -> Void) { let dateLayout = TextNode.asyncLayout(self.dateNode) let textLayout = TextNodeWithEntities.asyncLayout(self.textNode) + let makeTrailingTextBadgeLayout = TextNode.asyncLayout(self.trailingTextBadgeNode) let titleLayout = TextNode.asyncLayout(self.titleNode) let authorLayout = self.authorNode.asyncLayout() let makeMeasureLayout = TextNode.asyncLayout(self.measureNode) @@ -2045,26 +2117,41 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { let editingOffset: CGFloat var reorderInset: CGFloat = 0.0 if item.editing { - let sizeAndApply = selectableControlLayout(item.presentationData.theme.list.itemCheckColors.strokeColor, item.presentationData.theme.list.itemCheckColors.fillColor, item.presentationData.theme.list.itemCheckColors.foregroundColor, item.selected, true) + let selectionControlStyle: ItemListSelectableControlNode.Style + if case let .peer(peerData) = item.content, let customMessageListData = peerData.customMessageListData, customMessageListData.commandPrefix != nil { + selectionControlStyle = .small + } else { + selectionControlStyle = .compact + } + + let sizeAndApply = selectableControlLayout(item.presentationData.theme.list.itemCheckColors.strokeColor, item.presentationData.theme.list.itemCheckColors.fillColor, item.presentationData.theme.list.itemCheckColors.foregroundColor, item.selected, selectionControlStyle) if promoInfo == nil && !isPeerGroup { selectableControlSizeAndApply = sizeAndApply } editingOffset = sizeAndApply.0 + var canReorder = false + if case let .chatList(index) = item.index, index.pinningIndex != nil, promoInfo == nil, !isPeerGroup { - let sizeAndApply = reorderControlLayout(item.presentationData.theme) - reorderControlSizeAndApply = sizeAndApply - reorderInset = sizeAndApply.0 + canReorder = true } else if case let .forum(pinnedIndex, _, _, _, _) = item.index, case .index = pinnedIndex { if case let .chat(itemPeer) = contentPeer, case let .channel(channel) = itemPeer.peer { let canPin = channel.flags.contains(.isCreator) || channel.hasPermission(.pinMessages) if canPin { - let sizeAndApply = reorderControlLayout(item.presentationData.theme) - reorderControlSizeAndApply = sizeAndApply - reorderInset = sizeAndApply.0 + canReorder = true } } } + + if case let .peer(peerData) = item.content, let customMessageListData = peerData.customMessageListData, customMessageListData.commandPrefix != nil { + canReorder = true + } + + if canReorder { + let sizeAndApply = reorderControlLayout(item.presentationData.theme) + reorderControlSizeAndApply = sizeAndApply + reorderInset = sizeAndApply.0 + } } else { editingOffset = 0.0 } @@ -2076,15 +2163,21 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { let enableChatListPhotos = true - let avatarDiameter = min(60.0, floor(item.presentationData.fontSize.baseDisplaySize * 60.0 / 17.0)) - + // if changed, adjust setupItem accordingly + var avatarDiameter = min(60.0, floor(item.presentationData.fontSize.baseDisplaySize * 60.0 / 17.0)) let avatarLeftInset: CGFloat - if item.interaction.isInlineMode { - avatarLeftInset = 12.0 - } else if !useChatListLayout { - avatarLeftInset = 50.0 + + if case let .peer(peerData) = item.content, let customMessageListData = peerData.customMessageListData, customMessageListData.commandPrefix != nil { + avatarDiameter = 40.0 + avatarLeftInset = 17.0 + avatarDiameter } else { - avatarLeftInset = 18.0 + avatarDiameter + if item.interaction.isInlineMode { + avatarLeftInset = 12.0 + } else if !useChatListLayout { + avatarLeftInset = 50.0 + } else { + avatarLeftInset = 18.0 + avatarDiameter + } } let badgeDiameter = floor(item.presentationData.fontSize.baseDisplaySize * 20.0 / 17.0) @@ -2138,7 +2231,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { hideAuthor = true } - let attributedText: NSAttributedString + var attributedText: NSAttributedString var hasDraft = false var inlineAuthorPrefix: String? @@ -2147,13 +2240,23 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { useInlineAuthorPrefix = true } if !itemTags.isEmpty { - if case let .chat(peer) = contentPeer, peer.peerId == item.context.account.peerId { - } else { - useInlineAuthorPrefix = true - } - forumTopicData = nil topForumTopicItems = [] + + if case let .chat(itemPeer, _, _, _, _, _, _) = contentData { + if let messagePeer = itemPeer.chatMainPeer { + switch messagePeer { + case let .channel(channel): + if case .group = channel.info { + useInlineAuthorPrefix = true + } + case .legacyGroup: + useInlineAuthorPrefix = true + default: + break + } + } + } } if useInlineAuthorPrefix { @@ -2176,7 +2279,9 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { let contentImageSpacing: CGFloat = 2.0 let forwardedIconSpacing: CGFloat = 6.0 let contentImageTrailingSpace: CGFloat = 5.0 - var contentImageSpecs: [(message: EngineMessage, media: EngineMedia, size: CGSize)] = [] + + var contentImageSpecs: [ContentImageSpec] = [] + var avatarContentImageSpec: ContentImageSpec? var forumThread: (id: Int64, title: String, iconId: Int64?, iconColor: Int32, isUnread: Bool)? var displayForwardedIcon = false @@ -2279,20 +2384,27 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { hasDraft = true authorAttributedString = NSAttributedString(string: item.presentationData.strings.DialogList_Draft, font: textFont, textColor: theme.messageDraftTextColor) - //TODO:localize switch mediaDraftContentType { case .audio: - attributedText = NSAttributedString(string: "Voice Message", font: textFont, textColor: theme.messageTextColor) + attributedText = NSAttributedString(string: item.presentationData.strings.Message_Audio, font: textFont, textColor: theme.messageTextColor) case .video: - attributedText = NSAttributedString(string: "Video Message", font: textFont, textColor: theme.messageTextColor) + attributedText = NSAttributedString(string: item.presentationData.strings.Message_VideoMessage, font: textFont, textColor: theme.messageTextColor) } } else if inlineAuthorPrefix == nil, let draftState = draftState { hasDraft = true - authorAttributedString = NSAttributedString(string: item.presentationData.strings.DialogList_Draft, font: textFont, textColor: theme.messageDraftTextColor) - let draftText = stringWithAppliedEntities(draftState.text, entities: draftState.entities, baseColor: theme.messageTextColor, linkColor: theme.messageTextColor, baseFont: textFont, linkFont: textFont, boldFont: textFont, italicFont: textFont, boldItalicFont: textFont, fixedFont: textFont, blockQuoteFont: textFont, message: nil) - attributedText = foldLineBreaks(draftText) + if !itemTags.isEmpty { + let tempAttributedText = foldLineBreaks(draftText) + let attributedTextWithDraft = NSMutableAttributedString() + attributedTextWithDraft.append(NSAttributedString(string: item.presentationData.strings.DialogList_Draft + ": ", font: textFont, textColor: theme.messageDraftTextColor)) + attributedTextWithDraft.append(tempAttributedText) + attributedText = attributedTextWithDraft + } else { + authorAttributedString = NSAttributedString(string: item.presentationData.strings.DialogList_Draft, font: textFont, textColor: theme.messageDraftTextColor) + + attributedText = foldLineBreaks(draftText) + } } else if let message = messages.first { var composedString: NSMutableAttributedString @@ -2472,6 +2584,19 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { attributedText = composedString + if case let .peer(peerData) = item.content, let customMessageListData = peerData.customMessageListData, let commandPrefix = customMessageListData.commandPrefix { + let mutableAttributedText = NSMutableAttributedString(attributedString: attributedText) + let boldTextFont = Font.semibold(floor(item.presentationData.fontSize.itemListBaseFontSize * 15.0 / 17.0)) + mutableAttributedText.insert(NSAttributedString(string: commandPrefix + " ", font: boldTextFont, textColor: theme.titleColor), at: 0) + if let searchQuery = customMessageListData.searchQuery { + let range = (mutableAttributedText.string as NSString).range(of: searchQuery) + if range.location == 0 { + mutableAttributedText.addAttribute(.foregroundColor, value: item.presentationData.theme.list.itemAccentColor, range: range) + } + } + attributedText = mutableAttributedText + } + if !ignoreForwardedIcon { if case .savedMessagesChats = item.chatListLocation { displayForwardedIcon = false @@ -2491,21 +2616,31 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { if displayMediaPreviews { let contentImageFillSize = CGSize(width: 8.0, height: contentImageSize.height) _ = contentImageFillSize + + var contentImageIsDisplayedAsAvatar = false + if case let .peer(peerData) = item.content, let customMessageListData = peerData.customMessageListData, customMessageListData.commandPrefix != nil { + contentImageIsDisplayedAsAvatar = true + } + for message in messages { if contentImageSpecs.count >= 3 { break } + inner: for media in message.media { if let image = media as? TelegramMediaImage { if let _ = largestImageRepresentation(image.representations) { let fitSize = contentImageSize - contentImageSpecs.append((message, .image(image), fitSize)) + contentImageSpecs.append(ContentImageSpec(message: message, media: .image(image), size: fitSize)) } break inner } else if let file = media as? TelegramMediaFile { if file.isVideo, !file.isVideoSticker, let _ = file.dimensions { let fitSize = contentImageSize - contentImageSpecs.append((message, .file(file), fitSize)) + contentImageSpecs.append(ContentImageSpec(message: message, media: .file(file), size: fitSize)) + } else if contentImageIsDisplayedAsAvatar && (file.isSticker || file.isVideoSticker) { + let fitSize = contentImageSize + contentImageSpecs.append(ContentImageSpec(message: message, media: .file(file), size: fitSize)) } break inner } else if let webpage = media as? TelegramMediaWebpage, case let .Loaded(content) = webpage.content { @@ -2513,36 +2648,41 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { if let image = content.image, let type = content.type, imageTypes.contains(type) { if let _ = largestImageRepresentation(image.representations) { let fitSize = contentImageSize - contentImageSpecs.append((message, .image(image), fitSize)) + contentImageSpecs.append(ContentImageSpec(message: message, media: .image(image), size: fitSize)) } break inner } else if let file = content.file { if file.isVideo, !file.isInstantVideo, let _ = file.dimensions { let fitSize = contentImageSize - contentImageSpecs.append((message, .file(file), fitSize)) + contentImageSpecs.append(ContentImageSpec(message: message, media: .file(file), size: fitSize)) } break inner } } else if let action = media as? TelegramMediaAction, case let .suggestedProfilePhoto(image) = action.action, let _ = image { let fitSize = contentImageSize - contentImageSpecs.append((message, .action(action), fitSize)) + contentImageSpecs.append(ContentImageSpec(message: message, media: .action(action), size: fitSize)) } else if let storyMedia = media as? TelegramMediaStory, let story = message.associatedStories[storyMedia.storyId], !story.data.isEmpty, case let .item(storyItem) = story.get(Stories.StoredItem.self) { if let image = storyItem.media as? TelegramMediaImage { if let _ = largestImageRepresentation(image.representations) { let fitSize = contentImageSize - contentImageSpecs.append((message, .image(image), fitSize)) + contentImageSpecs.append(ContentImageSpec(message: message, media: .image(image), size: fitSize)) } break inner } else if let file = storyItem.media as? TelegramMediaFile { if file.isVideo, !file.isInstantVideo, let _ = file.dimensions { let fitSize = contentImageSize - contentImageSpecs.append((message, .file(file), fitSize)) + contentImageSpecs.append(ContentImageSpec(message: message, media: .file(file), size: fitSize)) } break inner } } } } + + if contentImageIsDisplayedAsAvatar { + avatarContentImageSpec = contentImageSpecs.first + contentImageSpecs.removeAll() + } } } else { attributedText = NSAttributedString(string: messageText, font: textFont, textColor: theme.messageTextColor) @@ -2619,7 +2759,21 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { switch contentData { case let .chat(itemPeer, threadInfo, _, _, _, _, _): - if let threadInfo = threadInfo { + if case let .peer(peerData) = item.content, let customMessageListData = peerData.customMessageListData { + if customMessageListData.commandPrefix != nil { + titleAttributedString = nil + } else { + if let displayTitle = itemPeer.chatMainPeer?.displayTitle(strings: item.presentationData.strings, displayOrder: item.presentationData.nameDisplayOrder) { + let textColor: UIColor + if case let .chatList(index) = item.index, index.messageIndex.id.peerId.namespace == Namespaces.Peer.SecretChat { + textColor = theme.secretTitleColor + } else { + textColor = theme.titleColor + } + titleAttributedString = NSAttributedString(string: displayTitle, font: titleFont, textColor: textColor) + } + } + } else if let threadInfo = threadInfo { titleAttributedString = NSAttributedString(string: threadInfo.info.title, font: titleFont, textColor: theme.titleColor) } else if let message = messages.last, case let .user(author) = message.author, displayAsMessage { titleAttributedString = NSAttributedString(string: author.id == account.peerId ? item.presentationData.strings.DialogList_You : EnginePeer.user(author).displayTitle(strings: item.presentationData.strings, displayOrder: item.presentationData.nameDisplayOrder), font: titleFont, textColor: theme.titleColor) @@ -2658,7 +2812,13 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { case let .peer(peerData): topIndex = peerData.messages.first?.index } - if let topIndex { + if case let .peer(peerData) = item.content, let customMessageListData = peerData.customMessageListData { + if let messageCount = customMessageListData.messageCount, customMessageListData.commandPrefix == nil { + dateText = "\(messageCount)" + } else { + dateText = " " + } + } else if let topIndex { var t = Int(topIndex.timestamp) var timeinfo = tm() localtime_r(&t, &timeinfo) @@ -2827,7 +2987,9 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { switch item.content { case let .peer(peerData): if let peer = peerData.messages.last?.author { - if case .savedMessagesChats = item.chatListLocation, peer.id == item.context.account.peerId { + if case let .peer(peerData) = item.content, peerData.customMessageListData != nil { + currentCredibilityIconContent = nil + } else if case .savedMessagesChats = item.chatListLocation, peer.id == item.context.account.peerId { currentCredibilityIconContent = nil } else if peer.isScam { currentCredibilityIconContent = .text(color: item.presentationData.theme.chat.message.incoming.scamColor, string: item.presentationData.strings.Message_ScamAccount.uppercased()) @@ -2849,7 +3011,9 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { break } } else if case let .chat(itemPeer) = contentPeer, let peer = itemPeer.chatMainPeer { - if case .savedMessagesChats = item.chatListLocation, peer.id == item.context.account.peerId { + if case let .peer(peerData) = item.content, peerData.customMessageListData != nil { + currentCredibilityIconContent = nil + } else if case .savedMessagesChats = item.chatListLocation, peer.id == item.context.account.peerId { currentCredibilityIconContent = nil } else if peer.isScam { currentCredibilityIconContent = .text(color: item.presentationData.theme.chat.message.incoming.scamColor, string: item.presentationData.strings.Message_ScamAccount.uppercased()) @@ -2984,9 +3148,22 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { let (authorLayout, authorApply) = authorLayout(item.context, rawContentWidth - badgeSize, item.presentationData.theme, effectiveAuthorTitle, forumThreads) + var textBottomRightCutout: CGFloat = 0.0 + + let trailingTextBadgeInsets = UIEdgeInsets(top: 2.0 - UIScreenPixel, left: 5.0, bottom: 2.0 - UIScreenPixel, right: 5.0) + var trailingTextBadgeLayoutAndApply: (TextNodeLayout, () -> TextNode)? + if case let .peer(peerData) = item.content, let customMessageListData = peerData.customMessageListData, customMessageListData.commandPrefix != nil, let messageCount = customMessageListData.messageCount, messageCount > 1 { + let trailingText: String + trailingText = item.presentationData.strings.ChatList_ItemMoreMessagesFormat(Int32(messageCount - 1)) + let trailingAttributedText = NSAttributedString(string: trailingText, font: Font.regular(12.0), textColor: theme.messageTextColor) + let (layout, apply) = makeTrailingTextBadgeLayout(TextNodeLayoutArguments(attributedString: trailingAttributedText, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: rawContentWidth, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + trailingTextBadgeLayoutAndApply = (layout, apply) + textBottomRightCutout += layout.size.width + 4.0 + trailingTextBadgeInsets.left + trailingTextBadgeInsets.right + } + var textCutout: TextNodeCutout? - if !textLeftCutout.isZero { - textCutout = TextNodeCutout(topLeft: CGSize(width: textLeftCutout, height: 10.0), topRight: nil, bottomRight: nil) + if !textLeftCutout.isZero || !textBottomRightCutout.isZero { + textCutout = TextNodeCutout(topLeft: textLeftCutout.isZero ? nil : CGSize(width: textLeftCutout, height: 10.0), topRight: nil, bottomRight: textBottomRightCutout.isZero ? nil : CGSize(width: textBottomRightCutout, height: 10.0)) } var textMaxWidth = rawContentWidth - badgeSize @@ -3128,6 +3305,16 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { ItemListRevealOption(key: RevealOptionKey.hidePsa.rawValue, title: item.presentationData.strings.ChatList_HideAction, icon: deleteIcon, color: item.presentationData.theme.list.itemDisclosureActions.inactive.fillColor, textColor: item.presentationData.theme.list.itemDisclosureActions.neutral1.foregroundColor) ] peerLeftRevealOptions = [] + } else if case let .peer(peerData) = item.content, let customMessageListData = peerData.customMessageListData { + peerLeftRevealOptions = [] + if customMessageListData.commandPrefix != nil { + peerRevealOptions = [ + ItemListRevealOption(key: RevealOptionKey.edit.rawValue, title: item.presentationData.strings.ChatList_ItemMenuEdit, icon: .none, color: item.presentationData.theme.list.itemDisclosureActions.neutral2.fillColor, textColor: item.presentationData.theme.list.itemDisclosureActions.neutral2.foregroundColor), + ItemListRevealOption(key: RevealOptionKey.delete.rawValue, title: item.presentationData.strings.ChatList_ItemMenuDelete, icon: .none, color: item.presentationData.theme.list.itemDisclosureActions.destructive.fillColor, textColor: item.presentationData.theme.list.itemDisclosureActions.destructive.foregroundColor) + ] + } else { + peerRevealOptions = [] + } } else if promoInfo == nil { peerRevealOptions = revealOptions(strings: item.presentationData.strings, theme: item.presentationData.theme, isPinned: isPinned, isMuted: !isAccountPeer ? (currentMutedIconImage != nil) : nil, location: item.chatListLocation, peerId: renderedPeer.peerId, accountPeerId: item.context.account.peerId, canDelete: true, isEditing: item.editing, filterData: item.filterData) if case let .chat(itemPeer) = contentPeer { @@ -3159,16 +3346,21 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { animateContent = true } - let (measureLayout, measureApply) = makeMeasureLayout(TextNodeLayoutArguments(attributedString: titleAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: titleRectWidth, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + let (measureLayout, measureApply) = makeMeasureLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: " ", font: titleFont, textColor: .black), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: titleRectWidth, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) let titleSpacing: CGFloat = -1.0 let authorSpacing: CGFloat = -3.0 var itemHeight: CGFloat = 8.0 * 2.0 + 1.0 itemHeight -= 21.0 - itemHeight += titleLayout.size.height - itemHeight += measureLayout.size.height * 3.0 - itemHeight += titleSpacing - itemHeight += authorSpacing + if case let .peer(peerData) = item.content, let customMessageListData = peerData.customMessageListData, customMessageListData.commandPrefix != nil { + itemHeight += measureLayout.size.height * 2.0 + itemHeight += 20.0 + } else { + itemHeight += titleLayout.size.height + itemHeight += measureLayout.size.height * 3.0 + itemHeight += titleSpacing + itemHeight += authorSpacing + } let rawContentRect = CGRect(origin: CGPoint(x: 2.0, y: layoutOffset + floor(item.presentationData.fontSize.itemListBaseFontSize * 8.0 / 17.0)), size: CGSize(width: rawContentWidth, height: itemHeight - 12.0 - 9.0)) @@ -3541,6 +3733,19 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { let _ = measureApply() let _ = dateApply() + var currentTextSnapshotView: UIView? + if transition.isAnimated, let currentItem, currentItem.editing != item.editing, strongSelf.textNode.textNode.cachedLayout?.linesRects() != textLayout.linesRects() { + if let textSnapshotView = strongSelf.textNode.textNode.view.snapshotContentTree() { + textSnapshotView.layer.anchorPoint = CGPoint() + currentTextSnapshotView = textSnapshotView + strongSelf.textNode.textNode.view.superview?.insertSubview(textSnapshotView, aboveSubview: strongSelf.textNode.textNode.view) + textSnapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak textSnapshotView] _ in + textSnapshotView?.removeFromSuperview() + }) + strongSelf.textNode.textNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.18) + } + } + let _ = textApply(TextNodeWithEntities.Arguments( context: item.context, cache: item.interaction.animationCache, @@ -3559,7 +3764,32 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { let _ = mentionBadgeApply(animateBadges, true) let _ = onlineApply(animateContent && animateOnline) - transition.updateFrame(node: strongSelf.dateNode, frame: CGRect(origin: CGPoint(x: contentRect.origin.x + contentRect.size.width - dateLayout.size.width, y: contentRect.origin.y + 2.0), size: dateLayout.size)) + var dateFrame = CGRect(origin: CGPoint(x: contentRect.origin.x + contentRect.size.width - dateLayout.size.width, y: contentRect.origin.y + 2.0), size: dateLayout.size) + + if case let .peer(peerData) = item.content, let customMessageListData = peerData.customMessageListData, customMessageListData.messageCount != nil, customMessageListData.commandPrefix == nil { + dateFrame.origin.x -= 10.0 + + let dateDisclosureIconView: UIImageView + if let current = strongSelf.dateDisclosureIconView { + dateDisclosureIconView = current + } else { + dateDisclosureIconView = UIImageView(image: UIImage(bundleImageName: "Item List/DisclosureArrow")?.withRenderingMode(.alwaysTemplate)) + strongSelf.dateDisclosureIconView = dateDisclosureIconView + strongSelf.mainContentContainerNode.view.addSubview(dateDisclosureIconView) + } + dateDisclosureIconView.tintColor = item.presentationData.theme.list.disclosureArrowColor + let iconScale: CGFloat = 0.7 + if let image = dateDisclosureIconView.image { + let imageSize = CGSize(width: floor(image.size.width * iconScale), height: floor(image.size.height * iconScale)) + let iconFrame = CGRect(origin: CGPoint(x: contentRect.origin.x + contentRect.size.width - imageSize.width + 4.0, y: floorToScreenPixels(dateFrame.midY - imageSize.height * 0.5)), size: imageSize) + dateDisclosureIconView.frame = iconFrame + } + } else if let dateDisclosureIconView = strongSelf.dateDisclosureIconView { + strongSelf.dateDisclosureIconView = nil + dateDisclosureIconView.removeFromSuperview() + } + + transition.updateFrame(node: strongSelf.dateNode, frame: dateFrame) var statusOffset: CGFloat = 0.0 if let dateIconImage { @@ -3782,13 +4012,71 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { strongSelf.authorNode.assignParentNode(parentNode: nil) } + if let currentTextSnapshotView { + transition.updatePosition(layer: currentTextSnapshotView.layer, position: textNodeFrame.origin) + } + + if let trailingTextBadgeLayoutAndApply { + let badgeSize = CGSize(width: trailingTextBadgeLayoutAndApply.0.size.width + trailingTextBadgeInsets.left + trailingTextBadgeInsets.right, height: trailingTextBadgeLayoutAndApply.0.size.height + trailingTextBadgeInsets.top + trailingTextBadgeInsets.bottom - UIScreenPixel) + + var badgeFrame: CGRect + if textLayout.numberOfLines > 1 { + badgeFrame = CGRect(origin: CGPoint(x: textLayout.trailingLineWidth + 4.0, y: textNodeFrame.height - 3.0 - badgeSize.height), size: badgeSize) + } else { + let firstLineFrame = textLayout.linesRects().first ?? CGRect(origin: CGPoint(), size: textNodeFrame.size) + badgeFrame = CGRect(origin: CGPoint(x: 0.0, y: firstLineFrame.height + 5.0), size: badgeSize) + } + + if badgeFrame.origin.x + badgeFrame.width >= textNodeFrame.width - 2.0 - 10.0 { + badgeFrame.origin.x = textNodeFrame.width - 2.0 - badgeFrame.width + } + + let trailingTextBadgeBackground: UIImageView + if let current = strongSelf.trailingTextBadgeBackground { + trailingTextBadgeBackground = current + } else { + trailingTextBadgeBackground = UIImageView(image: tagBackgroundImage) + strongSelf.trailingTextBadgeBackground = trailingTextBadgeBackground + strongSelf.textNode.textNode.view.addSubview(trailingTextBadgeBackground) + } + trailingTextBadgeBackground.tintColor = theme.pinnedItemBackgroundColor.mixedWith(theme.unreadBadgeInactiveBackgroundColor, alpha: 0.1) + + trailingTextBadgeBackground.frame = badgeFrame + + let trailingTextBadgeFrame = CGRect(origin: CGPoint(x: badgeFrame.minX + trailingTextBadgeInsets.left, y: badgeFrame.minY + trailingTextBadgeInsets.top), size: trailingTextBadgeLayoutAndApply.0.size) + let trailingTextBadgeNode = trailingTextBadgeLayoutAndApply.1() + if strongSelf.trailingTextBadgeNode !== trailingTextBadgeNode { + strongSelf.trailingTextBadgeNode?.removeFromSupernode() + strongSelf.trailingTextBadgeNode = trailingTextBadgeNode + + strongSelf.textNode.textNode.addSubnode(trailingTextBadgeNode) + + trailingTextBadgeNode.layer.anchorPoint = CGPoint() + } + + trailingTextBadgeNode.frame = trailingTextBadgeFrame + } else { + if let trailingTextBadgeNode = strongSelf.trailingTextBadgeNode { + strongSelf.trailingTextBadgeNode = nil + trailingTextBadgeNode.removeFromSupernode() + } + if let trailingTextBadgeBackground = strongSelf.trailingTextBadgeBackground { + strongSelf.trailingTextBadgeBackground = nil + trailingTextBadgeBackground.removeFromSuperview() + } + } + if !itemTags.isEmpty { - let itemTagListFrame = CGRect(origin: CGPoint(x: contentRect.minX, y: contentRect.maxY - 12.0), size: CGSize(width: contentRect.width, height: 20.0)) + let sizeFactor = item.presentationData.fontSize.itemListBaseFontSize / 17.0 + let itemTagListFrame = CGRect(origin: CGPoint(x: contentRect.minX, y: contentRect.minY + measureLayout.size.height * 2.0 + floorToScreenPixels(2.0 * sizeFactor)), size: CGSize(width: contentRect.width, height: floorToScreenPixels(20.0 * sizeFactor))) + + var itemTagListTransition = transition let itemTagList: ComponentView if let current = strongSelf.itemTagList { itemTagList = current } else { + itemTagListTransition = .immediate itemTagList = ComponentView() strongSelf.itemTagList = itemTagList } @@ -3797,7 +4085,8 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { component: AnyComponent(ChatListItemTagListComponent( context: item.context, tags: itemTags, - theme: item.presentationData.theme + theme: item.presentationData.theme, + sizeFactor: sizeFactor )), environment: {}, containerSize: itemTagListFrame.size @@ -3807,7 +4096,8 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { itemTagListView.isUserInteractionEnabled = false strongSelf.mainContentContainerNode.view.addSubview(itemTagListView) } - itemTagListView.frame = itemTagListFrame + + itemTagListTransition.updateFrame(view: itemTagListView, frame: itemTagListFrame) } } else { if let itemTagList = strongSelf.itemTagList { @@ -3926,7 +4216,11 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { } var validMediaIds: [EngineMedia.Id] = [] - for (message, media, mediaSize) in contentImageSpecs { + for spec in contentImageSpecs { + let message = spec.message + let media = spec.media + let mediaSize = spec.size + var mediaId = media.id if mediaId == nil, case let .action(action) = media, case let .suggestedProfilePhoto(image) = action.action { mediaId = image?.id @@ -3965,6 +4259,36 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { strongSelf.currentMediaPreviewSpecs = contentImageSpecs strongSelf.currentTextLeftCutout = textLeftCutout + if let avatarContentImageSpec { + strongSelf.avatarNode.isHidden = true + + if let previous = strongSelf.avatarMediaNode, previous.media != avatarContentImageSpec.media { + strongSelf.avatarMediaNode = nil + previous.removeFromSupernode() + } + + var avatarMediaNodeTransition = transition + let avatarMediaNode: ChatListMediaPreviewNode + if let current = strongSelf.avatarMediaNode { + avatarMediaNode = current + } else { + avatarMediaNodeTransition = .immediate + avatarMediaNode = ChatListMediaPreviewNode(context: item.context, message: avatarContentImageSpec.message, media: avatarContentImageSpec.media) + strongSelf.avatarMediaNode = avatarMediaNode + strongSelf.contextContainer.addSubnode(avatarMediaNode) + } + + avatarMediaNodeTransition.updateFrame(node: avatarMediaNode, frame: avatarFrame) + avatarMediaNode.updateLayout(size: avatarFrame.size, synchronousLoads: synchronousLoads) + } else { + strongSelf.avatarNode.isHidden = false + + if let avatarMediaNode = strongSelf.avatarMediaNode { + strongSelf.avatarMediaNode = nil + avatarMediaNode.removeFromSupernode() + } + } + if !contentDelta.x.isZero || !contentDelta.y.isZero { let titlePosition = strongSelf.titleNode.position transition.animatePosition(node: strongSelf.titleNode, from: CGPoint(x: titlePosition.x - contentDelta.x, y: titlePosition.y - contentDelta.y)) @@ -4090,6 +4414,12 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { transition.updateAlpha(node: strongSelf.separatorNode, alpha: 1.0) } + if case let .peer(peerData) = item.content, let customMessageListData = peerData.customMessageListData { + if customMessageListData.hideSeparator { + strongSelf.separatorNode.isHidden = true + } + } + transition.updateFrame(node: strongSelf.backgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: layout.contentSize.width, height: itemHeight))) let backgroundColor: UIColor let highlightedBackgroundColor: UIColor @@ -4105,7 +4435,11 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { highlightedBackgroundColor = theme.pinnedItemHighlightedBackgroundColor } } else { - backgroundColor = theme.itemBackgroundColor + if case let .peer(peerData) = item.content, peerData.customMessageListData != nil { + backgroundColor = .clear + } else { + backgroundColor = theme.itemBackgroundColor + } highlightedBackgroundColor = theme.itemHighlightedBackgroundColor } @@ -4340,6 +4674,9 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { self.revealOptionsInteractivelyClosed() self.customAnimationInProgress = false } + case RevealOptionKey.edit.rawValue: + item.interaction.editPeer(item) + close = true default: break } diff --git a/submodules/ChatListUI/Sources/Node/ChatListNode.swift b/submodules/ChatListUI/Sources/Node/ChatListNode.swift index a1c0d487420..0ac1958dbfe 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListNode.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListNode.swift @@ -125,6 +125,7 @@ public final class ChatListNodeInteraction { let hideChatFolderUpdates: () -> Void let openStories: (ChatListNode.OpenStoriesSubject, ASDisplayNode?) -> Void let dismissNotice: (ChatListNotice) -> Void + let editPeer: (ChatListItem) -> Void public var searchTextHighightState: String? var highlightedChatLocation: ChatListHighlightedLocation? @@ -179,7 +180,8 @@ public final class ChatListNodeInteraction { openChatFolderUpdates: @escaping () -> Void, hideChatFolderUpdates: @escaping () -> Void, openStories: @escaping (ChatListNode.OpenStoriesSubject, ASDisplayNode?) -> Void, - dismissNotice: @escaping (ChatListNotice) -> Void + dismissNotice: @escaping (ChatListNotice) -> Void, + editPeer: @escaping (ChatListItem) -> Void ) { self.activateSearch = activateSearch // MARK: Nicegram PinnedChats @@ -222,6 +224,7 @@ public final class ChatListNodeInteraction { self.hideChatFolderUpdates = hideChatFolderUpdates self.openStories = openStories self.dismissNotice = dismissNotice + self.editPeer = editPeer } } @@ -370,7 +373,7 @@ public struct ChatListNodeState: Equatable { } } -private func mappedInsertEntries(context: AccountContext, nodeInteraction: ChatListNodeInteraction, location: ChatListControllerLocation, filterData: ChatListItemFilterData?, chatListFilters: [ChatListFilter]?, mode: ChatListNodeMode, isPeerEnabled: ((EnginePeer) -> Bool)?, entries: [ChatListNodeViewTransitionInsertEntry]) -> [ListViewInsertItem] { +private func mappedInsertEntries(context: AccountContext, nodeInteraction: ChatListNodeInteraction, location: ChatListControllerLocation, isPremium: Bool, filterData: ChatListItemFilterData?, chatListFilters: [ChatListFilter]?, mode: ChatListNodeMode, isPeerEnabled: ((EnginePeer) -> Bool)?, entries: [ChatListNodeViewTransitionInsertEntry]) -> [ListViewInsertItem] { return entries.map { entry -> ListViewInsertItem in switch entry.entry { case .HeaderEntry: @@ -456,7 +459,7 @@ private func mappedInsertEntries(context: AccountContext, nodeInteraction: ChatL }, requiresPremiumForMessaging: peerEntry.requiresPremiumForMessaging, displayAsTopicList: peerEntry.displayAsTopicList, - tags: chatListItemTags(accountPeerId: context.account.peerId, peer: peer.chatMainPeer, isUnread: combinedReadState?.isUnread ?? false, isMuted: isRemovedFromTotalUnreadCount, isContact: isContact, hasUnseenMentions: hasUnseenMentions, chatListFilters: chatListFilters) + tags: chatListItemTags(location: location, accountPeerId: context.account.peerId, isPremium: isPremium, peer: peer.chatMainPeer, isUnread: combinedReadState?.isUnread ?? false, isMuted: isRemovedFromTotalUnreadCount, isContact: isContact, hasUnseenMentions: hasUnseenMentions, chatListFilters: chatListFilters) )), editing: editing, hasActiveRevealControls: hasActiveRevealControls, @@ -774,7 +777,7 @@ private func mappedInsertEntries(context: AccountContext, nodeInteraction: ChatL } } -private func mappedUpdateEntries(context: AccountContext, nodeInteraction: ChatListNodeInteraction, location: ChatListControllerLocation, filterData: ChatListItemFilterData?, chatListFilters: [ChatListFilter]?, mode: ChatListNodeMode, isPeerEnabled: ((EnginePeer) -> Bool)?, entries: [ChatListNodeViewTransitionUpdateEntry]) -> [ListViewUpdateItem] { +private func mappedUpdateEntries(context: AccountContext, nodeInteraction: ChatListNodeInteraction, location: ChatListControllerLocation, isPremium: Bool, filterData: ChatListItemFilterData?, chatListFilters: [ChatListFilter]?, mode: ChatListNodeMode, isPeerEnabled: ((EnginePeer) -> Bool)?, entries: [ChatListNodeViewTransitionUpdateEntry]) -> [ListViewUpdateItem] { return entries.map { entry -> ListViewUpdateItem in switch entry.entry { case let .PeerEntry(peerEntry): @@ -837,7 +840,7 @@ private func mappedUpdateEntries(context: AccountContext, nodeInteraction: ChatL }, requiresPremiumForMessaging: peerEntry.requiresPremiumForMessaging, displayAsTopicList: peerEntry.displayAsTopicList, - tags: chatListItemTags(accountPeerId: context.account.peerId, peer: peer.chatMainPeer, isUnread: combinedReadState?.isUnread ?? false, isMuted: isRemovedFromTotalUnreadCount, isContact: isContact, hasUnseenMentions: hasUnseenMentions, chatListFilters: chatListFilters) + tags: chatListItemTags(location: location, accountPeerId: context.account.peerId, isPremium: isPremium, peer: peer.chatMainPeer, isUnread: combinedReadState?.isUnread ?? false, isMuted: isRemovedFromTotalUnreadCount, isContact: isContact, hasUnseenMentions: hasUnseenMentions, chatListFilters: chatListFilters) )), editing: editing, hasActiveRevealControls: hasActiveRevealControls, @@ -1132,8 +1135,8 @@ private func mappedUpdateEntries(context: AccountContext, nodeInteraction: ChatL } } -private func mappedChatListNodeViewListTransition(context: AccountContext, nodeInteraction: ChatListNodeInteraction, location: ChatListControllerLocation, filterData: ChatListItemFilterData?, chatListFilters: [ChatListFilter]?, mode: ChatListNodeMode, isPeerEnabled: ((EnginePeer) -> Bool)?, transition: ChatListNodeViewTransition) -> ChatListNodeListViewTransition { - return ChatListNodeListViewTransition(chatListView: transition.chatListView, deleteItems: transition.deleteItems, insertItems: mappedInsertEntries(context: context, nodeInteraction: nodeInteraction, location: location, filterData: filterData, chatListFilters: chatListFilters, mode: mode, isPeerEnabled: isPeerEnabled, entries: transition.insertEntries), updateItems: mappedUpdateEntries(context: context, nodeInteraction: nodeInteraction, location: location, filterData: filterData, chatListFilters: chatListFilters, mode: mode, isPeerEnabled: isPeerEnabled, entries: transition.updateEntries), options: transition.options, scrollToItem: transition.scrollToItem, stationaryItemRange: transition.stationaryItemRange, adjustScrollToFirstItem: transition.adjustScrollToFirstItem, animateCrossfade: transition.animateCrossfade) +private func mappedChatListNodeViewListTransition(context: AccountContext, nodeInteraction: ChatListNodeInteraction, location: ChatListControllerLocation, isPremium: Bool, filterData: ChatListItemFilterData?, chatListFilters: [ChatListFilter]?, mode: ChatListNodeMode, isPeerEnabled: ((EnginePeer) -> Bool)?, transition: ChatListNodeViewTransition) -> ChatListNodeListViewTransition { + return ChatListNodeListViewTransition(chatListView: transition.chatListView, deleteItems: transition.deleteItems, insertItems: mappedInsertEntries(context: context, nodeInteraction: nodeInteraction, location: location, isPremium: isPremium, filterData: filterData, chatListFilters: chatListFilters, mode: mode, isPeerEnabled: isPeerEnabled, entries: transition.insertEntries), updateItems: mappedUpdateEntries(context: context, nodeInteraction: nodeInteraction, location: location, isPremium: isPremium, filterData: filterData, chatListFilters: chatListFilters, mode: mode, isPeerEnabled: isPeerEnabled, entries: transition.updateEntries), options: transition.options, scrollToItem: transition.scrollToItem, stationaryItemRange: transition.stationaryItemRange, adjustScrollToFirstItem: transition.adjustScrollToFirstItem, animateCrossfade: transition.animateCrossfade) } private final class ChatListOpaqueTransactionState { @@ -1822,6 +1825,7 @@ public final class ChatListNode: ListView { default: break } + }, editPeer: { _ in }) nodeInteraction.isInlineMode = isInlineMode @@ -2172,8 +2176,17 @@ public final class ChatListNode: ListView { } let previousChatListFilters = Atomic<[ChatListFilter]?>(value: nil) - let chatListNodeViewTransition = combineLatest( - queue: viewProcessingQueue, + let previousAccountIsPremium = Atomic(value: nil) + + let accountIsPremium = context.engine.data.subscribe( + TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId) + ) + |> map { peer -> Bool in + return peer?.isPremium ?? false + } + |> distinctUntilChanged + + let chatListNodeViewTransition = combineLatest(queue: viewProcessingQueue, hideArchivedFolderByDefault, displayArchiveIntro, storageInfo, @@ -2188,11 +2201,12 @@ public final class ChatListNode: ListView { // self.statePromise.get(), contacts, - chatListFilters + chatListFilters, + accountIsPremium ) // MARK: Nicegram PinnedChats, nicegramItems added // MARK: Nicegram HiddenChats, hiddenChats added - |> mapToQueue { (hideArchivedFolderByDefault, displayArchiveIntro, storageInfo, suggestedChatListNotice, savedMessagesPeer, updateAndFilter, nicegramItems, hiddenChats, state, contacts, chatListFilters) -> Signal in + |> mapToQueue { (hideArchivedFolderByDefault, displayArchiveIntro, storageInfo, suggestedChatListNotice, savedMessagesPeer, updateAndFilter, nicegramItems, hiddenChats, state, contacts, chatListFilters, accountIsPremium) -> Signal in let (update, filter) = updateAndFilter let previousHideArchivedFolderByDefaultValue = previousHideArchivedFolderByDefault.swap(hideArchivedFolderByDefault) @@ -2675,9 +2689,12 @@ public final class ChatListNode: ListView { if chatListFilters != previousChatListFiltersValue { forceAllUpdated = true } + if accountIsPremium != previousAccountIsPremium.swap(accountIsPremium) { + forceAllUpdated = true + } return preparedChatListNodeViewTransition(from: previousView, to: processedView, reason: reason, previewing: previewing, disableAnimations: disableAnimations, account: context.account, scrollPosition: updatedScrollPosition, searchMode: searchMode, forceAllUpdated: forceAllUpdated) - |> map({ mappedChatListNodeViewListTransition(context: context, nodeInteraction: nodeInteraction, location: location, filterData: filterData, chatListFilters: chatListFilters, mode: mode, isPeerEnabled: isPeerEnabled, transition: $0) }) + |> map({ mappedChatListNodeViewListTransition(context: context, nodeInteraction: nodeInteraction, location: location, isPremium: accountIsPremium, filterData: filterData, chatListFilters: chatListFilters, mode: mode, isPeerEnabled: isPeerEnabled, transition: $0) }) |> runOn(prepareOnMainQueue ? Queue.mainQueue() : viewProcessingQueue) } @@ -3277,7 +3294,7 @@ public final class ChatListNode: ListView { } private func resetFilter() { - if let chatListFilter = self.chatListFilter { + if let chatListFilter = self.chatListFilter, chatListFilter.id != Int32.max { self.updatedFilterDisposable.set((self.context.engine.peers.updatedChatListFilters() |> map { filters -> ChatListFilter? in for filter in filters { @@ -4292,7 +4309,14 @@ func hideChatListContacts(context: AccountContext) { let _ = ApplicationSpecificNotice.setDisplayChatListContacts(accountManager: context.sharedContext.accountManager).startStandalone() } -func chatListItemTags(accountPeerId: EnginePeer.Id, peer: EnginePeer?, isUnread: Bool, isMuted: Bool, isContact: Bool, hasUnseenMentions: Bool, chatListFilters: [ChatListFilter]?) -> [ChatListItemContent.Tag] { +func chatListItemTags(location: ChatListControllerLocation, accountPeerId: EnginePeer.Id, isPremium: Bool, peer: EnginePeer?, isUnread: Bool, isMuted: Bool, isContact: Bool, hasUnseenMentions: Bool, chatListFilters: [ChatListFilter]?) -> [ChatListItemContent.Tag] { + if !isPremium { + return [] + } + if case .chatList = location { + } else { + return [] + } guard let chatListFilters, !chatListFilters.isEmpty else { return [] } @@ -4302,13 +4326,15 @@ func chatListItemTags(accountPeerId: EnginePeer.Id, peer: EnginePeer?, isUnread: var result: [ChatListItemContent.Tag] = [] for case let .filter(id, title, _, data) in chatListFilters { - let predicate = chatListFilterPredicate(filter: data, accountPeerId: accountPeerId) - if predicate.includes(peer: peer._asPeer(), groupId: .root, isRemovedFromTotalUnreadCount: isMuted, isUnread: isUnread, isContact: isContact, messageTagSummaryResult: hasUnseenMentions) { - result.append(ChatListItemContent.Tag( - id: id, - title: title, - colorId: data.color?.rawValue ?? PeerNameColor.blue.rawValue - )) + if data.color != nil { + let predicate = chatListFilterPredicate(filter: data, accountPeerId: accountPeerId) + if predicate.pinnedPeerIds.contains(peer.id) || predicate.includes(peer: peer._asPeer(), groupId: .root, isRemovedFromTotalUnreadCount: isMuted, isUnread: isUnread, isContact: isContact, messageTagSummaryResult: hasUnseenMentions) { + result.append(ChatListItemContent.Tag( + id: id, + title: title, + colorId: data.color?.rawValue ?? PeerNameColor.blue.rawValue + )) + } } } return result diff --git a/submodules/ChatListUI/Sources/Node/ChatListNodeLocation.swift b/submodules/ChatListUI/Sources/Node/ChatListNodeLocation.swift index c4526126451..7b67cae1b2f 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListNodeLocation.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListNodeLocation.swift @@ -67,7 +67,7 @@ public func chatListFilterPredicate(filter: ChatListFilterData, accountPeerId: E } if !filter.categories.contains(.contacts) && isContact { if let user = peer as? TelegramUser { - if user.botInfo == nil { + if user.botInfo == nil && !user.flags.contains(.isSupport) { return false } } else if let _ = peer as? TelegramSecretChat { @@ -88,7 +88,7 @@ public func chatListFilterPredicate(filter: ChatListFilterData, accountPeerId: E } if !filter.categories.contains(.bots) { if let user = peer as? TelegramUser { - if user.botInfo != nil { + if user.botInfo != nil || user.flags.contains(.isSupport) { return false } } diff --git a/submodules/ChatPresentationInterfaceState/Sources/ChatPanelInterfaceInteraction.swift b/submodules/ChatPresentationInterfaceState/Sources/ChatPanelInterfaceInteraction.swift index 8b8e94bd367..0b947f19102 100644 --- a/submodules/ChatPresentationInterfaceState/Sources/ChatPanelInterfaceInteraction.swift +++ b/submodules/ChatPresentationInterfaceState/Sources/ChatPanelInterfaceInteraction.swift @@ -111,6 +111,8 @@ public final class ChatPanelInterfaceInteraction { public let togglePeerNotifications: () -> Void public let sendContextResult: (ChatContextResultCollection, ChatContextResult, ASDisplayNode, CGRect) -> Bool public let sendBotCommand: (Peer, String) -> Void + public let sendShortcut: (Int32) -> Void + public let openEditShortcuts: () -> Void public let sendBotStart: (String?) -> Void public let botSwitchChatWithPayload: (PeerId, String) -> Void public let beginMediaRecording: (Bool) -> Void @@ -231,6 +233,8 @@ public final class ChatPanelInterfaceInteraction { togglePeerNotifications: @escaping () -> Void, sendContextResult: @escaping (ChatContextResultCollection, ChatContextResult, ASDisplayNode, CGRect) -> Bool, sendBotCommand: @escaping (Peer, String) -> Void, + sendShortcut: @escaping (Int32) -> Void, + openEditShortcuts: @escaping () -> Void, sendBotStart: @escaping (String?) -> Void, botSwitchChatWithPayload: @escaping (PeerId, String) -> Void, beginMediaRecording: @escaping (Bool) -> Void, @@ -350,6 +354,8 @@ public final class ChatPanelInterfaceInteraction { self.togglePeerNotifications = togglePeerNotifications self.sendContextResult = sendContextResult self.sendBotCommand = sendBotCommand + self.sendShortcut = sendShortcut + self.openEditShortcuts = openEditShortcuts self.sendBotStart = sendBotStart self.botSwitchChatWithPayload = botSwitchChatWithPayload self.beginMediaRecording = beginMediaRecording @@ -473,6 +479,8 @@ public final class ChatPanelInterfaceInteraction { }, sendContextResult: { _, _, _, _ in return false }, sendBotCommand: { _, _ in + }, sendShortcut: { _ in + }, openEditShortcuts: { }, sendBotStart: { _ in }, botSwitchChatWithPayload: { _, _ in }, beginMediaRecording: { _ in diff --git a/submodules/ChatPresentationInterfaceState/Sources/ChatPresentationInterfaceState.swift b/submodules/ChatPresentationInterfaceState/Sources/ChatPresentationInterfaceState.swift index 2bf79006efc..75406cdcf66 100644 --- a/submodules/ChatPresentationInterfaceState/Sources/ChatPresentationInterfaceState.swift +++ b/submodules/ChatPresentationInterfaceState/Sources/ChatPresentationInterfaceState.swift @@ -17,7 +17,7 @@ public extension ChatLocation { return peerId case let .replyThread(replyThreadMessage): return replyThreadMessage.peerId - case .feed: + case .customChatContents: return nil } } @@ -28,7 +28,7 @@ public extension ChatLocation { return nil case let .replyThread(replyThreadMessage): return replyThreadMessage.threadId - case .feed: + case .customChatContents: return nil } } @@ -1202,6 +1202,8 @@ public func canSendMessagesToChat(_ state: ChatPresentationInterfaceState) -> Bo } else { return false } + } else if case .customChatContents = state.chatLocation { + return true } else { return false } diff --git a/submodules/ChatSendMessageActionUI/Sources/ChatSendMessageActionSheetControllerNode.swift b/submodules/ChatSendMessageActionUI/Sources/ChatSendMessageActionSheetControllerNode.swift index 77d610c7434..a2afd7dc9b5 100644 --- a/submodules/ChatSendMessageActionUI/Sources/ChatSendMessageActionSheetControllerNode.swift +++ b/submodules/ChatSendMessageActionUI/Sources/ChatSendMessageActionSheetControllerNode.swift @@ -550,7 +550,7 @@ final class ChatSendMessageActionSheetControllerNode: ViewControllerTracingNode, } var clipDelta = delta - if inputHeight.isZero || layout.isNonExclusive { + if inputHeight < 70.0 || layout.isNonExclusive { clipDelta -= self.contentContainerNode.frame.height + 16.0 } @@ -673,7 +673,7 @@ final class ChatSendMessageActionSheetControllerNode: ViewControllerTracingNode, } var clipDelta = delta - if inputHeight.isZero || layout.isNonExclusive { + if inputHeight < 70.0 || layout.isNonExclusive { clipDelta -= self.contentContainerNode.frame.height + 16.0 } @@ -745,7 +745,7 @@ final class ChatSendMessageActionSheetControllerNode: ViewControllerTracingNode, } else { contentOrigin = CGPoint(x: layout.size.width - sideInset - contentSize.width - layout.safeInsets.right, y: layout.size.height - 6.0 - insets.bottom - contentSize.height) } - if inputHeight > 0.0 && !layout.isNonExclusive && self.animateInputField { + if inputHeight > 70.0 && !layout.isNonExclusive && self.animateInputField { contentOrigin.y += menuHeightWithInset } contentOrigin.y = min(contentOrigin.y + contentOffset, layout.size.height - 6.0 - layout.intrinsicInsets.bottom - contentSize.height) @@ -759,7 +759,7 @@ final class ChatSendMessageActionSheetControllerNode: ViewControllerTracingNode, } var sendButtonFrame = CGRect(origin: CGPoint(x: layout.size.width - initialSendButtonFrame.width + 1.0 - UIScreenPixel - layout.safeInsets.right, y: layout.size.height - insets.bottom - initialSendButtonFrame.height), size: initialSendButtonFrame.size) - if (inputHeight.isZero || layout.isNonExclusive) && self.animateInputField { + if (inputHeight < 70.0 || layout.isNonExclusive) && self.animateInputField { sendButtonFrame.origin.y -= menuHeightWithInset } sendButtonFrame.origin.y = min(sendButtonFrame.origin.y + contentOffset, layout.size.height - layout.intrinsicInsets.bottom - initialSendButtonFrame.height) @@ -772,7 +772,7 @@ final class ChatSendMessageActionSheetControllerNode: ViewControllerTracingNode, let messageHeightAddition: CGFloat = max(0.0, 35.0 - messageFrame.size.height) - if inputHeight.isZero || layout.isNonExclusive { + if inputHeight < 70.0 || layout.isNonExclusive { messageFrame.origin.y += menuHeightWithInset } diff --git a/submodules/ComponentFlow/Source/Base/CombinedComponent.swift b/submodules/ComponentFlow/Source/Base/CombinedComponent.swift index b4f27b849e8..04df0fd8cea 100644 --- a/submodules/ComponentFlow/Source/Base/CombinedComponent.swift +++ b/submodules/ComponentFlow/Source/Base/CombinedComponent.swift @@ -722,11 +722,11 @@ public extension CombinedComponent { updatedChild.view.layer.shadowRadius = 0.0 updatedChild.view.layer.shadowOpacity = 0.0 } - updatedChild.view.context(typeErasedComponent: updatedChild.component).erasedState._updated = { [weak viewContext] transition in + updatedChild.view.context(typeErasedComponent: updatedChild.component).erasedState._updated = { [weak viewContext] transition, isLocal in guard let viewContext = viewContext else { return } - viewContext.state.updated(transition: transition) + viewContext.state.updated(transition: transition, isLocal: isLocal) } if let transitionAppearWithGuide = updatedChild.transitionAppearWithGuide { diff --git a/submodules/ComponentFlow/Source/Base/Component.swift b/submodules/ComponentFlow/Source/Base/Component.swift index 77163495773..fabce5cf5cc 100644 --- a/submodules/ComponentFlow/Source/Base/Component.swift +++ b/submodules/ComponentFlow/Source/Base/Component.swift @@ -89,15 +89,15 @@ extension UIView { } open class ComponentState { - open var _updated: ((Transition) -> Void)? + open var _updated: ((Transition, Bool) -> Void)? var isUpdated: Bool = false public init() { } - public final func updated(transition: Transition = .immediate) { + public final func updated(transition: Transition = .immediate, isLocal: Bool = false) { self.isUpdated = true - self._updated?(transition) + self._updated?(transition, isLocal) } } diff --git a/submodules/ComponentFlow/Source/Components/RoundedRectangle.swift b/submodules/ComponentFlow/Source/Components/RoundedRectangle.swift index 32e0c7edfe2..275772854a1 100644 --- a/submodules/ComponentFlow/Source/Components/RoundedRectangle.swift +++ b/submodules/ComponentFlow/Source/Components/RoundedRectangle.swift @@ -8,16 +8,16 @@ public final class RoundedRectangle: Component { } public let colors: [UIColor] - public let cornerRadius: CGFloat + public let cornerRadius: CGFloat? public let gradientDirection: GradientDirection public let stroke: CGFloat? public let strokeColor: UIColor? - public convenience init(color: UIColor, cornerRadius: CGFloat, stroke: CGFloat? = nil, strokeColor: UIColor? = nil) { + public convenience init(color: UIColor, cornerRadius: CGFloat?, stroke: CGFloat? = nil, strokeColor: UIColor? = nil) { self.init(colors: [color], cornerRadius: cornerRadius, stroke: stroke, strokeColor: strokeColor) } - public init(colors: [UIColor], cornerRadius: CGFloat, gradientDirection: GradientDirection = .horizontal, stroke: CGFloat? = nil, strokeColor: UIColor? = nil) { + public init(colors: [UIColor], cornerRadius: CGFloat?, gradientDirection: GradientDirection = .horizontal, stroke: CGFloat? = nil, strokeColor: UIColor? = nil) { self.colors = colors self.cornerRadius = cornerRadius self.gradientDirection = gradientDirection @@ -49,8 +49,10 @@ public final class RoundedRectangle: Component { func update(component: RoundedRectangle, availableSize: CGSize, transition: Transition) -> CGSize { if self.component != component { + let cornerRadius = component.cornerRadius ?? min(availableSize.width, availableSize.height) * 0.5 + if component.colors.count == 1, let color = component.colors.first { - let imageSize = CGSize(width: max(component.stroke ?? 0.0, component.cornerRadius) * 2.0, height: max(component.stroke ?? 0.0, component.cornerRadius) * 2.0) + let imageSize = CGSize(width: max(component.stroke ?? 0.0, cornerRadius) * 2.0, height: max(component.stroke ?? 0.0, cornerRadius) * 2.0) UIGraphicsBeginImageContextWithOptions(imageSize, false, 0.0) if let context = UIGraphicsGetCurrentContext() { if let strokeColor = component.strokeColor { @@ -69,13 +71,13 @@ public final class RoundedRectangle: Component { context.fillEllipse(in: CGRect(origin: CGPoint(), size: imageSize).insetBy(dx: stroke, dy: stroke)) } } - self.image = UIGraphicsGetImageFromCurrentImageContext()?.stretchableImage(withLeftCapWidth: Int(component.cornerRadius), topCapHeight: Int(component.cornerRadius)) + self.image = UIGraphicsGetImageFromCurrentImageContext()?.stretchableImage(withLeftCapWidth: Int(cornerRadius), topCapHeight: Int(cornerRadius)) UIGraphicsEndImageContext() } else if component.colors.count > 1 { let imageSize = availableSize UIGraphicsBeginImageContextWithOptions(imageSize, false, 0.0) if let context = UIGraphicsGetCurrentContext() { - context.addPath(UIBezierPath(roundedRect: CGRect(origin: CGPoint(), size: imageSize), cornerRadius: component.cornerRadius).cgPath) + context.addPath(UIBezierPath(roundedRect: CGRect(origin: CGPoint(), size: imageSize), cornerRadius: cornerRadius).cgPath) context.clip() let colors = component.colors @@ -93,12 +95,12 @@ public final class RoundedRectangle: Component { if let stroke = component.stroke, stroke > 0.0 { context.resetClip() - context.addPath(UIBezierPath(roundedRect: CGRect(origin: CGPoint(), size: imageSize).insetBy(dx: stroke, dy: stroke), cornerRadius: component.cornerRadius).cgPath) + context.addPath(UIBezierPath(roundedRect: CGRect(origin: CGPoint(), size: imageSize).insetBy(dx: stroke, dy: stroke), cornerRadius: cornerRadius).cgPath) context.setBlendMode(.clear) context.fill(CGRect(origin: .zero, size: imageSize)) } } - self.image = UIGraphicsGetImageFromCurrentImageContext()?.stretchableImage(withLeftCapWidth: Int(component.cornerRadius), topCapHeight: Int(component.cornerRadius)) + self.image = UIGraphicsGetImageFromCurrentImageContext()?.stretchableImage(withLeftCapWidth: Int(cornerRadius), topCapHeight: Int(cornerRadius)) UIGraphicsEndImageContext() } } diff --git a/submodules/ComponentFlow/Source/Host/ComponentHostView.swift b/submodules/ComponentFlow/Source/Host/ComponentHostView.swift index 2c8a3d87b6c..cdc4a7c3abd 100644 --- a/submodules/ComponentFlow/Source/Host/ComponentHostView.swift +++ b/submodules/ComponentFlow/Source/Host/ComponentHostView.swift @@ -82,7 +82,7 @@ public final class ComponentHostView: UIView { self.currentComponent = component self.currentContainerSize = containerSize - componentState._updated = { [weak self] transition in + componentState._updated = { [weak self] transition, _ in guard let strongSelf = self else { return } @@ -208,11 +208,11 @@ public final class ComponentView { self.currentComponent = component self.currentContainerSize = containerSize - componentState._updated = { [weak self] transition in + componentState._updated = { [weak self] transition, isLocal in guard let strongSelf = self else { return } - if let parentState = strongSelf.parentState { + if !isLocal, let parentState = strongSelf.parentState { parentState.updated(transition: transition) } else { let _ = strongSelf._update(transition: transition, component: component, maybeEnvironment: { diff --git a/submodules/Components/ViewControllerComponent/Sources/ViewControllerComponent.swift b/submodules/Components/ViewControllerComponent/Sources/ViewControllerComponent.swift index aedc76242ed..d86b99280ac 100644 --- a/submodules/Components/ViewControllerComponent/Sources/ViewControllerComponent.swift +++ b/submodules/Components/ViewControllerComponent/Sources/ViewControllerComponent.swift @@ -46,6 +46,7 @@ open class ViewControllerComponentContainer: ViewController { public let statusBarHeight: CGFloat public let navigationHeight: CGFloat public let safeInsets: UIEdgeInsets + public let additionalInsets: UIEdgeInsets public let inputHeight: CGFloat public let metrics: LayoutMetrics public let deviceMetrics: DeviceMetrics @@ -60,6 +61,7 @@ open class ViewControllerComponentContainer: ViewController { statusBarHeight: CGFloat, navigationHeight: CGFloat, safeInsets: UIEdgeInsets, + additionalInsets: UIEdgeInsets, inputHeight: CGFloat, metrics: LayoutMetrics, deviceMetrics: DeviceMetrics, @@ -73,6 +75,7 @@ open class ViewControllerComponentContainer: ViewController { self.statusBarHeight = statusBarHeight self.navigationHeight = navigationHeight self.safeInsets = safeInsets + self.additionalInsets = additionalInsets self.inputHeight = inputHeight self.metrics = metrics self.deviceMetrics = deviceMetrics @@ -98,6 +101,9 @@ open class ViewControllerComponentContainer: ViewController { if lhs.safeInsets != rhs.safeInsets { return false } + if lhs.additionalInsets != rhs.additionalInsets { + return false + } if lhs.inputHeight != rhs.inputHeight { return false } @@ -167,6 +173,7 @@ open class ViewControllerComponentContainer: ViewController { statusBarHeight: layout.statusBarHeight ?? 0.0, navigationHeight: navigationHeight, safeInsets: UIEdgeInsets(top: layout.intrinsicInsets.top + layout.safeInsets.top, left: layout.safeInsets.left, bottom: layout.intrinsicInsets.bottom + layout.safeInsets.bottom, right: layout.safeInsets.right), + additionalInsets: layout.additionalInsets, inputHeight: layout.inputHeight ?? 0.0, metrics: layout.metrics, deviceMetrics: layout.deviceMetrics, diff --git a/submodules/ContactListUI/Sources/ContactListNode.swift b/submodules/ContactListUI/Sources/ContactListNode.swift index 3787faaee95..ad63bda0fef 100644 --- a/submodules/ContactListUI/Sources/ContactListNode.swift +++ b/submodules/ContactListUI/Sources/ContactListNode.swift @@ -702,9 +702,29 @@ private struct ContactsListNodeTransition { } public enum ContactListPresentation { + public struct Search { + public var signal: Signal + public var searchChatList: Bool + public var searchDeviceContacts: Bool + public var searchGroups: Bool + public var searchChannels: Bool + public var globalSearch: Bool + public var displaySavedMessages: Bool + + public init(signal: Signal, searchChatList: Bool, searchDeviceContacts: Bool, searchGroups: Bool, searchChannels: Bool, globalSearch: Bool, displaySavedMessages: Bool) { + self.signal = signal + self.searchChatList = searchChatList + self.searchDeviceContacts = searchDeviceContacts + self.searchGroups = searchGroups + self.searchChannels = searchChannels + self.globalSearch = globalSearch + self.displaySavedMessages = displaySavedMessages + } + } + case orderedByPresence(options: [ContactListAdditionalOption]) case natural(options: [ContactListAdditionalOption], includeChatList: Bool, topPeers: Bool) - case search(signal: Signal, searchChatList: Bool, searchDeviceContacts: Bool, searchGroups: Bool, searchChannels: Bool, globalSearch: Bool) + case search(Search) public var sortOrder: ContactsSortOrder? { switch self { @@ -1098,7 +1118,15 @@ public final class ContactListNode: ASDisplayNode { displayTopPeers = displayTopPeersValue } - if case let .search(query, searchChatList, searchDeviceContacts, searchGroups, searchChannels, globalSearch) = presentation { + if case let .search(search) = presentation { + let query = search.signal + let searchChatList = search.searchChatList + let searchDeviceContacts = search.searchDeviceContacts + let searchGroups = search.searchGroups + let searchChannels = search.searchChannels + let globalSearch = search.globalSearch + let displaySavedMessages = search.displaySavedMessages + return query |> mapToSignal { query in let foundLocalContacts: Signal<([FoundPeer], [EnginePeer.Id: EnginePeer.Presence]), NoError> @@ -1109,6 +1137,12 @@ public final class ContactListNode: ASDisplayNode { var resultPeers: [FoundPeer] = [] for peer in peers { + if !displaySavedMessages { + if peer.peerId == context.account.peerId { + continue + } + } + if searchGroups || searchChannels { let mainPeer = peer.chatMainPeer if let _ = mainPeer as? TelegramUser { diff --git a/submodules/ContactListUI/Sources/ContactsController.swift b/submodules/ContactListUI/Sources/ContactsController.swift index 5f95d9bf267..c79c7a5bb55 100644 --- a/submodules/ContactListUI/Sources/ContactsController.swift +++ b/submodules/ContactListUI/Sources/ContactsController.swift @@ -678,41 +678,18 @@ public class ContactsController: ViewController { return false } if value == .commit { - let presentationData = self.presentationData - let deleteContactsFromDevice: Signal if let contactDataManager = self.context.sharedContext.contactDataManager { - deleteContactsFromDevice = contactDataManager.deleteContactWithAppSpecificReference(peerId: peerIds.first!) + deleteContactsFromDevice = combineLatest(peerIds.map { contactDataManager.deleteContactWithAppSpecificReference(peerId: $0) } + ) + |> ignoreValues } else { deleteContactsFromDevice = .complete() } - var deleteSignal = self.context.engine.contacts.deleteContacts(peerIds: peerIds) + let deleteSignal = self.context.engine.contacts.deleteContacts(peerIds: peerIds) |> then(deleteContactsFromDevice) - let progressSignal = Signal { [weak self] subscriber in - guard let self else { - return EmptyDisposable - } - let statusController = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: nil)) - self.present(statusController, in: .window(.root)) - return ActionDisposable { [weak statusController] in - Queue.mainQueue().async() { - statusController?.dismiss() - } - } - } - |> runOn(Queue.mainQueue()) - |> delay(0.15, queue: Queue.mainQueue()) - let progressDisposable = progressSignal.start() - - deleteSignal = deleteSignal - |> afterDisposed { - Queue.mainQueue().async { - progressDisposable.dispose() - } - } - for peerId in peerIds { deleteSendMessageIntents(peerId: peerId) } @@ -724,6 +701,9 @@ public class ContactsController: ViewController { } return state } + + let _ = deleteSignal.start() + return true } else if value == .undo { self.contactsNode.contactListNode.updatePendingRemovalPeerIds { state in diff --git a/submodules/ContactListUI/Sources/ContactsControllerNode.swift b/submodules/ContactListUI/Sources/ContactsControllerNode.swift index 6105a64ba90..32642969bfc 100644 --- a/submodules/ContactListUI/Sources/ContactsControllerNode.swift +++ b/submodules/ContactListUI/Sources/ContactsControllerNode.swift @@ -362,6 +362,7 @@ final class ContactsControllerNode: ASDisplayNode, UIGestureRecognizerDelegate { statusBarHeight: layout.statusBarHeight ?? 0.0, sideInset: layout.safeInsets.left, isSearchActive: self.isSearchDisplayControllerActive, + isSearchEnabled: true, primaryContent: primaryContent, secondaryContent: nil, secondaryTransition: 0.0, diff --git a/submodules/Display/Source/AlertController.swift b/submodules/Display/Source/AlertController.swift index 00bb5cf1238..8589a0baef0 100644 --- a/submodules/Display/Source/AlertController.swift +++ b/submodules/Display/Source/AlertController.swift @@ -83,7 +83,7 @@ open class AlertController: ViewController, StandalonePresentableController, Key } } } - private let contentNode: AlertContentNode + public let contentNode: AlertContentNode private let allowInputInset: Bool private weak var existingAlertController: AlertController? diff --git a/submodules/Display/Source/TextNode.swift b/submodules/Display/Source/TextNode.swift index d0ac26e952f..b469147b603 100644 --- a/submodules/Display/Source/TextNode.swift +++ b/submodules/Display/Source/TextNode.swift @@ -409,6 +409,8 @@ public final class TextNodeLayout: NSObject { switch self.resolvedAlignment { case .center: lineFrame = CGRect(origin: CGPoint(x: floor((size.width - line.frame.size.width) / 2.0), y: line.frame.minY), size: line.frame.size) + case .right: + lineFrame = CGRect(origin: CGPoint(x: size.width - line.frame.size.width, y: line.frame.minY), size: line.frame.size) default: lineFrame = displayLineFrame(frame: line.frame, isRTL: line.isRTL, boundingRect: CGRect(origin: CGPoint(), size: size), cutout: cutout) } @@ -521,6 +523,8 @@ public final class TextNodeLayout: NSObject { lineFrame.origin.x = floor((self.size.width - lineFrame.size.width) / 2.0) case .natural: lineFrame = displayLineFrame(frame: lineFrame, isRTL: line.isRTL, boundingRect: CGRect(origin: CGPoint(), size: self.size), cutout: self.cutout) + case .right: + lineFrame.origin.x = self.size.width - lineFrame.size.width default: break } @@ -589,6 +593,8 @@ public final class TextNodeLayout: NSObject { lineFrame.origin.x = floor((self.size.width - lineFrame.size.width) / 2.0) case .natural: lineFrame = displayLineFrame(frame: lineFrame, isRTL: line.isRTL, boundingRect: CGRect(origin: CGPoint(), size: self.size), cutout: self.cutout) + case .right: + lineFrame.origin.x = self.size.width - lineFrame.size.width default: break } @@ -666,6 +672,8 @@ public final class TextNodeLayout: NSObject { lineFrame.origin.x = floor((self.size.width - lineFrame.size.width) / 2.0) case .natural: lineFrame = displayLineFrame(frame: lineFrame, isRTL: line.isRTL, boundingRect: CGRect(origin: CGPoint(), size: self.size), cutout: self.cutout) + case .right: + lineFrame.origin.x = self.size.width - lineFrame.size.width default: break } @@ -846,6 +854,8 @@ public final class TextNodeLayout: NSObject { lineFrame.origin.x = floor((self.size.width - lineFrame.size.width) / 2.0) case .natural: lineFrame = displayLineFrame(frame: lineFrame, isRTL: line.isRTL, boundingRect: CGRect(origin: CGPoint(), size: self.size), cutout: self.cutout) + case .right: + lineFrame.origin.x = self.size.width - lineFrame.size.width default: break } @@ -1430,7 +1440,8 @@ open class TextNode: ASDisplayNode { let line = CTTypesetterCreateLine(typesetter, CFRange(location: currentLineStartIndex, length: lineCharacterCount)) var lineAscent: CGFloat = 0.0 var lineDescent: CGFloat = 0.0 - let lineWidth = CTLineGetTypographicBounds(line, &lineAscent, &lineDescent, nil) + var lineWidth = CTLineGetTypographicBounds(line, &lineAscent, &lineDescent, nil) + lineWidth = min(lineWidth, constrainedSegmentWidth - additionalSegmentRightInset) var isRTL = false let glyphRuns = CTLineGetGlyphRuns(line) as NSArray @@ -2009,7 +2020,7 @@ open class TextNode: ASDisplayNode { var descent: CGFloat = 0.0 CTLineGetTypographicBounds(coreTextLine, &ascent, &descent, nil) - addAttachment(attachment: attachment, line: coreTextLine, ascent: ascent, descent: descent, startIndex: range.location, endIndex: max(range.location, min(lineRange.location + lineRange.length - 1, range.location + range.length)), isAtEndOfTheLine: range.location + range.length >= lineRange.location + lineRange.length - 1) + addAttachment(attachment: attachment, line: coreTextLine, ascent: ascent, descent: descent, startIndex: range.location, endIndex: max(range.location, min(lineRange.location + lineRange.length, range.location + range.length)), isAtEndOfTheLine: range.location + range.length >= lineRange.location + lineRange.length - 1) } } } @@ -2124,7 +2135,7 @@ open class TextNode: ASDisplayNode { var descent: CGFloat = 0.0 CTLineGetTypographicBounds(coreTextLine, &ascent, &descent, nil) - addAttachment(attachment: attachment, line: coreTextLine, ascent: ascent, descent: descent, startIndex: range.location, endIndex: max(range.location, min(lineRange.location + lineRange.length - 1, range.location + range.length)), isAtEndOfTheLine: range.location + range.length >= lineRange.location + lineRange.length - 1) + addAttachment(attachment: attachment, line: coreTextLine, ascent: ascent, descent: descent, startIndex: range.location, endIndex: max(range.location, min(lineRange.location + lineRange.length, range.location + range.length)), isAtEndOfTheLine: range.location + range.length >= lineRange.location + lineRange.length - 1) } } @@ -2407,6 +2418,8 @@ open class TextNode: ASDisplayNode { } else { lineFrame.origin.x += offset.x } + } else if alignment == .right { + lineFrame.origin.x = offset.x + (bounds.size.width - lineFrame.width) } //context.setStrokeColor(UIColor.red.cgColor) diff --git a/submodules/Display/Source/WindowContent.swift b/submodules/Display/Source/WindowContent.swift index cdc5ee9df87..19416153361 100644 --- a/submodules/Display/Source/WindowContent.swift +++ b/submodules/Display/Source/WindowContent.swift @@ -243,7 +243,6 @@ public final class WindowKeyboardGestureRecognizerDelegate: NSObject, UIGestureR public class Window1 { public let hostView: WindowHostView public let badgeView: UIImageView - private let customProximityDimView: UIView private var deviceMetrics: DeviceMetrics @@ -331,10 +330,6 @@ public class Window1 { self.badgeView.image = UIImage(bundleImageName: "Components/AppBadge") self.badgeView.isHidden = true - self.customProximityDimView = UIView() - self.customProximityDimView.backgroundColor = .black - self.customProximityDimView.isHidden = true - self.systemUserInterfaceStyle = hostView.systemUserInterfaceStyle let boundsSize = self.hostView.eventView.bounds.size @@ -673,7 +668,6 @@ public class Window1 { self.windowPanRecognizer = recognizer self.hostView.containerView.addGestureRecognizer(recognizer) self.hostView.containerView.addSubview(self.badgeView) - self.hostView.containerView.addSubview(self.customProximityDimView) } public required init(coder aDecoder: NSCoder) { @@ -707,11 +701,18 @@ public class Window1 { self.updateBadgeVisibility() } + private var proximityDimController: CustomDimController? public func setProximityDimHidden(_ hidden: Bool) { - guard hidden != self.customProximityDimView.isHidden else { - return + if !hidden { + if self.proximityDimController == nil { + let proximityDimController = CustomDimController(navigationBarPresentationData: nil) + self.proximityDimController = proximityDimController + (self.viewController as? NavigationController)?.presentOverlay(controller: proximityDimController, inGlobal: true, blockInteraction: false) + } + } else if let proximityDimController = self.proximityDimController { + self.proximityDimController = nil + proximityDimController.dismiss() } - self.customProximityDimView.isHidden = hidden } private func updateBadgeVisibility() { @@ -1172,8 +1173,6 @@ public class Window1 { self.updateBadgeVisibility() self.badgeView.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((self.windowLayout.size.width - image.size.width) / 2.0), y: 5.0), size: image.size) } - - self.customProximityDimView.frame = CGRect(origin: .zero, size: self.windowLayout.size) } } } @@ -1393,3 +1392,25 @@ public extension Window1 { } } // + +private class CustomDimController: ViewController { + class Node: ASDisplayNode { + override init() { + super.init() + + self.backgroundColor = .black + } + } + override init(navigationBarPresentationData: NavigationBarPresentationData?) { + super.init(navigationBarPresentationData: nil) + } + + required public init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func loadDisplayNode() { + let node = Node() + self.displayNode = node + } +} diff --git a/submodules/DrawingUI/Sources/DrawingEntitiesView.swift b/submodules/DrawingUI/Sources/DrawingEntitiesView.swift index 78d220a9116..34253dde775 100644 --- a/submodules/DrawingUI/Sources/DrawingEntitiesView.swift +++ b/submodules/DrawingUI/Sources/DrawingEntitiesView.swift @@ -60,6 +60,7 @@ public final class DrawingEntitiesView: UIView, TGPhotoDrawingEntitiesView { private let context: AccountContext private let size: CGSize private let hasBin: Bool + private let isStickerEditor: Bool weak var drawingView: DrawingView? public weak var selectionContainerView: DrawingSelectionContainerView? @@ -95,16 +96,20 @@ public final class DrawingEntitiesView: UIView, TGPhotoDrawingEntitiesView { private let yAxisView = UIView() private let angleLayer = SimpleShapeLayer() private let bin = ComponentView() - + + private let stickerOverlayLayer = SimpleShapeLayer() + private let stickerFrameLayer = SimpleShapeLayer() + public var onInteractionUpdated: (Bool) -> Void = { _ in } public var edgePreviewUpdated: (Bool) -> Void = { _ in } private let hapticFeedback = HapticFeedback() - public init(context: AccountContext, size: CGSize, hasBin: Bool = false) { + public init(context: AccountContext, size: CGSize, hasBin: Bool = false, isStickerEditor: Bool = false) { self.context = context self.size = size self.hasBin = hasBin + self.isStickerEditor = isStickerEditor super.init(frame: CGRect(origin: .zero, size: size)) @@ -140,6 +145,13 @@ public final class DrawingEntitiesView: UIView, TGPhotoDrawingEntitiesView { self.angleLayer.opacity = 0.0 self.angleLayer.lineDashPattern = [12, 12] as [NSNumber] + self.stickerOverlayLayer.fillColor = UIColor(rgb: 0x000000, alpha: 0.6).cgColor + + self.stickerFrameLayer.fillColor = UIColor.clear.cgColor + self.stickerFrameLayer.strokeColor = UIColor(rgb: 0xffffff, alpha: 0.55).cgColor + self.stickerFrameLayer.lineDashPattern = [24, 24] as [NSNumber] + self.stickerFrameLayer.lineCap = .round + self.addSubview(self.topEdgeView) self.addSubview(self.leftEdgeView) self.addSubview(self.rightEdgeView) @@ -148,12 +160,25 @@ public final class DrawingEntitiesView: UIView, TGPhotoDrawingEntitiesView { self.addSubview(self.xAxisView) self.addSubview(self.yAxisView) self.layer.addSublayer(self.angleLayer) + + if isStickerEditor { + self.layer.addSublayer(self.stickerOverlayLayer) + self.layer.addSublayer(self.stickerFrameLayer) + } } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } + public override func addSubview(_ view: UIView) { + super.addSubview(view) + if self.stickerOverlayLayer.superlayer != nil, view is DrawingEntityView { + self.layer.addSublayer(self.stickerOverlayLayer) + self.layer.addSublayer(self.stickerFrameLayer) + } + } + public override func layoutSubviews() { super.layoutSubviews() @@ -189,6 +214,25 @@ public final class DrawingEntitiesView: UIView, TGPhotoDrawingEntitiesView { self.angleLayer.path = anglePath self.angleLayer.lineWidth = width self.angleLayer.bounds = CGRect(origin: .zero, size: CGSize(width: 3000.0, height: width)) + + let frameWidth = floor(self.bounds.width * 0.97) + let frameRect = CGRect(origin: CGPoint(x: floor((self.bounds.width - frameWidth) / 2.0), y: floor((self.bounds.height - frameWidth) / 2.0)), size: CGSize(width: frameWidth, height: frameWidth)) + + self.stickerOverlayLayer.frame = self.bounds + + let overlayOuterRect = UIBezierPath(rect: self.bounds) + let overlayInnerRect = UIBezierPath(cgPath: CGPath(roundedRect: frameRect, cornerWidth: frameWidth / 8.0, cornerHeight: frameWidth / 8.0, transform: nil)) + let overlayLineWidth: CGFloat = 2.0 * 2.2 + + overlayOuterRect.append(overlayInnerRect) + overlayOuterRect.usesEvenOddFillRule = true + + self.stickerOverlayLayer.path = overlayOuterRect.cgPath + self.stickerOverlayLayer.fillRule = .evenOdd + + self.stickerFrameLayer.frame = self.bounds + self.stickerFrameLayer.lineWidth = overlayLineWidth + self.stickerFrameLayer.path = CGPath(roundedRect: frameRect.insetBy(dx: -overlayLineWidth / 2.0, dy: -overlayLineWidth / 2.0), cornerWidth: frameWidth / 8.0 * 1.02, cornerHeight: frameWidth / 8.0 * 1.02, transform: nil) } public var entities: [DrawingEntity] { @@ -841,7 +885,7 @@ public final class DrawingEntitiesView: UIView, TGPhotoDrawingEntitiesView { } else if self.autoSelectEntities, gestureRecognizer.numberOfTouches == 1, let viewToSelect = self.entity(at: location) { self.selectEntity(viewToSelect.entity, animate: false) self.onInteractionUpdated(true) - } else if gestureRecognizer.numberOfTouches == 2, let mediaEntityView = self.subviews.first(where: { $0 is DrawingEntityMediaView }) as? DrawingEntityMediaView { + } else if gestureRecognizer.numberOfTouches == 2 || (self.isStickerEditor && self.autoSelectEntities), let mediaEntityView = self.subviews.first(where: { $0 is DrawingEntityMediaView }) as? DrawingEntityMediaView { mediaEntityView.handlePan(gestureRecognizer) } } diff --git a/submodules/DrawingUI/Sources/DrawingScreen.swift b/submodules/DrawingUI/Sources/DrawingScreen.swift index b996d290e13..8ce1998d791 100644 --- a/submodules/DrawingUI/Sources/DrawingScreen.swift +++ b/submodules/DrawingUI/Sources/DrawingScreen.swift @@ -2634,6 +2634,7 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController, U bottom: layout.intrinsicInsets.bottom + layout.safeInsets.bottom, right: layout.safeInsets.right ), + additionalInsets: layout.additionalInsets, inputHeight: layout.inputHeight ?? 0.0, metrics: layout.metrics, deviceMetrics: layout.deviceMetrics, diff --git a/submodules/DrawingUI/Sources/ImageObjectSeparation.swift b/submodules/DrawingUI/Sources/ImageObjectSeparation.swift deleted file mode 100644 index 764975d4f6b..00000000000 --- a/submodules/DrawingUI/Sources/ImageObjectSeparation.swift +++ /dev/null @@ -1,84 +0,0 @@ -import Foundation -import UIKit -import Vision -import CoreImage -import CoreImage.CIFilterBuiltins -import SwiftSignalKit -import VideoToolbox - -private let queue = Queue() - -public func cutoutStickerImage(from image: UIImage) -> Signal { - if #available(iOS 17.0, *) { - guard let cgImage = image.cgImage else { - return .single(nil) - } - return Signal { subscriber in - let ciContext = CIContext(options: nil) - let inputImage = CIImage(cgImage: cgImage) - let handler = VNImageRequestHandler(cgImage: cgImage, options: [:]) - let request = VNGenerateForegroundInstanceMaskRequest { [weak handler] request, error in - guard let handler, let result = request.results?.first as? VNInstanceMaskObservation else { - subscriber.putNext(nil) - subscriber.putCompletion() - return - } - let instances = instances(atPoint: nil, inObservation: result) - if let mask = try? result.generateScaledMaskForImage(forInstances: instances, from: handler) { - let filter = CIFilter.blendWithMask() - filter.inputImage = inputImage - filter.backgroundImage = CIImage(color: .clear) - filter.maskImage = CIImage(cvPixelBuffer: mask) - if let output = filter.outputImage, let cgImage = ciContext.createCGImage(output, from: inputImage.extent) { - let image = UIImage(cgImage: cgImage) - subscriber.putNext(image) - subscriber.putCompletion() - return - } - } - subscriber.putNext(nil) - subscriber.putCompletion() - } - try? handler.perform([request]) - return ActionDisposable { - request.cancel() - } - } - |> runOn(queue) - } else { - return .single(nil) - } -} - -@available(iOS 17.0, *) -private func instances(atPoint maybePoint: CGPoint?, inObservation observation: VNInstanceMaskObservation) -> IndexSet { - guard let point = maybePoint else { - return observation.allInstances - } - - let instanceMap = observation.instanceMask - let coords = VNImagePointForNormalizedPoint(point, CVPixelBufferGetWidth(instanceMap) - 1, CVPixelBufferGetHeight(instanceMap) - 1) - - CVPixelBufferLockBaseAddress(instanceMap, .readOnly) - guard let pixels = CVPixelBufferGetBaseAddress(instanceMap) else { - fatalError() - } - let bytesPerRow = CVPixelBufferGetBytesPerRow(instanceMap) - let instanceLabel = pixels.load(fromByteOffset: Int(coords.y) * bytesPerRow + Int(coords.x), as: UInt8.self) - CVPixelBufferUnlockBaseAddress(instanceMap, .readOnly) - - return instanceLabel == 0 ? observation.allInstances : [Int(instanceLabel)] -} - -private extension UIImage { - convenience init?(pixelBuffer: CVPixelBuffer) { - var cgImage: CGImage? - VTCreateCGImageFromCVPixelBuffer(pixelBuffer, options: nil, imageOut: &cgImage) - - guard let cgImage = cgImage else { - return nil - } - - self.init(cgImage: cgImage) - } -} diff --git a/submodules/DrawingUI/Sources/StickerPickerScreen.swift b/submodules/DrawingUI/Sources/StickerPickerScreen.swift index f56914ff3f7..270874cdbc9 100644 --- a/submodules/DrawingUI/Sources/StickerPickerScreen.swift +++ b/submodules/DrawingUI/Sources/StickerPickerScreen.swift @@ -192,6 +192,7 @@ private final class StickerSelectionComponent: Component { insertText: { _ in }, backwardsDeleteText: {}, + openStickerEditor: {}, presentController: { [weak self] c, a in if let self, let controller = self.component?.getController() { controller.present(c, in: .window(.root), with: a) diff --git a/submodules/Emoji/Sources/EmojiUtils.swift b/submodules/Emoji/Sources/EmojiUtils.swift index 690a82aebc8..4b5033e7b85 100644 --- a/submodules/Emoji/Sources/EmojiUtils.swift +++ b/submodules/Emoji/Sources/EmojiUtils.swift @@ -2,7 +2,7 @@ import Foundation import CoreText import AVFoundation -extension Character { +public extension Character { var isSimpleEmoji: Bool { guard let firstScalar = unicodeScalars.first else { return false } if #available(iOS 10.2, macOS 10.12.2, *) { diff --git a/submodules/GalleryData/Sources/GalleryData.swift b/submodules/GalleryData/Sources/GalleryData.swift index 916c791d542..26087254b04 100644 --- a/submodules/GalleryData/Sources/GalleryData.swift +++ b/submodules/GalleryData/Sources/GalleryData.swift @@ -236,6 +236,11 @@ public func chatMessageGalleryControllerData(context: AccountContext, chatLocati }*/ } + var source = source + if standalone { + source = .standaloneMessage(message) + } + if internalDocumentItemSupportsMimeType(file.mimeType, fileName: file.fileName ?? "file") { let gallery = GalleryController(context: context, source: source ?? .peerMessagesAtId(messageId: message.id, chatLocation: chatLocation ?? .peer(id: message.id.peerId), customTag: chatFilterTag, chatLocationContextHolder: chatLocationContextHolder ?? Atomic(value: nil)), invertItemOrder: reverseMessageGalleryOrder, streamSingleVideo: stream, fromPlayingVideo: autoplayingVideo, landscape: landscape, timecode: timecode, synchronousLoad: synchronousLoad, replaceRootController: { [weak navigationController] controller, ready in navigationController?.replaceTopController(controller, animated: false, ready: ready) diff --git a/submodules/GalleryUI/Sources/ChatItemGalleryFooterContentNode.swift b/submodules/GalleryUI/Sources/ChatItemGalleryFooterContentNode.swift index 1ad9b62ab5e..29ca5ae77b8 100644 --- a/submodules/GalleryUI/Sources/ChatItemGalleryFooterContentNode.swift +++ b/submodules/GalleryUI/Sources/ChatItemGalleryFooterContentNode.swift @@ -819,8 +819,13 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, UIScroll func setMessage(_ message: Message, displayInfo: Bool = true, translateToLanguage: String? = nil, peerIsCopyProtected: Bool = false) { self.currentMessage = message + var displayInfo = displayInfo + if Namespaces.Message.allNonRegular.contains(message.id.namespace) { + displayInfo = false + } + var canDelete: Bool - var canShare = !message.containsSecretMedia + var canShare = !message.containsSecretMedia && !Namespaces.Message.allNonRegular.contains(message.id.namespace) var canFullscreen = false diff --git a/submodules/GalleryUI/Sources/GalleryController.swift b/submodules/GalleryUI/Sources/GalleryController.swift index 295df4a0122..2ddb27fafed 100644 --- a/submodules/GalleryUI/Sources/GalleryController.swift +++ b/submodules/GalleryUI/Sources/GalleryController.swift @@ -251,13 +251,18 @@ public func galleryItemForEntry( if let result = addLocallyGeneratedEntities(text, enabledTypes: [.timecode], entities: entities, mediaDuration: file.duration.flatMap(Double.init)) { entities = result } + + var originData = GalleryItemOriginData(title: message.effectiveAuthor.flatMap(EnginePeer.init)?.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder), timestamp: message.timestamp) + if Namespaces.Message.allNonRegular.contains(message.id.namespace) { + originData = GalleryItemOriginData(title: nil, timestamp: nil) + } let caption = galleryCaptionStringWithAppliedEntities(context: context, text: text, entities: entities, message: message) return UniversalVideoGalleryItem( context: context, presentationData: presentationData, content: content, - originData: GalleryItemOriginData(title: message.effectiveAuthor.flatMap(EnginePeer.init)?.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder), timestamp: message.timestamp), + originData: originData, indexData: location.flatMap { GalleryItemIndexData(position: Int32($0.index), totalCount: Int32($0.count)) }, contentInfo: .message(message), caption: caption, @@ -351,11 +356,17 @@ public func galleryItemForEntry( } description = galleryCaptionStringWithAppliedEntities(context: context, text: descriptionText, entities: entities, message: message) } + + var originData = GalleryItemOriginData(title: message.effectiveAuthor.flatMap(EnginePeer.init)?.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder), timestamp: message.timestamp) + if Namespaces.Message.allNonRegular.contains(message.id.namespace) { + originData = GalleryItemOriginData(title: nil, timestamp: nil) + } + return UniversalVideoGalleryItem( context: context, presentationData: presentationData, content: content, - originData: GalleryItemOriginData(title: message.effectiveAuthor.flatMap(EnginePeer.init)?.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder), timestamp: message.timestamp), + originData: originData, indexData: location.flatMap { GalleryItemIndexData(position: Int32($0.index), totalCount: Int32($0.count)) }, contentInfo: .message(message), caption: NSAttributedString(string: ""), @@ -611,7 +622,7 @@ public class GalleryController: ViewController, StandalonePresentableController, case let .replyThread(message): peerIdValue = message.peerId threadIdValue = message.threadId - case .feed: + case .customChatContents: break } if peerIdValue == context.account.peerId, let customTag { @@ -662,8 +673,10 @@ public class GalleryController: ViewController, StandalonePresentableController, let namespaces: MessageIdNamespaces if Namespaces.Message.allScheduled.contains(message.id.namespace) { namespaces = .just(Namespaces.Message.allScheduled) + } else if Namespaces.Message.allQuickReply.contains(message.id.namespace) { + namespaces = .just(Namespaces.Message.allQuickReply) } else { - namespaces = .not(Namespaces.Message.allScheduled) + namespaces = .not(Namespaces.Message.allNonRegular) } let inputTag: HistoryViewInputTag if let customTag { @@ -1397,8 +1410,10 @@ public class GalleryController: ViewController, StandalonePresentableController, let namespaces: MessageIdNamespaces if Namespaces.Message.allScheduled.contains(message.id.namespace) { namespaces = .just(Namespaces.Message.allScheduled) + } else if Namespaces.Message.allQuickReply.contains(message.id.namespace) { + namespaces = .just(Namespaces.Message.allQuickReply) } else { - namespaces = .not(Namespaces.Message.allScheduled) + namespaces = .not(Namespaces.Message.allNonRegular) } let signal = strongSelf.context.account.postbox.aroundMessageHistoryViewForLocation(strongSelf.context.chatLocationInput(for: chatLocation, contextHolder: chatLocationContextHolder), anchor: .index(reloadAroundIndex), ignoreMessagesInTimestampRange: nil, count: 50, clipHoles: false, fixedCombinedReadStates: nil, topTaggedMessageIdNamespaces: [], tag: tag, appendMessagesFromTheSameGroup: false, namespaces: namespaces, orderStatistics: [.combinedLocation]) |> mapToSignal { (view, _, _) -> Signal in diff --git a/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift b/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift index 688f3ea1b6e..11d90442d43 100644 --- a/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift +++ b/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift @@ -1184,7 +1184,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { var hintSeekable = false if let contentInfo = item.contentInfo, case let .message(message) = contentInfo { - if Namespaces.Message.allScheduled.contains(message.id.namespace) { + if Namespaces.Message.allNonRegular.contains(message.id.namespace) { disablePictureInPicture = true } else { let throttledSignal = videoNode.status diff --git a/submodules/HashtagSearchUI/Sources/HashtagSearchController.swift b/submodules/HashtagSearchUI/Sources/HashtagSearchController.swift index a9a40ac489e..f2ea14db130 100644 --- a/submodules/HashtagSearchUI/Sources/HashtagSearchController.swift +++ b/submodules/HashtagSearchUI/Sources/HashtagSearchController.swift @@ -102,6 +102,7 @@ public final class HashtagSearchController: TelegramBaseController { }, hideChatFolderUpdates: { }, openStories: { _, _ in }, dismissNotice: { _ in + }, editPeer: { _ in }) let previousSearchItems = Atomic<[ChatListSearchEntry]?>(value: nil) diff --git a/submodules/ItemListAddressItem/Sources/ItemListAddressItem.swift b/submodules/ItemListAddressItem/Sources/ItemListAddressItem.swift index ede2fdcd3a5..950b799e93e 100644 --- a/submodules/ItemListAddressItem/Sources/ItemListAddressItem.swift +++ b/submodules/ItemListAddressItem/Sources/ItemListAddressItem.swift @@ -20,12 +20,12 @@ public final class ItemListAddressItem: ListViewItem, ItemListItem { let style: ItemListStyle let displayDecorations: Bool let action: (() -> Void)? - let longTapAction: (() -> Void)? + let longTapAction: ((ASDisplayNode, String) -> Void)? let linkItemAction: ((TextLinkItemActionType, TextLinkItem) -> Void)? public let tag: Any? - public init(theme: PresentationTheme, label: String, text: String, imageSignal: Signal<(TransformImageArguments) -> DrawingContext?, NoError>?, selected: Bool? = nil, sectionId: ItemListSectionId, style: ItemListStyle, displayDecorations: Bool = true, action: (() -> Void)?, longTapAction: (() -> Void)? = nil, linkItemAction: ((TextLinkItemActionType, TextLinkItem) -> Void)? = nil, tag: Any? = nil) { + public init(theme: PresentationTheme, label: String, text: String, imageSignal: Signal<(TransformImageArguments) -> DrawingContext?, NoError>?, selected: Bool? = nil, sectionId: ItemListSectionId, style: ItemListStyle, displayDecorations: Bool = true, action: (() -> Void)?, longTapAction: ((ASDisplayNode, String) -> Void)? = nil, linkItemAction: ((TextLinkItemActionType, TextLinkItem) -> Void)? = nil, tag: Any? = nil) { self.theme = theme self.label = label self.text = text @@ -188,7 +188,7 @@ public class ItemListAddressItemNode: ListViewItemNode { var leftOffset: CGFloat = 0.0 var selectionNodeWidthAndApply: (CGFloat, (CGSize, Bool) -> ItemListSelectableControlNode)? if let selected = item.selected { - let (selectionWidth, selectionApply) = selectionNodeLayout(item.theme.list.itemCheckColors.strokeColor, item.theme.list.itemCheckColors.fillColor, item.theme.list.itemCheckColors.foregroundColor, selected, false) + let (selectionWidth, selectionApply) = selectionNodeLayout(item.theme.list.itemCheckColors.strokeColor, item.theme.list.itemCheckColors.fillColor, item.theme.list.itemCheckColors.foregroundColor, selected, .regular) selectionNodeWidthAndApply = (selectionWidth, selectionApply) leftOffset += selectionWidth - 8.0 } @@ -202,11 +202,11 @@ public class ItemListAddressItemNode: ListViewItemNode { let (textLayout, textApply) = makeTextLayout(TextNodeLayoutArguments(attributedString: string, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: params.width - leftOffset - leftInset - rightInset - 98.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) let padding: CGFloat = !item.label.isEmpty ? 39.0 : 20.0 - let imageSide = min(90.0, max(46.0, textLayout.size.height + padding - 18.0)) + let imageSide = min(90.0, max(66.0, textLayout.size.height + padding - 18.0)) let imageSize = CGSize(width: imageSide, height: imageSide) let imageApply = makeImageLayout(TransformImageArguments(corners: ImageCorners(radius: 4.0), imageSize: imageSize, boundingSize: imageSize, intrinsicInsets: UIEdgeInsets())) - let contentSize = CGSize(width: params.width, height: max(textLayout.size.height + padding, imageSize.height + 18.0)) + let contentSize = CGSize(width: params.width, height: max(textLayout.size.height + padding, imageSize.height + 14.0)) let nodeLayout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets) return (nodeLayout, { [weak self] animation in @@ -268,7 +268,7 @@ public class ItemListAddressItemNode: ListViewItemNode { strongSelf.labelNode.frame = CGRect(origin: CGPoint(x: leftOffset + leftInset, y: 11.0), size: labelLayout.size) strongSelf.textNode.frame = CGRect(origin: CGPoint(x: leftOffset + leftInset, y: item.label.isEmpty ? 11.0 : 31.0), size: textLayout.size) - let imageFrame = CGRect(origin: CGPoint(x: params.width - imageSize.width - rightInset, y: floorToScreenPixels((contentSize.height - imageSize.height) / 2.0)), size: imageSize) + let imageFrame = CGRect(origin: CGPoint(x: params.width - imageSize.width - rightInset, y: 7.0), size: imageSize) strongSelf.imageNode.frame = imageFrame if let icon = strongSelf.iconNode.image { @@ -396,7 +396,10 @@ public class ItemListAddressItemNode: ListViewItemNode { } override public func longTapped() { - self.item?.longTapAction?() + guard let item = self.item else { + return + } + item.longTapAction?(self, item.text) } public var tag: Any? { diff --git a/submodules/ItemListPeerActionItem/Sources/ItemListPeerActionItem.swift b/submodules/ItemListPeerActionItem/Sources/ItemListPeerActionItem.swift index 591f0177488..d51d900bf3a 100644 --- a/submodules/ItemListPeerActionItem/Sources/ItemListPeerActionItem.swift +++ b/submodules/ItemListPeerActionItem/Sources/ItemListPeerActionItem.swift @@ -55,6 +55,9 @@ public class ItemListPeerActionItem: ListViewItem, ItemListItem { if self.alwaysPlain { neighbors.top = .sameSection(alwaysPlain: false) } + if self.alwaysPlain { + neighbors.bottom = .sameSection(alwaysPlain: false) + } let (layout, apply) = node.asyncLayout()(self, params, neighbors) node.contentSize = layout.contentSize @@ -83,6 +86,9 @@ public class ItemListPeerActionItem: ListViewItem, ItemListItem { if self.alwaysPlain { neighbors.top = .sameSection(alwaysPlain: false) } + if self.alwaysPlain { + neighbors.bottom = .sameSection(alwaysPlain: false) + } let (layout, apply) = makeLayout(self, params, neighbors) Queue.mainQueue().async { completion(layout, { _ in diff --git a/submodules/ItemListStickerPackItem/Sources/ItemListStickerPackItem.swift b/submodules/ItemListStickerPackItem/Sources/ItemListStickerPackItem.swift index dd7996cd039..43710530f40 100644 --- a/submodules/ItemListStickerPackItem/Sources/ItemListStickerPackItem.swift +++ b/submodules/ItemListStickerPackItem/Sources/ItemListStickerPackItem.swift @@ -440,7 +440,7 @@ class ItemListStickerPackItemNode: ItemListRevealOptionsItemNode { if case let .check(checked) = item.control { selected = checked } - let sizeAndApply = selectableControlLayout(item.presentationData.theme.list.itemCheckColors.strokeColor, item.presentationData.theme.list.itemCheckColors.fillColor, item.presentationData.theme.list.itemCheckColors.foregroundColor, selected, true) + let sizeAndApply = selectableControlLayout(item.presentationData.theme.list.itemCheckColors.strokeColor, item.presentationData.theme.list.itemCheckColors.fillColor, item.presentationData.theme.list.itemCheckColors.foregroundColor, selected, .compact) selectableControlSizeAndApply = sizeAndApply editingOffset = sizeAndApply.0 } else { diff --git a/submodules/ItemListUI/Sources/ItemListController.swift b/submodules/ItemListUI/Sources/ItemListController.swift index ea6a8cb6336..e0bfc8910e1 100644 --- a/submodules/ItemListUI/Sources/ItemListController.swift +++ b/submodules/ItemListUI/Sources/ItemListController.swift @@ -251,6 +251,13 @@ open class ItemListController: ViewController, KeyShortcutResponder, Presentable public var willDisappear: ((Bool) -> Void)? public var didDisappear: ((Bool) -> Void)? + public var afterTransactionCompleted: (() -> Void)? { + didSet { + if self.isNodeLoaded { + (self.displayNode as! ItemListControllerNode).afterTransactionCompleted = self.afterTransactionCompleted + } + } + } public init(presentationData: ItemListPresentationData, updatedPresentationData: Signal, state: Signal<(ItemListControllerState, (ItemListNodeState, ItemGenerationArguments)), NoError>, tabBarItem: Signal?) { self.state = state @@ -486,6 +493,7 @@ open class ItemListController: ViewController, KeyShortcutResponder, Presentable displayNode.searchActivated = self.searchActivated displayNode.reorderEntry = self.reorderEntry displayNode.reorderCompleted = self.reorderCompleted + displayNode.afterTransactionCompleted = self.afterTransactionCompleted displayNode.listNode.experimentalSnapScrollToItem = self.experimentalSnapScrollToItem displayNode.listNode.didScrollWithOffset = self.didScrollWithOffset displayNode.requestLayout = { [weak self] transition in diff --git a/submodules/ItemListUI/Sources/ItemListControllerNode.swift b/submodules/ItemListUI/Sources/ItemListControllerNode.swift index 32654583a98..995c5a2d1f3 100644 --- a/submodules/ItemListUI/Sources/ItemListControllerNode.swift +++ b/submodules/ItemListUI/Sources/ItemListControllerNode.swift @@ -285,6 +285,7 @@ open class ItemListControllerNode: ASDisplayNode { public var searchActivated: ((Bool) -> Void)? public var reorderEntry: ((Int, Int, [ItemListNodeAnyEntry]) -> Signal)? public var reorderCompleted: (([ItemListNodeAnyEntry]) -> Void)? + public var afterTransactionCompleted: (() -> Void)? public var requestLayout: ((ContainedViewLayoutTransition) -> Void)? public var enableInteractiveDismiss = false { @@ -920,6 +921,8 @@ open class ItemListControllerNode: ASDisplayNode { } } } + + strongSelf.afterTransactionCompleted?() } }) var updateEmptyStateItem = false diff --git a/submodules/ItemListUI/Sources/ItemListSelectableControlNode.swift b/submodules/ItemListUI/Sources/ItemListSelectableControlNode.swift index 87cd88ca508..ceac041c539 100644 --- a/submodules/ItemListUI/Sources/ItemListSelectableControlNode.swift +++ b/submodules/ItemListUI/Sources/ItemListSelectableControlNode.swift @@ -5,6 +5,12 @@ import Display import CheckNode public final class ItemListSelectableControlNode: ASDisplayNode { + public enum Style { + case regular + case compact + case small + } + private let checkNode: CheckNode public init(strokeColor: UIColor, fillColor: UIColor, foregroundColor: UIColor) { @@ -16,8 +22,8 @@ public final class ItemListSelectableControlNode: ASDisplayNode { self.addSubnode(self.checkNode) } - public static func asyncLayout(_ node: ItemListSelectableControlNode?) -> (_ strokeColor: UIColor, _ fillColor: UIColor, _ foregroundColor: UIColor, _ selected: Bool, _ compact: Bool) -> (CGFloat, (CGSize, Bool) -> ItemListSelectableControlNode) { - return { strokeColor, fillColor, foregroundColor, selected, compact in + public static func asyncLayout(_ node: ItemListSelectableControlNode?) -> (_ strokeColor: UIColor, _ fillColor: UIColor, _ foregroundColor: UIColor, _ selected: Bool, _ style: Style) -> (CGFloat, (CGSize, Bool) -> ItemListSelectableControlNode) { + return { strokeColor, fillColor, foregroundColor, selected, style in let resultNode: ItemListSelectableControlNode if let node = node { resultNode = node @@ -25,9 +31,28 @@ public final class ItemListSelectableControlNode: ASDisplayNode { resultNode = ItemListSelectableControlNode(strokeColor: strokeColor, fillColor: fillColor, foregroundColor: foregroundColor) } - return (compact ? 38.0 : 45.0, { size, animated in - let checkSize = CGSize(width: 26.0, height: 26.0) - resultNode.checkNode.frame = CGRect(origin: CGPoint(x: compact ? 11.0 : 13.0, y: floorToScreenPixels((size.height - checkSize.height) / 2.0)), size: checkSize) + let offsetSize: CGFloat + switch style { + case .regular: + offsetSize = 45.0 + case .compact: + offsetSize = 38.0 + case .small: + offsetSize = 44.0 + } + + return (offsetSize, { size, animated in + let checkSize: CGSize + let checkOffset: CGFloat + switch style { + case .regular, .compact: + checkSize = CGSize(width: 26.0, height: 26.0) + checkOffset = style == .compact ? 11.0 : 13.0 + case .small: + checkSize = CGSize(width: 22.0, height: 22.0) + checkOffset = 16.0 + } + resultNode.checkNode.frame = CGRect(origin: CGPoint(x: checkOffset, y: floorToScreenPixels((size.height - checkSize.height) / 2.0)), size: checkSize) resultNode.checkNode.setSelected(selected, animated: animated) return resultNode }) diff --git a/submodules/ItemListUI/Sources/Items/ItemListTextWithLabelItem.swift b/submodules/ItemListUI/Sources/Items/ItemListTextWithLabelItem.swift index 6dd6f49b087..1f4a6c2f4a5 100644 --- a/submodules/ItemListUI/Sources/Items/ItemListTextWithLabelItem.swift +++ b/submodules/ItemListUI/Sources/Items/ItemListTextWithLabelItem.swift @@ -189,7 +189,7 @@ public class ItemListTextWithLabelItemNode: ListViewItemNode { var leftOffset: CGFloat = 0.0 var selectionNodeWidthAndApply: (CGFloat, (CGSize, Bool) -> ItemListSelectableControlNode)? if let selected = item.selected { - let (selectionWidth, selectionApply) = selectionNodeLayout(item.presentationData.theme.list.itemCheckColors.strokeColor, item.presentationData.theme.list.itemCheckColors.fillColor, item.presentationData.theme.list.itemCheckColors.foregroundColor, selected, false) + let (selectionWidth, selectionApply) = selectionNodeLayout(item.presentationData.theme.list.itemCheckColors.strokeColor, item.presentationData.theme.list.itemCheckColors.fillColor, item.presentationData.theme.list.itemCheckColors.foregroundColor, selected, .regular) selectionNodeWidthAndApply = (selectionWidth, selectionApply) leftOffset += selectionWidth - 8.0 } diff --git a/submodules/LegacyComponents/Sources/TGMediaAssetsController.m b/submodules/LegacyComponents/Sources/TGMediaAssetsController.m index 90541a5d095..994ddbf39e1 100644 --- a/submodules/LegacyComponents/Sources/TGMediaAssetsController.m +++ b/submodules/LegacyComponents/Sources/TGMediaAssetsController.m @@ -567,6 +567,9 @@ - (instancetype)initWithContext:(id)context intent:(TGM updateGroupingButtonVisibility(); } file:__FILE_NAME__ line:__LINE__]]; + if (_adjustmentsChangedDisposable) { + [_adjustmentsChangedDisposable dispose]; + } _adjustmentsChangedDisposable = [[SMetaDisposable alloc] init]; [_adjustmentsChangedDisposable setDisposable:[_editingContext.adjustmentsUpdatedSignal startStrictWithNext:^(__unused NSNumber *next) { @@ -583,6 +586,7 @@ - (void)dealloc self.delegate = nil; [_selectionChangedDisposable dispose]; [_tooltipDismissDisposable dispose]; + [_adjustmentsChangedDisposable dispose]; } - (void)loadView diff --git a/submodules/LegacyMediaPickerUI/Sources/LegacyAttachmentMenu.swift b/submodules/LegacyMediaPickerUI/Sources/LegacyAttachmentMenu.swift index e6101f8cc4d..6f668dfad33 100644 --- a/submodules/LegacyMediaPickerUI/Sources/LegacyAttachmentMenu.swift +++ b/submodules/LegacyMediaPickerUI/Sources/LegacyAttachmentMenu.swift @@ -222,7 +222,7 @@ public func legacyMediaEditor(context: AccountContext, peer: Peer, threadTitle: }) } -public func legacyAttachmentMenu(context: AccountContext, peer: Peer, threadTitle: String?, chatLocation: ChatLocation, editMediaOptions: LegacyAttachmentMenuMediaEditing?, saveEditedPhotos: Bool, allowGrouping: Bool, hasSchedule: Bool, canSendPolls: Bool, updatedPresentationData: (initial: PresentationData, signal: Signal), parentController: LegacyController, recentlyUsedInlineBots: [Peer], initialCaption: NSAttributedString, openGallery: @escaping () -> Void, openCamera: @escaping (TGAttachmentCameraView?, TGMenuSheetController?) -> Void, openFileGallery: @escaping () -> Void, openWebSearch: @escaping () -> Void, openMap: @escaping () -> Void, openContacts: @escaping () -> Void, openPoll: @escaping () -> Void, presentSelectionLimitExceeded: @escaping () -> Void, presentCantSendMultipleFiles: @escaping () -> Void, presentJpegConversionAlert: @escaping (@escaping (Bool) -> Void) -> Void, presentSchedulePicker: @escaping (Bool, @escaping (Int32) -> Void) -> Void, presentTimerPicker: @escaping (@escaping (Int32) -> Void) -> Void, sendMessagesWithSignals: @escaping ([Any]?, Bool, Int32, ((String) -> UIView?)?, @escaping () -> Void) -> Void, selectRecentlyUsedInlineBot: @escaping (Peer) -> Void, getCaptionPanelView: @escaping () -> TGCaptionPanelView?, present: @escaping (ViewController, Any?) -> Void) -> TGMenuSheetController { +public func legacyAttachmentMenu(context: AccountContext, peer: Peer?, threadTitle: String?, chatLocation: ChatLocation, editMediaOptions: LegacyAttachmentMenuMediaEditing?, saveEditedPhotos: Bool, allowGrouping: Bool, hasSchedule: Bool, canSendPolls: Bool, updatedPresentationData: (initial: PresentationData, signal: Signal), parentController: LegacyController, recentlyUsedInlineBots: [Peer], initialCaption: NSAttributedString, openGallery: @escaping () -> Void, openCamera: @escaping (TGAttachmentCameraView?, TGMenuSheetController?) -> Void, openFileGallery: @escaping () -> Void, openWebSearch: @escaping () -> Void, openMap: @escaping () -> Void, openContacts: @escaping () -> Void, openPoll: @escaping () -> Void, presentSelectionLimitExceeded: @escaping () -> Void, presentCantSendMultipleFiles: @escaping () -> Void, presentJpegConversionAlert: @escaping (@escaping (Bool) -> Void) -> Void, presentSchedulePicker: @escaping (Bool, @escaping (Int32) -> Void) -> Void, presentTimerPicker: @escaping (@escaping (Int32) -> Void) -> Void, sendMessagesWithSignals: @escaping ([Any]?, Bool, Int32, ((String) -> UIView?)?, @escaping () -> Void) -> Void, selectRecentlyUsedInlineBot: @escaping (Peer) -> Void, getCaptionPanelView: @escaping () -> TGCaptionPanelView?, present: @escaping (ViewController, Any?) -> Void) -> TGMenuSheetController { let defaultVideoPreset = defaultVideoPresetForContext(context) UserDefaults.standard.set(defaultVideoPreset.rawValue as NSNumber, forKey: "TG_preferredVideoPreset_v0") @@ -230,18 +230,20 @@ public func legacyAttachmentMenu(context: AccountContext, peer: Peer, threadTitl let recipientName: String if let threadTitle { recipientName = threadTitle - } else { + } else if let peer { if peer.id == context.account.peerId { recipientName = presentationData.strings.DialogList_SavedMessages } else { recipientName = EnginePeer(peer).displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) } + } else { + recipientName = "" } let actionSheetTheme = ActionSheetControllerTheme(presentationData: presentationData) let fontSize = floor(actionSheetTheme.baseFontSize * 20.0 / 17.0) - let isSecretChat = peer.id.namespace == Namespaces.Peer.SecretChat + let isSecretChat = peer?.id.namespace == Namespaces.Peer.SecretChat let controller = TGMenuSheetController(context: parentController.context, dark: false)! controller.dismissesByOutsideTap = true @@ -314,14 +316,14 @@ public func legacyAttachmentMenu(context: AccountContext, peer: Peer, threadTitl carouselItem.selectionLimitExceeded = { presentSelectionLimitExceeded() } - if peer.id != context.account.peerId { + if let peer, peer.id != context.account.peerId { if peer is TelegramUser { carouselItem.hasTimer = hasSchedule } carouselItem.hasSilentPosting = true } carouselItem.hasSchedule = hasSchedule - carouselItem.reminder = peer.id == context.account.peerId + carouselItem.reminder = peer?.id == context.account.peerId carouselItem.presentScheduleController = { media, done in presentSchedulePicker(media, { time in done?(time) @@ -449,7 +451,12 @@ public func legacyAttachmentMenu(context: AccountContext, peer: Peer, threadTitl navigationController.setNavigationBarHidden(true, animated: false) legacyController.bind(controller: navigationController) - let recipientName = EnginePeer(peer).displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) + let recipientName: String + if let peer { + recipientName = EnginePeer(peer).displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) + } else { + recipientName = "" + } legacyController.enableSizeClassSignal = true @@ -489,12 +496,14 @@ public func legacyAttachmentMenu(context: AccountContext, peer: Peer, threadTitl itemViews.append(locationItem) var peerSupportsPolls = false - if peer is TelegramGroup || peer is TelegramChannel { - peerSupportsPolls = true - } else if let user = peer as? TelegramUser, let _ = user.botInfo { - peerSupportsPolls = true + if let peer { + if peer is TelegramGroup || peer is TelegramChannel { + peerSupportsPolls = true + } else if let user = peer as? TelegramUser, let _ = user.botInfo { + peerSupportsPolls = true + } } - if peerSupportsPolls && canSendMessagesToPeer(peer) && canSendPolls { + if let peer, peerSupportsPolls, canSendMessagesToPeer(peer) && canSendPolls { let pollItem = TGMenuSheetButtonItemView(title: presentationData.strings.AttachmentMenu_Poll, type: TGMenuSheetButtonTypeDefault, fontSize: fontSize, action: { [weak controller] in controller?.dismiss(animated: true) openPoll() diff --git a/submodules/ListMessageItem/Sources/ListMessageFileItemNode.swift b/submodules/ListMessageItem/Sources/ListMessageFileItemNode.swift index c91bb0375ab..723d732467f 100644 --- a/submodules/ListMessageItem/Sources/ListMessageFileItemNode.swift +++ b/submodules/ListMessageItem/Sources/ListMessageFileItemNode.swift @@ -595,7 +595,7 @@ public final class ListMessageFileItemNode: ListMessageNode { var leftOffset: CGFloat = 0.0 var selectionNodeWidthAndApply: (CGFloat, (CGSize, Bool) -> ItemListSelectableControlNode)? if case let .selectable(selected) = item.selection { - let (selectionWidth, selectionApply) = selectionNodeLayout(item.presentationData.theme.theme.list.itemCheckColors.strokeColor, item.presentationData.theme.theme.list.itemCheckColors.fillColor, item.presentationData.theme.theme.list.itemCheckColors.foregroundColor, selected, false) + let (selectionWidth, selectionApply) = selectionNodeLayout(item.presentationData.theme.theme.list.itemCheckColors.strokeColor, item.presentationData.theme.theme.list.itemCheckColors.fillColor, item.presentationData.theme.theme.list.itemCheckColors.foregroundColor, selected, .regular) selectionNodeWidthAndApply = (selectionWidth, selectionApply) leftOffset += selectionWidth } diff --git a/submodules/ListMessageItem/Sources/ListMessageSnippetItemNode.swift b/submodules/ListMessageItem/Sources/ListMessageSnippetItemNode.swift index a640ceb8d12..a585475f196 100644 --- a/submodules/ListMessageItem/Sources/ListMessageSnippetItemNode.swift +++ b/submodules/ListMessageItem/Sources/ListMessageSnippetItemNode.swift @@ -259,7 +259,7 @@ public final class ListMessageSnippetItemNode: ListMessageNode { var leftOffset: CGFloat = 0.0 var selectionNodeWidthAndApply: (CGFloat, (CGSize, Bool) -> ItemListSelectableControlNode)? if case let .selectable(selected) = item.selection { - let (selectionWidth, selectionApply) = selectionNodeLayout(item.presentationData.theme.theme.list.itemCheckColors.strokeColor, item.presentationData.theme.theme.list.itemCheckColors.fillColor, item.presentationData.theme.theme.list.itemCheckColors.foregroundColor, selected, false) + let (selectionWidth, selectionApply) = selectionNodeLayout(item.presentationData.theme.theme.list.itemCheckColors.strokeColor, item.presentationData.theme.theme.list.itemCheckColors.fillColor, item.presentationData.theme.theme.list.itemCheckColors.foregroundColor, selected, .regular) selectionNodeWidthAndApply = (selectionWidth, selectionApply) leftOffset += selectionWidth } diff --git a/submodules/LocationUI/Sources/LocationPickerController.swift b/submodules/LocationUI/Sources/LocationPickerController.swift index c557339e3ac..6d73920913a 100644 --- a/submodules/LocationUI/Sources/LocationPickerController.swift +++ b/submodules/LocationUI/Sources/LocationPickerController.swift @@ -187,7 +187,7 @@ public final class LocationPickerController: ViewController, AttachmentContainab if ["home", "work"].contains(venueType) { completion(TelegramMediaMap(latitude: venue.latitude, longitude: venue.longitude, heading: nil, accuracyRadius: nil, geoPlace: nil, venue: nil, liveBroadcastingTimeout: nil, liveProximityNotificationRadius: nil), nil, nil, nil, nil) } else { - completion(venue, queryId, resultId, nil, nil) + completion(venue, queryId, resultId, venue.venue?.address, nil) } strongSelf.dismiss() }, toggleMapModeSelection: { [weak self] in diff --git a/submodules/LocationUI/Sources/LocationPickerControllerNode.swift b/submodules/LocationUI/Sources/LocationPickerControllerNode.swift index 7b2a3931806..069066261f0 100644 --- a/submodules/LocationUI/Sources/LocationPickerControllerNode.swift +++ b/submodules/LocationUI/Sources/LocationPickerControllerNode.swift @@ -889,11 +889,15 @@ final class LocationPickerControllerNode: ViewControllerTracingNode, CLLocationM } }) - if case let .share(_, selfPeer, _) = self.mode { + switch self.mode { + case let .share(_, selfPeer, _): if let selfPeer { self.headerNode.mapNode.userLocationAnnotation = LocationPinAnnotation(context: context, theme: self.presentationData.theme, peer: selfPeer) } self.headerNode.mapNode.hasPickerAnnotation = true + case .pick: + self.headerNode.mapNode.userLocationAnnotation = LocationPinAnnotation(context: context, theme: self.presentationData.theme, location: TelegramMediaMap(coordinate: CLLocationCoordinate2DMake(0, 0)), queryId: nil, resultId: nil, forcedSelection: true) + self.headerNode.mapNode.hasPickerAnnotation = true } self.listNode.updateFloatingHeaderOffset = { [weak self] offset, listTransition in diff --git a/submodules/MediaPickerUI/Sources/LegacyMediaPickerGallery.swift b/submodules/MediaPickerUI/Sources/LegacyMediaPickerGallery.swift index b1f9141e160..d048ffb3eac 100644 --- a/submodules/MediaPickerUI/Sources/LegacyMediaPickerGallery.swift +++ b/submodules/MediaPickerUI/Sources/LegacyMediaPickerGallery.swift @@ -228,7 +228,7 @@ func presentLegacyMediaPickerGallery(context: AccountContext, peer: EnginePeer?, }) } } - if !isScheduledMessages { + if !isScheduledMessages && peer != nil { model.interfaceView.doneLongPressed = { [weak selectionContext, weak editingContext, weak legacyController, weak model] item in if let legacyController = legacyController, let item = item as? TGMediaPickerGalleryItem, let model = model, let selectionContext = selectionContext { var effectiveHasSchedule = hasSchedule @@ -281,8 +281,8 @@ func presentLegacyMediaPickerGallery(context: AccountContext, peer: EnginePeer?, // let _ = (sendWhenOnlineAvailable - |> take(1) - |> deliverOnMainQueue).start(next: { sendWhenOnlineAvailable in + |> take(1) + |> deliverOnMainQueue).start(next: { sendWhenOnlineAvailable in let legacySheetController = LegacyController(presentation: .custom, theme: presentationData.theme, initialLayout: nil) // MARK: Nicegram RoundedVideos, canSendAsRoundedVideo added let sheetController = TGMediaPickerSendActionSheetController(context: legacyController.context, isDark: true, sendButtonFrame: model.interfaceView.doneButtonFrame, canSendAsRoundedVideo: canSendAsRoundedVideo, canSendSilently: hasSilentPosting, canSendWhenOnline: sendWhenOnlineAvailable && effectiveHasSchedule, canSchedule: effectiveHasSchedule, reminder: reminder, hasTimer: hasTimer) diff --git a/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift b/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift index 5194733f640..535be68ba9c 100644 --- a/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift +++ b/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift @@ -159,6 +159,7 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable { case wallpaper case story case addImage + case createSticker } case assets(PHAssetCollection?, AssetsMode) @@ -277,7 +278,7 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable { self.presentationData = controller.presentationData var assetType: PHAssetMediaType? - if case let .assets(_, mode) = controller.subject, [.wallpaper, .addImage].contains(mode) { + if case let .assets(_, mode) = controller.subject, [.wallpaper, .addImage, .createSticker].contains(mode) { assetType = .image } let mediaAssetsContext = MediaAssetsContext(assetType: assetType) @@ -436,7 +437,7 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable { self.gridNode.scrollView.alwaysBounceVertical = true self.gridNode.scrollView.showsVerticalScrollIndicator = false - if case let .assets(_, mode) = controller.subject, [.wallpaper, .story, .addImage].contains(mode) { + if case let .assets(_, mode) = controller.subject, [.wallpaper, .story, .addImage, .createSticker].contains(mode) { } else { let selectionGesture = MediaPickerGridSelectionGesture() @@ -1570,7 +1571,7 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable { self.titleView.title = collection.localizedTitle ?? presentationData.strings.Attachment_Gallery } else { switch mode { - case .default: + case .default, .createSticker: self.titleView.title = presentationData.strings.MediaPicker_Recents self.titleView.isEnabled = true case .story: @@ -2336,15 +2337,15 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable { return self.controllerNode.defaultTransitionView() } - fileprivate func transitionView(for identifier: String, snapshot: Bool, hideSource: Bool = false) -> UIView? { + public func transitionView(for identifier: String, snapshot: Bool, hideSource: Bool = false) -> UIView? { return self.controllerNode.transitionView(for: identifier, snapshot: snapshot, hideSource: hideSource) } - fileprivate func transitionImage(for identifier: String) -> UIImage? { + public func transitionImage(for identifier: String) -> UIImage? { return self.controllerNode.transitionImage(for: identifier) } - func updateHiddenMediaId(_ id: String?) { + public func updateHiddenMediaId(_ id: String?) { self.controllerNode.hiddenMediaId.set(.single(id)) } diff --git a/submodules/PassportUI/Sources/SecureIdPlaintextFormControllerNode.swift b/submodules/PassportUI/Sources/SecureIdPlaintextFormControllerNode.swift index a13b1d0ca6f..5e98f572940 100644 --- a/submodules/PassportUI/Sources/SecureIdPlaintextFormControllerNode.swift +++ b/submodules/PassportUI/Sources/SecureIdPlaintextFormControllerNode.swift @@ -5,7 +5,6 @@ import Display import TelegramCore import Postbox import SwiftSignalKit -import CoreTelephony import TelegramPresentationData import AccountContext import AlertUI @@ -382,17 +381,9 @@ extension SecureIdPlaintextFormInnerState { init(type: SecureIdPlaintextFormType, immediatelyAvailableValue: SecureIdValue?) { switch type { case .phone: - var countryId: String? = nil - if let carrier = CTTelephonyNetworkInfo().serviceSubscriberCellularProviders?.values.first { - countryId = carrier.isoCountryCode - } - - if countryId == nil { - countryId = (Locale.current as NSLocale).object(forKey: .countryCode) as? String - } + let countryId = (Locale.current as NSLocale).object(forKey: .countryCode) as? String var countryCodeAndId: (Int32, String) = (1, "US") - if let countryId = countryId { let normalizedId = countryId.uppercased() for (code, idAndName) in countryCodeToIdAndName { diff --git a/submodules/PeerInfoUI/Sources/DeviceContactInfoController.swift b/submodules/PeerInfoUI/Sources/DeviceContactInfoController.swift index f55b840c697..c4daf6d5e22 100644 --- a/submodules/PeerInfoUI/Sources/DeviceContactInfoController.swift +++ b/submodules/PeerInfoUI/Sources/DeviceContactInfoController.swift @@ -510,7 +510,7 @@ private enum DeviceContactInfoEntry: ItemListNodeEntry { } else { arguments.openAddress(value) } - }, longTapAction: { + }, longTapAction: { _, _ in if selected == nil { arguments.displayCopyContextMenu(.info(index), string) } diff --git a/submodules/Postbox/Sources/AdditionalMessageHistoryViewData.swift b/submodules/Postbox/Sources/AdditionalMessageHistoryViewData.swift index 9cf5cb60e61..88ccb841c34 100644 --- a/submodules/Postbox/Sources/AdditionalMessageHistoryViewData.swift +++ b/submodules/Postbox/Sources/AdditionalMessageHistoryViewData.swift @@ -1,6 +1,6 @@ import Foundation -public enum AdditionalMessageHistoryViewData { +public enum AdditionalMessageHistoryViewData: Equatable { case cachedPeerData(PeerId) case cachedPeerDataMessages(PeerId) case peerChatState(PeerId) diff --git a/submodules/Postbox/Sources/ChatLocation.swift b/submodules/Postbox/Sources/ChatLocation.swift index 45a6eaacd08..b4e4cfdb54f 100644 --- a/submodules/Postbox/Sources/ChatLocation.swift +++ b/submodules/Postbox/Sources/ChatLocation.swift @@ -4,7 +4,7 @@ import SwiftSignalKit public enum ChatLocationInput { case peer(peerId: PeerId, threadId: Int64?) case thread(peerId: PeerId, threadId: Int64, data: Signal) - case feed(id: Int32, data: Signal) + case customChatContents } public extension ChatLocationInput { @@ -14,7 +14,7 @@ public extension ChatLocationInput { return peerId case let .thread(peerId, _, _): return peerId - case .feed: + case .customChatContents: return nil } } @@ -25,7 +25,7 @@ public extension ChatLocationInput { return threadId case let .thread(_, threadId, _): return threadId - case .feed: + case .customChatContents: return nil } } diff --git a/submodules/Postbox/Sources/Message.swift b/submodules/Postbox/Sources/Message.swift index 4a85acefda2..255cf48937e 100644 --- a/submodules/Postbox/Sources/Message.swift +++ b/submodules/Postbox/Sources/Message.swift @@ -639,6 +639,10 @@ public extension MessageAttribute { public struct MessageGroupInfo: Equatable { public let stableId: UInt32 + + public init(stableId: UInt32) { + self.stableId = stableId + } } public final class Message { @@ -732,6 +736,14 @@ public final class Message { self.associatedStories = associatedStories } + public func withUpdatedStableId(stableId: UInt32) -> Message { + return Message(stableId: stableId, stableVersion: self.stableVersion, id: self.id, globallyUniqueId: self.globallyUniqueId, groupingKey: self.groupingKey, groupInfo: self.groupInfo, threadId: self.threadId, timestamp: self.timestamp, flags: self.flags, tags: self.tags, globalTags: self.globalTags, localTags: self.localTags, customTags: self.customTags, forwardInfo: self.forwardInfo, author: self.author, text: self.text, attributes: self.attributes, media: self.media, peers: self.peers, associatedMessages: self.associatedMessages, associatedMessageIds: self.associatedMessageIds, associatedMedia: self.associatedMedia, associatedThreadInfo: self.associatedThreadInfo, associatedStories: self.associatedStories) + } + + public func withUpdatedId(id: MessageId) -> Message { + return Message(stableId: self.stableId, stableVersion: self.stableVersion, id: id, globallyUniqueId: self.globallyUniqueId, groupingKey: self.groupingKey, groupInfo: self.groupInfo, threadId: self.threadId, timestamp: self.timestamp, flags: self.flags, tags: self.tags, globalTags: self.globalTags, localTags: self.localTags, customTags: self.customTags, forwardInfo: self.forwardInfo, author: self.author, text: self.text, attributes: self.attributes, media: self.media, peers: self.peers, associatedMessages: self.associatedMessages, associatedMessageIds: self.associatedMessageIds, associatedMedia: self.associatedMedia, associatedThreadInfo: self.associatedThreadInfo, associatedStories: self.associatedStories) + } + public func withUpdatedStableVersion(stableVersion: UInt32) -> Message { return Message(stableId: self.stableId, stableVersion: stableVersion, id: self.id, globallyUniqueId: self.globallyUniqueId, groupingKey: self.groupingKey, groupInfo: self.groupInfo, threadId: self.threadId, timestamp: self.timestamp, flags: self.flags, tags: self.tags, globalTags: self.globalTags, localTags: self.localTags, customTags: self.customTags, forwardInfo: self.forwardInfo, author: self.author, text: self.text, attributes: self.attributes, media: self.media, peers: self.peers, associatedMessages: self.associatedMessages, associatedMessageIds: self.associatedMessageIds, associatedMedia: self.associatedMedia, associatedThreadInfo: self.associatedThreadInfo, associatedStories: self.associatedStories) } @@ -1017,7 +1029,7 @@ final class InternalStoreMessage { } } -public enum MessageIdNamespaces { +public enum MessageIdNamespaces: Equatable { case all case just(Set) case not(Set) @@ -1033,3 +1045,23 @@ public enum MessageIdNamespaces { } } } + +public struct PeerAndThreadId: Hashable { + public var peerId: PeerId + public var threadId: Int64? + + public init(peerId: PeerId, threadId: Int64?) { + self.peerId = peerId + self.threadId = threadId + } +} + +public struct MessageAndThreadId: Hashable { + public var messageId: MessageId + public var threadId: Int64? + + public init(messageId: MessageId, threadId: Int64?) { + self.messageId = messageId + self.threadId = threadId + } +} diff --git a/submodules/Postbox/Sources/MessageHistoryView.swift b/submodules/Postbox/Sources/MessageHistoryView.swift index 22cf028d3dc..cca6c8d97d1 100644 --- a/submodules/Postbox/Sources/MessageHistoryView.swift +++ b/submodules/Postbox/Sources/MessageHistoryView.swift @@ -237,7 +237,7 @@ public enum MessageHistoryViewRelativeHoleDirection: Equatable, Hashable, Custom } } -public struct MessageHistoryViewOrderStatistics: OptionSet { +public struct MessageHistoryViewOrderStatistics: OptionSet, Equatable { public var rawValue: Int32 public init(rawValue: Int32) { @@ -290,7 +290,7 @@ public enum MessageHistoryViewInput: Equatable { case external(MessageHistoryViewExternalInput) } -public enum MessageHistoryViewReadState { +public enum MessageHistoryViewReadState: Equatable { case peer([PeerId: CombinedPeerReadState]) } @@ -302,7 +302,7 @@ public enum HistoryViewInputAnchor: Equatable { case unread } -final class MutableMessageHistoryView { +final class MutableMessageHistoryView: MutablePostboxView { private(set) var peerIds: MessageHistoryViewInput private let ignoreMessagesInTimestampRange: ClosedRange? let tag: HistoryViewInputTag? @@ -310,6 +310,7 @@ final class MutableMessageHistoryView { let namespaces: MessageIdNamespaces private let orderStatistics: MessageHistoryViewOrderStatistics private let clipHoles: Bool + private let trackHoles: Bool private let anchor: HistoryViewInputAnchor fileprivate var combinedReadStates: MessageHistoryViewReadState? @@ -333,6 +334,7 @@ final class MutableMessageHistoryView { postbox: PostboxImpl, orderStatistics: MessageHistoryViewOrderStatistics, clipHoles: Bool, + trackHoles: Bool, peerIds: MessageHistoryViewInput, ignoreMessagesInTimestampRange: ClosedRange?, anchor inputAnchor: HistoryViewInputAnchor, @@ -343,13 +345,13 @@ final class MutableMessageHistoryView { namespaces: MessageIdNamespaces, count: Int, topTaggedMessages: [MessageId.Namespace: MessageHistoryTopTaggedMessage?], - additionalDatas: [AdditionalMessageHistoryViewDataEntry], - getMessageCountInRange: (MessageIndex, MessageIndex) -> Int32 + additionalDatas: [AdditionalMessageHistoryViewDataEntry] ) { self.anchor = inputAnchor self.orderStatistics = orderStatistics self.clipHoles = clipHoles + self.trackHoles = trackHoles self.peerIds = peerIds self.ignoreMessagesInTimestampRange = ignoreMessagesInTimestampRange self.combinedReadStates = combinedReadStates @@ -1040,6 +1042,10 @@ final class MutableMessageHistoryView { } func firstHole() -> (MessageHistoryViewHole, MessageHistoryViewRelativeHoleDirection, Int, Int64?)? { + if !self.trackHoles { + return nil + } + switch self.sampledState { case let .loading(loadingSample): switch loadingSample { @@ -1065,9 +1071,13 @@ final class MutableMessageHistoryView { } } } + + func immutableView() -> PostboxView { + return MessageHistoryView(self) + } } -public final class MessageHistoryView { +public final class MessageHistoryView: PostboxView { public let tag: HistoryViewInputTag? public let namespaces: MessageIdNamespaces public let anchorIndex: MessageHistoryAnchorIndex @@ -1101,7 +1111,7 @@ public final class MessageHistoryView { self.topTaggedMessages = [] self.additionalData = [] self.isLoading = isLoading - self.isLoadingEarlier = true + self.isLoadingEarlier = false self.isAddedToChatList = false self.peerStoryStats = [:] } diff --git a/submodules/Postbox/Sources/MessageOfInterestHolesView.swift b/submodules/Postbox/Sources/MessageOfInterestHolesView.swift index 978b7b52c32..48ab8b21d8f 100644 --- a/submodules/Postbox/Sources/MessageOfInterestHolesView.swift +++ b/submodules/Postbox/Sources/MessageOfInterestHolesView.swift @@ -60,7 +60,7 @@ final class MutableMessageOfInterestHolesView: MutablePostboxView { } } self.anchor = anchor - self.wrappedView = MutableMessageHistoryView(postbox: postbox, orderStatistics: [], clipHoles: true, peerIds: peerIds, ignoreMessagesInTimestampRange: nil, anchor: self.anchor, combinedReadStates: nil, transientReadStates: nil, tag: nil, appendMessagesFromTheSameGroup: false, namespaces: .all, count: self.count, topTaggedMessages: [:], additionalDatas: [], getMessageCountInRange: { _, _ in return 0}) + self.wrappedView = MutableMessageHistoryView(postbox: postbox, orderStatistics: [], clipHoles: true, trackHoles: false, peerIds: peerIds, ignoreMessagesInTimestampRange: nil, anchor: self.anchor, combinedReadStates: nil, transientReadStates: nil, tag: nil, appendMessagesFromTheSameGroup: false, namespaces: .all, count: self.count, topTaggedMessages: [:], additionalDatas: []) let _ = self.updateFromView() } @@ -134,7 +134,7 @@ final class MutableMessageOfInterestHolesView: MutablePostboxView { case let .peer(id, threadId): peerIds = postbox.peerIdsForLocation(.peer(peerId: id, threadId: threadId), ignoreRelatedChats: false) } - self.wrappedView = MutableMessageHistoryView(postbox: postbox, orderStatistics: [], clipHoles: true, peerIds: peerIds, ignoreMessagesInTimestampRange: nil, anchor: self.anchor, combinedReadStates: nil, transientReadStates: nil, tag: nil, appendMessagesFromTheSameGroup: false, namespaces: .all, count: self.count, topTaggedMessages: [:], additionalDatas: [], getMessageCountInRange: { _, _ in return 0}) + self.wrappedView = MutableMessageHistoryView(postbox: postbox, orderStatistics: [], clipHoles: true, trackHoles: false, peerIds: peerIds, ignoreMessagesInTimestampRange: nil, anchor: self.anchor, combinedReadStates: nil, transientReadStates: nil, tag: nil, appendMessagesFromTheSameGroup: false, namespaces: .all, count: self.count, topTaggedMessages: [:], additionalDatas: []) return self.updateFromView() } else if self.wrappedView.replay(postbox: postbox, transaction: transaction) { var reloadView = false @@ -167,7 +167,7 @@ final class MutableMessageOfInterestHolesView: MutablePostboxView { case let .peer(id, threadId): peerIds = postbox.peerIdsForLocation(.peer(peerId: id, threadId: threadId), ignoreRelatedChats: false) } - self.wrappedView = MutableMessageHistoryView(postbox: postbox, orderStatistics: [], clipHoles: true, peerIds: peerIds, ignoreMessagesInTimestampRange: nil, anchor: self.anchor, combinedReadStates: nil, transientReadStates: nil, tag: nil, appendMessagesFromTheSameGroup: false, namespaces: .all, count: self.count, topTaggedMessages: [:], additionalDatas: [], getMessageCountInRange: { _, _ in return 0}) + self.wrappedView = MutableMessageHistoryView(postbox: postbox, orderStatistics: [], clipHoles: true, trackHoles: false, peerIds: peerIds, ignoreMessagesInTimestampRange: nil, anchor: self.anchor, combinedReadStates: nil, transientReadStates: nil, tag: nil, appendMessagesFromTheSameGroup: false, namespaces: .all, count: self.count, topTaggedMessages: [:], additionalDatas: []) } return self.updateFromView() diff --git a/submodules/Postbox/Sources/Postbox.swift b/submodules/Postbox/Sources/Postbox.swift index 170b35af202..2b706d4cc7e 100644 --- a/submodules/Postbox/Sources/Postbox.swift +++ b/submodules/Postbox/Sources/Postbox.swift @@ -3019,9 +3019,11 @@ final class PostboxImpl { let isInTransaction: Atomic - private func internalTransaction(_ f: (Transaction) -> T) -> (result: T, updatedTransactionStateVersion: Int64?, updatedMasterClientId: Int64?) { + private func internalTransaction(_ f: (Transaction) -> T, file: String = #file, line: Int = #line) -> (result: T, updatedTransactionStateVersion: Int64?, updatedMasterClientId: Int64?) { let _ = self.isInTransaction.swap(true) + let startTime = CFAbsoluteTimeGetCurrent() + self.valueBox.begin() let transaction = Transaction(queue: self.queue, postbox: self) self.afterBegin(transaction: transaction) @@ -3030,6 +3032,12 @@ final class PostboxImpl { transaction.disposed = true self.valueBox.commit() + let endTime = CFAbsoluteTimeGetCurrent() + let transactionDuration = endTime - startTime + if transactionDuration > 0.1 { + postboxLog("Postbox transaction took \(transactionDuration * 1000.0) ms, from: \(file), on:\(line)") + } + let _ = self.isInTransaction.swap(false) if let currentUpdatedState = self.currentUpdatedState { @@ -3069,13 +3077,14 @@ final class PostboxImpl { } } - public func transaction(userInteractive: Bool = false, ignoreDisabled: Bool = false, _ f: @escaping(Transaction) -> T) -> Signal { + + public func transaction(userInteractive: Bool = false, ignoreDisabled: Bool = false, _ f: @escaping(Transaction) -> T, file: String = #file, line: Int = #line) -> Signal { return Signal { subscriber in let f: () -> Void = { self.beginInternalTransaction(ignoreDisabled: ignoreDisabled, { let (result, updatedTransactionState, updatedMasterClientId) = self.internalTransaction({ transaction in return f(transaction) - }) + }, file: file, line: line) if updatedTransactionState != nil || updatedMasterClientId != nil { //self.pipeNotifier.notify() @@ -3104,7 +3113,7 @@ final class PostboxImpl { switch chatLocation { case let .peer(peerId, threadId): return .single((.peer(peerId: peerId, threadId: threadId), false)) - case .thread(_, _, let data), .feed(_, let data): + case .thread(_, _, let data): return Signal { subscriber in var isHoleFill = false return (data @@ -3114,6 +3123,9 @@ final class PostboxImpl { return (.external(value), wasHoleFill) }).start(next: subscriber.putNext, error: subscriber.putError, completed: subscriber.putCompletion) } + case .customChatContents: + assert(false) + return .never() } } @@ -3358,13 +3370,7 @@ final class PostboxImpl { readStates = transientReadStates } - let mutableView = MutableMessageHistoryView(postbox: self, orderStatistics: orderStatistics, clipHoles: clipHoles, peerIds: peerIds, ignoreMessagesInTimestampRange: ignoreMessagesInTimestampRange, anchor: anchor, combinedReadStates: readStates, transientReadStates: transientReadStates, tag: tag, appendMessagesFromTheSameGroup: appendMessagesFromTheSameGroup, namespaces: namespaces, count: count, topTaggedMessages: topTaggedMessages, additionalDatas: additionalDataEntries, getMessageCountInRange: { lowerBound, upperBound in - if case let .tag(tagMask) = tag { - return Int32(self.messageHistoryTable.getMessageCountInRange(peerId: lowerBound.id.peerId, namespace: lowerBound.id.namespace, tag: tagMask, lowerBound: lowerBound, upperBound: upperBound)) - } else { - return 0 - } - }) + let mutableView = MutableMessageHistoryView(postbox: self, orderStatistics: orderStatistics, clipHoles: clipHoles, trackHoles: true, peerIds: peerIds, ignoreMessagesInTimestampRange: ignoreMessagesInTimestampRange, anchor: anchor, combinedReadStates: readStates, transientReadStates: transientReadStates, tag: tag, appendMessagesFromTheSameGroup: appendMessagesFromTheSameGroup, namespaces: namespaces, count: count, topTaggedMessages: topTaggedMessages, additionalDatas: additionalDataEntries) let initialUpdateType: ViewUpdateType = .Initial @@ -4421,12 +4427,12 @@ public class Postbox { } } - public func transaction(userInteractive: Bool = false, ignoreDisabled: Bool = false, _ f: @escaping(Transaction) -> T) -> Signal { + public func transaction(userInteractive: Bool = false, ignoreDisabled: Bool = false, _ f: @escaping(Transaction) -> T, file: String = #file, line: Int = #line) -> Signal { return Signal { subscriber in let disposable = MetaDisposable() self.impl.with { impl in - disposable.set(impl.transaction(userInteractive: userInteractive, ignoreDisabled: ignoreDisabled, f).start(next: subscriber.putNext, error: subscriber.putError, completed: subscriber.putCompletion)) + disposable.set(impl.transaction(userInteractive: userInteractive, ignoreDisabled: ignoreDisabled, f, file: file, line: line).start(next: subscriber.putNext, error: subscriber.putError, completed: subscriber.putCompletion)) } return disposable diff --git a/submodules/Postbox/Sources/PostboxView.swift b/submodules/Postbox/Sources/PostboxView.swift index 7cf89152ae5..eb629a5d168 100644 --- a/submodules/Postbox/Sources/PostboxView.swift +++ b/submodules/Postbox/Sources/PostboxView.swift @@ -1,9 +1,9 @@ import Foundation -public protocol PostboxView { +public protocol PostboxView: AnyObject { } -protocol MutablePostboxView { +protocol MutablePostboxView: AnyObject { func replay(postbox: PostboxImpl, transaction: PostboxTransaction) -> Bool func refreshDueToExternalTransaction(postbox: PostboxImpl) -> Bool func immutableView() -> PostboxView @@ -16,14 +16,71 @@ final class CombinedMutableView { self.views = views } - func replay(postbox: PostboxImpl, transaction: PostboxTransaction) -> Bool { - var updated = false + func replay(postbox: PostboxImpl, transaction: PostboxTransaction) -> (updated: Bool, updateTrackedHoles: Bool) { + var anyUpdated = false + var updateTrackedHoles = false for (_, view) in self.views { - if view.replay(postbox: postbox, transaction: transaction) { - updated = true + if let mutableView = view as? MutableMessageHistoryView { + var innerUpdated = false + + let previousPeerIds = mutableView.peerIds + + if mutableView.replay(postbox: postbox, transaction: transaction) { + innerUpdated = true + } + + var updateType: ViewUpdateType = .Generic + switch mutableView.peerIds { + case let .single(peerId, threadId): + for key in transaction.currentPeerHoleOperations.keys { + if key.peerId == peerId && key.threadId == threadId { + updateType = .FillHole + break + } + } + case .associated: + var ids = Set() + switch mutableView.peerIds { + case .single, .external: + assertionFailure() + case let .associated(mainPeerId, associatedId): + ids.insert(mainPeerId) + if let associatedId = associatedId { + ids.insert(associatedId.peerId) + } + } + + if !ids.isEmpty { + for key in transaction.currentPeerHoleOperations.keys { + if ids.contains(key.peerId) { + updateType = .FillHole + break + } + } + } + case .external: + break + } + + mutableView.updatePeerIds(transaction: transaction) + if mutableView.peerIds != previousPeerIds { + updateType = .UpdateVisible + + let _ = mutableView.refreshDueToExternalTransaction(postbox: postbox) + innerUpdated = true + } + + if innerUpdated { + anyUpdated = true + updateTrackedHoles = true + let _ = updateType + //pipe.putNext((MessageHistoryView(mutableView), updateType)) + } + } else if view.replay(postbox: postbox, transaction: transaction) { + anyUpdated = true } } - return updated + return (anyUpdated, updateTrackedHoles) } func refreshDueToExternalTransaction(postbox: PostboxImpl) -> Bool { diff --git a/submodules/Postbox/Sources/ViewTracker.swift b/submodules/Postbox/Sources/ViewTracker.swift index 12d96f1118d..ff3f3252c8f 100644 --- a/submodules/Postbox/Sources/ViewTracker.swift +++ b/submodules/Postbox/Sources/ViewTracker.swift @@ -1,7 +1,7 @@ import Foundation import SwiftSignalKit -public enum ViewUpdateType : Equatable { +public enum ViewUpdateType: Equatable { case Initial case InitialUnread(MessageIndex) case Generic @@ -351,10 +351,6 @@ final class ViewTracker { self.updateTrackedChatListHoles() - if updateTrackedHoles { - self.updateTrackedHoles() - } - if self.unsentMessageView.replay(transaction.unsentMessageOperations) { self.unsentViewUpdated() } @@ -414,9 +410,14 @@ final class ViewTracker { } for (mutableView, pipe) in self.combinedViews.copyItems() { - if mutableView.replay(postbox: postbox, transaction: transaction) { + let result = mutableView.replay(postbox: postbox, transaction: transaction) + + if result.updated { pipe.putNext(mutableView.immutableView()) } + if result.updateTrackedHoles { + updateTrackedHoles = true + } } for (view, pipe) in self.failedMessageIdsViews.copyItems() { @@ -425,6 +426,10 @@ final class ViewTracker { } } + if updateTrackedHoles { + self.updateTrackedHoles() + } + self.updateTrackedForumTopicListHoles() } @@ -486,6 +491,26 @@ final class ViewTracker { firstHolesAndTags.insert(MessageHistoryHolesViewEntry(hole: hole, direction: direction, space: space, count: count, userId: userId)) } } + for (view, _) in self.combinedViews.copyItems() { + for (_, subview) in view.views { + if let subview = subview as? MutableMessageHistoryView { + if let (hole, direction, count, userId) = subview.firstHole() { + let space: MessageHistoryHoleOperationSpace + if let tag = subview.tag { + switch tag { + case let .tag(value): + space = .tag(value) + case let .customTag(value, regularTag): + space = .customTag(value, regularTag) + } + } else { + space = .everywhere + } + firstHolesAndTags.insert(MessageHistoryHolesViewEntry(hole: hole, direction: direction, space: space, count: count, userId: userId)) + } + } + } + } if self.messageHistoryHolesView.update(firstHolesAndTags) { for subscriber in self.messageHistoryHolesViewSubscribers.copyItems() { diff --git a/submodules/Postbox/Sources/Views.swift b/submodules/Postbox/Sources/Views.swift index 9fd7e8b9e47..c3b0e95f187 100644 --- a/submodules/Postbox/Sources/Views.swift +++ b/submodules/Postbox/Sources/Views.swift @@ -1,6 +1,52 @@ import Foundation public enum PostboxViewKey: Hashable { + public struct HistoryView: Equatable { + public var peerId: PeerId + public var threadId: Int64? + public var clipHoles: Bool + public var trackHoles: Bool + public var orderStatistics: MessageHistoryViewOrderStatistics + public var ignoreMessagesInTimestampRange: ClosedRange? + public var anchor: HistoryViewInputAnchor + public var combinedReadStates: MessageHistoryViewReadState? + public var transientReadStates: MessageHistoryViewReadState? + public var tag: HistoryViewInputTag? + public var appendMessagesFromTheSameGroup: Bool + public var namespaces: MessageIdNamespaces + public var count: Int + + public init( + peerId: PeerId, + threadId: Int64?, + clipHoles: Bool, + trackHoles: Bool, + orderStatistics: MessageHistoryViewOrderStatistics = [], + ignoreMessagesInTimestampRange: ClosedRange? = nil, + anchor: HistoryViewInputAnchor, + combinedReadStates: MessageHistoryViewReadState? = nil, + transientReadStates: MessageHistoryViewReadState? = nil, + tag: HistoryViewInputTag? = nil, + appendMessagesFromTheSameGroup: Bool, + namespaces: MessageIdNamespaces, + count: Int + ) { + self.peerId = peerId + self.threadId = threadId + self.clipHoles = clipHoles + self.trackHoles = trackHoles + self.orderStatistics = orderStatistics + self.ignoreMessagesInTimestampRange = ignoreMessagesInTimestampRange + self.anchor = anchor + self.combinedReadStates = combinedReadStates + self.transientReadStates = transientReadStates + self.tag = tag + self.appendMessagesFromTheSameGroup = appendMessagesFromTheSameGroup + self.namespaces = namespaces + self.count = count + } + } + case itemCollectionInfos(namespaces: [ItemCollectionId.Namespace]) case itemCollectionIds(namespaces: [ItemCollectionId.Namespace]) case itemCollectionInfo(id: ItemCollectionId) @@ -50,6 +96,7 @@ public enum PostboxViewKey: Hashable { case savedMessagesIndex(peerId: PeerId) case savedMessagesStats(peerId: PeerId) case chatInterfaceState(peerId: PeerId) + case historyView(HistoryView) public func hash(into hasher: inout Hasher) { switch self { @@ -168,6 +215,10 @@ public enum PostboxViewKey: Hashable { hasher.combine(peerId) case let .chatInterfaceState(peerId): hasher.combine(peerId) + case let .historyView(historyView): + hasher.combine(20) + hasher.combine(historyView.peerId) + hasher.combine(historyView.threadId) } } @@ -467,6 +518,12 @@ public enum PostboxViewKey: Hashable { } else { return false } + case let .historyView(historyView): + if case .historyView(historyView) = rhs { + return true + } else { + return false + } } } } @@ -571,5 +628,23 @@ func postboxViewForKey(postbox: PostboxImpl, key: PostboxViewKey) -> MutablePost return MutableMessageHistorySavedMessagesStatsView(postbox: postbox, peerId: peerId) case let .chatInterfaceState(peerId): return MutableChatInterfaceStateView(postbox: postbox, peerId: peerId) + case let .historyView(historyView): + return MutableMessageHistoryView( + postbox: postbox, + orderStatistics: historyView.orderStatistics, + clipHoles: historyView.clipHoles, + trackHoles: historyView.trackHoles, + peerIds: .single(peerId: historyView.peerId, threadId: historyView.threadId), + ignoreMessagesInTimestampRange: historyView.ignoreMessagesInTimestampRange, + anchor: historyView.anchor, + combinedReadStates: historyView.combinedReadStates, + transientReadStates: historyView.transientReadStates, + tag: historyView.tag, + appendMessagesFromTheSameGroup: historyView.appendMessagesFromTheSameGroup, + namespaces: historyView.namespaces, + count: historyView.count, + topTaggedMessages: [:], + additionalDatas: [] + ) } } diff --git a/submodules/PremiumUI/BUILD b/submodules/PremiumUI/BUILD index fe7b51fe7b9..078e57cf19a 100644 --- a/submodules/PremiumUI/BUILD +++ b/submodules/PremiumUI/BUILD @@ -118,6 +118,10 @@ swift_library( "//submodules/AlertUI", "//submodules/TelegramUI/Components/Chat/MergedAvatarsNode", "//submodules/TelegramUI/Components/LottieComponent", + "//submodules/TelegramUI/Components/ListSectionComponent", + "//submodules/TelegramUI/Components/ListActionItemComponent", + "//submodules/TelegramUI/Components/EmojiStatusSelectionComponent", + "//submodules/TelegramUI/Components/EntityKeyboard", ], visibility = [ "//visibility:public", diff --git a/submodules/PremiumUI/Resources/badge b/submodules/PremiumUI/Resources/badge new file mode 100644 index 00000000000..933f6153e38 Binary files /dev/null and b/submodules/PremiumUI/Resources/badge differ diff --git a/submodules/PremiumUI/Resources/badge.scn b/submodules/PremiumUI/Resources/badge.scn deleted file mode 100644 index 6c59e8ffb0f..00000000000 Binary files a/submodules/PremiumUI/Resources/badge.scn and /dev/null differ diff --git a/submodules/PremiumUI/Resources/boost b/submodules/PremiumUI/Resources/boost new file mode 100644 index 00000000000..3c0b0748ef5 Binary files /dev/null and b/submodules/PremiumUI/Resources/boost differ diff --git a/submodules/PremiumUI/Resources/boost.scn b/submodules/PremiumUI/Resources/boost.scn deleted file mode 100644 index d301679181d..00000000000 Binary files a/submodules/PremiumUI/Resources/boost.scn and /dev/null differ diff --git a/submodules/PremiumUI/Resources/business.png b/submodules/PremiumUI/Resources/business.png new file mode 100644 index 00000000000..ed3acfa1f7a Binary files /dev/null and b/submodules/PremiumUI/Resources/business.png differ diff --git a/submodules/PremiumUI/Resources/business.scn b/submodules/PremiumUI/Resources/business.scn new file mode 100644 index 00000000000..a88303fa791 Binary files /dev/null and b/submodules/PremiumUI/Resources/business.scn differ diff --git a/submodules/PremiumUI/Resources/coin b/submodules/PremiumUI/Resources/coin new file mode 100644 index 00000000000..dbb4f922899 Binary files /dev/null and b/submodules/PremiumUI/Resources/coin differ diff --git a/submodules/PremiumUI/Resources/coin_anim.png b/submodules/PremiumUI/Resources/coin_anim.png new file mode 100644 index 00000000000..8ce4e406ab3 Binary files /dev/null and b/submodules/PremiumUI/Resources/coin_anim.png differ diff --git a/submodules/PremiumUI/Resources/coin_edge.png b/submodules/PremiumUI/Resources/coin_edge.png new file mode 100644 index 00000000000..f0bf2761f18 Binary files /dev/null and b/submodules/PremiumUI/Resources/coin_edge.png differ diff --git a/submodules/PremiumUI/Resources/darkerTexture.jpg b/submodules/PremiumUI/Resources/darkerTexture.jpg new file mode 100644 index 00000000000..75d2fa9fa66 Binary files /dev/null and b/submodules/PremiumUI/Resources/darkerTexture.jpg differ diff --git a/submodules/PremiumUI/Resources/diagonal_shine.png b/submodules/PremiumUI/Resources/diagonal_shine.png new file mode 100644 index 00000000000..1194211fb7d Binary files /dev/null and b/submodules/PremiumUI/Resources/diagonal_shine.png differ diff --git a/submodules/PremiumUI/Resources/emoji b/submodules/PremiumUI/Resources/emoji new file mode 100644 index 00000000000..6eddddf5e4d Binary files /dev/null and b/submodules/PremiumUI/Resources/emoji differ diff --git a/submodules/PremiumUI/Resources/emoji.scn b/submodules/PremiumUI/Resources/emoji.scn deleted file mode 100644 index f4f25b3ba7a..00000000000 Binary files a/submodules/PremiumUI/Resources/emoji.scn and /dev/null differ diff --git a/submodules/PremiumUI/Resources/gift b/submodules/PremiumUI/Resources/gift new file mode 100644 index 00000000000..87a9c2a5e84 Binary files /dev/null and b/submodules/PremiumUI/Resources/gift differ diff --git a/submodules/PremiumUI/Resources/gift.scn b/submodules/PremiumUI/Resources/gift.scn deleted file mode 100644 index 9a6e2769d9b..00000000000 Binary files a/submodules/PremiumUI/Resources/gift.scn and /dev/null differ diff --git a/submodules/PremiumUI/Resources/lighterTexture.jpg b/submodules/PremiumUI/Resources/lighterTexture.jpg new file mode 100644 index 00000000000..02937417477 Binary files /dev/null and b/submodules/PremiumUI/Resources/lighterTexture.jpg differ diff --git a/submodules/PremiumUI/Resources/lightspeed b/submodules/PremiumUI/Resources/lightspeed new file mode 100644 index 00000000000..bf04894009f Binary files /dev/null and b/submodules/PremiumUI/Resources/lightspeed differ diff --git a/submodules/PremiumUI/Resources/lightspeed.scn b/submodules/PremiumUI/Resources/lightspeed.scn deleted file mode 100644 index 660d751f6d9..00000000000 Binary files a/submodules/PremiumUI/Resources/lightspeed.scn and /dev/null differ diff --git a/submodules/PremiumUI/Resources/star b/submodules/PremiumUI/Resources/star index 3c0c2710e31..3b27e0220e6 100644 Binary files a/submodules/PremiumUI/Resources/star and b/submodules/PremiumUI/Resources/star differ diff --git a/submodules/PremiumUI/Resources/swirl b/submodules/PremiumUI/Resources/swirl new file mode 100644 index 00000000000..cd47c82fbb7 Binary files /dev/null and b/submodules/PremiumUI/Resources/swirl differ diff --git a/submodules/PremiumUI/Resources/swirl.scn b/submodules/PremiumUI/Resources/swirl.scn deleted file mode 100644 index 6238c1d96d9..00000000000 Binary files a/submodules/PremiumUI/Resources/swirl.scn and /dev/null differ diff --git a/submodules/PremiumUI/Resources/tag b/submodules/PremiumUI/Resources/tag new file mode 100644 index 00000000000..5c89a52aa15 Binary files /dev/null and b/submodules/PremiumUI/Resources/tag differ diff --git a/submodules/PremiumUI/Resources/tag.scn b/submodules/PremiumUI/Resources/tag.scn deleted file mode 100644 index 03eef5673cc..00000000000 Binary files a/submodules/PremiumUI/Resources/tag.scn and /dev/null differ diff --git a/submodules/PremiumUI/Resources/texture.jpg b/submodules/PremiumUI/Resources/texture.jpg index 52057beb40d..c4ff553a065 100644 Binary files a/submodules/PremiumUI/Resources/texture.jpg and b/submodules/PremiumUI/Resources/texture.jpg differ diff --git a/submodules/PremiumUI/Sources/BadgeBusinessView.swift b/submodules/PremiumUI/Sources/BadgeBusinessView.swift new file mode 100644 index 00000000000..1127c153662 --- /dev/null +++ b/submodules/PremiumUI/Sources/BadgeBusinessView.swift @@ -0,0 +1,61 @@ +import Foundation +import UIKit +import SceneKit +import Display +import AppBundle + +private let sceneVersion: Int = 1 + +final class BadgeBusinessView: UIView, PhoneDemoDecorationView { + private let sceneView: SCNView + + private var leftParticles: SCNNode? + private var rightParticles: SCNNode? + + override init(frame: CGRect) { + self.sceneView = SCNView(frame: CGRect(origin: .zero, size: frame.size)) + self.sceneView.backgroundColor = .clear + if let scene = loadCompressedScene(name: "business", version: sceneVersion) { + self.sceneView.scene = scene + } + self.sceneView.isUserInteractionEnabled = false + self.sceneView.preferredFramesPerSecond = 60 + + super.init(frame: frame) + + self.alpha = 0.0 + + self.addSubview(self.sceneView) + + self.leftParticles = self.sceneView.scene?.rootNode.childNode(withName: "leftParticles", recursively: false) + self.rightParticles = self.sceneView.scene?.rootNode.childNode(withName: "rightParticles", recursively: false) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func setVisible(_ visible: Bool) { + if visible, let leftParticles = self.leftParticles, let rightParticles = self.rightParticles, leftParticles.parent == nil { + self.sceneView.scene?.rootNode.addChildNode(leftParticles) + self.sceneView.scene?.rootNode.addChildNode(rightParticles) + } + + let transition = ContainedViewLayoutTransition.animated(duration: 0.3, curve: .linear) + transition.updateAlpha(layer: self.layer, alpha: visible ? 0.5 : 0.0, completion: { [weak self] finished in + if let strongSelf = self, finished && !visible && strongSelf.leftParticles?.parent != nil { + strongSelf.leftParticles?.removeFromParentNode() + strongSelf.rightParticles?.removeFromParentNode() + } + }) + } + + func resetAnimation() { + } + + override func layoutSubviews() { + super.layoutSubviews() + + self.sceneView.frame = CGRect(origin: .zero, size: frame.size) + } +} diff --git a/submodules/PremiumUI/Sources/BadgeStarsView.swift b/submodules/PremiumUI/Sources/BadgeStarsView.swift index f8f80a116d3..f1374916e56 100644 --- a/submodules/PremiumUI/Sources/BadgeStarsView.swift +++ b/submodules/PremiumUI/Sources/BadgeStarsView.swift @@ -13,8 +13,8 @@ final class BadgeStarsView: UIView, PhoneDemoDecorationView { override init(frame: CGRect) { self.sceneView = SCNView(frame: CGRect(origin: .zero, size: frame.size)) self.sceneView.backgroundColor = .clear - if let url = getAppBundle().url(forResource: "badge", withExtension: "scn") { - self.sceneView.scene = try? SCNScene(url: url, options: nil) + if let scene = loadCompressedScene(name: "badge", version: 1) { + self.sceneView.scene = scene } self.sceneView.isUserInteractionEnabled = false self.sceneView.preferredFramesPerSecond = 60 @@ -67,8 +67,8 @@ final class EmojiStarsView: UIView, PhoneDemoDecorationView { override init(frame: CGRect) { self.sceneView = SCNView(frame: CGRect(origin: .zero, size: frame.size)) self.sceneView.backgroundColor = .clear - if let url = getAppBundle().url(forResource: "emoji", withExtension: "scn") { - self.sceneView.scene = try? SCNScene(url: url, options: nil) + if let scene = loadCompressedScene(name: "emoji", version: 1) { + self.sceneView.scene = scene } self.sceneView.isUserInteractionEnabled = false self.sceneView.preferredFramesPerSecond = 60 @@ -121,8 +121,8 @@ final class TagStarsView: UIView, PhoneDemoDecorationView { override init(frame: CGRect) { self.sceneView = SCNView(frame: CGRect(origin: .zero, size: frame.size)) self.sceneView.backgroundColor = .clear - if let url = getAppBundle().url(forResource: "tag", withExtension: "scn") { - self.sceneView.scene = try? SCNScene(url: url, options: nil) + if let scene = loadCompressedScene(name: "tag", version: 1) { + self.sceneView.scene = scene } self.sceneView.isUserInteractionEnabled = false self.sceneView.preferredFramesPerSecond = 60 diff --git a/submodules/PremiumUI/Sources/BoostHeaderBackgroundComponent.swift b/submodules/PremiumUI/Sources/BoostHeaderBackgroundComponent.swift index b82af37cfd7..fc1216ae68e 100644 --- a/submodules/PremiumUI/Sources/BoostHeaderBackgroundComponent.swift +++ b/submodules/PremiumUI/Sources/BoostHeaderBackgroundComponent.swift @@ -12,7 +12,7 @@ import TelegramCore import MultilineTextComponent import TelegramPresentationData -private let sceneVersion: Int = 3 +private let sceneVersion: Int = 1 public final class BoostHeaderBackgroundComponent: Component { let isVisible: Bool @@ -58,7 +58,7 @@ public final class BoostHeaderBackgroundComponent: Component { private func setup() { - guard let url = getAppBundle().url(forResource: "boost", withExtension: "scn"), let scene = try? SCNScene(url: url, options: nil) else { + guard let scene = loadCompressedScene(name: "boost", version: sceneVersion) else { return } diff --git a/submodules/PremiumUI/Sources/BusinessPageComponent.swift b/submodules/PremiumUI/Sources/BusinessPageComponent.swift new file mode 100644 index 00000000000..4a1be46cf05 --- /dev/null +++ b/submodules/PremiumUI/Sources/BusinessPageComponent.swift @@ -0,0 +1,652 @@ +import Foundation +import UIKit +import Display +import ComponentFlow +import SwiftSignalKit +import TelegramCore +import AccountContext +import MultilineTextComponent +import BlurredBackgroundComponent +import Markdown +import TelegramPresentationData +import BundleIconComponent + +private final class HeaderComponent: Component { + let context: AccountContext + let theme: PresentationTheme + let strings: PresentationStrings + + init(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings) { + self.context = context + self.theme = theme + self.strings = strings + } + + static func ==(lhs: HeaderComponent, rhs: HeaderComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.theme !== rhs.theme { + return false + } + return true + } + + final class View: UIView { + private let coin = ComponentView() + private let text = ComponentView() + + private var component: HeaderComponent? + private weak var state: EmptyComponentState? + + override init(frame: CGRect) { + super.init(frame: frame) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func update(component: HeaderComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + self.component = component + self.state = state + + let containerSize = CGSize(width: min(414.0, availableSize.width), height: 220.0) + + let coinSize = self.coin.update( + transition: .immediate, + component: AnyComponent(PremiumCoinComponent(isIntro: true, isVisible: true, hasIdleAnimations: true)), + environment: {}, + containerSize: containerSize + ) + if let view = self.coin.view { + if view.superview == nil { + self.addSubview(view) + } + view.frame = CGRect(origin: CGPoint(x: floor((availableSize.width - coinSize.width) / 2.0), y: -84.0), size: coinSize) + } + + let textSize = self.text.update( + transition: .immediate, + component: AnyComponent( + MultilineTextComponent( + text: .plain(NSAttributedString(string: component.strings.Premium_Business_Description, font: Font.regular(15.0), textColor: component.theme.list.itemPrimaryTextColor)), + horizontalAlignment: .center, + maximumNumberOfLines: 0, + lineSpacing: 0.2 + ) + ), + environment: {}, + containerSize: CGSize(width: availableSize.width - 32.0, height: 1000.0) + ) + if let view = self.text.view { + if view.superview == nil { + self.addSubview(view) + } + view.frame = CGRect(origin: CGPoint(x: floor((availableSize.width - textSize.width) / 2.0), y: 139.0), size: textSize) + } + + return CGSize(width: availableSize.width, height: 210.0) + } + } + + func makeView() -> View { + return View(frame: CGRect()) + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} + +private final class ParagraphComponent: CombinedComponent { + let title: String + let titleColor: UIColor + let text: String + let textColor: UIColor + let iconName: String + let iconColor: UIColor + + public init( + title: String, + titleColor: UIColor, + text: String, + textColor: UIColor, + iconName: String, + iconColor: UIColor + ) { + self.title = title + self.titleColor = titleColor + self.text = text + self.textColor = textColor + self.iconName = iconName + self.iconColor = iconColor + } + + static func ==(lhs: ParagraphComponent, rhs: ParagraphComponent) -> Bool { + if lhs.title != rhs.title { + return false + } + if lhs.titleColor != rhs.titleColor { + return false + } + if lhs.text != rhs.text { + return false + } + if lhs.textColor != rhs.textColor { + return false + } + if lhs.iconName != rhs.iconName { + return false + } + if lhs.iconColor != rhs.iconColor { + return false + } + return true + } + + static var body: Body { + let title = Child(MultilineTextComponent.self) + let text = Child(MultilineTextComponent.self) + let icon = Child(BundleIconComponent.self) + + return { context in + let component = context.component + + let leftInset: CGFloat = 64.0 + let rightInset: CGFloat = 32.0 + let textSideInset: CGFloat = leftInset + 8.0 + let spacing: CGFloat = 5.0 + + let textTopInset: CGFloat = 9.0 + + let title = title.update( + component: MultilineTextComponent( + text: .plain(NSAttributedString( + string: component.title, + font: Font.semibold(15.0), + textColor: component.titleColor, + paragraphAlignment: .natural + )), + horizontalAlignment: .center, + maximumNumberOfLines: 1 + ), + availableSize: CGSize(width: context.availableSize.width - leftInset - rightInset, height: CGFloat.greatestFiniteMagnitude), + transition: .immediate + ) + + let textFont = Font.regular(15.0) + let boldTextFont = Font.semibold(15.0) + let textColor = component.textColor + let markdownAttributes = MarkdownAttributes( + body: MarkdownAttributeSet(font: textFont, textColor: textColor), + bold: MarkdownAttributeSet(font: boldTextFont, textColor: textColor), + link: MarkdownAttributeSet(font: textFont, textColor: textColor), + linkAttribute: { _ in + return nil + } + ) + + let text = text.update( + component: MultilineTextComponent( + text: .markdown(text: component.text, attributes: markdownAttributes), + horizontalAlignment: .natural, + maximumNumberOfLines: 0, + lineSpacing: 0.2 + ), + availableSize: CGSize(width: context.availableSize.width - leftInset - rightInset, height: context.availableSize.height), + transition: .immediate + ) + + let icon = icon.update( + component: BundleIconComponent( + name: component.iconName, + tintColor: component.iconColor + ), + availableSize: CGSize(width: context.availableSize.width, height: context.availableSize.height), + transition: .immediate + ) + + context.add(title + .position(CGPoint(x: textSideInset + title.size.width / 2.0, y: textTopInset + title.size.height / 2.0)) + ) + + context.add(text + .position(CGPoint(x: textSideInset + text.size.width / 2.0, y: textTopInset + title.size.height + spacing + text.size.height / 2.0)) + ) + + context.add(icon + .position(CGPoint(x: 47.0, y: textTopInset + 18.0)) + ) + + return CGSize(width: context.availableSize.width, height: textTopInset + title.size.height + text.size.height + 25.0) + } + } +} + +private final class BusinessListComponent: CombinedComponent { + typealias EnvironmentType = (Empty, ScrollChildEnvironment) + + let context: AccountContext + let theme: PresentationTheme + let topInset: CGFloat + let bottomInset: CGFloat + + init(context: AccountContext, theme: PresentationTheme, topInset: CGFloat, bottomInset: CGFloat) { + self.context = context + self.theme = theme + self.topInset = topInset + self.bottomInset = bottomInset + } + + static func ==(lhs: BusinessListComponent, rhs: BusinessListComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.theme !== rhs.theme { + return false + } + if lhs.topInset != rhs.topInset { + return false + } + if lhs.bottomInset != rhs.bottomInset { + return false + } + return true + } + + final class State: ComponentState { + private let context: AccountContext + + private var disposable: Disposable? + var limits: EngineConfiguration.UserLimits = .defaultValue + var premiumLimits: EngineConfiguration.UserLimits = .defaultValue + + var accountPeer: EnginePeer? + + init(context: AccountContext) { + self.context = context + + super.init() + + self.disposable = (context.engine.data.get( + TelegramEngine.EngineData.Item.Configuration.UserLimits(isPremium: false), + TelegramEngine.EngineData.Item.Configuration.UserLimits(isPremium: true), + TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId) + ) + |> deliverOnMainQueue).start(next: { [weak self] limits, premiumLimits, accountPeer in + if let strongSelf = self { + strongSelf.limits = limits + strongSelf.premiumLimits = premiumLimits + strongSelf.accountPeer = accountPeer + strongSelf.updated(transition: .immediate) + } + }) + } + + deinit { + self.disposable?.dispose() + } + } + + func makeState() -> State { + return State(context: self.context) + } + + static var body: Body { + let list = Child(List.self) + + return { context in + let theme = context.component.theme + let strings = context.component.context.sharedContext.currentPresentationData.with { $0 }.strings + + let colors = [ + UIColor(rgb: 0x007aff), + UIColor(rgb: 0x798aff), + UIColor(rgb: 0xac64f3), + UIColor(rgb: 0xc456ae), + UIColor(rgb: 0xe95d44), + UIColor(rgb: 0xf2822a), + UIColor(rgb: 0xe79519), + UIColor(rgb: 0xe7ad19) + ] + + let titleColor = theme.list.itemPrimaryTextColor + let textColor = theme.list.itemSecondaryTextColor + + var items: [AnyComponentWithIdentity] = [] + + items.append( + AnyComponentWithIdentity( + id: "header", + component: AnyComponent(HeaderComponent( + context: context.component.context, + theme: theme, + strings: strings + )) + ) + ) + + items.append( + AnyComponentWithIdentity( + id: "location", + component: AnyComponent(ParagraphComponent( + title: strings.Premium_Business_Location_Title, + titleColor: titleColor, + text: strings.Premium_Business_Location_Text, + textColor: textColor, + iconName: "Premium/Business/Location", + iconColor: colors[0] + )) + ) + ) + + items.append( + AnyComponentWithIdentity( + id: "hours", + component: AnyComponent(ParagraphComponent( + title: strings.Premium_Business_Hours_Title, + titleColor: titleColor, + text: strings.Premium_Business_Hours_Text, + textColor: textColor, + iconName: "Premium/Business/Hours", + iconColor: colors[1] + )) + ) + ) + + items.append( + AnyComponentWithIdentity( + id: "replies", + component: AnyComponent(ParagraphComponent( + title: strings.Premium_Business_Replies_Title, + titleColor: titleColor, + text: strings.Premium_Business_Replies_Text, + textColor: textColor, + iconName: "Premium/Business/Replies", + iconColor: colors[2] + )) + ) + ) + + items.append( + AnyComponentWithIdentity( + id: "greetings", + component: AnyComponent(ParagraphComponent( + title: strings.Premium_Business_Greetings_Title, + titleColor: titleColor, + text: strings.Premium_Business_Greetings_Text, + textColor: textColor, + iconName: "Premium/Business/Greetings", + iconColor: colors[3] + )) + ) + ) + + items.append( + AnyComponentWithIdentity( + id: "away", + component: AnyComponent(ParagraphComponent( + title: strings.Premium_Business_Away_Title, + titleColor: titleColor, + text: strings.Premium_Business_Away_Text, + textColor: textColor, + iconName: "Premium/Business/Away", + iconColor: colors[4] + )) + ) + ) + + items.append( + AnyComponentWithIdentity( + id: "chatbots", + component: AnyComponent(ParagraphComponent( + title: strings.Premium_Business_Chatbots_Title, + titleColor: titleColor, + text: strings.Premium_Business_Chatbots_Text, + textColor: textColor, + iconName: "Premium/Business/Chatbots", + iconColor: colors[5] + )) + ) + ) + + let list = list.update( + component: List(items), + availableSize: CGSize(width: context.availableSize.width, height: 10000.0), + transition: context.transition + ) + + let contentHeight = context.component.topInset - 56.0 + list.size.height + context.component.bottomInset + context.add(list + .position(CGPoint(x: list.size.width / 2.0, y: context.component.topInset + list.size.height / 2.0)) + ) + + return CGSize(width: context.availableSize.width, height: contentHeight) + } + } +} + +final class BusinessPageComponent: CombinedComponent { + typealias EnvironmentType = DemoPageEnvironment + + let context: AccountContext + let theme: PresentationTheme + let neighbors: PageNeighbors + let bottomInset: CGFloat + let updatedBottomAlpha: (CGFloat) -> Void + let updatedDismissOffset: (CGFloat) -> Void + let updatedIsDisplaying: (Bool) -> Void + + init(context: AccountContext, theme: PresentationTheme, neighbors: PageNeighbors, bottomInset: CGFloat, updatedBottomAlpha: @escaping (CGFloat) -> Void, updatedDismissOffset: @escaping (CGFloat) -> Void, updatedIsDisplaying: @escaping (Bool) -> Void) { + self.context = context + self.theme = theme + self.neighbors = neighbors + self.bottomInset = bottomInset + self.updatedBottomAlpha = updatedBottomAlpha + self.updatedDismissOffset = updatedDismissOffset + self.updatedIsDisplaying = updatedIsDisplaying + } + + static func ==(lhs: BusinessPageComponent, rhs: BusinessPageComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.theme !== rhs.theme { + return false + } + if lhs.neighbors != rhs.neighbors { + return false + } + if lhs.bottomInset != rhs.bottomInset { + return false + } + return true + } + + final class State: ComponentState { + let updateBottomAlpha: (CGFloat) -> Void + let updateDismissOffset: (CGFloat) -> Void + let updatedIsDisplaying: (Bool) -> Void + + var resetScroll: ActionSlot? + + var topContentOffset: CGFloat = 0.0 + var bottomContentOffset: CGFloat = 100.0 { + didSet { + self.updateAlpha() + } + } + + var position: CGFloat? { + didSet { + self.updateAlpha() + } + } + + var isDisplaying = false { + didSet { + if oldValue != self.isDisplaying { + self.updatedIsDisplaying(self.isDisplaying) + + if !self.isDisplaying { + self.resetScroll?.invoke(Void()) + } + } + } + } + + var neighbors = PageNeighbors(leftIsList: false, rightIsList: false) + + init(updateBottomAlpha: @escaping (CGFloat) -> Void, updateDismissOffset: @escaping (CGFloat) -> Void, updateIsDisplaying: @escaping (Bool) -> Void) { + self.updateBottomAlpha = updateBottomAlpha + self.updateDismissOffset = updateDismissOffset + self.updatedIsDisplaying = updateIsDisplaying + + super.init() + } + + func updateAlpha() { + var dismissToLeft = false + if let position = self.position, position > 0.0 { + dismissToLeft = true + } + var dismissPosition = min(1.0, abs(self.position ?? 0.0) / 1.3333) + var position = min(1.0, abs(self.position ?? 0.0)) + if position > 0.001, (dismissToLeft && self.neighbors.leftIsList) || (!dismissToLeft && self.neighbors.rightIsList) { + dismissPosition = 0.0 + position = 1.0 + } + self.updateDismissOffset(dismissPosition) + + let verticalPosition = 1.0 - min(30.0, self.bottomContentOffset) / 30.0 + + let backgroundAlpha: CGFloat = max(position, verticalPosition) + self.updateBottomAlpha(backgroundAlpha) + } + } + + func makeState() -> State { + return State(updateBottomAlpha: self.updatedBottomAlpha, updateDismissOffset: self.updatedDismissOffset, updateIsDisplaying: self.updatedIsDisplaying) + } + + static var body: Body { + let background = Child(Rectangle.self) + let scroll = Child(ScrollComponent.self) + let topPanel = Child(BlurredBackgroundComponent.self) + let topSeparator = Child(Rectangle.self) + let title = Child(MultilineTextComponent.self) + + let resetScroll = ActionSlot() + + return { context in + let state = context.state + + let environment = context.environment[DemoPageEnvironment.self].value + state.neighbors = context.component.neighbors + state.resetScroll = resetScroll + state.position = environment.position + state.isDisplaying = environment.isDisplaying + + let theme = context.component.theme + let strings = context.component.context.sharedContext.currentPresentationData.with { $0 }.strings + + let topInset: CGFloat = 56.0 + + let scroll = scroll.update( + component: ScrollComponent( + content: AnyComponent( + BusinessListComponent( + context: context.component.context, + theme: theme, + topInset: topInset, + bottomInset: context.component.bottomInset + 110.0 + ) + ), + contentInsets: UIEdgeInsets(top: topInset, left: 0.0, bottom: 0.0, right: 0.0), + contentOffsetUpdated: { [weak state] topContentOffset, bottomContentOffset in + state?.topContentOffset = topContentOffset + state?.bottomContentOffset = bottomContentOffset + Queue.mainQueue().justDispatch { + state?.updated(transition: .immediate) + } + }, + contentOffsetWillCommit: { _ in }, + resetScroll: resetScroll + ), + availableSize: context.availableSize, + transition: context.transition + ) + + let background = background.update( + component: Rectangle(color: theme.overallDarkAppearance ? theme.list.blocksBackgroundColor : theme.list.plainBackgroundColor), + availableSize: scroll.size, + transition: context.transition + ) + context.add(background + .position(CGPoint(x: context.availableSize.width / 2.0, y: background.size.height / 2.0)) + ) + + context.add(scroll + .position(CGPoint(x: context.availableSize.width / 2.0, y: scroll.size.height / 2.0)) + ) + + let topPanel = topPanel.update( + component: BlurredBackgroundComponent( + color: theme.rootController.navigationBar.blurredBackgroundColor + ), + availableSize: CGSize(width: context.availableSize.width, height: topInset), + transition: context.transition + ) + + let topSeparator = topSeparator.update( + component: Rectangle( + color: theme.rootController.navigationBar.separatorColor + ), + availableSize: CGSize(width: context.availableSize.width, height: UIScreenPixel), + transition: context.transition + ) + + let title = title.update( + component: MultilineTextComponent( + text: .plain(NSAttributedString(string: strings.Premium_Business, font: Font.semibold(20.0), textColor: theme.rootController.navigationBar.primaryTextColor)), + horizontalAlignment: .center, + truncationType: .end, + maximumNumberOfLines: 1 + ), + availableSize: context.availableSize, + transition: context.transition + ) + + let topPanelAlpha: CGFloat + if state.topContentOffset > 78.0 { + topPanelAlpha = min(30.0, state.topContentOffset - 78.0) / 30.0 + } else { + topPanelAlpha = 0.0 + } + context.add(topPanel + .position(CGPoint(x: context.availableSize.width / 2.0, y: topPanel.size.height / 2.0)) + .opacity(topPanelAlpha) + ) + context.add(topSeparator + .position(CGPoint(x: context.availableSize.width / 2.0, y: topPanel.size.height)) + .opacity(topPanelAlpha) + ) + + let titleTopOriginY = topPanel.size.height / 2.0 + let titleBottomOriginY: CGFloat = 176.0 + let titleOriginDelta = titleTopOriginY - titleBottomOriginY + + let fraction = min(1.0, state.topContentOffset / abs(titleOriginDelta)) + let titleOriginY: CGFloat = titleBottomOriginY + fraction * titleOriginDelta + let titleScale = 1.0 - max(0.0, fraction * 0.2) + + context.add(title + .position(CGPoint(x: context.availableSize.width / 2.0, y: titleOriginY)) + .scale(titleScale) + ) + + return scroll.size + } + } +} diff --git a/submodules/PremiumUI/Sources/EmojiHeaderComponent.swift b/submodules/PremiumUI/Sources/EmojiHeaderComponent.swift index 4f020cfca58..765be52fb3f 100644 --- a/submodules/PremiumUI/Sources/EmojiHeaderComponent.swift +++ b/submodules/PremiumUI/Sources/EmojiHeaderComponent.swift @@ -13,8 +13,6 @@ import AnimationCache import MultiAnimationRenderer import EmojiStatusComponent -private let sceneVersion: Int = 3 - class EmojiHeaderComponent: Component { let context: AccountContext let animationCache: AnimationCache diff --git a/submodules/PremiumUI/Sources/FasterStarsView.swift b/submodules/PremiumUI/Sources/FasterStarsView.swift index a96b632a1f9..967fc2213df 100644 --- a/submodules/PremiumUI/Sources/FasterStarsView.swift +++ b/submodules/PremiumUI/Sources/FasterStarsView.swift @@ -5,6 +5,8 @@ import Display import AppBundle import LegacyComponents +private let sceneVersion: Int = 1 + final class FasterStarsView: UIView, PhoneDemoDecorationView { private let sceneView: SCNView @@ -13,8 +15,8 @@ final class FasterStarsView: UIView, PhoneDemoDecorationView { override init(frame: CGRect) { self.sceneView = SCNView(frame: CGRect(origin: .zero, size: frame.size)) self.sceneView.backgroundColor = .clear - if let url = getAppBundle().url(forResource: "lightspeed", withExtension: "scn") { - self.sceneView.scene = try? SCNScene(url: url, options: nil) + if let scene = loadCompressedScene(name: "lightspeed", version: sceneVersion) { + self.sceneView.scene = scene } self.sceneView.isUserInteractionEnabled = false self.sceneView.preferredFramesPerSecond = 60 diff --git a/submodules/PremiumUI/Sources/GiftAvatarComponent.swift b/submodules/PremiumUI/Sources/GiftAvatarComponent.swift index 6a1b8796498..771982bd653 100644 --- a/submodules/PremiumUI/Sources/GiftAvatarComponent.swift +++ b/submodules/PremiumUI/Sources/GiftAvatarComponent.swift @@ -14,7 +14,7 @@ import MergedAvatarsNode import MultilineTextComponent import TelegramPresentationData -private let sceneVersion: Int = 3 +private let sceneVersion: Int = 1 final class GiftAvatarComponent: Component { let context: AccountContext @@ -106,7 +106,7 @@ final class GiftAvatarComponent: Component { } private func setup() { - guard let url = getAppBundle().url(forResource: "gift", withExtension: "scn"), let scene = try? SCNScene(url: url, options: nil) else { + guard let scene = loadCompressedScene(name: "gift", version: sceneVersion) else { return } diff --git a/submodules/PremiumUI/Sources/GiftOptionItem.swift b/submodules/PremiumUI/Sources/GiftOptionItem.swift index 0ff43e8df6e..882912171be 100644 --- a/submodules/PremiumUI/Sources/GiftOptionItem.swift +++ b/submodules/PremiumUI/Sources/GiftOptionItem.swift @@ -296,7 +296,7 @@ class GiftOptionItemNode: ItemListRevealOptionsItemNode { var editingOffset: CGFloat = 0.0 if let isSelected = item.isSelected { - let sizeAndApply = selectableControlLayout(item.presentationData.theme.list.itemCheckColors.strokeColor, item.presentationData.theme.list.itemCheckColors.fillColor, item.presentationData.theme.list.itemCheckColors.foregroundColor, isSelected, false) + let sizeAndApply = selectableControlLayout(item.presentationData.theme.list.itemCheckColors.strokeColor, item.presentationData.theme.list.itemCheckColors.fillColor, item.presentationData.theme.list.itemCheckColors.foregroundColor, isSelected, .regular) selectableControlSizeAndApply = sizeAndApply editingOffset = sizeAndApply.0 } @@ -309,6 +309,10 @@ class GiftOptionItemNode: ItemListRevealOptionsItemNode { textConstrainedWidth -= 54.0 subtitleConstrainedWidth -= 30.0 } + if let _ = item.titleBadge { + textConstrainedWidth -= 32.0 + subtitleConstrainedWidth -= 32.0 + } let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: titleAttributedString, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: textConstrainedWidth, height: .greatestFiniteMagnitude))) let (statusLayout, statusApply) = makeStatusLayout(TextNodeLayoutArguments(attributedString: statusAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: subtitleConstrainedWidth, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) diff --git a/submodules/PremiumUI/Sources/PhoneDemoComponent.swift b/submodules/PremiumUI/Sources/PhoneDemoComponent.swift index 5a918550c98..d65a427ff33 100644 --- a/submodules/PremiumUI/Sources/PhoneDemoComponent.swift +++ b/submodules/PremiumUI/Sources/PhoneDemoComponent.swift @@ -371,6 +371,7 @@ final class PhoneDemoComponent: Component { case emoji case hello case tag + case business } enum Model { @@ -547,6 +548,13 @@ final class PhoneDemoComponent: Component { self.decorationView = starsView self.decorationContainerView.addSubview(starsView) } + case .business: + if let _ = self.decorationView as? BadgeBusinessView { + } else { + let starsView = BadgeBusinessView(frame: self.decorationContainerView.bounds) + self.decorationView = starsView + self.decorationContainerView.addSubview(starsView) + } } self.phoneView.setup(context: component.context, videoFile: component.videoFile, position: component.position) diff --git a/submodules/PremiumUI/Sources/PremiumBoostLevelsScreen.swift b/submodules/PremiumUI/Sources/PremiumBoostLevelsScreen.swift index 1c97beee065..96c2cbfb8f1 100644 --- a/submodules/PremiumUI/Sources/PremiumBoostLevelsScreen.swift +++ b/submodules/PremiumUI/Sources/PremiumBoostLevelsScreen.swift @@ -698,7 +698,7 @@ private final class SheetContent: CombinedComponent { isCurrent = mode == .current } case .features: - textString = strings.GroupBoost_AdditionalFeaturesText + textString = isGroup ? strings.GroupBoost_AdditionalFeaturesText : strings.ChannelBoost_AdditionalFeaturesText } let defaultTitle = strings.ChannelBoost_Level("\(level)").string diff --git a/submodules/PremiumUI/Sources/PremiumCoinComponent.swift b/submodules/PremiumUI/Sources/PremiumCoinComponent.swift new file mode 100644 index 00000000000..24a3c5ec509 --- /dev/null +++ b/submodules/PremiumUI/Sources/PremiumCoinComponent.swift @@ -0,0 +1,509 @@ +import Foundation +import UIKit +import Display +import ComponentFlow +import SwiftSignalKit +import SceneKit +import GZip +import AppBundle +import LegacyComponents + +private let sceneVersion: Int = 2 + +private func deg2rad(_ number: Float) -> Float { + return number * .pi / 180 +} + +private func rad2deg(_ number: Float) -> Float { + return number * 180.0 / .pi +} + +class PremiumCoinComponent: Component { + let isIntro: Bool + let isVisible: Bool + let hasIdleAnimations: Bool + + init(isIntro: Bool, isVisible: Bool, hasIdleAnimations: Bool) { + self.isIntro = isIntro + self.isVisible = isVisible + self.hasIdleAnimations = hasIdleAnimations + } + + static func ==(lhs: PremiumCoinComponent, rhs: PremiumCoinComponent) -> Bool { + return lhs.isIntro == rhs.isIntro && lhs.isVisible == rhs.isVisible && lhs.hasIdleAnimations == rhs.hasIdleAnimations + } + + final class View: UIView, SCNSceneRendererDelegate, ComponentTaggedView { + final class Tag { + } + + func matches(tag: Any) -> Bool { + if let _ = tag as? Tag { + return true + } + return false + } + + private var _ready = Promise() + var ready: Signal { + return self._ready.get() + } + + weak var animateFrom: UIView? + weak var containerView: UIView? + + private let sceneView: SCNView + + private var previousInteractionTimestamp: Double = 0.0 + private var timer: SwiftSignalKit.Timer? + private var hasIdleAnimations = false + + private let isIntro: Bool + + init(frame: CGRect, isIntro: Bool) { + self.isIntro = isIntro + + self.sceneView = SCNView(frame: CGRect(origin: .zero, size: CGSize(width: 64.0, height: 64.0))) + self.sceneView.backgroundColor = .clear + self.sceneView.transform = CGAffineTransform(scaleX: 0.5, y: 0.5) + self.sceneView.isUserInteractionEnabled = false + self.sceneView.preferredFramesPerSecond = 60 + self.sceneView.isJitteringEnabled = true + + super.init(frame: frame) + + self.addSubview(self.sceneView) + + self.setup() + + let panGestureRecoginzer = UIPanGestureRecognizer(target: self, action: #selector(self.handlePan(_:))) + self.addGestureRecognizer(panGestureRecoginzer) + + let tapGestureRecoginzer = UITapGestureRecognizer(target: self, action: #selector(self.handleTap(_:))) + self.addGestureRecognizer(tapGestureRecoginzer) + + self.disablesInteractiveModalDismiss = true + self.disablesInteractiveTransitionGestureRecognizer = true + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + self.timer?.invalidate() + } + + private let hapticFeedback = HapticFeedback() + + private var delayTapsTill: Double? + @objc private func handleTap(_ gesture: UITapGestureRecognizer) { + guard let scene = self.sceneView.scene, let node = scene.rootNode.childNode(withName: "star", recursively: false) else { + return + } + + let currentTime = CACurrentMediaTime() + self.previousInteractionTimestamp = currentTime + if let delayTapsTill = self.delayTapsTill, currentTime < delayTapsTill { + return + } + + var left: Bool? + var top: Bool? + if let view = gesture.view { + let point = gesture.location(in: view) + let horizontalDistanceFromCenter = abs(point.x - view.frame.size.width / 2.0) + if horizontalDistanceFromCenter > 60.0 { + return + } + let verticalDistanceFromCenter = abs(point.y - view.frame.size.height / 2.0) + if horizontalDistanceFromCenter > 20.0 { + left = point.x < view.frame.width / 2.0 + } + if verticalDistanceFromCenter > 20.0 { + top = point.y < view.frame.height / 2.0 + } + } + + if node.animationKeys.contains("tapRotate"), let left = left { + self.playAppearanceAnimation(velocity: nil, mirror: left, explode: true) + + self.hapticFeedback.impact(.medium) + return + } + + let initial = node.eulerAngles + var yaw: CGFloat = 0.0 + var pitch: CGFloat = 0.0 + if let left = left { + yaw = left ? -0.6 : 0.6 + } + if let top = top { + pitch = top ? -0.3 : 0.3 + } + let target = SCNVector3(pitch, yaw, 0.0) + + let animation = CABasicAnimation(keyPath: "eulerAngles") + animation.fromValue = NSValue(scnVector3: initial) + animation.toValue = NSValue(scnVector3: target) + animation.duration = 0.25 + animation.timingFunction = CAMediaTimingFunction(name: .easeOut) + animation.fillMode = .forwards + node.addAnimation(animation, forKey: "tapRotate") + + node.eulerAngles = target + + Queue.mainQueue().after(0.25) { + node.eulerAngles = initial + let springAnimation = CASpringAnimation(keyPath: "eulerAngles") + springAnimation.fromValue = NSValue(scnVector3: target) + springAnimation.toValue = NSValue(scnVector3: SCNVector3(x: 0.0, y: 0.0, z: 0.0)) + springAnimation.mass = 1.0 + springAnimation.stiffness = 21.0 + springAnimation.damping = 5.8 + springAnimation.duration = springAnimation.settlingDuration * 0.8 + node.addAnimation(springAnimation, forKey: "tapRotate") + } + + self.hapticFeedback.tap() + } + + private var previousYaw: Float = 0.0 + @objc private func handlePan(_ gesture: UIPanGestureRecognizer) { + guard let scene = self.sceneView.scene, let node = scene.rootNode.childNode(withName: "star", recursively: false) else { + return + } + + self.previousInteractionTimestamp = CACurrentMediaTime() + + let keys = [ + "rotate", + "tapRotate" + ] + + for key in keys { + node.removeAnimation(forKey: key) + } + + switch gesture.state { + case .began: + self.previousYaw = 0.0 + case .changed: + let translation = gesture.translation(in: gesture.view) + let yawPan = deg2rad(Float(translation.x)) + + func rubberBandingOffset(offset: CGFloat, bandingStart: CGFloat) -> CGFloat { + let bandedOffset = offset - bandingStart + let range: CGFloat = 60.0 + let coefficient: CGFloat = 0.4 + return bandingStart + (1.0 - (1.0 / ((bandedOffset * coefficient / range) + 1.0))) * range + } + + var pitchTranslation = rubberBandingOffset(offset: abs(translation.y), bandingStart: 0.0) + if translation.y < 0.0 { + pitchTranslation *= -1.0 + } + let pitchPan = deg2rad(Float(pitchTranslation)) + + self.previousYaw = yawPan + node.eulerAngles = SCNVector3(pitchPan, yawPan, 0.0) + case .ended: + let velocity = gesture.velocity(in: gesture.view) + + var smallAngle = false + if (self.previousYaw < .pi / 2 && self.previousYaw > -.pi / 2) && abs(velocity.x) < 200 { + smallAngle = true + } + + self.playAppearanceAnimation(velocity: velocity.x, smallAngle: smallAngle, explode: !smallAngle && abs(velocity.x) > 600) + node.eulerAngles = SCNVector3(0.0, 0.0, 0.0) + default: + break + } + } + + private func setup() { + guard let scene = loadCompressedScene(name: "coin", version: sceneVersion) else { + return + } + + self.sceneView.scene = scene + self.sceneView.delegate = self + + let _ = self.sceneView.snapshot() + } + + private var didSetReady = false + func renderer(_ renderer: SCNSceneRenderer, didRenderScene scene: SCNScene, atTime time: TimeInterval) { + if !self.didSetReady { + self.didSetReady = true + + Queue.mainQueue().justDispatch { + self._ready.set(.single(true)) + self.onReady() + } + } + } + + private func maybeAnimateIn() { + guard let scene = self.sceneView.scene, let _ = scene.rootNode.childNode(withName: "star", recursively: false), let animateFrom = self.animateFrom, var containerView = self.containerView else { + return + } + + containerView = containerView.subviews[2].subviews[1] + + let initialPosition = self.sceneView.center + let targetPosition = self.sceneView.superview!.convert(self.sceneView.center, to: containerView) + let sourcePosition = animateFrom.superview!.convert(animateFrom.center, to: containerView).offsetBy(dx: 0.0, dy: -20.0) + + containerView.addSubview(self.sceneView) + self.sceneView.center = targetPosition + + animateFrom.alpha = 0.0 + self.sceneView.layer.animateScale(from: 0.05, to: 0.5, duration: 1.0, timingFunction: kCAMediaTimingFunctionSpring) + self.sceneView.layer.animatePosition(from: sourcePosition, to: targetPosition, duration: 1.0, timingFunction: kCAMediaTimingFunctionSpring, completion: { _ in + self.addSubview(self.sceneView) + self.sceneView.center = initialPosition + }) + + Queue.mainQueue().after(0.4, { + animateFrom.alpha = 1.0 + }) + + self.animateFrom = nil + self.containerView = nil + } + + private func onReady() { + self.setupScaleAnimation() + self.setupGradientAnimation() + self.setupShineAnimation() + + self.maybeAnimateIn() + self.playAppearanceAnimation(explode: true) + + self.previousInteractionTimestamp = CACurrentMediaTime() + self.timer = SwiftSignalKit.Timer(timeout: 1.0, repeat: true, completion: { [weak self] in + if let strongSelf = self, strongSelf.hasIdleAnimations { + let currentTimestamp = CACurrentMediaTime() + if currentTimestamp > strongSelf.previousInteractionTimestamp + 5.0 { + strongSelf.playAppearanceAnimation() + } + } + }, queue: Queue.mainQueue()) + self.timer?.start() + } + + private func setupScaleAnimation() { + guard let scene = self.sceneView.scene, let node = scene.rootNode.childNode(withName: "star", recursively: false) else { + return + } + + let fromScale: Float = 0.9 + let toScale: Float = 1.0 + + let animation = CABasicAnimation(keyPath: "scale") + animation.duration = 2.0 + animation.fromValue = NSValue(scnVector3: SCNVector3(x: fromScale, y: fromScale, z: fromScale)) + animation.toValue = NSValue(scnVector3: SCNVector3(x: toScale, y: toScale, z: toScale)) + animation.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut) + animation.autoreverses = true + animation.repeatCount = .infinity + + node.addAnimation(animation, forKey: "scale") + } + + private func setupGradientAnimation() { + guard let scene = self.sceneView.scene, let node = scene.rootNode.childNode(withName: "star", recursively: false) else { + return + } + + for node in node.childNodes { + guard let initial = node.geometry?.materials.first?.diffuse.contentsTransform else { + return + } + + let animation = CABasicAnimation(keyPath: "contentsTransform") + animation.duration = 4.5 + animation.fromValue = NSValue(scnMatrix4: initial) + animation.toValue = NSValue(scnMatrix4: SCNMatrix4Translate(initial, -0.35, 0.35, 0)) + animation.timingFunction = CAMediaTimingFunction(name: .linear) + animation.autoreverses = true + animation.repeatCount = .infinity + + node.geometry?.materials.first?.diffuse.addAnimation(animation, forKey: "gradient") + } + } + + private func setupShineAnimation() { + guard let scene = self.sceneView.scene, let node = scene.rootNode.childNode(withName: "star", recursively: false) else { + return + } + + for node in node.childNodes { + guard let initial = node.geometry?.materials.first?.emission.contentsTransform else { + return + } + + if #available(iOS 17.0, *), let material = node.geometry?.materials.first { + material.metalness.intensity = 0.3 + } + + let animation = CABasicAnimation(keyPath: "contentsTransform") + animation.fillMode = .forwards + animation.fromValue = NSValue(scnMatrix4: initial) + animation.toValue = NSValue(scnMatrix4: SCNMatrix4Translate(initial, -1.6, 0.0, 0.0)) + animation.timingFunction = CAMediaTimingFunction(name: .easeOut) + animation.beginTime = 1.1 + animation.duration = 0.9 + + let group = CAAnimationGroup() + group.animations = [animation] + group.beginTime = 1.0 + group.duration = 4.0 + group.repeatCount = .infinity + + node.geometry?.materials.first?.emission.addAnimation(group, forKey: "shimmer") + } + } + + private func playAppearanceAnimation(velocity: CGFloat? = nil, smallAngle: Bool = false, mirror: Bool = false, explode: Bool = false) { + guard let scene = self.sceneView.scene, let node = scene.rootNode.childNode(withName: "star", recursively: false) else { + return + } + + let currentTime = CACurrentMediaTime() + self.previousInteractionTimestamp = currentTime + self.delayTapsTill = currentTime + 0.85 + + if explode, let node = scene.rootNode.childNode(withName: "swirl", recursively: false), let particlesLeft = scene.rootNode.childNode(withName: "particles_left", recursively: false), let particlesRight = scene.rootNode.childNode(withName: "particles_right", recursively: false), let particlesBottomLeft = scene.rootNode.childNode(withName: "particles_left_bottom", recursively: false), let particlesBottomRight = scene.rootNode.childNode(withName: "particles_right_bottom", recursively: false) { + if let leftParticleSystem = particlesLeft.particleSystems?.first, let rightParticleSystem = particlesRight.particleSystems?.first, let leftBottomParticleSystem = particlesBottomLeft.particleSystems?.first, let rightBottomParticleSystem = particlesBottomRight.particleSystems?.first { + leftParticleSystem.speedFactor = 2.0 + leftParticleSystem.particleVelocity = 1.6 + leftParticleSystem.birthRate = 60.0 + leftParticleSystem.particleLifeSpan = 4.0 + + rightParticleSystem.speedFactor = 2.0 + rightParticleSystem.particleVelocity = 1.6 + rightParticleSystem.birthRate = 60.0 + rightParticleSystem.particleLifeSpan = 4.0 + + leftBottomParticleSystem.particleVelocity = 1.6 + leftBottomParticleSystem.birthRate = 24.0 + leftBottomParticleSystem.particleLifeSpan = 7.0 + + rightBottomParticleSystem.particleVelocity = 1.6 + rightBottomParticleSystem.birthRate = 24.0 + rightBottomParticleSystem.particleLifeSpan = 7.0 + + node.physicsField?.isActive = true + Queue.mainQueue().after(1.0) { + node.physicsField?.isActive = false + + leftParticleSystem.birthRate = 15.0 + leftParticleSystem.particleVelocity = 1.0 + leftParticleSystem.particleLifeSpan = 3.0 + + rightParticleSystem.birthRate = 15.0 + rightParticleSystem.particleVelocity = 1.0 + rightParticleSystem.particleLifeSpan = 3.0 + + leftBottomParticleSystem.particleVelocity = 1.0 + leftBottomParticleSystem.birthRate = 10.0 + leftBottomParticleSystem.particleLifeSpan = 5.0 + + rightBottomParticleSystem.particleVelocity = 1.0 + rightBottomParticleSystem.birthRate = 10.0 + rightBottomParticleSystem.particleLifeSpan = 5.0 + + let leftAnimation = POPBasicAnimation() + leftAnimation.property = (POPAnimatableProperty.property(withName: "speedFactor", initializer: { property in + property?.readBlock = { particleSystem, values in + values?.pointee = (particleSystem as! SCNParticleSystem).speedFactor + } + property?.writeBlock = { particleSystem, values in + (particleSystem as! SCNParticleSystem).speedFactor = values!.pointee + } + property?.threshold = 0.01 + }) as! POPAnimatableProperty) + leftAnimation.fromValue = 1.2 as NSNumber + leftAnimation.toValue = 0.85 as NSNumber + leftAnimation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.linear) + leftAnimation.duration = 0.5 + leftParticleSystem.pop_add(leftAnimation, forKey: "speedFactor") + + let rightAnimation = POPBasicAnimation() + rightAnimation.property = (POPAnimatableProperty.property(withName: "speedFactor", initializer: { property in + property?.readBlock = { particleSystem, values in + values?.pointee = (particleSystem as! SCNParticleSystem).speedFactor + } + property?.writeBlock = { particleSystem, values in + (particleSystem as! SCNParticleSystem).speedFactor = values!.pointee + } + property?.threshold = 0.01 + }) as! POPAnimatableProperty) + rightAnimation.fromValue = 1.2 as NSNumber + rightAnimation.toValue = 0.85 as NSNumber + rightAnimation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.linear) + rightAnimation.duration = 0.5 + rightParticleSystem.pop_add(rightAnimation, forKey: "speedFactor") + } + } + } + + var from = node.presentation.eulerAngles + if abs(from.y - .pi * 2.0) < 0.001 { + from.y = 0.0 + } + node.removeAnimation(forKey: "tapRotate") + + var toValue: Float = smallAngle ? 0.0 : .pi * 2.0 + if let velocity = velocity, !smallAngle && abs(velocity) > 200 && velocity < 0.0 { + toValue *= -1 + } + if mirror { + toValue *= -1 + } + let to = SCNVector3(x: 0.0, y: toValue, z: 0.0) + let distance = rad2deg(to.y - from.y) + + guard !distance.isZero else { + return + } + + let springAnimation = CASpringAnimation(keyPath: "eulerAngles") + springAnimation.fromValue = NSValue(scnVector3: from) + springAnimation.toValue = NSValue(scnVector3: to) + springAnimation.mass = 1.0 + springAnimation.stiffness = 21.0 + springAnimation.damping = 5.8 + springAnimation.duration = springAnimation.settlingDuration * 0.75 + springAnimation.initialVelocity = velocity.flatMap { abs($0 / CGFloat(distance)) } ?? 1.7 + springAnimation.completion = { [weak node] finished in + if finished { + node?.eulerAngles = SCNVector3(x: 0.0, y: 0.0, z: 0.0) + } + } + node.addAnimation(springAnimation, forKey: "rotate") + } + + func update(component: PremiumCoinComponent, availableSize: CGSize, transition: Transition) -> CGSize { + self.sceneView.bounds = CGRect(origin: .zero, size: CGSize(width: availableSize.width * 2.0, height: availableSize.height * 2.0)) + if self.sceneView.superview == self { + self.sceneView.center = CGPoint(x: availableSize.width / 2.0, y: availableSize.height / 2.0) + } + + self.hasIdleAnimations = component.hasIdleAnimations + + return availableSize + } + } + + func makeView() -> View { + return View(frame: CGRect(), isIntro: self.isIntro) + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, transition: transition) + } +} diff --git a/submodules/PremiumUI/Sources/PremiumDemoScreen.swift b/submodules/PremiumUI/Sources/PremiumDemoScreen.swift index 470f4d2f62c..5957a9920f1 100644 --- a/submodules/PremiumUI/Sources/PremiumDemoScreen.swift +++ b/submodules/PremiumUI/Sources/PremiumDemoScreen.swift @@ -1004,7 +1004,7 @@ private final class DemoSheetContent: CombinedComponent { position: .top, model: .island, videoFile: configuration.videos["last_seen"], - decoration: .tag + decoration: .badgeStars )), title: strings.Premium_LastSeen, text: strings.Premium_LastSeenInfo, @@ -1023,7 +1023,7 @@ private final class DemoSheetContent: CombinedComponent { position: .top, model: .island, videoFile: configuration.videos["message_privacy"], - decoration: .tag + decoration: .swirlStars )), title: strings.Premium_MessagePrivacy, text: strings.Premium_MessagePrivacyInfo, @@ -1033,6 +1033,26 @@ private final class DemoSheetContent: CombinedComponent { ) ) + availableItems[.folderTags] = DemoPagerComponent.Item( + AnyComponentWithIdentity( + id: PremiumDemoScreen.Subject.folderTags, + component: AnyComponent( + PageComponent( + content: AnyComponent(PhoneDemoComponent( + context: component.context, + position: .top, + model: .island, + videoFile: configuration.videos["folder_tags"], + decoration: .tag + )), + title: strings.Premium_FolderTags, + text: strings.Premium_FolderTagsStandaloneInfo, + textColor: textColor + ) + ) + ) + ) + let index: Int = 0 var items: [DemoPagerComponent.Item] = [] if let item = availableItems.first(where: { $0.value.content.id == component.subject as AnyHashable }) { @@ -1136,6 +1156,8 @@ private final class DemoSheetContent: CombinedComponent { buttonText = strings.Premium_LastSeen_Proceed case .messagePrivacy: buttonText = strings.Premium_MessagePrivacy_Proceed + case .folderTags: + buttonText = strings.Premium_FolderTags_Proceed default: buttonText = strings.Common_OK } @@ -1177,7 +1199,9 @@ private final class DemoSheetContent: CombinedComponent { text = strings.Premium_LastSeenInfo case .messagePrivacy: text = strings.Premium_MessagePrivacyInfo - case .doubleLimits, .stories: + case .folderTags: + text = strings.Premium_FolderTagsStandaloneInfo + default: text = "" } @@ -1391,6 +1415,15 @@ public class PremiumDemoScreen: ViewControllerComponentContainer { case messageTags case lastSeen case messagePrivacy + case business + case folderTags + + case businessLocation + case businessHours + case businessGreetingMessage + case businessQuickReplies + case businessAwayMessage + case businessChatBots } public enum Source: Equatable { diff --git a/submodules/PremiumUI/Sources/PremiumGiftScreen.swift b/submodules/PremiumUI/Sources/PremiumGiftScreen.swift index ee7bc25662d..2e81b49ed3f 100644 --- a/submodules/PremiumUI/Sources/PremiumGiftScreen.swift +++ b/submodules/PremiumUI/Sources/PremiumGiftScreen.swift @@ -424,6 +424,7 @@ private final class PremiumGiftScreenContentComponent: CombinedComponent { UIColor(rgb: 0xef6922), UIColor(rgb: 0xe95a2c), UIColor(rgb: 0xe74e33), + UIColor(rgb: 0xe74e33), //replace UIColor(rgb: 0xe54937), UIColor(rgb: 0xe3433c), UIColor(rgb: 0xdb374b), @@ -434,6 +435,7 @@ private final class PremiumGiftScreenContentComponent: CombinedComponent { UIColor(rgb: 0x9b4fed), UIColor(rgb: 0x8958ff), UIColor(rgb: 0x676bff), + UIColor(rgb: 0x676bff), //replace UIColor(rgb: 0x6172ff), UIColor(rgb: 0x5b79ff), UIColor(rgb: 0x4492ff), @@ -530,6 +532,10 @@ private final class PremiumGiftScreenContentComponent: CombinedComponent { demoSubject = .lastSeen case .messagePrivacy: demoSubject = .messagePrivacy + case .business: + demoSubject = .business + default: + demoSubject = .doubleLimits } let buttonText: String diff --git a/submodules/PremiumUI/Sources/PremiumIntroScreen.swift b/submodules/PremiumUI/Sources/PremiumIntroScreen.swift index 6fb944509b2..b6c6202a8ed 100644 --- a/submodules/PremiumUI/Sources/PremiumIntroScreen.swift +++ b/submodules/PremiumUI/Sources/PremiumIntroScreen.swift @@ -31,6 +31,11 @@ import MultiAnimationRenderer import TelegramNotices import UndoUI import TelegramStringFormatting +import ListSectionComponent +import ListActionItemComponent +import EmojiStatusSelectionComponent +import EmojiStatusComponent +import EntityKeyboard public enum PremiumSource: Equatable { public static func == (lhs: PremiumSource, rhs: PremiumSource) -> Bool { @@ -281,6 +286,12 @@ public enum PremiumSource: Equatable { } else { return false } + case .folderTags: + if case .folderTags = rhs { + return true + } else { + return false + } } } @@ -325,6 +336,7 @@ public enum PremiumSource: Equatable { case presence case readTime case messageTags + case folderTags var identifier: String? { switch self { @@ -412,6 +424,8 @@ public enum PremiumSource: Equatable { return "read_time" case .messageTags: return "saved_tags" + case .folderTags: + return "folder_tags" } } } @@ -437,6 +451,15 @@ public enum PremiumPerk: CaseIterable { case messageTags case lastSeen case messagePrivacy + case business + case folderTags + + case businessLocation + case businessHours + case businessGreetingMessage + case businessQuickReplies + case businessAwayMessage + case businessChatBots public static var allCases: [PremiumPerk] { return [ @@ -459,12 +482,29 @@ public enum PremiumPerk: CaseIterable { .wallpapers, .messageTags, .lastSeen, - .messagePrivacy + .messagePrivacy, + .folderTags, + .business ] } - init?(identifier: String) { - for perk in PremiumPerk.allCases { + public static var allBusinessCases: [PremiumPerk] { + return [ + .businessLocation, + .businessHours, + .businessGreetingMessage, + .businessQuickReplies, + .businessAwayMessage, + .businessChatBots, +// .emojiStatus, +// .folderTags, +// .stories, + ] + } + + + init?(identifier: String, business: Bool) { + for perk in business ? PremiumPerk.allBusinessCases : PremiumPerk.allCases { if perk.identifier == identifier { self = perk return @@ -515,6 +555,22 @@ public enum PremiumPerk: CaseIterable { return "last_seen" case .messagePrivacy: return "message_privacy" + case .folderTags: + return "folder_tags" + case .business: + return "business" + case .businessLocation: + return "business_location" + case .businessHours: + return "business_hours" + case .businessQuickReplies: + return "quick_replies" + case .businessGreetingMessage: + return "greeting_message" + case .businessAwayMessage: + return "away_message" + case .businessChatBots: + return "business_bots" } } @@ -560,6 +616,23 @@ public enum PremiumPerk: CaseIterable { return strings.Premium_LastSeen case .messagePrivacy: return strings.Premium_MessagePrivacy + case .folderTags: + return strings.Premium_FolderTags + case .business: + return strings.Premium_Business + + case .businessLocation: + return strings.Business_Location + case .businessHours: + return strings.Business_OpeningHours + case .businessQuickReplies: + return strings.Business_QuickReplies + case .businessGreetingMessage: + return strings.Business_GreetingMessages + case .businessAwayMessage: + return strings.Business_AwayMessages + case .businessChatBots: + return strings.Business_Chatbots } } @@ -605,6 +678,23 @@ public enum PremiumPerk: CaseIterable { return strings.Premium_LastSeenInfo case .messagePrivacy: return strings.Premium_MessagePrivacyInfo + case .folderTags: + return strings.Premium_FolderTagsInfo + case .business: + return strings.Premium_BusinessInfo + + case .businessLocation: + return strings.Business_LocationInfo + case .businessHours: + return strings.Business_OpeningHoursInfo + case .businessQuickReplies: + return strings.Business_QuickRepliesInfo + case .businessGreetingMessage: + return strings.Business_GreetingMessagesInfo + case .businessAwayMessage: + return strings.Business_AwayMessagesInfo + case .businessChatBots: + return strings.Business_ChatbotsInfo } } @@ -650,6 +740,23 @@ public enum PremiumPerk: CaseIterable { return "Premium/Perk/LastSeen" case .messagePrivacy: return "Premium/Perk/MessagePrivacy" + case .folderTags: + return "Premium/Perk/MessageTags" + case .business: + return "Premium/Perk/Business" + + case .businessLocation: + return "Premium/BusinessPerk/Location" + case .businessHours: + return "Premium/BusinessPerk/Hours" + case .businessQuickReplies: + return "Premium/BusinessPerk/Replies" + case .businessGreetingMessage: + return "Premium/BusinessPerk/Greetings" + case .businessAwayMessage: + return "Premium/BusinessPerk/Away" + case .businessChatBots: + return "Premium/BusinessPerk/Chatbots" } } } @@ -676,21 +783,34 @@ struct PremiumIntroConfiguration { .appIcons, .uniqueReactions, .animatedUserpics, - .premiumStickers + .premiumStickers, + .business + ], businessPerks: [ + .businessGreetingMessage, + .businessAwayMessage, + .businessQuickReplies, + .businessChatBots, + .businessHours, + .businessLocation +// .emojiStatus, +// .folderTags, +// .stories ]) } let perks: [PremiumPerk] + let businessPerks: [PremiumPerk] - fileprivate init(perks: [PremiumPerk]) { + fileprivate init(perks: [PremiumPerk], businessPerks: [PremiumPerk]) { self.perks = perks + self.businessPerks = businessPerks } public static func with(appConfiguration: AppConfiguration) -> PremiumIntroConfiguration { if let data = appConfiguration.data, let values = data["premium_promo_order"] as? [String] { var perks: [PremiumPerk] = [] for value in values { - if let perk = PremiumPerk(identifier: value) { + if let perk = PremiumPerk(identifier: value, business: false) { if !perks.contains(perk) { perks.append(perk) } else { @@ -715,8 +835,30 @@ struct PremiumIntroConfiguration { if !perks.contains(.messageTags) { perks.append(.messageTags) } + if !perks.contains(.business) { + perks.append(.business) + } #endif - return PremiumIntroConfiguration(perks: perks) + + + var businessPerks: [PremiumPerk] = [] + if let values = data["business_promo_order"] as? [String] { + for value in values { + if let perk = PremiumPerk(identifier: value, business: true) { + if !businessPerks.contains(perk) { + businessPerks.append(perk) + } else { + businessPerks = [] + break + } + } + } + } + if businessPerks.count < 4 { + businessPerks = PremiumIntroConfiguration.defaultValue.businessPerks + } + + return PremiumIntroConfiguration(perks: perks, businessPerks: businessPerks) } else { return .defaultValue } @@ -752,337 +894,71 @@ private struct PremiumProduct: Equatable { } } -final class PremiumOptionComponent: CombinedComponent { - let title: String - let subtitle: String - let labelPrice: String - let discount: String - let multiple: Bool - let selected: Bool - let primaryTextColor: UIColor - let secondaryTextColor: UIColor - let accentColor: UIColor - let checkForegroundColor: UIColor - let checkBorderColor: UIColor +final class PerkIconComponent: CombinedComponent { + let backgroundColor: UIColor + let foregroundColor: UIColor + let iconName: String init( - title: String, - subtitle: String, - labelPrice: String, - discount: String, - multiple: Bool = false, - selected: Bool, - primaryTextColor: UIColor, - secondaryTextColor: UIColor, - accentColor: UIColor, - checkForegroundColor: UIColor, - checkBorderColor: UIColor + backgroundColor: UIColor, + foregroundColor: UIColor, + iconName: String ) { - self.title = title - self.subtitle = subtitle - self.labelPrice = labelPrice - self.discount = discount - self.multiple = multiple - self.selected = selected - self.primaryTextColor = primaryTextColor - self.secondaryTextColor = secondaryTextColor - self.accentColor = accentColor - self.checkForegroundColor = checkForegroundColor - self.checkBorderColor = checkBorderColor + self.backgroundColor = backgroundColor + self.foregroundColor = foregroundColor + self.iconName = iconName } - static func ==(lhs: PremiumOptionComponent, rhs: PremiumOptionComponent) -> Bool { - if lhs.title != rhs.title { - return false - } - if lhs.subtitle != rhs.subtitle { - return false - } - if lhs.labelPrice != rhs.labelPrice { - return false - } - if lhs.discount != rhs.discount { - return false - } - if lhs.multiple != rhs.multiple { - return false - } - if lhs.selected != rhs.selected { - return false - } - if lhs.primaryTextColor != rhs.primaryTextColor { - return false - } - if lhs.secondaryTextColor != rhs.secondaryTextColor { - return false - } - if lhs.accentColor != rhs.accentColor { + static func ==(lhs: PerkIconComponent, rhs: PerkIconComponent) -> Bool { + if lhs.backgroundColor != rhs.backgroundColor { return false } - if lhs.checkForegroundColor != rhs.checkForegroundColor { + if lhs.foregroundColor != rhs.foregroundColor { return false } - if lhs.checkBorderColor != rhs.checkBorderColor { + if lhs.iconName != rhs.iconName { return false } return true } static var body: Body { - let check = Child(CheckComponent.self) - let title = Child(MultilineTextComponent.self) - let subtitle = Child(MultilineTextComponent.self) - let discountBackground = Child(RoundedRectangle.self) - let discount = Child(MultilineTextComponent.self) - let label = Child(MultilineTextComponent.self) - + let background = Child(RoundedRectangle.self) + let icon = Child(BundleIconComponent.self) + return { context in let component = context.component + + let iconSize = CGSize(width: 30.0, height: 30.0) - var insets = UIEdgeInsets(top: 11.0, left: 46.0, bottom: 13.0, right: 16.0) - - let label = label.update( - component: MultilineTextComponent( - text: .plain( - NSAttributedString( - string: component.labelPrice, - font: Font.regular(17), - textColor: component.secondaryTextColor - ) - ), - maximumNumberOfLines: 1 - ), - availableSize: context.availableSize, - transition: context.transition - ) - - let title = title.update( - component: MultilineTextComponent( - text: .plain( - NSAttributedString( - string: component.title, - font: Font.regular(17), - textColor: component.primaryTextColor - ) - ), - maximumNumberOfLines: 1 + let background = background.update( + component: RoundedRectangle( + color: component.backgroundColor, + cornerRadius: 7.0 ), - availableSize: CGSize(width: context.availableSize.width - insets.left - insets.right - label.size.width, height: context.availableSize.height), + availableSize: iconSize, transition: context.transition ) - - let discountSize: CGSize - if !component.discount.isEmpty { - let discount = discount.update( - component: MultilineTextComponent( - text: .plain( - NSAttributedString( - string: component.discount, - font: Font.with(size: 14.0, design: .round, weight: .semibold, traits: []), - textColor: .white - ) - ), - maximumNumberOfLines: 1 - ), - availableSize: context.availableSize, - transition: context.transition - ) - - discountSize = CGSize(width: discount.size.width + 6.0, height: 18.0) - let discountBackground = discountBackground.update( - component: RoundedRectangle( - color: component.accentColor, - cornerRadius: 5.0 - ), - availableSize: discountSize, - transition: context.transition - ) - - let discountPosition = CGPoint(x: insets.left + title.size.width + 6.0 + discountSize.width / 2.0, y: insets.top + title.size.height / 2.0) - - context.add(discountBackground - .position(discountPosition) - ) - context.add(discount - .position(discountPosition) - ) - } else { - discountSize = CGSize(width: 0.0, height: 18.0) - } - - var spacing: CGFloat = 0.0 - var subtitleSize = CGSize() - if !component.subtitle.isEmpty { - spacing = 2.0 - - let subtitleFont = Font.regular(13) - let subtitleColor = component.secondaryTextColor - - let subtitleString = parseMarkdownIntoAttributedString( - component.subtitle, - attributes: MarkdownAttributes( - body: MarkdownAttributeSet(font: subtitleFont, textColor: subtitleColor), - bold: MarkdownAttributeSet(font: subtitleFont, textColor: subtitleColor, additionalAttributes: [NSAttributedString.Key.strikethroughStyle.rawValue: NSUnderlineStyle.single.rawValue as NSNumber]), - link: MarkdownAttributeSet(font: subtitleFont, textColor: subtitleColor), - linkAttribute: { _ in return nil } - ) - ) - - let subtitle = subtitle.update( - component: MultilineTextComponent( - text: .plain(subtitleString), - maximumNumberOfLines: 1 - ), - availableSize: CGSize(width: context.availableSize.width - insets.left - insets.right, height: context.availableSize.height), - transition: context.transition - ) - context.add(subtitle - .position(CGPoint(x: insets.left + subtitle.size.width / 2.0, y: insets.top + title.size.height + spacing + subtitle.size.height / 2.0)) - ) - subtitleSize = subtitle.size - - insets.top -= 2.0 - insets.bottom -= 2.0 - } - - let check = check.update( - component: CheckComponent( - theme: CheckComponent.Theme( - backgroundColor: component.accentColor, - strokeColor: component.checkForegroundColor, - borderColor: component.checkBorderColor, - overlayBorder: false, - hasInset: false, - hasShadow: false - ), - selected: component.selected + let icon = icon.update( + component: BundleIconComponent( + name: component.iconName, + tintColor: .white ), - availableSize: context.availableSize, + availableSize: iconSize, transition: context.transition ) - - context.add(title - .position(CGPoint(x: insets.left + title.size.width / 2.0, y: insets.top + title.size.height / 2.0)) - ) - - let size = CGSize(width: context.availableSize.width, height: insets.top + title.size.height + spacing + subtitleSize.height + insets.bottom) - let distance = context.availableSize.width - insets.left - insets.right - label.size.width - subtitleSize.width - - let labelY: CGFloat - if distance > 8.0 { - labelY = size.height / 2.0 - } else { - labelY = insets.top + title.size.height / 2.0 - } - - context.add(label - .position(CGPoint(x: context.availableSize.width - insets.right - label.size.width / 2.0, y: labelY)) - ) - - context.add(check - .position(CGPoint(x: 4.0 + check.size.width / 2.0, y: size.height / 2.0)) + let iconPosition = CGPoint(x: background.size.width / 2.0, y: background.size.height / 2.0) + context.add(background + .position(iconPosition) ) - - return size - } - } -} - -private final class CheckComponent: Component { - struct Theme: Equatable { - public let backgroundColor: UIColor - public let strokeColor: UIColor - public let borderColor: UIColor - public let overlayBorder: Bool - public let hasInset: Bool - public let hasShadow: Bool - public let filledBorder: Bool - public let borderWidth: CGFloat? - - public init(backgroundColor: UIColor, strokeColor: UIColor, borderColor: UIColor, overlayBorder: Bool, hasInset: Bool, hasShadow: Bool, filledBorder: Bool = false, borderWidth: CGFloat? = nil) { - self.backgroundColor = backgroundColor - self.strokeColor = strokeColor - self.borderColor = borderColor - self.overlayBorder = overlayBorder - self.hasInset = hasInset - self.hasShadow = hasShadow - self.filledBorder = filledBorder - self.borderWidth = borderWidth - } - - var checkNodeTheme: CheckNodeTheme { - return CheckNodeTheme( - backgroundColor: self.backgroundColor, - strokeColor: self.strokeColor, - borderColor: self.borderColor, - overlayBorder: self.overlayBorder, - hasInset: self.hasInset, - hasShadow: self.hasShadow, - filledBorder: self.filledBorder, - borderWidth: self.borderWidth + context.add(icon + .position(iconPosition) ) + return iconSize } } - - let theme: Theme - let selected: Bool - - init( - theme: Theme, - selected: Bool - ) { - self.theme = theme - self.selected = selected - } - - static func ==(lhs: CheckComponent, rhs: CheckComponent) -> Bool { - if lhs.theme != rhs.theme { - return false - } - if lhs.selected != rhs.selected { - return false - } - return true - } - - final class View: UIView { - private var currentValue: CGFloat? - private var animator: DisplayLinkAnimator? - - private var checkLayer: CheckLayer { - return self.layer as! CheckLayer - } - - override class var layerClass: AnyClass { - return CheckLayer.self - } - - init() { - super.init(frame: CGRect()) - } - - required init?(coder aDecoder: NSCoder) { - preconditionFailure() - } - - - func update(component: CheckComponent, availableSize: CGSize, transition: Transition) -> CGSize { - self.checkLayer.setSelected(component.selected, animated: true) - self.checkLayer.theme = component.theme.checkNodeTheme - - return CGSize(width: 22.0, height: 22.0) - } - } - - func makeView() -> View { - return View() - } - - func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { - return view.update(component: self, availableSize: availableSize, transition: transition) - } } final class SectionGroupComponent: Component { @@ -1483,6 +1359,7 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent { typealias EnvironmentType = (ViewControllerComponentContainer.Environment, ScrollChildEnvironment) let context: AccountContext + let mode: PremiumIntroScreen.Mode let source: PremiumSource let forceDark: Bool let isPremium: Bool? @@ -1493,6 +1370,7 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent { let validPurchases: [InAppPurchaseManager.ReceiptPurchase] let promoConfiguration: PremiumPromoConfiguration? let present: (ViewController) -> Void + let push: (ViewController) -> Void let selectProduct: (String) -> Void let buy: () -> Void let updateIsFocused: (Bool) -> Void @@ -1501,6 +1379,7 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent { init( context: AccountContext, + mode: PremiumIntroScreen.Mode, source: PremiumSource, forceDark: Bool, isPremium: Bool?, @@ -1511,6 +1390,7 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent { validPurchases: [InAppPurchaseManager.ReceiptPurchase], promoConfiguration: PremiumPromoConfiguration?, present: @escaping (ViewController) -> Void, + push: @escaping (ViewController) -> Void, selectProduct: @escaping (String) -> Void, buy: @escaping () -> Void, updateIsFocused: @escaping (Bool) -> Void, @@ -1518,6 +1398,7 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent { shareLink: @escaping (String) -> Void ) { self.context = context + self.mode = mode self.source = source self.forceDark = forceDark self.isPremium = isPremium @@ -1528,6 +1409,7 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent { self.validPurchases = validPurchases self.promoConfiguration = promoConfiguration self.present = present + self.push = push self.selectProduct = selectProduct self.buy = buy self.updateIsFocused = updateIsFocused @@ -1572,6 +1454,7 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent { final class State: ComponentState { private let context: AccountContext + private let present: (ViewController) -> Void var products: [PremiumProduct]? var selectedProductId: String? @@ -1580,6 +1463,7 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent { var newPerks: [String] = [] var isPremium: Bool? + var peer: EnginePeer? private var disposable: Disposable? private(set) var configuration = PremiumIntroConfiguration.defaultValue @@ -1608,20 +1492,29 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent { } } - init(context: AccountContext, source: PremiumSource) { + init( + context: AccountContext, + source: PremiumSource, + present: @escaping (ViewController) -> Void + ) { self.context = context + self.present = present super.init() self.disposable = (context.engine.data.subscribe( - TelegramEngine.EngineData.Item.Configuration.App() + TelegramEngine.EngineData.Item.Configuration.App(), + TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId) ) - |> deliverOnMainQueue).start(next: { [weak self] appConfiguration in + |> deliverOnMainQueue).start(next: { [weak self] appConfiguration, accountPeer in if let strongSelf = self { + let isFirstTime = strongSelf.peer == nil + strongSelf.configuration = PremiumIntroConfiguration.with(appConfiguration: appConfiguration) + strongSelf.peer = accountPeer strongSelf.updated(transition: .immediate) - if let identifier = source.identifier { + if let identifier = source.identifier, isFirstTime { var jsonString: String = "{" jsonString += "\"source\": \"\(identifier)\"," @@ -1671,8 +1564,9 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent { ApplicationSpecificNotice.dismissedPremiumColorsBadge(accountManager: context.sharedContext.accountManager), ApplicationSpecificNotice.dismissedMessageTagsBadge(accountManager: context.sharedContext.accountManager), ApplicationSpecificNotice.dismissedLastSeenBadge(accountManager: context.sharedContext.accountManager), - ApplicationSpecificNotice.dismissedMessagePrivacyBadge(accountManager: context.sharedContext.accountManager) - ).startStrict(next: { [weak self] dismissedPremiumAppIconsBadge, dismissedPremiumWallpapersBadge, dismissedPremiumColorsBadge, dismissedMessageTagsBadge, dismissedLastSeenBadge, dismissedMessagePrivacyBadge in + ApplicationSpecificNotice.dismissedMessagePrivacyBadge(accountManager: context.sharedContext.accountManager), + ApplicationSpecificNotice.dismissedBusinessBadge(accountManager: context.sharedContext.accountManager) + ).startStrict(next: { [weak self] dismissedPremiumAppIconsBadge, dismissedPremiumWallpapersBadge, dismissedPremiumColorsBadge, dismissedMessageTagsBadge, dismissedLastSeenBadge, dismissedMessagePrivacyBadge, dismissedBusinessBadge in guard let self else { return } @@ -1692,6 +1586,9 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent { if !dismissedMessagePrivacyBadge { newPerks.append(PremiumPerk.messagePrivacy.identifier) } + if !dismissedBusinessBadge { + newPerks.append(PremiumPerk.business.identifier) + } self.newPerks = newPerks self.updated() }) @@ -1703,10 +1600,59 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent { self.stickersDisposable?.dispose() self.newPerksDisposable?.dispose() } + + private var updatedPeerStatus: PeerEmojiStatus? + + private weak var emojiStatusSelectionController: ViewController? + private var previousEmojiSetupTimestamp: Double? + func openEmojiSetup(sourceView: UIView, currentFileId: Int64?, color: UIColor?) { + let currentTimestamp = CACurrentMediaTime() + if let previousTimestamp = self.previousEmojiSetupTimestamp, currentTimestamp < previousTimestamp + 1.0 { + return + } + self.previousEmojiSetupTimestamp = currentTimestamp + + self.emojiStatusSelectionController?.dismiss() + var selectedItems = Set() + if let currentFileId { + selectedItems.insert(MediaId(namespace: Namespaces.Media.CloudFile, id: currentFileId)) + } + + let controller = EmojiStatusSelectionController( + context: self.context, + mode: .statusSelection, + sourceView: sourceView, + emojiContent: EmojiPagerContentComponent.emojiInputData( + context: self.context, + animationCache: self.context.animationCache, + animationRenderer: self.context.animationRenderer, + isStandalone: false, + subject: .status, + hasTrending: false, + topReactionItems: [], + areUnicodeEmojiEnabled: false, + areCustomEmojiEnabled: true, + chatPeerId: self.context.account.peerId, + selectedItems: selectedItems, + topStatusTitle: nil, + backgroundIconColor: color + ), + currentSelection: currentFileId, + color: color, + destinationItemView: { [weak sourceView] in + guard let sourceView else { + return nil + } + return sourceView + } + ) + self.emojiStatusSelectionController = controller + self.present(controller) + } } func makeState() -> State { - return State(context: self.context, source: self.source) + return State(context: self.context, source: self.source, present: self.present) } static var body: Body { @@ -1716,8 +1662,9 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent { let completedText = Child(MultilineTextComponent.self) let linkButton = Child(Button.self) let optionsSection = Child(SectionGroupComponent.self) - let perksTitle = Child(MultilineTextComponent.self) - let perksSection = Child(SectionGroupComponent.self) + let businessSection = Child(ListSectionComponent.self) + let moreBusinessSection = Child(ListSectionComponent.self) + let perksSection = Child(ListSectionComponent.self) let infoBackground = Child(RoundedRectangle.self) let infoTitle = Child(MultilineTextComponent.self) let infoText = Child(MultilineTextComponent.self) @@ -1736,6 +1683,7 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent { let theme = environment.theme let strings = environment.strings + let presentationData = context.component.context.sharedContext.currentPresentationData.with { $0 } let availableWidth = context.availableSize.width let sideInsets = sideInset * 2.0 + environment.safeInsets.left + environment.safeInsets.right @@ -1776,9 +1724,7 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent { let textColor = theme.list.itemPrimaryTextColor let accentColor = theme.list.itemAccentColor - let titleColor = theme.list.itemPrimaryTextColor let subtitleColor = theme.list.itemSecondaryTextColor - let arrowColor = theme.list.disclosureArrowColor let textFont = Font.regular(15.0) let boldTextFont = Font.semibold(15.0) @@ -1809,13 +1755,21 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent { textString = strings.Premium_PersonalDescription } } else if context.component.isPremium == true { - if !context.component.justBought, let products = state.products, let current = products.first(where: { $0.isCurrent }), current.months == 1 { - textString = strings.Premium_UpgradeDescription + if case .business = context.component.mode { + textString = strings.Business_SubscribedDescription } else { - textString = strings.Premium_SubscribedDescription + if !context.component.justBought, let products = state.products, let current = products.first(where: { $0.isCurrent }), current.months == 1 { + textString = strings.Premium_UpgradeDescription + } else { + textString = strings.Premium_SubscribedDescription + } } } else { - textString = strings.Premium_Description + if case .business = context.component.mode { + textString = strings.Business_Description + } else { + textString = strings.Premium_Description + } } let markdownAttributes = MarkdownAttributes(body: MarkdownAttributeSet(font: textFont, textColor: textColor), bold: MarkdownAttributeSet(font: boldTextFont, textColor: textColor), link: MarkdownAttributeSet(font: textFont, textColor: accentColor), linkAttribute: { contents in @@ -1866,6 +1820,7 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent { UIColor(rgb: 0xef6922), UIColor(rgb: 0xe95a2c), UIColor(rgb: 0xe74e33), + UIColor(rgb: 0xe74e33), //replace UIColor(rgb: 0xe54937), UIColor(rgb: 0xe3433c), UIColor(rgb: 0xdb374b), @@ -1876,6 +1831,7 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent { UIColor(rgb: 0x9b4fed), UIColor(rgb: 0x8958ff), UIColor(rgb: 0x676bff), + UIColor(rgb: 0x676bff), //replace UIColor(rgb: 0x6172ff), UIColor(rgb: 0x5b79ff), UIColor(rgb: 0x4492ff), @@ -1887,6 +1843,7 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent { let accountContext = context.component.context let present = context.component.present + let push = context.component.push let selectProduct = context.component.selectProduct let buy = context.component.buy let updateIsFocused = context.component.updateIsFocused @@ -2009,50 +1966,54 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent { let forceDark = context.component.forceDark let layoutPerks = { size.height += 8.0 - let perksTitle = perksTitle.update( - component: MultilineTextComponent( - text: .plain( - NSAttributedString(string: strings.Premium_WhatsIncluded.uppercased(), font: Font.regular(14.0), textColor: environment.theme.list.freeTextColor) - ), - horizontalAlignment: .natural, - maximumNumberOfLines: 0, - lineSpacing: 0.2 - ), - environment: {}, - availableSize: CGSize(width: availableWidth - sideInsets, height: .greatestFiniteMagnitude), - transition: context.transition - ) - context.add(perksTitle - .position(CGPoint(x: sideInset + environment.safeInsets.left + textSideInset + perksTitle.size.width / 2.0, y: size.height + perksTitle.size.height / 2.0)) - ) - size.height += perksTitle.size.height - size.height += 3.0 - + var i = 0 - var perksItems: [SectionGroupComponent.Item] = [] - for perk in state.configuration.perks { - let iconBackgroundColors = gradientColors[i] - perksItems.append(SectionGroupComponent.Item( - AnyComponentWithIdentity( - id: perk.identifier, - component: AnyComponent( - PerkComponent( - iconName: perk.iconName, - iconBackgroundColors: [ - iconBackgroundColors - ], - title: perk.title(strings: strings), - titleColor: titleColor, - subtitle: perk.subtitle(strings: strings), - subtitleColor: subtitleColor, - arrowColor: arrowColor, - accentColor: accentColor, - badge: state.newPerks.contains(perk.identifier) ? strings.Premium_New : nil - ) - ) - ), - accessibilityLabel: "\(perk.title(strings: strings)). \(perk.subtitle(strings: strings))", - action: { [weak state] in + var perksItems: [AnyComponentWithIdentity] = [] + for perk in state.configuration.perks { + if case .business = context.component.mode, case .business = perk { + continue + } + + let isNew = state.newPerks.contains(perk.identifier) + let titleComponent = AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: perk.title(strings: strings), + font: Font.regular(presentationData.listsFontSize.baseDisplaySize), + textColor: environment.theme.list.itemPrimaryTextColor + )), + maximumNumberOfLines: 0 + )) + + let titleCombinedComponent: AnyComponent + if isNew { + titleCombinedComponent = AnyComponent(HStack([ + AnyComponentWithIdentity(id: AnyHashable(0), component: titleComponent), + AnyComponentWithIdentity(id: AnyHashable(1), component: AnyComponent(BadgeComponent(color: gradientColors[i], text: strings.Premium_New))) + ], spacing: 5.0)) + } else { + titleCombinedComponent = AnyComponent(HStack([AnyComponentWithIdentity(id: AnyHashable(0), component: titleComponent)], spacing: 0.0)) + } + + perksItems.append(AnyComponentWithIdentity(id: perksItems.count, component: AnyComponent(ListActionItemComponent( + theme: environment.theme, + title: AnyComponent(VStack([ + AnyComponentWithIdentity(id: AnyHashable(0), component: titleCombinedComponent), + AnyComponentWithIdentity(id: AnyHashable(1), component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: perk.subtitle(strings: strings), + font: Font.regular(floor(presentationData.listsFontSize.baseDisplaySize * 13.0 / 17.0)), + textColor: environment.theme.list.itemSecondaryTextColor + )), + maximumNumberOfLines: 0, + lineSpacing: 0.18 + ))) + ], alignment: .left, spacing: 2.0)), + leftIcon: AnyComponentWithIdentity(id: 0, component: AnyComponent(PerkIconComponent( + backgroundColor: gradientColors[i], + foregroundColor: .white, + iconName: perk.iconName + ))), + action: { [weak state] _ in var demoSubject: PremiumDemoScreen.Subject switch perk { case .doubleLimits: @@ -2077,7 +2038,6 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent { demoSubject = .animatedUserpics case .appIcons: demoSubject = .appIcons -// let _ = ApplicationSpecificNotice.setDismissedPremiumAppIconsBadge(accountManager: accountContext.sharedContext.accountManager).startStandalone() case .animatedEmoji: demoSubject = .animatedEmoji case .emojiStatus: @@ -2101,8 +2061,13 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent { case .messagePrivacy: demoSubject = .messagePrivacy let _ = ApplicationSpecificNotice.setDismissedMessagePrivacyBadge(accountManager: accountContext.sharedContext.accountManager).startStandalone() + case .business: + demoSubject = .business + let _ = ApplicationSpecificNotice.setDismissedBusinessBadge(accountManager: accountContext.sharedContext.accountManager).startStandalone() + default: + demoSubject = .doubleLimits } - + let isPremium = state?.isPremium == true var dismissImpl: (() -> Void)? let controller = PremiumLimitsListScreen(context: accountContext, subject: demoSubject, source: .intro(state?.price), order: state?.configuration.perks, buttonText: isPremium ? strings.Common_OK : (state?.isAnnual == true ? strings.Premium_SubscribeForAnnual(state?.price ?? "—").string : strings.Premium_SubscribeFor(state?.price ?? "–").string), isPremium: isPremium, forceDark: forceDark) @@ -2120,19 +2085,26 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent { controller?.dismiss(animated: true, completion: nil) } updateIsFocused(true) - + addAppLogEvent(postbox: accountContext.account.postbox, type: "premium.promo_screen_tap", data: ["item": perk.identifier]) } - )) + )))) i += 1 } let perksSection = perksSection.update( - component: SectionGroupComponent( - items: perksItems, - backgroundColor: environment.theme.list.itemBlocksBackgroundColor, - selectionColor: environment.theme.list.itemHighlightedBackgroundColor, - separatorColor: environment.theme.list.itemBlocksSeparatorColor + component: ListSectionComponent( + theme: environment.theme, + header: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: strings.Premium_WhatsIncluded.uppercased(), + font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize), + textColor: environment.theme.list.freeTextColor + )), + maximumNumberOfLines: 0 + )), + footer: nil, + items: perksItems ), environment: {}, availableSize: CGSize(width: availableWidth - sideInsets, height: .greatestFiniteMagnitude), @@ -2142,6 +2114,7 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent { .position(CGPoint(x: availableWidth / 2.0, y: size.height + perksSection.size.height / 2.0)) .clipsToBounds(true) .cornerRadius(10.0) + .disappear(.default(alpha: true)) ) size.height += perksSection.size.height @@ -2156,6 +2129,314 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent { } } + let layoutBusinessPerks = { + size.height += 8.0 + + let gradientColors: [UIColor] = [ + UIColor(rgb: 0xef6922), + UIColor(rgb: 0xe54937), + UIColor(rgb: 0xdb374b), + UIColor(rgb: 0xbc4395), + UIColor(rgb: 0x9b4fed), + UIColor(rgb: 0x8958ff) + ] + + var i = 0 + var perksItems: [AnyComponentWithIdentity] = [] + for perk in state.configuration.businessPerks { + perksItems.append(AnyComponentWithIdentity(id: perksItems.count, component: AnyComponent(ListActionItemComponent( + theme: environment.theme, + title: AnyComponent(VStack([ + AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: perk.title(strings: strings), + font: Font.regular(presentationData.listsFontSize.baseDisplaySize), + textColor: environment.theme.list.itemPrimaryTextColor + )), + maximumNumberOfLines: 0 + ))), + AnyComponentWithIdentity(id: AnyHashable(1), component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: perk.subtitle(strings: strings), + font: Font.regular(floor(presentationData.listsFontSize.baseDisplaySize * 13.0 / 17.0)), + textColor: environment.theme.list.itemSecondaryTextColor + )), + maximumNumberOfLines: 0, + lineSpacing: 0.18 + ))) + ], alignment: .left, spacing: 2.0)), + leftIcon: AnyComponentWithIdentity(id: 0, component: AnyComponent(PerkIconComponent( + backgroundColor: gradientColors[i], + foregroundColor: .white, + iconName: perk.iconName + ))), + action: { [weak state] _ in + let isPremium = state?.isPremium == true + if isPremium { + switch perk { + case .businessLocation: + let _ = (accountContext.engine.data.get( + TelegramEngine.EngineData.Item.Peer.BusinessLocation(id: accountContext.account.peerId) + ) + |> deliverOnMainQueue).start(next: { [weak accountContext] businessLocation in + guard let accountContext else { + return + } + push(accountContext.sharedContext.makeBusinessLocationSetupScreen(context: accountContext, initialValue: businessLocation, completion: { _ in })) + }) + case .businessHours: + let _ = (accountContext.engine.data.get( + TelegramEngine.EngineData.Item.Peer.BusinessHours(id: accountContext.account.peerId) + ) + |> deliverOnMainQueue).start(next: { [weak accountContext] businessHours in + guard let accountContext else { + return + } + push(accountContext.sharedContext.makeBusinessHoursSetupScreen(context: accountContext, initialValue: businessHours, completion: { _ in })) + }) + case .businessQuickReplies: + let _ = (accountContext.sharedContext.makeQuickReplySetupScreenInitialData(context: accountContext) + |> take(1) + |> deliverOnMainQueue).start(next: { [weak accountContext] initialData in + guard let accountContext else { + return + } + push(accountContext.sharedContext.makeQuickReplySetupScreen(context: accountContext, initialData: initialData)) + }) + case .businessGreetingMessage: + let _ = (accountContext.sharedContext.makeAutomaticBusinessMessageSetupScreenInitialData(context: accountContext) + |> take(1) + |> deliverOnMainQueue).start(next: { [weak accountContext] initialData in + guard let accountContext else { + return + } + push(accountContext.sharedContext.makeAutomaticBusinessMessageSetupScreen(context: accountContext, initialData: initialData, isAwayMode: false)) + }) + case .businessAwayMessage: + let _ = (accountContext.sharedContext.makeAutomaticBusinessMessageSetupScreenInitialData(context: accountContext) + |> take(1) + |> deliverOnMainQueue).start(next: { [weak accountContext] initialData in + guard let accountContext else { + return + } + push(accountContext.sharedContext.makeAutomaticBusinessMessageSetupScreen(context: accountContext, initialData: initialData, isAwayMode: true)) + }) + case .businessChatBots: + let _ = (accountContext.sharedContext.makeChatbotSetupScreenInitialData(context: accountContext) + |> take(1) + |> deliverOnMainQueue).start(next: { [weak accountContext] initialData in + guard let accountContext else { + return + } + push(accountContext.sharedContext.makeChatbotSetupScreen(context: accountContext, initialData: initialData)) + }) + default: + fatalError() + } + } else { + var demoSubject: PremiumDemoScreen.Subject + switch perk { + case .businessLocation: + demoSubject = .businessLocation + case .businessHours: + demoSubject = .businessHours + case .businessQuickReplies: + demoSubject = .businessQuickReplies + case .businessGreetingMessage: + demoSubject = .businessGreetingMessage + case .businessAwayMessage: + demoSubject = .businessAwayMessage + case .businessChatBots: + demoSubject = .businessChatBots + default: + fatalError() + } + var dismissImpl: (() -> Void)? + let controller = PremiumLimitsListScreen(context: accountContext, subject: demoSubject, source: .intro(state?.price), order: state?.configuration.businessPerks, buttonText: isPremium ? strings.Common_OK : (state?.isAnnual == true ? strings.Premium_SubscribeForAnnual(state?.price ?? "—").string : strings.Premium_SubscribeFor(state?.price ?? "–").string), isPremium: isPremium, forceDark: forceDark) + controller.action = { [weak state] in + dismissImpl?() + if state?.isPremium == false { + buy() + } + } + controller.disposed = { + updateIsFocused(false) + } + present(controller) + dismissImpl = { [weak controller] in + controller?.dismiss(animated: true, completion: nil) + } + updateIsFocused(true) + } + } + )))) + i += 1 + } + + let businessSection = businessSection.update( + component: ListSectionComponent( + theme: environment.theme, + header: nil, + footer: nil, + items: perksItems + ), + environment: {}, + availableSize: CGSize(width: availableWidth - sideInsets, height: .greatestFiniteMagnitude), + transition: context.transition + ) + context.add(businessSection + .position(CGPoint(x: availableWidth / 2.0, y: size.height + businessSection.size.height / 2.0)) + .clipsToBounds(true) + .cornerRadius(10.0) + ) + size.height += businessSection.size.height + size.height += 23.0 + } + + let layoutMoreBusinessPerks = { + size.height += 8.0 + + let status = state.peer?.emojiStatus + + let accentColor = environment.theme.list.itemAccentColor + var perksItems: [AnyComponentWithIdentity] = [] + perksItems.append(AnyComponentWithIdentity(id: perksItems.count, component: AnyComponent(ListActionItemComponent( + theme: environment.theme, + title: AnyComponent(VStack([ + AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: strings.Business_SetEmojiStatus, + font: Font.regular(presentationData.listsFontSize.baseDisplaySize), + textColor: environment.theme.list.itemPrimaryTextColor + )), + maximumNumberOfLines: 0 + ))), + AnyComponentWithIdentity(id: AnyHashable(1), component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: strings.Business_SetEmojiStatusInfo, + font: Font.regular(floor(presentationData.listsFontSize.baseDisplaySize * 13.0 / 17.0)), + textColor: environment.theme.list.itemSecondaryTextColor + )), + maximumNumberOfLines: 0, + lineSpacing: 0.18 + ))) + ], alignment: .left, spacing: 2.0)), + leftIcon: AnyComponentWithIdentity(id: 0, component: AnyComponent(PerkIconComponent( + backgroundColor: UIColor(rgb: 0x676bff), + foregroundColor: .white, + iconName: "Premium/BusinessPerk/Status" + ))), + icon: ListActionItemComponent.Icon(component: AnyComponentWithIdentity(id: 0, component: AnyComponent(EmojiActionIconComponent( + context: context.component.context, + color: accentColor, + fileId: status?.fileId, + file: nil + )))), + accessory: nil, + action: { [weak state] view in + guard let view = view as? ListActionItemComponent.View, let iconView = view.iconView else { + return + } + state?.openEmojiSetup(sourceView: iconView, currentFileId: nil, color: accentColor) + } + )))) + + perksItems.append(AnyComponentWithIdentity(id: perksItems.count, component: AnyComponent(ListActionItemComponent( + theme: environment.theme, + title: AnyComponent(VStack([ + AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: strings.Business_TagYourChats, + font: Font.regular(presentationData.listsFontSize.baseDisplaySize), + textColor: environment.theme.list.itemPrimaryTextColor + )), + maximumNumberOfLines: 0 + ))), + AnyComponentWithIdentity(id: AnyHashable(1), component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: strings.Business_TagYourChatsInfo, + font: Font.regular(floor(presentationData.listsFontSize.baseDisplaySize * 13.0 / 17.0)), + textColor: environment.theme.list.itemSecondaryTextColor + )), + maximumNumberOfLines: 0, + lineSpacing: 0.18 + ))) + ], alignment: .left, spacing: 2.0)), + leftIcon: AnyComponentWithIdentity(id: 0, component: AnyComponent(PerkIconComponent( + backgroundColor: UIColor(rgb: 0x4492ff), + foregroundColor: .white, + iconName: "Premium/BusinessPerk/Tag" + ))), + action: { _ in + push(accountContext.sharedContext.makeFilterSettingsController(context: accountContext, modal: false, scrollToTags: true, dismissed: nil)) + } + )))) + + perksItems.append(AnyComponentWithIdentity(id: perksItems.count, component: AnyComponent(ListActionItemComponent( + theme: environment.theme, + title: AnyComponent(VStack([ + AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: strings.Business_AddPost, + font: Font.regular(presentationData.listsFontSize.baseDisplaySize), + textColor: environment.theme.list.itemPrimaryTextColor + )), + maximumNumberOfLines: 0 + ))), + AnyComponentWithIdentity(id: AnyHashable(1), component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: strings.Business_AddPostInfo, + font: Font.regular(floor(presentationData.listsFontSize.baseDisplaySize * 13.0 / 17.0)), + textColor: environment.theme.list.itemSecondaryTextColor + )), + maximumNumberOfLines: 0, + lineSpacing: 0.18 + ))) + ], alignment: .left, spacing: 2.0)), + leftIcon: AnyComponentWithIdentity(id: 0, component: AnyComponent(PerkIconComponent( + backgroundColor: UIColor(rgb: 0x41a6a5), + foregroundColor: .white, + iconName: "Premium/Perk/Stories" + ))), + action: { _ in + push(accountContext.sharedContext.makeMyStoriesController(context: accountContext, isArchive: false)) + } + )))) + + let moreBusinessSection = moreBusinessSection.update( + component: ListSectionComponent( + theme: environment.theme, + header: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: strings.Business_MoreFeaturesTitle.uppercased(), + font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize), + textColor: environment.theme.list.freeTextColor + )), + maximumNumberOfLines: 0 + )), + footer: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: environment.strings.Business_MoreFeaturesInfo, + font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize), + textColor: environment.theme.list.freeTextColor + )), + maximumNumberOfLines: 0 + )), + items: perksItems + ), + environment: {}, + availableSize: CGSize(width: availableWidth - sideInsets, height: .greatestFiniteMagnitude), + transition: context.transition + ) + context.add(moreBusinessSection + .position(CGPoint(x: availableWidth / 2.0, y: size.height + moreBusinessSection.size.height / 2.0)) + .clipsToBounds(true) + .cornerRadius(10.0) + ) + size.height += moreBusinessSection.size.height + size.height += 23.0 + } + let copyLink = context.component.copyLink if case .emojiStatus = context.component.source { layoutPerks() @@ -2186,154 +2467,165 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent { layoutPerks() } else { layoutOptions() - layoutPerks() - let textPadding: CGFloat = 13.0 + if case .business = context.component.mode { + layoutBusinessPerks() + if context.component.isPremium == true { + layoutMoreBusinessPerks() + } + } else { + layoutPerks() - let infoTitle = infoTitle.update( - component: MultilineTextComponent( - text: .plain( - NSAttributedString(string: strings.Premium_AboutTitle.uppercased(), font: Font.regular(14.0), textColor: environment.theme.list.freeTextColor) + let textPadding: CGFloat = 13.0 + + let infoTitle = infoTitle.update( + component: MultilineTextComponent( + text: .plain( + NSAttributedString(string: strings.Premium_AboutTitle.uppercased(), font: Font.regular(14.0), textColor: environment.theme.list.freeTextColor) + ), + horizontalAlignment: .natural, + maximumNumberOfLines: 0, + lineSpacing: 0.2 ), - horizontalAlignment: .natural, - maximumNumberOfLines: 0, - lineSpacing: 0.2 - ), - environment: {}, - availableSize: CGSize(width: availableWidth - sideInsets, height: .greatestFiniteMagnitude), - transition: context.transition - ) - context.add(infoTitle - .position(CGPoint(x: sideInset + environment.safeInsets.left + textSideInset + infoTitle.size.width / 2.0, y: size.height + infoTitle.size.height / 2.0)) - ) - size.height += infoTitle.size.height - size.height += 3.0 - - let infoText = infoText.update( - component: MultilineTextComponent( - text: .markdown( - text: strings.Premium_AboutText, - attributes: markdownAttributes + environment: {}, + availableSize: CGSize(width: availableWidth - sideInsets, height: .greatestFiniteMagnitude), + transition: context.transition + ) + context.add(infoTitle + .position(CGPoint(x: sideInset + environment.safeInsets.left + textSideInset + infoTitle.size.width / 2.0, y: size.height + infoTitle.size.height / 2.0)) + ) + size.height += infoTitle.size.height + size.height += 3.0 + + let infoText = infoText.update( + component: MultilineTextComponent( + text: .markdown( + text: strings.Premium_AboutText, + attributes: markdownAttributes + ), + horizontalAlignment: .natural, + maximumNumberOfLines: 0, + lineSpacing: 0.2 ), - horizontalAlignment: .natural, - maximumNumberOfLines: 0, - lineSpacing: 0.2 - ), - environment: {}, - availableSize: CGSize(width: availableWidth - sideInsets - textSideInset * 2.0, height: .greatestFiniteMagnitude), - transition: context.transition - ) - - let infoBackground = infoBackground.update( - component: RoundedRectangle( - color: environment.theme.list.itemBlocksBackgroundColor, - cornerRadius: 10.0 - ), - environment: {}, - availableSize: CGSize(width: availableWidth - sideInsets, height: infoText.size.height + textPadding * 2.0), - transition: context.transition - ) - context.add(infoBackground - .position(CGPoint(x: size.width / 2.0, y: size.height + infoBackground.size.height / 2.0)) - ) - context.add(infoText - .position(CGPoint(x: sideInset + environment.safeInsets.left + textSideInset + infoText.size.width / 2.0, y: size.height + textPadding + infoText.size.height / 2.0)) - ) - size.height += infoBackground.size.height - size.height += 6.0 - - let termsFont = Font.regular(13.0) - let boldTermsFont = Font.semibold(13.0) - let italicTermsFont = Font.italic(13.0) - let boldItalicTermsFont = Font.semiboldItalic(13.0) - let monospaceTermsFont = Font.monospace(13.0) - let termsTextColor = environment.theme.list.freeTextColor - let termsMarkdownAttributes = MarkdownAttributes(body: MarkdownAttributeSet(font: termsFont, textColor: termsTextColor), bold: MarkdownAttributeSet(font: termsFont, textColor: termsTextColor), link: MarkdownAttributeSet(font: termsFont, textColor: environment.theme.list.itemAccentColor), linkAttribute: { contents in - return (TelegramTextAttributes.URL, contents) - }) - - var isGiftView = false - if case let .gift(fromId, _, _, _) = context.component.source { - if fromId == context.component.context.account.peerId { - isGiftView = true - } - } - - let termsString: MultilineTextComponent.TextContent - if isGiftView { - termsString = .plain(NSAttributedString()) - } else if let promoConfiguration = context.component.promoConfiguration { - let attributedString = stringWithAppliedEntities(promoConfiguration.status, entities: promoConfiguration.statusEntities, baseColor: termsTextColor, linkColor: environment.theme.list.itemAccentColor, baseFont: termsFont, linkFont: termsFont, boldFont: boldTermsFont, italicFont: italicTermsFont, boldItalicFont: boldItalicTermsFont, fixedFont: monospaceTermsFont, blockQuoteFont: termsFont, message: nil) - termsString = .plain(attributedString) - } else { - termsString = .markdown( - text: strings.Premium_Terms, - attributes: termsMarkdownAttributes + environment: {}, + availableSize: CGSize(width: availableWidth - sideInsets - textSideInset * 2.0, height: .greatestFiniteMagnitude), + transition: context.transition ) - } - - let controller = environment.controller - let termsTapActionImpl: ([NSAttributedString.Key: Any]) -> Void = { attributes in - if let url = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] as? String, - let controller = controller() as? PremiumIntroScreen, let navigationController = controller.navigationController as? NavigationController { - if url.hasPrefix("https://apps.apple.com/account/subscriptions") { - controller.context.sharedContext.applicationBindings.openSubscriptions() - } else if url.hasPrefix("https://") || url.hasPrefix("tg://") { - controller.context.sharedContext.openExternalUrl(context: controller.context, urlContext: .generic, url: url, forceExternal: !url.hasPrefix("tg://"), presentationData: controller.context.sharedContext.currentPresentationData.with({$0}), navigationController: nil, dismissInput: {}) - } else { - let context = controller.context - let signal: Signal? - switch url { - case "terms": - signal = cachedTermsPage(context: context) - case "privacy": - signal = cachedPrivacyPage(context: context) - default: - signal = nil - } - if let signal = signal { - let _ = (signal - |> deliverOnMainQueue).start(next: { resolvedUrl in - context.sharedContext.openResolvedUrl(resolvedUrl, context: context, urlContext: .generic, navigationController: navigationController, forceExternal: false, openPeer: { peer, navigation in - }, sendFile: nil, sendSticker: nil, requestMessageActionUrlAuth: nil, joinVoiceChat: nil, present: { [weak controller] c, arguments in - controller?.push(c) - }, dismissInput: {}, contentContext: nil, progress: nil, completion: nil) - }) - } + + let infoBackground = infoBackground.update( + component: RoundedRectangle( + color: environment.theme.list.itemBlocksBackgroundColor, + cornerRadius: 10.0 + ), + environment: {}, + availableSize: CGSize(width: availableWidth - sideInsets, height: infoText.size.height + textPadding * 2.0), + transition: context.transition + ) + context.add(infoBackground + .position(CGPoint(x: size.width / 2.0, y: size.height + infoBackground.size.height / 2.0)) + ) + context.add(infoText + .position(CGPoint(x: sideInset + environment.safeInsets.left + textSideInset + infoText.size.width / 2.0, y: size.height + textPadding + infoText.size.height / 2.0)) + ) + size.height += infoBackground.size.height + size.height += 6.0 + + let termsFont = Font.regular(13.0) + let boldTermsFont = Font.semibold(13.0) + let italicTermsFont = Font.italic(13.0) + let boldItalicTermsFont = Font.semiboldItalic(13.0) + let monospaceTermsFont = Font.monospace(13.0) + let termsTextColor = environment.theme.list.freeTextColor + let termsMarkdownAttributes = MarkdownAttributes(body: MarkdownAttributeSet(font: termsFont, textColor: termsTextColor), bold: MarkdownAttributeSet(font: termsFont, textColor: termsTextColor), link: MarkdownAttributeSet(font: termsFont, textColor: environment.theme.list.itemAccentColor), linkAttribute: { contents in + return (TelegramTextAttributes.URL, contents) + }) + + var isGiftView = false + if case let .gift(fromId, _, _, _) = context.component.source { + if fromId == context.component.context.account.peerId { + isGiftView = true } } - } - - let termsText = termsText.update( - component: MultilineTextComponent( - text: termsString, - horizontalAlignment: .natural, - maximumNumberOfLines: 0, - lineSpacing: 0.0, - highlightColor: environment.theme.list.itemAccentColor.withAlphaComponent(0.2), - highlightAction: { attributes in - if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] { - return NSAttributedString.Key(rawValue: TelegramTextAttributes.URL) + + let termsString: MultilineTextComponent.TextContent + if isGiftView { + termsString = .plain(NSAttributedString()) + } else if let promoConfiguration = context.component.promoConfiguration { + let attributedString = stringWithAppliedEntities(promoConfiguration.status, entities: promoConfiguration.statusEntities, baseColor: termsTextColor, linkColor: environment.theme.list.itemAccentColor, baseFont: termsFont, linkFont: termsFont, boldFont: boldTermsFont, italicFont: italicTermsFont, boldItalicFont: boldItalicTermsFont, fixedFont: monospaceTermsFont, blockQuoteFont: termsFont, message: nil) + termsString = .plain(attributedString) + } else { + termsString = .markdown( + text: strings.Premium_Terms, + attributes: termsMarkdownAttributes + ) + } + + let controller = environment.controller + let termsTapActionImpl: ([NSAttributedString.Key: Any]) -> Void = { attributes in + if let url = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] as? String, + let controller = controller() as? PremiumIntroScreen, let navigationController = controller.navigationController as? NavigationController { + if url.hasPrefix("https://apps.apple.com/account/subscriptions") { + controller.context.sharedContext.applicationBindings.openSubscriptions() + } else if url.hasPrefix("https://") || url.hasPrefix("tg://") { + controller.context.sharedContext.openExternalUrl(context: controller.context, urlContext: .generic, url: url, forceExternal: !url.hasPrefix("tg://"), presentationData: controller.context.sharedContext.currentPresentationData.with({$0}), navigationController: nil, dismissInput: {}) } else { - return nil + let context = controller.context + let signal: Signal? + switch url { + case "terms": + signal = cachedTermsPage(context: context) + case "privacy": + signal = cachedPrivacyPage(context: context) + default: + signal = nil + } + if let signal = signal { + let _ = (signal + |> deliverOnMainQueue).start(next: { resolvedUrl in + context.sharedContext.openResolvedUrl(resolvedUrl, context: context, urlContext: .generic, navigationController: navigationController, forceExternal: false, openPeer: { peer, navigation in + }, sendFile: nil, sendSticker: nil, requestMessageActionUrlAuth: nil, joinVoiceChat: nil, present: { [weak controller] c, arguments in + controller?.push(c) + }, dismissInput: {}, contentContext: nil, progress: nil, completion: nil) + }) + } } - }, - tapAction: { attributes, _ in - termsTapActionImpl(attributes) } - ), - environment: {}, - availableSize: CGSize(width: availableWidth - sideInsets - textSideInset * 2.0, height: .greatestFiniteMagnitude), - transition: context.transition - ) - context.add(termsText - .position(CGPoint(x: sideInset + environment.safeInsets.left + textSideInset + termsText.size.width / 2.0, y: size.height + termsText.size.height / 2.0)) - ) - size.height += termsText.size.height - size.height += 10.0 + } + + let termsText = termsText.update( + component: MultilineTextComponent( + text: termsString, + horizontalAlignment: .natural, + maximumNumberOfLines: 0, + lineSpacing: 0.0, + highlightColor: environment.theme.list.itemAccentColor.withAlphaComponent(0.2), + highlightAction: { attributes in + if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] { + return NSAttributedString.Key(rawValue: TelegramTextAttributes.URL) + } else { + return nil + } + }, + tapAction: { attributes, _ in + termsTapActionImpl(attributes) + } + ), + environment: {}, + availableSize: CGSize(width: availableWidth - sideInsets - textSideInset * 2.0, height: .greatestFiniteMagnitude), + transition: context.transition + ) + context.add(termsText + .position(CGPoint(x: sideInset + environment.safeInsets.left + textSideInset + termsText.size.width / 2.0, y: size.height + termsText.size.height / 2.0)) + ) + size.height += termsText.size.height + size.height += 10.0 + } } size.height += scrollEnvironment.insets.bottom + if case .business = context.component.mode, state.isPremium == false { + size.height += 123.0 + } if context.component.source != .settings { size.height += 44.0 @@ -2348,6 +2640,7 @@ private final class PremiumIntroScreenComponent: CombinedComponent { typealias EnvironmentType = ViewControllerComponentContainer.Environment let context: AccountContext + let mode: PremiumIntroScreen.Mode let source: PremiumSource let forceDark: Bool let forceHasPremium: Bool @@ -2358,8 +2651,9 @@ private final class PremiumIntroScreenComponent: CombinedComponent { let copyLink: (String) -> Void let shareLink: (String) -> Void - init(context: AccountContext, source: PremiumSource, forceDark: Bool, forceHasPremium: Bool, updateInProgress: @escaping (Bool) -> Void, present: @escaping (ViewController) -> Void, push: @escaping (ViewController) -> Void, completion: @escaping () -> Void, copyLink: @escaping (String) -> Void, shareLink: @escaping (String) -> Void) { + init(context: AccountContext, mode: PremiumIntroScreen.Mode, source: PremiumSource, forceDark: Bool, forceHasPremium: Bool, updateInProgress: @escaping (Bool) -> Void, present: @escaping (ViewController) -> Void, push: @escaping (ViewController) -> Void, completion: @escaping () -> Void, copyLink: @escaping (String) -> Void, shareLink: @escaping (String) -> Void) { self.context = context + self.mode = mode self.source = source self.forceDark = forceDark self.forceHasPremium = forceHasPremium @@ -2375,6 +2669,9 @@ private final class PremiumIntroScreenComponent: CombinedComponent { if lhs.context !== rhs.context { return false } + if lhs.mode != rhs.mode { + return false + } if lhs.source != rhs.source { return false } @@ -2757,6 +3054,7 @@ private final class PremiumIntroScreenComponent: CombinedComponent { let scrollContent = Child(ScrollComponent.self) let star = Child(PremiumStarComponent.self) let emoji = Child(EmojiHeaderComponent.self) + let coin = Child(PremiumCoinComponent.self) let topPanel = Child(BlurredBackgroundComponent.self) let topSeparator = Child(Rectangle.self) let title = Child(MultilineTextComponent.self) @@ -2782,7 +3080,17 @@ private final class PremiumIntroScreenComponent: CombinedComponent { } let header: _UpdatedChildComponent - if case let .emojiStatus(_, fileId, _, _) = context.component.source { + if case .business = context.component.mode { + header = coin.update( + component: PremiumCoinComponent( + isIntro: isIntro, + isVisible: starIsVisible, + hasIdleAnimations: state.hasIdleAnimations + ), + availableSize: CGSize(width: min(414.0, context.availableSize.width), height: 220.0), + transition: context.transition + ) + } else if case let .emojiStatus(_, fileId, _, _) = context.component.source { header = emoji.update( component: EmojiHeaderComponent( context: context.component.context, @@ -2826,7 +3134,9 @@ private final class PremiumIntroScreenComponent: CombinedComponent { ) let titleString: String - if case .emojiStatus = context.component.source { + if case .business = context.component.mode { + titleString = environment.strings.Business_Title + } else if case .emojiStatus = context.component.source { titleString = environment.strings.Premium_Title } else if case .giftTerms = context.component.source { titleString = environment.strings.Premium_Title @@ -2858,10 +3168,14 @@ private final class PremiumIntroScreenComponent: CombinedComponent { let secondaryTitleText: String var isAnonymous = false if var otherPeerName = state.otherPeerName { - if case let .emojiStatus(_, _, file, maybeEmojiPack) = context.component.source, let emojiPack = maybeEmojiPack, case let .result(info, _, _) = emojiPack { + if case let .emojiStatus(peerId, _, file, maybeEmojiPack) = context.component.source, let emojiPack = maybeEmojiPack, case let .result(info, _, _) = emojiPack { loadedEmojiPack = maybeEmojiPack highlightableLinks = true + if peerId.isGroupOrChannel, otherPeerName.count > 20 { + otherPeerName = otherPeerName.prefix(20).trimmingCharacters(in: .whitespacesAndNewlines) + "\u{2026}" + } + var packReference: StickerPackReference? if let file = file { for attribute in file.attributes { @@ -2978,6 +3292,7 @@ private final class PremiumIntroScreenComponent: CombinedComponent { component: ScrollComponent( content: AnyComponent(PremiumIntroScreenContentComponent( context: context.component.context, + mode: context.component.mode, source: context.component.source, forceDark: context.component.forceDark, isPremium: state.isPremium, @@ -2988,6 +3303,7 @@ private final class PremiumIntroScreenComponent: CombinedComponent { validPurchases: state.validPurchases, promoConfiguration: state.promoConfiguration, present: context.component.present, + push: context.component.push, selectProduct: { [weak state] productId in state?.selectProduct(productId) }, @@ -3204,7 +3520,13 @@ private final class PremiumIntroScreenComponent: CombinedComponent { } public final class PremiumIntroScreen: ViewControllerComponentContainer { + public enum Mode { + case premium + case business + } + fileprivate let context: AccountContext + fileprivate let mode: Mode private var didSetReady = false private let _ready = Promise() @@ -3216,8 +3538,9 @@ public final class PremiumIntroScreen: ViewControllerComponentContainer { public weak var containerView: UIView? public var animationColor: UIColor? - public init(context: AccountContext, modal: Bool = true, source: PremiumSource, forceDark: Bool = false, forceHasPremium: Bool = false) { + public init(context: AccountContext, mode: Mode = .premium, source: PremiumSource, modal: Bool = true, forceDark: Bool = false, forceHasPremium: Bool = false) { self.context = context + self.mode = mode var updateInProgressImpl: ((Bool) -> Void)? var pushImpl: ((ViewController) -> Void)? @@ -3227,6 +3550,7 @@ public final class PremiumIntroScreen: ViewControllerComponentContainer { var shareLinkImpl: ((String) -> Void)? super.init(context: context, component: PremiumIntroScreenComponent( context: context, + mode: mode, source: source, forceDark: forceDark, forceHasPremium: forceHasPremium, @@ -3324,6 +3648,10 @@ public final class PremiumIntroScreen: ViewControllerComponentContainer { } navigationController.pushViewController(peerSelectionController) } + + if case .business = mode { + context.account.viewTracker.keepQuickRepliesApproximatelyUpdated() + } } required public init(coder aDecoder: NSCoder) { @@ -3362,7 +3690,10 @@ public final class PremiumIntroScreen: ViewControllerComponentContainer { super.containerLayoutUpdated(layout, transition: transition) if !self.didSetReady { - if let view = self.node.hostView.findTaggedView(tag: PremiumStarComponent.View.Tag()) as? PremiumStarComponent.View { + if let view = self.node.hostView.findTaggedView(tag: PremiumCoinComponent.View.Tag()) as? PremiumCoinComponent.View { + self.didSetReady = true + self._ready.set(view.ready) + } else if let view = self.node.hostView.findTaggedView(tag: PremiumStarComponent.View.Tag()) as? PremiumStarComponent.View { self.didSetReady = true self._ready.set(view.ready) @@ -3393,3 +3724,144 @@ public final class PremiumIntroScreen: ViewControllerComponentContainer { } } } + + + + +private final class EmojiActionIconComponent: Component { + let context: AccountContext + let color: UIColor + let fileId: Int64? + let file: TelegramMediaFile? + + init( + context: AccountContext, + color: UIColor, + fileId: Int64?, + file: TelegramMediaFile? + ) { + self.context = context + self.color = color + self.fileId = fileId + self.file = file + } + + static func ==(lhs: EmojiActionIconComponent, rhs: EmojiActionIconComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.color != rhs.color { + return false + } + if lhs.fileId != rhs.fileId { + return false + } + if lhs.file != rhs.file { + return false + } + return true + } + + final class View: UIView { + private let icon = ComponentView() + + func update(component: EmojiActionIconComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + let size = CGSize(width: 24.0, height: 24.0) + + let _ = self.icon.update( + transition: .immediate, + component: AnyComponent(EmojiStatusComponent( + context: component.context, + animationCache: component.context.animationCache, + animationRenderer: component.context.animationRenderer, + content: component.fileId.flatMap { .animation( + content: .customEmoji(fileId: $0), + size: CGSize(width: size.width * 2.0, height: size.height * 2.0), + placeholderColor: .lightGray, + themeColor: component.color, + loopMode: .forever + ) } ?? .premium(color: component.color), + isVisibleForAnimations: false, + action: nil + )), + environment: {}, + containerSize: size + ) + let iconFrame = CGRect(origin: CGPoint(), size: size) + if let iconView = self.icon.view { + if iconView.superview == nil { + self.addSubview(iconView) + } + iconView.frame = iconFrame + } + + return size + } + } + + func makeView() -> View { + return View(frame: CGRect()) + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} + +private final class BadgeComponent: CombinedComponent { + let color: UIColor + let text: String + + init( + color: UIColor, + text: String + ) { + self.color = color + self.text = text + } + + static func ==(lhs: BadgeComponent, rhs: BadgeComponent) -> Bool { + if lhs.color != rhs.color { + return false + } + if lhs.text != rhs.text { + return false + } + return true + } + + static var body: Body { + let badgeBackground = Child(RoundedRectangle.self) + let badgeText = Child(MultilineTextComponent.self) + + return { context in + let component = context.component + + let badgeText = badgeText.update( + component: MultilineTextComponent(text: .plain(NSAttributedString(string: component.text, font: Font.semibold(11.0), textColor: .white))), + availableSize: context.availableSize, + transition: context.transition + ) + + let badgeSize = CGSize(width: badgeText.size.width + 7.0, height: 16.0) + let badgeBackground = badgeBackground.update( + component: RoundedRectangle( + color: component.color, + cornerRadius: 5.0 + ), + availableSize: badgeSize, + transition: context.transition + ) + + context.add(badgeBackground + .position(CGPoint(x: badgeSize.width / 2.0, y: badgeSize.height / 2.0)) + ) + + context.add(badgeText + .position(CGPoint(x: badgeSize.width / 2.0, y: badgeSize.height / 2.0)) + ) + + return badgeSize + } + } +} diff --git a/submodules/PremiumUI/Sources/PremiumLimitsListScreen.swift b/submodules/PremiumUI/Sources/PremiumLimitsListScreen.swift index 0ca02854bfe..dc6accd6486 100644 --- a/submodules/PremiumUI/Sources/PremiumLimitsListScreen.swift +++ b/submodules/PremiumUI/Sources/PremiumLimitsListScreen.swift @@ -29,6 +29,7 @@ public class PremiumLimitsListScreen: ViewController { let backgroundView: ComponentHostView let pagerView: ComponentHostView let closeView: ComponentHostView + let closeDarkIconView = UIImageView() fileprivate let footerNode: FooterNode @@ -227,6 +228,8 @@ public class PremiumLimitsListScreen: ViewController { if scrollView.contentSize.width > scrollView.contentSize.height || scrollView.contentSize.height > 1500.0 { return false } + } else if otherGestureRecognizer.view is PremiumCoinComponent.View { + return false } return true } @@ -380,22 +383,46 @@ public class PremiumLimitsListScreen: ViewController { isStandalone = true } - if let stickers = self.stickers, let appIcons = self.appIcons, let configuration = self.promoConfiguration { + let theme = self.presentationData.theme + let strings = self.presentationData.strings + + let videos: [String: TelegramMediaFile] = self.promoConfiguration?.videos ?? [:] + let stickers = self.stickers ?? [] + let appIcons = self.appIcons ?? [] + + let isReady: Bool + switch controller.subject { + case .premiumStickers: + isReady = !stickers.isEmpty + case .appIcons: + isReady = !appIcons.isEmpty + case .stories: + isReady = true + case .doubleLimits: + isReady = true + case .business: + isReady = true + default: + isReady = !videos.isEmpty + } + + if isReady { let context = controller.context - let theme = self.presentationData.theme - let strings = self.presentationData.strings - + let textColor = theme.actionSheet.primaryTextColor var availableItems: [PremiumPerk: DemoPagerComponent.Item] = [:] var storiesIndex: Int? var limitsIndex: Int? + var businessIndex: Int? var storiesNeighbors = PageNeighbors(leftIsList: false, rightIsList: false) var limitsNeighbors = PageNeighbors(leftIsList: false, rightIsList: false) + let businessNeighbors = PageNeighbors(leftIsList: false, rightIsList: false) if let order = controller.order { storiesIndex = order.firstIndex(where: { $0 == .stories }) limitsIndex = order.firstIndex(where: { $0 == .doubleLimits }) + businessIndex = order.firstIndex(where: { $0 == .business }) if let limitsIndex, let storiesIndex { if limitsIndex == storiesIndex + 1 { storiesNeighbors.rightIsList = true @@ -477,7 +504,7 @@ public class PremiumLimitsListScreen: ViewController { content: AnyComponent(PhoneDemoComponent( context: context, position: .bottom, - videoFile: configuration.videos["more_upload"], + videoFile: videos["more_upload"], decoration: .dataRain )), title: strings.Premium_UploadSize, @@ -495,7 +522,7 @@ public class PremiumLimitsListScreen: ViewController { content: AnyComponent(PhoneDemoComponent( context: context, position: .top, - videoFile: configuration.videos["faster_download"], + videoFile: videos["faster_download"], decoration: .fasterStars )), title: strings.Premium_FasterSpeed, @@ -513,7 +540,7 @@ public class PremiumLimitsListScreen: ViewController { content: AnyComponent(PhoneDemoComponent( context: context, position: .top, - videoFile: configuration.videos["voice_to_text"], + videoFile: videos["voice_to_text"], decoration: .badgeStars )), title: strings.Premium_VoiceToText, @@ -531,7 +558,7 @@ public class PremiumLimitsListScreen: ViewController { content: AnyComponent(PhoneDemoComponent( context: context, position: .bottom, - videoFile: configuration.videos["no_ads"], + videoFile: videos["no_ads"], decoration: .swirlStars )), title: strings.Premium_NoAds, @@ -549,7 +576,7 @@ public class PremiumLimitsListScreen: ViewController { content: AnyComponent(PhoneDemoComponent( context: context, position: .top, - videoFile: configuration.videos["infinite_reactions"], + videoFile: videos["infinite_reactions"], decoration: .swirlStars )), title: strings.Premium_InfiniteReactions, @@ -588,7 +615,7 @@ public class PremiumLimitsListScreen: ViewController { content: AnyComponent(PhoneDemoComponent( context: context, position: .top, - videoFile: configuration.videos["emoji_status"], + videoFile: videos["emoji_status"], decoration: .badgeStars )), title: strings.Premium_EmojiStatus, @@ -606,7 +633,7 @@ public class PremiumLimitsListScreen: ViewController { content: AnyComponent(PhoneDemoComponent( context: context, position: .top, - videoFile: configuration.videos["advanced_chat_management"], + videoFile: videos["advanced_chat_management"], decoration: .swirlStars )), title: strings.Premium_ChatManagement, @@ -624,7 +651,7 @@ public class PremiumLimitsListScreen: ViewController { content: AnyComponent(PhoneDemoComponent( context: context, position: .top, - videoFile: configuration.videos["profile_badge"], + videoFile: videos["profile_badge"], decoration: .badgeStars )), title: strings.Premium_Badge, @@ -642,7 +669,7 @@ public class PremiumLimitsListScreen: ViewController { content: AnyComponent(PhoneDemoComponent( context: context, position: .top, - videoFile: configuration.videos["animated_userpics"], + videoFile: videos["animated_userpics"], decoration: .swirlStars )), title: strings.Premium_Avatar, @@ -676,7 +703,7 @@ public class PremiumLimitsListScreen: ViewController { content: AnyComponent(PhoneDemoComponent( context: context, position: .bottom, - videoFile: configuration.videos["animated_emoji"], + videoFile: videos["animated_emoji"], decoration: .emoji )), title: strings.Premium_AnimatedEmoji, @@ -695,7 +722,7 @@ public class PremiumLimitsListScreen: ViewController { context: context, position: .top, model: .island, - videoFile: configuration.videos["translations"], + videoFile: videos["translations"], decoration: .hello )), title: strings.Premium_Translation, @@ -713,7 +740,7 @@ public class PremiumLimitsListScreen: ViewController { content: AnyComponent(PhoneDemoComponent( context: context, position: .top, - videoFile: configuration.videos["peer_colors"], + videoFile: videos["peer_colors"], decoration: .badgeStars )), title: strings.Premium_Colors, @@ -732,7 +759,7 @@ public class PremiumLimitsListScreen: ViewController { context: context, position: .top, model: .island, - videoFile: configuration.videos["wallpapers"], + videoFile: videos["wallpapers"], decoration: .swirlStars )), title: strings.Premium_Wallpapers, @@ -751,7 +778,7 @@ public class PremiumLimitsListScreen: ViewController { context: context, position: .top, model: .island, - videoFile: configuration.videos["saved_tags"], + videoFile: videos["saved_tags"], decoration: .tag )), title: strings.Premium_MessageTags, @@ -770,7 +797,7 @@ public class PremiumLimitsListScreen: ViewController { context: context, position: .top, model: .island, - videoFile: configuration.videos["last_seen"], + videoFile: videos["last_seen"], decoration: .badgeStars )), title: strings.Premium_LastSeen, @@ -789,7 +816,7 @@ public class PremiumLimitsListScreen: ViewController { context: context, position: .top, model: .island, - videoFile: configuration.videos["message_privacy"], + videoFile: videos["message_privacy"], decoration: .swirlStars )), title: strings.Premium_MessagePrivacy, @@ -799,7 +826,159 @@ public class PremiumLimitsListScreen: ViewController { ) ) ) - + availableItems[.business] = DemoPagerComponent.Item( + AnyComponentWithIdentity( + id: PremiumDemoScreen.Subject.business, + component: AnyComponent( + BusinessPageComponent( + context: context, + theme: self.presentationData.theme, + neighbors: businessNeighbors, + bottomInset: self.footerNode.frame.height, + updatedBottomAlpha: { [weak self] alpha in + if let strongSelf = self { + strongSelf.footerNode.updateCoverAlpha(alpha, transition: .immediate) + } + }, + updatedDismissOffset: { [weak self] offset in + if let strongSelf = self { + strongSelf.updateDismissOffset(offset) + } + }, + updatedIsDisplaying: { [weak self] isDisplaying in + if let self, self.isExpanded && !isDisplaying { + if let businessIndex, let indexPosition = self.indexPosition, abs(CGFloat(businessIndex) - indexPosition) < 0.1 { + } else { + self.update(isExpanded: false, transition: .animated(duration: 0.2, curve: .easeInOut)) + } + } + } + ) + ) + ) + ) + + + availableItems[.businessLocation] = DemoPagerComponent.Item( + AnyComponentWithIdentity( + id: PremiumDemoScreen.Subject.businessLocation, + component: AnyComponent( + PageComponent( + content: AnyComponent(PhoneDemoComponent( + context: context, + position: .top, + model: .island, + videoFile: videos["business_location"], + decoration: .business + )), + title: strings.Business_Location, + text: strings.Business_LocationInfo, + textColor: textColor + ) + ) + ) + ) + + availableItems[.businessHours] = DemoPagerComponent.Item( + AnyComponentWithIdentity( + id: PremiumDemoScreen.Subject.businessHours, + component: AnyComponent( + PageComponent( + content: AnyComponent(PhoneDemoComponent( + context: context, + position: .top, + model: .island, + videoFile: videos["business_hours"], + decoration: .business + )), + title: strings.Business_OpeningHours, + text: strings.Business_OpeningHoursInfo, + textColor: textColor + ) + ) + ) + ) + + availableItems[.businessQuickReplies] = DemoPagerComponent.Item( + AnyComponentWithIdentity( + id: PremiumDemoScreen.Subject.businessQuickReplies, + component: AnyComponent( + PageComponent( + content: AnyComponent(PhoneDemoComponent( + context: context, + position: .top, + model: .island, + videoFile: videos["quick_replies"], + decoration: .business + )), + title: strings.Business_QuickReplies, + text: strings.Business_QuickRepliesInfo, + textColor: textColor + ) + ) + ) + ) + + availableItems[.businessGreetingMessage] = DemoPagerComponent.Item( + AnyComponentWithIdentity( + id: PremiumDemoScreen.Subject.businessGreetingMessage, + component: AnyComponent( + PageComponent( + content: AnyComponent(PhoneDemoComponent( + context: context, + position: .top, + model: .island, + videoFile: videos["greeting_message"], + decoration: .business + )), + title: strings.Business_GreetingMessages, + text: strings.Business_GreetingMessagesInfo, + textColor: textColor + ) + ) + ) + ) + + availableItems[.businessAwayMessage] = DemoPagerComponent.Item( + AnyComponentWithIdentity( + id: PremiumDemoScreen.Subject.businessAwayMessage, + component: AnyComponent( + PageComponent( + content: AnyComponent(PhoneDemoComponent( + context: context, + position: .top, + model: .island, + videoFile: videos["away_message"], + decoration: .business + )), + title: strings.Business_AwayMessages, + text: strings.Business_AwayMessagesInfo, + textColor: textColor + ) + ) + ) + ) + + availableItems[.businessChatBots] = DemoPagerComponent.Item( + AnyComponentWithIdentity( + id: PremiumDemoScreen.Subject.businessChatBots, + component: AnyComponent( + PageComponent( + content: AnyComponent(PhoneDemoComponent( + context: context, + position: .top, + model: .island, + videoFile: videos["business_bots"], + decoration: .business + )), + title: strings.Business_Chatbots, + text: strings.Business_ChatbotsInfo, + textColor: textColor + ) + ) + ) + ) + if let order = controller.order { var items: [DemoPagerComponent.Item] = order.compactMap { availableItems[$0] } let initialIndex: Int @@ -822,8 +1001,42 @@ public class PremiumLimitsListScreen: ViewController { nextAction: nextAction, updated: { [weak self] position, count in if let self { - self.indexPosition = position * CGFloat(count) + let indexPosition = position * CGFloat(count - 1) + self.indexPosition = indexPosition self.footerNode.updatePosition(position, count: count) + + var distance: CGFloat? + if let storiesIndex { + let value = indexPosition - CGFloat(storiesIndex) + if abs(value) < 1.0 { + distance = value + } + } + if let limitsIndex { + let value = indexPosition - CGFloat(limitsIndex) + if abs(value) < 1.0 { + distance = value + } + } + if let businessIndex { + let value = indexPosition - CGFloat(businessIndex) + if abs(value) < 1.0 { + distance = value + } + } + var distanceToPage: CGFloat = 1.0 + if let distance { + if distance >= 0.0 && distance < 0.1 { + distanceToPage = distance / 0.1 + } else if distance < 0.0 { + if distance >= -1.0 && distance < -0.9 { + distanceToPage = ((distance * -1.0) - 0.9) / 0.1 + } else { + distanceToPage = 0.0 + } + } + } + self.closeDarkIconView.alpha = 1.0 - max(0.0, min(1.0, distanceToPage)) } } ) @@ -852,7 +1065,7 @@ public class PremiumLimitsListScreen: ViewController { id: "background", component: AnyComponent( BlurredBackgroundComponent( - color: UIColor(rgb: 0x888888, alpha: 0.3) + color: UIColor(rgb: 0xbbbbbb, alpha: 0.22) ) ) ), @@ -874,6 +1087,12 @@ public class PremiumLimitsListScreen: ViewController { self.closeView.clipsToBounds = true self.closeView.layer.cornerRadius = 15.0 self.closeView.frame = CGRect(origin: CGPoint(x: contentSize.width - closeSize.width * 1.5, y: 28.0 - closeSize.height / 2.0), size: closeSize) + + if self.closeDarkIconView.image == nil { + self.closeDarkIconView.image = generateCloseButtonImage(backgroundColor: .clear, foregroundColor: theme.list.itemSecondaryTextColor)! + self.closeDarkIconView.frame = CGRect(origin: .zero, size: closeSize) + self.closeView.addSubview(self.closeDarkIconView) + } } private var cachedCloseImage: UIImage? diff --git a/submodules/PremiumUI/Sources/PremiumOptionComponent.swift b/submodules/PremiumUI/Sources/PremiumOptionComponent.swift new file mode 100644 index 00000000000..23ec27e787c --- /dev/null +++ b/submodules/PremiumUI/Sources/PremiumOptionComponent.swift @@ -0,0 +1,340 @@ +import Foundation +import UIKit +import Display +import ComponentFlow +import MultilineTextComponent +import CheckNode +import Markdown + +final class PremiumOptionComponent: CombinedComponent { + let title: String + let subtitle: String + let labelPrice: String + let discount: String + let multiple: Bool + let selected: Bool + let primaryTextColor: UIColor + let secondaryTextColor: UIColor + let accentColor: UIColor + let checkForegroundColor: UIColor + let checkBorderColor: UIColor + + init( + title: String, + subtitle: String, + labelPrice: String, + discount: String, + multiple: Bool = false, + selected: Bool, + primaryTextColor: UIColor, + secondaryTextColor: UIColor, + accentColor: UIColor, + checkForegroundColor: UIColor, + checkBorderColor: UIColor + ) { + self.title = title + self.subtitle = subtitle + self.labelPrice = labelPrice + self.discount = discount + self.multiple = multiple + self.selected = selected + self.primaryTextColor = primaryTextColor + self.secondaryTextColor = secondaryTextColor + self.accentColor = accentColor + self.checkForegroundColor = checkForegroundColor + self.checkBorderColor = checkBorderColor + } + + static func ==(lhs: PremiumOptionComponent, rhs: PremiumOptionComponent) -> Bool { + if lhs.title != rhs.title { + return false + } + if lhs.subtitle != rhs.subtitle { + return false + } + if lhs.labelPrice != rhs.labelPrice { + return false + } + if lhs.discount != rhs.discount { + return false + } + if lhs.multiple != rhs.multiple { + return false + } + if lhs.selected != rhs.selected { + return false + } + if lhs.primaryTextColor != rhs.primaryTextColor { + return false + } + if lhs.secondaryTextColor != rhs.secondaryTextColor { + return false + } + if lhs.accentColor != rhs.accentColor { + return false + } + if lhs.checkForegroundColor != rhs.checkForegroundColor { + return false + } + if lhs.checkBorderColor != rhs.checkBorderColor { + return false + } + return true + } + + static var body: Body { + let check = Child(CheckComponent.self) + let title = Child(MultilineTextComponent.self) + let subtitle = Child(MultilineTextComponent.self) + let discountBackground = Child(RoundedRectangle.self) + let discount = Child(MultilineTextComponent.self) + let label = Child(MultilineTextComponent.self) + + return { context in + let component = context.component + + var insets = UIEdgeInsets(top: 11.0, left: 46.0, bottom: 13.0, right: 16.0) + + let label = label.update( + component: MultilineTextComponent( + text: .plain( + NSAttributedString( + string: component.labelPrice, + font: Font.regular(17), + textColor: component.secondaryTextColor + ) + ), + maximumNumberOfLines: 1 + ), + availableSize: context.availableSize, + transition: context.transition + ) + + let title = title.update( + component: MultilineTextComponent( + text: .plain( + NSAttributedString( + string: component.title, + font: Font.regular(17), + textColor: component.primaryTextColor + ) + ), + maximumNumberOfLines: 1 + ), + availableSize: CGSize(width: context.availableSize.width - insets.left - insets.right - label.size.width, height: context.availableSize.height), + transition: context.transition + ) + + let discountSize: CGSize + if !component.discount.isEmpty { + let discount = discount.update( + component: MultilineTextComponent( + text: .plain( + NSAttributedString( + string: component.discount, + font: Font.with(size: 14.0, design: .round, weight: .semibold, traits: []), + textColor: .white + ) + ), + maximumNumberOfLines: 1 + ), + availableSize: context.availableSize, + transition: context.transition + ) + + discountSize = CGSize(width: discount.size.width + 6.0, height: 18.0) + + let discountBackground = discountBackground.update( + component: RoundedRectangle( + color: component.accentColor, + cornerRadius: 5.0 + ), + availableSize: discountSize, + transition: context.transition + ) + + let discountPosition = CGPoint(x: insets.left + title.size.width + 6.0 + discountSize.width / 2.0, y: insets.top + title.size.height / 2.0) + + context.add(discountBackground + .position(discountPosition) + ) + context.add(discount + .position(discountPosition) + ) + } else { + discountSize = CGSize(width: 0.0, height: 18.0) + } + + var spacing: CGFloat = 0.0 + var subtitleSize = CGSize() + if !component.subtitle.isEmpty { + spacing = 2.0 + + let subtitleFont = Font.regular(13) + let subtitleColor = component.secondaryTextColor + + let subtitleString = parseMarkdownIntoAttributedString( + component.subtitle, + attributes: MarkdownAttributes( + body: MarkdownAttributeSet(font: subtitleFont, textColor: subtitleColor), + bold: MarkdownAttributeSet(font: subtitleFont, textColor: subtitleColor, additionalAttributes: [NSAttributedString.Key.strikethroughStyle.rawValue: NSUnderlineStyle.single.rawValue as NSNumber]), + link: MarkdownAttributeSet(font: subtitleFont, textColor: subtitleColor), + linkAttribute: { _ in return nil } + ) + ) + + let subtitle = subtitle.update( + component: MultilineTextComponent( + text: .plain(subtitleString), + maximumNumberOfLines: 1 + ), + availableSize: CGSize(width: context.availableSize.width - insets.left - insets.right, height: context.availableSize.height), + transition: context.transition + ) + context.add(subtitle + .position(CGPoint(x: insets.left + subtitle.size.width / 2.0, y: insets.top + title.size.height + spacing + subtitle.size.height / 2.0)) + ) + subtitleSize = subtitle.size + + insets.top -= 2.0 + insets.bottom -= 2.0 + } + + let check = check.update( + component: CheckComponent( + theme: CheckComponent.Theme( + backgroundColor: component.accentColor, + strokeColor: component.checkForegroundColor, + borderColor: component.checkBorderColor, + overlayBorder: false, + hasInset: false, + hasShadow: false + ), + selected: component.selected + ), + availableSize: context.availableSize, + transition: context.transition + ) + + context.add(title + .position(CGPoint(x: insets.left + title.size.width / 2.0, y: insets.top + title.size.height / 2.0)) + ) + + let size = CGSize(width: context.availableSize.width, height: insets.top + title.size.height + spacing + subtitleSize.height + insets.bottom) + + let distance = context.availableSize.width - insets.left - insets.right - label.size.width - subtitleSize.width + + let labelY: CGFloat + if distance > 8.0 { + labelY = size.height / 2.0 + } else { + labelY = insets.top + title.size.height / 2.0 + } + + context.add(label + .position(CGPoint(x: context.availableSize.width - insets.right - label.size.width / 2.0, y: labelY)) + ) + + context.add(check + .position(CGPoint(x: 4.0 + check.size.width / 2.0, y: size.height / 2.0)) + ) + + return size + } + } +} + +private final class CheckComponent: Component { + struct Theme: Equatable { + public let backgroundColor: UIColor + public let strokeColor: UIColor + public let borderColor: UIColor + public let overlayBorder: Bool + public let hasInset: Bool + public let hasShadow: Bool + public let filledBorder: Bool + public let borderWidth: CGFloat? + + public init(backgroundColor: UIColor, strokeColor: UIColor, borderColor: UIColor, overlayBorder: Bool, hasInset: Bool, hasShadow: Bool, filledBorder: Bool = false, borderWidth: CGFloat? = nil) { + self.backgroundColor = backgroundColor + self.strokeColor = strokeColor + self.borderColor = borderColor + self.overlayBorder = overlayBorder + self.hasInset = hasInset + self.hasShadow = hasShadow + self.filledBorder = filledBorder + self.borderWidth = borderWidth + } + + var checkNodeTheme: CheckNodeTheme { + return CheckNodeTheme( + backgroundColor: self.backgroundColor, + strokeColor: self.strokeColor, + borderColor: self.borderColor, + overlayBorder: self.overlayBorder, + hasInset: self.hasInset, + hasShadow: self.hasShadow, + filledBorder: self.filledBorder, + borderWidth: self.borderWidth + ) + } + } + + let theme: Theme + let selected: Bool + + init( + theme: Theme, + selected: Bool + ) { + self.theme = theme + self.selected = selected + } + + static func ==(lhs: CheckComponent, rhs: CheckComponent) -> Bool { + if lhs.theme != rhs.theme { + return false + } + if lhs.selected != rhs.selected { + return false + } + return true + } + + final class View: UIView { + private var currentValue: CGFloat? + private var animator: DisplayLinkAnimator? + + private var checkLayer: CheckLayer { + return self.layer as! CheckLayer + } + + override class var layerClass: AnyClass { + return CheckLayer.self + } + + init() { + super.init(frame: CGRect()) + } + + required init?(coder aDecoder: NSCoder) { + preconditionFailure() + } + + + func update(component: CheckComponent, availableSize: CGSize, transition: Transition) -> CGSize { + self.checkLayer.setSelected(component.selected, animated: true) + self.checkLayer.theme = component.theme.checkNodeTheme + + return CGSize(width: 22.0, height: 22.0) + } + } + + func makeView() -> View { + return View() + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, transition: transition) + } +} diff --git a/submodules/PremiumUI/Sources/PremiumStarComponent.swift b/submodules/PremiumUI/Sources/PremiumStarComponent.swift index 4dc409d4802..549f8e605a9 100644 --- a/submodules/PremiumUI/Sources/PremiumStarComponent.swift +++ b/submodules/PremiumUI/Sources/PremiumStarComponent.swift @@ -8,7 +8,7 @@ import GZip import AppBundle import LegacyComponents -private let sceneVersion: Int = 6 +private let sceneVersion: Int = 7 private func deg2rad(_ number: Float) -> Float { return number * .pi / 180 @@ -45,7 +45,31 @@ private func generateDiffuseTexture() -> UIImage { })! } -class PremiumStarComponent: Component { +func loadCompressedScene(name: String, version: Int) -> SCNScene? { + let resourceUrl: URL + if let url = getAppBundle().url(forResource: name, withExtension: "scn") { + resourceUrl = url + } else { + let fileName = "\(name)_\(version).scn" + let tmpUrl = URL(fileURLWithPath: NSTemporaryDirectory() + fileName) + if !FileManager.default.fileExists(atPath: tmpUrl.path) { + guard let url = getAppBundle().url(forResource: name, withExtension: ""), + let compressedData = try? Data(contentsOf: url), + let decompressedData = TGGUnzipData(compressedData, 8 * 1024 * 1024) else { + return nil + } + try? decompressedData.write(to: tmpUrl) + } + resourceUrl = tmpUrl + } + + guard let scene = try? SCNScene(url: resourceUrl, options: nil) else { + return nil + } + return scene +} + +final class PremiumStarComponent: Component { let isIntro: Bool let isVisible: Bool let hasIdleAnimations: Bool @@ -251,24 +275,7 @@ class PremiumStarComponent: Component { } private func setup() { - let resourceUrl: URL - if let url = getAppBundle().url(forResource: "star", withExtension: "scn") { - resourceUrl = url - } else { - let fileName = "star_\(sceneVersion).scn" - let tmpUrl = URL(fileURLWithPath: NSTemporaryDirectory() + fileName) - if !FileManager.default.fileExists(atPath: tmpUrl.path) { - guard let url = getAppBundle().url(forResource: "star", withExtension: ""), - let compressedData = try? Data(contentsOf: url), - let decompressedData = TGGUnzipData(compressedData, 8 * 1024 * 1024) else { - return - } - try? decompressedData.write(to: tmpUrl) - } - resourceUrl = tmpUrl - } - - guard let scene = try? SCNScene(url: resourceUrl, options: nil) else { + guard let scene = loadCompressedScene(name: "star", version: sceneVersion) else { return } diff --git a/submodules/PremiumUI/Sources/ReplaceBoostScreen.swift b/submodules/PremiumUI/Sources/ReplaceBoostScreen.swift index 973e4d3fb49..3f596f9ca95 100644 --- a/submodules/PremiumUI/Sources/ReplaceBoostScreen.swift +++ b/submodules/PremiumUI/Sources/ReplaceBoostScreen.swift @@ -477,6 +477,7 @@ public class ReplaceBoostScreen: ViewController { statusBarHeight: 0.0, navigationHeight: navigationHeight, safeInsets: UIEdgeInsets(top: layout.intrinsicInsets.top + layout.safeInsets.top, left: layout.safeInsets.left, bottom: layout.intrinsicInsets.bottom + layout.safeInsets.bottom, right: layout.safeInsets.right), + additionalInsets: layout.additionalInsets, inputHeight: layout.inputHeight ?? 0.0, metrics: layout.metrics, deviceMetrics: layout.deviceMetrics, diff --git a/submodules/PremiumUI/Sources/SwirlStarsView.swift b/submodules/PremiumUI/Sources/SwirlStarsView.swift index d7ba345bf7a..8c3ce911b09 100644 --- a/submodules/PremiumUI/Sources/SwirlStarsView.swift +++ b/submodules/PremiumUI/Sources/SwirlStarsView.swift @@ -5,6 +5,8 @@ import Display import AppBundle import SwiftSignalKit +private let sceneVersion: Int = 1 + final class SwirlStarsView: UIView, PhoneDemoDecorationView { private let sceneView: SCNView @@ -13,8 +15,8 @@ final class SwirlStarsView: UIView, PhoneDemoDecorationView { override init(frame: CGRect) { self.sceneView = SCNView(frame: CGRect(origin: .zero, size: frame.size)) self.sceneView.backgroundColor = .clear - if let url = getAppBundle().url(forResource: "swirl", withExtension: "scn") { - self.sceneView.scene = try? SCNScene(url: url, options: nil) + if let scene = loadCompressedScene(name: "swirl", version: sceneVersion) { + self.sceneView.scene = scene } self.sceneView.isUserInteractionEnabled = false self.sceneView.preferredFramesPerSecond = 60 diff --git a/submodules/SearchPeerMembers/Sources/SearchPeerMembers.swift b/submodules/SearchPeerMembers/Sources/SearchPeerMembers.swift index 8cef0161a84..5581cdb9d37 100644 --- a/submodules/SearchPeerMembers/Sources/SearchPeerMembers.swift +++ b/submodules/SearchPeerMembers/Sources/SearchPeerMembers.swift @@ -81,7 +81,7 @@ public func searchPeerMembers(context: AccountContext, peerId: EnginePeer.Id, ch return ActionDisposable { disposable.dispose() } - case .feed: + case .customChatContents: subscriber.putNext(([], true)) return ActionDisposable { diff --git a/submodules/SettingsUI/Sources/ChangePhoneNumberController.swift b/submodules/SettingsUI/Sources/ChangePhoneNumberController.swift index d2ad53873f0..17a5ce1a2a2 100644 --- a/submodules/SettingsUI/Sources/ChangePhoneNumberController.swift +++ b/submodules/SettingsUI/Sources/ChangePhoneNumberController.swift @@ -162,8 +162,26 @@ public func ChangePhoneNumberController(context: AccountContext) -> ViewControll } Queue.mainQueue().justDispatch { - controller.updateData(countryCode: AuthorizationSequenceController.defaultCountryCode(), countryName: nil, number: "") - controller.updateCountryCode() + let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId)) + |> deliverOnMainQueue).start(next: { accountPeer in + guard let accountPeer, case let .user(user) = accountPeer else { + return + } + + let initialCountryCode: Int32 + if let phone = user.phone { + if let (_, countryCode) = lookupCountryIdByNumber(phone, configuration: context.currentCountriesConfiguration.with { $0 }), let codeValue = Int32(countryCode.code) { + initialCountryCode = codeValue + } else { + initialCountryCode = AuthorizationSequenceController.defaultCountryCode() + } + } else { + initialCountryCode = AuthorizationSequenceController.defaultCountryCode() + } + controller.updateData(countryCode: initialCountryCode, countryName: nil, number: "") + controller.updateCountryCode() + }) + } return controller diff --git a/submodules/SettingsUI/Sources/ChangePhoneNumberControllerNode.swift b/submodules/SettingsUI/Sources/ChangePhoneNumberControllerNode.swift index 856b8fa1c47..e7152020851 100644 --- a/submodules/SettingsUI/Sources/ChangePhoneNumberControllerNode.swift +++ b/submodules/SettingsUI/Sources/ChangePhoneNumberControllerNode.swift @@ -3,7 +3,6 @@ import UIKit import AsyncDisplayKit import Display import TelegramCore -import CoreTelephony import TelegramPresentationData import PhoneInputNode import CountrySelectionUI @@ -211,18 +210,9 @@ final class ChangePhoneNumberControllerNode: ASDisplayNode { } } - var countryId: String? = nil - let networkInfo = CTTelephonyNetworkInfo() - if let carrier = networkInfo.serviceSubscriberCellularProviders?.values.first { - countryId = carrier.isoCountryCode - } - - if countryId == nil { - countryId = (Locale.current as NSLocale).object(forKey: .countryCode) as? String - } - + let countryId = (Locale.current as NSLocale).object(forKey: .countryCode) as? String + var countryCodeAndId: (Int32, String) = (1, "US") - if let countryId = countryId { let normalizedId = countryId.uppercased() for (code, idAndName) in countryCodeToIdAndName { diff --git a/submodules/SettingsUI/Sources/DeleteAccountPhoneItem.swift b/submodules/SettingsUI/Sources/DeleteAccountPhoneItem.swift index a64fdfdd586..0e0fe77e80e 100644 --- a/submodules/SettingsUI/Sources/DeleteAccountPhoneItem.swift +++ b/submodules/SettingsUI/Sources/DeleteAccountPhoneItem.swift @@ -10,7 +10,6 @@ import ItemListUI import PresentationDataUtils import PhoneInputNode import CountrySelectionUI -import CoreTelephony private func generateCountryButtonBackground(color: UIColor, strokeColor: UIColor) -> UIImage? { return generateImage(CGSize(width: 56, height: 44.0 + 6.0), rotatedContext: { size, context in @@ -234,18 +233,9 @@ class DeleteAccountPhoneItemNode: ListViewItemNode, ItemListItemNode { } } - var countryId: String? = nil - let networkInfo = CTTelephonyNetworkInfo() - if let carrier = networkInfo.serviceSubscriberCellularProviders?.values.first { - countryId = carrier.isoCountryCode - } - - if countryId == nil { - countryId = (Locale.current as NSLocale).object(forKey: .countryCode) as? String - } - + let countryId = (Locale.current as NSLocale).object(forKey: .countryCode) as? String + var countryCodeAndId: (Int32, String) = (1, "US") - if let countryId = countryId { let normalizedId = countryId.uppercased() for (code, idAndName) in countryCodeToIdAndName { diff --git a/submodules/SettingsUI/Sources/Search/SettingsSearchableItems.swift b/submodules/SettingsUI/Sources/Search/SettingsSearchableItems.swift index f6dc8b4ecef..1d0507719d0 100644 --- a/submodules/SettingsUI/Sources/Search/SettingsSearchableItems.swift +++ b/submodules/SettingsUI/Sources/Search/SettingsSearchableItems.swift @@ -279,13 +279,13 @@ private func premiumSearchableItems(context: AccountContext) -> [SettingsSearcha var result: [SettingsSearchableItem] = [] result.append(SettingsSearchableItem(id: .premium(0), title: strings.Settings_Premium, alternate: synonyms(strings.SettingsSearch_Synonyms_Premium), icon: icon, breadcrumbs: [], present: { context, _, present in - present(.push, PremiumIntroScreen(context: context, modal: false, source: .settings)) + present(.push, PremiumIntroScreen(context: context, source: .settings, modal: false)) })) let presentDemo: (PremiumDemoScreen.Subject, (SettingsSearchableItemPresentation, ViewController?) -> Void) -> Void = { subject, present in var replaceImpl: ((ViewController) -> Void)? let controller = PremiumDemoScreen(context: context, subject: subject, action: { - let controller = PremiumIntroScreen(context: context, modal: false, source: .settings) + let controller = PremiumIntroScreen(context: context, source: .settings, modal: false) replaceImpl?(controller) }) replaceImpl = { [weak controller] c in diff --git a/submodules/SettingsUI/Sources/Text Size/TextSizeSelectionController.swift b/submodules/SettingsUI/Sources/Text Size/TextSizeSelectionController.swift index 17af5284bfa..9bf1d44c491 100644 --- a/submodules/SettingsUI/Sources/Text Size/TextSizeSelectionController.swift +++ b/submodules/SettingsUI/Sources/Text Size/TextSizeSelectionController.swift @@ -227,6 +227,7 @@ private final class TextSizeSelectionControllerNode: ASDisplayNode, UIScrollView }, openChatFolderUpdates: {}, hideChatFolderUpdates: { }, openStories: { _, _ in }, dismissNotice: { _ in + }, editPeer: { _ in }) let chatListPresentationData = ChatListPresentationData(theme: self.presentationData.theme, fontSize: self.presentationData.listsFontSize, strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat, nameSortOrder: self.presentationData.nameSortOrder, nameDisplayOrder: self.presentationData.nameDisplayOrder, disableAnimations: true) diff --git a/submodules/SettingsUI/Sources/Themes/ThemePreviewControllerNode.swift b/submodules/SettingsUI/Sources/Themes/ThemePreviewControllerNode.swift index d096a99ee1d..f9201ecc1c4 100644 --- a/submodules/SettingsUI/Sources/Themes/ThemePreviewControllerNode.swift +++ b/submodules/SettingsUI/Sources/Themes/ThemePreviewControllerNode.swift @@ -376,6 +376,7 @@ final class ThemePreviewControllerNode: ASDisplayNode, UIScrollViewDelegate { }, openChatFolderUpdates: {}, hideChatFolderUpdates: { }, openStories: { _, _ in }, dismissNotice: { _ in + }, editPeer: { _ in }) func makeChatListItem( diff --git a/submodules/ShareController/BUILD b/submodules/ShareController/BUILD index cbb874ee479..f94e1882f2d 100644 --- a/submodules/ShareController/BUILD +++ b/submodules/ShareController/BUILD @@ -44,7 +44,6 @@ swift_library( "//submodules/TelegramUI/Components/AnimationCache", "//submodules/TelegramUI/Components/MultiAnimationRenderer", "//submodules/UndoUI", - "//submodules/PremiumUI", ], visibility = [ "//visibility:public", diff --git a/submodules/ShareController/Sources/ShareController.swift b/submodules/ShareController/Sources/ShareController.swift index 6206a025a45..21d03d474b3 100644 --- a/submodules/ShareController/Sources/ShareController.swift +++ b/submodules/ShareController/Sources/ShareController.swift @@ -23,7 +23,6 @@ import TelegramIntents import AnimationCache import MultiAnimationRenderer import ObjectiveC -import PremiumUI import UndoUI private var ObjCKey_DeinitWatcher: Int? @@ -1273,7 +1272,8 @@ public final class ShareController: ViewController { } if case .undo = action { self.controllerNode.cancel?() - let premiumController = PremiumIntroScreen(context: context.context, source: .settings) + + let premiumController = context.context.sharedContext.makePremiumIntroController(context: context.context, source: .settings, forceDark: false, dismissed: nil) parentNavigationController.pushViewController(premiumController) } return false diff --git a/submodules/StatisticsUI/Sources/ChannelStatsController.swift b/submodules/StatisticsUI/Sources/ChannelStatsController.swift index 1e28116b269..e9ecb15ee60 100644 --- a/submodules/StatisticsUI/Sources/ChannelStatsController.swift +++ b/submodules/StatisticsUI/Sources/ChannelStatsController.swift @@ -1311,9 +1311,13 @@ public func channelStatsController(context: AccountContext, updatedPresentationD var headerItem: BoostHeaderItem? var leftNavigationButton: ItemListNavigationButton? var boostsOnly = false - if isGroup, section == .boosts { + if section == .boosts { title = .text("") - headerItem = BoostHeaderItem(context: context, theme: presentationData.theme, strings: presentationData.strings, status: boostData, title: presentationData.strings.GroupBoost_Title, text: presentationData.strings.GroupBoost_Info, openBoost: { + + let headerTitle = isGroup ? presentationData.strings.GroupBoost_Title : presentationData.strings.ChannelBoost_Title + let headerText = isGroup ? presentationData.strings.GroupBoost_Info : presentationData.strings.ChannelBoost_Info + + headerItem = BoostHeaderItem(context: context, theme: presentationData.theme, strings: presentationData.strings, status: boostData, title: headerTitle, text: headerText, openBoost: { openBoostImpl?(false) }, createGiveaway: { arguments.openGifts() diff --git a/submodules/StickerPeekUI/BUILD b/submodules/StickerPeekUI/BUILD index 2772c1830cd..ec677548331 100644 --- a/submodules/StickerPeekUI/BUILD +++ b/submodules/StickerPeekUI/BUILD @@ -28,7 +28,6 @@ swift_library( "//submodules/ShimmerEffect:ShimmerEffect", "//submodules/ContextUI:ContextUI", "//submodules/SolidRoundedButtonNode:SolidRoundedButtonNode", - "//submodules/PremiumUI:PremiumUI", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramApi/Sources/Api0.swift b/submodules/TelegramApi/Sources/Api0.swift index 4188aaf0c25..1a147326a55 100644 --- a/submodules/TelegramApi/Sources/Api0.swift +++ b/submodules/TelegramApi/Sources/Api0.swift @@ -12,6 +12,7 @@ public enum Api { public enum phone {} public enum photos {} public enum premium {} + public enum smsjobs {} public enum stats {} public enum stickers {} public enum storage {} @@ -34,6 +35,7 @@ public enum Api { public enum phone {} public enum photos {} public enum premium {} + public enum smsjobs {} public enum stats {} public enum stickers {} public enum stories {} @@ -78,6 +80,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[706514033] = { return Api.Boost.parse_boost($0) } dict[-1778593322] = { return Api.BotApp.parse_botApp($0) } dict[1571189943] = { return Api.BotApp.parse_botAppNotModified($0) } + dict[-1989921868] = { return Api.BotBusinessConnection.parse_botBusinessConnection($0) } dict[-1032140601] = { return Api.BotCommand.parse_botCommand($0) } dict[-1180016534] = { return Api.BotCommandScope.parse_botCommandScopeChatAdmins($0) } dict[1877059713] = { return Api.BotCommandScope.parse_botCommandScopeChats($0) } @@ -99,6 +102,15 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[-944407322] = { return Api.BotMenuButton.parse_botMenuButton($0) } dict[1113113093] = { return Api.BotMenuButton.parse_botMenuButtonCommands($0) } dict[1966318984] = { return Api.BotMenuButton.parse_botMenuButtonDefault($0) } + dict[-283809188] = { return Api.BusinessAwayMessage.parse_businessAwayMessage($0) } + dict[-910564679] = { return Api.BusinessAwayMessageSchedule.parse_businessAwayMessageScheduleAlways($0) } + dict[-867328308] = { return Api.BusinessAwayMessageSchedule.parse_businessAwayMessageScheduleCustom($0) } + dict[-1007487743] = { return Api.BusinessAwayMessageSchedule.parse_businessAwayMessageScheduleOutsideWorkHours($0) } + dict[-451302485] = { return Api.BusinessGreetingMessage.parse_businessGreetingMessage($0) } + dict[-1403249929] = { return Api.BusinessLocation.parse_businessLocation($0) } + dict[554733559] = { return Api.BusinessRecipients.parse_businessRecipients($0) } + dict[302717625] = { return Api.BusinessWeeklyOpen.parse_businessWeeklyOpen($0) } + dict[-1936543592] = { return Api.BusinessWorkHours.parse_businessWorkHours($0) } dict[1462101002] = { return Api.CdnConfig.parse_cdnConfig($0) } dict[-914167110] = { return Api.CdnPublicKey.parse_cdnPublicKey($0) } dict[531458253] = { return Api.ChannelAdminLogEvent.parse_channelAdminLogEvent($0) } @@ -196,6 +208,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[1713193015] = { return Api.ChatReactions.parse_chatReactionsSome($0) } dict[-1390068360] = { return Api.CodeSettings.parse_codeSettings($0) } dict[-870702050] = { return Api.Config.parse_config($0) } + dict[-404121113] = { return Api.ConnectedBot.parse_connectedBot($0) } dict[341499403] = { return Api.Contact.parse_contact($0) } dict[383348795] = { return Api.ContactStatus.parse_contactStatus($0) } dict[2104790276] = { return Api.DataJSON.parse_dataJSON($0) } @@ -203,8 +216,8 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[1135897376] = { return Api.DefaultHistoryTTL.parse_defaultHistoryTTL($0) } dict[-712374074] = { return Api.Dialog.parse_dialog($0) } dict[1908216652] = { return Api.Dialog.parse_dialogFolder($0) } - dict[1949890536] = { return Api.DialogFilter.parse_dialogFilter($0) } - dict[-699792216] = { return Api.DialogFilter.parse_dialogFilterChatlist($0) } + dict[1605718587] = { return Api.DialogFilter.parse_dialogFilter($0) } + dict[-1612542300] = { return Api.DialogFilter.parse_dialogFilterChatlist($0) } dict[909284270] = { return Api.DialogFilter.parse_dialogFilterDefault($0) } dict[2004110666] = { return Api.DialogFilterSuggested.parse_dialogFilterSuggested($0) } dict[-445792507] = { return Api.DialogPeer.parse_dialogPeer($0) } @@ -295,6 +308,9 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[-459324] = { return Api.InputBotInlineResult.parse_inputBotInlineResultDocument($0) } dict[1336154098] = { return Api.InputBotInlineResult.parse_inputBotInlineResultGame($0) } dict[-1462213465] = { return Api.InputBotInlineResult.parse_inputBotInlineResultPhoto($0) } + dict[-2094959136] = { return Api.InputBusinessAwayMessage.parse_inputBusinessAwayMessage($0) } + dict[26528571] = { return Api.InputBusinessGreetingMessage.parse_inputBusinessGreetingMessage($0) } + dict[1871393450] = { return Api.InputBusinessRecipients.parse_inputBusinessRecipients($0) } dict[-212145112] = { return Api.InputChannel.parse_inputChannel($0) } dict[-292807034] = { return Api.InputChannel.parse_inputChannelEmpty($0) } dict[1536380829] = { return Api.InputChannel.parse_inputChannelFromMessage($0) } @@ -396,6 +412,8 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[-380694650] = { return Api.InputPrivacyRule.parse_inputPrivacyValueDisallowChatParticipants($0) } dict[195371015] = { return Api.InputPrivacyRule.parse_inputPrivacyValueDisallowContacts($0) } dict[-1877932953] = { return Api.InputPrivacyRule.parse_inputPrivacyValueDisallowUsers($0) } + dict[609840449] = { return Api.InputQuickReplyShortcut.parse_inputQuickReplyShortcut($0) } + dict[18418929] = { return Api.InputQuickReplyShortcut.parse_inputQuickReplyShortcutId($0) } dict[583071445] = { return Api.InputReplyTo.parse_inputReplyToMessage($0) } dict[1484862010] = { return Api.InputReplyTo.parse_inputReplyToStory($0) } dict[1399317950] = { return Api.InputSecureFile.parse_inputSecureFile($0) } @@ -473,7 +491,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[340088945] = { return Api.MediaArea.parse_mediaAreaSuggestedReaction($0) } dict[-1098720356] = { return Api.MediaArea.parse_mediaAreaVenue($0) } dict[64088654] = { return Api.MediaAreaCoordinates.parse_mediaAreaCoordinates($0) } - dict[508332649] = { return Api.Message.parse_message($0) } + dict[-1502839044] = { return Api.Message.parse_message($0) } dict[-1868117372] = { return Api.Message.parse_messageEmpty($0) } dict[721967202] = { return Api.Message.parse_messageService($0) } dict[-872240531] = { return Api.MessageAction.parse_messageActionBoostApply($0) } @@ -705,6 +723,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[-463335103] = { return Api.PrivacyRule.parse_privacyValueDisallowUsers($0) } dict[32685898] = { return Api.PublicForward.parse_publicForwardMessage($0) } dict[-302797360] = { return Api.PublicForward.parse_publicForwardStory($0) } + dict[110563371] = { return Api.QuickReply.parse_quickReply($0) } dict[-1992950669] = { return Api.Reaction.parse_reactionCustomEmoji($0) } dict[455247544] = { return Api.Reaction.parse_reactionEmoji($0) } dict[2046153753] = { return Api.Reaction.parse_reactionEmpty($0) } @@ -812,6 +831,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[-651419003] = { return Api.SendMessageAction.parse_speakingInGroupCallAction($0) } dict[-1239335713] = { return Api.ShippingOption.parse_shippingOption($0) } dict[-2010155333] = { return Api.SimpleWebViewResult.parse_simpleWebViewResultUrl($0) } + dict[-425595208] = { return Api.SmsJob.parse_smsJob($0) } dict[-313293833] = { return Api.SponsoredMessage.parse_sponsoredMessage($0) } dict[1035529315] = { return Api.SponsoredWebPage.parse_sponsoredWebPage($0) } dict[-884757282] = { return Api.StatsAbsValueAndPrev.parse_statsAbsValueAndPrev($0) } @@ -846,6 +866,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[1964978502] = { return Api.TextWithEntities.parse_textWithEntities($0) } dict[-1609668650] = { return Api.Theme.parse_theme($0) } dict[-94849324] = { return Api.ThemeSettings.parse_themeSettings($0) } + dict[-7173643] = { return Api.Timezone.parse_timezone($0) } dict[-305282981] = { return Api.TopPeer.parse_topPeer($0) } dict[344356834] = { return Api.TopPeerCategory.parse_topPeerCategoryBotsInline($0) } dict[-1419371685] = { return Api.TopPeerCategory.parse_topPeerCategoryBotsPM($0) } @@ -858,15 +879,19 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[-75283823] = { return Api.TopPeerCategoryPeers.parse_topPeerCategoryPeers($0) } dict[397910539] = { return Api.Update.parse_updateAttachMenuBots($0) } dict[-335171433] = { return Api.Update.parse_updateAutoSaveSettings($0) } + dict[-1964652166] = { return Api.Update.parse_updateBotBusinessConnect($0) } dict[-1177566067] = { return Api.Update.parse_updateBotCallbackQuery($0) } dict[-1873947492] = { return Api.Update.parse_updateBotChatBoost($0) } dict[299870598] = { return Api.Update.parse_updateBotChatInviteRequester($0) } dict[1299263278] = { return Api.Update.parse_updateBotCommands($0) } + dict[-1590796039] = { return Api.Update.parse_updateBotDeleteBusinessMessage($0) } + dict[1420915171] = { return Api.Update.parse_updateBotEditBusinessMessage($0) } dict[1232025500] = { return Api.Update.parse_updateBotInlineQuery($0) } dict[317794823] = { return Api.Update.parse_updateBotInlineSend($0) } dict[347625491] = { return Api.Update.parse_updateBotMenuButton($0) } dict[-1407069234] = { return Api.Update.parse_updateBotMessageReaction($0) } dict[164329305] = { return Api.Update.parse_updateBotMessageReactions($0) } + dict[-2142069794] = { return Api.Update.parse_updateBotNewBusinessMessage($0) } dict[-1934976362] = { return Api.Update.parse_updateBotPrecheckoutQuery($0) } dict[-1246823043] = { return Api.Update.parse_updateBotShippingQuery($0) } dict[-997782967] = { return Api.Update.parse_updateBotStopped($0) } @@ -897,6 +922,8 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[-1906403213] = { return Api.Update.parse_updateDcOptions($0) } dict[-1020437742] = { return Api.Update.parse_updateDeleteChannelMessages($0) } dict[-1576161051] = { return Api.Update.parse_updateDeleteMessages($0) } + dict[1407644140] = { return Api.Update.parse_updateDeleteQuickReply($0) } + dict[1450174413] = { return Api.Update.parse_updateDeleteQuickReplyMessages($0) } dict[-1870238482] = { return Api.Update.parse_updateDeleteScheduledMessages($0) } dict[654302845] = { return Api.Update.parse_updateDialogFilter($0) } dict[-1512627963] = { return Api.Update.parse_updateDialogFilterOrder($0) } @@ -930,6 +957,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[1656358105] = { return Api.Update.parse_updateNewChannelMessage($0) } dict[314359194] = { return Api.Update.parse_updateNewEncryptedMessage($0) } dict[522914557] = { return Api.Update.parse_updateNewMessage($0) } + dict[-180508905] = { return Api.Update.parse_updateNewQuickReply($0) } dict[967122427] = { return Api.Update.parse_updateNewScheduledMessage($0) } dict[1753886890] = { return Api.Update.parse_updateNewStickerSet($0) } dict[-1094555409] = { return Api.Update.parse_updateNotifySettings($0) } @@ -947,6 +975,8 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[1751942566] = { return Api.Update.parse_updatePinnedSavedDialogs($0) } dict[-298113238] = { return Api.Update.parse_updatePrivacy($0) } dict[861169551] = { return Api.Update.parse_updatePtsChanged($0) } + dict[-112784718] = { return Api.Update.parse_updateQuickReplies($0) } + dict[1040518415] = { return Api.Update.parse_updateQuickReplyMessage($0) } dict[-693004986] = { return Api.Update.parse_updateReadChannelDiscussionInbox($0) } dict[1767677564] = { return Api.Update.parse_updateReadChannelDiscussionOutbox($0) } dict[-1842450928] = { return Api.Update.parse_updateReadChannelInbox($0) } @@ -966,6 +996,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[1960361625] = { return Api.Update.parse_updateSavedRingtones($0) } dict[2103604867] = { return Api.Update.parse_updateSentStoryReaction($0) } dict[-337352679] = { return Api.Update.parse_updateServiceNotification($0) } + dict[-245208620] = { return Api.Update.parse_updateSmsJob($0) } dict[834816008] = { return Api.Update.parse_updateStickerSets($0) } dict[196268545] = { return Api.Update.parse_updateStickerSetsOrder($0) } dict[738741697] = { return Api.Update.parse_updateStoriesStealthMode($0) } @@ -993,7 +1024,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[-1831650802] = { return Api.UrlAuthResult.parse_urlAuthResultRequest($0) } dict[559694904] = { return Api.User.parse_user($0) } dict[-742634630] = { return Api.User.parse_userEmpty($0) } - dict[-1179571092] = { return Api.UserFull.parse_userFull($0) } + dict[587153029] = { return Api.UserFull.parse_userFull($0) } dict[-2100168954] = { return Api.UserProfilePhoto.parse_userProfilePhoto($0) } dict[1326562017] = { return Api.UserProfilePhoto.parse_userProfilePhotoEmpty($0) } dict[164646985] = { return Api.UserStatus.parse_userStatusEmpty($0) } @@ -1024,6 +1055,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[1275039392] = { return Api.account.Authorizations.parse_authorizations($0) } dict[1674235686] = { return Api.account.AutoDownloadSettings.parse_autoDownloadSettings($0) } dict[1279133341] = { return Api.account.AutoSaveSettings.parse_autoSaveSettings($0) } + dict[400029819] = { return Api.account.ConnectedBots.parse_connectedBots($0) } dict[1474462241] = { return Api.account.ContentSettings.parse_contentSettings($0) } dict[731303195] = { return Api.account.EmailVerified.parse_emailVerified($0) } dict[-507835039] = { return Api.account.EmailVerified.parse_emailVerifiedLogin($0) } @@ -1120,6 +1152,8 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[2013922064] = { return Api.help.TermsOfService.parse_termsOfService($0) } dict[686618977] = { return Api.help.TermsOfServiceUpdate.parse_termsOfServiceUpdate($0) } dict[-483352705] = { return Api.help.TermsOfServiceUpdate.parse_termsOfServiceUpdateEmpty($0) } + dict[2071260529] = { return Api.help.TimezonesList.parse_timezonesList($0) } + dict[-1761146676] = { return Api.help.TimezonesList.parse_timezonesListNotModified($0) } dict[32192344] = { return Api.help.UserInfo.parse_userInfo($0) } dict[-206688531] = { return Api.help.UserInfo.parse_userInfoEmpty($0) } dict[-275956116] = { return Api.messages.AffectedFoundMessages.parse_affectedFoundMessages($0) } @@ -1141,6 +1175,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[-1571952873] = { return Api.messages.CheckedHistoryImportPeer.parse_checkedHistoryImportPeer($0) } dict[740433629] = { return Api.messages.DhConfig.parse_dhConfig($0) } dict[-1058912715] = { return Api.messages.DhConfig.parse_dhConfigNotModified($0) } + dict[718878489] = { return Api.messages.DialogFilters.parse_dialogFilters($0) } dict[364538944] = { return Api.messages.Dialogs.parse_dialogs($0) } dict[-253500010] = { return Api.messages.Dialogs.parse_dialogsNotModified($0) } dict[1910543603] = { return Api.messages.Dialogs.parse_dialogsSlice($0) } @@ -1170,6 +1205,8 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[978610270] = { return Api.messages.Messages.parse_messagesSlice($0) } dict[863093588] = { return Api.messages.PeerDialogs.parse_peerDialogs($0) } dict[1753266509] = { return Api.messages.PeerSettings.parse_peerSettings($0) } + dict[-963811691] = { return Api.messages.QuickReplies.parse_quickReplies($0) } + dict[1603398491] = { return Api.messages.QuickReplies.parse_quickRepliesNotModified($0) } dict[-352454890] = { return Api.messages.Reactions.parse_reactions($0) } dict[-1334846497] = { return Api.messages.Reactions.parse_reactionsNotModified($0) } dict[-1999405994] = { return Api.messages.RecentStickers.parse_recentStickers($0) } @@ -1222,6 +1259,8 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[-2030542532] = { return Api.premium.BoostsList.parse_boostsList($0) } dict[1230586490] = { return Api.premium.BoostsStatus.parse_boostsStatus($0) } dict[-1696454430] = { return Api.premium.MyBoosts.parse_myBoosts($0) } + dict[-594852657] = { return Api.smsjobs.EligibilityToJoin.parse_eligibleToJoin($0) } + dict[720277905] = { return Api.smsjobs.Status.parse_status($0) } dict[963421692] = { return Api.stats.BroadcastStats.parse_broadcastStats($0) } dict[-276825834] = { return Api.stats.MegagroupStats.parse_megagroupStats($0) } dict[2145983508] = { return Api.stats.MessageStats.parse_messageStats($0) } @@ -1354,6 +1393,8 @@ public extension Api { _1.serialize(buffer, boxed) case let _1 as Api.BotApp: _1.serialize(buffer, boxed) + case let _1 as Api.BotBusinessConnection: + _1.serialize(buffer, boxed) case let _1 as Api.BotCommand: _1.serialize(buffer, boxed) case let _1 as Api.BotCommandScope: @@ -1366,6 +1407,20 @@ public extension Api { _1.serialize(buffer, boxed) case let _1 as Api.BotMenuButton: _1.serialize(buffer, boxed) + case let _1 as Api.BusinessAwayMessage: + _1.serialize(buffer, boxed) + case let _1 as Api.BusinessAwayMessageSchedule: + _1.serialize(buffer, boxed) + case let _1 as Api.BusinessGreetingMessage: + _1.serialize(buffer, boxed) + case let _1 as Api.BusinessLocation: + _1.serialize(buffer, boxed) + case let _1 as Api.BusinessRecipients: + _1.serialize(buffer, boxed) + case let _1 as Api.BusinessWeeklyOpen: + _1.serialize(buffer, boxed) + case let _1 as Api.BusinessWorkHours: + _1.serialize(buffer, boxed) case let _1 as Api.CdnConfig: _1.serialize(buffer, boxed) case let _1 as Api.CdnPublicKey: @@ -1412,6 +1467,8 @@ public extension Api { _1.serialize(buffer, boxed) case let _1 as Api.Config: _1.serialize(buffer, boxed) + case let _1 as Api.ConnectedBot: + _1.serialize(buffer, boxed) case let _1 as Api.Contact: _1.serialize(buffer, boxed) case let _1 as Api.ContactStatus: @@ -1514,6 +1571,12 @@ public extension Api { _1.serialize(buffer, boxed) case let _1 as Api.InputBotInlineResult: _1.serialize(buffer, boxed) + case let _1 as Api.InputBusinessAwayMessage: + _1.serialize(buffer, boxed) + case let _1 as Api.InputBusinessGreetingMessage: + _1.serialize(buffer, boxed) + case let _1 as Api.InputBusinessRecipients: + _1.serialize(buffer, boxed) case let _1 as Api.InputChannel: _1.serialize(buffer, boxed) case let _1 as Api.InputChatPhoto: @@ -1568,6 +1631,8 @@ public extension Api { _1.serialize(buffer, boxed) case let _1 as Api.InputPrivacyRule: _1.serialize(buffer, boxed) + case let _1 as Api.InputQuickReplyShortcut: + _1.serialize(buffer, boxed) case let _1 as Api.InputReplyTo: _1.serialize(buffer, boxed) case let _1 as Api.InputSecureFile: @@ -1738,6 +1803,8 @@ public extension Api { _1.serialize(buffer, boxed) case let _1 as Api.PublicForward: _1.serialize(buffer, boxed) + case let _1 as Api.QuickReply: + _1.serialize(buffer, boxed) case let _1 as Api.Reaction: _1.serialize(buffer, boxed) case let _1 as Api.ReactionCount: @@ -1798,6 +1865,8 @@ public extension Api { _1.serialize(buffer, boxed) case let _1 as Api.SimpleWebViewResult: _1.serialize(buffer, boxed) + case let _1 as Api.SmsJob: + _1.serialize(buffer, boxed) case let _1 as Api.SponsoredMessage: _1.serialize(buffer, boxed) case let _1 as Api.SponsoredWebPage: @@ -1844,6 +1913,8 @@ public extension Api { _1.serialize(buffer, boxed) case let _1 as Api.ThemeSettings: _1.serialize(buffer, boxed) + case let _1 as Api.Timezone: + _1.serialize(buffer, boxed) case let _1 as Api.TopPeer: _1.serialize(buffer, boxed) case let _1 as Api.TopPeerCategory: @@ -1892,6 +1963,8 @@ public extension Api { _1.serialize(buffer, boxed) case let _1 as Api.account.AutoSaveSettings: _1.serialize(buffer, boxed) + case let _1 as Api.account.ConnectedBots: + _1.serialize(buffer, boxed) case let _1 as Api.account.ContentSettings: _1.serialize(buffer, boxed) case let _1 as Api.account.EmailVerified: @@ -2006,6 +2079,8 @@ public extension Api { _1.serialize(buffer, boxed) case let _1 as Api.help.TermsOfServiceUpdate: _1.serialize(buffer, boxed) + case let _1 as Api.help.TimezonesList: + _1.serialize(buffer, boxed) case let _1 as Api.help.UserInfo: _1.serialize(buffer, boxed) case let _1 as Api.messages.AffectedFoundMessages: @@ -2038,6 +2113,8 @@ public extension Api { _1.serialize(buffer, boxed) case let _1 as Api.messages.DhConfig: _1.serialize(buffer, boxed) + case let _1 as Api.messages.DialogFilters: + _1.serialize(buffer, boxed) case let _1 as Api.messages.Dialogs: _1.serialize(buffer, boxed) case let _1 as Api.messages.DiscussionMessage: @@ -2076,6 +2153,8 @@ public extension Api { _1.serialize(buffer, boxed) case let _1 as Api.messages.PeerSettings: _1.serialize(buffer, boxed) + case let _1 as Api.messages.QuickReplies: + _1.serialize(buffer, boxed) case let _1 as Api.messages.Reactions: _1.serialize(buffer, boxed) case let _1 as Api.messages.RecentStickers: @@ -2152,6 +2231,10 @@ public extension Api { _1.serialize(buffer, boxed) case let _1 as Api.premium.MyBoosts: _1.serialize(buffer, boxed) + case let _1 as Api.smsjobs.EligibilityToJoin: + _1.serialize(buffer, boxed) + case let _1 as Api.smsjobs.Status: + _1.serialize(buffer, boxed) case let _1 as Api.stats.BroadcastStats: _1.serialize(buffer, boxed) case let _1 as Api.stats.MegagroupStats: diff --git a/submodules/TelegramApi/Sources/Api1.swift b/submodules/TelegramApi/Sources/Api1.swift index f223f6a5e62..106159d9de9 100644 --- a/submodules/TelegramApi/Sources/Api1.swift +++ b/submodules/TelegramApi/Sources/Api1.swift @@ -1040,6 +1040,58 @@ public extension Api { } } +public extension Api { + enum BotBusinessConnection: TypeConstructorDescription { + case botBusinessConnection(flags: Int32, connectionId: String, userId: Int64, dcId: Int32, date: Int32) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .botBusinessConnection(let flags, let connectionId, let userId, let dcId, let date): + if boxed { + buffer.appendInt32(-1989921868) + } + serializeInt32(flags, buffer: buffer, boxed: false) + serializeString(connectionId, buffer: buffer, boxed: false) + serializeInt64(userId, buffer: buffer, boxed: false) + serializeInt32(dcId, buffer: buffer, boxed: false) + serializeInt32(date, buffer: buffer, boxed: false) + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .botBusinessConnection(let flags, let connectionId, let userId, let dcId, let date): + return ("botBusinessConnection", [("flags", flags as Any), ("connectionId", connectionId as Any), ("userId", userId as Any), ("dcId", dcId as Any), ("date", date as Any)]) + } + } + + public static func parse_botBusinessConnection(_ reader: BufferReader) -> BotBusinessConnection? { + var _1: Int32? + _1 = reader.readInt32() + var _2: String? + _2 = parseString(reader) + var _3: Int64? + _3 = reader.readInt64() + var _4: Int32? + _4 = reader.readInt32() + var _5: Int32? + _5 = reader.readInt32() + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = _4 != nil + let _c5 = _5 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 { + return Api.BotBusinessConnection.botBusinessConnection(flags: _1!, connectionId: _2!, userId: _3!, dcId: _4!, date: _5!) + } + else { + return nil + } + } + + } +} public extension Api { enum BotCommand: TypeConstructorDescription { case botCommand(command: String, description: String) diff --git a/submodules/TelegramApi/Sources/Api10.swift b/submodules/TelegramApi/Sources/Api10.swift index 7249151a229..4fe4c26f44a 100644 --- a/submodules/TelegramApi/Sources/Api10.swift +++ b/submodules/TelegramApi/Sources/Api10.swift @@ -1,3 +1,59 @@ +public extension Api { + enum InputQuickReplyShortcut: TypeConstructorDescription { + case inputQuickReplyShortcut(shortcut: String) + case inputQuickReplyShortcutId(shortcutId: Int32) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .inputQuickReplyShortcut(let shortcut): + if boxed { + buffer.appendInt32(609840449) + } + serializeString(shortcut, buffer: buffer, boxed: false) + break + case .inputQuickReplyShortcutId(let shortcutId): + if boxed { + buffer.appendInt32(18418929) + } + serializeInt32(shortcutId, buffer: buffer, boxed: false) + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .inputQuickReplyShortcut(let shortcut): + return ("inputQuickReplyShortcut", [("shortcut", shortcut as Any)]) + case .inputQuickReplyShortcutId(let shortcutId): + return ("inputQuickReplyShortcutId", [("shortcutId", shortcutId as Any)]) + } + } + + public static func parse_inputQuickReplyShortcut(_ reader: BufferReader) -> InputQuickReplyShortcut? { + var _1: String? + _1 = parseString(reader) + let _c1 = _1 != nil + if _c1 { + return Api.InputQuickReplyShortcut.inputQuickReplyShortcut(shortcut: _1!) + } + else { + return nil + } + } + public static func parse_inputQuickReplyShortcutId(_ reader: BufferReader) -> InputQuickReplyShortcut? { + var _1: Int32? + _1 = reader.readInt32() + let _c1 = _1 != nil + if _c1 { + return Api.InputQuickReplyShortcut.inputQuickReplyShortcutId(shortcutId: _1!) + } + else { + return nil + } + } + + } +} public extension Api { indirect enum InputReplyTo: TypeConstructorDescription { case inputReplyToMessage(flags: Int32, replyToMsgId: Int32, topMsgId: Int32?, replyToPeerId: Api.InputPeer?, quoteText: String?, quoteEntities: [Api.MessageEntity]?, quoteOffset: Int32?) @@ -1014,83 +1070,3 @@ public extension Api { } } -public extension Api { - enum InputWallPaper: TypeConstructorDescription { - case inputWallPaper(id: Int64, accessHash: Int64) - case inputWallPaperNoFile(id: Int64) - case inputWallPaperSlug(slug: String) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .inputWallPaper(let id, let accessHash): - if boxed { - buffer.appendInt32(-433014407) - } - serializeInt64(id, buffer: buffer, boxed: false) - serializeInt64(accessHash, buffer: buffer, boxed: false) - break - case .inputWallPaperNoFile(let id): - if boxed { - buffer.appendInt32(-1770371538) - } - serializeInt64(id, buffer: buffer, boxed: false) - break - case .inputWallPaperSlug(let slug): - if boxed { - buffer.appendInt32(1913199744) - } - serializeString(slug, buffer: buffer, boxed: false) - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .inputWallPaper(let id, let accessHash): - return ("inputWallPaper", [("id", id as Any), ("accessHash", accessHash as Any)]) - case .inputWallPaperNoFile(let id): - return ("inputWallPaperNoFile", [("id", id as Any)]) - case .inputWallPaperSlug(let slug): - return ("inputWallPaperSlug", [("slug", slug as Any)]) - } - } - - public static func parse_inputWallPaper(_ reader: BufferReader) -> InputWallPaper? { - var _1: Int64? - _1 = reader.readInt64() - var _2: Int64? - _2 = reader.readInt64() - let _c1 = _1 != nil - let _c2 = _2 != nil - if _c1 && _c2 { - return Api.InputWallPaper.inputWallPaper(id: _1!, accessHash: _2!) - } - else { - return nil - } - } - public static func parse_inputWallPaperNoFile(_ reader: BufferReader) -> InputWallPaper? { - var _1: Int64? - _1 = reader.readInt64() - let _c1 = _1 != nil - if _c1 { - return Api.InputWallPaper.inputWallPaperNoFile(id: _1!) - } - else { - return nil - } - } - public static func parse_inputWallPaperSlug(_ reader: BufferReader) -> InputWallPaper? { - var _1: String? - _1 = parseString(reader) - let _c1 = _1 != nil - if _c1 { - return Api.InputWallPaper.inputWallPaperSlug(slug: _1!) - } - else { - return nil - } - } - - } -} diff --git a/submodules/TelegramApi/Sources/Api11.swift b/submodules/TelegramApi/Sources/Api11.swift index 666ea7ff21c..89037ef849d 100644 --- a/submodules/TelegramApi/Sources/Api11.swift +++ b/submodules/TelegramApi/Sources/Api11.swift @@ -1,3 +1,83 @@ +public extension Api { + enum InputWallPaper: TypeConstructorDescription { + case inputWallPaper(id: Int64, accessHash: Int64) + case inputWallPaperNoFile(id: Int64) + case inputWallPaperSlug(slug: String) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .inputWallPaper(let id, let accessHash): + if boxed { + buffer.appendInt32(-433014407) + } + serializeInt64(id, buffer: buffer, boxed: false) + serializeInt64(accessHash, buffer: buffer, boxed: false) + break + case .inputWallPaperNoFile(let id): + if boxed { + buffer.appendInt32(-1770371538) + } + serializeInt64(id, buffer: buffer, boxed: false) + break + case .inputWallPaperSlug(let slug): + if boxed { + buffer.appendInt32(1913199744) + } + serializeString(slug, buffer: buffer, boxed: false) + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .inputWallPaper(let id, let accessHash): + return ("inputWallPaper", [("id", id as Any), ("accessHash", accessHash as Any)]) + case .inputWallPaperNoFile(let id): + return ("inputWallPaperNoFile", [("id", id as Any)]) + case .inputWallPaperSlug(let slug): + return ("inputWallPaperSlug", [("slug", slug as Any)]) + } + } + + public static func parse_inputWallPaper(_ reader: BufferReader) -> InputWallPaper? { + var _1: Int64? + _1 = reader.readInt64() + var _2: Int64? + _2 = reader.readInt64() + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return Api.InputWallPaper.inputWallPaper(id: _1!, accessHash: _2!) + } + else { + return nil + } + } + public static func parse_inputWallPaperNoFile(_ reader: BufferReader) -> InputWallPaper? { + var _1: Int64? + _1 = reader.readInt64() + let _c1 = _1 != nil + if _c1 { + return Api.InputWallPaper.inputWallPaperNoFile(id: _1!) + } + else { + return nil + } + } + public static func parse_inputWallPaperSlug(_ reader: BufferReader) -> InputWallPaper? { + var _1: String? + _1 = parseString(reader) + let _c1 = _1 != nil + if _c1 { + return Api.InputWallPaper.inputWallPaperSlug(slug: _1!) + } + else { + return nil + } + } + + } +} public extension Api { enum InputWebDocument: TypeConstructorDescription { case inputWebDocument(url: String, size: Int32, mimeType: String, attributes: [Api.DocumentAttribute]) @@ -904,165 +984,3 @@ public extension Api { } } -public extension Api { - enum LabeledPrice: TypeConstructorDescription { - case labeledPrice(label: String, amount: Int64) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .labeledPrice(let label, let amount): - if boxed { - buffer.appendInt32(-886477832) - } - serializeString(label, buffer: buffer, boxed: false) - serializeInt64(amount, buffer: buffer, boxed: false) - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .labeledPrice(let label, let amount): - return ("labeledPrice", [("label", label as Any), ("amount", amount as Any)]) - } - } - - public static func parse_labeledPrice(_ reader: BufferReader) -> LabeledPrice? { - var _1: String? - _1 = parseString(reader) - var _2: Int64? - _2 = reader.readInt64() - let _c1 = _1 != nil - let _c2 = _2 != nil - if _c1 && _c2 { - return Api.LabeledPrice.labeledPrice(label: _1!, amount: _2!) - } - else { - return nil - } - } - - } -} -public extension Api { - enum LangPackDifference: TypeConstructorDescription { - case langPackDifference(langCode: String, fromVersion: Int32, version: Int32, strings: [Api.LangPackString]) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .langPackDifference(let langCode, let fromVersion, let version, let strings): - if boxed { - buffer.appendInt32(-209337866) - } - serializeString(langCode, buffer: buffer, boxed: false) - serializeInt32(fromVersion, buffer: buffer, boxed: false) - serializeInt32(version, buffer: buffer, boxed: false) - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(strings.count)) - for item in strings { - item.serialize(buffer, true) - } - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .langPackDifference(let langCode, let fromVersion, let version, let strings): - return ("langPackDifference", [("langCode", langCode as Any), ("fromVersion", fromVersion as Any), ("version", version as Any), ("strings", strings as Any)]) - } - } - - public static func parse_langPackDifference(_ reader: BufferReader) -> LangPackDifference? { - var _1: String? - _1 = parseString(reader) - var _2: Int32? - _2 = reader.readInt32() - var _3: Int32? - _3 = reader.readInt32() - var _4: [Api.LangPackString]? - if let _ = reader.readInt32() { - _4 = Api.parseVector(reader, elementSignature: 0, elementType: Api.LangPackString.self) - } - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = _3 != nil - let _c4 = _4 != nil - if _c1 && _c2 && _c3 && _c4 { - return Api.LangPackDifference.langPackDifference(langCode: _1!, fromVersion: _2!, version: _3!, strings: _4!) - } - else { - return nil - } - } - - } -} -public extension Api { - enum LangPackLanguage: TypeConstructorDescription { - case langPackLanguage(flags: Int32, name: String, nativeName: String, langCode: String, baseLangCode: String?, pluralCode: String, stringsCount: Int32, translatedCount: Int32, translationsUrl: String) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .langPackLanguage(let flags, let name, let nativeName, let langCode, let baseLangCode, let pluralCode, let stringsCount, let translatedCount, let translationsUrl): - if boxed { - buffer.appendInt32(-288727837) - } - serializeInt32(flags, buffer: buffer, boxed: false) - serializeString(name, buffer: buffer, boxed: false) - serializeString(nativeName, buffer: buffer, boxed: false) - serializeString(langCode, buffer: buffer, boxed: false) - if Int(flags) & Int(1 << 1) != 0 {serializeString(baseLangCode!, buffer: buffer, boxed: false)} - serializeString(pluralCode, buffer: buffer, boxed: false) - serializeInt32(stringsCount, buffer: buffer, boxed: false) - serializeInt32(translatedCount, buffer: buffer, boxed: false) - serializeString(translationsUrl, buffer: buffer, boxed: false) - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .langPackLanguage(let flags, let name, let nativeName, let langCode, let baseLangCode, let pluralCode, let stringsCount, let translatedCount, let translationsUrl): - return ("langPackLanguage", [("flags", flags as Any), ("name", name as Any), ("nativeName", nativeName as Any), ("langCode", langCode as Any), ("baseLangCode", baseLangCode as Any), ("pluralCode", pluralCode as Any), ("stringsCount", stringsCount as Any), ("translatedCount", translatedCount as Any), ("translationsUrl", translationsUrl as Any)]) - } - } - - public static func parse_langPackLanguage(_ reader: BufferReader) -> LangPackLanguage? { - var _1: Int32? - _1 = reader.readInt32() - var _2: String? - _2 = parseString(reader) - var _3: String? - _3 = parseString(reader) - var _4: String? - _4 = parseString(reader) - var _5: String? - if Int(_1!) & Int(1 << 1) != 0 {_5 = parseString(reader) } - var _6: String? - _6 = parseString(reader) - var _7: Int32? - _7 = reader.readInt32() - var _8: Int32? - _8 = reader.readInt32() - var _9: String? - _9 = parseString(reader) - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = _3 != nil - let _c4 = _4 != nil - let _c5 = (Int(_1!) & Int(1 << 1) == 0) || _5 != nil - let _c6 = _6 != nil - let _c7 = _7 != nil - let _c8 = _8 != nil - let _c9 = _9 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 { - return Api.LangPackLanguage.langPackLanguage(flags: _1!, name: _2!, nativeName: _3!, langCode: _4!, baseLangCode: _5, pluralCode: _6!, stringsCount: _7!, translatedCount: _8!, translationsUrl: _9!) - } - else { - return nil - } - } - - } -} diff --git a/submodules/TelegramApi/Sources/Api12.swift b/submodules/TelegramApi/Sources/Api12.swift index f63228f098c..1edeff5668b 100644 --- a/submodules/TelegramApi/Sources/Api12.swift +++ b/submodules/TelegramApi/Sources/Api12.swift @@ -1,3 +1,165 @@ +public extension Api { + enum LabeledPrice: TypeConstructorDescription { + case labeledPrice(label: String, amount: Int64) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .labeledPrice(let label, let amount): + if boxed { + buffer.appendInt32(-886477832) + } + serializeString(label, buffer: buffer, boxed: false) + serializeInt64(amount, buffer: buffer, boxed: false) + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .labeledPrice(let label, let amount): + return ("labeledPrice", [("label", label as Any), ("amount", amount as Any)]) + } + } + + public static func parse_labeledPrice(_ reader: BufferReader) -> LabeledPrice? { + var _1: String? + _1 = parseString(reader) + var _2: Int64? + _2 = reader.readInt64() + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return Api.LabeledPrice.labeledPrice(label: _1!, amount: _2!) + } + else { + return nil + } + } + + } +} +public extension Api { + enum LangPackDifference: TypeConstructorDescription { + case langPackDifference(langCode: String, fromVersion: Int32, version: Int32, strings: [Api.LangPackString]) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .langPackDifference(let langCode, let fromVersion, let version, let strings): + if boxed { + buffer.appendInt32(-209337866) + } + serializeString(langCode, buffer: buffer, boxed: false) + serializeInt32(fromVersion, buffer: buffer, boxed: false) + serializeInt32(version, buffer: buffer, boxed: false) + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(strings.count)) + for item in strings { + item.serialize(buffer, true) + } + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .langPackDifference(let langCode, let fromVersion, let version, let strings): + return ("langPackDifference", [("langCode", langCode as Any), ("fromVersion", fromVersion as Any), ("version", version as Any), ("strings", strings as Any)]) + } + } + + public static func parse_langPackDifference(_ reader: BufferReader) -> LangPackDifference? { + var _1: String? + _1 = parseString(reader) + var _2: Int32? + _2 = reader.readInt32() + var _3: Int32? + _3 = reader.readInt32() + var _4: [Api.LangPackString]? + if let _ = reader.readInt32() { + _4 = Api.parseVector(reader, elementSignature: 0, elementType: Api.LangPackString.self) + } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = _4 != nil + if _c1 && _c2 && _c3 && _c4 { + return Api.LangPackDifference.langPackDifference(langCode: _1!, fromVersion: _2!, version: _3!, strings: _4!) + } + else { + return nil + } + } + + } +} +public extension Api { + enum LangPackLanguage: TypeConstructorDescription { + case langPackLanguage(flags: Int32, name: String, nativeName: String, langCode: String, baseLangCode: String?, pluralCode: String, stringsCount: Int32, translatedCount: Int32, translationsUrl: String) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .langPackLanguage(let flags, let name, let nativeName, let langCode, let baseLangCode, let pluralCode, let stringsCount, let translatedCount, let translationsUrl): + if boxed { + buffer.appendInt32(-288727837) + } + serializeInt32(flags, buffer: buffer, boxed: false) + serializeString(name, buffer: buffer, boxed: false) + serializeString(nativeName, buffer: buffer, boxed: false) + serializeString(langCode, buffer: buffer, boxed: false) + if Int(flags) & Int(1 << 1) != 0 {serializeString(baseLangCode!, buffer: buffer, boxed: false)} + serializeString(pluralCode, buffer: buffer, boxed: false) + serializeInt32(stringsCount, buffer: buffer, boxed: false) + serializeInt32(translatedCount, buffer: buffer, boxed: false) + serializeString(translationsUrl, buffer: buffer, boxed: false) + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .langPackLanguage(let flags, let name, let nativeName, let langCode, let baseLangCode, let pluralCode, let stringsCount, let translatedCount, let translationsUrl): + return ("langPackLanguage", [("flags", flags as Any), ("name", name as Any), ("nativeName", nativeName as Any), ("langCode", langCode as Any), ("baseLangCode", baseLangCode as Any), ("pluralCode", pluralCode as Any), ("stringsCount", stringsCount as Any), ("translatedCount", translatedCount as Any), ("translationsUrl", translationsUrl as Any)]) + } + } + + public static func parse_langPackLanguage(_ reader: BufferReader) -> LangPackLanguage? { + var _1: Int32? + _1 = reader.readInt32() + var _2: String? + _2 = parseString(reader) + var _3: String? + _3 = parseString(reader) + var _4: String? + _4 = parseString(reader) + var _5: String? + if Int(_1!) & Int(1 << 1) != 0 {_5 = parseString(reader) } + var _6: String? + _6 = parseString(reader) + var _7: Int32? + _7 = reader.readInt32() + var _8: Int32? + _8 = reader.readInt32() + var _9: String? + _9 = parseString(reader) + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = _4 != nil + let _c5 = (Int(_1!) & Int(1 << 1) == 0) || _5 != nil + let _c6 = _6 != nil + let _c7 = _7 != nil + let _c8 = _8 != nil + let _c9 = _9 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 { + return Api.LangPackLanguage.langPackLanguage(flags: _1!, name: _2!, nativeName: _3!, langCode: _4!, baseLangCode: _5, pluralCode: _6!, stringsCount: _7!, translatedCount: _8!, translationsUrl: _9!) + } + else { + return nil + } + } + + } +} public extension Api { enum LangPackString: TypeConstructorDescription { case langPackString(key: String, value: String) @@ -424,15 +586,15 @@ public extension Api { } public extension Api { indirect enum Message: TypeConstructorDescription { - case message(flags: Int32, id: Int32, fromId: Api.Peer?, fromBoostsApplied: Int32?, peerId: Api.Peer, savedPeerId: Api.Peer?, fwdFrom: Api.MessageFwdHeader?, viaBotId: Int64?, replyTo: Api.MessageReplyHeader?, date: Int32, message: String, media: Api.MessageMedia?, replyMarkup: Api.ReplyMarkup?, entities: [Api.MessageEntity]?, views: Int32?, forwards: Int32?, replies: Api.MessageReplies?, editDate: Int32?, postAuthor: String?, groupedId: Int64?, reactions: Api.MessageReactions?, restrictionReason: [Api.RestrictionReason]?, ttlPeriod: Int32?) + case message(flags: Int32, id: Int32, fromId: Api.Peer?, fromBoostsApplied: Int32?, peerId: Api.Peer, savedPeerId: Api.Peer?, fwdFrom: Api.MessageFwdHeader?, viaBotId: Int64?, replyTo: Api.MessageReplyHeader?, date: Int32, message: String, media: Api.MessageMedia?, replyMarkup: Api.ReplyMarkup?, entities: [Api.MessageEntity]?, views: Int32?, forwards: Int32?, replies: Api.MessageReplies?, editDate: Int32?, postAuthor: String?, groupedId: Int64?, reactions: Api.MessageReactions?, restrictionReason: [Api.RestrictionReason]?, ttlPeriod: Int32?, quickReplyShortcutId: Int32?) case messageEmpty(flags: Int32, id: Int32, peerId: Api.Peer?) case messageService(flags: Int32, id: Int32, fromId: Api.Peer?, peerId: Api.Peer, replyTo: Api.MessageReplyHeader?, date: Int32, action: Api.MessageAction, ttlPeriod: Int32?) public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { switch self { - case .message(let flags, let id, let fromId, let fromBoostsApplied, let peerId, let savedPeerId, let fwdFrom, let viaBotId, let replyTo, let date, let message, let media, let replyMarkup, let entities, let views, let forwards, let replies, let editDate, let postAuthor, let groupedId, let reactions, let restrictionReason, let ttlPeriod): + case .message(let flags, let id, let fromId, let fromBoostsApplied, let peerId, let savedPeerId, let fwdFrom, let viaBotId, let replyTo, let date, let message, let media, let replyMarkup, let entities, let views, let forwards, let replies, let editDate, let postAuthor, let groupedId, let reactions, let restrictionReason, let ttlPeriod, let quickReplyShortcutId): if boxed { - buffer.appendInt32(508332649) + buffer.appendInt32(-1502839044) } serializeInt32(flags, buffer: buffer, boxed: false) serializeInt32(id, buffer: buffer, boxed: false) @@ -465,6 +627,7 @@ public extension Api { item.serialize(buffer, true) }} if Int(flags) & Int(1 << 25) != 0 {serializeInt32(ttlPeriod!, buffer: buffer, boxed: false)} + if Int(flags) & Int(1 << 30) != 0 {serializeInt32(quickReplyShortcutId!, buffer: buffer, boxed: false)} break case .messageEmpty(let flags, let id, let peerId): if boxed { @@ -492,8 +655,8 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { - case .message(let flags, let id, let fromId, let fromBoostsApplied, let peerId, let savedPeerId, let fwdFrom, let viaBotId, let replyTo, let date, let message, let media, let replyMarkup, let entities, let views, let forwards, let replies, let editDate, let postAuthor, let groupedId, let reactions, let restrictionReason, let ttlPeriod): - return ("message", [("flags", flags as Any), ("id", id as Any), ("fromId", fromId as Any), ("fromBoostsApplied", fromBoostsApplied as Any), ("peerId", peerId as Any), ("savedPeerId", savedPeerId as Any), ("fwdFrom", fwdFrom as Any), ("viaBotId", viaBotId as Any), ("replyTo", replyTo as Any), ("date", date as Any), ("message", message as Any), ("media", media as Any), ("replyMarkup", replyMarkup as Any), ("entities", entities as Any), ("views", views as Any), ("forwards", forwards as Any), ("replies", replies as Any), ("editDate", editDate as Any), ("postAuthor", postAuthor as Any), ("groupedId", groupedId as Any), ("reactions", reactions as Any), ("restrictionReason", restrictionReason as Any), ("ttlPeriod", ttlPeriod as Any)]) + case .message(let flags, let id, let fromId, let fromBoostsApplied, let peerId, let savedPeerId, let fwdFrom, let viaBotId, let replyTo, let date, let message, let media, let replyMarkup, let entities, let views, let forwards, let replies, let editDate, let postAuthor, let groupedId, let reactions, let restrictionReason, let ttlPeriod, let quickReplyShortcutId): + return ("message", [("flags", flags as Any), ("id", id as Any), ("fromId", fromId as Any), ("fromBoostsApplied", fromBoostsApplied as Any), ("peerId", peerId as Any), ("savedPeerId", savedPeerId as Any), ("fwdFrom", fwdFrom as Any), ("viaBotId", viaBotId as Any), ("replyTo", replyTo as Any), ("date", date as Any), ("message", message as Any), ("media", media as Any), ("replyMarkup", replyMarkup as Any), ("entities", entities as Any), ("views", views as Any), ("forwards", forwards as Any), ("replies", replies as Any), ("editDate", editDate as Any), ("postAuthor", postAuthor as Any), ("groupedId", groupedId as Any), ("reactions", reactions as Any), ("restrictionReason", restrictionReason as Any), ("ttlPeriod", ttlPeriod as Any), ("quickReplyShortcutId", quickReplyShortcutId as Any)]) case .messageEmpty(let flags, let id, let peerId): return ("messageEmpty", [("flags", flags as Any), ("id", id as Any), ("peerId", peerId as Any)]) case .messageService(let flags, let id, let fromId, let peerId, let replyTo, let date, let action, let ttlPeriod): @@ -570,6 +733,8 @@ public extension Api { } } var _23: Int32? if Int(_1!) & Int(1 << 25) != 0 {_23 = reader.readInt32() } + var _24: Int32? + if Int(_1!) & Int(1 << 30) != 0 {_24 = reader.readInt32() } let _c1 = _1 != nil let _c2 = _2 != nil let _c3 = (Int(_1!) & Int(1 << 8) == 0) || _3 != nil @@ -593,8 +758,9 @@ public extension Api { let _c21 = (Int(_1!) & Int(1 << 20) == 0) || _21 != nil let _c22 = (Int(_1!) & Int(1 << 22) == 0) || _22 != nil let _c23 = (Int(_1!) & Int(1 << 25) == 0) || _23 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 && _c11 && _c12 && _c13 && _c14 && _c15 && _c16 && _c17 && _c18 && _c19 && _c20 && _c21 && _c22 && _c23 { - return Api.Message.message(flags: _1!, id: _2!, fromId: _3, fromBoostsApplied: _4, peerId: _5!, savedPeerId: _6, fwdFrom: _7, viaBotId: _8, replyTo: _9, date: _10!, message: _11!, media: _12, replyMarkup: _13, entities: _14, views: _15, forwards: _16, replies: _17, editDate: _18, postAuthor: _19, groupedId: _20, reactions: _21, restrictionReason: _22, ttlPeriod: _23) + let _c24 = (Int(_1!) & Int(1 << 30) == 0) || _24 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 && _c11 && _c12 && _c13 && _c14 && _c15 && _c16 && _c17 && _c18 && _c19 && _c20 && _c21 && _c22 && _c23 && _c24 { + return Api.Message.message(flags: _1!, id: _2!, fromId: _3, fromBoostsApplied: _4, peerId: _5!, savedPeerId: _6, fwdFrom: _7, viaBotId: _8, replyTo: _9, date: _10!, message: _11!, media: _12, replyMarkup: _13, entities: _14, views: _15, forwards: _16, replies: _17, editDate: _18, postAuthor: _19, groupedId: _20, reactions: _21, restrictionReason: _22, ttlPeriod: _23, quickReplyShortcutId: _24) } else { return nil diff --git a/submodules/TelegramApi/Sources/Api18.swift b/submodules/TelegramApi/Sources/Api18.swift index cce8c834d79..144d26302ee 100644 --- a/submodules/TelegramApi/Sources/Api18.swift +++ b/submodules/TelegramApi/Sources/Api18.swift @@ -244,6 +244,54 @@ public extension Api { } } +public extension Api { + enum QuickReply: TypeConstructorDescription { + case quickReply(shortcutId: Int32, shortcut: String, topMessage: Int32, count: Int32) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .quickReply(let shortcutId, let shortcut, let topMessage, let count): + if boxed { + buffer.appendInt32(110563371) + } + serializeInt32(shortcutId, buffer: buffer, boxed: false) + serializeString(shortcut, buffer: buffer, boxed: false) + serializeInt32(topMessage, buffer: buffer, boxed: false) + serializeInt32(count, buffer: buffer, boxed: false) + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .quickReply(let shortcutId, let shortcut, let topMessage, let count): + return ("quickReply", [("shortcutId", shortcutId as Any), ("shortcut", shortcut as Any), ("topMessage", topMessage as Any), ("count", count as Any)]) + } + } + + public static func parse_quickReply(_ reader: BufferReader) -> QuickReply? { + var _1: Int32? + _1 = reader.readInt32() + var _2: String? + _2 = parseString(reader) + var _3: Int32? + _3 = reader.readInt32() + var _4: Int32? + _4 = reader.readInt32() + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = _4 != nil + if _c1 && _c2 && _c3 && _c4 { + return Api.QuickReply.quickReply(shortcutId: _1!, shortcut: _2!, topMessage: _3!, count: _4!) + } + else { + return nil + } + } + + } +} public extension Api { enum Reaction: TypeConstructorDescription { case reactionCustomEmoji(documentId: Int64) diff --git a/submodules/TelegramApi/Sources/Api2.swift b/submodules/TelegramApi/Sources/Api2.swift index 5f696bd03ae..9436ea52397 100644 --- a/submodules/TelegramApi/Sources/Api2.swift +++ b/submodules/TelegramApi/Sources/Api2.swift @@ -588,6 +588,350 @@ public extension Api { } } +public extension Api { + enum BusinessAwayMessage: TypeConstructorDescription { + case businessAwayMessage(flags: Int32, shortcutId: Int32, schedule: Api.BusinessAwayMessageSchedule, recipients: Api.BusinessRecipients) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .businessAwayMessage(let flags, let shortcutId, let schedule, let recipients): + if boxed { + buffer.appendInt32(-283809188) + } + serializeInt32(flags, buffer: buffer, boxed: false) + serializeInt32(shortcutId, buffer: buffer, boxed: false) + schedule.serialize(buffer, true) + recipients.serialize(buffer, true) + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .businessAwayMessage(let flags, let shortcutId, let schedule, let recipients): + return ("businessAwayMessage", [("flags", flags as Any), ("shortcutId", shortcutId as Any), ("schedule", schedule as Any), ("recipients", recipients as Any)]) + } + } + + public static func parse_businessAwayMessage(_ reader: BufferReader) -> BusinessAwayMessage? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int32? + _2 = reader.readInt32() + var _3: Api.BusinessAwayMessageSchedule? + if let signature = reader.readInt32() { + _3 = Api.parse(reader, signature: signature) as? Api.BusinessAwayMessageSchedule + } + var _4: Api.BusinessRecipients? + if let signature = reader.readInt32() { + _4 = Api.parse(reader, signature: signature) as? Api.BusinessRecipients + } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = _4 != nil + if _c1 && _c2 && _c3 && _c4 { + return Api.BusinessAwayMessage.businessAwayMessage(flags: _1!, shortcutId: _2!, schedule: _3!, recipients: _4!) + } + else { + return nil + } + } + + } +} +public extension Api { + enum BusinessAwayMessageSchedule: TypeConstructorDescription { + case businessAwayMessageScheduleAlways + case businessAwayMessageScheduleCustom(startDate: Int32, endDate: Int32) + case businessAwayMessageScheduleOutsideWorkHours + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .businessAwayMessageScheduleAlways: + if boxed { + buffer.appendInt32(-910564679) + } + + break + case .businessAwayMessageScheduleCustom(let startDate, let endDate): + if boxed { + buffer.appendInt32(-867328308) + } + serializeInt32(startDate, buffer: buffer, boxed: false) + serializeInt32(endDate, buffer: buffer, boxed: false) + break + case .businessAwayMessageScheduleOutsideWorkHours: + if boxed { + buffer.appendInt32(-1007487743) + } + + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .businessAwayMessageScheduleAlways: + return ("businessAwayMessageScheduleAlways", []) + case .businessAwayMessageScheduleCustom(let startDate, let endDate): + return ("businessAwayMessageScheduleCustom", [("startDate", startDate as Any), ("endDate", endDate as Any)]) + case .businessAwayMessageScheduleOutsideWorkHours: + return ("businessAwayMessageScheduleOutsideWorkHours", []) + } + } + + public static func parse_businessAwayMessageScheduleAlways(_ reader: BufferReader) -> BusinessAwayMessageSchedule? { + return Api.BusinessAwayMessageSchedule.businessAwayMessageScheduleAlways + } + public static func parse_businessAwayMessageScheduleCustom(_ reader: BufferReader) -> BusinessAwayMessageSchedule? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int32? + _2 = reader.readInt32() + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return Api.BusinessAwayMessageSchedule.businessAwayMessageScheduleCustom(startDate: _1!, endDate: _2!) + } + else { + return nil + } + } + public static func parse_businessAwayMessageScheduleOutsideWorkHours(_ reader: BufferReader) -> BusinessAwayMessageSchedule? { + return Api.BusinessAwayMessageSchedule.businessAwayMessageScheduleOutsideWorkHours + } + + } +} +public extension Api { + enum BusinessGreetingMessage: TypeConstructorDescription { + case businessGreetingMessage(shortcutId: Int32, recipients: Api.BusinessRecipients, noActivityDays: Int32) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .businessGreetingMessage(let shortcutId, let recipients, let noActivityDays): + if boxed { + buffer.appendInt32(-451302485) + } + serializeInt32(shortcutId, buffer: buffer, boxed: false) + recipients.serialize(buffer, true) + serializeInt32(noActivityDays, buffer: buffer, boxed: false) + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .businessGreetingMessage(let shortcutId, let recipients, let noActivityDays): + return ("businessGreetingMessage", [("shortcutId", shortcutId as Any), ("recipients", recipients as Any), ("noActivityDays", noActivityDays as Any)]) + } + } + + public static func parse_businessGreetingMessage(_ reader: BufferReader) -> BusinessGreetingMessage? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Api.BusinessRecipients? + if let signature = reader.readInt32() { + _2 = Api.parse(reader, signature: signature) as? Api.BusinessRecipients + } + var _3: Int32? + _3 = reader.readInt32() + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + if _c1 && _c2 && _c3 { + return Api.BusinessGreetingMessage.businessGreetingMessage(shortcutId: _1!, recipients: _2!, noActivityDays: _3!) + } + else { + return nil + } + } + + } +} +public extension Api { + enum BusinessLocation: TypeConstructorDescription { + case businessLocation(flags: Int32, geoPoint: Api.GeoPoint?, address: String) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .businessLocation(let flags, let geoPoint, let address): + if boxed { + buffer.appendInt32(-1403249929) + } + serializeInt32(flags, buffer: buffer, boxed: false) + if Int(flags) & Int(1 << 0) != 0 {geoPoint!.serialize(buffer, true)} + serializeString(address, buffer: buffer, boxed: false) + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .businessLocation(let flags, let geoPoint, let address): + return ("businessLocation", [("flags", flags as Any), ("geoPoint", geoPoint as Any), ("address", address as Any)]) + } + } + + public static func parse_businessLocation(_ reader: BufferReader) -> BusinessLocation? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Api.GeoPoint? + if Int(_1!) & Int(1 << 0) != 0 {if let signature = reader.readInt32() { + _2 = Api.parse(reader, signature: signature) as? Api.GeoPoint + } } + var _3: String? + _3 = parseString(reader) + let _c1 = _1 != nil + let _c2 = (Int(_1!) & Int(1 << 0) == 0) || _2 != nil + let _c3 = _3 != nil + if _c1 && _c2 && _c3 { + return Api.BusinessLocation.businessLocation(flags: _1!, geoPoint: _2, address: _3!) + } + else { + return nil + } + } + + } +} +public extension Api { + enum BusinessRecipients: TypeConstructorDescription { + case businessRecipients(flags: Int32, users: [Int64]?) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .businessRecipients(let flags, let users): + if boxed { + buffer.appendInt32(554733559) + } + serializeInt32(flags, buffer: buffer, boxed: false) + if Int(flags) & Int(1 << 4) != 0 {buffer.appendInt32(481674261) + buffer.appendInt32(Int32(users!.count)) + for item in users! { + serializeInt64(item, buffer: buffer, boxed: false) + }} + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .businessRecipients(let flags, let users): + return ("businessRecipients", [("flags", flags as Any), ("users", users as Any)]) + } + } + + public static func parse_businessRecipients(_ reader: BufferReader) -> BusinessRecipients? { + var _1: Int32? + _1 = reader.readInt32() + var _2: [Int64]? + if Int(_1!) & Int(1 << 4) != 0 {if let _ = reader.readInt32() { + _2 = Api.parseVector(reader, elementSignature: 570911930, elementType: Int64.self) + } } + let _c1 = _1 != nil + let _c2 = (Int(_1!) & Int(1 << 4) == 0) || _2 != nil + if _c1 && _c2 { + return Api.BusinessRecipients.businessRecipients(flags: _1!, users: _2) + } + else { + return nil + } + } + + } +} +public extension Api { + enum BusinessWeeklyOpen: TypeConstructorDescription { + case businessWeeklyOpen(startMinute: Int32, endMinute: Int32) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .businessWeeklyOpen(let startMinute, let endMinute): + if boxed { + buffer.appendInt32(302717625) + } + serializeInt32(startMinute, buffer: buffer, boxed: false) + serializeInt32(endMinute, buffer: buffer, boxed: false) + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .businessWeeklyOpen(let startMinute, let endMinute): + return ("businessWeeklyOpen", [("startMinute", startMinute as Any), ("endMinute", endMinute as Any)]) + } + } + + public static func parse_businessWeeklyOpen(_ reader: BufferReader) -> BusinessWeeklyOpen? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int32? + _2 = reader.readInt32() + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return Api.BusinessWeeklyOpen.businessWeeklyOpen(startMinute: _1!, endMinute: _2!) + } + else { + return nil + } + } + + } +} +public extension Api { + enum BusinessWorkHours: TypeConstructorDescription { + case businessWorkHours(flags: Int32, timezoneId: String, weeklyOpen: [Api.BusinessWeeklyOpen]) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .businessWorkHours(let flags, let timezoneId, let weeklyOpen): + if boxed { + buffer.appendInt32(-1936543592) + } + serializeInt32(flags, buffer: buffer, boxed: false) + serializeString(timezoneId, buffer: buffer, boxed: false) + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(weeklyOpen.count)) + for item in weeklyOpen { + item.serialize(buffer, true) + } + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .businessWorkHours(let flags, let timezoneId, let weeklyOpen): + return ("businessWorkHours", [("flags", flags as Any), ("timezoneId", timezoneId as Any), ("weeklyOpen", weeklyOpen as Any)]) + } + } + + public static func parse_businessWorkHours(_ reader: BufferReader) -> BusinessWorkHours? { + var _1: Int32? + _1 = reader.readInt32() + var _2: String? + _2 = parseString(reader) + var _3: [Api.BusinessWeeklyOpen]? + if let _ = reader.readInt32() { + _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.BusinessWeeklyOpen.self) + } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + if _c1 && _c2 && _c3 { + return Api.BusinessWorkHours.businessWorkHours(flags: _1!, timezoneId: _2!, weeklyOpen: _3!) + } + else { + return nil + } + } + + } +} public extension Api { enum CdnConfig: TypeConstructorDescription { case cdnConfig(publicKeys: [Api.CdnPublicKey]) diff --git a/submodules/TelegramApi/Sources/Api21.swift b/submodules/TelegramApi/Sources/Api21.swift index 174f9c3cb72..c09e5c1ee60 100644 --- a/submodules/TelegramApi/Sources/Api21.swift +++ b/submodules/TelegramApi/Sources/Api21.swift @@ -84,6 +84,50 @@ public extension Api { } } +public extension Api { + enum SmsJob: TypeConstructorDescription { + case smsJob(jobId: String, phoneNumber: String, text: String) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .smsJob(let jobId, let phoneNumber, let text): + if boxed { + buffer.appendInt32(-425595208) + } + serializeString(jobId, buffer: buffer, boxed: false) + serializeString(phoneNumber, buffer: buffer, boxed: false) + serializeString(text, buffer: buffer, boxed: false) + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .smsJob(let jobId, let phoneNumber, let text): + return ("smsJob", [("jobId", jobId as Any), ("phoneNumber", phoneNumber as Any), ("text", text as Any)]) + } + } + + public static func parse_smsJob(_ reader: BufferReader) -> SmsJob? { + var _1: String? + _1 = parseString(reader) + var _2: String? + _2 = parseString(reader) + var _3: String? + _3 = parseString(reader) + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + if _c1 && _c2 && _c3 { + return Api.SmsJob.smsJob(jobId: _1!, phoneNumber: _2!, text: _3!) + } + else { + return nil + } + } + + } +} public extension Api { indirect enum SponsoredMessage: TypeConstructorDescription { case sponsoredMessage(flags: Int32, randomId: Buffer, fromId: Api.Peer?, chatInvite: Api.ChatInvite?, chatInviteHash: String?, channelPost: Int32?, startParam: String?, webpage: Api.SponsoredWebPage?, app: Api.BotApp?, message: String, entities: [Api.MessageEntity]?, buttonText: String?, sponsorInfo: String?, additionalInfo: String?) diff --git a/submodules/TelegramApi/Sources/Api22.swift b/submodules/TelegramApi/Sources/Api22.swift index 3d1854bcb31..e7175c51f54 100644 --- a/submodules/TelegramApi/Sources/Api22.swift +++ b/submodules/TelegramApi/Sources/Api22.swift @@ -254,6 +254,50 @@ public extension Api { } } +public extension Api { + enum Timezone: TypeConstructorDescription { + case timezone(id: String, name: String, utcOffset: Int32) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .timezone(let id, let name, let utcOffset): + if boxed { + buffer.appendInt32(-7173643) + } + serializeString(id, buffer: buffer, boxed: false) + serializeString(name, buffer: buffer, boxed: false) + serializeInt32(utcOffset, buffer: buffer, boxed: false) + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .timezone(let id, let name, let utcOffset): + return ("timezone", [("id", id as Any), ("name", name as Any), ("utcOffset", utcOffset as Any)]) + } + } + + public static func parse_timezone(_ reader: BufferReader) -> Timezone? { + var _1: String? + _1 = parseString(reader) + var _2: String? + _2 = parseString(reader) + var _3: Int32? + _3 = reader.readInt32() + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + if _c1 && _c2 && _c3 { + return Api.Timezone.timezone(id: _1!, name: _2!, utcOffset: _3!) + } + else { + return nil + } + } + + } +} public extension Api { enum TopPeer: TypeConstructorDescription { case topPeer(peer: Api.Peer, rating: Double) @@ -464,15 +508,19 @@ public extension Api { indirect enum Update: TypeConstructorDescription { case updateAttachMenuBots case updateAutoSaveSettings + case updateBotBusinessConnect(connection: Api.BotBusinessConnection, qts: Int32) case updateBotCallbackQuery(flags: Int32, queryId: Int64, userId: Int64, peer: Api.Peer, msgId: Int32, chatInstance: Int64, data: Buffer?, gameShortName: String?) case updateBotChatBoost(peer: Api.Peer, boost: Api.Boost, qts: Int32) case updateBotChatInviteRequester(peer: Api.Peer, date: Int32, userId: Int64, about: String, invite: Api.ExportedChatInvite, qts: Int32) case updateBotCommands(peer: Api.Peer, botId: Int64, commands: [Api.BotCommand]) + case updateBotDeleteBusinessMessage(connectionId: String, messages: [Int32], qts: Int32) + case updateBotEditBusinessMessage(connectionId: String, message: Api.Message, qts: Int32) case updateBotInlineQuery(flags: Int32, queryId: Int64, userId: Int64, query: String, geo: Api.GeoPoint?, peerType: Api.InlineQueryPeerType?, offset: String) case updateBotInlineSend(flags: Int32, userId: Int64, query: String, geo: Api.GeoPoint?, id: String, msgId: Api.InputBotInlineMessageID?) case updateBotMenuButton(botId: Int64, button: Api.BotMenuButton) case updateBotMessageReaction(peer: Api.Peer, msgId: Int32, date: Int32, actor: Api.Peer, oldReactions: [Api.Reaction], newReactions: [Api.Reaction], qts: Int32) case updateBotMessageReactions(peer: Api.Peer, msgId: Int32, date: Int32, reactions: [Api.ReactionCount], qts: Int32) + case updateBotNewBusinessMessage(connectionId: String, message: Api.Message, qts: Int32) case updateBotPrecheckoutQuery(flags: Int32, queryId: Int64, userId: Int64, payload: Buffer, info: Api.PaymentRequestedInfo?, shippingOptionId: String?, currency: String, totalAmount: Int64) case updateBotShippingQuery(queryId: Int64, userId: Int64, payload: Buffer, shippingAddress: Api.PostAddress) case updateBotStopped(userId: Int64, date: Int32, stopped: Api.Bool, qts: Int32) @@ -503,6 +551,8 @@ public extension Api { case updateDcOptions(dcOptions: [Api.DcOption]) case updateDeleteChannelMessages(channelId: Int64, messages: [Int32], pts: Int32, ptsCount: Int32) case updateDeleteMessages(messages: [Int32], pts: Int32, ptsCount: Int32) + case updateDeleteQuickReply(shortcutId: Int32) + case updateDeleteQuickReplyMessages(shortcutId: Int32, messages: [Int32]) case updateDeleteScheduledMessages(peer: Api.Peer, messages: [Int32]) case updateDialogFilter(flags: Int32, id: Int32, filter: Api.DialogFilter?) case updateDialogFilterOrder(order: [Int32]) @@ -536,6 +586,7 @@ public extension Api { case updateNewChannelMessage(message: Api.Message, pts: Int32, ptsCount: Int32) case updateNewEncryptedMessage(message: Api.EncryptedMessage, qts: Int32) case updateNewMessage(message: Api.Message, pts: Int32, ptsCount: Int32) + case updateNewQuickReply(quickReply: Api.QuickReply) case updateNewScheduledMessage(message: Api.Message) case updateNewStickerSet(stickerset: Api.messages.StickerSet) case updateNotifySettings(peer: Api.NotifyPeer, notifySettings: Api.PeerNotifySettings) @@ -553,6 +604,8 @@ public extension Api { case updatePinnedSavedDialogs(flags: Int32, order: [Api.DialogPeer]?) case updatePrivacy(key: Api.PrivacyKey, rules: [Api.PrivacyRule]) case updatePtsChanged + case updateQuickReplies(quickReplies: [Api.QuickReply]) + case updateQuickReplyMessage(message: Api.Message) case updateReadChannelDiscussionInbox(flags: Int32, channelId: Int64, topMsgId: Int32, readMaxId: Int32, broadcastId: Int64?, broadcastPost: Int32?) case updateReadChannelDiscussionOutbox(channelId: Int64, topMsgId: Int32, readMaxId: Int32) case updateReadChannelInbox(flags: Int32, folderId: Int32?, channelId: Int64, maxId: Int32, stillUnreadCount: Int32, pts: Int32) @@ -572,6 +625,7 @@ public extension Api { case updateSavedRingtones case updateSentStoryReaction(peer: Api.Peer, storyId: Int32, reaction: Api.Reaction) case updateServiceNotification(flags: Int32, inboxDate: Int32?, type: String, message: String, media: Api.MessageMedia, entities: [Api.MessageEntity]) + case updateSmsJob(jobId: String) case updateStickerSets(flags: Int32) case updateStickerSetsOrder(flags: Int32, order: [Int64]) case updateStoriesStealthMode(stealthMode: Api.StoriesStealthMode) @@ -601,6 +655,13 @@ public extension Api { buffer.appendInt32(-335171433) } + break + case .updateBotBusinessConnect(let connection, let qts): + if boxed { + buffer.appendInt32(-1964652166) + } + connection.serialize(buffer, true) + serializeInt32(qts, buffer: buffer, boxed: false) break case .updateBotCallbackQuery(let flags, let queryId, let userId, let peer, let msgId, let chatInstance, let data, let gameShortName): if boxed { @@ -646,6 +707,26 @@ public extension Api { item.serialize(buffer, true) } break + case .updateBotDeleteBusinessMessage(let connectionId, let messages, let qts): + if boxed { + buffer.appendInt32(-1590796039) + } + serializeString(connectionId, buffer: buffer, boxed: false) + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(messages.count)) + for item in messages { + serializeInt32(item, buffer: buffer, boxed: false) + } + serializeInt32(qts, buffer: buffer, boxed: false) + break + case .updateBotEditBusinessMessage(let connectionId, let message, let qts): + if boxed { + buffer.appendInt32(1420915171) + } + serializeString(connectionId, buffer: buffer, boxed: false) + message.serialize(buffer, true) + serializeInt32(qts, buffer: buffer, boxed: false) + break case .updateBotInlineQuery(let flags, let queryId, let userId, let query, let geo, let peerType, let offset): if boxed { buffer.appendInt32(1232025500) @@ -710,6 +791,14 @@ public extension Api { } serializeInt32(qts, buffer: buffer, boxed: false) break + case .updateBotNewBusinessMessage(let connectionId, let message, let qts): + if boxed { + buffer.appendInt32(-2142069794) + } + serializeString(connectionId, buffer: buffer, boxed: false) + message.serialize(buffer, true) + serializeInt32(qts, buffer: buffer, boxed: false) + break case .updateBotPrecheckoutQuery(let flags, let queryId, let userId, let payload, let info, let shippingOptionId, let currency, let totalAmount): if boxed { buffer.appendInt32(-1934976362) @@ -981,6 +1070,23 @@ public extension Api { serializeInt32(pts, buffer: buffer, boxed: false) serializeInt32(ptsCount, buffer: buffer, boxed: false) break + case .updateDeleteQuickReply(let shortcutId): + if boxed { + buffer.appendInt32(1407644140) + } + serializeInt32(shortcutId, buffer: buffer, boxed: false) + break + case .updateDeleteQuickReplyMessages(let shortcutId, let messages): + if boxed { + buffer.appendInt32(1450174413) + } + serializeInt32(shortcutId, buffer: buffer, boxed: false) + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(messages.count)) + for item in messages { + serializeInt32(item, buffer: buffer, boxed: false) + } + break case .updateDeleteScheduledMessages(let peer, let messages): if boxed { buffer.appendInt32(-1870238482) @@ -1251,6 +1357,12 @@ public extension Api { serializeInt32(pts, buffer: buffer, boxed: false) serializeInt32(ptsCount, buffer: buffer, boxed: false) break + case .updateNewQuickReply(let quickReply): + if boxed { + buffer.appendInt32(-180508905) + } + quickReply.serialize(buffer, true) + break case .updateNewScheduledMessage(let message): if boxed { buffer.appendInt32(967122427) @@ -1402,6 +1514,22 @@ public extension Api { buffer.appendInt32(861169551) } + break + case .updateQuickReplies(let quickReplies): + if boxed { + buffer.appendInt32(-112784718) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(quickReplies.count)) + for item in quickReplies { + item.serialize(buffer, true) + } + break + case .updateQuickReplyMessage(let message): + if boxed { + buffer.appendInt32(1040518415) + } + message.serialize(buffer, true) break case .updateReadChannelDiscussionInbox(let flags, let channelId, let topMsgId, let readMaxId, let broadcastId, let broadcastPost): if boxed { @@ -1560,6 +1688,12 @@ public extension Api { item.serialize(buffer, true) } break + case .updateSmsJob(let jobId): + if boxed { + buffer.appendInt32(-245208620) + } + serializeString(jobId, buffer: buffer, boxed: false) + break case .updateStickerSets(let flags): if boxed { buffer.appendInt32(834816008) @@ -1683,6 +1817,8 @@ public extension Api { return ("updateAttachMenuBots", []) case .updateAutoSaveSettings: return ("updateAutoSaveSettings", []) + case .updateBotBusinessConnect(let connection, let qts): + return ("updateBotBusinessConnect", [("connection", connection as Any), ("qts", qts as Any)]) case .updateBotCallbackQuery(let flags, let queryId, let userId, let peer, let msgId, let chatInstance, let data, let gameShortName): return ("updateBotCallbackQuery", [("flags", flags as Any), ("queryId", queryId as Any), ("userId", userId as Any), ("peer", peer as Any), ("msgId", msgId as Any), ("chatInstance", chatInstance as Any), ("data", data as Any), ("gameShortName", gameShortName as Any)]) case .updateBotChatBoost(let peer, let boost, let qts): @@ -1691,6 +1827,10 @@ public extension Api { return ("updateBotChatInviteRequester", [("peer", peer as Any), ("date", date as Any), ("userId", userId as Any), ("about", about as Any), ("invite", invite as Any), ("qts", qts as Any)]) case .updateBotCommands(let peer, let botId, let commands): return ("updateBotCommands", [("peer", peer as Any), ("botId", botId as Any), ("commands", commands as Any)]) + case .updateBotDeleteBusinessMessage(let connectionId, let messages, let qts): + return ("updateBotDeleteBusinessMessage", [("connectionId", connectionId as Any), ("messages", messages as Any), ("qts", qts as Any)]) + case .updateBotEditBusinessMessage(let connectionId, let message, let qts): + return ("updateBotEditBusinessMessage", [("connectionId", connectionId as Any), ("message", message as Any), ("qts", qts as Any)]) case .updateBotInlineQuery(let flags, let queryId, let userId, let query, let geo, let peerType, let offset): return ("updateBotInlineQuery", [("flags", flags as Any), ("queryId", queryId as Any), ("userId", userId as Any), ("query", query as Any), ("geo", geo as Any), ("peerType", peerType as Any), ("offset", offset as Any)]) case .updateBotInlineSend(let flags, let userId, let query, let geo, let id, let msgId): @@ -1701,6 +1841,8 @@ public extension Api { return ("updateBotMessageReaction", [("peer", peer as Any), ("msgId", msgId as Any), ("date", date as Any), ("actor", actor as Any), ("oldReactions", oldReactions as Any), ("newReactions", newReactions as Any), ("qts", qts as Any)]) case .updateBotMessageReactions(let peer, let msgId, let date, let reactions, let qts): return ("updateBotMessageReactions", [("peer", peer as Any), ("msgId", msgId as Any), ("date", date as Any), ("reactions", reactions as Any), ("qts", qts as Any)]) + case .updateBotNewBusinessMessage(let connectionId, let message, let qts): + return ("updateBotNewBusinessMessage", [("connectionId", connectionId as Any), ("message", message as Any), ("qts", qts as Any)]) case .updateBotPrecheckoutQuery(let flags, let queryId, let userId, let payload, let info, let shippingOptionId, let currency, let totalAmount): return ("updateBotPrecheckoutQuery", [("flags", flags as Any), ("queryId", queryId as Any), ("userId", userId as Any), ("payload", payload as Any), ("info", info as Any), ("shippingOptionId", shippingOptionId as Any), ("currency", currency as Any), ("totalAmount", totalAmount as Any)]) case .updateBotShippingQuery(let queryId, let userId, let payload, let shippingAddress): @@ -1761,6 +1903,10 @@ public extension Api { return ("updateDeleteChannelMessages", [("channelId", channelId as Any), ("messages", messages as Any), ("pts", pts as Any), ("ptsCount", ptsCount as Any)]) case .updateDeleteMessages(let messages, let pts, let ptsCount): return ("updateDeleteMessages", [("messages", messages as Any), ("pts", pts as Any), ("ptsCount", ptsCount as Any)]) + case .updateDeleteQuickReply(let shortcutId): + return ("updateDeleteQuickReply", [("shortcutId", shortcutId as Any)]) + case .updateDeleteQuickReplyMessages(let shortcutId, let messages): + return ("updateDeleteQuickReplyMessages", [("shortcutId", shortcutId as Any), ("messages", messages as Any)]) case .updateDeleteScheduledMessages(let peer, let messages): return ("updateDeleteScheduledMessages", [("peer", peer as Any), ("messages", messages as Any)]) case .updateDialogFilter(let flags, let id, let filter): @@ -1827,6 +1973,8 @@ public extension Api { return ("updateNewEncryptedMessage", [("message", message as Any), ("qts", qts as Any)]) case .updateNewMessage(let message, let pts, let ptsCount): return ("updateNewMessage", [("message", message as Any), ("pts", pts as Any), ("ptsCount", ptsCount as Any)]) + case .updateNewQuickReply(let quickReply): + return ("updateNewQuickReply", [("quickReply", quickReply as Any)]) case .updateNewScheduledMessage(let message): return ("updateNewScheduledMessage", [("message", message as Any)]) case .updateNewStickerSet(let stickerset): @@ -1861,6 +2009,10 @@ public extension Api { return ("updatePrivacy", [("key", key as Any), ("rules", rules as Any)]) case .updatePtsChanged: return ("updatePtsChanged", []) + case .updateQuickReplies(let quickReplies): + return ("updateQuickReplies", [("quickReplies", quickReplies as Any)]) + case .updateQuickReplyMessage(let message): + return ("updateQuickReplyMessage", [("message", message as Any)]) case .updateReadChannelDiscussionInbox(let flags, let channelId, let topMsgId, let readMaxId, let broadcastId, let broadcastPost): return ("updateReadChannelDiscussionInbox", [("flags", flags as Any), ("channelId", channelId as Any), ("topMsgId", topMsgId as Any), ("readMaxId", readMaxId as Any), ("broadcastId", broadcastId as Any), ("broadcastPost", broadcastPost as Any)]) case .updateReadChannelDiscussionOutbox(let channelId, let topMsgId, let readMaxId): @@ -1899,6 +2051,8 @@ public extension Api { return ("updateSentStoryReaction", [("peer", peer as Any), ("storyId", storyId as Any), ("reaction", reaction as Any)]) case .updateServiceNotification(let flags, let inboxDate, let type, let message, let media, let entities): return ("updateServiceNotification", [("flags", flags as Any), ("inboxDate", inboxDate as Any), ("type", type as Any), ("message", message as Any), ("media", media as Any), ("entities", entities as Any)]) + case .updateSmsJob(let jobId): + return ("updateSmsJob", [("jobId", jobId as Any)]) case .updateStickerSets(let flags): return ("updateStickerSets", [("flags", flags as Any)]) case .updateStickerSetsOrder(let flags, let order): @@ -1938,6 +2092,22 @@ public extension Api { public static func parse_updateAutoSaveSettings(_ reader: BufferReader) -> Update? { return Api.Update.updateAutoSaveSettings } + public static func parse_updateBotBusinessConnect(_ reader: BufferReader) -> Update? { + var _1: Api.BotBusinessConnection? + if let signature = reader.readInt32() { + _1 = Api.parse(reader, signature: signature) as? Api.BotBusinessConnection + } + var _2: Int32? + _2 = reader.readInt32() + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return Api.Update.updateBotBusinessConnect(connection: _1!, qts: _2!) + } + else { + return nil + } + } public static func parse_updateBotCallbackQuery(_ reader: BufferReader) -> Update? { var _1: Int32? _1 = reader.readInt32() @@ -2044,6 +2214,44 @@ public extension Api { return nil } } + public static func parse_updateBotDeleteBusinessMessage(_ reader: BufferReader) -> Update? { + var _1: String? + _1 = parseString(reader) + var _2: [Int32]? + if let _ = reader.readInt32() { + _2 = Api.parseVector(reader, elementSignature: -1471112230, elementType: Int32.self) + } + var _3: Int32? + _3 = reader.readInt32() + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + if _c1 && _c2 && _c3 { + return Api.Update.updateBotDeleteBusinessMessage(connectionId: _1!, messages: _2!, qts: _3!) + } + else { + return nil + } + } + public static func parse_updateBotEditBusinessMessage(_ reader: BufferReader) -> Update? { + var _1: String? + _1 = parseString(reader) + var _2: Api.Message? + if let signature = reader.readInt32() { + _2 = Api.parse(reader, signature: signature) as? Api.Message + } + var _3: Int32? + _3 = reader.readInt32() + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + if _c1 && _c2 && _c3 { + return Api.Update.updateBotEditBusinessMessage(connectionId: _1!, message: _2!, qts: _3!) + } + else { + return nil + } + } public static func parse_updateBotInlineQuery(_ reader: BufferReader) -> Update? { var _1: Int32? _1 = reader.readInt32() @@ -2187,6 +2395,25 @@ public extension Api { return nil } } + public static func parse_updateBotNewBusinessMessage(_ reader: BufferReader) -> Update? { + var _1: String? + _1 = parseString(reader) + var _2: Api.Message? + if let signature = reader.readInt32() { + _2 = Api.parse(reader, signature: signature) as? Api.Message + } + var _3: Int32? + _3 = reader.readInt32() + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + if _c1 && _c2 && _c3 { + return Api.Update.updateBotNewBusinessMessage(connectionId: _1!, message: _2!, qts: _3!) + } + else { + return nil + } + } public static func parse_updateBotPrecheckoutQuery(_ reader: BufferReader) -> Update? { var _1: Int32? _1 = reader.readInt32() @@ -2766,6 +2993,33 @@ public extension Api { return nil } } + public static func parse_updateDeleteQuickReply(_ reader: BufferReader) -> Update? { + var _1: Int32? + _1 = reader.readInt32() + let _c1 = _1 != nil + if _c1 { + return Api.Update.updateDeleteQuickReply(shortcutId: _1!) + } + else { + return nil + } + } + public static func parse_updateDeleteQuickReplyMessages(_ reader: BufferReader) -> Update? { + var _1: Int32? + _1 = reader.readInt32() + var _2: [Int32]? + if let _ = reader.readInt32() { + _2 = Api.parseVector(reader, elementSignature: -1471112230, elementType: Int32.self) + } + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return Api.Update.updateDeleteQuickReplyMessages(shortcutId: _1!, messages: _2!) + } + else { + return nil + } + } public static func parse_updateDeleteScheduledMessages(_ reader: BufferReader) -> Update? { var _1: Api.Peer? if let signature = reader.readInt32() { @@ -3321,6 +3575,19 @@ public extension Api { return nil } } + public static func parse_updateNewQuickReply(_ reader: BufferReader) -> Update? { + var _1: Api.QuickReply? + if let signature = reader.readInt32() { + _1 = Api.parse(reader, signature: signature) as? Api.QuickReply + } + let _c1 = _1 != nil + if _c1 { + return Api.Update.updateNewQuickReply(quickReply: _1!) + } + else { + return nil + } + } public static func parse_updateNewScheduledMessage(_ reader: BufferReader) -> Update? { var _1: Api.Message? if let signature = reader.readInt32() { @@ -3608,6 +3875,32 @@ public extension Api { public static func parse_updatePtsChanged(_ reader: BufferReader) -> Update? { return Api.Update.updatePtsChanged } + public static func parse_updateQuickReplies(_ reader: BufferReader) -> Update? { + var _1: [Api.QuickReply]? + if let _ = reader.readInt32() { + _1 = Api.parseVector(reader, elementSignature: 0, elementType: Api.QuickReply.self) + } + let _c1 = _1 != nil + if _c1 { + return Api.Update.updateQuickReplies(quickReplies: _1!) + } + else { + return nil + } + } + public static func parse_updateQuickReplyMessage(_ reader: BufferReader) -> Update? { + var _1: Api.Message? + if let signature = reader.readInt32() { + _1 = Api.parse(reader, signature: signature) as? Api.Message + } + let _c1 = _1 != nil + if _c1 { + return Api.Update.updateQuickReplyMessage(message: _1!) + } + else { + return nil + } + } public static func parse_updateReadChannelDiscussionInbox(_ reader: BufferReader) -> Update? { var _1: Int32? _1 = reader.readInt32() @@ -3876,6 +4169,17 @@ public extension Api { return nil } } + public static func parse_updateSmsJob(_ reader: BufferReader) -> Update? { + var _1: String? + _1 = parseString(reader) + let _c1 = _1 != nil + if _c1 { + return Api.Update.updateSmsJob(jobId: _1!) + } + else { + return nil + } + } public static func parse_updateStickerSets(_ reader: BufferReader) -> Update? { var _1: Int32? _1 = reader.readInt32() diff --git a/submodules/TelegramApi/Sources/Api23.swift b/submodules/TelegramApi/Sources/Api23.swift index 2d684d02d37..12f4890d7c7 100644 --- a/submodules/TelegramApi/Sources/Api23.swift +++ b/submodules/TelegramApi/Sources/Api23.swift @@ -602,15 +602,16 @@ public extension Api { } public extension Api { enum UserFull: TypeConstructorDescription { - case userFull(flags: Int32, id: Int64, about: String?, settings: Api.PeerSettings, personalPhoto: Api.Photo?, profilePhoto: Api.Photo?, fallbackPhoto: Api.Photo?, notifySettings: Api.PeerNotifySettings, botInfo: Api.BotInfo?, pinnedMsgId: Int32?, commonChatsCount: Int32, folderId: Int32?, ttlPeriod: Int32?, themeEmoticon: String?, privateForwardName: String?, botGroupAdminRights: Api.ChatAdminRights?, botBroadcastAdminRights: Api.ChatAdminRights?, premiumGifts: [Api.PremiumGiftOption]?, wallpaper: Api.WallPaper?, stories: Api.PeerStories?) + case userFull(flags: Int32, flags2: Int32, id: Int64, about: String?, settings: Api.PeerSettings, personalPhoto: Api.Photo?, profilePhoto: Api.Photo?, fallbackPhoto: Api.Photo?, notifySettings: Api.PeerNotifySettings, botInfo: Api.BotInfo?, pinnedMsgId: Int32?, commonChatsCount: Int32, folderId: Int32?, ttlPeriod: Int32?, themeEmoticon: String?, privateForwardName: String?, botGroupAdminRights: Api.ChatAdminRights?, botBroadcastAdminRights: Api.ChatAdminRights?, premiumGifts: [Api.PremiumGiftOption]?, wallpaper: Api.WallPaper?, stories: Api.PeerStories?, businessWorkHours: Api.BusinessWorkHours?, businessLocation: Api.BusinessLocation?, businessGreetingMessage: Api.BusinessGreetingMessage?, businessAwayMessage: Api.BusinessAwayMessage?) public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { switch self { - case .userFull(let flags, let id, let about, let settings, let personalPhoto, let profilePhoto, let fallbackPhoto, let notifySettings, let botInfo, let pinnedMsgId, let commonChatsCount, let folderId, let ttlPeriod, let themeEmoticon, let privateForwardName, let botGroupAdminRights, let botBroadcastAdminRights, let premiumGifts, let wallpaper, let stories): + case .userFull(let flags, let flags2, let id, let about, let settings, let personalPhoto, let profilePhoto, let fallbackPhoto, let notifySettings, let botInfo, let pinnedMsgId, let commonChatsCount, let folderId, let ttlPeriod, let themeEmoticon, let privateForwardName, let botGroupAdminRights, let botBroadcastAdminRights, let premiumGifts, let wallpaper, let stories, let businessWorkHours, let businessLocation, let businessGreetingMessage, let businessAwayMessage): if boxed { - buffer.appendInt32(-1179571092) + buffer.appendInt32(587153029) } serializeInt32(flags, buffer: buffer, boxed: false) + serializeInt32(flags2, buffer: buffer, boxed: false) serializeInt64(id, buffer: buffer, boxed: false) if Int(flags) & Int(1 << 1) != 0 {serializeString(about!, buffer: buffer, boxed: false)} settings.serialize(buffer, true) @@ -634,102 +635,129 @@ public extension Api { }} if Int(flags) & Int(1 << 24) != 0 {wallpaper!.serialize(buffer, true)} if Int(flags) & Int(1 << 25) != 0 {stories!.serialize(buffer, true)} + if Int(flags2) & Int(1 << 0) != 0 {businessWorkHours!.serialize(buffer, true)} + if Int(flags2) & Int(1 << 1) != 0 {businessLocation!.serialize(buffer, true)} + if Int(flags2) & Int(1 << 2) != 0 {businessGreetingMessage!.serialize(buffer, true)} + if Int(flags2) & Int(1 << 3) != 0 {businessAwayMessage!.serialize(buffer, true)} break } } public func descriptionFields() -> (String, [(String, Any)]) { switch self { - case .userFull(let flags, let id, let about, let settings, let personalPhoto, let profilePhoto, let fallbackPhoto, let notifySettings, let botInfo, let pinnedMsgId, let commonChatsCount, let folderId, let ttlPeriod, let themeEmoticon, let privateForwardName, let botGroupAdminRights, let botBroadcastAdminRights, let premiumGifts, let wallpaper, let stories): - return ("userFull", [("flags", flags as Any), ("id", id as Any), ("about", about as Any), ("settings", settings as Any), ("personalPhoto", personalPhoto as Any), ("profilePhoto", profilePhoto as Any), ("fallbackPhoto", fallbackPhoto as Any), ("notifySettings", notifySettings as Any), ("botInfo", botInfo as Any), ("pinnedMsgId", pinnedMsgId as Any), ("commonChatsCount", commonChatsCount as Any), ("folderId", folderId as Any), ("ttlPeriod", ttlPeriod as Any), ("themeEmoticon", themeEmoticon as Any), ("privateForwardName", privateForwardName as Any), ("botGroupAdminRights", botGroupAdminRights as Any), ("botBroadcastAdminRights", botBroadcastAdminRights as Any), ("premiumGifts", premiumGifts as Any), ("wallpaper", wallpaper as Any), ("stories", stories as Any)]) + case .userFull(let flags, let flags2, let id, let about, let settings, let personalPhoto, let profilePhoto, let fallbackPhoto, let notifySettings, let botInfo, let pinnedMsgId, let commonChatsCount, let folderId, let ttlPeriod, let themeEmoticon, let privateForwardName, let botGroupAdminRights, let botBroadcastAdminRights, let premiumGifts, let wallpaper, let stories, let businessWorkHours, let businessLocation, let businessGreetingMessage, let businessAwayMessage): + return ("userFull", [("flags", flags as Any), ("flags2", flags2 as Any), ("id", id as Any), ("about", about as Any), ("settings", settings as Any), ("personalPhoto", personalPhoto as Any), ("profilePhoto", profilePhoto as Any), ("fallbackPhoto", fallbackPhoto as Any), ("notifySettings", notifySettings as Any), ("botInfo", botInfo as Any), ("pinnedMsgId", pinnedMsgId as Any), ("commonChatsCount", commonChatsCount as Any), ("folderId", folderId as Any), ("ttlPeriod", ttlPeriod as Any), ("themeEmoticon", themeEmoticon as Any), ("privateForwardName", privateForwardName as Any), ("botGroupAdminRights", botGroupAdminRights as Any), ("botBroadcastAdminRights", botBroadcastAdminRights as Any), ("premiumGifts", premiumGifts as Any), ("wallpaper", wallpaper as Any), ("stories", stories as Any), ("businessWorkHours", businessWorkHours as Any), ("businessLocation", businessLocation as Any), ("businessGreetingMessage", businessGreetingMessage as Any), ("businessAwayMessage", businessAwayMessage as Any)]) } } public static func parse_userFull(_ reader: BufferReader) -> UserFull? { var _1: Int32? _1 = reader.readInt32() - var _2: Int64? - _2 = reader.readInt64() - var _3: String? - if Int(_1!) & Int(1 << 1) != 0 {_3 = parseString(reader) } - var _4: Api.PeerSettings? + var _2: Int32? + _2 = reader.readInt32() + var _3: Int64? + _3 = reader.readInt64() + var _4: String? + if Int(_1!) & Int(1 << 1) != 0 {_4 = parseString(reader) } + var _5: Api.PeerSettings? if let signature = reader.readInt32() { - _4 = Api.parse(reader, signature: signature) as? Api.PeerSettings + _5 = Api.parse(reader, signature: signature) as? Api.PeerSettings } - var _5: Api.Photo? - if Int(_1!) & Int(1 << 21) != 0 {if let signature = reader.readInt32() { - _5 = Api.parse(reader, signature: signature) as? Api.Photo - } } var _6: Api.Photo? - if Int(_1!) & Int(1 << 2) != 0 {if let signature = reader.readInt32() { + if Int(_1!) & Int(1 << 21) != 0 {if let signature = reader.readInt32() { _6 = Api.parse(reader, signature: signature) as? Api.Photo } } var _7: Api.Photo? - if Int(_1!) & Int(1 << 22) != 0 {if let signature = reader.readInt32() { + if Int(_1!) & Int(1 << 2) != 0 {if let signature = reader.readInt32() { _7 = Api.parse(reader, signature: signature) as? Api.Photo } } - var _8: Api.PeerNotifySettings? + var _8: Api.Photo? + if Int(_1!) & Int(1 << 22) != 0 {if let signature = reader.readInt32() { + _8 = Api.parse(reader, signature: signature) as? Api.Photo + } } + var _9: Api.PeerNotifySettings? if let signature = reader.readInt32() { - _8 = Api.parse(reader, signature: signature) as? Api.PeerNotifySettings + _9 = Api.parse(reader, signature: signature) as? Api.PeerNotifySettings } - var _9: Api.BotInfo? + var _10: Api.BotInfo? if Int(_1!) & Int(1 << 3) != 0 {if let signature = reader.readInt32() { - _9 = Api.parse(reader, signature: signature) as? Api.BotInfo + _10 = Api.parse(reader, signature: signature) as? Api.BotInfo } } - var _10: Int32? - if Int(_1!) & Int(1 << 6) != 0 {_10 = reader.readInt32() } var _11: Int32? - _11 = reader.readInt32() + if Int(_1!) & Int(1 << 6) != 0 {_11 = reader.readInt32() } var _12: Int32? - if Int(_1!) & Int(1 << 11) != 0 {_12 = reader.readInt32() } + _12 = reader.readInt32() var _13: Int32? - if Int(_1!) & Int(1 << 14) != 0 {_13 = reader.readInt32() } - var _14: String? - if Int(_1!) & Int(1 << 15) != 0 {_14 = parseString(reader) } + if Int(_1!) & Int(1 << 11) != 0 {_13 = reader.readInt32() } + var _14: Int32? + if Int(_1!) & Int(1 << 14) != 0 {_14 = reader.readInt32() } var _15: String? - if Int(_1!) & Int(1 << 16) != 0 {_15 = parseString(reader) } - var _16: Api.ChatAdminRights? + if Int(_1!) & Int(1 << 15) != 0 {_15 = parseString(reader) } + var _16: String? + if Int(_1!) & Int(1 << 16) != 0 {_16 = parseString(reader) } + var _17: Api.ChatAdminRights? if Int(_1!) & Int(1 << 17) != 0 {if let signature = reader.readInt32() { - _16 = Api.parse(reader, signature: signature) as? Api.ChatAdminRights + _17 = Api.parse(reader, signature: signature) as? Api.ChatAdminRights } } - var _17: Api.ChatAdminRights? + var _18: Api.ChatAdminRights? if Int(_1!) & Int(1 << 18) != 0 {if let signature = reader.readInt32() { - _17 = Api.parse(reader, signature: signature) as? Api.ChatAdminRights + _18 = Api.parse(reader, signature: signature) as? Api.ChatAdminRights } } - var _18: [Api.PremiumGiftOption]? + var _19: [Api.PremiumGiftOption]? if Int(_1!) & Int(1 << 19) != 0 {if let _ = reader.readInt32() { - _18 = Api.parseVector(reader, elementSignature: 0, elementType: Api.PremiumGiftOption.self) + _19 = Api.parseVector(reader, elementSignature: 0, elementType: Api.PremiumGiftOption.self) } } - var _19: Api.WallPaper? + var _20: Api.WallPaper? if Int(_1!) & Int(1 << 24) != 0 {if let signature = reader.readInt32() { - _19 = Api.parse(reader, signature: signature) as? Api.WallPaper + _20 = Api.parse(reader, signature: signature) as? Api.WallPaper } } - var _20: Api.PeerStories? + var _21: Api.PeerStories? if Int(_1!) & Int(1 << 25) != 0 {if let signature = reader.readInt32() { - _20 = Api.parse(reader, signature: signature) as? Api.PeerStories + _21 = Api.parse(reader, signature: signature) as? Api.PeerStories + } } + var _22: Api.BusinessWorkHours? + if Int(_2!) & Int(1 << 0) != 0 {if let signature = reader.readInt32() { + _22 = Api.parse(reader, signature: signature) as? Api.BusinessWorkHours + } } + var _23: Api.BusinessLocation? + if Int(_2!) & Int(1 << 1) != 0 {if let signature = reader.readInt32() { + _23 = Api.parse(reader, signature: signature) as? Api.BusinessLocation + } } + var _24: Api.BusinessGreetingMessage? + if Int(_2!) & Int(1 << 2) != 0 {if let signature = reader.readInt32() { + _24 = Api.parse(reader, signature: signature) as? Api.BusinessGreetingMessage + } } + var _25: Api.BusinessAwayMessage? + if Int(_2!) & Int(1 << 3) != 0 {if let signature = reader.readInt32() { + _25 = Api.parse(reader, signature: signature) as? Api.BusinessAwayMessage } } let _c1 = _1 != nil let _c2 = _2 != nil - let _c3 = (Int(_1!) & Int(1 << 1) == 0) || _3 != nil - let _c4 = _4 != nil - let _c5 = (Int(_1!) & Int(1 << 21) == 0) || _5 != nil - let _c6 = (Int(_1!) & Int(1 << 2) == 0) || _6 != nil - let _c7 = (Int(_1!) & Int(1 << 22) == 0) || _7 != nil - let _c8 = _8 != nil - let _c9 = (Int(_1!) & Int(1 << 3) == 0) || _9 != nil - let _c10 = (Int(_1!) & Int(1 << 6) == 0) || _10 != nil - let _c11 = _11 != nil - let _c12 = (Int(_1!) & Int(1 << 11) == 0) || _12 != nil - let _c13 = (Int(_1!) & Int(1 << 14) == 0) || _13 != nil - let _c14 = (Int(_1!) & Int(1 << 15) == 0) || _14 != nil - let _c15 = (Int(_1!) & Int(1 << 16) == 0) || _15 != nil - let _c16 = (Int(_1!) & Int(1 << 17) == 0) || _16 != nil - let _c17 = (Int(_1!) & Int(1 << 18) == 0) || _17 != nil - let _c18 = (Int(_1!) & Int(1 << 19) == 0) || _18 != nil - let _c19 = (Int(_1!) & Int(1 << 24) == 0) || _19 != nil - let _c20 = (Int(_1!) & Int(1 << 25) == 0) || _20 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 && _c11 && _c12 && _c13 && _c14 && _c15 && _c16 && _c17 && _c18 && _c19 && _c20 { - return Api.UserFull.userFull(flags: _1!, id: _2!, about: _3, settings: _4!, personalPhoto: _5, profilePhoto: _6, fallbackPhoto: _7, notifySettings: _8!, botInfo: _9, pinnedMsgId: _10, commonChatsCount: _11!, folderId: _12, ttlPeriod: _13, themeEmoticon: _14, privateForwardName: _15, botGroupAdminRights: _16, botBroadcastAdminRights: _17, premiumGifts: _18, wallpaper: _19, stories: _20) + let _c3 = _3 != nil + let _c4 = (Int(_1!) & Int(1 << 1) == 0) || _4 != nil + let _c5 = _5 != nil + let _c6 = (Int(_1!) & Int(1 << 21) == 0) || _6 != nil + let _c7 = (Int(_1!) & Int(1 << 2) == 0) || _7 != nil + let _c8 = (Int(_1!) & Int(1 << 22) == 0) || _8 != nil + let _c9 = _9 != nil + let _c10 = (Int(_1!) & Int(1 << 3) == 0) || _10 != nil + let _c11 = (Int(_1!) & Int(1 << 6) == 0) || _11 != nil + let _c12 = _12 != nil + let _c13 = (Int(_1!) & Int(1 << 11) == 0) || _13 != nil + let _c14 = (Int(_1!) & Int(1 << 14) == 0) || _14 != nil + let _c15 = (Int(_1!) & Int(1 << 15) == 0) || _15 != nil + let _c16 = (Int(_1!) & Int(1 << 16) == 0) || _16 != nil + let _c17 = (Int(_1!) & Int(1 << 17) == 0) || _17 != nil + let _c18 = (Int(_1!) & Int(1 << 18) == 0) || _18 != nil + let _c19 = (Int(_1!) & Int(1 << 19) == 0) || _19 != nil + let _c20 = (Int(_1!) & Int(1 << 24) == 0) || _20 != nil + let _c21 = (Int(_1!) & Int(1 << 25) == 0) || _21 != nil + let _c22 = (Int(_2!) & Int(1 << 0) == 0) || _22 != nil + let _c23 = (Int(_2!) & Int(1 << 1) == 0) || _23 != nil + let _c24 = (Int(_2!) & Int(1 << 2) == 0) || _24 != nil + let _c25 = (Int(_2!) & Int(1 << 3) == 0) || _25 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 && _c11 && _c12 && _c13 && _c14 && _c15 && _c16 && _c17 && _c18 && _c19 && _c20 && _c21 && _c22 && _c23 && _c24 && _c25 { + return Api.UserFull.userFull(flags: _1!, flags2: _2!, id: _3!, about: _4, settings: _5!, personalPhoto: _6, profilePhoto: _7, fallbackPhoto: _8, notifySettings: _9!, botInfo: _10, pinnedMsgId: _11, commonChatsCount: _12!, folderId: _13, ttlPeriod: _14, themeEmoticon: _15, privateForwardName: _16, botGroupAdminRights: _17, botBroadcastAdminRights: _18, premiumGifts: _19, wallpaper: _20, stories: _21, businessWorkHours: _22, businessLocation: _23, businessGreetingMessage: _24, businessAwayMessage: _25) } else { return nil diff --git a/submodules/TelegramApi/Sources/Api24.swift b/submodules/TelegramApi/Sources/Api24.swift index 55a953bc119..1cac2119e83 100644 --- a/submodules/TelegramApi/Sources/Api24.swift +++ b/submodules/TelegramApi/Sources/Api24.swift @@ -424,6 +424,58 @@ public extension Api.account { } } +public extension Api.account { + enum ConnectedBots: TypeConstructorDescription { + case connectedBots(connectedBots: [Api.ConnectedBot], users: [Api.User]) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .connectedBots(let connectedBots, let users): + if boxed { + buffer.appendInt32(400029819) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(connectedBots.count)) + for item in connectedBots { + item.serialize(buffer, true) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(users.count)) + for item in users { + item.serialize(buffer, true) + } + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .connectedBots(let connectedBots, let users): + return ("connectedBots", [("connectedBots", connectedBots as Any), ("users", users as Any)]) + } + } + + public static func parse_connectedBots(_ reader: BufferReader) -> ConnectedBots? { + var _1: [Api.ConnectedBot]? + if let _ = reader.readInt32() { + _1 = Api.parseVector(reader, elementSignature: 0, elementType: Api.ConnectedBot.self) + } + var _2: [Api.User]? + if let _ = reader.readInt32() { + _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) + } + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return Api.account.ConnectedBots.connectedBots(connectedBots: _1!, users: _2!) + } + else { + return nil + } + } + + } +} public extension Api.account { enum ContentSettings: TypeConstructorDescription { case contentSettings(flags: Int32) @@ -1238,55 +1290,3 @@ public extension Api.account { } } -public extension Api.account { - enum WebAuthorizations: TypeConstructorDescription { - case webAuthorizations(authorizations: [Api.WebAuthorization], users: [Api.User]) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .webAuthorizations(let authorizations, let users): - if boxed { - buffer.appendInt32(-313079300) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(authorizations.count)) - for item in authorizations { - item.serialize(buffer, true) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(users.count)) - for item in users { - item.serialize(buffer, true) - } - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .webAuthorizations(let authorizations, let users): - return ("webAuthorizations", [("authorizations", authorizations as Any), ("users", users as Any)]) - } - } - - public static func parse_webAuthorizations(_ reader: BufferReader) -> WebAuthorizations? { - var _1: [Api.WebAuthorization]? - if let _ = reader.readInt32() { - _1 = Api.parseVector(reader, elementSignature: 0, elementType: Api.WebAuthorization.self) - } - var _2: [Api.User]? - if let _ = reader.readInt32() { - _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) - } - let _c1 = _1 != nil - let _c2 = _2 != nil - if _c1 && _c2 { - return Api.account.WebAuthorizations.webAuthorizations(authorizations: _1!, users: _2!) - } - else { - return nil - } - } - - } -} diff --git a/submodules/TelegramApi/Sources/Api25.swift b/submodules/TelegramApi/Sources/Api25.swift index 95534e002c8..a204a5cc8ef 100644 --- a/submodules/TelegramApi/Sources/Api25.swift +++ b/submodules/TelegramApi/Sources/Api25.swift @@ -1,3 +1,55 @@ +public extension Api.account { + enum WebAuthorizations: TypeConstructorDescription { + case webAuthorizations(authorizations: [Api.WebAuthorization], users: [Api.User]) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .webAuthorizations(let authorizations, let users): + if boxed { + buffer.appendInt32(-313079300) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(authorizations.count)) + for item in authorizations { + item.serialize(buffer, true) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(users.count)) + for item in users { + item.serialize(buffer, true) + } + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .webAuthorizations(let authorizations, let users): + return ("webAuthorizations", [("authorizations", authorizations as Any), ("users", users as Any)]) + } + } + + public static func parse_webAuthorizations(_ reader: BufferReader) -> WebAuthorizations? { + var _1: [Api.WebAuthorization]? + if let _ = reader.readInt32() { + _1 = Api.parseVector(reader, elementSignature: 0, elementType: Api.WebAuthorization.self) + } + var _2: [Api.User]? + if let _ = reader.readInt32() { + _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) + } + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return Api.account.WebAuthorizations.webAuthorizations(authorizations: _1!, users: _2!) + } + else { + return nil + } + } + + } +} public extension Api.auth { enum Authorization: TypeConstructorDescription { case authorization(flags: Int32, otherwiseReloginDays: Int32?, tmpSessions: Int32?, futureAuthToken: Buffer?, user: Api.User) diff --git a/submodules/TelegramApi/Sources/Api27.swift b/submodules/TelegramApi/Sources/Api27.swift index 90373cb39cb..5069e6504ec 100644 --- a/submodules/TelegramApi/Sources/Api27.swift +++ b/submodules/TelegramApi/Sources/Api27.swift @@ -354,6 +354,64 @@ public extension Api.help { } } +public extension Api.help { + enum TimezonesList: TypeConstructorDescription { + case timezonesList(timezones: [Api.Timezone], hash: Int32) + case timezonesListNotModified + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .timezonesList(let timezones, let hash): + if boxed { + buffer.appendInt32(2071260529) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(timezones.count)) + for item in timezones { + item.serialize(buffer, true) + } + serializeInt32(hash, buffer: buffer, boxed: false) + break + case .timezonesListNotModified: + if boxed { + buffer.appendInt32(-1761146676) + } + + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .timezonesList(let timezones, let hash): + return ("timezonesList", [("timezones", timezones as Any), ("hash", hash as Any)]) + case .timezonesListNotModified: + return ("timezonesListNotModified", []) + } + } + + public static func parse_timezonesList(_ reader: BufferReader) -> TimezonesList? { + var _1: [Api.Timezone]? + if let _ = reader.readInt32() { + _1 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Timezone.self) + } + var _2: Int32? + _2 = reader.readInt32() + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return Api.help.TimezonesList.timezonesList(timezones: _1!, hash: _2!) + } + else { + return nil + } + } + public static func parse_timezonesListNotModified(_ reader: BufferReader) -> TimezonesList? { + return Api.help.TimezonesList.timezonesListNotModified + } + + } +} public extension Api.help { enum UserInfo: TypeConstructorDescription { case userInfo(message: String, entities: [Api.MessageEntity], author: String, date: Int32) @@ -1233,67 +1291,19 @@ public extension Api.messages { } } public extension Api.messages { - enum Dialogs: TypeConstructorDescription { - case dialogs(dialogs: [Api.Dialog], messages: [Api.Message], chats: [Api.Chat], users: [Api.User]) - case dialogsNotModified(count: Int32) - case dialogsSlice(count: Int32, dialogs: [Api.Dialog], messages: [Api.Message], chats: [Api.Chat], users: [Api.User]) + enum DialogFilters: TypeConstructorDescription { + case dialogFilters(flags: Int32, filters: [Api.DialogFilter]) public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { switch self { - case .dialogs(let dialogs, let messages, let chats, let users): + case .dialogFilters(let flags, let filters): if boxed { - buffer.appendInt32(364538944) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(dialogs.count)) - for item in dialogs { - item.serialize(buffer, true) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(messages.count)) - for item in messages { - item.serialize(buffer, true) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(chats.count)) - for item in chats { - item.serialize(buffer, true) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(users.count)) - for item in users { - item.serialize(buffer, true) - } - break - case .dialogsNotModified(let count): - if boxed { - buffer.appendInt32(-253500010) - } - serializeInt32(count, buffer: buffer, boxed: false) - break - case .dialogsSlice(let count, let dialogs, let messages, let chats, let users): - if boxed { - buffer.appendInt32(1910543603) - } - serializeInt32(count, buffer: buffer, boxed: false) - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(dialogs.count)) - for item in dialogs { - item.serialize(buffer, true) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(messages.count)) - for item in messages { - item.serialize(buffer, true) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(chats.count)) - for item in chats { - item.serialize(buffer, true) + buffer.appendInt32(718878489) } + serializeInt32(flags, buffer: buffer, boxed: false) buffer.appendInt32(481674261) - buffer.appendInt32(Int32(users.count)) - for item in users { + buffer.appendInt32(Int32(filters.count)) + for item in filters { item.serialize(buffer, true) } break @@ -1302,80 +1312,22 @@ public extension Api.messages { public func descriptionFields() -> (String, [(String, Any)]) { switch self { - case .dialogs(let dialogs, let messages, let chats, let users): - return ("dialogs", [("dialogs", dialogs as Any), ("messages", messages as Any), ("chats", chats as Any), ("users", users as Any)]) - case .dialogsNotModified(let count): - return ("dialogsNotModified", [("count", count as Any)]) - case .dialogsSlice(let count, let dialogs, let messages, let chats, let users): - return ("dialogsSlice", [("count", count as Any), ("dialogs", dialogs as Any), ("messages", messages as Any), ("chats", chats as Any), ("users", users as Any)]) + case .dialogFilters(let flags, let filters): + return ("dialogFilters", [("flags", flags as Any), ("filters", filters as Any)]) } } - public static func parse_dialogs(_ reader: BufferReader) -> Dialogs? { - var _1: [Api.Dialog]? - if let _ = reader.readInt32() { - _1 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Dialog.self) - } - var _2: [Api.Message]? - if let _ = reader.readInt32() { - _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Message.self) - } - var _3: [Api.Chat]? - if let _ = reader.readInt32() { - _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Chat.self) - } - var _4: [Api.User]? - if let _ = reader.readInt32() { - _4 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) - } - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = _3 != nil - let _c4 = _4 != nil - if _c1 && _c2 && _c3 && _c4 { - return Api.messages.Dialogs.dialogs(dialogs: _1!, messages: _2!, chats: _3!, users: _4!) - } - else { - return nil - } - } - public static func parse_dialogsNotModified(_ reader: BufferReader) -> Dialogs? { - var _1: Int32? - _1 = reader.readInt32() - let _c1 = _1 != nil - if _c1 { - return Api.messages.Dialogs.dialogsNotModified(count: _1!) - } - else { - return nil - } - } - public static func parse_dialogsSlice(_ reader: BufferReader) -> Dialogs? { + public static func parse_dialogFilters(_ reader: BufferReader) -> DialogFilters? { var _1: Int32? _1 = reader.readInt32() - var _2: [Api.Dialog]? + var _2: [Api.DialogFilter]? if let _ = reader.readInt32() { - _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Dialog.self) - } - var _3: [Api.Message]? - if let _ = reader.readInt32() { - _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Message.self) - } - var _4: [Api.Chat]? - if let _ = reader.readInt32() { - _4 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Chat.self) - } - var _5: [Api.User]? - if let _ = reader.readInt32() { - _5 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) + _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.DialogFilter.self) } let _c1 = _1 != nil let _c2 = _2 != nil - let _c3 = _3 != nil - let _c4 = _4 != nil - let _c5 = _5 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 { - return Api.messages.Dialogs.dialogsSlice(count: _1!, dialogs: _2!, messages: _3!, chats: _4!, users: _5!) + if _c1 && _c2 { + return Api.messages.DialogFilters.dialogFilters(flags: _1!, filters: _2!) } else { return nil diff --git a/submodules/TelegramApi/Sources/Api28.swift b/submodules/TelegramApi/Sources/Api28.swift index 639e18a7b26..cb218918b14 100644 --- a/submodules/TelegramApi/Sources/Api28.swift +++ b/submodules/TelegramApi/Sources/Api28.swift @@ -1,3 +1,155 @@ +public extension Api.messages { + enum Dialogs: TypeConstructorDescription { + case dialogs(dialogs: [Api.Dialog], messages: [Api.Message], chats: [Api.Chat], users: [Api.User]) + case dialogsNotModified(count: Int32) + case dialogsSlice(count: Int32, dialogs: [Api.Dialog], messages: [Api.Message], chats: [Api.Chat], users: [Api.User]) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .dialogs(let dialogs, let messages, let chats, let users): + if boxed { + buffer.appendInt32(364538944) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(dialogs.count)) + for item in dialogs { + item.serialize(buffer, true) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(messages.count)) + for item in messages { + item.serialize(buffer, true) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(chats.count)) + for item in chats { + item.serialize(buffer, true) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(users.count)) + for item in users { + item.serialize(buffer, true) + } + break + case .dialogsNotModified(let count): + if boxed { + buffer.appendInt32(-253500010) + } + serializeInt32(count, buffer: buffer, boxed: false) + break + case .dialogsSlice(let count, let dialogs, let messages, let chats, let users): + if boxed { + buffer.appendInt32(1910543603) + } + serializeInt32(count, buffer: buffer, boxed: false) + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(dialogs.count)) + for item in dialogs { + item.serialize(buffer, true) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(messages.count)) + for item in messages { + item.serialize(buffer, true) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(chats.count)) + for item in chats { + item.serialize(buffer, true) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(users.count)) + for item in users { + item.serialize(buffer, true) + } + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .dialogs(let dialogs, let messages, let chats, let users): + return ("dialogs", [("dialogs", dialogs as Any), ("messages", messages as Any), ("chats", chats as Any), ("users", users as Any)]) + case .dialogsNotModified(let count): + return ("dialogsNotModified", [("count", count as Any)]) + case .dialogsSlice(let count, let dialogs, let messages, let chats, let users): + return ("dialogsSlice", [("count", count as Any), ("dialogs", dialogs as Any), ("messages", messages as Any), ("chats", chats as Any), ("users", users as Any)]) + } + } + + public static func parse_dialogs(_ reader: BufferReader) -> Dialogs? { + var _1: [Api.Dialog]? + if let _ = reader.readInt32() { + _1 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Dialog.self) + } + var _2: [Api.Message]? + if let _ = reader.readInt32() { + _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Message.self) + } + var _3: [Api.Chat]? + if let _ = reader.readInt32() { + _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Chat.self) + } + var _4: [Api.User]? + if let _ = reader.readInt32() { + _4 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) + } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = _4 != nil + if _c1 && _c2 && _c3 && _c4 { + return Api.messages.Dialogs.dialogs(dialogs: _1!, messages: _2!, chats: _3!, users: _4!) + } + else { + return nil + } + } + public static func parse_dialogsNotModified(_ reader: BufferReader) -> Dialogs? { + var _1: Int32? + _1 = reader.readInt32() + let _c1 = _1 != nil + if _c1 { + return Api.messages.Dialogs.dialogsNotModified(count: _1!) + } + else { + return nil + } + } + public static func parse_dialogsSlice(_ reader: BufferReader) -> Dialogs? { + var _1: Int32? + _1 = reader.readInt32() + var _2: [Api.Dialog]? + if let _ = reader.readInt32() { + _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Dialog.self) + } + var _3: [Api.Message]? + if let _ = reader.readInt32() { + _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Message.self) + } + var _4: [Api.Chat]? + if let _ = reader.readInt32() { + _4 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Chat.self) + } + var _5: [Api.User]? + if let _ = reader.readInt32() { + _5 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) + } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = _4 != nil + let _c5 = _5 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 { + return Api.messages.Dialogs.dialogsSlice(count: _1!, dialogs: _2!, messages: _3!, chats: _4!, users: _5!) + } + else { + return nil + } + } + + } +} public extension Api.messages { enum DiscussionMessage: TypeConstructorDescription { case discussionMessage(flags: Int32, messages: [Api.Message], maxId: Int32?, readInboxMaxId: Int32?, readOutboxMaxId: Int32?, unreadCount: Int32, chats: [Api.Chat], users: [Api.User]) @@ -1289,94 +1441,40 @@ public extension Api.messages { } } public extension Api.messages { - enum Reactions: TypeConstructorDescription { - case reactions(hash: Int64, reactions: [Api.Reaction]) - case reactionsNotModified + enum QuickReplies: TypeConstructorDescription { + case quickReplies(quickReplies: [Api.QuickReply], messages: [Api.Message], chats: [Api.Chat], users: [Api.User]) + case quickRepliesNotModified public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { switch self { - case .reactions(let hash, let reactions): + case .quickReplies(let quickReplies, let messages, let chats, let users): if boxed { - buffer.appendInt32(-352454890) + buffer.appendInt32(-963811691) } - serializeInt64(hash, buffer: buffer, boxed: false) buffer.appendInt32(481674261) - buffer.appendInt32(Int32(reactions.count)) - for item in reactions { + buffer.appendInt32(Int32(quickReplies.count)) + for item in quickReplies { item.serialize(buffer, true) } - break - case .reactionsNotModified: - if boxed { - buffer.appendInt32(-1334846497) - } - - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .reactions(let hash, let reactions): - return ("reactions", [("hash", hash as Any), ("reactions", reactions as Any)]) - case .reactionsNotModified: - return ("reactionsNotModified", []) - } - } - - public static func parse_reactions(_ reader: BufferReader) -> Reactions? { - var _1: Int64? - _1 = reader.readInt64() - var _2: [Api.Reaction]? - if let _ = reader.readInt32() { - _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Reaction.self) - } - let _c1 = _1 != nil - let _c2 = _2 != nil - if _c1 && _c2 { - return Api.messages.Reactions.reactions(hash: _1!, reactions: _2!) - } - else { - return nil - } - } - public static func parse_reactionsNotModified(_ reader: BufferReader) -> Reactions? { - return Api.messages.Reactions.reactionsNotModified - } - - } -} -public extension Api.messages { - enum RecentStickers: TypeConstructorDescription { - case recentStickers(hash: Int64, packs: [Api.StickerPack], stickers: [Api.Document], dates: [Int32]) - case recentStickersNotModified - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .recentStickers(let hash, let packs, let stickers, let dates): - if boxed { - buffer.appendInt32(-1999405994) - } - serializeInt64(hash, buffer: buffer, boxed: false) buffer.appendInt32(481674261) - buffer.appendInt32(Int32(packs.count)) - for item in packs { + buffer.appendInt32(Int32(messages.count)) + for item in messages { item.serialize(buffer, true) } buffer.appendInt32(481674261) - buffer.appendInt32(Int32(stickers.count)) - for item in stickers { + buffer.appendInt32(Int32(chats.count)) + for item in chats { item.serialize(buffer, true) } buffer.appendInt32(481674261) - buffer.appendInt32(Int32(dates.count)) - for item in dates { - serializeInt32(item, buffer: buffer, boxed: false) + buffer.appendInt32(Int32(users.count)) + for item in users { + item.serialize(buffer, true) } break - case .recentStickersNotModified: + case .quickRepliesNotModified: if boxed { - buffer.appendInt32(186120336) + buffer.appendInt32(1603398491) } break @@ -1385,193 +1483,101 @@ public extension Api.messages { public func descriptionFields() -> (String, [(String, Any)]) { switch self { - case .recentStickers(let hash, let packs, let stickers, let dates): - return ("recentStickers", [("hash", hash as Any), ("packs", packs as Any), ("stickers", stickers as Any), ("dates", dates as Any)]) - case .recentStickersNotModified: - return ("recentStickersNotModified", []) + case .quickReplies(let quickReplies, let messages, let chats, let users): + return ("quickReplies", [("quickReplies", quickReplies as Any), ("messages", messages as Any), ("chats", chats as Any), ("users", users as Any)]) + case .quickRepliesNotModified: + return ("quickRepliesNotModified", []) } } - public static func parse_recentStickers(_ reader: BufferReader) -> RecentStickers? { - var _1: Int64? - _1 = reader.readInt64() - var _2: [Api.StickerPack]? + public static func parse_quickReplies(_ reader: BufferReader) -> QuickReplies? { + var _1: [Api.QuickReply]? if let _ = reader.readInt32() { - _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.StickerPack.self) + _1 = Api.parseVector(reader, elementSignature: 0, elementType: Api.QuickReply.self) } - var _3: [Api.Document]? + var _2: [Api.Message]? if let _ = reader.readInt32() { - _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Document.self) + _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Message.self) } - var _4: [Int32]? + var _3: [Api.Chat]? if let _ = reader.readInt32() { - _4 = Api.parseVector(reader, elementSignature: -1471112230, elementType: Int32.self) + _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Chat.self) + } + var _4: [Api.User]? + if let _ = reader.readInt32() { + _4 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) } let _c1 = _1 != nil let _c2 = _2 != nil let _c3 = _3 != nil let _c4 = _4 != nil if _c1 && _c2 && _c3 && _c4 { - return Api.messages.RecentStickers.recentStickers(hash: _1!, packs: _2!, stickers: _3!, dates: _4!) + return Api.messages.QuickReplies.quickReplies(quickReplies: _1!, messages: _2!, chats: _3!, users: _4!) } else { return nil } } - public static func parse_recentStickersNotModified(_ reader: BufferReader) -> RecentStickers? { - return Api.messages.RecentStickers.recentStickersNotModified + public static func parse_quickRepliesNotModified(_ reader: BufferReader) -> QuickReplies? { + return Api.messages.QuickReplies.quickRepliesNotModified } } } public extension Api.messages { - enum SavedDialogs: TypeConstructorDescription { - case savedDialogs(dialogs: [Api.SavedDialog], messages: [Api.Message], chats: [Api.Chat], users: [Api.User]) - case savedDialogsNotModified(count: Int32) - case savedDialogsSlice(count: Int32, dialogs: [Api.SavedDialog], messages: [Api.Message], chats: [Api.Chat], users: [Api.User]) + enum Reactions: TypeConstructorDescription { + case reactions(hash: Int64, reactions: [Api.Reaction]) + case reactionsNotModified public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { switch self { - case .savedDialogs(let dialogs, let messages, let chats, let users): + case .reactions(let hash, let reactions): if boxed { - buffer.appendInt32(-130358751) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(dialogs.count)) - for item in dialogs { - item.serialize(buffer, true) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(messages.count)) - for item in messages { - item.serialize(buffer, true) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(chats.count)) - for item in chats { - item.serialize(buffer, true) + buffer.appendInt32(-352454890) } + serializeInt64(hash, buffer: buffer, boxed: false) buffer.appendInt32(481674261) - buffer.appendInt32(Int32(users.count)) - for item in users { + buffer.appendInt32(Int32(reactions.count)) + for item in reactions { item.serialize(buffer, true) } break - case .savedDialogsNotModified(let count): - if boxed { - buffer.appendInt32(-1071681560) - } - serializeInt32(count, buffer: buffer, boxed: false) - break - case .savedDialogsSlice(let count, let dialogs, let messages, let chats, let users): + case .reactionsNotModified: if boxed { - buffer.appendInt32(1153080793) - } - serializeInt32(count, buffer: buffer, boxed: false) - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(dialogs.count)) - for item in dialogs { - item.serialize(buffer, true) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(messages.count)) - for item in messages { - item.serialize(buffer, true) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(chats.count)) - for item in chats { - item.serialize(buffer, true) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(users.count)) - for item in users { - item.serialize(buffer, true) + buffer.appendInt32(-1334846497) } + break } } public func descriptionFields() -> (String, [(String, Any)]) { switch self { - case .savedDialogs(let dialogs, let messages, let chats, let users): - return ("savedDialogs", [("dialogs", dialogs as Any), ("messages", messages as Any), ("chats", chats as Any), ("users", users as Any)]) - case .savedDialogsNotModified(let count): - return ("savedDialogsNotModified", [("count", count as Any)]) - case .savedDialogsSlice(let count, let dialogs, let messages, let chats, let users): - return ("savedDialogsSlice", [("count", count as Any), ("dialogs", dialogs as Any), ("messages", messages as Any), ("chats", chats as Any), ("users", users as Any)]) + case .reactions(let hash, let reactions): + return ("reactions", [("hash", hash as Any), ("reactions", reactions as Any)]) + case .reactionsNotModified: + return ("reactionsNotModified", []) } } - public static func parse_savedDialogs(_ reader: BufferReader) -> SavedDialogs? { - var _1: [Api.SavedDialog]? - if let _ = reader.readInt32() { - _1 = Api.parseVector(reader, elementSignature: 0, elementType: Api.SavedDialog.self) - } - var _2: [Api.Message]? - if let _ = reader.readInt32() { - _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Message.self) - } - var _3: [Api.Chat]? - if let _ = reader.readInt32() { - _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Chat.self) - } - var _4: [Api.User]? + public static func parse_reactions(_ reader: BufferReader) -> Reactions? { + var _1: Int64? + _1 = reader.readInt64() + var _2: [Api.Reaction]? if let _ = reader.readInt32() { - _4 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) + _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Reaction.self) } let _c1 = _1 != nil let _c2 = _2 != nil - let _c3 = _3 != nil - let _c4 = _4 != nil - if _c1 && _c2 && _c3 && _c4 { - return Api.messages.SavedDialogs.savedDialogs(dialogs: _1!, messages: _2!, chats: _3!, users: _4!) - } - else { - return nil - } - } - public static func parse_savedDialogsNotModified(_ reader: BufferReader) -> SavedDialogs? { - var _1: Int32? - _1 = reader.readInt32() - let _c1 = _1 != nil - if _c1 { - return Api.messages.SavedDialogs.savedDialogsNotModified(count: _1!) + if _c1 && _c2 { + return Api.messages.Reactions.reactions(hash: _1!, reactions: _2!) } else { return nil } } - public static func parse_savedDialogsSlice(_ reader: BufferReader) -> SavedDialogs? { - var _1: Int32? - _1 = reader.readInt32() - var _2: [Api.SavedDialog]? - if let _ = reader.readInt32() { - _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.SavedDialog.self) - } - var _3: [Api.Message]? - if let _ = reader.readInt32() { - _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Message.self) - } - var _4: [Api.Chat]? - if let _ = reader.readInt32() { - _4 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Chat.self) - } - var _5: [Api.User]? - if let _ = reader.readInt32() { - _5 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) - } - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = _3 != nil - let _c4 = _4 != nil - let _c5 = _5 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 { - return Api.messages.SavedDialogs.savedDialogsSlice(count: _1!, dialogs: _2!, messages: _3!, chats: _4!, users: _5!) - } - else { - return nil - } + public static func parse_reactionsNotModified(_ reader: BufferReader) -> Reactions? { + return Api.messages.Reactions.reactionsNotModified } } diff --git a/submodules/TelegramApi/Sources/Api29.swift b/submodules/TelegramApi/Sources/Api29.swift index e19625e5582..fa6a3987160 100644 --- a/submodules/TelegramApi/Sources/Api29.swift +++ b/submodules/TelegramApi/Sources/Api29.swift @@ -1,3 +1,233 @@ +public extension Api.messages { + enum RecentStickers: TypeConstructorDescription { + case recentStickers(hash: Int64, packs: [Api.StickerPack], stickers: [Api.Document], dates: [Int32]) + case recentStickersNotModified + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .recentStickers(let hash, let packs, let stickers, let dates): + if boxed { + buffer.appendInt32(-1999405994) + } + serializeInt64(hash, buffer: buffer, boxed: false) + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(packs.count)) + for item in packs { + item.serialize(buffer, true) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(stickers.count)) + for item in stickers { + item.serialize(buffer, true) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(dates.count)) + for item in dates { + serializeInt32(item, buffer: buffer, boxed: false) + } + break + case .recentStickersNotModified: + if boxed { + buffer.appendInt32(186120336) + } + + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .recentStickers(let hash, let packs, let stickers, let dates): + return ("recentStickers", [("hash", hash as Any), ("packs", packs as Any), ("stickers", stickers as Any), ("dates", dates as Any)]) + case .recentStickersNotModified: + return ("recentStickersNotModified", []) + } + } + + public static func parse_recentStickers(_ reader: BufferReader) -> RecentStickers? { + var _1: Int64? + _1 = reader.readInt64() + var _2: [Api.StickerPack]? + if let _ = reader.readInt32() { + _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.StickerPack.self) + } + var _3: [Api.Document]? + if let _ = reader.readInt32() { + _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Document.self) + } + var _4: [Int32]? + if let _ = reader.readInt32() { + _4 = Api.parseVector(reader, elementSignature: -1471112230, elementType: Int32.self) + } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = _4 != nil + if _c1 && _c2 && _c3 && _c4 { + return Api.messages.RecentStickers.recentStickers(hash: _1!, packs: _2!, stickers: _3!, dates: _4!) + } + else { + return nil + } + } + public static func parse_recentStickersNotModified(_ reader: BufferReader) -> RecentStickers? { + return Api.messages.RecentStickers.recentStickersNotModified + } + + } +} +public extension Api.messages { + enum SavedDialogs: TypeConstructorDescription { + case savedDialogs(dialogs: [Api.SavedDialog], messages: [Api.Message], chats: [Api.Chat], users: [Api.User]) + case savedDialogsNotModified(count: Int32) + case savedDialogsSlice(count: Int32, dialogs: [Api.SavedDialog], messages: [Api.Message], chats: [Api.Chat], users: [Api.User]) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .savedDialogs(let dialogs, let messages, let chats, let users): + if boxed { + buffer.appendInt32(-130358751) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(dialogs.count)) + for item in dialogs { + item.serialize(buffer, true) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(messages.count)) + for item in messages { + item.serialize(buffer, true) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(chats.count)) + for item in chats { + item.serialize(buffer, true) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(users.count)) + for item in users { + item.serialize(buffer, true) + } + break + case .savedDialogsNotModified(let count): + if boxed { + buffer.appendInt32(-1071681560) + } + serializeInt32(count, buffer: buffer, boxed: false) + break + case .savedDialogsSlice(let count, let dialogs, let messages, let chats, let users): + if boxed { + buffer.appendInt32(1153080793) + } + serializeInt32(count, buffer: buffer, boxed: false) + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(dialogs.count)) + for item in dialogs { + item.serialize(buffer, true) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(messages.count)) + for item in messages { + item.serialize(buffer, true) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(chats.count)) + for item in chats { + item.serialize(buffer, true) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(users.count)) + for item in users { + item.serialize(buffer, true) + } + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .savedDialogs(let dialogs, let messages, let chats, let users): + return ("savedDialogs", [("dialogs", dialogs as Any), ("messages", messages as Any), ("chats", chats as Any), ("users", users as Any)]) + case .savedDialogsNotModified(let count): + return ("savedDialogsNotModified", [("count", count as Any)]) + case .savedDialogsSlice(let count, let dialogs, let messages, let chats, let users): + return ("savedDialogsSlice", [("count", count as Any), ("dialogs", dialogs as Any), ("messages", messages as Any), ("chats", chats as Any), ("users", users as Any)]) + } + } + + public static func parse_savedDialogs(_ reader: BufferReader) -> SavedDialogs? { + var _1: [Api.SavedDialog]? + if let _ = reader.readInt32() { + _1 = Api.parseVector(reader, elementSignature: 0, elementType: Api.SavedDialog.self) + } + var _2: [Api.Message]? + if let _ = reader.readInt32() { + _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Message.self) + } + var _3: [Api.Chat]? + if let _ = reader.readInt32() { + _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Chat.self) + } + var _4: [Api.User]? + if let _ = reader.readInt32() { + _4 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) + } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = _4 != nil + if _c1 && _c2 && _c3 && _c4 { + return Api.messages.SavedDialogs.savedDialogs(dialogs: _1!, messages: _2!, chats: _3!, users: _4!) + } + else { + return nil + } + } + public static func parse_savedDialogsNotModified(_ reader: BufferReader) -> SavedDialogs? { + var _1: Int32? + _1 = reader.readInt32() + let _c1 = _1 != nil + if _c1 { + return Api.messages.SavedDialogs.savedDialogsNotModified(count: _1!) + } + else { + return nil + } + } + public static func parse_savedDialogsSlice(_ reader: BufferReader) -> SavedDialogs? { + var _1: Int32? + _1 = reader.readInt32() + var _2: [Api.SavedDialog]? + if let _ = reader.readInt32() { + _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.SavedDialog.self) + } + var _3: [Api.Message]? + if let _ = reader.readInt32() { + _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Message.self) + } + var _4: [Api.Chat]? + if let _ = reader.readInt32() { + _4 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Chat.self) + } + var _5: [Api.User]? + if let _ = reader.readInt32() { + _5 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) + } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = _4 != nil + let _c5 = _5 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 { + return Api.messages.SavedDialogs.savedDialogsSlice(count: _1!, dialogs: _2!, messages: _3!, chats: _4!, users: _5!) + } + else { + return nil + } + } + + } +} public extension Api.messages { enum SavedGifs: TypeConstructorDescription { case savedGifs(hash: Int64, gifs: [Api.Document]) @@ -1234,259 +1464,3 @@ public extension Api.payments { } } -public extension Api.payments { - enum PaymentReceipt: TypeConstructorDescription { - case paymentReceipt(flags: Int32, date: Int32, botId: Int64, providerId: Int64, title: String, description: String, photo: Api.WebDocument?, invoice: Api.Invoice, info: Api.PaymentRequestedInfo?, shipping: Api.ShippingOption?, tipAmount: Int64?, currency: String, totalAmount: Int64, credentialsTitle: String, users: [Api.User]) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .paymentReceipt(let flags, let date, let botId, let providerId, let title, let description, let photo, let invoice, let info, let shipping, let tipAmount, let currency, let totalAmount, let credentialsTitle, let users): - if boxed { - buffer.appendInt32(1891958275) - } - serializeInt32(flags, buffer: buffer, boxed: false) - serializeInt32(date, buffer: buffer, boxed: false) - serializeInt64(botId, buffer: buffer, boxed: false) - serializeInt64(providerId, buffer: buffer, boxed: false) - serializeString(title, buffer: buffer, boxed: false) - serializeString(description, buffer: buffer, boxed: false) - if Int(flags) & Int(1 << 2) != 0 {photo!.serialize(buffer, true)} - invoice.serialize(buffer, true) - if Int(flags) & Int(1 << 0) != 0 {info!.serialize(buffer, true)} - if Int(flags) & Int(1 << 1) != 0 {shipping!.serialize(buffer, true)} - if Int(flags) & Int(1 << 3) != 0 {serializeInt64(tipAmount!, buffer: buffer, boxed: false)} - serializeString(currency, buffer: buffer, boxed: false) - serializeInt64(totalAmount, buffer: buffer, boxed: false) - serializeString(credentialsTitle, buffer: buffer, boxed: false) - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(users.count)) - for item in users { - item.serialize(buffer, true) - } - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .paymentReceipt(let flags, let date, let botId, let providerId, let title, let description, let photo, let invoice, let info, let shipping, let tipAmount, let currency, let totalAmount, let credentialsTitle, let users): - return ("paymentReceipt", [("flags", flags as Any), ("date", date as Any), ("botId", botId as Any), ("providerId", providerId as Any), ("title", title as Any), ("description", description as Any), ("photo", photo as Any), ("invoice", invoice as Any), ("info", info as Any), ("shipping", shipping as Any), ("tipAmount", tipAmount as Any), ("currency", currency as Any), ("totalAmount", totalAmount as Any), ("credentialsTitle", credentialsTitle as Any), ("users", users as Any)]) - } - } - - public static func parse_paymentReceipt(_ reader: BufferReader) -> PaymentReceipt? { - var _1: Int32? - _1 = reader.readInt32() - var _2: Int32? - _2 = reader.readInt32() - var _3: Int64? - _3 = reader.readInt64() - var _4: Int64? - _4 = reader.readInt64() - var _5: String? - _5 = parseString(reader) - var _6: String? - _6 = parseString(reader) - var _7: Api.WebDocument? - if Int(_1!) & Int(1 << 2) != 0 {if let signature = reader.readInt32() { - _7 = Api.parse(reader, signature: signature) as? Api.WebDocument - } } - var _8: Api.Invoice? - if let signature = reader.readInt32() { - _8 = Api.parse(reader, signature: signature) as? Api.Invoice - } - var _9: Api.PaymentRequestedInfo? - if Int(_1!) & Int(1 << 0) != 0 {if let signature = reader.readInt32() { - _9 = Api.parse(reader, signature: signature) as? Api.PaymentRequestedInfo - } } - var _10: Api.ShippingOption? - if Int(_1!) & Int(1 << 1) != 0 {if let signature = reader.readInt32() { - _10 = Api.parse(reader, signature: signature) as? Api.ShippingOption - } } - var _11: Int64? - if Int(_1!) & Int(1 << 3) != 0 {_11 = reader.readInt64() } - var _12: String? - _12 = parseString(reader) - var _13: Int64? - _13 = reader.readInt64() - var _14: String? - _14 = parseString(reader) - var _15: [Api.User]? - if let _ = reader.readInt32() { - _15 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) - } - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = _3 != nil - let _c4 = _4 != nil - let _c5 = _5 != nil - let _c6 = _6 != nil - let _c7 = (Int(_1!) & Int(1 << 2) == 0) || _7 != nil - let _c8 = _8 != nil - let _c9 = (Int(_1!) & Int(1 << 0) == 0) || _9 != nil - let _c10 = (Int(_1!) & Int(1 << 1) == 0) || _10 != nil - let _c11 = (Int(_1!) & Int(1 << 3) == 0) || _11 != nil - let _c12 = _12 != nil - let _c13 = _13 != nil - let _c14 = _14 != nil - let _c15 = _15 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 && _c11 && _c12 && _c13 && _c14 && _c15 { - return Api.payments.PaymentReceipt.paymentReceipt(flags: _1!, date: _2!, botId: _3!, providerId: _4!, title: _5!, description: _6!, photo: _7, invoice: _8!, info: _9, shipping: _10, tipAmount: _11, currency: _12!, totalAmount: _13!, credentialsTitle: _14!, users: _15!) - } - else { - return nil - } - } - - } -} -public extension Api.payments { - indirect enum PaymentResult: TypeConstructorDescription { - case paymentResult(updates: Api.Updates) - case paymentVerificationNeeded(url: String) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .paymentResult(let updates): - if boxed { - buffer.appendInt32(1314881805) - } - updates.serialize(buffer, true) - break - case .paymentVerificationNeeded(let url): - if boxed { - buffer.appendInt32(-666824391) - } - serializeString(url, buffer: buffer, boxed: false) - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .paymentResult(let updates): - return ("paymentResult", [("updates", updates as Any)]) - case .paymentVerificationNeeded(let url): - return ("paymentVerificationNeeded", [("url", url as Any)]) - } - } - - public static func parse_paymentResult(_ reader: BufferReader) -> PaymentResult? { - var _1: Api.Updates? - if let signature = reader.readInt32() { - _1 = Api.parse(reader, signature: signature) as? Api.Updates - } - let _c1 = _1 != nil - if _c1 { - return Api.payments.PaymentResult.paymentResult(updates: _1!) - } - else { - return nil - } - } - public static func parse_paymentVerificationNeeded(_ reader: BufferReader) -> PaymentResult? { - var _1: String? - _1 = parseString(reader) - let _c1 = _1 != nil - if _c1 { - return Api.payments.PaymentResult.paymentVerificationNeeded(url: _1!) - } - else { - return nil - } - } - - } -} -public extension Api.payments { - enum SavedInfo: TypeConstructorDescription { - case savedInfo(flags: Int32, savedInfo: Api.PaymentRequestedInfo?) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .savedInfo(let flags, let savedInfo): - if boxed { - buffer.appendInt32(-74456004) - } - serializeInt32(flags, buffer: buffer, boxed: false) - if Int(flags) & Int(1 << 0) != 0 {savedInfo!.serialize(buffer, true)} - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .savedInfo(let flags, let savedInfo): - return ("savedInfo", [("flags", flags as Any), ("savedInfo", savedInfo as Any)]) - } - } - - public static func parse_savedInfo(_ reader: BufferReader) -> SavedInfo? { - var _1: Int32? - _1 = reader.readInt32() - var _2: Api.PaymentRequestedInfo? - if Int(_1!) & Int(1 << 0) != 0 {if let signature = reader.readInt32() { - _2 = Api.parse(reader, signature: signature) as? Api.PaymentRequestedInfo - } } - let _c1 = _1 != nil - let _c2 = (Int(_1!) & Int(1 << 0) == 0) || _2 != nil - if _c1 && _c2 { - return Api.payments.SavedInfo.savedInfo(flags: _1!, savedInfo: _2) - } - else { - return nil - } - } - - } -} -public extension Api.payments { - enum ValidatedRequestedInfo: TypeConstructorDescription { - case validatedRequestedInfo(flags: Int32, id: String?, shippingOptions: [Api.ShippingOption]?) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .validatedRequestedInfo(let flags, let id, let shippingOptions): - if boxed { - buffer.appendInt32(-784000893) - } - serializeInt32(flags, buffer: buffer, boxed: false) - if Int(flags) & Int(1 << 0) != 0 {serializeString(id!, buffer: buffer, boxed: false)} - if Int(flags) & Int(1 << 1) != 0 {buffer.appendInt32(481674261) - buffer.appendInt32(Int32(shippingOptions!.count)) - for item in shippingOptions! { - item.serialize(buffer, true) - }} - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .validatedRequestedInfo(let flags, let id, let shippingOptions): - return ("validatedRequestedInfo", [("flags", flags as Any), ("id", id as Any), ("shippingOptions", shippingOptions as Any)]) - } - } - - public static func parse_validatedRequestedInfo(_ reader: BufferReader) -> ValidatedRequestedInfo? { - var _1: Int32? - _1 = reader.readInt32() - var _2: String? - if Int(_1!) & Int(1 << 0) != 0 {_2 = parseString(reader) } - var _3: [Api.ShippingOption]? - if Int(_1!) & Int(1 << 1) != 0 {if let _ = reader.readInt32() { - _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.ShippingOption.self) - } } - let _c1 = _1 != nil - let _c2 = (Int(_1!) & Int(1 << 0) == 0) || _2 != nil - let _c3 = (Int(_1!) & Int(1 << 1) == 0) || _3 != nil - if _c1 && _c2 && _c3 { - return Api.payments.ValidatedRequestedInfo.validatedRequestedInfo(flags: _1!, id: _2, shippingOptions: _3) - } - else { - return nil - } - } - - } -} diff --git a/submodules/TelegramApi/Sources/Api30.swift b/submodules/TelegramApi/Sources/Api30.swift index 5c39686c7dd..3e5ca3e260f 100644 --- a/submodules/TelegramApi/Sources/Api30.swift +++ b/submodules/TelegramApi/Sources/Api30.swift @@ -1,3 +1,259 @@ +public extension Api.payments { + enum PaymentReceipt: TypeConstructorDescription { + case paymentReceipt(flags: Int32, date: Int32, botId: Int64, providerId: Int64, title: String, description: String, photo: Api.WebDocument?, invoice: Api.Invoice, info: Api.PaymentRequestedInfo?, shipping: Api.ShippingOption?, tipAmount: Int64?, currency: String, totalAmount: Int64, credentialsTitle: String, users: [Api.User]) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .paymentReceipt(let flags, let date, let botId, let providerId, let title, let description, let photo, let invoice, let info, let shipping, let tipAmount, let currency, let totalAmount, let credentialsTitle, let users): + if boxed { + buffer.appendInt32(1891958275) + } + serializeInt32(flags, buffer: buffer, boxed: false) + serializeInt32(date, buffer: buffer, boxed: false) + serializeInt64(botId, buffer: buffer, boxed: false) + serializeInt64(providerId, buffer: buffer, boxed: false) + serializeString(title, buffer: buffer, boxed: false) + serializeString(description, buffer: buffer, boxed: false) + if Int(flags) & Int(1 << 2) != 0 {photo!.serialize(buffer, true)} + invoice.serialize(buffer, true) + if Int(flags) & Int(1 << 0) != 0 {info!.serialize(buffer, true)} + if Int(flags) & Int(1 << 1) != 0 {shipping!.serialize(buffer, true)} + if Int(flags) & Int(1 << 3) != 0 {serializeInt64(tipAmount!, buffer: buffer, boxed: false)} + serializeString(currency, buffer: buffer, boxed: false) + serializeInt64(totalAmount, buffer: buffer, boxed: false) + serializeString(credentialsTitle, buffer: buffer, boxed: false) + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(users.count)) + for item in users { + item.serialize(buffer, true) + } + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .paymentReceipt(let flags, let date, let botId, let providerId, let title, let description, let photo, let invoice, let info, let shipping, let tipAmount, let currency, let totalAmount, let credentialsTitle, let users): + return ("paymentReceipt", [("flags", flags as Any), ("date", date as Any), ("botId", botId as Any), ("providerId", providerId as Any), ("title", title as Any), ("description", description as Any), ("photo", photo as Any), ("invoice", invoice as Any), ("info", info as Any), ("shipping", shipping as Any), ("tipAmount", tipAmount as Any), ("currency", currency as Any), ("totalAmount", totalAmount as Any), ("credentialsTitle", credentialsTitle as Any), ("users", users as Any)]) + } + } + + public static func parse_paymentReceipt(_ reader: BufferReader) -> PaymentReceipt? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int32? + _2 = reader.readInt32() + var _3: Int64? + _3 = reader.readInt64() + var _4: Int64? + _4 = reader.readInt64() + var _5: String? + _5 = parseString(reader) + var _6: String? + _6 = parseString(reader) + var _7: Api.WebDocument? + if Int(_1!) & Int(1 << 2) != 0 {if let signature = reader.readInt32() { + _7 = Api.parse(reader, signature: signature) as? Api.WebDocument + } } + var _8: Api.Invoice? + if let signature = reader.readInt32() { + _8 = Api.parse(reader, signature: signature) as? Api.Invoice + } + var _9: Api.PaymentRequestedInfo? + if Int(_1!) & Int(1 << 0) != 0 {if let signature = reader.readInt32() { + _9 = Api.parse(reader, signature: signature) as? Api.PaymentRequestedInfo + } } + var _10: Api.ShippingOption? + if Int(_1!) & Int(1 << 1) != 0 {if let signature = reader.readInt32() { + _10 = Api.parse(reader, signature: signature) as? Api.ShippingOption + } } + var _11: Int64? + if Int(_1!) & Int(1 << 3) != 0 {_11 = reader.readInt64() } + var _12: String? + _12 = parseString(reader) + var _13: Int64? + _13 = reader.readInt64() + var _14: String? + _14 = parseString(reader) + var _15: [Api.User]? + if let _ = reader.readInt32() { + _15 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) + } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = _4 != nil + let _c5 = _5 != nil + let _c6 = _6 != nil + let _c7 = (Int(_1!) & Int(1 << 2) == 0) || _7 != nil + let _c8 = _8 != nil + let _c9 = (Int(_1!) & Int(1 << 0) == 0) || _9 != nil + let _c10 = (Int(_1!) & Int(1 << 1) == 0) || _10 != nil + let _c11 = (Int(_1!) & Int(1 << 3) == 0) || _11 != nil + let _c12 = _12 != nil + let _c13 = _13 != nil + let _c14 = _14 != nil + let _c15 = _15 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 && _c11 && _c12 && _c13 && _c14 && _c15 { + return Api.payments.PaymentReceipt.paymentReceipt(flags: _1!, date: _2!, botId: _3!, providerId: _4!, title: _5!, description: _6!, photo: _7, invoice: _8!, info: _9, shipping: _10, tipAmount: _11, currency: _12!, totalAmount: _13!, credentialsTitle: _14!, users: _15!) + } + else { + return nil + } + } + + } +} +public extension Api.payments { + indirect enum PaymentResult: TypeConstructorDescription { + case paymentResult(updates: Api.Updates) + case paymentVerificationNeeded(url: String) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .paymentResult(let updates): + if boxed { + buffer.appendInt32(1314881805) + } + updates.serialize(buffer, true) + break + case .paymentVerificationNeeded(let url): + if boxed { + buffer.appendInt32(-666824391) + } + serializeString(url, buffer: buffer, boxed: false) + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .paymentResult(let updates): + return ("paymentResult", [("updates", updates as Any)]) + case .paymentVerificationNeeded(let url): + return ("paymentVerificationNeeded", [("url", url as Any)]) + } + } + + public static func parse_paymentResult(_ reader: BufferReader) -> PaymentResult? { + var _1: Api.Updates? + if let signature = reader.readInt32() { + _1 = Api.parse(reader, signature: signature) as? Api.Updates + } + let _c1 = _1 != nil + if _c1 { + return Api.payments.PaymentResult.paymentResult(updates: _1!) + } + else { + return nil + } + } + public static func parse_paymentVerificationNeeded(_ reader: BufferReader) -> PaymentResult? { + var _1: String? + _1 = parseString(reader) + let _c1 = _1 != nil + if _c1 { + return Api.payments.PaymentResult.paymentVerificationNeeded(url: _1!) + } + else { + return nil + } + } + + } +} +public extension Api.payments { + enum SavedInfo: TypeConstructorDescription { + case savedInfo(flags: Int32, savedInfo: Api.PaymentRequestedInfo?) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .savedInfo(let flags, let savedInfo): + if boxed { + buffer.appendInt32(-74456004) + } + serializeInt32(flags, buffer: buffer, boxed: false) + if Int(flags) & Int(1 << 0) != 0 {savedInfo!.serialize(buffer, true)} + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .savedInfo(let flags, let savedInfo): + return ("savedInfo", [("flags", flags as Any), ("savedInfo", savedInfo as Any)]) + } + } + + public static func parse_savedInfo(_ reader: BufferReader) -> SavedInfo? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Api.PaymentRequestedInfo? + if Int(_1!) & Int(1 << 0) != 0 {if let signature = reader.readInt32() { + _2 = Api.parse(reader, signature: signature) as? Api.PaymentRequestedInfo + } } + let _c1 = _1 != nil + let _c2 = (Int(_1!) & Int(1 << 0) == 0) || _2 != nil + if _c1 && _c2 { + return Api.payments.SavedInfo.savedInfo(flags: _1!, savedInfo: _2) + } + else { + return nil + } + } + + } +} +public extension Api.payments { + enum ValidatedRequestedInfo: TypeConstructorDescription { + case validatedRequestedInfo(flags: Int32, id: String?, shippingOptions: [Api.ShippingOption]?) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .validatedRequestedInfo(let flags, let id, let shippingOptions): + if boxed { + buffer.appendInt32(-784000893) + } + serializeInt32(flags, buffer: buffer, boxed: false) + if Int(flags) & Int(1 << 0) != 0 {serializeString(id!, buffer: buffer, boxed: false)} + if Int(flags) & Int(1 << 1) != 0 {buffer.appendInt32(481674261) + buffer.appendInt32(Int32(shippingOptions!.count)) + for item in shippingOptions! { + item.serialize(buffer, true) + }} + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .validatedRequestedInfo(let flags, let id, let shippingOptions): + return ("validatedRequestedInfo", [("flags", flags as Any), ("id", id as Any), ("shippingOptions", shippingOptions as Any)]) + } + } + + public static func parse_validatedRequestedInfo(_ reader: BufferReader) -> ValidatedRequestedInfo? { + var _1: Int32? + _1 = reader.readInt32() + var _2: String? + if Int(_1!) & Int(1 << 0) != 0 {_2 = parseString(reader) } + var _3: [Api.ShippingOption]? + if Int(_1!) & Int(1 << 1) != 0 {if let _ = reader.readInt32() { + _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.ShippingOption.self) + } } + let _c1 = _1 != nil + let _c2 = (Int(_1!) & Int(1 << 0) == 0) || _2 != nil + let _c3 = (Int(_1!) & Int(1 << 1) == 0) || _3 != nil + if _c1 && _c2 && _c3 { + return Api.payments.ValidatedRequestedInfo.validatedRequestedInfo(flags: _1!, id: _2, shippingOptions: _3) + } + else { + return nil + } + } + + } +} public extension Api.phone { enum ExportedGroupCallInvite: TypeConstructorDescription { case exportedGroupCallInvite(link: String) @@ -724,6 +980,110 @@ public extension Api.premium { } } +public extension Api.smsjobs { + enum EligibilityToJoin: TypeConstructorDescription { + case eligibleToJoin(termsUrl: String, monthlySentSms: Int32) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .eligibleToJoin(let termsUrl, let monthlySentSms): + if boxed { + buffer.appendInt32(-594852657) + } + serializeString(termsUrl, buffer: buffer, boxed: false) + serializeInt32(monthlySentSms, buffer: buffer, boxed: false) + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .eligibleToJoin(let termsUrl, let monthlySentSms): + return ("eligibleToJoin", [("termsUrl", termsUrl as Any), ("monthlySentSms", monthlySentSms as Any)]) + } + } + + public static func parse_eligibleToJoin(_ reader: BufferReader) -> EligibilityToJoin? { + var _1: String? + _1 = parseString(reader) + var _2: Int32? + _2 = reader.readInt32() + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return Api.smsjobs.EligibilityToJoin.eligibleToJoin(termsUrl: _1!, monthlySentSms: _2!) + } + else { + return nil + } + } + + } +} +public extension Api.smsjobs { + enum Status: TypeConstructorDescription { + case status(flags: Int32, recentSent: Int32, recentSince: Int32, recentRemains: Int32, totalSent: Int32, totalSince: Int32, lastGiftSlug: String?, termsUrl: String) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .status(let flags, let recentSent, let recentSince, let recentRemains, let totalSent, let totalSince, let lastGiftSlug, let termsUrl): + if boxed { + buffer.appendInt32(720277905) + } + serializeInt32(flags, buffer: buffer, boxed: false) + serializeInt32(recentSent, buffer: buffer, boxed: false) + serializeInt32(recentSince, buffer: buffer, boxed: false) + serializeInt32(recentRemains, buffer: buffer, boxed: false) + serializeInt32(totalSent, buffer: buffer, boxed: false) + serializeInt32(totalSince, buffer: buffer, boxed: false) + if Int(flags) & Int(1 << 1) != 0 {serializeString(lastGiftSlug!, buffer: buffer, boxed: false)} + serializeString(termsUrl, buffer: buffer, boxed: false) + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .status(let flags, let recentSent, let recentSince, let recentRemains, let totalSent, let totalSince, let lastGiftSlug, let termsUrl): + return ("status", [("flags", flags as Any), ("recentSent", recentSent as Any), ("recentSince", recentSince as Any), ("recentRemains", recentRemains as Any), ("totalSent", totalSent as Any), ("totalSince", totalSince as Any), ("lastGiftSlug", lastGiftSlug as Any), ("termsUrl", termsUrl as Any)]) + } + } + + public static func parse_status(_ reader: BufferReader) -> Status? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int32? + _2 = reader.readInt32() + var _3: Int32? + _3 = reader.readInt32() + var _4: Int32? + _4 = reader.readInt32() + var _5: Int32? + _5 = reader.readInt32() + var _6: Int32? + _6 = reader.readInt32() + var _7: String? + if Int(_1!) & Int(1 << 1) != 0 {_7 = parseString(reader) } + var _8: String? + _8 = parseString(reader) + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = _4 != nil + let _c5 = _5 != nil + let _c6 = _6 != nil + let _c7 = (Int(_1!) & Int(1 << 1) == 0) || _7 != nil + let _c8 = _8 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 { + return Api.smsjobs.Status.status(flags: _1!, recentSent: _2!, recentSince: _3!, recentRemains: _4!, totalSent: _5!, totalSince: _6!, lastGiftSlug: _7, termsUrl: _8!) + } + else { + return nil + } + } + + } +} public extension Api.stats { enum BroadcastStats: TypeConstructorDescription { case broadcastStats(period: Api.StatsDateRangeDays, followers: Api.StatsAbsValueAndPrev, viewsPerPost: Api.StatsAbsValueAndPrev, sharesPerPost: Api.StatsAbsValueAndPrev, reactionsPerPost: Api.StatsAbsValueAndPrev, viewsPerStory: Api.StatsAbsValueAndPrev, sharesPerStory: Api.StatsAbsValueAndPrev, reactionsPerStory: Api.StatsAbsValueAndPrev, enabledNotifications: Api.StatsPercentValue, growthGraph: Api.StatsGraph, followersGraph: Api.StatsGraph, muteGraph: Api.StatsGraph, topHoursGraph: Api.StatsGraph, interactionsGraph: Api.StatsGraph, ivInteractionsGraph: Api.StatsGraph, viewsBySourceGraph: Api.StatsGraph, newFollowersBySourceGraph: Api.StatsGraph, languagesGraph: Api.StatsGraph, reactionsByEmotionGraph: Api.StatsGraph, storyInteractionsGraph: Api.StatsGraph, storyReactionsByEmotionGraph: Api.StatsGraph, recentPostsInteractions: [Api.PostInteractionCounters]) @@ -1376,171 +1736,3 @@ public extension Api.storage { } } -public extension Api.stories { - enum AllStories: TypeConstructorDescription { - case allStories(flags: Int32, count: Int32, state: String, peerStories: [Api.PeerStories], chats: [Api.Chat], users: [Api.User], stealthMode: Api.StoriesStealthMode) - case allStoriesNotModified(flags: Int32, state: String, stealthMode: Api.StoriesStealthMode) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .allStories(let flags, let count, let state, let peerStories, let chats, let users, let stealthMode): - if boxed { - buffer.appendInt32(1862033025) - } - serializeInt32(flags, buffer: buffer, boxed: false) - serializeInt32(count, buffer: buffer, boxed: false) - serializeString(state, buffer: buffer, boxed: false) - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(peerStories.count)) - for item in peerStories { - item.serialize(buffer, true) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(chats.count)) - for item in chats { - item.serialize(buffer, true) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(users.count)) - for item in users { - item.serialize(buffer, true) - } - stealthMode.serialize(buffer, true) - break - case .allStoriesNotModified(let flags, let state, let stealthMode): - if boxed { - buffer.appendInt32(291044926) - } - serializeInt32(flags, buffer: buffer, boxed: false) - serializeString(state, buffer: buffer, boxed: false) - stealthMode.serialize(buffer, true) - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .allStories(let flags, let count, let state, let peerStories, let chats, let users, let stealthMode): - return ("allStories", [("flags", flags as Any), ("count", count as Any), ("state", state as Any), ("peerStories", peerStories as Any), ("chats", chats as Any), ("users", users as Any), ("stealthMode", stealthMode as Any)]) - case .allStoriesNotModified(let flags, let state, let stealthMode): - return ("allStoriesNotModified", [("flags", flags as Any), ("state", state as Any), ("stealthMode", stealthMode as Any)]) - } - } - - public static func parse_allStories(_ reader: BufferReader) -> AllStories? { - var _1: Int32? - _1 = reader.readInt32() - var _2: Int32? - _2 = reader.readInt32() - var _3: String? - _3 = parseString(reader) - var _4: [Api.PeerStories]? - if let _ = reader.readInt32() { - _4 = Api.parseVector(reader, elementSignature: 0, elementType: Api.PeerStories.self) - } - var _5: [Api.Chat]? - if let _ = reader.readInt32() { - _5 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Chat.self) - } - var _6: [Api.User]? - if let _ = reader.readInt32() { - _6 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) - } - var _7: Api.StoriesStealthMode? - if let signature = reader.readInt32() { - _7 = Api.parse(reader, signature: signature) as? Api.StoriesStealthMode - } - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = _3 != nil - let _c4 = _4 != nil - let _c5 = _5 != nil - let _c6 = _6 != nil - let _c7 = _7 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 { - return Api.stories.AllStories.allStories(flags: _1!, count: _2!, state: _3!, peerStories: _4!, chats: _5!, users: _6!, stealthMode: _7!) - } - else { - return nil - } - } - public static func parse_allStoriesNotModified(_ reader: BufferReader) -> AllStories? { - var _1: Int32? - _1 = reader.readInt32() - var _2: String? - _2 = parseString(reader) - var _3: Api.StoriesStealthMode? - if let signature = reader.readInt32() { - _3 = Api.parse(reader, signature: signature) as? Api.StoriesStealthMode - } - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = _3 != nil - if _c1 && _c2 && _c3 { - return Api.stories.AllStories.allStoriesNotModified(flags: _1!, state: _2!, stealthMode: _3!) - } - else { - return nil - } - } - - } -} -public extension Api.stories { - enum PeerStories: TypeConstructorDescription { - case peerStories(stories: Api.PeerStories, chats: [Api.Chat], users: [Api.User]) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .peerStories(let stories, let chats, let users): - if boxed { - buffer.appendInt32(-890861720) - } - stories.serialize(buffer, true) - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(chats.count)) - for item in chats { - item.serialize(buffer, true) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(users.count)) - for item in users { - item.serialize(buffer, true) - } - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .peerStories(let stories, let chats, let users): - return ("peerStories", [("stories", stories as Any), ("chats", chats as Any), ("users", users as Any)]) - } - } - - public static func parse_peerStories(_ reader: BufferReader) -> PeerStories? { - var _1: Api.PeerStories? - if let signature = reader.readInt32() { - _1 = Api.parse(reader, signature: signature) as? Api.PeerStories - } - var _2: [Api.Chat]? - if let _ = reader.readInt32() { - _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Chat.self) - } - var _3: [Api.User]? - if let _ = reader.readInt32() { - _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) - } - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = _3 != nil - if _c1 && _c2 && _c3 { - return Api.stories.PeerStories.peerStories(stories: _1!, chats: _2!, users: _3!) - } - else { - return nil - } - } - - } -} diff --git a/submodules/TelegramApi/Sources/Api31.swift b/submodules/TelegramApi/Sources/Api31.swift index 52e6eb8e1fe..2b161fbd976 100644 --- a/submodules/TelegramApi/Sources/Api31.swift +++ b/submodules/TelegramApi/Sources/Api31.swift @@ -1,3 +1,171 @@ +public extension Api.stories { + enum AllStories: TypeConstructorDescription { + case allStories(flags: Int32, count: Int32, state: String, peerStories: [Api.PeerStories], chats: [Api.Chat], users: [Api.User], stealthMode: Api.StoriesStealthMode) + case allStoriesNotModified(flags: Int32, state: String, stealthMode: Api.StoriesStealthMode) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .allStories(let flags, let count, let state, let peerStories, let chats, let users, let stealthMode): + if boxed { + buffer.appendInt32(1862033025) + } + serializeInt32(flags, buffer: buffer, boxed: false) + serializeInt32(count, buffer: buffer, boxed: false) + serializeString(state, buffer: buffer, boxed: false) + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(peerStories.count)) + for item in peerStories { + item.serialize(buffer, true) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(chats.count)) + for item in chats { + item.serialize(buffer, true) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(users.count)) + for item in users { + item.serialize(buffer, true) + } + stealthMode.serialize(buffer, true) + break + case .allStoriesNotModified(let flags, let state, let stealthMode): + if boxed { + buffer.appendInt32(291044926) + } + serializeInt32(flags, buffer: buffer, boxed: false) + serializeString(state, buffer: buffer, boxed: false) + stealthMode.serialize(buffer, true) + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .allStories(let flags, let count, let state, let peerStories, let chats, let users, let stealthMode): + return ("allStories", [("flags", flags as Any), ("count", count as Any), ("state", state as Any), ("peerStories", peerStories as Any), ("chats", chats as Any), ("users", users as Any), ("stealthMode", stealthMode as Any)]) + case .allStoriesNotModified(let flags, let state, let stealthMode): + return ("allStoriesNotModified", [("flags", flags as Any), ("state", state as Any), ("stealthMode", stealthMode as Any)]) + } + } + + public static func parse_allStories(_ reader: BufferReader) -> AllStories? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int32? + _2 = reader.readInt32() + var _3: String? + _3 = parseString(reader) + var _4: [Api.PeerStories]? + if let _ = reader.readInt32() { + _4 = Api.parseVector(reader, elementSignature: 0, elementType: Api.PeerStories.self) + } + var _5: [Api.Chat]? + if let _ = reader.readInt32() { + _5 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Chat.self) + } + var _6: [Api.User]? + if let _ = reader.readInt32() { + _6 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) + } + var _7: Api.StoriesStealthMode? + if let signature = reader.readInt32() { + _7 = Api.parse(reader, signature: signature) as? Api.StoriesStealthMode + } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = _4 != nil + let _c5 = _5 != nil + let _c6 = _6 != nil + let _c7 = _7 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 { + return Api.stories.AllStories.allStories(flags: _1!, count: _2!, state: _3!, peerStories: _4!, chats: _5!, users: _6!, stealthMode: _7!) + } + else { + return nil + } + } + public static func parse_allStoriesNotModified(_ reader: BufferReader) -> AllStories? { + var _1: Int32? + _1 = reader.readInt32() + var _2: String? + _2 = parseString(reader) + var _3: Api.StoriesStealthMode? + if let signature = reader.readInt32() { + _3 = Api.parse(reader, signature: signature) as? Api.StoriesStealthMode + } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + if _c1 && _c2 && _c3 { + return Api.stories.AllStories.allStoriesNotModified(flags: _1!, state: _2!, stealthMode: _3!) + } + else { + return nil + } + } + + } +} +public extension Api.stories { + enum PeerStories: TypeConstructorDescription { + case peerStories(stories: Api.PeerStories, chats: [Api.Chat], users: [Api.User]) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .peerStories(let stories, let chats, let users): + if boxed { + buffer.appendInt32(-890861720) + } + stories.serialize(buffer, true) + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(chats.count)) + for item in chats { + item.serialize(buffer, true) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(users.count)) + for item in users { + item.serialize(buffer, true) + } + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .peerStories(let stories, let chats, let users): + return ("peerStories", [("stories", stories as Any), ("chats", chats as Any), ("users", users as Any)]) + } + } + + public static func parse_peerStories(_ reader: BufferReader) -> PeerStories? { + var _1: Api.PeerStories? + if let signature = reader.readInt32() { + _1 = Api.parse(reader, signature: signature) as? Api.PeerStories + } + var _2: [Api.Chat]? + if let _ = reader.readInt32() { + _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Chat.self) + } + var _3: [Api.User]? + if let _ = reader.readInt32() { + _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) + } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + if _c1 && _c2 && _c3 { + return Api.stories.PeerStories.peerStories(stories: _1!, chats: _2!, users: _3!) + } + else { + return nil + } + } + + } +} public extension Api.stories { enum Stories: TypeConstructorDescription { case stories(count: Int32, stories: [Api.StoryItem], chats: [Api.Chat], users: [Api.User]) diff --git a/submodules/TelegramApi/Sources/Api32.swift b/submodules/TelegramApi/Sources/Api32.swift index d280ba8d816..5b224f22fcb 100644 --- a/submodules/TelegramApi/Sources/Api32.swift +++ b/submodules/TelegramApi/Sources/Api32.swift @@ -328,6 +328,21 @@ public extension Api.functions.account { }) } } +public extension Api.functions.account { + static func getBotBusinessConnection(connectionId: String) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + let buffer = Buffer() + buffer.appendInt32(1990746736) + serializeString(connectionId, buffer: buffer, boxed: false) + return (FunctionDescription(name: "account.getBotBusinessConnection", parameters: [("connectionId", String(describing: connectionId))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Updates? in + let reader = BufferReader(buffer) + var result: Api.Updates? + if let signature = reader.readInt32() { + result = Api.parse(reader, signature: signature) as? Api.Updates + } + return result + }) + } +} public extension Api.functions.account { static func getChannelDefaultEmojiStatuses(hash: Int64) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() @@ -373,6 +388,21 @@ public extension Api.functions.account { }) } } +public extension Api.functions.account { + static func getConnectedBots() -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + let buffer = Buffer() + buffer.appendInt32(1319421967) + + return (FunctionDescription(name: "account.getConnectedBots", parameters: []), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.account.ConnectedBots? in + let reader = BufferReader(buffer) + var result: Api.account.ConnectedBots? + if let signature = reader.readInt32() { + result = Api.parse(reader, signature: signature) as? Api.account.ConnectedBots + } + return result + }) + } +} public extension Api.functions.account { static func getContactSignUpNotification() -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() @@ -1261,6 +1291,71 @@ public extension Api.functions.account { }) } } +public extension Api.functions.account { + static func updateBusinessAwayMessage(flags: Int32, message: Api.InputBusinessAwayMessage?) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + let buffer = Buffer() + buffer.appendInt32(-1570078811) + serializeInt32(flags, buffer: buffer, boxed: false) + if Int(flags) & Int(1 << 0) != 0 {message!.serialize(buffer, true)} + return (FunctionDescription(name: "account.updateBusinessAwayMessage", parameters: [("flags", String(describing: flags)), ("message", String(describing: message))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Bool? in + let reader = BufferReader(buffer) + var result: Api.Bool? + if let signature = reader.readInt32() { + result = Api.parse(reader, signature: signature) as? Api.Bool + } + return result + }) + } +} +public extension Api.functions.account { + static func updateBusinessGreetingMessage(flags: Int32, message: Api.InputBusinessGreetingMessage?) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + let buffer = Buffer() + buffer.appendInt32(1724755908) + serializeInt32(flags, buffer: buffer, boxed: false) + if Int(flags) & Int(1 << 0) != 0 {message!.serialize(buffer, true)} + return (FunctionDescription(name: "account.updateBusinessGreetingMessage", parameters: [("flags", String(describing: flags)), ("message", String(describing: message))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Bool? in + let reader = BufferReader(buffer) + var result: Api.Bool? + if let signature = reader.readInt32() { + result = Api.parse(reader, signature: signature) as? Api.Bool + } + return result + }) + } +} +public extension Api.functions.account { + static func updateBusinessLocation(flags: Int32, geoPoint: Api.InputGeoPoint?, address: String?) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + let buffer = Buffer() + buffer.appendInt32(-1637149926) + serializeInt32(flags, buffer: buffer, boxed: false) + if Int(flags) & Int(1 << 1) != 0 {geoPoint!.serialize(buffer, true)} + if Int(flags) & Int(1 << 0) != 0 {serializeString(address!, buffer: buffer, boxed: false)} + return (FunctionDescription(name: "account.updateBusinessLocation", parameters: [("flags", String(describing: flags)), ("geoPoint", String(describing: geoPoint)), ("address", String(describing: address))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Bool? in + let reader = BufferReader(buffer) + var result: Api.Bool? + if let signature = reader.readInt32() { + result = Api.parse(reader, signature: signature) as? Api.Bool + } + return result + }) + } +} +public extension Api.functions.account { + static func updateBusinessWorkHours(flags: Int32, businessWorkHours: Api.BusinessWorkHours?) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + let buffer = Buffer() + buffer.appendInt32(1258348646) + serializeInt32(flags, buffer: buffer, boxed: false) + if Int(flags) & Int(1 << 0) != 0 {businessWorkHours!.serialize(buffer, true)} + return (FunctionDescription(name: "account.updateBusinessWorkHours", parameters: [("flags", String(describing: flags)), ("businessWorkHours", String(describing: businessWorkHours))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Bool? in + let reader = BufferReader(buffer) + var result: Api.Bool? + if let signature = reader.readInt32() { + result = Api.parse(reader, signature: signature) as? Api.Bool + } + return result + }) + } +} public extension Api.functions.account { static func updateColor(flags: Int32, color: Int32?, backgroundEmojiId: Int64?) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() @@ -1278,6 +1373,23 @@ public extension Api.functions.account { }) } } +public extension Api.functions.account { + static func updateConnectedBot(flags: Int32, bot: Api.InputUser, recipients: Api.InputBusinessRecipients) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + let buffer = Buffer() + buffer.appendInt32(-1674751363) + serializeInt32(flags, buffer: buffer, boxed: false) + bot.serialize(buffer, true) + recipients.serialize(buffer, true) + return (FunctionDescription(name: "account.updateConnectedBot", parameters: [("flags", String(describing: flags)), ("bot", String(describing: bot)), ("recipients", String(describing: recipients))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Updates? in + let reader = BufferReader(buffer) + var result: Api.Updates? + if let signature = reader.readInt32() { + result = Api.parse(reader, signature: signature) as? Api.Updates + } + return result + }) + } +} public extension Api.functions.account { static func updateDeviceLocked(period: Int32) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() @@ -4130,6 +4242,21 @@ public extension Api.functions.help { }) } } +public extension Api.functions.help { + static func getTimezonesList(hash: Int32) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + let buffer = Buffer() + buffer.appendInt32(1236468288) + serializeInt32(hash, buffer: buffer, boxed: false) + return (FunctionDescription(name: "help.getTimezonesList", parameters: [("hash", String(describing: hash))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.help.TimezonesList? in + let reader = BufferReader(buffer) + var result: Api.help.TimezonesList? + if let signature = reader.readInt32() { + result = Api.parse(reader, signature: signature) as? Api.help.TimezonesList + } + return result + }) + } +} public extension Api.functions.help { static func getUserInfo(userId: Api.InputUser) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() @@ -4393,6 +4520,21 @@ public extension Api.functions.messages { }) } } +public extension Api.functions.messages { + static func checkQuickReplyShortcut(shortcut: String) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + let buffer = Buffer() + buffer.appendInt32(-237962285) + serializeString(shortcut, buffer: buffer, boxed: false) + return (FunctionDescription(name: "messages.checkQuickReplyShortcut", parameters: [("shortcut", String(describing: shortcut))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Bool? in + let reader = BufferReader(buffer) + var result: Api.Bool? + if let signature = reader.readInt32() { + result = Api.parse(reader, signature: signature) as? Api.Bool + } + return result + }) + } +} public extension Api.functions.messages { static func clearAllDrafts() -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() @@ -4562,6 +4704,41 @@ public extension Api.functions.messages { }) } } +public extension Api.functions.messages { + static func deleteQuickReplyMessages(shortcutId: Int32, id: [Int32]) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + let buffer = Buffer() + buffer.appendInt32(-519706352) + serializeInt32(shortcutId, buffer: buffer, boxed: false) + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(id.count)) + for item in id { + serializeInt32(item, buffer: buffer, boxed: false) + } + return (FunctionDescription(name: "messages.deleteQuickReplyMessages", parameters: [("shortcutId", String(describing: shortcutId)), ("id", String(describing: id))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Updates? in + let reader = BufferReader(buffer) + var result: Api.Updates? + if let signature = reader.readInt32() { + result = Api.parse(reader, signature: signature) as? Api.Updates + } + return result + }) + } +} +public extension Api.functions.messages { + static func deleteQuickReplyShortcut(shortcutId: Int32) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + let buffer = Buffer() + buffer.appendInt32(1019234112) + serializeInt32(shortcutId, buffer: buffer, boxed: false) + return (FunctionDescription(name: "messages.deleteQuickReplyShortcut", parameters: [("shortcutId", String(describing: shortcutId))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Bool? in + let reader = BufferReader(buffer) + var result: Api.Bool? + if let signature = reader.readInt32() { + result = Api.parse(reader, signature: signature) as? Api.Bool + } + return result + }) + } +} public extension Api.functions.messages { static func deleteRevokedExportedChatInvites(peer: Api.InputPeer, adminId: Api.InputUser) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() @@ -4760,9 +4937,9 @@ public extension Api.functions.messages { } } public extension Api.functions.messages { - static func editMessage(flags: Int32, peer: Api.InputPeer, id: Int32, message: String?, media: Api.InputMedia?, replyMarkup: Api.ReplyMarkup?, entities: [Api.MessageEntity]?, scheduleDate: Int32?) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + static func editMessage(flags: Int32, peer: Api.InputPeer, id: Int32, message: String?, media: Api.InputMedia?, replyMarkup: Api.ReplyMarkup?, entities: [Api.MessageEntity]?, scheduleDate: Int32?, quickReplyShortcutId: Int32?) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() - buffer.appendInt32(1224152952) + buffer.appendInt32(-539934715) serializeInt32(flags, buffer: buffer, boxed: false) peer.serialize(buffer, true) serializeInt32(id, buffer: buffer, boxed: false) @@ -4775,7 +4952,8 @@ public extension Api.functions.messages { item.serialize(buffer, true) }} if Int(flags) & Int(1 << 15) != 0 {serializeInt32(scheduleDate!, buffer: buffer, boxed: false)} - return (FunctionDescription(name: "messages.editMessage", parameters: [("flags", String(describing: flags)), ("peer", String(describing: peer)), ("id", String(describing: id)), ("message", String(describing: message)), ("media", String(describing: media)), ("replyMarkup", String(describing: replyMarkup)), ("entities", String(describing: entities)), ("scheduleDate", String(describing: scheduleDate))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Updates? in + if Int(flags) & Int(1 << 17) != 0 {serializeInt32(quickReplyShortcutId!, buffer: buffer, boxed: false)} + return (FunctionDescription(name: "messages.editMessage", parameters: [("flags", String(describing: flags)), ("peer", String(describing: peer)), ("id", String(describing: id)), ("message", String(describing: message)), ("media", String(describing: media)), ("replyMarkup", String(describing: replyMarkup)), ("entities", String(describing: entities)), ("scheduleDate", String(describing: scheduleDate)), ("quickReplyShortcutId", String(describing: quickReplyShortcutId))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Updates? in let reader = BufferReader(buffer) var result: Api.Updates? if let signature = reader.readInt32() { @@ -4785,6 +4963,22 @@ public extension Api.functions.messages { }) } } +public extension Api.functions.messages { + static func editQuickReplyShortcut(shortcutId: Int32, shortcut: String) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + let buffer = Buffer() + buffer.appendInt32(1543519471) + serializeInt32(shortcutId, buffer: buffer, boxed: false) + serializeString(shortcut, buffer: buffer, boxed: false) + return (FunctionDescription(name: "messages.editQuickReplyShortcut", parameters: [("shortcutId", String(describing: shortcutId)), ("shortcut", String(describing: shortcut))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Bool? in + let reader = BufferReader(buffer) + var result: Api.Bool? + if let signature = reader.readInt32() { + result = Api.parse(reader, signature: signature) as? Api.Bool + } + return result + }) + } +} public extension Api.functions.messages { static func exportChatInvite(flags: Int32, peer: Api.InputPeer, expireDate: Int32?, usageLimit: Int32?, title: String?) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() @@ -4821,9 +5015,9 @@ public extension Api.functions.messages { } } public extension Api.functions.messages { - static func forwardMessages(flags: Int32, fromPeer: Api.InputPeer, id: [Int32], randomId: [Int64], toPeer: Api.InputPeer, topMsgId: Int32?, scheduleDate: Int32?, sendAs: Api.InputPeer?) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + static func forwardMessages(flags: Int32, fromPeer: Api.InputPeer, id: [Int32], randomId: [Int64], toPeer: Api.InputPeer, topMsgId: Int32?, scheduleDate: Int32?, sendAs: Api.InputPeer?, quickReplyShortcut: Api.InputQuickReplyShortcut?) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() - buffer.appendInt32(-966673468) + buffer.appendInt32(-721186296) serializeInt32(flags, buffer: buffer, boxed: false) fromPeer.serialize(buffer, true) buffer.appendInt32(481674261) @@ -4840,7 +5034,8 @@ public extension Api.functions.messages { if Int(flags) & Int(1 << 9) != 0 {serializeInt32(topMsgId!, buffer: buffer, boxed: false)} if Int(flags) & Int(1 << 10) != 0 {serializeInt32(scheduleDate!, buffer: buffer, boxed: false)} if Int(flags) & Int(1 << 13) != 0 {sendAs!.serialize(buffer, true)} - return (FunctionDescription(name: "messages.forwardMessages", parameters: [("flags", String(describing: flags)), ("fromPeer", String(describing: fromPeer)), ("id", String(describing: id)), ("randomId", String(describing: randomId)), ("toPeer", String(describing: toPeer)), ("topMsgId", String(describing: topMsgId)), ("scheduleDate", String(describing: scheduleDate)), ("sendAs", String(describing: sendAs))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Updates? in + if Int(flags) & Int(1 << 17) != 0 {quickReplyShortcut!.serialize(buffer, true)} + return (FunctionDescription(name: "messages.forwardMessages", parameters: [("flags", String(describing: flags)), ("fromPeer", String(describing: fromPeer)), ("id", String(describing: id)), ("randomId", String(describing: randomId)), ("toPeer", String(describing: toPeer)), ("topMsgId", String(describing: topMsgId)), ("scheduleDate", String(describing: scheduleDate)), ("sendAs", String(describing: sendAs)), ("quickReplyShortcut", String(describing: quickReplyShortcut))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Updates? in let reader = BufferReader(buffer) var result: Api.Updates? if let signature = reader.readInt32() { @@ -5130,15 +5325,15 @@ public extension Api.functions.messages { } } public extension Api.functions.messages { - static func getDialogFilters() -> (FunctionDescription, Buffer, DeserializeFunctionResponse<[Api.DialogFilter]>) { + static func getDialogFilters() -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() - buffer.appendInt32(-241247891) + buffer.appendInt32(-271283063) - return (FunctionDescription(name: "messages.getDialogFilters", parameters: []), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> [Api.DialogFilter]? in + return (FunctionDescription(name: "messages.getDialogFilters", parameters: []), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.messages.DialogFilters? in let reader = BufferReader(buffer) - var result: [Api.DialogFilter]? - if let _ = reader.readInt32() { - result = Api.parseVector(reader, elementSignature: 0, elementType: Api.DialogFilter.self) + var result: Api.messages.DialogFilters? + if let signature = reader.readInt32() { + result = Api.parse(reader, signature: signature) as? Api.messages.DialogFilters } return result }) @@ -5804,6 +5999,43 @@ public extension Api.functions.messages { }) } } +public extension Api.functions.messages { + static func getQuickReplies(hash: Int64) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + let buffer = Buffer() + buffer.appendInt32(-729550168) + serializeInt64(hash, buffer: buffer, boxed: false) + return (FunctionDescription(name: "messages.getQuickReplies", parameters: [("hash", String(describing: hash))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.messages.QuickReplies? in + let reader = BufferReader(buffer) + var result: Api.messages.QuickReplies? + if let signature = reader.readInt32() { + result = Api.parse(reader, signature: signature) as? Api.messages.QuickReplies + } + return result + }) + } +} +public extension Api.functions.messages { + static func getQuickReplyMessages(flags: Int32, shortcutId: Int32, id: [Int32]?, hash: Int64) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + let buffer = Buffer() + buffer.appendInt32(-1801153085) + serializeInt32(flags, buffer: buffer, boxed: false) + serializeInt32(shortcutId, buffer: buffer, boxed: false) + if Int(flags) & Int(1 << 0) != 0 {buffer.appendInt32(481674261) + buffer.appendInt32(Int32(id!.count)) + for item in id! { + serializeInt32(item, buffer: buffer, boxed: false) + }} + serializeInt64(hash, buffer: buffer, boxed: false) + return (FunctionDescription(name: "messages.getQuickReplyMessages", parameters: [("flags", String(describing: flags)), ("shortcutId", String(describing: shortcutId)), ("id", String(describing: id)), ("hash", String(describing: hash))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.messages.Messages? in + let reader = BufferReader(buffer) + var result: Api.messages.Messages? + if let signature = reader.readInt32() { + result = Api.parse(reader, signature: signature) as? Api.messages.Messages + } + return result + }) + } +} public extension Api.functions.messages { static func getRecentLocations(peer: Api.InputPeer, limit: Int32, hash: Int64) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() @@ -6566,6 +6798,25 @@ public extension Api.functions.messages { }) } } +public extension Api.functions.messages { + static func reorderQuickReplies(order: [Int32]) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + let buffer = Buffer() + buffer.appendInt32(1613961479) + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(order.count)) + for item in order { + serializeInt32(item, buffer: buffer, boxed: false) + } + return (FunctionDescription(name: "messages.reorderQuickReplies", parameters: [("order", String(describing: order))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Bool? in + let reader = BufferReader(buffer) + var result: Api.Bool? + if let signature = reader.readInt32() { + result = Api.parse(reader, signature: signature) as? Api.Bool + } + return result + }) + } +} public extension Api.functions.messages { static func reorderStickerSets(flags: Int32, order: [Int64]) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() @@ -7029,9 +7280,9 @@ public extension Api.functions.messages { } } public extension Api.functions.messages { - static func sendInlineBotResult(flags: Int32, peer: Api.InputPeer, replyTo: Api.InputReplyTo?, randomId: Int64, queryId: Int64, id: String, scheduleDate: Int32?, sendAs: Api.InputPeer?) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + static func sendInlineBotResult(flags: Int32, peer: Api.InputPeer, replyTo: Api.InputReplyTo?, randomId: Int64, queryId: Int64, id: String, scheduleDate: Int32?, sendAs: Api.InputPeer?, quickReplyShortcut: Api.InputQuickReplyShortcut?) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() - buffer.appendInt32(-138647366) + buffer.appendInt32(1052698730) serializeInt32(flags, buffer: buffer, boxed: false) peer.serialize(buffer, true) if Int(flags) & Int(1 << 0) != 0 {replyTo!.serialize(buffer, true)} @@ -7040,7 +7291,8 @@ public extension Api.functions.messages { serializeString(id, buffer: buffer, boxed: false) if Int(flags) & Int(1 << 10) != 0 {serializeInt32(scheduleDate!, buffer: buffer, boxed: false)} if Int(flags) & Int(1 << 13) != 0 {sendAs!.serialize(buffer, true)} - return (FunctionDescription(name: "messages.sendInlineBotResult", parameters: [("flags", String(describing: flags)), ("peer", String(describing: peer)), ("replyTo", String(describing: replyTo)), ("randomId", String(describing: randomId)), ("queryId", String(describing: queryId)), ("id", String(describing: id)), ("scheduleDate", String(describing: scheduleDate)), ("sendAs", String(describing: sendAs))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Updates? in + if Int(flags) & Int(1 << 17) != 0 {quickReplyShortcut!.serialize(buffer, true)} + return (FunctionDescription(name: "messages.sendInlineBotResult", parameters: [("flags", String(describing: flags)), ("peer", String(describing: peer)), ("replyTo", String(describing: replyTo)), ("randomId", String(describing: randomId)), ("queryId", String(describing: queryId)), ("id", String(describing: id)), ("scheduleDate", String(describing: scheduleDate)), ("sendAs", String(describing: sendAs)), ("quickReplyShortcut", String(describing: quickReplyShortcut))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Updates? in let reader = BufferReader(buffer) var result: Api.Updates? if let signature = reader.readInt32() { @@ -7051,9 +7303,9 @@ public extension Api.functions.messages { } } public extension Api.functions.messages { - static func sendMedia(flags: Int32, peer: Api.InputPeer, replyTo: Api.InputReplyTo?, media: Api.InputMedia, message: String, randomId: Int64, replyMarkup: Api.ReplyMarkup?, entities: [Api.MessageEntity]?, scheduleDate: Int32?, sendAs: Api.InputPeer?) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + static func sendMedia(flags: Int32, peer: Api.InputPeer, replyTo: Api.InputReplyTo?, media: Api.InputMedia, message: String, randomId: Int64, replyMarkup: Api.ReplyMarkup?, entities: [Api.MessageEntity]?, scheduleDate: Int32?, sendAs: Api.InputPeer?, quickReplyShortcut: Api.InputQuickReplyShortcut?) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() - buffer.appendInt32(1926021693) + buffer.appendInt32(2077646913) serializeInt32(flags, buffer: buffer, boxed: false) peer.serialize(buffer, true) if Int(flags) & Int(1 << 0) != 0 {replyTo!.serialize(buffer, true)} @@ -7068,7 +7320,8 @@ public extension Api.functions.messages { }} if Int(flags) & Int(1 << 10) != 0 {serializeInt32(scheduleDate!, buffer: buffer, boxed: false)} if Int(flags) & Int(1 << 13) != 0 {sendAs!.serialize(buffer, true)} - return (FunctionDescription(name: "messages.sendMedia", parameters: [("flags", String(describing: flags)), ("peer", String(describing: peer)), ("replyTo", String(describing: replyTo)), ("media", String(describing: media)), ("message", String(describing: message)), ("randomId", String(describing: randomId)), ("replyMarkup", String(describing: replyMarkup)), ("entities", String(describing: entities)), ("scheduleDate", String(describing: scheduleDate)), ("sendAs", String(describing: sendAs))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Updates? in + if Int(flags) & Int(1 << 17) != 0 {quickReplyShortcut!.serialize(buffer, true)} + return (FunctionDescription(name: "messages.sendMedia", parameters: [("flags", String(describing: flags)), ("peer", String(describing: peer)), ("replyTo", String(describing: replyTo)), ("media", String(describing: media)), ("message", String(describing: message)), ("randomId", String(describing: randomId)), ("replyMarkup", String(describing: replyMarkup)), ("entities", String(describing: entities)), ("scheduleDate", String(describing: scheduleDate)), ("sendAs", String(describing: sendAs)), ("quickReplyShortcut", String(describing: quickReplyShortcut))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Updates? in let reader = BufferReader(buffer) var result: Api.Updates? if let signature = reader.readInt32() { @@ -7079,9 +7332,9 @@ public extension Api.functions.messages { } } public extension Api.functions.messages { - static func sendMessage(flags: Int32, peer: Api.InputPeer, replyTo: Api.InputReplyTo?, message: String, randomId: Int64, replyMarkup: Api.ReplyMarkup?, entities: [Api.MessageEntity]?, scheduleDate: Int32?, sendAs: Api.InputPeer?) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + static func sendMessage(flags: Int32, peer: Api.InputPeer, replyTo: Api.InputReplyTo?, message: String, randomId: Int64, replyMarkup: Api.ReplyMarkup?, entities: [Api.MessageEntity]?, scheduleDate: Int32?, sendAs: Api.InputPeer?, quickReplyShortcut: Api.InputQuickReplyShortcut?) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() - buffer.appendInt32(671943023) + buffer.appendInt32(-537394132) serializeInt32(flags, buffer: buffer, boxed: false) peer.serialize(buffer, true) if Int(flags) & Int(1 << 0) != 0 {replyTo!.serialize(buffer, true)} @@ -7095,7 +7348,8 @@ public extension Api.functions.messages { }} if Int(flags) & Int(1 << 10) != 0 {serializeInt32(scheduleDate!, buffer: buffer, boxed: false)} if Int(flags) & Int(1 << 13) != 0 {sendAs!.serialize(buffer, true)} - return (FunctionDescription(name: "messages.sendMessage", parameters: [("flags", String(describing: flags)), ("peer", String(describing: peer)), ("replyTo", String(describing: replyTo)), ("message", String(describing: message)), ("randomId", String(describing: randomId)), ("replyMarkup", String(describing: replyMarkup)), ("entities", String(describing: entities)), ("scheduleDate", String(describing: scheduleDate)), ("sendAs", String(describing: sendAs))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Updates? in + if Int(flags) & Int(1 << 17) != 0 {quickReplyShortcut!.serialize(buffer, true)} + return (FunctionDescription(name: "messages.sendMessage", parameters: [("flags", String(describing: flags)), ("peer", String(describing: peer)), ("replyTo", String(describing: replyTo)), ("message", String(describing: message)), ("randomId", String(describing: randomId)), ("replyMarkup", String(describing: replyMarkup)), ("entities", String(describing: entities)), ("scheduleDate", String(describing: scheduleDate)), ("sendAs", String(describing: sendAs)), ("quickReplyShortcut", String(describing: quickReplyShortcut))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Updates? in let reader = BufferReader(buffer) var result: Api.Updates? if let signature = reader.readInt32() { @@ -7106,9 +7360,9 @@ public extension Api.functions.messages { } } public extension Api.functions.messages { - static func sendMultiMedia(flags: Int32, peer: Api.InputPeer, replyTo: Api.InputReplyTo?, multiMedia: [Api.InputSingleMedia], scheduleDate: Int32?, sendAs: Api.InputPeer?) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + static func sendMultiMedia(flags: Int32, peer: Api.InputPeer, replyTo: Api.InputReplyTo?, multiMedia: [Api.InputSingleMedia], scheduleDate: Int32?, sendAs: Api.InputPeer?, quickReplyShortcut: Api.InputQuickReplyShortcut?) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() - buffer.appendInt32(1164872071) + buffer.appendInt32(211175177) serializeInt32(flags, buffer: buffer, boxed: false) peer.serialize(buffer, true) if Int(flags) & Int(1 << 0) != 0 {replyTo!.serialize(buffer, true)} @@ -7119,7 +7373,24 @@ public extension Api.functions.messages { } if Int(flags) & Int(1 << 10) != 0 {serializeInt32(scheduleDate!, buffer: buffer, boxed: false)} if Int(flags) & Int(1 << 13) != 0 {sendAs!.serialize(buffer, true)} - return (FunctionDescription(name: "messages.sendMultiMedia", parameters: [("flags", String(describing: flags)), ("peer", String(describing: peer)), ("replyTo", String(describing: replyTo)), ("multiMedia", String(describing: multiMedia)), ("scheduleDate", String(describing: scheduleDate)), ("sendAs", String(describing: sendAs))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Updates? in + if Int(flags) & Int(1 << 17) != 0 {quickReplyShortcut!.serialize(buffer, true)} + return (FunctionDescription(name: "messages.sendMultiMedia", parameters: [("flags", String(describing: flags)), ("peer", String(describing: peer)), ("replyTo", String(describing: replyTo)), ("multiMedia", String(describing: multiMedia)), ("scheduleDate", String(describing: scheduleDate)), ("sendAs", String(describing: sendAs)), ("quickReplyShortcut", String(describing: quickReplyShortcut))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Updates? in + let reader = BufferReader(buffer) + var result: Api.Updates? + if let signature = reader.readInt32() { + result = Api.parse(reader, signature: signature) as? Api.Updates + } + return result + }) + } +} +public extension Api.functions.messages { + static func sendQuickReplyMessages(peer: Api.InputPeer, shortcutId: Int32) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + let buffer = Buffer() + buffer.appendInt32(857029332) + peer.serialize(buffer, true) + serializeInt32(shortcutId, buffer: buffer, boxed: false) + return (FunctionDescription(name: "messages.sendQuickReplyMessages", parameters: [("peer", String(describing: peer)), ("shortcutId", String(describing: shortcutId))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Updates? in let reader = BufferReader(buffer) var result: Api.Updates? if let signature = reader.readInt32() { @@ -7545,6 +7816,21 @@ public extension Api.functions.messages { }) } } +public extension Api.functions.messages { + static func toggleDialogFilterTags(enabled: Api.Bool) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + let buffer = Buffer() + buffer.appendInt32(-47326647) + enabled.serialize(buffer, true) + return (FunctionDescription(name: "messages.toggleDialogFilterTags", parameters: [("enabled", String(describing: enabled))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Bool? in + let reader = BufferReader(buffer) + var result: Api.Bool? + if let signature = reader.readInt32() { + result = Api.parse(reader, signature: signature) as? Api.Bool + } + return result + }) + } +} public extension Api.functions.messages { static func toggleDialogPin(flags: Int32, peer: Api.InputDialogPeer) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() @@ -8795,6 +9081,113 @@ public extension Api.functions.premium { }) } } +public extension Api.functions.smsjobs { + static func finishJob(flags: Int32, jobId: String, error: String?) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + let buffer = Buffer() + buffer.appendInt32(1327415076) + serializeInt32(flags, buffer: buffer, boxed: false) + serializeString(jobId, buffer: buffer, boxed: false) + if Int(flags) & Int(1 << 0) != 0 {serializeString(error!, buffer: buffer, boxed: false)} + return (FunctionDescription(name: "smsjobs.finishJob", parameters: [("flags", String(describing: flags)), ("jobId", String(describing: jobId)), ("error", String(describing: error))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Bool? in + let reader = BufferReader(buffer) + var result: Api.Bool? + if let signature = reader.readInt32() { + result = Api.parse(reader, signature: signature) as? Api.Bool + } + return result + }) + } +} +public extension Api.functions.smsjobs { + static func getSmsJob(jobId: String) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + let buffer = Buffer() + buffer.appendInt32(2005766191) + serializeString(jobId, buffer: buffer, boxed: false) + return (FunctionDescription(name: "smsjobs.getSmsJob", parameters: [("jobId", String(describing: jobId))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.SmsJob? in + let reader = BufferReader(buffer) + var result: Api.SmsJob? + if let signature = reader.readInt32() { + result = Api.parse(reader, signature: signature) as? Api.SmsJob + } + return result + }) + } +} +public extension Api.functions.smsjobs { + static func getStatus() -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + let buffer = Buffer() + buffer.appendInt32(279353576) + + return (FunctionDescription(name: "smsjobs.getStatus", parameters: []), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.smsjobs.Status? in + let reader = BufferReader(buffer) + var result: Api.smsjobs.Status? + if let signature = reader.readInt32() { + result = Api.parse(reader, signature: signature) as? Api.smsjobs.Status + } + return result + }) + } +} +public extension Api.functions.smsjobs { + static func isEligibleToJoin() -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + let buffer = Buffer() + buffer.appendInt32(249313744) + + return (FunctionDescription(name: "smsjobs.isEligibleToJoin", parameters: []), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.smsjobs.EligibilityToJoin? in + let reader = BufferReader(buffer) + var result: Api.smsjobs.EligibilityToJoin? + if let signature = reader.readInt32() { + result = Api.parse(reader, signature: signature) as? Api.smsjobs.EligibilityToJoin + } + return result + }) + } +} +public extension Api.functions.smsjobs { + static func join() -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + let buffer = Buffer() + buffer.appendInt32(-1488007635) + + return (FunctionDescription(name: "smsjobs.join", parameters: []), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Bool? in + let reader = BufferReader(buffer) + var result: Api.Bool? + if let signature = reader.readInt32() { + result = Api.parse(reader, signature: signature) as? Api.Bool + } + return result + }) + } +} +public extension Api.functions.smsjobs { + static func leave() -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + let buffer = Buffer() + buffer.appendInt32(-1734824589) + + return (FunctionDescription(name: "smsjobs.leave", parameters: []), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Bool? in + let reader = BufferReader(buffer) + var result: Api.Bool? + if let signature = reader.readInt32() { + result = Api.parse(reader, signature: signature) as? Api.Bool + } + return result + }) + } +} +public extension Api.functions.smsjobs { + static func updateSettings(flags: Int32) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + let buffer = Buffer() + buffer.appendInt32(155164863) + serializeInt32(flags, buffer: buffer, boxed: false) + return (FunctionDescription(name: "smsjobs.updateSettings", parameters: [("flags", String(describing: flags))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Bool? in + let reader = BufferReader(buffer) + var result: Api.Bool? + if let signature = reader.readInt32() { + result = Api.parse(reader, signature: signature) as? Api.Bool + } + return result + }) + } +} public extension Api.functions.stats { static func getBroadcastStats(flags: Int32, channel: Api.InputChannel) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() diff --git a/submodules/TelegramApi/Sources/Api4.swift b/submodules/TelegramApi/Sources/Api4.swift index 9a71f34213d..6e5cd0aab4b 100644 --- a/submodules/TelegramApi/Sources/Api4.swift +++ b/submodules/TelegramApi/Sources/Api4.swift @@ -662,6 +662,52 @@ public extension Api { } } +public extension Api { + enum ConnectedBot: TypeConstructorDescription { + case connectedBot(flags: Int32, botId: Int64, recipients: Api.BusinessRecipients) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .connectedBot(let flags, let botId, let recipients): + if boxed { + buffer.appendInt32(-404121113) + } + serializeInt32(flags, buffer: buffer, boxed: false) + serializeInt64(botId, buffer: buffer, boxed: false) + recipients.serialize(buffer, true) + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .connectedBot(let flags, let botId, let recipients): + return ("connectedBot", [("flags", flags as Any), ("botId", botId as Any), ("recipients", recipients as Any)]) + } + } + + public static func parse_connectedBot(_ reader: BufferReader) -> ConnectedBot? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int64? + _2 = reader.readInt64() + var _3: Api.BusinessRecipients? + if let signature = reader.readInt32() { + _3 = Api.parse(reader, signature: signature) as? Api.BusinessRecipients + } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + if _c1 && _c2 && _c3 { + return Api.ConnectedBot.connectedBot(flags: _1!, botId: _2!, recipients: _3!) + } + else { + return nil + } + } + + } +} public extension Api { enum Contact: TypeConstructorDescription { case contact(userId: Int64, mutual: Api.Bool) @@ -1014,20 +1060,21 @@ public extension Api { } public extension Api { enum DialogFilter: TypeConstructorDescription { - case dialogFilter(flags: Int32, id: Int32, title: String, emoticon: String?, pinnedPeers: [Api.InputPeer], includePeers: [Api.InputPeer], excludePeers: [Api.InputPeer]) - case dialogFilterChatlist(flags: Int32, id: Int32, title: String, emoticon: String?, pinnedPeers: [Api.InputPeer], includePeers: [Api.InputPeer]) + case dialogFilter(flags: Int32, id: Int32, title: String, emoticon: String?, color: Int32?, pinnedPeers: [Api.InputPeer], includePeers: [Api.InputPeer], excludePeers: [Api.InputPeer]) + case dialogFilterChatlist(flags: Int32, id: Int32, title: String, emoticon: String?, color: Int32?, pinnedPeers: [Api.InputPeer], includePeers: [Api.InputPeer]) case dialogFilterDefault public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { switch self { - case .dialogFilter(let flags, let id, let title, let emoticon, let pinnedPeers, let includePeers, let excludePeers): + case .dialogFilter(let flags, let id, let title, let emoticon, let color, let pinnedPeers, let includePeers, let excludePeers): if boxed { - buffer.appendInt32(1949890536) + buffer.appendInt32(1605718587) } serializeInt32(flags, buffer: buffer, boxed: false) serializeInt32(id, buffer: buffer, boxed: false) serializeString(title, buffer: buffer, boxed: false) if Int(flags) & Int(1 << 25) != 0 {serializeString(emoticon!, buffer: buffer, boxed: false)} + if Int(flags) & Int(1 << 27) != 0 {serializeInt32(color!, buffer: buffer, boxed: false)} buffer.appendInt32(481674261) buffer.appendInt32(Int32(pinnedPeers.count)) for item in pinnedPeers { @@ -1044,14 +1091,15 @@ public extension Api { item.serialize(buffer, true) } break - case .dialogFilterChatlist(let flags, let id, let title, let emoticon, let pinnedPeers, let includePeers): + case .dialogFilterChatlist(let flags, let id, let title, let emoticon, let color, let pinnedPeers, let includePeers): if boxed { - buffer.appendInt32(-699792216) + buffer.appendInt32(-1612542300) } serializeInt32(flags, buffer: buffer, boxed: false) serializeInt32(id, buffer: buffer, boxed: false) serializeString(title, buffer: buffer, boxed: false) if Int(flags) & Int(1 << 25) != 0 {serializeString(emoticon!, buffer: buffer, boxed: false)} + if Int(flags) & Int(1 << 27) != 0 {serializeInt32(color!, buffer: buffer, boxed: false)} buffer.appendInt32(481674261) buffer.appendInt32(Int32(pinnedPeers.count)) for item in pinnedPeers { @@ -1074,10 +1122,10 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { - case .dialogFilter(let flags, let id, let title, let emoticon, let pinnedPeers, let includePeers, let excludePeers): - return ("dialogFilter", [("flags", flags as Any), ("id", id as Any), ("title", title as Any), ("emoticon", emoticon as Any), ("pinnedPeers", pinnedPeers as Any), ("includePeers", includePeers as Any), ("excludePeers", excludePeers as Any)]) - case .dialogFilterChatlist(let flags, let id, let title, let emoticon, let pinnedPeers, let includePeers): - return ("dialogFilterChatlist", [("flags", flags as Any), ("id", id as Any), ("title", title as Any), ("emoticon", emoticon as Any), ("pinnedPeers", pinnedPeers as Any), ("includePeers", includePeers as Any)]) + case .dialogFilter(let flags, let id, let title, let emoticon, let color, let pinnedPeers, let includePeers, let excludePeers): + return ("dialogFilter", [("flags", flags as Any), ("id", id as Any), ("title", title as Any), ("emoticon", emoticon as Any), ("color", color as Any), ("pinnedPeers", pinnedPeers as Any), ("includePeers", includePeers as Any), ("excludePeers", excludePeers as Any)]) + case .dialogFilterChatlist(let flags, let id, let title, let emoticon, let color, let pinnedPeers, let includePeers): + return ("dialogFilterChatlist", [("flags", flags as Any), ("id", id as Any), ("title", title as Any), ("emoticon", emoticon as Any), ("color", color as Any), ("pinnedPeers", pinnedPeers as Any), ("includePeers", includePeers as Any)]) case .dialogFilterDefault: return ("dialogFilterDefault", []) } @@ -1092,10 +1140,8 @@ public extension Api { _3 = parseString(reader) var _4: String? if Int(_1!) & Int(1 << 25) != 0 {_4 = parseString(reader) } - var _5: [Api.InputPeer]? - if let _ = reader.readInt32() { - _5 = Api.parseVector(reader, elementSignature: 0, elementType: Api.InputPeer.self) - } + var _5: Int32? + if Int(_1!) & Int(1 << 27) != 0 {_5 = reader.readInt32() } var _6: [Api.InputPeer]? if let _ = reader.readInt32() { _6 = Api.parseVector(reader, elementSignature: 0, elementType: Api.InputPeer.self) @@ -1104,15 +1150,20 @@ public extension Api { if let _ = reader.readInt32() { _7 = Api.parseVector(reader, elementSignature: 0, elementType: Api.InputPeer.self) } + var _8: [Api.InputPeer]? + if let _ = reader.readInt32() { + _8 = Api.parseVector(reader, elementSignature: 0, elementType: Api.InputPeer.self) + } let _c1 = _1 != nil let _c2 = _2 != nil let _c3 = _3 != nil let _c4 = (Int(_1!) & Int(1 << 25) == 0) || _4 != nil - let _c5 = _5 != nil + let _c5 = (Int(_1!) & Int(1 << 27) == 0) || _5 != nil let _c6 = _6 != nil let _c7 = _7 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 { - return Api.DialogFilter.dialogFilter(flags: _1!, id: _2!, title: _3!, emoticon: _4, pinnedPeers: _5!, includePeers: _6!, excludePeers: _7!) + let _c8 = _8 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 { + return Api.DialogFilter.dialogFilter(flags: _1!, id: _2!, title: _3!, emoticon: _4, color: _5, pinnedPeers: _6!, includePeers: _7!, excludePeers: _8!) } else { return nil @@ -1127,22 +1178,25 @@ public extension Api { _3 = parseString(reader) var _4: String? if Int(_1!) & Int(1 << 25) != 0 {_4 = parseString(reader) } - var _5: [Api.InputPeer]? - if let _ = reader.readInt32() { - _5 = Api.parseVector(reader, elementSignature: 0, elementType: Api.InputPeer.self) - } + var _5: Int32? + if Int(_1!) & Int(1 << 27) != 0 {_5 = reader.readInt32() } var _6: [Api.InputPeer]? if let _ = reader.readInt32() { _6 = Api.parseVector(reader, elementSignature: 0, elementType: Api.InputPeer.self) } + var _7: [Api.InputPeer]? + if let _ = reader.readInt32() { + _7 = Api.parseVector(reader, elementSignature: 0, elementType: Api.InputPeer.self) + } let _c1 = _1 != nil let _c2 = _2 != nil let _c3 = _3 != nil let _c4 = (Int(_1!) & Int(1 << 25) == 0) || _4 != nil - let _c5 = _5 != nil + let _c5 = (Int(_1!) & Int(1 << 27) == 0) || _5 != nil let _c6 = _6 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 { - return Api.DialogFilter.dialogFilterChatlist(flags: _1!, id: _2!, title: _3!, emoticon: _4, pinnedPeers: _5!, includePeers: _6!) + let _c7 = _7 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 { + return Api.DialogFilter.dialogFilterChatlist(flags: _1!, id: _2!, title: _3!, emoticon: _4, color: _5, pinnedPeers: _6!, includePeers: _7!) } else { return nil diff --git a/submodules/TelegramApi/Sources/Api7.swift b/submodules/TelegramApi/Sources/Api7.swift index 80bc038f382..cbdf0ebf38a 100644 --- a/submodules/TelegramApi/Sources/Api7.swift +++ b/submodules/TelegramApi/Sources/Api7.swift @@ -262,6 +262,150 @@ public extension Api { } } +public extension Api { + enum InputBusinessAwayMessage: TypeConstructorDescription { + case inputBusinessAwayMessage(flags: Int32, shortcutId: Int32, schedule: Api.BusinessAwayMessageSchedule, recipients: Api.InputBusinessRecipients) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .inputBusinessAwayMessage(let flags, let shortcutId, let schedule, let recipients): + if boxed { + buffer.appendInt32(-2094959136) + } + serializeInt32(flags, buffer: buffer, boxed: false) + serializeInt32(shortcutId, buffer: buffer, boxed: false) + schedule.serialize(buffer, true) + recipients.serialize(buffer, true) + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .inputBusinessAwayMessage(let flags, let shortcutId, let schedule, let recipients): + return ("inputBusinessAwayMessage", [("flags", flags as Any), ("shortcutId", shortcutId as Any), ("schedule", schedule as Any), ("recipients", recipients as Any)]) + } + } + + public static func parse_inputBusinessAwayMessage(_ reader: BufferReader) -> InputBusinessAwayMessage? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int32? + _2 = reader.readInt32() + var _3: Api.BusinessAwayMessageSchedule? + if let signature = reader.readInt32() { + _3 = Api.parse(reader, signature: signature) as? Api.BusinessAwayMessageSchedule + } + var _4: Api.InputBusinessRecipients? + if let signature = reader.readInt32() { + _4 = Api.parse(reader, signature: signature) as? Api.InputBusinessRecipients + } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = _4 != nil + if _c1 && _c2 && _c3 && _c4 { + return Api.InputBusinessAwayMessage.inputBusinessAwayMessage(flags: _1!, shortcutId: _2!, schedule: _3!, recipients: _4!) + } + else { + return nil + } + } + + } +} +public extension Api { + enum InputBusinessGreetingMessage: TypeConstructorDescription { + case inputBusinessGreetingMessage(shortcutId: Int32, recipients: Api.InputBusinessRecipients, noActivityDays: Int32) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .inputBusinessGreetingMessage(let shortcutId, let recipients, let noActivityDays): + if boxed { + buffer.appendInt32(26528571) + } + serializeInt32(shortcutId, buffer: buffer, boxed: false) + recipients.serialize(buffer, true) + serializeInt32(noActivityDays, buffer: buffer, boxed: false) + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .inputBusinessGreetingMessage(let shortcutId, let recipients, let noActivityDays): + return ("inputBusinessGreetingMessage", [("shortcutId", shortcutId as Any), ("recipients", recipients as Any), ("noActivityDays", noActivityDays as Any)]) + } + } + + public static func parse_inputBusinessGreetingMessage(_ reader: BufferReader) -> InputBusinessGreetingMessage? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Api.InputBusinessRecipients? + if let signature = reader.readInt32() { + _2 = Api.parse(reader, signature: signature) as? Api.InputBusinessRecipients + } + var _3: Int32? + _3 = reader.readInt32() + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + if _c1 && _c2 && _c3 { + return Api.InputBusinessGreetingMessage.inputBusinessGreetingMessage(shortcutId: _1!, recipients: _2!, noActivityDays: _3!) + } + else { + return nil + } + } + + } +} +public extension Api { + enum InputBusinessRecipients: TypeConstructorDescription { + case inputBusinessRecipients(flags: Int32, users: [Api.InputUser]?) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .inputBusinessRecipients(let flags, let users): + if boxed { + buffer.appendInt32(1871393450) + } + serializeInt32(flags, buffer: buffer, boxed: false) + if Int(flags) & Int(1 << 4) != 0 {buffer.appendInt32(481674261) + buffer.appendInt32(Int32(users!.count)) + for item in users! { + item.serialize(buffer, true) + }} + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .inputBusinessRecipients(let flags, let users): + return ("inputBusinessRecipients", [("flags", flags as Any), ("users", users as Any)]) + } + } + + public static func parse_inputBusinessRecipients(_ reader: BufferReader) -> InputBusinessRecipients? { + var _1: Int32? + _1 = reader.readInt32() + var _2: [Api.InputUser]? + if Int(_1!) & Int(1 << 4) != 0 {if let _ = reader.readInt32() { + _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.InputUser.self) + } } + let _c1 = _1 != nil + let _c2 = (Int(_1!) & Int(1 << 4) == 0) || _2 != nil + if _c1 && _c2 { + return Api.InputBusinessRecipients.inputBusinessRecipients(flags: _1!, users: _2) + } + else { + return nil + } + } + + } +} public extension Api { indirect enum InputChannel: TypeConstructorDescription { case inputChannel(channelId: Int64, accessHash: Int64) diff --git a/submodules/TelegramCore/Sources/Account/AccountIntermediateState.swift b/submodules/TelegramCore/Sources/Account/AccountIntermediateState.swift index 0d1df5f992f..79c3c4658a1 100644 --- a/submodules/TelegramCore/Sources/Account/AccountIntermediateState.swift +++ b/submodules/TelegramCore/Sources/Account/AccountIntermediateState.swift @@ -62,6 +62,7 @@ enum AccountStateGlobalNotificationSettingsSubject { enum AccountStateMutationOperation { case AddMessages([StoreMessage], AddMessagesLocation) case AddScheduledMessages([StoreMessage]) + case AddQuickReplyMessages([StoreMessage]) case DeleteMessagesWithGlobalIds([Int32]) case DeleteMessages([MessageId]) case EditMessage(MessageId, StoreMessage) @@ -332,6 +333,10 @@ struct AccountMutableState { self.addOperation(.AddScheduledMessages(messages)) } + mutating func addQuickReplyMessages(_ messages: [StoreMessage]) { + self.addOperation(.AddQuickReplyMessages(messages)) + } + mutating func addDisplayAlert(_ text: String, isDropAuth: Bool) { self.displayAlerts.append((text: text, isDropAuth: isDropAuth)) } @@ -709,6 +714,19 @@ struct AccountMutableState { } } } + case let .AddQuickReplyMessages(messages): + for message in messages { + if case let .Id(id) = message.id { + self.storedMessages.insert(id) + inner: for attribute in message.attributes { + if let attribute = attribute as? ReplyMessageAttribute { + self.referencedReplyMessageIds.add(sourceId: id, targetId: attribute.messageId) + } else if let attribute = attribute as? ReplyStoryAttribute { + self.referencedStoryIds.insert(attribute.storyId) + } + } + } + } case let .UpdateState(state): self.state = state case let .UpdateChannelState(peerId, pts): diff --git a/submodules/TelegramCore/Sources/Account/AccountManager.swift b/submodules/TelegramCore/Sources/Account/AccountManager.swift index b302e346b5b..e7808d6f7da 100644 --- a/submodules/TelegramCore/Sources/Account/AccountManager.swift +++ b/submodules/TelegramCore/Sources/Account/AccountManager.swift @@ -293,6 +293,7 @@ private var declaredEncodables: Void = { declareEncodable(WebpagePreviewMessageAttribute.self, f: { WebpagePreviewMessageAttribute(decoder: $0) }) declareEncodable(DerivedDataMessageAttribute.self, f: { DerivedDataMessageAttribute(decoder: $0) }) declareEncodable(TelegramApplicationIcons.self, f: { TelegramApplicationIcons(decoder: $0) }) + declareEncodable(OutgoingQuickReplyMessageAttribute.self, f: { OutgoingQuickReplyMessageAttribute(decoder: $0) }) return }() diff --git a/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift b/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift index 76754ca04cb..5ecd19cfc1b 100644 --- a/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift +++ b/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift @@ -126,7 +126,7 @@ public func tagsForStoreMessage(incoming: Bool, attributes: [MessageAttribute], func apiMessagePeerId(_ messsage: Api.Message) -> PeerId? { switch messsage { - case let .message(_, _, _, _, messagePeerId, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _): + case let .message(_, _, _, _, messagePeerId, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _): let chatPeerId = messagePeerId return chatPeerId.peerId case let .messageEmpty(_, _, peerId): @@ -142,7 +142,7 @@ func apiMessagePeerId(_ messsage: Api.Message) -> PeerId? { func apiMessagePeerIds(_ message: Api.Message) -> [PeerId] { switch message { - case let .message(_, _, fromId, _, chatPeerId, savedPeerId, fwdHeader, viaBotId, replyTo, _, _, media, _, entities, _, _, _, _, _, _, _, _, _): + case let .message(_, _, fromId, _, chatPeerId, savedPeerId, fwdHeader, viaBotId, replyTo, _, _, media, _, entities, _, _, _, _, _, _, _, _, _, _): let peerId: PeerId = chatPeerId.peerId var result = [peerId] @@ -263,7 +263,7 @@ func apiMessagePeerIds(_ message: Api.Message) -> [PeerId] { func apiMessageAssociatedMessageIds(_ message: Api.Message) -> (replyIds: ReferencedReplyMessageIds, generalIds: [MessageId])? { switch message { - case let .message(_, id, _, _, chatPeerId, _, _, _, replyTo, _, _, _, _, _, _, _, _, _, _, _, _, _, _): + case let .message(_, id, _, _, chatPeerId, _, _, _, replyTo, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _): if let replyTo = replyTo { let peerId: PeerId = chatPeerId.peerId @@ -597,8 +597,13 @@ func messageTextEntitiesFromApiEntities(_ entities: [Api.MessageEntity]) -> [Mes extension StoreMessage { convenience init?(apiMessage: Api.Message, accountPeerId: PeerId, peerIsForum: Bool, namespace: MessageId.Namespace = Namespaces.Message.Cloud) { switch apiMessage { - case let .message(flags, id, fromId, boosts, chatPeerId, savedPeerId, fwdFrom, viaBotId, replyTo, date, message, media, replyMarkup, entities, views, forwards, replies, editDate, postAuthor, groupingId, reactions, restrictionReason, ttlPeriod): + case let .message(flags, id, fromId, boosts, chatPeerId, savedPeerId, fwdFrom, viaBotId, replyTo, date, message, media, replyMarkup, entities, views, forwards, replies, editDate, postAuthor, groupingId, reactions, restrictionReason, ttlPeriod, quickReplyShortcutId): let resolvedFromId = fromId?.peerId ?? chatPeerId.peerId + + var namespace = namespace + if quickReplyShortcutId != nil { + namespace = Namespaces.Message.QuickReplyCloud + } let peerId: PeerId var authorId: PeerId? @@ -663,7 +668,7 @@ extension StoreMessage { threadId = Int64(threadIdValue.id) } } - attributes.append(ReplyMessageAttribute(messageId: MessageId(peerId: replyPeerId, namespace: Namespaces.Message.Cloud, id: replyToMsgId), threadMessageId: threadMessageId, quote: quote, isQuote: isQuote)) + attributes.append(ReplyMessageAttribute(messageId: MessageId(peerId: replyPeerId, namespace: namespace, id: replyToMsgId), threadMessageId: threadMessageId, quote: quote, isQuote: isQuote)) } if let replyHeader = replyHeader { attributes.append(QuotedReplyMessageAttribute(apiHeader: replyHeader, quote: quote, isQuote: isQuote)) @@ -729,6 +734,10 @@ extension StoreMessage { if peerId == accountPeerId, let savedPeerId = savedPeerId { threadId = savedPeerId.peerId.toInt64() } + + if let quickReplyShortcutId { + threadId = Int64(quickReplyShortcutId) + } let messageText = message var medias: [Media] = [] @@ -791,7 +800,7 @@ extension StoreMessage { attributes.append(InlineBotMessageAttribute(peerId: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(viaBotId)), title: nil)) } - if namespace != Namespaces.Message.ScheduledCloud { + if namespace != Namespaces.Message.ScheduledCloud && namespace != Namespaces.Message.QuickReplyCloud { if let views = views { attributes.append(ViewCountMessageAttribute(count: Int(views))) } diff --git a/submodules/TelegramCore/Sources/PendingMessages/EnqueueMessage.swift b/submodules/TelegramCore/Sources/PendingMessages/EnqueueMessage.swift index 73a643cec61..300da992bd4 100644 --- a/submodules/TelegramCore/Sources/PendingMessages/EnqueueMessage.swift +++ b/submodules/TelegramCore/Sources/PendingMessages/EnqueueMessage.swift @@ -138,6 +138,15 @@ public enum EnqueueMessage { } } + public func withUpdatedThreadId(_ threadId: Int64?) -> EnqueueMessage { + switch self { + case let .message(text, attributes, inlineStickers, mediaReference, _, replyToMessageId, replyToStoryId, localGroupingKey, correlationId, bubbleUpEmojiOrStickersets): + return .message(text: text, attributes: attributes, inlineStickers: inlineStickers, mediaReference: mediaReference, threadId: threadId, replyToMessageId: replyToMessageId, replyToStoryId: replyToStoryId, localGroupingKey: localGroupingKey, correlationId: correlationId, bubbleUpEmojiOrStickersets: bubbleUpEmojiOrStickersets) + case let .forward(source, _, grouping, attributes, correlationId, asCopy): + return .forward(source: source, threadId: threadId, grouping: grouping, attributes: attributes, correlationId: correlationId, asCopy: asCopy) + } + } + public var groupingKey: Int64? { if case let .message(_, _, _, _, _, _, _, localGroupingKey, _, _) = self { return localGroupingKey @@ -225,6 +234,8 @@ private func filterMessageAttributesForOutgoingMessage(_ attributes: [MessageAtt return true case _ as OutgoingScheduleInfoMessageAttribute: return true + case _ as OutgoingQuickReplyMessageAttribute: + return true case _ as EmbeddedMediaStickersMessageAttribute: return true case _ as EmojiSearchQueryMessageAttribute: @@ -254,6 +265,8 @@ private func filterMessageAttributesForForwardedMessage(_ attributes: [MessageAt return true case _ as OutgoingScheduleInfoMessageAttribute: return true + case _ as OutgoingQuickReplyMessageAttribute: + return true case _ as ForwardOptionsMessageAttribute: return true case _ as SendAsMessageAttribute: @@ -679,6 +692,9 @@ func enqueueMessages(transaction: Transaction, account: Account, peerId: PeerId, messageNamespace = Namespaces.Message.ScheduledLocal effectiveTimestamp = attribute.scheduleTime } + } else if attribute is OutgoingQuickReplyMessageAttribute { + messageNamespace = Namespaces.Message.QuickReplyLocal + effectiveTimestamp = 0 } else if let attribute = attribute as? SendAsMessageAttribute { if let peer = transaction.getPeer(attribute.peerId) { sendAsPeer = peer @@ -704,11 +720,14 @@ func enqueueMessages(transaction: Transaction, account: Account, peerId: PeerId, if messageNamespace != Namespaces.Message.ScheduledLocal { attributes.removeAll(where: { $0 is OutgoingScheduleInfoMessageAttribute }) } + if messageNamespace != Namespaces.Message.QuickReplyLocal { + attributes.removeAll(where: { $0 is OutgoingQuickReplyMessageAttribute }) + } if let peer = peer as? TelegramChannel { switch peer.info { case let .broadcast(info): - if messageNamespace != Namespaces.Message.ScheduledLocal { + if messageNamespace != Namespaces.Message.ScheduledLocal && messageNamespace != Namespaces.Message.QuickReplyLocal { attributes.append(ViewCountMessageAttribute(count: 1)) } if info.flags.contains(.messagesShouldHaveSignatures) { @@ -917,6 +936,9 @@ func enqueueMessages(transaction: Transaction, account: Account, peerId: PeerId, messageNamespace = Namespaces.Message.ScheduledLocal effectiveTimestamp = attribute.scheduleTime } + } else if attribute is OutgoingQuickReplyMessageAttribute { + messageNamespace = Namespaces.Message.QuickReplyLocal + effectiveTimestamp = 0 } else if let attribute = attribute as? ReplyMessageAttribute { if let threadMessageId = attribute.threadMessageId { threadId = Int64(threadMessageId.id) @@ -946,6 +968,9 @@ func enqueueMessages(transaction: Transaction, account: Account, peerId: PeerId, if messageNamespace != Namespaces.Message.ScheduledLocal { attributes.removeAll(where: { $0 is OutgoingScheduleInfoMessageAttribute }) } + if messageNamespace != Namespaces.Message.QuickReplyLocal { + attributes.removeAll(where: { $0 is OutgoingQuickReplyMessageAttribute }) + } let (tags, globalTags) = tagsForStoreMessage(incoming: false, attributes: attributes, media: sourceMessage.media, textEntities: entitiesAttribute?.entities, isPinned: false) diff --git a/submodules/TelegramCore/Sources/PendingMessages/RequestEditMessage.swift b/submodules/TelegramCore/Sources/PendingMessages/RequestEditMessage.swift index 2532a673a46..87447920860 100644 --- a/submodules/TelegramCore/Sources/PendingMessages/RequestEditMessage.swift +++ b/submodules/TelegramCore/Sources/PendingMessages/RequestEditMessage.swift @@ -174,7 +174,13 @@ private func requestEditMessageInternal(accountPeerId: PeerId, postbox: Postbox, } } - return network.request(Api.functions.messages.editMessage(flags: flags, peer: inputPeer, id: messageId.id, message: text, media: inputMedia, replyMarkup: nil, entities: apiEntities, scheduleDate: effectiveScheduleTime)) + var quickReplyShortcutId: Int32? + if messageId.namespace == Namespaces.Message.QuickReplyCloud { + quickReplyShortcutId = Int32(clamping: message.threadId ?? 0) + flags |= Int32(1 << 17) + } + + return network.request(Api.functions.messages.editMessage(flags: flags, peer: inputPeer, id: messageId.id, message: text, media: inputMedia, replyMarkup: nil, entities: apiEntities, scheduleDate: effectiveScheduleTime, quickReplyShortcutId: quickReplyShortcutId)) |> map { result -> Api.Updates? in return result } @@ -304,7 +310,7 @@ func _internal_requestEditLiveLocation(postbox: Postbox, network: Network, state inputMedia = .inputMediaGeoLive(flags: 1 << 0, geoPoint: .inputGeoPoint(flags: 0, lat: media.latitude, long: media.longitude, accuracyRadius: nil), heading: nil, period: nil, proximityNotificationRadius: nil) } - return network.request(Api.functions.messages.editMessage(flags: 1 << 14, peer: inputPeer, id: messageId.id, message: nil, media: inputMedia, replyMarkup: nil, entities: nil, scheduleDate: nil)) + return network.request(Api.functions.messages.editMessage(flags: 1 << 14, peer: inputPeer, id: messageId.id, message: nil, media: inputMedia, replyMarkup: nil, entities: nil, scheduleDate: nil, quickReplyShortcutId: nil)) |> map(Optional.init) |> `catch` { _ -> Signal in return .single(nil) diff --git a/submodules/TelegramCore/Sources/PendingMessages/StandaloneSendMessage.swift b/submodules/TelegramCore/Sources/PendingMessages/StandaloneSendMessage.swift index 5496447f8c9..bfeb8299480 100644 --- a/submodules/TelegramCore/Sources/PendingMessages/StandaloneSendMessage.swift +++ b/submodules/TelegramCore/Sources/PendingMessages/StandaloneSendMessage.swift @@ -410,7 +410,7 @@ private func sendUploadedMessageContent( } } - sendMessageRequest = network.requestWithAdditionalInfo(Api.functions.messages.sendMessage(flags: flags, peer: inputPeer, replyTo: replyTo, message: text, randomId: uniqueId, replyMarkup: nil, entities: messageEntities, scheduleDate: scheduleTime, sendAs: sendAsInputPeer), info: .acknowledgement, tag: dependencyTag) + sendMessageRequest = network.requestWithAdditionalInfo(Api.functions.messages.sendMessage(flags: flags, peer: inputPeer, replyTo: replyTo, message: text, randomId: uniqueId, replyMarkup: nil, entities: messageEntities, scheduleDate: scheduleTime, sendAs: sendAsInputPeer, quickReplyShortcut: nil), info: .acknowledgement, tag: dependencyTag) case let .media(inputMedia, text): if bubbleUpEmojiOrStickersets { flags |= Int32(1 << 15) @@ -432,7 +432,7 @@ private func sendUploadedMessageContent( } } - sendMessageRequest = network.request(Api.functions.messages.sendMedia(flags: flags, peer: inputPeer, replyTo: replyTo, media: inputMedia, message: text, randomId: uniqueId, replyMarkup: nil, entities: messageEntities, scheduleDate: scheduleTime, sendAs: sendAsInputPeer), tag: dependencyTag) + sendMessageRequest = network.request(Api.functions.messages.sendMedia(flags: flags, peer: inputPeer, replyTo: replyTo, media: inputMedia, message: text, randomId: uniqueId, replyMarkup: nil, entities: messageEntities, scheduleDate: scheduleTime, sendAs: sendAsInputPeer, quickReplyShortcut: nil), tag: dependencyTag) |> map(NetworkRequestResult.result) case let .forward(sourceInfo): var topMsgId: Int32? @@ -442,7 +442,7 @@ private func sendUploadedMessageContent( } if let forwardSourceInfoAttribute = forwardSourceInfoAttribute, let sourcePeer = transaction.getPeer(forwardSourceInfoAttribute.messageId.peerId), let sourceInputPeer = apiInputPeer(sourcePeer) { - sendMessageRequest = network.request(Api.functions.messages.forwardMessages(flags: flags, fromPeer: sourceInputPeer, id: [sourceInfo.messageId.id], randomId: [uniqueId], toPeer: inputPeer, topMsgId: topMsgId, scheduleDate: scheduleTime, sendAs: sendAsInputPeer), tag: dependencyTag) + sendMessageRequest = network.request(Api.functions.messages.forwardMessages(flags: flags, fromPeer: sourceInputPeer, id: [sourceInfo.messageId.id], randomId: [uniqueId], toPeer: inputPeer, topMsgId: topMsgId, scheduleDate: scheduleTime, sendAs: sendAsInputPeer, quickReplyShortcut: nil), tag: dependencyTag) |> map(NetworkRequestResult.result) } else { sendMessageRequest = .fail(MTRpcError(errorCode: 400, errorDescription: "internal")) @@ -468,7 +468,7 @@ private func sendUploadedMessageContent( } } - sendMessageRequest = network.request(Api.functions.messages.sendInlineBotResult(flags: flags, peer: inputPeer, replyTo: replyTo, randomId: uniqueId, queryId: chatContextResult.queryId, id: chatContextResult.id, scheduleDate: scheduleTime, sendAs: sendAsInputPeer)) + sendMessageRequest = network.request(Api.functions.messages.sendInlineBotResult(flags: flags, peer: inputPeer, replyTo: replyTo, randomId: uniqueId, queryId: chatContextResult.queryId, id: chatContextResult.id, scheduleDate: scheduleTime, sendAs: sendAsInputPeer, quickReplyShortcut: nil)) |> map(NetworkRequestResult.result) case .messageScreenshot: let replyTo: Api.InputReplyTo @@ -633,7 +633,7 @@ private func sendMessageContent(account: Account, peerId: PeerId, attributes: [M } } - sendMessageRequest = account.network.request(Api.functions.messages.sendMessage(flags: flags, peer: inputPeer, replyTo: replyTo, message: text, randomId: uniqueId, replyMarkup: nil, entities: messageEntities, scheduleDate: scheduleTime, sendAs: sendAsInputPeer)) + sendMessageRequest = account.network.request(Api.functions.messages.sendMessage(flags: flags, peer: inputPeer, replyTo: replyTo, message: text, randomId: uniqueId, replyMarkup: nil, entities: messageEntities, scheduleDate: scheduleTime, sendAs: sendAsInputPeer, quickReplyShortcut: nil)) |> `catch` { _ -> Signal in return .complete() } @@ -651,7 +651,7 @@ private func sendMessageContent(account: Account, peerId: PeerId, attributes: [M } } - sendMessageRequest = account.network.request(Api.functions.messages.sendMedia(flags: flags, peer: inputPeer, replyTo: replyTo, media: inputMedia, message: text, randomId: uniqueId, replyMarkup: nil, entities: messageEntities, scheduleDate: scheduleTime, sendAs: sendAsInputPeer)) + sendMessageRequest = account.network.request(Api.functions.messages.sendMedia(flags: flags, peer: inputPeer, replyTo: replyTo, media: inputMedia, message: text, randomId: uniqueId, replyMarkup: nil, entities: messageEntities, scheduleDate: scheduleTime, sendAs: sendAsInputPeer, quickReplyShortcut: nil)) |> `catch` { _ -> Signal in return .complete() } diff --git a/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift b/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift index a68c802ace2..7dfad231f59 100644 --- a/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift +++ b/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift @@ -1658,12 +1658,26 @@ private func finalStateWithUpdatesAndServerTime(accountPeerId: PeerId, postbox: if let message = StoreMessage(apiMessage: apiMessage, accountPeerId: accountPeerId, peerIsForum: peerIsForum, namespace: Namespaces.Message.ScheduledCloud) { updatedState.addScheduledMessages([message]) } + case let .updateQuickReplyMessage(apiMessage): + var peerIsForum = false + if let peerId = apiMessage.peerId { + peerIsForum = updatedState.isPeerForum(peerId: peerId) + } + if let message = StoreMessage(apiMessage: apiMessage, accountPeerId: accountPeerId, peerIsForum: peerIsForum, namespace: Namespaces.Message.QuickReplyCloud) { + updatedState.addQuickReplyMessages([message]) + } case let .updateDeleteScheduledMessages(peer, messages): var messageIds: [MessageId] = [] for message in messages { messageIds.append(MessageId(peerId: peer.peerId, namespace: Namespaces.Message.ScheduledCloud, id: message)) } updatedState.deleteMessages(messageIds) + case let .updateDeleteQuickReplyMessages(_, messages): + var messageIds: [MessageId] = [] + for message in messages { + messageIds.append(MessageId(peerId: accountPeerId, namespace: Namespaces.Message.QuickReplyCloud, id: message)) + } + updatedState.deleteMessages(messageIds) case let .updateTheme(theme): updatedState.updateTheme(TelegramTheme(apiTheme: theme)) case let .updateMessageID(id, randomId): @@ -3250,6 +3264,7 @@ private func optimizedOperations(_ operations: [AccountStateMutationOperation]) var currentAddMessages: OptimizeAddMessagesState? var currentAddScheduledMessages: OptimizeAddMessagesState? + var currentAddQuickReplyMessages: OptimizeAddMessagesState? for operation in operations { switch operation { case .DeleteMessages, .DeleteMessagesWithGlobalIds, .EditMessage, .UpdateMessagePoll, .UpdateMessageReactions, .UpdateMedia, .MergeApiChats, .MergeApiUsers, .MergePeerPresences, .UpdatePeer, .ReadInbox, .ReadOutbox, .ReadGroupFeedInbox, .ResetReadState, .ResetIncomingReadState, .UpdatePeerChatUnreadMark, .ResetMessageTagSummary, .UpdateNotificationSettings, .UpdateGlobalNotificationSettings, .UpdateSecretChat, .AddSecretMessages, .ReadSecretOutbox, .AddPeerInputActivity, .UpdateCachedPeerData, .UpdatePinnedItemIds, .UpdatePinnedSavedItemIds, .UpdatePinnedTopic, .UpdatePinnedTopicOrder, .ReadMessageContents, .UpdateMessageImpressionCount, .UpdateMessageForwardsCount, .UpdateInstalledStickerPacks, .UpdateRecentGifs, .UpdateChatInputState, .UpdateCall, .AddCallSignalingData, .UpdateLangPack, .UpdateMinAvailableMessage, .UpdateIsContact, .UpdatePeerChatInclusion, .UpdatePeersNearby, .UpdateTheme, .SyncChatListFilters, .UpdateChatListFilter, .UpdateChatListFilterOrder, .UpdateReadThread, .UpdateMessagesPinned, .UpdateGroupCallParticipants, .UpdateGroupCall, .UpdateAutoremoveTimeout, .UpdateAttachMenuBots, .UpdateAudioTranscription, .UpdateConfig, .UpdateExtendedMedia, .ResetForumTopic, .UpdateStory, .UpdateReadStories, .UpdateStoryStealthMode, .UpdateStorySentReaction, .UpdateNewAuthorization, .UpdateWallpaper: @@ -3259,6 +3274,9 @@ private func optimizedOperations(_ operations: [AccountStateMutationOperation]) if let currentAddScheduledMessages = currentAddScheduledMessages, !currentAddScheduledMessages.messages.isEmpty { result.append(.AddScheduledMessages(currentAddScheduledMessages.messages)) } + if let currentAddQuickReplyMessages = currentAddQuickReplyMessages, !currentAddQuickReplyMessages.messages.isEmpty { + result.append(.AddQuickReplyMessages(currentAddQuickReplyMessages.messages)) + } currentAddMessages = nil result.append(operation) case let .UpdateState(state): @@ -3284,6 +3302,12 @@ private func optimizedOperations(_ operations: [AccountStateMutationOperation]) } else { currentAddScheduledMessages = OptimizeAddMessagesState(messages: messages, location: .Random) } + case let .AddQuickReplyMessages(messages): + if let currentAddQuickReplyMessages = currentAddQuickReplyMessages { + currentAddQuickReplyMessages.messages.append(contentsOf: messages) + } else { + currentAddQuickReplyMessages = OptimizeAddMessagesState(messages: messages, location: .Random) + } } } if let currentAddMessages = currentAddMessages, !currentAddMessages.messages.isEmpty { @@ -3294,6 +3318,10 @@ private func optimizedOperations(_ operations: [AccountStateMutationOperation]) result.append(.AddScheduledMessages(currentAddScheduledMessages.messages)) } + if let currentAddQuickReplyMessages = currentAddQuickReplyMessages, !currentAddQuickReplyMessages.messages.isEmpty { + result.append(.AddQuickReplyMessages(currentAddQuickReplyMessages.messages)) + } + if let updatedState = updatedState { result.append(.UpdateState(updatedState)) } @@ -3745,6 +3773,16 @@ func replayFinalState( let _ = transaction.addMessages(messages, location: .Random) } } + case let .AddQuickReplyMessages(messages): + for message in messages { + if case let .Id(id) = message.id, let _ = transaction.getMessage(id) { + transaction.updateMessage(id) { _ -> PostboxUpdateMessage in + return .update(message) + } + } else { + let _ = transaction.addMessages(messages, location: .Random) + } + } case let .DeleteMessagesWithGlobalIds(ids): var resourceIds: [MediaResourceId] = [] transaction.deleteMessagesWithGlobalIds(ids, forEachMedia: { media in diff --git a/submodules/TelegramCore/Sources/State/AccountViewTracker.swift b/submodules/TelegramCore/Sources/State/AccountViewTracker.swift index d6aff342b5e..b82d15f6666 100644 --- a/submodules/TelegramCore/Sources/State/AccountViewTracker.swift +++ b/submodules/TelegramCore/Sources/State/AccountViewTracker.swift @@ -61,16 +61,30 @@ private func pollMessages(entries: [MessageHistoryEntry]) -> (Set, [M return (messageIds, messages) } -private func fetchWebpage(account: Account, messageId: MessageId) -> Signal { +private func fetchWebpage(account: Account, messageId: MessageId, threadId: Int64?) -> Signal { let accountPeerId = account.peerId return account.postbox.loadedPeerWithId(messageId.peerId) |> take(1) |> mapToSignal { peer in if let inputPeer = apiInputPeer(peer) { - let isScheduledMessage = Namespaces.Message.allScheduled.contains(messageId.namespace) + let targetMessageNamespace: MessageId.Namespace + if Namespaces.Message.allScheduled.contains(messageId.namespace) { + targetMessageNamespace = Namespaces.Message.ScheduledCloud + } else if Namespaces.Message.allQuickReply.contains(messageId.namespace) { + targetMessageNamespace = Namespaces.Message.QuickReplyCloud + } else { + targetMessageNamespace = Namespaces.Message.Cloud + } + let messages: Signal - if isScheduledMessage { + if Namespaces.Message.allScheduled.contains(messageId.namespace) { messages = account.network.request(Api.functions.messages.getScheduledMessages(peer: inputPeer, id: [messageId.id])) + } else if Namespaces.Message.allQuickReply.contains(messageId.namespace) { + if let threadId { + messages = account.network.request(Api.functions.messages.getQuickReplyMessages(flags: 1 << 0, shortcutId: Int32(clamping: threadId), id: [messageId.id], hash: 0)) + } else { + messages = .never() + } } else { switch inputPeer { case let .inputPeerChannel(channelId, accessHash): @@ -109,7 +123,7 @@ private func fetchWebpage(account: Account, messageId: MessageId) -> Signal() - private var updatedUnsupportedMediaMessageIdsAndTimestamps: [MessageId: Int32] = [:] + private var updatedUnsupportedMediaMessageIdsAndTimestamps: [MessageAndThreadId: Int32] = [:] private var refreshSecretChatMediaMessageIdsAndTimestamps: [MessageId: Int32] = [:] private var refreshStoriesForMessageIdsAndTimestamps: [MessageId: Int32] = [:] private var nextUpdatedUnsupportedMediaDisposableId: Int32 = 0 @@ -334,6 +348,9 @@ public final class AccountViewTracker { var resetPeerHoleManagement: ((PeerId) -> Void)? + private var quickRepliesUpdateDisposable: Disposable? + private var quickRepliesUpdateTimestamp: Double = 0.0 + init(account: Account) { self.account = account self.accountPeerId = account.peerId @@ -359,6 +376,7 @@ public final class AccountViewTracker { self.updatedViewCountDisposables.dispose() self.updatedReactionsDisposables.dispose() self.externallyUpdatedPeerIdDisposable.dispose() + self.quickRepliesUpdateDisposable?.dispose() } func reset() { @@ -367,7 +385,7 @@ public final class AccountViewTracker { } } - private func updatePendingWebpages(viewId: Int32, messageIds: Set, localWebpages: [MessageId: (MediaId, String)]) { + private func updatePendingWebpages(viewId: Int32, threadId: Int64?, messageIds: Set, localWebpages: [MessageId: (MediaId, String)]) { self.queue.async { var addedMessageIds: [MessageId] = [] var removedMessageIds: [MessageId] = [] @@ -440,7 +458,7 @@ public final class AccountViewTracker { } }) } else if messageId.namespace == Namespaces.Message.Cloud { - self.webpageDisposables[messageId] = fetchWebpage(account: account, messageId: messageId).start(completed: { [weak self] in + self.webpageDisposables[messageId] = fetchWebpage(account: account, messageId: messageId, threadId: threadId).start(completed: { [weak self] in if let strongSelf = self { strongSelf.queue.async { strongSelf.webpageDisposables.removeValue(forKey: messageId) @@ -1009,9 +1027,9 @@ public final class AccountViewTracker { } } - public func updateUnsupportedMediaForMessageIds(messageIds: Set) { + public func updateUnsupportedMediaForMessageIds(messageIds: Set) { self.queue.async { - var addedMessageIds: [MessageId] = [] + var addedMessageIds: [MessageAndThreadId] = [] let timestamp = Int32(CFAbsoluteTimeGetCurrent()) for messageId in messageIds { let messageTimestamp = self.updatedUnsupportedMediaMessageIdsAndTimestamps[messageId] @@ -1021,14 +1039,14 @@ public final class AccountViewTracker { } } if !addedMessageIds.isEmpty { - for (peerId, messageIds) in messagesIdsGroupedByPeerId(Set(addedMessageIds)) { + for (peerIdAndThreadId, messageIds) in messagesIdsGroupedByPeerId(Set(addedMessageIds)) { let disposableId = self.nextUpdatedUnsupportedMediaDisposableId self.nextUpdatedUnsupportedMediaDisposableId += 1 if let account = self.account { let accountPeerId = account.peerId let signal = account.postbox.transaction { transaction -> Peer? in - if let peer = transaction.getPeer(peerId) { + if let peer = transaction.getPeer(peerIdAndThreadId.peerId) { return peer } else { return nil @@ -1043,9 +1061,15 @@ public final class AccountViewTracker { if let inputPeer = apiInputPeer(peer) { fetchSignal = account.network.request(Api.functions.messages.getScheduledMessages(peer: inputPeer, id: messageIds.map { $0.id })) } - } else if peerId.namespace == Namespaces.Peer.CloudUser || peerId.namespace == Namespaces.Peer.CloudGroup { + } else if let messageId = messageIds.first, messageId.namespace == Namespaces.Message.QuickReplyCloud { + if let threadId = peerIdAndThreadId.threadId { + fetchSignal = account.network.request(Api.functions.messages.getQuickReplyMessages(flags: 1 << 0, shortcutId: Int32(clamping: threadId), id: messageIds.map { $0.id }, hash: 0)) + } else { + fetchSignal = .never() + } + } else if peerIdAndThreadId.peerId.namespace == Namespaces.Peer.CloudUser || peerIdAndThreadId.peerId.namespace == Namespaces.Peer.CloudGroup { fetchSignal = account.network.request(Api.functions.messages.getMessages(id: messageIds.map { Api.InputMessage.inputMessageID(id: $0.id) })) - } else if peerId.namespace == Namespaces.Peer.CloudChannel { + } else if peerIdAndThreadId.peerId.namespace == Namespaces.Peer.CloudChannel { if let inputChannel = apiInputChannel(peer) { fetchSignal = account.network.request(Api.functions.channels.getMessages(channel: inputChannel, id: messageIds.map { Api.InputMessage.inputMessageID(id: $0.id) })) } @@ -1444,7 +1468,7 @@ public final class AccountViewTracker { if i < slice.count { let value = result[i] transaction.updatePeerCachedData(peerIds: Set([slice[i].0]), update: { _, cachedData in - var cachedData = cachedData as? CachedUserData ?? CachedUserData(about: nil, botInfo: nil, editableBotInfo: nil, peerStatusSettings: nil, pinnedMessageId: nil, isBlocked: false, commonGroupCount: 0, voiceCallsAvailable: true, videoCallsAvailable: true, callsPrivate: true, canPinMessages: true, hasScheduledMessages: true, autoremoveTimeout: .unknown, themeEmoticon: nil, photo: .unknown, personalPhoto: .unknown, fallbackPhoto: .unknown, premiumGiftOptions: [], voiceMessagesAvailable: true, wallpaper: nil, flags: []) + var cachedData = cachedData as? CachedUserData ?? CachedUserData(about: nil, botInfo: nil, editableBotInfo: nil, peerStatusSettings: nil, pinnedMessageId: nil, isBlocked: false, commonGroupCount: 0, voiceCallsAvailable: true, videoCallsAvailable: true, callsPrivate: true, canPinMessages: true, hasScheduledMessages: true, autoremoveTimeout: .unknown, themeEmoticon: nil, photo: .unknown, personalPhoto: .unknown, fallbackPhoto: .unknown, premiumGiftOptions: [], voiceMessagesAvailable: true, wallpaper: nil, flags: [], businessHours: nil, businessLocation: nil, greetingMessage: nil, awayMessage: nil, connectedBot: nil) var flags = cachedData.flags if case .boolTrue = value { flags.insert(.premiumRequired) @@ -1815,7 +1839,7 @@ public final class AccountViewTracker { if let strongSelf = self { strongSelf.queue.async { let (messageIds, localWebpages) = pendingWebpages(entries: next.0.entries) - strongSelf.updatePendingWebpages(viewId: viewId, messageIds: messageIds, localWebpages: localWebpages) + strongSelf.updatePendingWebpages(viewId: viewId, threadId: chatLocation.threadId, messageIds: messageIds, localWebpages: localWebpages) let (pollMessageIds, pollMessageDict) = pollMessages(entries: next.0.entries) strongSelf.updatePolls(viewId: viewId, messageIds: pollMessageIds, messages: pollMessageDict) if case let .peer(peerId, _) = chatLocation, peerId.namespace == Namespaces.Peer.CloudChannel { @@ -1828,7 +1852,7 @@ public final class AccountViewTracker { }, disposed: { [weak self] viewId in if let strongSelf = self { strongSelf.queue.async { - strongSelf.updatePendingWebpages(viewId: viewId, messageIds: [], localWebpages: [:]) + strongSelf.updatePendingWebpages(viewId: viewId, threadId: chatLocation.threadId, messageIds: [], localWebpages: [:]) strongSelf.updatePolls(viewId: viewId, messageIds: [], messages: [:]) switch chatLocation { case let .peer(peerId, _): @@ -1839,7 +1863,7 @@ public final class AccountViewTracker { if peerId.namespace == Namespaces.Peer.CloudChannel { strongSelf.historyViewStateValidationContexts.updateView(id: viewId, view: nil, location: chatLocation) } - case .feed: + case .customChatContents: break } } @@ -1852,7 +1876,7 @@ public final class AccountViewTracker { peerId = peerIdValue case let .thread(peerIdValue, _, _): peerId = peerIdValue - case .feed: + case .customChatContents: peerId = nil } if let peerId = peerId, peerId.namespace == Namespaces.Peer.CloudChannel { @@ -1945,14 +1969,14 @@ public final class AccountViewTracker { if let strongSelf = self { strongSelf.queue.async { let (messageIds, localWebpages) = pendingWebpages(entries: next.0.entries) - strongSelf.updatePendingWebpages(viewId: viewId, messageIds: messageIds, localWebpages: localWebpages) + strongSelf.updatePendingWebpages(viewId: viewId, threadId: chatLocation.threadId, messageIds: messageIds, localWebpages: localWebpages) strongSelf.historyViewStateValidationContexts.updateView(id: viewId, view: next.0, location: chatLocation) } } }, disposed: { [weak self] viewId in if let strongSelf = self { strongSelf.queue.async { - strongSelf.updatePendingWebpages(viewId: viewId, messageIds: [], localWebpages: [:]) + strongSelf.updatePendingWebpages(viewId: viewId, threadId: chatLocation.threadId, messageIds: [], localWebpages: [:]) strongSelf.historyViewStateValidationContexts.updateView(id: viewId, view: nil, location: nil) } } @@ -1962,6 +1986,65 @@ public final class AccountViewTracker { } } + public func quickReplyMessagesViewForLocation(quickReplyId: Int32, additionalData: [AdditionalMessageHistoryViewData] = []) -> Signal<(MessageHistoryView, ViewUpdateType, InitialMessageHistoryData?), NoError> { + guard let account = self.account else { + return .never() + } + let chatLocation: ChatLocationInput = .peer(peerId: account.peerId, threadId: Int64(quickReplyId)) + let signal = account.postbox.aroundMessageHistoryViewForLocation(chatLocation, anchor: .upperBound, ignoreMessagesInTimestampRange: nil, count: 200, fixedCombinedReadStates: nil, topTaggedMessageIdNamespaces: [], tag: nil, appendMessagesFromTheSameGroup: false, namespaces: .just(Namespaces.Message.allQuickReply), orderStatistics: [], additionalData: additionalData) + return withState(signal, { [weak self] () -> Int32 in + if let strongSelf = self { + return OSAtomicIncrement32(&strongSelf.nextViewId) + } else { + return -1 + } + }, next: { [weak self] next, viewId in + if let strongSelf = self { + strongSelf.queue.async { + let (messageIds, localWebpages) = pendingWebpages(entries: next.0.entries) + strongSelf.updatePendingWebpages(viewId: viewId, threadId: Int64(quickReplyId), messageIds: messageIds, localWebpages: localWebpages) + strongSelf.historyViewStateValidationContexts.updateView(id: viewId, view: next.0, location: chatLocation) + } + } + }, disposed: { [weak self] viewId in + if let strongSelf = self { + strongSelf.queue.async { + strongSelf.updatePendingWebpages(viewId: viewId, threadId: Int64(quickReplyId), messageIds: [], localWebpages: [:]) + strongSelf.historyViewStateValidationContexts.updateView(id: viewId, view: nil, location: nil) + } + } + }) + } + + public func pendingQuickReplyMessagesViewForLocation(shortcut: String) -> Signal<(MessageHistoryView, ViewUpdateType, InitialMessageHistoryData?), NoError> { + guard let account = self.account else { + return .never() + } + let chatLocation: ChatLocationInput = .peer(peerId: account.peerId, threadId: nil) + let signal = account.postbox.aroundMessageHistoryViewForLocation(chatLocation, anchor: .upperBound, ignoreMessagesInTimestampRange: nil, count: 200, fixedCombinedReadStates: nil, topTaggedMessageIdNamespaces: [], tag: nil, appendMessagesFromTheSameGroup: false, namespaces: .just([Namespaces.Message.QuickReplyLocal]), orderStatistics: [], additionalData: []) + |> map { view, update, initialData in + var entries: [MessageHistoryEntry] = [] + for entry in view.entries { + var matches = false + inner: for attribute in entry.message.attributes { + if let attribute = attribute as? OutgoingQuickReplyMessageAttribute { + if attribute.shortcut == shortcut { + matches = true + } + break inner + } + } + if matches { + entries.append(entry) + } + } + let mappedView = MessageHistoryView(tag: nil, namespaces: .just([Namespaces.Message.QuickReplyLocal]), entries: entries, holeEarlier: false, holeLater: false, isLoading: false) + + return (mappedView, update, initialData) + } + return signal + } + public func aroundMessageOfInterestHistoryViewForLocation(_ chatLocation: ChatLocationInput, ignoreMessagesInTimestampRange: ClosedRange? = nil, count: Int, tag: HistoryViewInputTag? = nil, appendMessagesFromTheSameGroup: Bool = false, orderStatistics: MessageHistoryViewOrderStatistics = [], additionalData: [AdditionalMessageHistoryViewData] = [], useRootInterfaceStateForThread: Bool = false) -> Signal<(MessageHistoryView, ViewUpdateType, InitialMessageHistoryData?), NoError> { if let account = self.account { let signal: Signal<(MessageHistoryView, ViewUpdateType, InitialMessageHistoryData?), NoError> @@ -1994,7 +2077,7 @@ public final class AccountViewTracker { topTaggedMessageIdNamespaces: [], tag: tag, appendMessagesFromTheSameGroup: false, - namespaces: .not(Namespaces.Message.allScheduled), + namespaces: .not(Namespaces.Message.allNonRegular), orderStatistics: orderStatistics, additionalData: wrappedHistoryViewAdditionalData(chatLocation: chatLocation, additionalData: additionalData), useRootInterfaceStateForThread: useRootInterfaceStateForThread @@ -2020,7 +2103,7 @@ public final class AccountViewTracker { topTaggedMessageIdNamespaces: [], tag: tag, appendMessagesFromTheSameGroup: false, - namespaces: .not(Namespaces.Message.allScheduled), + namespaces: .not(Namespaces.Message.allNonRegular), orderStatistics: orderStatistics, additionalData: wrappedHistoryViewAdditionalData(chatLocation: chatLocation, additionalData: additionalData), useRootInterfaceStateForThread: useRootInterfaceStateForThread @@ -2028,10 +2111,10 @@ public final class AccountViewTracker { } } - return account.postbox.aroundMessageOfInterestHistoryViewForChatLocation(chatLocation, ignoreMessagesInTimestampRange: ignoreMessagesInTimestampRange, count: count, topTaggedMessageIdNamespaces: [Namespaces.Message.Cloud], tag: tag, appendMessagesFromTheSameGroup: appendMessagesFromTheSameGroup, namespaces: .not(Namespaces.Message.allScheduled), orderStatistics: orderStatistics, customUnreadMessageId: nil, additionalData: wrappedHistoryViewAdditionalData(chatLocation: chatLocation, additionalData: additionalData), useRootInterfaceStateForThread: useRootInterfaceStateForThread) + return account.postbox.aroundMessageOfInterestHistoryViewForChatLocation(chatLocation, ignoreMessagesInTimestampRange: ignoreMessagesInTimestampRange, count: count, topTaggedMessageIdNamespaces: [Namespaces.Message.Cloud], tag: tag, appendMessagesFromTheSameGroup: appendMessagesFromTheSameGroup, namespaces: .not(Namespaces.Message.allNonRegular), orderStatistics: orderStatistics, customUnreadMessageId: nil, additionalData: wrappedHistoryViewAdditionalData(chatLocation: chatLocation, additionalData: additionalData), useRootInterfaceStateForThread: useRootInterfaceStateForThread) } } else { - signal = account.postbox.aroundMessageOfInterestHistoryViewForChatLocation(chatLocation, ignoreMessagesInTimestampRange: ignoreMessagesInTimestampRange, count: count, topTaggedMessageIdNamespaces: [Namespaces.Message.Cloud], tag: tag, appendMessagesFromTheSameGroup: appendMessagesFromTheSameGroup, namespaces: .not(Namespaces.Message.allScheduled), orderStatistics: orderStatistics, customUnreadMessageId: nil, additionalData: wrappedHistoryViewAdditionalData(chatLocation: chatLocation, additionalData: additionalData), useRootInterfaceStateForThread: useRootInterfaceStateForThread) + signal = account.postbox.aroundMessageOfInterestHistoryViewForChatLocation(chatLocation, ignoreMessagesInTimestampRange: ignoreMessagesInTimestampRange, count: count, topTaggedMessageIdNamespaces: [Namespaces.Message.Cloud], tag: tag, appendMessagesFromTheSameGroup: appendMessagesFromTheSameGroup, namespaces: .not(Namespaces.Message.allNonRegular), orderStatistics: orderStatistics, customUnreadMessageId: nil, additionalData: wrappedHistoryViewAdditionalData(chatLocation: chatLocation, additionalData: additionalData), useRootInterfaceStateForThread: useRootInterfaceStateForThread) } return wrappedMessageHistorySignal(chatLocation: chatLocation, signal: signal, fixedCombinedReadStates: nil, addHoleIfNeeded: true) } else { @@ -2041,7 +2124,7 @@ public final class AccountViewTracker { public func aroundIdMessageHistoryViewForLocation(_ chatLocation: ChatLocationInput, ignoreMessagesInTimestampRange: ClosedRange? = nil, count: Int, ignoreRelatedChats: Bool, messageId: MessageId, tag: HistoryViewInputTag? = nil, appendMessagesFromTheSameGroup: Bool = false, orderStatistics: MessageHistoryViewOrderStatistics = [], additionalData: [AdditionalMessageHistoryViewData] = [], useRootInterfaceStateForThread: Bool = false) -> Signal<(MessageHistoryView, ViewUpdateType, InitialMessageHistoryData?), NoError> { if let account = self.account { - let signal = account.postbox.aroundIdMessageHistoryViewForLocation(chatLocation, ignoreMessagesInTimestampRange: ignoreMessagesInTimestampRange, count: count, ignoreRelatedChats: ignoreRelatedChats, messageId: messageId, topTaggedMessageIdNamespaces: [Namespaces.Message.Cloud], tag: tag, appendMessagesFromTheSameGroup: appendMessagesFromTheSameGroup, namespaces: .not(Namespaces.Message.allScheduled), orderStatistics: orderStatistics, additionalData: wrappedHistoryViewAdditionalData(chatLocation: chatLocation, additionalData: additionalData), useRootInterfaceStateForThread: useRootInterfaceStateForThread) + let signal = account.postbox.aroundIdMessageHistoryViewForLocation(chatLocation, ignoreMessagesInTimestampRange: ignoreMessagesInTimestampRange, count: count, ignoreRelatedChats: ignoreRelatedChats, messageId: messageId, topTaggedMessageIdNamespaces: [Namespaces.Message.Cloud], tag: tag, appendMessagesFromTheSameGroup: appendMessagesFromTheSameGroup, namespaces: .not(Namespaces.Message.allNonRegular), orderStatistics: orderStatistics, additionalData: wrappedHistoryViewAdditionalData(chatLocation: chatLocation, additionalData: additionalData), useRootInterfaceStateForThread: useRootInterfaceStateForThread) return wrappedMessageHistorySignal(chatLocation: chatLocation, signal: signal, fixedCombinedReadStates: nil, addHoleIfNeeded: false) } else { return .never() @@ -2059,7 +2142,7 @@ public final class AccountViewTracker { case let .message(index): inputAnchor = .index(index) } - let signal = account.postbox.aroundMessageHistoryViewForLocation(chatLocation, anchor: inputAnchor, ignoreMessagesInTimestampRange: ignoreMessagesInTimestampRange, count: count, clipHoles: clipHoles, ignoreRelatedChats: ignoreRelatedChats, fixedCombinedReadStates: fixedCombinedReadStates, topTaggedMessageIdNamespaces: [Namespaces.Message.Cloud], tag: tag, appendMessagesFromTheSameGroup: appendMessagesFromTheSameGroup, namespaces: .not(Namespaces.Message.allScheduled), orderStatistics: orderStatistics, additionalData: wrappedHistoryViewAdditionalData(chatLocation: chatLocation, additionalData: additionalData), useRootInterfaceStateForThread: useRootInterfaceStateForThread) + let signal = account.postbox.aroundMessageHistoryViewForLocation(chatLocation, anchor: inputAnchor, ignoreMessagesInTimestampRange: ignoreMessagesInTimestampRange, count: count, clipHoles: clipHoles, ignoreRelatedChats: ignoreRelatedChats, fixedCombinedReadStates: fixedCombinedReadStates, topTaggedMessageIdNamespaces: [Namespaces.Message.Cloud], tag: tag, appendMessagesFromTheSameGroup: appendMessagesFromTheSameGroup, namespaces: .not(Namespaces.Message.allNonRegular), orderStatistics: orderStatistics, additionalData: wrappedHistoryViewAdditionalData(chatLocation: chatLocation, additionalData: additionalData), useRootInterfaceStateForThread: useRootInterfaceStateForThread) return wrappedMessageHistorySignal(chatLocation: chatLocation, signal: signal, fixedCombinedReadStates: fixedCombinedReadStates, addHoleIfNeeded: false) } else { return .never() @@ -2498,6 +2581,20 @@ public final class AccountViewTracker { } } } + + public func keepQuickRepliesApproximatelyUpdated() { + self.queue.async { + guard let account = self.account else { + return + } + let timestamp = CFAbsoluteTimeGetCurrent() + if self.quickRepliesUpdateTimestamp + 16 * 60 * 60 < timestamp { + self.quickRepliesUpdateTimestamp = timestamp + self.quickRepliesUpdateDisposable?.dispose() + self.quickRepliesUpdateDisposable = _internal_keepShortcutMessagesUpdated(account: account).startStrict() + } + } + } } public final class ExtractedChatListItemCachedData: Hashable { diff --git a/submodules/TelegramCore/Sources/State/ApplyUpdateMessage.swift b/submodules/TelegramCore/Sources/State/ApplyUpdateMessage.swift index 6bb8c766577..0a2a0b1fb43 100644 --- a/submodules/TelegramCore/Sources/State/ApplyUpdateMessage.swift +++ b/submodules/TelegramCore/Sources/State/ApplyUpdateMessage.swift @@ -96,7 +96,7 @@ func applyUpdateMessage(postbox: Postbox, stateManager: AccountStateManager, mes var updatedTimestamp: Int32? if let apiMessage = apiMessage { switch apiMessage { - case let .message(_, _, _, _, _, _, _, _, _, date, _, _, _, _, _, _, _, _, _, _, _, _, _): + case let .message(_, _, _, _, _, _, _, _, _, date, _, _, _, _, _, _, _, _, _, _, _, _, _, _): updatedTimestamp = date case .messageEmpty: break @@ -129,7 +129,9 @@ func applyUpdateMessage(postbox: Postbox, stateManager: AccountStateManager, mes let updatedId: MessageId if let messageId = messageId { var namespace: MessageId.Namespace = Namespaces.Message.Cloud - if let updatedTimestamp = updatedTimestamp { + if Namespaces.Message.allQuickReply.contains(message.id.namespace) { + namespace = Namespaces.Message.QuickReplyCloud + } else if let updatedTimestamp = updatedTimestamp { if message.scheduleTime != nil && message.scheduleTime == updatedTimestamp { namespace = Namespaces.Message.ScheduledCloud } @@ -141,21 +143,17 @@ func applyUpdateMessage(postbox: Postbox, stateManager: AccountStateManager, mes updatedId = currentMessage.id } - for attribute in currentMessage.attributes { - if let attribute = attribute as? OutgoingMessageInfoAttribute { - bubbleUpEmojiOrStickersets = attribute.bubbleUpEmojiOrStickersets - } - } - let media: [Media] var attributes: [MessageAttribute] let text: String let forwardInfo: StoreMessageForwardInfo? + let threadId: Int64? if let apiMessage = apiMessage, let apiMessagePeerId = apiMessage.peerId, let updatedMessage = StoreMessage(apiMessage: apiMessage, accountPeerId: accountPeerId, peerIsForum: transaction.getPeer(apiMessagePeerId)?.isForum ?? false) { media = updatedMessage.media attributes = updatedMessage.attributes text = updatedMessage.text forwardInfo = updatedMessage.forwardInfo + threadId = updatedMessage.threadId } else if case let .updateShortSentMessage(_, _, _, _, _, apiMedia, entities, ttlPeriod) = result { let (mediaValue, _, nonPremium, hasSpoiler, _) = textMediaAndExpirationTimerFromApiMedia(apiMedia, currentMessage.id.peerId) if let mediaValue = mediaValue { @@ -197,16 +195,36 @@ func applyUpdateMessage(postbox: Postbox, stateManager: AccountStateManager, mes } } } + if Namespaces.Message.allQuickReply.contains(message.id.namespace) { + for i in 0 ..< updatedAttributes.count { + if updatedAttributes[i] is OutgoingQuickReplyMessageAttribute { + updatedAttributes.remove(at: i) + break + } + } + } attributes = updatedAttributes text = currentMessage.text forwardInfo = currentMessage.forwardInfo.flatMap(StoreMessageForwardInfo.init) + threadId = currentMessage.threadId } else { media = currentMessage.media attributes = currentMessage.attributes text = currentMessage.text forwardInfo = currentMessage.forwardInfo.flatMap(StoreMessageForwardInfo.init) + threadId = currentMessage.threadId + } + + for attribute in currentMessage.attributes { + if let attribute = attribute as? OutgoingMessageInfoAttribute { + bubbleUpEmojiOrStickersets = attribute.bubbleUpEmojiOrStickersets + } else if let attribute = attribute as? OutgoingQuickReplyMessageAttribute { + if let threadId { + _internal_applySentQuickReplyMessage(transaction: transaction, shortcut: attribute.shortcut, quickReplyId: Int32(clamping: threadId)) + } + } } if let channelPts = channelPts { @@ -255,7 +273,7 @@ func applyUpdateMessage(postbox: Postbox, stateManager: AccountStateManager, mes let (tags, globalTags) = tagsForStoreMessage(incoming: currentMessage.flags.contains(.Incoming), attributes: attributes, media: media, textEntities: entitiesAttribute?.entities, isPinned: currentMessage.tags.contains(.pinned)) - if currentMessage.id.peerId.namespace == Namespaces.Peer.CloudChannel, !currentMessage.flags.contains(.Incoming), !Namespaces.Message.allScheduled.contains(currentMessage.id.namespace) { + if currentMessage.id.peerId.namespace == Namespaces.Peer.CloudChannel, !currentMessage.flags.contains(.Incoming), !Namespaces.Message.allNonRegular.contains(currentMessage.id.namespace) { let peerId = currentMessage.id.peerId if let peer = transaction.getPeer(peerId) { if let peer = peer as? TelegramChannel { @@ -344,7 +362,9 @@ func applyUpdateGroupMessages(postbox: Postbox, stateManager: AccountStateManage let updatedRawMessageIds = result.updatedRawMessageIds var namespace = Namespaces.Message.Cloud - if let message = messages.first, let apiMessage = result.messages.first, message.scheduleTime != nil && message.scheduleTime == apiMessage.timestamp { + if Namespaces.Message.allQuickReply.contains(messages[0].id.namespace) { + namespace = Namespaces.Message.QuickReplyCloud + } else if let message = messages.first, let apiMessage = result.messages.first, message.scheduleTime != nil && message.scheduleTime == apiMessage.timestamp { namespace = Namespaces.Message.ScheduledCloud } @@ -411,6 +431,16 @@ func applyUpdateGroupMessages(postbox: Postbox, stateManager: AccountStateManage var bubbleUpEmojiOrStickersets: [ItemCollectionId] = [] + if let (message, _, updatedMessage) = mapping.first { + for attribute in message.attributes { + if let attribute = attribute as? OutgoingQuickReplyMessageAttribute { + if let threadId = updatedMessage.threadId { + _internal_applySentQuickReplyMessage(transaction: transaction, shortcut: attribute.shortcut, quickReplyId: Int32(clamping: threadId)) + } + } + } + } + for (message, _, updatedMessage) in mapping { transaction.updateMessage(message.id, update: { currentMessage in let updatedId: MessageId diff --git a/submodules/TelegramCore/Sources/State/CloudChatRemoveMessagesOperation.swift b/submodules/TelegramCore/Sources/State/CloudChatRemoveMessagesOperation.swift index 58b9bd40edb..acf9a302614 100644 --- a/submodules/TelegramCore/Sources/State/CloudChatRemoveMessagesOperation.swift +++ b/submodules/TelegramCore/Sources/State/CloudChatRemoveMessagesOperation.swift @@ -1,7 +1,7 @@ import Postbox -func cloudChatAddRemoveMessagesOperation(transaction: Transaction, peerId: PeerId, messageIds: [MessageId], type: CloudChatRemoveMessagesType) { - transaction.operationLogAddEntry(peerId: peerId, tag: OperationLogTags.CloudChatRemoveMessages, tagLocalIndex: .automatic, tagMergedIndex: .automatic, contents: CloudChatRemoveMessagesOperation(messageIds: messageIds, type: type)) +func cloudChatAddRemoveMessagesOperation(transaction: Transaction, peerId: PeerId, threadId: Int64?, messageIds: [MessageId], type: CloudChatRemoveMessagesType) { + transaction.operationLogAddEntry(peerId: peerId, tag: OperationLogTags.CloudChatRemoveMessages, tagLocalIndex: .automatic, tagMergedIndex: .automatic, contents: CloudChatRemoveMessagesOperation(messageIds: messageIds, threadId: threadId, type: type)) } func cloudChatAddRemoveChatOperation(transaction: Transaction, peerId: PeerId, reportChatSpam: Bool, deleteGloballyIfPossible: Bool) { @@ -15,7 +15,26 @@ func cloudChatAddClearHistoryOperation(transaction: Transaction, peerId: PeerId, messageIds.append(message.id) return true } - cloudChatAddRemoveMessagesOperation(transaction: transaction, peerId: peerId, messageIds: messageIds, type: .forLocalPeer) + cloudChatAddRemoveMessagesOperation(transaction: transaction, peerId: peerId, threadId: threadId, messageIds: messageIds, type: .forLocalPeer) + } else if type == .quickReplyMessages { + var messageIds: [MessageId] = [] + transaction.withAllMessages(peerId: peerId, namespace: Namespaces.Message.QuickReplyCloud) { message -> Bool in + messageIds.append(message.id) + return true + } + cloudChatAddRemoveMessagesOperation(transaction: transaction, peerId: peerId, threadId: threadId, messageIds: messageIds, type: .forLocalPeer) + + let topMessageId: MessageId? + if let explicitTopMessageId = explicitTopMessageId { + topMessageId = explicitTopMessageId + } else { + topMessageId = transaction.getTopPeerMessageId(peerId: peerId, namespace: Namespaces.Message.QuickReplyCloud) + } + if let topMessageId = topMessageId { + transaction.operationLogAddEntry(peerId: peerId, tag: OperationLogTags.CloudChatRemoveMessages, tagLocalIndex: .automatic, tagMergedIndex: .automatic, contents: CloudChatClearHistoryOperation(peerId: peerId, topMessageId: topMessageId, threadId: threadId, minTimestamp: minTimestamp, maxTimestamp: maxTimestamp, type: type)) + } else if case .forEveryone = type { + transaction.operationLogAddEntry(peerId: peerId, tag: OperationLogTags.CloudChatRemoveMessages, tagLocalIndex: .automatic, tagMergedIndex: .automatic, contents: CloudChatClearHistoryOperation(peerId: peerId, topMessageId: MessageId(peerId: peerId, namespace: Namespaces.Message.Cloud, id: .max), threadId: threadId, minTimestamp: minTimestamp, maxTimestamp: maxTimestamp, type: type)) + } } else { let topMessageId: MessageId? if let explicitTopMessageId = explicitTopMessageId { diff --git a/submodules/TelegramCore/Sources/State/HistoryViewStateValidation.swift b/submodules/TelegramCore/Sources/State/HistoryViewStateValidation.swift index ddaea2d2c3e..4290ad04f4b 100644 --- a/submodules/TelegramCore/Sources/State/HistoryViewStateValidation.swift +++ b/submodules/TelegramCore/Sources/State/HistoryViewStateValidation.swift @@ -28,78 +28,60 @@ private final class HistoryStateValidationContext { private enum HistoryState { case channel(PeerId, ChannelState) - //case group(PeerGroupId, TelegramPeerGroupState) case scheduledMessages(PeerId) + case quickReplyMessages(PeerId, Int32) var hasInvalidationIndex: Bool { switch self { - case let .channel(_, state): - return state.invalidatedPts != nil - /*case let .group(_, state): - return state.invalidatedStateIndex != nil*/ - case .scheduledMessages: - return false + case let .channel(_, state): + return state.invalidatedPts != nil + case .scheduledMessages: + return false + case .quickReplyMessages: + return false } } func isMessageValid(_ message: Message) -> Bool { switch self { - case let .channel(_, state): - if let invalidatedPts = state.invalidatedPts { - var messagePts: Int32? - inner: for attribute in message.attributes { - if let attribute = attribute as? ChannelMessageStateVersionAttribute { - messagePts = attribute.pts - break inner - } - } - var requiresValidation = false - if let messagePts = messagePts { - if messagePts < invalidatedPts { - requiresValidation = true - } - } else { - requiresValidation = true + case let .channel(_, state): + if let invalidatedPts = state.invalidatedPts { + var messagePts: Int32? + inner: for attribute in message.attributes { + if let attribute = attribute as? ChannelMessageStateVersionAttribute { + messagePts = attribute.pts + break inner } - - return !requiresValidation - } else { - return true } - /*case let .group(_, state): - if let invalidatedStateIndex = state.invalidatedStateIndex { - var messageStateIndex: Int32? - inner: for attribute in message.attributes { - if let attribute = attribute as? PeerGroupMessageStateVersionAttribute { - messageStateIndex = attribute.stateIndex - break inner - } - } - var requiresValidation = false - if let messageStateIndex = messageStateIndex { - if messageStateIndex < invalidatedStateIndex { - requiresValidation = true - } - } else { + + var requiresValidation = false + if let messagePts = messagePts { + if messagePts < invalidatedPts { requiresValidation = true } - return !requiresValidation } else { - return true - }*/ - case .scheduledMessages: - return false + requiresValidation = true + } + + return !requiresValidation + } else { + return true + } + case .scheduledMessages: + return false + case .quickReplyMessages: + return false } } func matchesPeerId(_ peerId: PeerId) -> Bool { switch self { - case let .channel(statePeerId, _): - return statePeerId == peerId - /*case .group: - return true*/ - case let .scheduledMessages(statePeerId): - return statePeerId == peerId + case let .channel(statePeerId, _): + return statePeerId == peerId + case let .scheduledMessages(statePeerId): + return statePeerId == peerId + case let .quickReplyMessages(statePeerId, _): + return statePeerId == peerId } } } @@ -411,6 +393,35 @@ final class HistoryViewStateValidationContexts { })) } } + } else if view.namespaces.contains(Namespaces.Message.QuickReplyCloud) { + if let _ = self.contexts[id] { + } else if let location = location, case let .peer(peerId, threadId) = location { + guard let threadId else { + return + } + + let timestamp = self.network.context.globalTime() + if let previousTimestamp = self.previousPeerValidationTimestamps[peerId], timestamp < previousTimestamp + 60 { + } else { + self.previousPeerValidationTimestamps[peerId] = timestamp + + let context = HistoryStateValidationContext() + self.contexts[id] = context + + let disposable = MetaDisposable() + let batch = HistoryStateValidationBatch(disposable: disposable) + context.batch = batch + + let messages: [Message] = view.entries.map { $0.message }.filter { $0.id.namespace == Namespaces.Message.QuickReplyCloud } + + disposable.set((validateQuickReplyMessagesBatch(postbox: self.postbox, network: self.network, accountPeerId: peerId, tag: nil, messages: messages, historyState: .quickReplyMessages(peerId, Int32(clamping: threadId))) + |> deliverOn(self.queue)).start(completed: { [weak self] in + if let strongSelf = self, let context = strongSelf.contexts[id] { + context.batch = nil + } + })) + } + } } } } @@ -690,6 +701,53 @@ private func validateScheduledMessagesBatch(postbox: Postbox, network: Network, } |> switchToLatest } +private func validateQuickReplyMessagesBatch(postbox: Postbox, network: Network, accountPeerId: PeerId, tag: MessageTags?, messages: [Message], historyState: HistoryState) -> Signal { + return postbox.transaction { transaction -> Signal in + var signal: Signal + switch historyState { + case let .quickReplyMessages(peerId, shortcutId): + if let peer = transaction.getPeer(peerId) { + let hash = hashForScheduledMessages(messages) + signal = network.request(Api.functions.messages.getQuickReplyMessages(flags: 0, shortcutId: shortcutId, id: nil, hash: hash)) + |> map { result -> ValidatedMessages in + let messages: [Api.Message] + let chats: [Api.Chat] + let users: [Api.User] + + switch result { + case let .messages(messages: apiMessages, chats: apiChats, users: apiUsers): + messages = apiMessages + chats = apiChats + users = apiUsers + case let .messagesSlice(_, _, _, _, messages: apiMessages, chats: apiChats, users: apiUsers): + messages = apiMessages + chats = apiChats + users = apiUsers + case let .channelMessages(_, _, _, _, apiMessages, apiTopics, apiChats, apiUsers): + messages = apiMessages + let _ = apiTopics + chats = apiChats + users = apiUsers + case .messagesNotModified: + return .notModified + } + return .messages(peer, messages, chats, users, nil) + } + } else { + signal = .complete() + } + default: + signal = .complete() + } + var previous: [MessageId: Message] = [:] + for message in messages { + previous[message.id] = message + } + return validateBatch(postbox: postbox, network: network, transaction: transaction, accountPeerId: accountPeerId, tag: tag, historyState: historyState, signal: signal, previous: previous, messageNamespace: Namespaces.Message.QuickReplyCloud) + } + |> switchToLatest +} + private func validateBatch(postbox: Postbox, network: Network, transaction: Transaction, accountPeerId: PeerId, tag: MessageTags?, historyState: HistoryState, signal: Signal, previous: [MessageId: Message], messageNamespace: MessageId.Namespace) -> Signal { return signal |> map(Optional.init) diff --git a/submodules/TelegramCore/Sources/State/ManagedCloudChatRemoveMessagesOperations.swift b/submodules/TelegramCore/Sources/State/ManagedCloudChatRemoveMessagesOperations.swift index 1e4572414b7..18743c84504 100644 --- a/submodules/TelegramCore/Sources/State/ManagedCloudChatRemoveMessagesOperations.swift +++ b/submodules/TelegramCore/Sources/State/ManagedCloudChatRemoveMessagesOperations.swift @@ -127,10 +127,14 @@ func managedCloudChatRemoveMessagesOperations(postbox: Postbox, network: Network private func removeMessages(postbox: Postbox, network: Network, stateManager: AccountStateManager, peer: Peer, operation: CloudChatRemoveMessagesOperation) -> Signal { var isScheduled = false + var isQuickReply = false for id in operation.messageIds { if id.namespace == Namespaces.Message.ScheduledCloud { isScheduled = true break + } else if id.namespace == Namespaces.Message.QuickReplyCloud { + isQuickReply = true + break } } @@ -160,6 +164,32 @@ private func removeMessages(postbox: Postbox, network: Network, stateManager: Ac } else { return .complete() } + } else if isQuickReply { + if let threadId = operation.threadId { + var signal: Signal = .complete() + for s in stride(from: 0, to: operation.messageIds.count, by: 100) { + let ids = Array(operation.messageIds[s ..< min(s + 100, operation.messageIds.count)]) + let partSignal = network.request(Api.functions.messages.deleteQuickReplyMessages(shortcutId: Int32(clamping: threadId), id: ids.map(\.id))) + |> map { result -> Api.Updates? in + return result + } + |> `catch` { _ in + return .single(nil) + } + |> mapToSignal { updates -> Signal in + if let updates = updates { + stateManager.addUpdates(updates) + } + return .complete() + } + + signal = signal + |> then(partSignal) + } + return signal + } else { + return .complete() + } } else if peer.id.namespace == Namespaces.Peer.CloudChannel { if let inputChannel = apiInputChannel(peer) { var signal: Signal = .complete() @@ -329,7 +359,7 @@ private func removeChat(transaction: Transaction, postbox: Postbox, network: Net return requestClearHistory(postbox: postbox, network: network, stateManager: stateManager, inputPeer: inputPeer, maxId: operation.topMessageId?.id ?? Int32.max - 1, justClear: false, minTimestamp: nil, maxTimestamp: nil, type: operation.deleteGloballyIfPossible ? .forEveryone : .forLocalPeer) |> then(reportSignal) |> then(postbox.transaction { transaction -> Void in - _internal_clearHistory(transaction: transaction, mediaBox: postbox.mediaBox, peerId: peer.id, threadId: nil, namespaces: .not(Namespaces.Message.allScheduled)) + _internal_clearHistory(transaction: transaction, mediaBox: postbox.mediaBox, peerId: peer.id, threadId: nil, namespaces: .not(Namespaces.Message.allNonRegular)) }) } else { return .complete() @@ -386,6 +416,27 @@ private func requestClearHistory(postbox: Postbox, network: Network, stateManage private func _internal_clearHistory(transaction: Transaction, postbox: Postbox, network: Network, stateManager: AccountStateManager, peer: Peer, operation: CloudChatClearHistoryOperation) -> Signal { if peer.id.namespace == Namespaces.Peer.CloudGroup || peer.id.namespace == Namespaces.Peer.CloudUser { + if case .quickReplyMessages = operation.type { + guard let threadId = operation.threadId else { + return .complete() + } + + let signal = network.request(Api.functions.messages.deleteQuickReplyShortcut(shortcutId: Int32(clamping: threadId))) + |> map { result -> Api.Bool? in + return result + } + |> `catch` { _ -> Signal in + return .single(nil) + } + |> mapToSignal { result -> Signal in + return .fail(true) + } + return (signal |> restart) + |> `catch` { _ -> Signal in + return .complete() + } + } + if let inputPeer = apiInputPeer(peer) { if peer.id == stateManager.accountPeerId, let threadId = operation.threadId { guard let inputSubPeer = transaction.getPeer(PeerId(threadId)).flatMap(apiInputPeer) else { @@ -407,7 +458,7 @@ private func _internal_clearHistory(transaction: Transaction, postbox: Postbox, return result } |> `catch` { _ -> Signal in - return .fail(true) + return .single(nil) } |> mapToSignal { result -> Signal in if let result = result { diff --git a/submodules/TelegramCore/Sources/State/ManagedConsumePersonalMessagesActions.swift b/submodules/TelegramCore/Sources/State/ManagedConsumePersonalMessagesActions.swift index f253215b505..ace5e77447a 100644 --- a/submodules/TelegramCore/Sources/State/ManagedConsumePersonalMessagesActions.swift +++ b/submodules/TelegramCore/Sources/State/ManagedConsumePersonalMessagesActions.swift @@ -21,7 +21,7 @@ private func md5Hash(_ data: Data) -> Md5Hash { return Md5Hash(data: hashData) } -private func md5StringHash(_ string: String) -> UInt64 { +func md5StringHash(_ string: String) -> UInt64 { guard let data = string.data(using: .utf8) else { return 0 } diff --git a/submodules/TelegramCore/Sources/State/PendingMessageManager.swift b/submodules/TelegramCore/Sources/State/PendingMessageManager.swift index bab864f5a68..8ed18226d66 100644 --- a/submodules/TelegramCore/Sources/State/PendingMessageManager.swift +++ b/submodules/TelegramCore/Sources/State/PendingMessageManager.swift @@ -793,6 +793,7 @@ public final class PendingMessageManager { var replyToStoryId: StoryId? var scheduleTime: Int32? var sendAsPeerId: PeerId? + var quickReply: OutgoingQuickReplyMessageAttribute? var flags: Int32 = 0 @@ -821,6 +822,8 @@ public final class PendingMessageManager { hideCaptions = attribute.hideCaptions } else if let attribute = attribute as? SendAsMessageAttribute { sendAsPeerId = attribute.peerId + } else if let attribute = attribute as? OutgoingQuickReplyMessageAttribute { + quickReply = attribute } } @@ -871,6 +874,16 @@ public final class PendingMessageManager { topMsgId = Int32(clamping: threadId) } + var quickReplyShortcut: Api.InputQuickReplyShortcut? + if let quickReply { + if let threadId = messages[0].0.threadId { + quickReplyShortcut = .inputQuickReplyShortcutId(shortcutId: Int32(clamping: threadId)) + } else { + quickReplyShortcut = .inputQuickReplyShortcut(shortcut: quickReply.shortcut) + } + flags |= 1 << 17 + } + let forwardPeerIds = Set(forwardIds.map { $0.0.peerId }) if forwardPeerIds.count != 1 { assertionFailure() @@ -878,7 +891,7 @@ public final class PendingMessageManager { } else if let inputSourcePeerId = forwardPeerIds.first, let inputSourcePeer = transaction.getPeer(inputSourcePeerId).flatMap(apiInputPeer) { let dependencyTag = PendingMessageRequestDependencyTag(messageId: messages[0].0.id) - sendMessageRequest = network.request(Api.functions.messages.forwardMessages(flags: flags, fromPeer: inputSourcePeer, id: forwardIds.map { $0.0.id }, randomId: forwardIds.map { $0.1 }, toPeer: inputPeer, topMsgId: topMsgId, scheduleDate: scheduleTime, sendAs: sendAsInputPeer), tag: dependencyTag) + sendMessageRequest = network.request(Api.functions.messages.forwardMessages(flags: flags, fromPeer: inputSourcePeer, id: forwardIds.map { $0.0.id }, randomId: forwardIds.map { $0.1 }, toPeer: inputPeer, topMsgId: topMsgId, scheduleDate: scheduleTime, sendAs: sendAsInputPeer, quickReplyShortcut: quickReplyShortcut), tag: dependencyTag) } else { assertionFailure() sendMessageRequest = .fail(MTRpcError(errorCode: 400, errorDescription: "Invalid forward source")) @@ -993,7 +1006,17 @@ public final class PendingMessageManager { } } - sendMessageRequest = network.request(Api.functions.messages.sendMultiMedia(flags: flags, peer: inputPeer, replyTo: replyTo, multiMedia: singleMedias, scheduleDate: scheduleTime, sendAs: sendAsInputPeer)) + var quickReplyShortcut: Api.InputQuickReplyShortcut? + if let quickReply { + if let threadId = messages[0].0.threadId { + quickReplyShortcut = .inputQuickReplyShortcutId(shortcutId: Int32(clamping: threadId)) + } else { + quickReplyShortcut = .inputQuickReplyShortcut(shortcut: quickReply.shortcut) + } + flags |= 1 << 17 + } + + sendMessageRequest = network.request(Api.functions.messages.sendMultiMedia(flags: flags, peer: inputPeer, replyTo: replyTo, multiMedia: singleMedias, scheduleDate: scheduleTime, sendAs: sendAsInputPeer, quickReplyShortcut: quickReplyShortcut)) } return sendMessageRequest @@ -1168,6 +1191,7 @@ public final class PendingMessageManager { var scheduleTime: Int32? var sendAsPeerId: PeerId? var bubbleUpEmojiOrStickersets = false + var quickReply: OutgoingQuickReplyMessageAttribute? var flags: Int32 = 0 @@ -1202,6 +1226,8 @@ public final class PendingMessageManager { scheduleTime = attribute.scheduleTime } else if let attribute = attribute as? SendAsMessageAttribute { sendAsPeerId = attribute.peerId + } else if let attribute = attribute as? OutgoingQuickReplyMessageAttribute { + quickReply = attribute } } @@ -1291,7 +1317,17 @@ public final class PendingMessageManager { } } - sendMessageRequest = network.requestWithAdditionalInfo(Api.functions.messages.sendMessage(flags: flags, peer: inputPeer, replyTo: replyTo, message: message.text, randomId: uniqueId, replyMarkup: nil, entities: messageEntities, scheduleDate: scheduleTime, sendAs: sendAsInputPeer), info: .acknowledgement, tag: dependencyTag) + var quickReplyShortcut: Api.InputQuickReplyShortcut? + if let quickReply { + if let threadId = message.threadId { + quickReplyShortcut = .inputQuickReplyShortcutId(shortcutId: Int32(clamping: threadId)) + } else { + quickReplyShortcut = .inputQuickReplyShortcut(shortcut: quickReply.shortcut) + } + flags |= 1 << 17 + } + + sendMessageRequest = network.requestWithAdditionalInfo(Api.functions.messages.sendMessage(flags: flags, peer: inputPeer, replyTo: replyTo, message: message.text, randomId: uniqueId, replyMarkup: nil, entities: messageEntities, scheduleDate: scheduleTime, sendAs: sendAsInputPeer, quickReplyShortcut: quickReplyShortcut), info: .acknowledgement, tag: dependencyTag) case let .media(inputMedia, text): if bubbleUpEmojiOrStickersets { flags |= Int32(1 << 15) @@ -1355,8 +1391,18 @@ public final class PendingMessageManager { flags |= 1 << 16 } } + + var quickReplyShortcut: Api.InputQuickReplyShortcut? + if let quickReply { + if let threadId = message.threadId { + quickReplyShortcut = .inputQuickReplyShortcutId(shortcutId: Int32(clamping: threadId)) + } else { + quickReplyShortcut = .inputQuickReplyShortcut(shortcut: quickReply.shortcut) + } + flags |= 1 << 17 + } - sendMessageRequest = network.request(Api.functions.messages.sendMedia(flags: flags, peer: inputPeer, replyTo: replyTo, media: inputMedia, message: text, randomId: uniqueId, replyMarkup: nil, entities: messageEntities, scheduleDate: scheduleTime, sendAs: sendAsInputPeer), tag: dependencyTag) + sendMessageRequest = network.request(Api.functions.messages.sendMedia(flags: flags, peer: inputPeer, replyTo: replyTo, media: inputMedia, message: text, randomId: uniqueId, replyMarkup: nil, entities: messageEntities, scheduleDate: scheduleTime, sendAs: sendAsInputPeer, quickReplyShortcut: quickReplyShortcut), tag: dependencyTag) |> map(NetworkRequestResult.result) case let .forward(sourceInfo): var topMsgId: Int32? @@ -1365,8 +1411,18 @@ public final class PendingMessageManager { topMsgId = Int32(clamping: threadId) } + var quickReplyShortcut: Api.InputQuickReplyShortcut? + if let quickReply { + if let threadId = message.threadId { + quickReplyShortcut = .inputQuickReplyShortcutId(shortcutId: Int32(clamping: threadId)) + } else { + quickReplyShortcut = .inputQuickReplyShortcut(shortcut: quickReply.shortcut) + } + flags |= 1 << 17 + } + if let forwardSourceInfoAttribute = forwardSourceInfoAttribute, let sourcePeer = transaction.getPeer(forwardSourceInfoAttribute.messageId.peerId), let sourceInputPeer = apiInputPeer(sourcePeer) { - sendMessageRequest = network.request(Api.functions.messages.forwardMessages(flags: flags, fromPeer: sourceInputPeer, id: [sourceInfo.messageId.id], randomId: [uniqueId], toPeer: inputPeer, topMsgId: topMsgId, scheduleDate: scheduleTime, sendAs: sendAsInputPeer), tag: dependencyTag) + sendMessageRequest = network.request(Api.functions.messages.forwardMessages(flags: flags, fromPeer: sourceInputPeer, id: [sourceInfo.messageId.id], randomId: [uniqueId], toPeer: inputPeer, topMsgId: topMsgId, scheduleDate: scheduleTime, sendAs: sendAsInputPeer, quickReplyShortcut: quickReplyShortcut), tag: dependencyTag) |> map(NetworkRequestResult.result) } else { sendMessageRequest = .fail(MTRpcError(errorCode: 400, errorDescription: "internal")) @@ -1429,7 +1485,17 @@ public final class PendingMessageManager { } } - sendMessageRequest = network.request(Api.functions.messages.sendInlineBotResult(flags: flags, peer: inputPeer, replyTo: replyTo, randomId: uniqueId, queryId: chatContextResult.queryId, id: chatContextResult.id, scheduleDate: scheduleTime, sendAs: sendAsInputPeer)) + var quickReplyShortcut: Api.InputQuickReplyShortcut? + if let quickReply { + if let threadId = message.threadId { + quickReplyShortcut = .inputQuickReplyShortcutId(shortcutId: Int32(clamping: threadId)) + } else { + quickReplyShortcut = .inputQuickReplyShortcut(shortcut: quickReply.shortcut) + } + flags |= 1 << 17 + } + + sendMessageRequest = network.request(Api.functions.messages.sendInlineBotResult(flags: flags, peer: inputPeer, replyTo: replyTo, randomId: uniqueId, queryId: chatContextResult.queryId, id: chatContextResult.id, scheduleDate: scheduleTime, sendAs: sendAsInputPeer, quickReplyShortcut: quickReplyShortcut)) |> map(NetworkRequestResult.result) case .messageScreenshot: let replyTo: Api.InputReplyTo @@ -1557,7 +1623,16 @@ public final class PendingMessageManager { private func applySentMessage(postbox: Postbox, stateManager: AccountStateManager, message: Message, content: PendingMessageUploadedContentAndReuploadInfo, result: Api.Updates) -> Signal { var apiMessage: Api.Message? for resultMessage in result.messages { - if let id = resultMessage.id(namespace: Namespaces.Message.allScheduled.contains(message.id.namespace) ? Namespaces.Message.ScheduledCloud : Namespaces.Message.Cloud) { + let targetNamespace: MessageId.Namespace + if Namespaces.Message.allScheduled.contains(message.id.namespace) { + targetNamespace = Namespaces.Message.ScheduledCloud + } else if Namespaces.Message.allQuickReply.contains(message.id.namespace) { + targetNamespace = Namespaces.Message.QuickReplyCloud + } else { + targetNamespace = Namespaces.Message.Cloud + } + + if let id = resultMessage.id(namespace: targetNamespace) { if id.peerId == message.id.peerId { apiMessage = resultMessage break @@ -1567,7 +1642,9 @@ public final class PendingMessageManager { let silent = message.muted var namespace = Namespaces.Message.Cloud - if let apiMessage = apiMessage, let id = apiMessage.id(namespace: message.scheduleTime != nil && message.scheduleTime == apiMessage.timestamp ? Namespaces.Message.ScheduledCloud : Namespaces.Message.Cloud) { + if message.id.namespace == Namespaces.Message.QuickReplyLocal { + namespace = Namespaces.Message.QuickReplyCloud + } else if let apiMessage = apiMessage, let id = apiMessage.id(namespace: message.scheduleTime != nil && message.scheduleTime == apiMessage.timestamp ? Namespaces.Message.ScheduledCloud : Namespaces.Message.Cloud) { namespace = id.namespace if let attribute = message.attributes.first(where: { $0 is OutgoingMessageInfoAttribute }) as? OutgoingMessageInfoAttribute, let correlationId = attribute.correlationId { @@ -1594,7 +1671,9 @@ public final class PendingMessageManager { private func applySentGroupMessages(postbox: Postbox, stateManager: AccountStateManager, messages: [Message], result: Api.Updates) -> Signal { var silent = false var namespace = Namespaces.Message.Cloud - if let message = messages.first, let apiMessage = result.messages.first, message.scheduleTime != nil && message.scheduleTime == apiMessage.timestamp { + if let message = messages.first, message.id.namespace == Namespaces.Message.QuickReplyLocal { + namespace = Namespaces.Message.QuickReplyCloud + } else if let message = messages.first, let apiMessage = result.messages.first, message.scheduleTime != nil && message.scheduleTime == apiMessage.timestamp { namespace = Namespaces.Message.ScheduledCloud if message.muted { silent = true @@ -1605,7 +1684,7 @@ public final class PendingMessageManager { for i in 0 ..< messages.count { let message = messages[i] let apiMessage = result.messages[i] - if let id = apiMessage.id(namespace: message.scheduleTime != nil && message.scheduleTime == apiMessage.timestamp ? Namespaces.Message.ScheduledCloud : Namespaces.Message.Cloud) { + if let id = apiMessage.id(namespace: namespace) { if let attribute = message.attributes.first(where: { $0 is OutgoingMessageInfoAttribute }) as? OutgoingMessageInfoAttribute, let correlationId = attribute.correlationId { self.correlationIdToSentMessageId.with { value in value.mapping[correlationId] = id diff --git a/submodules/TelegramCore/Sources/State/Serialization.swift b/submodules/TelegramCore/Sources/State/Serialization.swift index ebac41c4cf2..6e1311188e7 100644 --- a/submodules/TelegramCore/Sources/State/Serialization.swift +++ b/submodules/TelegramCore/Sources/State/Serialization.swift @@ -210,7 +210,7 @@ public class BoxedMessage: NSObject { public class Serialization: NSObject, MTSerialization { public func currentLayer() -> UInt { - return 174 + return 176 } public func parseMessage(_ data: Data!) -> Any! { diff --git a/submodules/TelegramCore/Sources/State/UpdateMessageService.swift b/submodules/TelegramCore/Sources/State/UpdateMessageService.swift index d3091dad121..279064d68a6 100644 --- a/submodules/TelegramCore/Sources/State/UpdateMessageService.swift +++ b/submodules/TelegramCore/Sources/State/UpdateMessageService.swift @@ -58,7 +58,7 @@ class UpdateMessageService: NSObject, MTMessageService { self.putNext(groups) } case let .updateShortChatMessage(flags, id, fromId, chatId, message, pts, ptsCount, date, fwdFrom, viaBotId, replyHeader, entities, ttlPeriod): - let generatedMessage = Api.Message.message(flags: flags, id: id, fromId: .peerUser(userId: fromId), fromBoostsApplied: nil, peerId: Api.Peer.peerChat(chatId: chatId), savedPeerId: nil, fwdFrom: fwdFrom, viaBotId: viaBotId, replyTo: replyHeader, date: date, message: message, media: Api.MessageMedia.messageMediaEmpty, replyMarkup: nil, entities: entities, views: nil, forwards: nil, replies: nil, editDate: nil, postAuthor: nil, groupedId: nil, reactions: nil, restrictionReason: nil, ttlPeriod: ttlPeriod) + let generatedMessage = Api.Message.message(flags: flags, id: id, fromId: .peerUser(userId: fromId), fromBoostsApplied: nil, peerId: Api.Peer.peerChat(chatId: chatId), savedPeerId: nil, fwdFrom: fwdFrom, viaBotId: viaBotId, replyTo: replyHeader, date: date, message: message, media: Api.MessageMedia.messageMediaEmpty, replyMarkup: nil, entities: entities, views: nil, forwards: nil, replies: nil, editDate: nil, postAuthor: nil, groupedId: nil, reactions: nil, restrictionReason: nil, ttlPeriod: ttlPeriod, quickReplyShortcutId: nil) let update = Api.Update.updateNewMessage(message: generatedMessage, pts: pts, ptsCount: ptsCount) let groups = groupUpdates([update], users: [], chats: [], date: date, seqRange: nil) if groups.count != 0 { @@ -74,7 +74,7 @@ class UpdateMessageService: NSObject, MTMessageService { let generatedPeerId = Api.Peer.peerUser(userId: userId) - let generatedMessage = Api.Message.message(flags: flags, id: id, fromId: generatedFromId, fromBoostsApplied: nil, peerId: generatedPeerId, savedPeerId: nil, fwdFrom: fwdFrom, viaBotId: viaBotId, replyTo: replyHeader, date: date, message: message, media: Api.MessageMedia.messageMediaEmpty, replyMarkup: nil, entities: entities, views: nil, forwards: nil, replies: nil, editDate: nil, postAuthor: nil, groupedId: nil, reactions: nil, restrictionReason: nil, ttlPeriod: ttlPeriod) + let generatedMessage = Api.Message.message(flags: flags, id: id, fromId: generatedFromId, fromBoostsApplied: nil, peerId: generatedPeerId, savedPeerId: nil, fwdFrom: fwdFrom, viaBotId: viaBotId, replyTo: replyHeader, date: date, message: message, media: Api.MessageMedia.messageMediaEmpty, replyMarkup: nil, entities: entities, views: nil, forwards: nil, replies: nil, editDate: nil, postAuthor: nil, groupedId: nil, reactions: nil, restrictionReason: nil, ttlPeriod: ttlPeriod, quickReplyShortcutId: nil) let update = Api.Update.updateNewMessage(message: generatedMessage, pts: pts, ptsCount: ptsCount) let groups = groupUpdates([update], users: [], chats: [], date: date, seqRange: nil) if groups.count != 0 { diff --git a/submodules/TelegramCore/Sources/State/UpdatesApiUtils.swift b/submodules/TelegramCore/Sources/State/UpdatesApiUtils.swift index 87f132c4ca7..a7fcc880937 100644 --- a/submodules/TelegramCore/Sources/State/UpdatesApiUtils.swift +++ b/submodules/TelegramCore/Sources/State/UpdatesApiUtils.swift @@ -104,7 +104,7 @@ extension Api.MessageMedia { extension Api.Message { var rawId: Int32 { switch self { - case let .message(_, id, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _): + case let .message(_, id, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _): return id case let .messageEmpty(_, id, _): return id @@ -115,7 +115,7 @@ extension Api.Message { func id(namespace: MessageId.Namespace = Namespaces.Message.Cloud) -> MessageId? { switch self { - case let .message(_, id, _, _, messagePeerId, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _): + case let .message(_, id, _, _, messagePeerId, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _): let peerId: PeerId = messagePeerId.peerId return MessageId(peerId: peerId, namespace: namespace, id: id) case let .messageEmpty(_, id, peerId): @@ -132,7 +132,7 @@ extension Api.Message { var peerId: PeerId? { switch self { - case let .message(_, _, _, _, messagePeerId, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _): + case let .message(_, _, _, _, messagePeerId, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _): let peerId: PeerId = messagePeerId.peerId return peerId case let .messageEmpty(_, _, peerId): @@ -145,7 +145,7 @@ extension Api.Message { var timestamp: Int32? { switch self { - case let .message(_, _, _, _, _, _, _, _, _, date, _, _, _, _, _, _, _, _, _, _, _, _, _): + case let .message(_, _, _, _, _, _, _, _, _, date, _, _, _, _, _, _, _, _, _, _, _, _, _, _): return date case let .messageService(_, _, _, _, _, date, _, _): return date @@ -156,7 +156,7 @@ extension Api.Message { var preCachedResources: [(MediaResource, Data)]? { switch self { - case let .message(_, _, _, _, _, _, _, _, _, _, _, media, _, _, _, _, _, _, _, _, _, _, _): + case let .message(_, _, _, _, _, _, _, _, _, _, _, media, _, _, _, _, _, _, _, _, _, _, _, _): return media?.preCachedResources default: return nil @@ -165,7 +165,7 @@ extension Api.Message { var preCachedStories: [StoryId: Api.StoryItem]? { switch self { - case let .message(_, _, _, _, _, _, _, _, _, _, _, media, _, _, _, _, _, _, _, _, _, _, _): + case let .message(_, _, _, _, _, _, _, _, _, _, _, media, _, _, _, _, _, _, _, _, _, _, _, _): return media?.preCachedStories default: return nil @@ -271,6 +271,8 @@ extension Api.Update { return message case let .updateNewScheduledMessage(message): return message + case let .updateQuickReplyMessage(message): + return message default: return nil } @@ -334,6 +336,8 @@ extension Api.Update { return [peer.peerId] case let .updateNewScheduledMessage(message): return apiMessagePeerIds(message) + case let .updateQuickReplyMessage(message): + return apiMessagePeerIds(message) default: return [] } @@ -349,6 +353,8 @@ extension Api.Update { return apiMessageAssociatedMessageIds(message) case let .updateNewScheduledMessage(message): return apiMessageAssociatedMessageIds(message) + case let .updateQuickReplyMessage(message): + return apiMessageAssociatedMessageIds(message) default: break } diff --git a/submodules/TelegramCore/Sources/SyncCore/QuickReplyMessageAttribute.swift b/submodules/TelegramCore/Sources/SyncCore/QuickReplyMessageAttribute.swift new file mode 100644 index 00000000000..9df4ade3998 --- /dev/null +++ b/submodules/TelegramCore/Sources/SyncCore/QuickReplyMessageAttribute.swift @@ -0,0 +1,23 @@ +import Foundation +import Postbox +import TelegramApi + +public final class OutgoingQuickReplyMessageAttribute: Equatable, MessageAttribute { + public let shortcut: String + + public init(shortcut: String) { + self.shortcut = shortcut + } + + required public init(decoder: PostboxDecoder) { + self.shortcut = decoder.decodeStringForKey("s", orElse: "") + } + + public func encode(_ encoder: PostboxEncoder) { + encoder.encodeString(self.shortcut, forKey: "s") + } + + public static func ==(lhs: OutgoingQuickReplyMessageAttribute, rhs: OutgoingQuickReplyMessageAttribute) -> Bool { + return true + } +} diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_CachedUserData.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_CachedUserData.swift index 87c5e650038..5e9a6f657bf 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_CachedUserData.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_CachedUserData.swift @@ -247,6 +247,211 @@ public final class EditableBotInfo: PostboxCoding, Equatable { } } +public final class TelegramBusinessHours: Equatable, Codable { + public struct WorkingTimeInterval: Equatable, Codable { + private enum CodingKeys: String, CodingKey { + case startMinute + case endMinute + } + + public let startMinute: Int + public let endMinute: Int + + public init(startMinute: Int, endMinute: Int) { + self.startMinute = startMinute + self.endMinute = endMinute + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + self.startMinute = Int(try container.decode(Int32.self, forKey: .startMinute)) + self.endMinute = Int(try container.decode(Int32.self, forKey: .endMinute)) + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + + try container.encode(Int32(clamping: self.startMinute), forKey: .startMinute) + try container.encode(Int32(clamping: self.endMinute), forKey: .endMinute) + } + + public static func ==(lhs: WorkingTimeInterval, rhs: WorkingTimeInterval) -> Bool { + if lhs.startMinute != rhs.startMinute { + return false + } + if lhs.endMinute != rhs.endMinute { + return false + } + return true + } + } + + public let timezoneId: String + public let weeklyTimeIntervals: [WorkingTimeInterval] + + public init(timezoneId: String, weeklyTimeIntervals: [WorkingTimeInterval]) { + self.timezoneId = timezoneId + self.weeklyTimeIntervals = weeklyTimeIntervals + } + + public static func ==(lhs: TelegramBusinessHours, rhs: TelegramBusinessHours) -> Bool { + if lhs.timezoneId != rhs.timezoneId { + return false + } + if lhs.weeklyTimeIntervals != rhs.weeklyTimeIntervals { + return false + } + return true + } + + public enum WeekDay { + case closed + case open + case intervals([WorkingTimeInterval]) + } + + public func splitIntoWeekDays() -> [WeekDay] { + var mappedDays: [[WorkingTimeInterval]] = Array(repeating: [], count: 7) + + var weekMinutes = IndexSet() + for interval in self.weeklyTimeIntervals { + weekMinutes.insert(integersIn: interval.startMinute ..< interval.endMinute) + } + + for i in 0 ..< mappedDays.count { + let dayRange = i * 24 * 60 ..< (i + 1) * 24 * 60 + var removeMinutes = IndexSet() + inner: for range in weekMinutes.rangeView { + if range.lowerBound >= dayRange.upperBound { + break inner + } else { + let clippedRange: Range + if range.lowerBound == dayRange.lowerBound { + clippedRange = range.lowerBound ..< min(range.upperBound, dayRange.upperBound) + } else { + clippedRange = range.lowerBound ..< min(range.upperBound, dayRange.upperBound + 12 * 60) + } + + let startTimeInsideDay = clippedRange.lowerBound - i * (24 * 60) + let endTimeInsideDay = clippedRange.upperBound - i * (24 * 60) + + mappedDays[i].append(WorkingTimeInterval( + startMinute: startTimeInsideDay, + endMinute: endTimeInsideDay + )) + removeMinutes.insert(integersIn: clippedRange) + } + } + + weekMinutes.subtract(removeMinutes) + } + + return mappedDays.map { day -> WeekDay in + var minutes = IndexSet() + for interval in day { + minutes.insert(integersIn: interval.startMinute ..< interval.endMinute) + } + if minutes.isEmpty { + return .closed + } else if minutes == IndexSet(integersIn: 0 ..< 24 * 60) || minutes == IndexSet(integersIn: 0 ..< (24 * 60 - 1)) { + return .open + } else { + return .intervals(day) + } + } + } + + public func weekMinuteSet() -> IndexSet { + var result = IndexSet() + + for interval in self.weeklyTimeIntervals { + result.insert(integersIn: interval.startMinute ..< interval.endMinute) + } + + return result + } +} + +public final class TelegramBusinessLocation: Equatable, Codable { + public struct Coordinates: Equatable, Codable { + public let latitude: Double + public let longitude: Double + + public init(latitude: Double, longitude: Double) { + self.latitude = latitude + self.longitude = longitude + } + } + + public let address: String + public let coordinates: Coordinates? + + public init(address: String, coordinates: Coordinates?) { + self.address = address + self.coordinates = coordinates + } + + public static func ==(lhs: TelegramBusinessLocation, rhs: TelegramBusinessLocation) -> Bool { + if lhs.address != rhs.address { + return false + } + if lhs.coordinates != rhs.coordinates { + return false + } + return true + } +} + +extension TelegramBusinessHours.WorkingTimeInterval { + init(apiInterval: Api.BusinessWeeklyOpen) { + switch apiInterval { + case let .businessWeeklyOpen(startMinute, endMinute): + self.init(startMinute: Int(startMinute), endMinute: Int(endMinute)) + } + } + + var apiInterval: Api.BusinessWeeklyOpen { + return .businessWeeklyOpen(startMinute: Int32(clamping: self.startMinute), endMinute: Int32(clamping: self.endMinute)) + } +} + +extension TelegramBusinessHours { + convenience init(apiWorkingHours: Api.BusinessWorkHours) { + switch apiWorkingHours { + case let .businessWorkHours(_, timezoneId, weeklyOpen): + self.init(timezoneId: timezoneId, weeklyTimeIntervals: weeklyOpen.map(TelegramBusinessHours.WorkingTimeInterval.init(apiInterval:))) + } + } + + var apiBusinessHours: Api.BusinessWorkHours { + return .businessWorkHours(flags: 0, timezoneId: self.timezoneId, weeklyOpen: self.weeklyTimeIntervals.map(\.apiInterval)) + } +} + +extension TelegramBusinessLocation.Coordinates { + init?(apiGeoPoint: Api.GeoPoint) { + switch apiGeoPoint { + case let .geoPoint(_, long, lat, _, _): + self.init(latitude: lat, longitude: long) + case .geoPointEmpty: + return nil + } + } + + var apiInputGeoPoint: Api.InputGeoPoint { + return .inputGeoPoint(flags: 0, lat: self.latitude, long: self.longitude, accuracyRadius: nil) + } +} + +extension TelegramBusinessLocation { + convenience init(apiLocation: Api.BusinessLocation) { + switch apiLocation { + case let .businessLocation(_, geoPoint, address): + self.init(address: address, coordinates: geoPoint.flatMap { Coordinates(apiGeoPoint: $0) }) + } + } +} public final class CachedUserData: CachedPeerData { public let about: String? @@ -270,6 +475,11 @@ public final class CachedUserData: CachedPeerData { public let voiceMessagesAvailable: Bool public let wallpaper: TelegramWallpaper? public let flags: CachedUserFlags + public let businessHours: TelegramBusinessHours? + public let businessLocation: TelegramBusinessLocation? + public let greetingMessage: TelegramBusinessGreetingMessage? + public let awayMessage: TelegramBusinessAwayMessage? + public let connectedBot: TelegramAccountConnectedBot? public let peerIds: Set public let messageIds: Set @@ -297,11 +507,16 @@ public final class CachedUserData: CachedPeerData { self.voiceMessagesAvailable = true self.wallpaper = nil self.flags = CachedUserFlags() + self.businessHours = nil + self.businessLocation = nil self.peerIds = Set() self.messageIds = Set() + self.greetingMessage = nil + self.awayMessage = nil + self.connectedBot = nil } - public init(about: String?, botInfo: BotInfo?, editableBotInfo: EditableBotInfo?, peerStatusSettings: PeerStatusSettings?, pinnedMessageId: MessageId?, isBlocked: Bool, commonGroupCount: Int32, voiceCallsAvailable: Bool, videoCallsAvailable: Bool, callsPrivate: Bool, canPinMessages: Bool, hasScheduledMessages: Bool, autoremoveTimeout: CachedPeerAutoremoveTimeout, themeEmoticon: String?, photo: CachedPeerProfilePhoto, personalPhoto: CachedPeerProfilePhoto, fallbackPhoto: CachedPeerProfilePhoto, premiumGiftOptions: [CachedPremiumGiftOption], voiceMessagesAvailable: Bool, wallpaper: TelegramWallpaper?, flags: CachedUserFlags) { + public init(about: String?, botInfo: BotInfo?, editableBotInfo: EditableBotInfo?, peerStatusSettings: PeerStatusSettings?, pinnedMessageId: MessageId?, isBlocked: Bool, commonGroupCount: Int32, voiceCallsAvailable: Bool, videoCallsAvailable: Bool, callsPrivate: Bool, canPinMessages: Bool, hasScheduledMessages: Bool, autoremoveTimeout: CachedPeerAutoremoveTimeout, themeEmoticon: String?, photo: CachedPeerProfilePhoto, personalPhoto: CachedPeerProfilePhoto, fallbackPhoto: CachedPeerProfilePhoto, premiumGiftOptions: [CachedPremiumGiftOption], voiceMessagesAvailable: Bool, wallpaper: TelegramWallpaper?, flags: CachedUserFlags, businessHours: TelegramBusinessHours?, businessLocation: TelegramBusinessLocation?, greetingMessage: TelegramBusinessGreetingMessage?, awayMessage: TelegramBusinessAwayMessage?, connectedBot: TelegramAccountConnectedBot?) { self.about = about self.botInfo = botInfo self.editableBotInfo = editableBotInfo @@ -323,6 +538,11 @@ public final class CachedUserData: CachedPeerData { self.voiceMessagesAvailable = voiceMessagesAvailable self.wallpaper = wallpaper self.flags = flags + self.businessHours = businessHours + self.businessLocation = businessLocation + self.greetingMessage = greetingMessage + self.awayMessage = awayMessage + self.connectedBot = connectedBot self.peerIds = Set() @@ -375,6 +595,13 @@ public final class CachedUserData: CachedPeerData { messageIds.insert(pinnedMessageId) } self.messageIds = messageIds + + self.businessHours = decoder.decodeCodable(TelegramBusinessHours.self, forKey: "bhrs") + self.businessLocation = decoder.decodeCodable(TelegramBusinessLocation.self, forKey: "bloc") + + self.greetingMessage = decoder.decodeCodable(TelegramBusinessGreetingMessage.self, forKey: "bgreet") + self.awayMessage = decoder.decodeCodable(TelegramBusinessAwayMessage.self, forKey: "baway") + self.connectedBot = decoder.decodeCodable(TelegramAccountConnectedBot.self, forKey: "bbot") } public func encode(_ encoder: PostboxEncoder) { @@ -435,6 +662,36 @@ public final class CachedUserData: CachedPeerData { } encoder.encodeInt32(self.flags.rawValue, forKey: "fl") + + if let businessHours = self.businessHours { + encoder.encodeCodable(businessHours, forKey: "bhrs") + } else { + encoder.encodeNil(forKey: "bhrs") + } + + if let businessLocation = self.businessLocation { + encoder.encodeCodable(businessLocation, forKey: "bloc") + } else { + encoder.encodeNil(forKey: "bloc") + } + + if let greetingMessage = self.greetingMessage { + encoder.encodeCodable(greetingMessage, forKey: "bgreet") + } else { + encoder.encodeNil(forKey: "bgreet") + } + + if let awayMessage = self.awayMessage { + encoder.encodeCodable(awayMessage, forKey: "baway") + } else { + encoder.encodeNil(forKey: "baway") + } + + if let connectedBot = self.connectedBot { + encoder.encodeCodable(connectedBot, forKey: "bbot") + } else { + encoder.encodeNil(forKey: "bbot") + } } public func isEqual(to: CachedPeerData) -> Bool { @@ -448,91 +705,126 @@ public final class CachedUserData: CachedPeerData { if other.canPinMessages != self.canPinMessages { return false } + if other.businessHours != self.businessHours { + return false + } + if other.businessLocation != self.businessLocation { + return false + } + if other.greetingMessage != self.greetingMessage { + return false + } + if other.awayMessage != self.awayMessage { + return false + } + if other.connectedBot != self.connectedBot { + return false + } return other.about == self.about && other.botInfo == self.botInfo && other.editableBotInfo == self.editableBotInfo && self.peerStatusSettings == other.peerStatusSettings && self.isBlocked == other.isBlocked && self.commonGroupCount == other.commonGroupCount && self.voiceCallsAvailable == other.voiceCallsAvailable && self.videoCallsAvailable == other.videoCallsAvailable && self.callsPrivate == other.callsPrivate && self.hasScheduledMessages == other.hasScheduledMessages && self.autoremoveTimeout == other.autoremoveTimeout && self.themeEmoticon == other.themeEmoticon && self.photo == other.photo && self.personalPhoto == other.personalPhoto && self.fallbackPhoto == other.fallbackPhoto && self.premiumGiftOptions == other.premiumGiftOptions && self.voiceMessagesAvailable == other.voiceMessagesAvailable && self.flags == other.flags && self.wallpaper == other.wallpaper } public func withUpdatedAbout(_ about: String?) -> CachedUserData { - return CachedUserData(about: about, botInfo: self.botInfo, editableBotInfo: self.editableBotInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, personalPhoto: self.personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, wallpaper: self.wallpaper, flags: self.flags) + return CachedUserData(about: about, botInfo: self.botInfo, editableBotInfo: self.editableBotInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, personalPhoto: self.personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, wallpaper: self.wallpaper, flags: self.flags, businessHours: self.businessHours, businessLocation: self.businessLocation, greetingMessage: self.greetingMessage, awayMessage: self.awayMessage, connectedBot: self.connectedBot) } public func withUpdatedBotInfo(_ botInfo: BotInfo?) -> CachedUserData { - return CachedUserData(about: self.about, botInfo: botInfo, editableBotInfo: self.editableBotInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, personalPhoto: self.personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, wallpaper: self.wallpaper, flags: self.flags) + return CachedUserData(about: self.about, botInfo: botInfo, editableBotInfo: self.editableBotInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, personalPhoto: self.personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, wallpaper: self.wallpaper, flags: self.flags, businessHours: self.businessHours, businessLocation: self.businessLocation, greetingMessage: self.greetingMessage, awayMessage: self.awayMessage, connectedBot: self.connectedBot) } public func withUpdatedEditableBotInfo(_ editableBotInfo: EditableBotInfo?) -> CachedUserData { - return CachedUserData(about: self.about, botInfo: self.botInfo, editableBotInfo: editableBotInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, personalPhoto: self.personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, wallpaper: self.wallpaper, flags: self.flags) + return CachedUserData(about: self.about, botInfo: self.botInfo, editableBotInfo: editableBotInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, personalPhoto: self.personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, wallpaper: self.wallpaper, flags: self.flags, businessHours: self.businessHours, businessLocation: self.businessLocation, greetingMessage: self.greetingMessage, awayMessage: self.awayMessage, connectedBot: self.connectedBot) } public func withUpdatedPeerStatusSettings(_ peerStatusSettings: PeerStatusSettings) -> CachedUserData { - return CachedUserData(about: self.about, botInfo: self.botInfo, editableBotInfo: self.editableBotInfo, peerStatusSettings: peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, personalPhoto: self.personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, wallpaper: self.wallpaper, flags: self.flags) + return CachedUserData(about: self.about, botInfo: self.botInfo, editableBotInfo: self.editableBotInfo, peerStatusSettings: peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, personalPhoto: self.personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, wallpaper: self.wallpaper, flags: self.flags, businessHours: self.businessHours, businessLocation: self.businessLocation, greetingMessage: self.greetingMessage, awayMessage: self.awayMessage, connectedBot: self.connectedBot) } public func withUpdatedPinnedMessageId(_ pinnedMessageId: MessageId?) -> CachedUserData { - return CachedUserData(about: self.about, botInfo: self.botInfo, editableBotInfo: self.editableBotInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, personalPhoto: self.personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, wallpaper: self.wallpaper, flags: self.flags) + return CachedUserData(about: self.about, botInfo: self.botInfo, editableBotInfo: self.editableBotInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, personalPhoto: self.personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, wallpaper: self.wallpaper, flags: self.flags, businessHours: self.businessHours, businessLocation: self.businessLocation, greetingMessage: self.greetingMessage, awayMessage: self.awayMessage, connectedBot: self.connectedBot) } public func withUpdatedIsBlocked(_ isBlocked: Bool) -> CachedUserData { - return CachedUserData(about: self.about, botInfo: self.botInfo, editableBotInfo: self.editableBotInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, personalPhoto: self.personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, wallpaper: self.wallpaper, flags: self.flags) + return CachedUserData(about: self.about, botInfo: self.botInfo, editableBotInfo: self.editableBotInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, personalPhoto: self.personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, wallpaper: self.wallpaper, flags: self.flags, businessHours: self.businessHours, businessLocation: self.businessLocation, greetingMessage: self.greetingMessage, awayMessage: self.awayMessage, connectedBot: self.connectedBot) } public func withUpdatedCommonGroupCount(_ commonGroupCount: Int32) -> CachedUserData { - return CachedUserData(about: self.about, botInfo: self.botInfo, editableBotInfo: self.editableBotInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, personalPhoto: self.personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, wallpaper: self.wallpaper, flags: self.flags) + return CachedUserData(about: self.about, botInfo: self.botInfo, editableBotInfo: self.editableBotInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, personalPhoto: self.personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, wallpaper: self.wallpaper, flags: self.flags, businessHours: self.businessHours, businessLocation: self.businessLocation, greetingMessage: self.greetingMessage, awayMessage: self.awayMessage, connectedBot: self.connectedBot) } public func withUpdatedVoiceCallsAvailable(_ voiceCallsAvailable: Bool) -> CachedUserData { - return CachedUserData(about: self.about, botInfo: self.botInfo, editableBotInfo: self.editableBotInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, personalPhoto: self.personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, wallpaper: self.wallpaper, flags: self.flags) + return CachedUserData(about: self.about, botInfo: self.botInfo, editableBotInfo: self.editableBotInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, personalPhoto: self.personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, wallpaper: self.wallpaper, flags: self.flags, businessHours: self.businessHours, businessLocation: self.businessLocation, greetingMessage: self.greetingMessage, awayMessage: self.awayMessage, connectedBot: self.connectedBot) } public func withUpdatedVideoCallsAvailable(_ videoCallsAvailable: Bool) -> CachedUserData { - return CachedUserData(about: self.about, botInfo: self.botInfo, editableBotInfo: self.editableBotInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, personalPhoto: self.personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, wallpaper: self.wallpaper, flags: self.flags) + return CachedUserData(about: self.about, botInfo: self.botInfo, editableBotInfo: self.editableBotInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, personalPhoto: self.personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, wallpaper: self.wallpaper, flags: self.flags, businessHours: self.businessHours, businessLocation: self.businessLocation, greetingMessage: self.greetingMessage, awayMessage: self.awayMessage, connectedBot: self.connectedBot) } public func withUpdatedCallsPrivate(_ callsPrivate: Bool) -> CachedUserData { - return CachedUserData(about: self.about, botInfo: self.botInfo, editableBotInfo: self.editableBotInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, personalPhoto: self.personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, wallpaper: self.wallpaper, flags: self.flags) + return CachedUserData(about: self.about, botInfo: self.botInfo, editableBotInfo: self.editableBotInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, personalPhoto: self.personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, wallpaper: self.wallpaper, flags: self.flags, businessHours: self.businessHours, businessLocation: self.businessLocation, greetingMessage: self.greetingMessage, awayMessage: self.awayMessage, connectedBot: self.connectedBot) } public func withUpdatedCanPinMessages(_ canPinMessages: Bool) -> CachedUserData { - return CachedUserData(about: self.about, botInfo: self.botInfo, editableBotInfo: self.editableBotInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, personalPhoto: self.personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, wallpaper: self.wallpaper, flags: self.flags) + return CachedUserData(about: self.about, botInfo: self.botInfo, editableBotInfo: self.editableBotInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, personalPhoto: self.personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, wallpaper: self.wallpaper, flags: self.flags, businessHours: self.businessHours, businessLocation: self.businessLocation, greetingMessage: self.greetingMessage, awayMessage: self.awayMessage, connectedBot: self.connectedBot) } public func withUpdatedHasScheduledMessages(_ hasScheduledMessages: Bool) -> CachedUserData { - return CachedUserData(about: self.about, botInfo: self.botInfo, editableBotInfo: self.editableBotInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, personalPhoto: self.personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, wallpaper: self.wallpaper, flags: self.flags) + return CachedUserData(about: self.about, botInfo: self.botInfo, editableBotInfo: self.editableBotInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, personalPhoto: self.personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, wallpaper: self.wallpaper, flags: self.flags, businessHours: self.businessHours, businessLocation: self.businessLocation, greetingMessage: self.greetingMessage, awayMessage: self.awayMessage, connectedBot: self.connectedBot) } public func withUpdatedAutoremoveTimeout(_ autoremoveTimeout: CachedPeerAutoremoveTimeout) -> CachedUserData { - return CachedUserData(about: self.about, botInfo: self.botInfo, editableBotInfo: self.editableBotInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, personalPhoto: self.personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, wallpaper: self.wallpaper, flags: self.flags) + return CachedUserData(about: self.about, botInfo: self.botInfo, editableBotInfo: self.editableBotInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, personalPhoto: self.personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, wallpaper: self.wallpaper, flags: self.flags, businessHours: self.businessHours, businessLocation: self.businessLocation, greetingMessage: self.greetingMessage, awayMessage: self.awayMessage, connectedBot: self.connectedBot) } public func withUpdatedThemeEmoticon(_ themeEmoticon: String?) -> CachedUserData { - return CachedUserData(about: self.about, botInfo: self.botInfo, editableBotInfo: self.editableBotInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: themeEmoticon, photo: self.photo, personalPhoto: self.personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, wallpaper: self.wallpaper, flags: self.flags) + return CachedUserData(about: self.about, botInfo: self.botInfo, editableBotInfo: self.editableBotInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: themeEmoticon, photo: self.photo, personalPhoto: self.personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, wallpaper: self.wallpaper, flags: self.flags, businessHours: self.businessHours, businessLocation: self.businessLocation, greetingMessage: self.greetingMessage, awayMessage: self.awayMessage, connectedBot: self.connectedBot) } public func withUpdatedPhoto(_ photo: CachedPeerProfilePhoto) -> CachedUserData { - return CachedUserData(about: self.about, botInfo: self.botInfo, editableBotInfo: self.editableBotInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: photo, personalPhoto: self.personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, wallpaper: self.wallpaper, flags: self.flags) + return CachedUserData(about: self.about, botInfo: self.botInfo, editableBotInfo: self.editableBotInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: photo, personalPhoto: self.personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, wallpaper: self.wallpaper, flags: self.flags, businessHours: self.businessHours, businessLocation: self.businessLocation, greetingMessage: self.greetingMessage, awayMessage: self.awayMessage, connectedBot: self.connectedBot) } public func withUpdatedPersonalPhoto(_ personalPhoto: CachedPeerProfilePhoto) -> CachedUserData { - return CachedUserData(about: self.about, botInfo: self.botInfo, editableBotInfo: self.editableBotInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, personalPhoto: personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, wallpaper: self.wallpaper, flags: self.flags) + return CachedUserData(about: self.about, botInfo: self.botInfo, editableBotInfo: self.editableBotInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, personalPhoto: personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, wallpaper: self.wallpaper, flags: self.flags, businessHours: self.businessHours, businessLocation: self.businessLocation, greetingMessage: self.greetingMessage, awayMessage: self.awayMessage, connectedBot: self.connectedBot) } public func withUpdatedFallbackPhoto(_ fallbackPhoto: CachedPeerProfilePhoto) -> CachedUserData { - return CachedUserData(about: self.about, botInfo: self.botInfo, editableBotInfo: self.editableBotInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, personalPhoto: self.personalPhoto, fallbackPhoto: fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, wallpaper: self.wallpaper, flags: self.flags) + return CachedUserData(about: self.about, botInfo: self.botInfo, editableBotInfo: self.editableBotInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, personalPhoto: self.personalPhoto, fallbackPhoto: fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, wallpaper: self.wallpaper, flags: self.flags, businessHours: self.businessHours, businessLocation: self.businessLocation, greetingMessage: self.greetingMessage, awayMessage: self.awayMessage, connectedBot: self.connectedBot) } public func withUpdatedPremiumGiftOptions(_ premiumGiftOptions: [CachedPremiumGiftOption]) -> CachedUserData { - return CachedUserData(about: self.about, botInfo: self.botInfo, editableBotInfo: self.editableBotInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, personalPhoto: self.personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, wallpaper: self.wallpaper, flags: self.flags) + return CachedUserData(about: self.about, botInfo: self.botInfo, editableBotInfo: self.editableBotInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, personalPhoto: self.personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, wallpaper: self.wallpaper, flags: self.flags, businessHours: self.businessHours, businessLocation: self.businessLocation, greetingMessage: self.greetingMessage, awayMessage: self.awayMessage, connectedBot: self.connectedBot) } public func withUpdatedVoiceMessagesAvailable(_ voiceMessagesAvailable: Bool) -> CachedUserData { - return CachedUserData(about: self.about, botInfo: self.botInfo, editableBotInfo: self.editableBotInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, personalPhoto: self.personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: voiceMessagesAvailable, wallpaper: self.wallpaper, flags: self.flags) + return CachedUserData(about: self.about, botInfo: self.botInfo, editableBotInfo: self.editableBotInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, personalPhoto: self.personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: voiceMessagesAvailable, wallpaper: self.wallpaper, flags: self.flags, businessHours: self.businessHours, businessLocation: self.businessLocation, greetingMessage: self.greetingMessage, awayMessage: self.awayMessage, connectedBot: self.connectedBot) } public func withUpdatedWallpaper(_ wallpaper: TelegramWallpaper?) -> CachedUserData { - return CachedUserData(about: self.about, botInfo: self.botInfo, editableBotInfo: self.editableBotInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, personalPhoto: self.personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, wallpaper: wallpaper, flags: self.flags) + return CachedUserData(about: self.about, botInfo: self.botInfo, editableBotInfo: self.editableBotInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, personalPhoto: self.personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, wallpaper: wallpaper, flags: self.flags, businessHours: self.businessHours, businessLocation: self.businessLocation, greetingMessage: self.greetingMessage, awayMessage: self.awayMessage, connectedBot: self.connectedBot) } public func withUpdatedFlags(_ flags: CachedUserFlags) -> CachedUserData { - return CachedUserData(about: self.about, botInfo: self.botInfo, editableBotInfo: self.editableBotInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, personalPhoto: self.personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, wallpaper: self.wallpaper, flags: flags) + return CachedUserData(about: self.about, botInfo: self.botInfo, editableBotInfo: self.editableBotInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, personalPhoto: self.personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, wallpaper: self.wallpaper, flags: flags, businessHours: self.businessHours, businessLocation: self.businessLocation, greetingMessage: self.greetingMessage, awayMessage: self.awayMessage, connectedBot: self.connectedBot) + } + + public func withUpdatedBusinessHours(_ businessHours: TelegramBusinessHours?) -> CachedUserData { + return CachedUserData(about: self.about, botInfo: self.botInfo, editableBotInfo: self.editableBotInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, personalPhoto: self.personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, wallpaper: self.wallpaper, flags: self.flags, businessHours: businessHours, businessLocation: self.businessLocation, greetingMessage: self.greetingMessage, awayMessage: self.awayMessage, connectedBot: self.connectedBot) + } + + public func withUpdatedBusinessLocation(_ businessLocation: TelegramBusinessLocation?) -> CachedUserData { + return CachedUserData(about: self.about, botInfo: self.botInfo, editableBotInfo: self.editableBotInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, personalPhoto: self.personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, wallpaper: self.wallpaper, flags: self.flags, businessHours: self.businessHours, businessLocation: businessLocation, greetingMessage: self.greetingMessage, awayMessage: self.awayMessage, connectedBot: self.connectedBot) + } + + public func withUpdatedGreetingMessage(_ greetingMessage: TelegramBusinessGreetingMessage?) -> CachedUserData { + return CachedUserData(about: self.about, botInfo: self.botInfo, editableBotInfo: self.editableBotInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, personalPhoto: self.personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, wallpaper: self.wallpaper, flags: self.flags, businessHours: self.businessHours, businessLocation: self.businessLocation, greetingMessage: greetingMessage, awayMessage: self.awayMessage, connectedBot: self.connectedBot) + } + + public func withUpdatedAwayMessage(_ awayMessage: TelegramBusinessAwayMessage?) -> CachedUserData { + return CachedUserData(about: self.about, botInfo: self.botInfo, editableBotInfo: self.editableBotInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, personalPhoto: self.personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, wallpaper: self.wallpaper, flags: self.flags, businessHours: self.businessHours, businessLocation: self.businessLocation, greetingMessage: self.greetingMessage, awayMessage: awayMessage, connectedBot: self.connectedBot) + } + + public func withUpdatedConnectedBot(_ connectedBot: TelegramAccountConnectedBot?) -> CachedUserData { + return CachedUserData(about: self.about, botInfo: self.botInfo, editableBotInfo: self.editableBotInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, personalPhoto: self.personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, wallpaper: self.wallpaper, flags: self.flags, businessHours: self.businessHours, businessLocation: self.businessLocation, greetingMessage: self.greetingMessage, awayMessage: self.awayMessage, connectedBot: connectedBot) } } diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_CloudChatRemoveMessagesOperation.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_CloudChatRemoveMessagesOperation.swift index 40d830bf1b8..41e23ccdcfd 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_CloudChatRemoveMessagesOperation.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_CloudChatRemoveMessagesOperation.swift @@ -24,21 +24,29 @@ public extension CloudChatRemoveMessagesType { public final class CloudChatRemoveMessagesOperation: PostboxCoding { public let messageIds: [MessageId] + public let threadId: Int64? public let type: CloudChatRemoveMessagesType - public init(messageIds: [MessageId], type: CloudChatRemoveMessagesType) { + public init(messageIds: [MessageId], threadId: Int64?, type: CloudChatRemoveMessagesType) { self.messageIds = messageIds + self.threadId = threadId self.type = type } public init(decoder: PostboxDecoder) { self.messageIds = MessageId.decodeArrayFromBuffer(decoder.decodeBytesForKeyNoCopy("i")!) + self.threadId = decoder.decodeOptionalInt64ForKey("threadId") self.type = CloudChatRemoveMessagesType(rawValue: decoder.decodeInt32ForKey("t", orElse: 0))! } public func encode(_ encoder: PostboxEncoder) { let buffer = WriteBuffer() MessageId.encodeArrayToBuffer(self.messageIds, buffer: buffer) + if let threadId = self.threadId { + encoder.encodeInt64(threadId, forKey: "threadId") + } else { + encoder.encodeNil(forKey: "threadId") + } encoder.encodeBytes(buffer, forKey: "i") encoder.encodeInt32(self.type.rawValue, forKey: "t") } @@ -88,6 +96,7 @@ public enum CloudChatClearHistoryType: Int32 { case forLocalPeer case forEveryone case scheduledMessages + case quickReplyMessages } public enum InteractiveHistoryClearingType: Int32 { diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_MediaReference.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_MediaReference.swift index 66f409e0fdd..57a8473844a 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_MediaReference.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_MediaReference.swift @@ -7,7 +7,7 @@ public struct MessageReference: PostboxCoding, Hashable, Equatable { switch content { case .none: return nil - case let .message(peer, _, _, _, _, _): + case let .message(peer, _, _, _, _, _, _): return peer } } @@ -15,7 +15,7 @@ public struct MessageReference: PostboxCoding, Hashable, Equatable { switch content { case .none: return nil - case let .message(_, author, _, _, _, _): + case let .message(_, author, _, _, _, _, _): return author } } @@ -24,7 +24,7 @@ public struct MessageReference: PostboxCoding, Hashable, Equatable { switch content { case .none: return nil - case let .message(_, _, id, _, _, _): + case let .message(_, _, id, _, _, _, _): return id } } @@ -33,7 +33,7 @@ public struct MessageReference: PostboxCoding, Hashable, Equatable { switch content { case .none: return nil - case let .message(_, _, _, timestamp, _, _): + case let .message(_, _, _, timestamp, _, _, _): return timestamp } } @@ -42,7 +42,7 @@ public struct MessageReference: PostboxCoding, Hashable, Equatable { switch content { case .none: return nil - case let .message(_, _, _, _, incoming, _): + case let .message(_, _, _, _, incoming, _, _): return incoming } } @@ -51,11 +51,20 @@ public struct MessageReference: PostboxCoding, Hashable, Equatable { switch content { case .none: return nil - case let .message(_, _, _, _, _, secret): + case let .message(_, _, _, _, _, secret, _): return secret } } + public var threadId: Int64? { + switch content { + case .none: + return nil + case let .message(_, _, _, _, _, _, threadId): + return threadId + } + } + public init(_ message: Message) { if message.id.namespace != Namespaces.Message.Local, let peer = message.peers[message.id.peerId], let inputPeer = PeerReference(peer) { let author: PeerReference? @@ -64,13 +73,13 @@ public struct MessageReference: PostboxCoding, Hashable, Equatable { } else { author = nil } - self.content = .message(peer: inputPeer, author: author, id: message.id, timestamp: message.timestamp, incoming: message.flags.contains(.Incoming), secret: message.containsSecretMedia) + self.content = .message(peer: inputPeer, author: author, id: message.id, timestamp: message.timestamp, incoming: message.flags.contains(.Incoming), secret: message.containsSecretMedia, threadId: message.threadId) } else { self.content = .none } } - public init(peer: Peer, author: Peer?, id: MessageId, timestamp: Int32, incoming: Bool, secret: Bool) { + public init(peer: Peer, author: Peer?, id: MessageId, timestamp: Int32, incoming: Bool, secret: Bool, threadId: Int64?) { if let inputPeer = PeerReference(peer) { let a: PeerReference? if let peer = author { @@ -78,7 +87,7 @@ public struct MessageReference: PostboxCoding, Hashable, Equatable { } else { a = nil } - self.content = .message(peer: inputPeer, author: a, id: id, timestamp: timestamp, incoming: incoming, secret: secret) + self.content = .message(peer: inputPeer, author: a, id: id, timestamp: timestamp, incoming: incoming, secret: secret, threadId: threadId) } else { self.content = .none } @@ -95,14 +104,14 @@ public struct MessageReference: PostboxCoding, Hashable, Equatable { public enum MessageReferenceContent: PostboxCoding, Hashable, Equatable { case none - case message(peer: PeerReference, author: PeerReference?, id: MessageId, timestamp: Int32, incoming: Bool, secret: Bool) + case message(peer: PeerReference, author: PeerReference?, id: MessageId, timestamp: Int32, incoming: Bool, secret: Bool, threadId: Int64?) public init(decoder: PostboxDecoder) { switch decoder.decodeInt32ForKey("_r", orElse: 0) { case 0: self = .none case 1: - self = .message(peer: decoder.decodeObjectForKey("p", decoder: { PeerReference(decoder: $0) }) as! PeerReference, author: decoder.decodeObjectForKey("author") as? PeerReference, id: MessageId(peerId: PeerId(decoder.decodeInt64ForKey("i.p", orElse: 0)), namespace: decoder.decodeInt32ForKey("i.n", orElse: 0), id: decoder.decodeInt32ForKey("i.i", orElse: 0)), timestamp: 0, incoming: false, secret: false) + self = .message(peer: decoder.decodeObjectForKey("p", decoder: { PeerReference(decoder: $0) }) as! PeerReference, author: decoder.decodeObjectForKey("author") as? PeerReference, id: MessageId(peerId: PeerId(decoder.decodeInt64ForKey("i.p", orElse: 0)), namespace: decoder.decodeInt32ForKey("i.n", orElse: 0), id: decoder.decodeInt32ForKey("i.i", orElse: 0)), timestamp: 0, incoming: false, secret: false, threadId: decoder.decodeOptionalInt64ForKey("tid")) default: assertionFailure() self = .none @@ -113,7 +122,7 @@ public enum MessageReferenceContent: PostboxCoding, Hashable, Equatable { switch self { case .none: encoder.encodeInt32(0, forKey: "_r") - case let .message(peer, author, id, _, _, _): + case let .message(peer, author, id, _, _, _, threadId): encoder.encodeInt32(1, forKey: "_r") encoder.encodeObject(peer, forKey: "p") if let author = author { @@ -124,6 +133,11 @@ public enum MessageReferenceContent: PostboxCoding, Hashable, Equatable { encoder.encodeInt64(id.peerId.toInt64(), forKey: "i.p") encoder.encodeInt32(id.namespace, forKey: "i.n") encoder.encodeInt32(id.id, forKey: "i.i") + if let threadId { + encoder.encodeInt64(threadId, forKey: "tid") + } else { + encoder.encodeNil(forKey: "tid") + } } } } diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift index c61b94706a2..958ac84482f 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift @@ -8,8 +8,12 @@ public struct Namespaces { public static let SecretIncoming: Int32 = 2 public static let ScheduledCloud: Int32 = 3 public static let ScheduledLocal: Int32 = 4 + public static let QuickReplyCloud: Int32 = 5 + public static let QuickReplyLocal: Int32 = 6 public static let allScheduled: Set = Set([Namespaces.Message.ScheduledCloud, Namespaces.Message.ScheduledLocal]) + public static let allQuickReply: Set = Set([Namespaces.Message.QuickReplyCloud, Namespaces.Message.QuickReplyLocal]) + public static let allNonRegular: Set = Set([Namespaces.Message.ScheduledCloud, Namespaces.Message.ScheduledLocal, Namespaces.Message.QuickReplyCloud, Namespaces.Message.QuickReplyLocal]) } public struct Media { @@ -280,6 +284,8 @@ private enum PreferencesKeyValues: Int32 { case audioTranscriptionTrialState = 33 case didCacheSavedMessageTagsPrefix = 34 case displaySavedChatsAsTopics = 35 + case shortcutMessages = 37 + case timezoneList = 38 } public func applicationSpecificPreferencesKey(_ value: Int32) -> ValueBoxKey { @@ -463,6 +469,18 @@ public struct PreferencesKeys { key.setInt32(0, value: PreferencesKeyValues.displaySavedChatsAsTopics.rawValue) return key } + + public static func shortcutMessages() -> ValueBoxKey { + let key = ValueBoxKey(length: 4) + key.setInt32(0, value: PreferencesKeyValues.shortcutMessages.rawValue) + return key + } + + public static func timezoneList() -> ValueBoxKey { + let key = ValueBoxKey(length: 4) + key.setInt32(0, value: PreferencesKeyValues.timezoneList.rawValue) + return key + } } private enum SharedDataKeyValues: Int32 { diff --git a/submodules/TelegramCore/Sources/TelegramEngine/AccountData/TelegramEngineAccountData.swift b/submodules/TelegramCore/Sources/TelegramEngine/AccountData/TelegramEngineAccountData.swift index f6d798ebe75..7c50cc13872 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/AccountData/TelegramEngineAccountData.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/AccountData/TelegramEngineAccountData.swift @@ -105,5 +105,105 @@ public extension TelegramEngine { |> ignoreValues |> then(remoteApply) } + + public func updateAccountBusinessHours(businessHours: TelegramBusinessHours?) -> Signal { + let peerId = self.account.peerId + + var flags: Int32 = 0 + if businessHours != nil { + flags |= 1 << 0 + } + let remoteApply: Signal = self.account.network.request(Api.functions.account.updateBusinessWorkHours(flags: flags, businessWorkHours: businessHours?.apiBusinessHours)) + |> `catch` { _ -> Signal in + return .single(.boolFalse) + } + |> ignoreValues + + return self.account.postbox.transaction { transaction -> Void in + transaction.updatePeerCachedData(peerIds: Set([peerId]), update: { _, current in + let current = current as? CachedUserData ?? CachedUserData() + return current.withUpdatedBusinessHours(businessHours) + }) + } + |> ignoreValues + |> then(remoteApply) + } + + public func updateAccountBusinessLocation(businessLocation: TelegramBusinessLocation?) -> Signal { + let peerId = self.account.peerId + + var flags: Int32 = 0 + + var inputGeoPoint: Api.InputGeoPoint? + var inputAddress: String? + if let businessLocation { + flags |= 1 << 0 + inputAddress = businessLocation.address + + inputGeoPoint = businessLocation.coordinates?.apiInputGeoPoint + if inputGeoPoint != nil { + flags |= 1 << 1 + } + } + + let remoteApply: Signal = self.account.network.request(Api.functions.account.updateBusinessLocation(flags: flags, geoPoint: inputGeoPoint, address: inputAddress)) + |> `catch` { _ -> Signal in + return .single(.boolFalse) + } + |> ignoreValues + + return self.account.postbox.transaction { transaction -> Void in + transaction.updatePeerCachedData(peerIds: Set([peerId]), update: { _, current in + let current = current as? CachedUserData ?? CachedUserData() + return current.withUpdatedBusinessLocation(businessLocation) + }) + } + |> ignoreValues + |> then(remoteApply) + } + + public func shortcutMessageList(onlyRemote: Bool) -> Signal { + return _internal_shortcutMessageList(account: self.account, onlyRemote: onlyRemote) + } + + public func keepShortcutMessageListUpdated() -> Signal { + return _internal_keepShortcutMessagesUpdated(account: self.account) + } + + public func editMessageShortcut(id: Int32, shortcut: String) { + let _ = _internal_editMessageShortcut(account: self.account, id: id, shortcut: shortcut).startStandalone() + } + + public func deleteMessageShortcuts(ids: [Int32]) { + let _ = _internal_deleteMessageShortcuts(account: self.account, ids: ids).startStandalone() + } + + public func reorderMessageShortcuts(ids: [Int32], completion: @escaping () -> Void) { + let _ = _internal_reorderMessageShortcuts(account: self.account, ids: ids, localCompletion: completion).startStandalone() + } + + public func sendMessageShortcut(peerId: EnginePeer.Id, id: Int32) { + let _ = _internal_sendMessageShortcut(account: self.account, peerId: peerId, id: id).startStandalone() + } + + public func cachedTimeZoneList() -> Signal { + return _internal_cachedTimeZoneList(account: self.account) + } + + public func keepCachedTimeZoneListUpdated() -> Signal { + return _internal_keepCachedTimeZoneListUpdated(account: self.account) + } + + public func updateBusinessGreetingMessage(greetingMessage: TelegramBusinessGreetingMessage?) -> Signal { + return _internal_updateBusinessGreetingMessage(account: self.account, greetingMessage: greetingMessage) + } + + public func updateBusinessAwayMessage(awayMessage: TelegramBusinessAwayMessage?) -> Signal { + return _internal_updateBusinessAwayMessage(account: self.account, awayMessage: awayMessage) + } + + public func setAccountConnectedBot(bot: TelegramAccountConnectedBot?) -> Signal { + return _internal_setAccountConnectedBot(account: self.account, bot: bot) + } } } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Data/PeersData.swift b/submodules/TelegramCore/Sources/TelegramEngine/Data/PeersData.swift index 08f63d5fab2..a036a33a419 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Data/PeersData.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Data/PeersData.swift @@ -4,6 +4,7 @@ import Postbox public typealias EngineExportedPeerInvitation = ExportedInvitation public typealias EngineSecretChatKeyFingerprint = SecretChatKeyFingerprint + public enum EnginePeerCachedInfoItem { case known(T) case unknown @@ -1113,6 +1114,91 @@ public extension TelegramEngine.EngineData.Item { } } + public struct SlowmodeTimeout: TelegramEngineDataItem, TelegramEngineMapKeyDataItem, PostboxViewDataItem { + public typealias Result = Int32? + + fileprivate var id: EnginePeer.Id + public var mapKey: EnginePeer.Id { + return self.id + } + + public init(id: EnginePeer.Id) { + self.id = id + } + + var key: PostboxViewKey { + return .cachedPeerData(peerId: self.id) + } + + func extract(view: PostboxView) -> Result { + guard let view = view as? CachedPeerDataView else { + preconditionFailure() + } + if let cachedData = view.cachedPeerData as? CachedChannelData { + return cachedData.slowModeTimeout + } else { + return nil + } + } + } + public struct SlowmodeValidUntilTimeout: TelegramEngineDataItem, TelegramEngineMapKeyDataItem, PostboxViewDataItem { + public typealias Result = Int32? + + fileprivate var id: EnginePeer.Id + public var mapKey: EnginePeer.Id { + return self.id + } + + public init(id: EnginePeer.Id) { + self.id = id + } + + var key: PostboxViewKey { + return .cachedPeerData(peerId: self.id) + } + + func extract(view: PostboxView) -> Result { + guard let view = view as? CachedPeerDataView else { + preconditionFailure() + } + if let cachedData = view.cachedPeerData as? CachedChannelData { + return cachedData.slowModeValidUntilTimestamp + } + return nil + } + } + + public struct CanAvoidGroupRestrictions: TelegramEngineDataItem, TelegramEngineMapKeyDataItem, PostboxViewDataItem { + public typealias Result = Bool + + fileprivate var id: EnginePeer.Id + public var mapKey: EnginePeer.Id { + return self.id + } + + public init(id: EnginePeer.Id) { + self.id = id + } + + var key: PostboxViewKey { + return .cachedPeerData(peerId: self.id) + } + + func extract(view: PostboxView) -> Result { + guard let view = view as? CachedPeerDataView else { + preconditionFailure() + } + if let cachedData = view.cachedPeerData as? CachedChannelData { + if let boostsToUnrestrict = cachedData.boostsToUnrestrict { + let appliedBoosts = cachedData.appliedBoosts ?? 0 + return boostsToUnrestrict <= appliedBoosts + } + } + return true + } + } + + public struct IsPremiumRequiredForMessaging: TelegramEngineDataItem, TelegramEngineMapKeyDataItem, AnyPostboxViewDataItem { public typealias Result = Bool @@ -1358,6 +1444,145 @@ public extension TelegramEngine.EngineData.Item { } } } + + public struct BusinessHours: TelegramEngineDataItem, TelegramEngineMapKeyDataItem, PostboxViewDataItem { + public typealias Result = TelegramBusinessHours? + + fileprivate var id: EnginePeer.Id + public var mapKey: EnginePeer.Id { + return self.id + } + public init(id: EnginePeer.Id) { + self.id = id + } + + var key: PostboxViewKey { + return .cachedPeerData(peerId: self.id) + } + + func extract(view: PostboxView) -> Result { + guard let view = view as? CachedPeerDataView else { + preconditionFailure() + } + if let cachedData = view.cachedPeerData as? CachedUserData { + return cachedData.businessHours + } else { + return nil + } + } + } + + public struct BusinessLocation: TelegramEngineDataItem, TelegramEngineMapKeyDataItem, PostboxViewDataItem { + public typealias Result = TelegramBusinessLocation? + + fileprivate var id: EnginePeer.Id + public var mapKey: EnginePeer.Id { + return self.id + } + + public init(id: EnginePeer.Id) { + self.id = id + } + + var key: PostboxViewKey { + return .cachedPeerData(peerId: self.id) + } + + func extract(view: PostboxView) -> Result { + guard let view = view as? CachedPeerDataView else { + preconditionFailure() + } + if let cachedData = view.cachedPeerData as? CachedUserData { + return cachedData.businessLocation + } else { + return nil + } + } + } + + public struct BusinessGreetingMessage: TelegramEngineDataItem, TelegramEngineMapKeyDataItem, PostboxViewDataItem { + public typealias Result = TelegramBusinessGreetingMessage? + + fileprivate var id: EnginePeer.Id + public var mapKey: EnginePeer.Id { + return self.id + } + + public init(id: EnginePeer.Id) { + self.id = id + } + + var key: PostboxViewKey { + return .cachedPeerData(peerId: self.id) + } + + func extract(view: PostboxView) -> Result { + guard let view = view as? CachedPeerDataView else { + preconditionFailure() + } + if let cachedData = view.cachedPeerData as? CachedUserData { + return cachedData.greetingMessage + } else { + return nil + } + } + } + + public struct BusinessAwayMessage: TelegramEngineDataItem, TelegramEngineMapKeyDataItem, PostboxViewDataItem { + public typealias Result = TelegramBusinessAwayMessage? + + fileprivate var id: EnginePeer.Id + public var mapKey: EnginePeer.Id { + return self.id + } + + public init(id: EnginePeer.Id) { + self.id = id + } + + var key: PostboxViewKey { + return .cachedPeerData(peerId: self.id) + } + + func extract(view: PostboxView) -> Result { + guard let view = view as? CachedPeerDataView else { + preconditionFailure() + } + if let cachedData = view.cachedPeerData as? CachedUserData { + return cachedData.awayMessage + } else { + return nil + } + } + } + + public struct BusinessConnectedBot: TelegramEngineDataItem, TelegramEngineMapKeyDataItem, PostboxViewDataItem { + public typealias Result = TelegramAccountConnectedBot? + + fileprivate var id: EnginePeer.Id + public var mapKey: EnginePeer.Id { + return self.id + } + + public init(id: EnginePeer.Id) { + self.id = id + } + + var key: PostboxViewKey { + return .cachedPeerData(peerId: self.id) + } + + func extract(view: PostboxView) -> Result { + guard let view = view as? CachedPeerDataView else { + preconditionFailure() + } + if let cachedData = view.cachedPeerData as? CachedUserData { + return cachedData.connectedBot + } else { + return nil + } + } + } } } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Data/TelegramEngineData.swift b/submodules/TelegramCore/Sources/TelegramEngine/Data/TelegramEngineData.swift index b72ee5fa4b2..dc59baf52f3 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Data/TelegramEngineData.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Data/TelegramEngineData.swift @@ -419,6 +419,7 @@ public extension TelegramEngine { T5.Result, T6.Result, T7.Result + ), NoError> { return self._subscribe(items: [ @@ -503,6 +504,72 @@ public extension TelegramEngine { ) } } + public func subscribe< + T0: TelegramEngineDataItem, + T1: TelegramEngineDataItem, + T2: TelegramEngineDataItem, + T3: TelegramEngineDataItem, + T4: TelegramEngineDataItem, + T5: TelegramEngineDataItem, + T6: TelegramEngineDataItem, + T7: TelegramEngineDataItem, + T8: TelegramEngineDataItem, + T9: TelegramEngineDataItem + + >( + _ t0: T0, + _ t1: T1, + _ t2: T2, + _ t3: T3, + _ t4: T4, + _ t5: T5, + _ t6: T6, + _ t7: T7, + _ t8: T8, + _ t9: T9 + + ) -> Signal< + ( + T0.Result, + T1.Result, + T2.Result, + T3.Result, + T4.Result, + T5.Result, + T6.Result, + T7.Result, + T8.Result, + T9.Result + ), + NoError> { + return self._subscribe(items: [ + t0 as! AnyPostboxViewDataItem, + t1 as! AnyPostboxViewDataItem, + t2 as! AnyPostboxViewDataItem, + t3 as! AnyPostboxViewDataItem, + t4 as! AnyPostboxViewDataItem, + t5 as! AnyPostboxViewDataItem, + t6 as! AnyPostboxViewDataItem, + t7 as! AnyPostboxViewDataItem, + t8 as! AnyPostboxViewDataItem, + t9 as! AnyPostboxViewDataItem + ]) + |> map { results -> (T0.Result, T1.Result, T2.Result, T3.Result, T4.Result, T5.Result, T6.Result, T7.Result, T8.Result, T9.Result) in + return ( + results[0] as! T0.Result, + results[1] as! T1.Result, + results[2] as! T2.Result, + results[3] as! T3.Result, + results[4] as! T4.Result, + results[5] as! T5.Result, + results[6] as! T6.Result, + results[7] as! T7.Result, + results[8] as! T8.Result, + results[9] as! T9.Result + ) + } + } + public func get< T0: TelegramEngineDataItem, diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/DeleteMessagesInteractively.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/DeleteMessagesInteractively.swift index 12e0fa696a7..6fb62b81421 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/DeleteMessagesInteractively.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/DeleteMessagesInteractively.swift @@ -12,35 +12,51 @@ func _internal_deleteMessagesInteractively(account: Account, messageIds: [Messag } func deleteMessagesInteractively(transaction: Transaction, stateManager: AccountStateManager?, postbox: Postbox, messageIds initialMessageIds: [MessageId], type: InteractiveMessagesDeletionType, deleteAllInGroup: Bool = false, removeIfPossiblyDelivered: Bool) { - var messageIds: [MessageId] = [] + var messageIds: [MessageAndThreadId] = [] if deleteAllInGroup { + var tempIds: [MessageId] = initialMessageIds for id in initialMessageIds { if let group = transaction.getMessageGroup(id) ?? transaction.getMessageForwardedGroup(id) { for message in group { - if !messageIds.contains(message.id) { - messageIds.append(message.id) + if !tempIds.contains(message.id) { + tempIds.append(message.id) } } } else { - messageIds.append(id) + tempIds.append(id) + } + } + + messageIds = tempIds.map { id in + if id.namespace == Namespaces.Message.QuickReplyCloud { + if let message = transaction.getMessage(id) { + return MessageAndThreadId(messageId: id, threadId: message.threadId) + } else { + return MessageAndThreadId(messageId: id, threadId: nil) + } + } else { + return MessageAndThreadId(messageId: id, threadId: nil) } } } else { - messageIds = initialMessageIds - } - - var messageIdsByPeerId: [PeerId: [MessageId]] = [:] - for id in messageIds { - if messageIdsByPeerId[id.peerId] == nil { - messageIdsByPeerId[id.peerId] = [id] - } else { - messageIdsByPeerId[id.peerId]!.append(id) + messageIds = initialMessageIds.map { id in + if id.namespace == Namespaces.Message.QuickReplyCloud { + if let message = transaction.getMessage(id) { + return MessageAndThreadId(messageId: id, threadId: message.threadId) + } else { + return MessageAndThreadId(messageId: id, threadId: nil) + } + } else { + return MessageAndThreadId(messageId: id, threadId: nil) + } } } var uniqueIds: [Int64: PeerId] = [:] - for (peerId, peerMessageIds) in messageIdsByPeerId { + for (peerAndThreadId, peerMessageIds) in messagesIdsGroupedByPeerId(messageIds) { + let peerId = peerAndThreadId.peerId + let threadId = peerAndThreadId.threadId for id in peerMessageIds { if let message = transaction.getMessage(id) { for attribute in message.attributes { @@ -53,13 +69,13 @@ func deleteMessagesInteractively(transaction: Transaction, stateManager: Account if peerId.namespace == Namespaces.Peer.CloudChannel || peerId.namespace == Namespaces.Peer.CloudGroup || peerId.namespace == Namespaces.Peer.CloudUser { let remoteMessageIds = peerMessageIds.filter { id in - if id.namespace == Namespaces.Message.Local || id.namespace == Namespaces.Message.ScheduledLocal { + if id.namespace == Namespaces.Message.Local || id.namespace == Namespaces.Message.ScheduledLocal || id.namespace == Namespaces.Message.QuickReplyLocal { return false } return true } if !remoteMessageIds.isEmpty { - cloudChatAddRemoveMessagesOperation(transaction: transaction, peerId: peerId, messageIds: remoteMessageIds, type: CloudChatRemoveMessagesType(type)) + cloudChatAddRemoveMessagesOperation(transaction: transaction, peerId: peerId, threadId: threadId, messageIds: remoteMessageIds, type: CloudChatRemoveMessagesType(type)) } } else if peerId.namespace == Namespaces.Peer.SecretChat { if let state = transaction.getPeerChatState(peerId) as? SecretChatState { @@ -87,9 +103,9 @@ func deleteMessagesInteractively(transaction: Transaction, stateManager: Account } } } - _internal_deleteMessages(transaction: transaction, mediaBox: postbox.mediaBox, ids: messageIds) + _internal_deleteMessages(transaction: transaction, mediaBox: postbox.mediaBox, ids: messageIds.map(\.messageId)) - stateManager?.notifyDeletedMessages(messageIds: messageIds) + stateManager?.notifyDeletedMessages(messageIds: messageIds.map(\.messageId)) if !uniqueIds.isEmpty && removeIfPossiblyDelivered { stateManager?.removePossiblyDeliveredMessages(uniqueIds: uniqueIds) @@ -102,7 +118,7 @@ func _internal_clearHistoryInRangeInteractively(postbox: Postbox, peerId: PeerId cloudChatAddClearHistoryOperation(transaction: transaction, peerId: peerId, threadId: threadId, explicitTopMessageId: nil, minTimestamp: minTimestamp, maxTimestamp: maxTimestamp, type: CloudChatClearHistoryType(type)) if type == .scheduledMessages { } else { - _internal_clearHistoryInRange(transaction: transaction, mediaBox: postbox.mediaBox, peerId: peerId, threadId: threadId, minTimestamp: minTimestamp, maxTimestamp: maxTimestamp, namespaces: .not(Namespaces.Message.allScheduled)) + _internal_clearHistoryInRange(transaction: transaction, mediaBox: postbox.mediaBox, peerId: peerId, threadId: threadId, minTimestamp: minTimestamp, maxTimestamp: maxTimestamp, namespaces: .not(Namespaces.Message.allNonRegular)) } } else if peerId.namespace == Namespaces.Peer.SecretChat { /*_internal_clearHistory(transaction: transaction, mediaBox: postbox.mediaBox, peerId: peerId, namespaces: .all) @@ -141,7 +157,7 @@ func _internal_clearHistoryInteractively(postbox: Postbox, peerId: PeerId, threa topIndex = topMessage.index } - _internal_clearHistory(transaction: transaction, mediaBox: postbox.mediaBox, peerId: peerId, threadId: threadId, namespaces: .not(Namespaces.Message.allScheduled)) + _internal_clearHistory(transaction: transaction, mediaBox: postbox.mediaBox, peerId: peerId, threadId: threadId, namespaces: .not(Namespaces.Message.allNonRegular)) if let cachedData = transaction.getPeerCachedData(peerId: peerId) as? CachedChannelData, let migrationReference = cachedData.migrationReference { cloudChatAddClearHistoryOperation(transaction: transaction, peerId: migrationReference.maxMessageId.peerId, threadId: threadId, explicitTopMessageId: MessageId(peerId: migrationReference.maxMessageId.peerId, namespace: migrationReference.maxMessageId.namespace, id: migrationReference.maxMessageId.id + 1), minTimestamp: nil, maxTimestamp: nil, type: CloudChatClearHistoryType(type)) _internal_clearHistory(transaction: transaction, mediaBox: postbox.mediaBox, peerId: migrationReference.maxMessageId.peerId, threadId: threadId, namespaces: .all) diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/ForwardGame.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/ForwardGame.swift index b61557dec8a..f7e5d428338 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/ForwardGame.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/ForwardGame.swift @@ -14,7 +14,7 @@ func _internal_forwardGameWithScore(account: Account, messageId: MessageId, to p flags |= (1 << 13) } - return account.network.request(Api.functions.messages.forwardMessages(flags: flags, fromPeer: fromInputPeer, id: [messageId.id], randomId: [Int64.random(in: Int64.min ... Int64.max)], toPeer: toInputPeer, topMsgId: threadId.flatMap { Int32(clamping: $0) }, scheduleDate: nil, sendAs: sendAsInputPeer)) + return account.network.request(Api.functions.messages.forwardMessages(flags: flags, fromPeer: fromInputPeer, id: [messageId.id], randomId: [Int64.random(in: Int64.min ... Int64.max)], toPeer: toInputPeer, topMsgId: threadId.flatMap { Int32(clamping: $0) }, scheduleDate: nil, sendAs: sendAsInputPeer, quickReplyShortcut: nil)) |> map(Optional.init) |> `catch` { _ -> Signal in return .single(nil) diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/Polls.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/Polls.swift index 1ddb77458b2..d7174c3e951 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/Polls.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/Polls.swift @@ -135,7 +135,7 @@ func _internal_requestClosePoll(postbox: Postbox, network: Network, stateManager pollMediaFlags |= 1 << 1 } - return network.request(Api.functions.messages.editMessage(flags: flags, peer: inputPeer, id: messageId.id, message: nil, media: .inputMediaPoll(flags: pollMediaFlags, poll: .poll(id: poll.pollId.id, flags: pollFlags, question: poll.text, answers: poll.options.map({ $0.apiOption }), closePeriod: poll.deadlineTimeout, closeDate: nil), correctAnswers: correctAnswers, solution: mappedSolution, solutionEntities: mappedSolutionEntities), replyMarkup: nil, entities: nil, scheduleDate: nil)) + return network.request(Api.functions.messages.editMessage(flags: flags, peer: inputPeer, id: messageId.id, message: nil, media: .inputMediaPoll(flags: pollMediaFlags, poll: .poll(id: poll.pollId.id, flags: pollFlags, question: poll.text, answers: poll.options.map({ $0.apiOption }), closePeriod: poll.deadlineTimeout, closeDate: nil), correctAnswers: correctAnswers, solution: mappedSolution, solutionEntities: mappedSolutionEntities), replyMarkup: nil, entities: nil, scheduleDate: nil, quickReplyShortcutId: nil)) |> map(Optional.init) |> `catch` { _ -> Signal in return .single(nil) diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/QuickReplyMessages.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/QuickReplyMessages.swift new file mode 100644 index 00000000000..a7e321c872b --- /dev/null +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/QuickReplyMessages.swift @@ -0,0 +1,899 @@ +import Foundation +import Postbox +import SwiftSignalKit +import TelegramApi +import MtProtoKit + +public final class QuickReplyMessageShortcut: Codable, Equatable { + public let id: Int32 + public let shortcut: String + + public init(id: Int32, shortcut: String) { + self.id = id + self.shortcut = shortcut + } + + public static func ==(lhs: QuickReplyMessageShortcut, rhs: QuickReplyMessageShortcut) -> Bool { + if lhs.id != rhs.id { + return false + } + if lhs.shortcut != rhs.shortcut { + return false + } + return true + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: StringCodingKey.self) + + self.id = try container.decode(Int32.self, forKey: "id") + self.shortcut = try container.decode(String.self, forKey: "shortcut") + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: StringCodingKey.self) + + try container.encode(self.id, forKey: "id") + try container.encode(self.shortcut, forKey: "shortcut") + } +} + +struct QuickReplyMessageShortcutsState: Codable, Equatable { + var shortcuts: [QuickReplyMessageShortcut] + + init(shortcuts: [QuickReplyMessageShortcut]) { + self.shortcuts = shortcuts + } +} + +public final class ShortcutMessageList: Equatable { + public final class Item: Equatable { + public let id: Int32? + public let shortcut: String + public let topMessage: EngineMessage + public let totalCount: Int + + public init(id: Int32?, shortcut: String, topMessage: EngineMessage, totalCount: Int) { + self.id = id + self.shortcut = shortcut + self.topMessage = topMessage + self.totalCount = totalCount + } + + public static func ==(lhs: Item, rhs: Item) -> Bool { + if lhs === rhs { + return true + } + if lhs.id != rhs.id { + return false + } + if lhs.shortcut != rhs.shortcut { + return false + } + if lhs.topMessage != rhs.topMessage { + return false + } + if lhs.totalCount != rhs.totalCount { + return false + } + return true + } + } + + public let items: [Item] + public let isLoading: Bool + + public init(items: [Item], isLoading: Bool) { + self.items = items + self.isLoading = isLoading + } + + public static func ==(lhs: ShortcutMessageList, rhs: ShortcutMessageList) -> Bool { + if lhs === rhs { + return true + } + if lhs.items != rhs.items { + return false + } + if lhs.isLoading != rhs.isLoading { + return false + } + return true + } +} + +func _internal_quickReplyMessageShortcutsState(account: Account) -> Signal { + let viewKey: PostboxViewKey = .preferences(keys: Set([PreferencesKeys.shortcutMessages()])) + return account.postbox.combinedView(keys: [viewKey]) + |> map { views -> QuickReplyMessageShortcutsState? in + guard let view = views.views[viewKey] as? PreferencesView else { + return nil + } + guard let value = view.values[PreferencesKeys.shortcutMessages()]?.get(QuickReplyMessageShortcutsState.self) else { + return nil + } + return value + } +} + +func _internal_keepShortcutMessagesUpdated(account: Account) -> Signal { + let updateSignal = _internal_shortcutMessageList(account: account, onlyRemote: true) + |> take(1) + |> mapToSignal { list -> Signal in + var acc: UInt64 = 0 + for item in list.items { + guard let itemId = item.id else { + continue + } + combineInt64Hash(&acc, with: UInt64(itemId)) + combineInt64Hash(&acc, with: md5StringHash(item.shortcut)) + combineInt64Hash(&acc, with: UInt64(item.topMessage.id.id)) + + var editTimestamp: Int32 = 0 + inner: for attribute in item.topMessage.attributes { + if let attribute = attribute as? EditedMessageAttribute { + editTimestamp = attribute.date + break inner + } + } + combineInt64Hash(&acc, with: UInt64(editTimestamp)) + } + + return account.network.request(Api.functions.messages.getQuickReplies(hash: finalizeInt64Hash(acc))) + |> map(Optional.init) + |> `catch` { _ -> Signal in + return .single(nil) + } + |> mapToSignal { result -> Signal in + guard let result else { + return .complete() + } + + return account.postbox.transaction { transaction in + var state = transaction.getPreferencesEntry(key: PreferencesKeys.shortcutMessages())?.get(QuickReplyMessageShortcutsState.self) ?? QuickReplyMessageShortcutsState(shortcuts: []) + switch result { + case let .quickReplies(quickReplies, messages, chats, users): + let previousShortcuts = state.shortcuts + state.shortcuts.removeAll() + + let parsedPeers = AccumulatedPeers(transaction: transaction, chats: chats, users: users) + updatePeers(transaction: transaction, accountPeerId: account.peerId, peers: parsedPeers) + + var storeMessages: [StoreMessage] = [] + + for message in messages { + if let message = StoreMessage(apiMessage: message, accountPeerId: account.peerId, peerIsForum: false) { + storeMessages.append(message) + } + } + let _ = transaction.addMessages(storeMessages, location: .Random) + var topMessageIds: [Int32: Int32] = [:] + + for quickReply in quickReplies { + switch quickReply { + case let .quickReply(shortcutId, shortcut, topMessage, _): + state.shortcuts.append(QuickReplyMessageShortcut( + id: shortcutId, + shortcut: shortcut + )) + topMessageIds[shortcutId] = topMessage + } + } + + if previousShortcuts != state.shortcuts { + for shortcut in previousShortcuts { + if let topMessageId = topMessageIds[shortcut.id] { + //TODO:remove earlier + let _ = topMessageId + } else { + let existingCloudMessages = transaction.getMessagesWithThreadId(peerId: account.peerId, namespace: Namespaces.Message.QuickReplyCloud, threadId: Int64(shortcut.id), from: MessageIndex.lowerBound(peerId: account.peerId, namespace: Namespaces.Message.QuickReplyCloud), includeFrom: false, to: MessageIndex.upperBound(peerId: account.peerId, namespace: Namespaces.Message.QuickReplyCloud), limit: 1000) + let existingLocalMessages = transaction.getMessagesWithThreadId(peerId: account.peerId, namespace: Namespaces.Message.QuickReplyLocal, threadId: Int64(shortcut.id), from: MessageIndex.lowerBound(peerId: account.peerId, namespace: Namespaces.Message.QuickReplyLocal), includeFrom: false, to: MessageIndex.upperBound(peerId: account.peerId, namespace: Namespaces.Message.QuickReplyLocal), limit: 1000) + + transaction.deleteMessages(existingCloudMessages.map(\.id), forEachMedia: nil) + transaction.deleteMessages(existingLocalMessages.map(\.id), forEachMedia: nil) + } + } + } + case .quickRepliesNotModified: + break + } + + transaction.setPreferencesEntry(key: PreferencesKeys.shortcutMessages(), value: PreferencesEntry(state)) + } + |> ignoreValues + } + } + + return updateSignal +} + +func _internal_shortcutMessageList(account: Account, onlyRemote: Bool) -> Signal { + let pendingShortcuts: Signal<[String: EngineMessage], NoError> + if onlyRemote { + pendingShortcuts = .single([:]) + } else { + pendingShortcuts = account.postbox.aroundMessageHistoryViewForLocation(.peer(peerId: account.peerId, threadId: nil), anchor: .upperBound, ignoreMessagesInTimestampRange: nil, count: 100, fixedCombinedReadStates: nil, topTaggedMessageIdNamespaces: Set(), tag: nil, appendMessagesFromTheSameGroup: false, namespaces: .just(Set([Namespaces.Message.QuickReplyLocal])), orderStatistics: []) + |> map { view , _, _ -> [String: EngineMessage] in + var topMessages: [String: EngineMessage] = [:] + for entry in view.entries { + var shortcut: String? + inner: for attribute in entry.message.attributes { + if let attribute = attribute as? OutgoingQuickReplyMessageAttribute { + shortcut = attribute.shortcut + break inner + } + } + if let shortcut { + if let currentTopMessage = topMessages[shortcut] { + if entry.message.index < currentTopMessage.index { + topMessages[shortcut] = EngineMessage(entry.message) + } + } else { + topMessages[shortcut] = EngineMessage(entry.message) + } + } + } + return topMessages + } + |> distinctUntilChanged + } + + return combineLatest(queue: .mainQueue(), + _internal_quickReplyMessageShortcutsState(account: account) |> distinctUntilChanged, + pendingShortcuts + ) + |> mapToSignal { state, pendingShortcuts -> Signal in + guard let state else { + return .single(ShortcutMessageList(items: [], isLoading: true)) + } + + var keys: [PostboxViewKey] = [] + var historyViewKeys: [Int32: PostboxViewKey] = [:] + var summaryKeys: [Int32: PostboxViewKey] = [:] + for shortcut in state.shortcuts { + let historyViewKey: PostboxViewKey = .historyView(PostboxViewKey.HistoryView( + peerId: account.peerId, + threadId: Int64(shortcut.id), + clipHoles: false, + trackHoles: false, + anchor: .lowerBound, + appendMessagesFromTheSameGroup: false, + namespaces: .just(Set([Namespaces.Message.QuickReplyCloud])), + count: 10 + )) + historyViewKeys[shortcut.id] = historyViewKey + keys.append(historyViewKey) + + let summaryKey: PostboxViewKey = .historyTagSummaryView(tag: [], peerId: account.peerId, threadId: Int64(shortcut.id), namespace: Namespaces.Message.QuickReplyCloud, customTag: nil) + summaryKeys[shortcut.id] = summaryKey + keys.append(summaryKey) + } + return account.postbox.combinedView( + keys: keys + ) + |> map { views -> ShortcutMessageList in + var items: [ShortcutMessageList.Item] = [] + + for shortcut in state.shortcuts { + guard let historyViewKey = historyViewKeys[shortcut.id], let historyView = views.views[historyViewKey] as? MessageHistoryView else { + continue + } + + var totalCount = 1 + if let summaryKey = summaryKeys[shortcut.id], let summaryView = views.views[summaryKey] as? MessageHistoryTagSummaryView { + if let count = summaryView.count { + totalCount = max(1, Int(count)) + } + } + + if let entry = historyView.entries.first { + items.append(ShortcutMessageList.Item(id: shortcut.id, shortcut: shortcut.shortcut, topMessage: EngineMessage(entry.message), totalCount: totalCount)) + } + } + + for (shortcut, message) in pendingShortcuts.sorted(by: { $0.key < $1.key }) { + if !items.contains(where: { $0.shortcut == shortcut }) { + items.append(ShortcutMessageList.Item( + id: nil, + shortcut: shortcut, + topMessage: message, + totalCount: 1 + )) + } + } + + return ShortcutMessageList(items: items, isLoading: false) + } + |> distinctUntilChanged + } +} + +func _internal_editMessageShortcut(account: Account, id: Int32, shortcut: String) -> Signal { + let remoteApply = account.network.request(Api.functions.messages.editQuickReplyShortcut(shortcutId: id, shortcut: shortcut)) + |> `catch` { _ -> Signal in + return .single(.boolFalse) + } + |> mapToSignal { _ -> Signal in + return .complete() + } + + return account.postbox.transaction { transaction in + var state = transaction.getPreferencesEntry(key: PreferencesKeys.shortcutMessages())?.get(QuickReplyMessageShortcutsState.self) ?? QuickReplyMessageShortcutsState(shortcuts: []) + if let index = state.shortcuts.firstIndex(where: { $0.id == id }) { + state.shortcuts[index] = QuickReplyMessageShortcut(id: id, shortcut: shortcut) + } + transaction.setPreferencesEntry(key: PreferencesKeys.shortcutMessages(), value: PreferencesEntry(state)) + } + |> ignoreValues + |> then(remoteApply) +} + +func _internal_deleteMessageShortcuts(account: Account, ids: [Int32]) -> Signal { + return account.postbox.transaction { transaction in + var state = transaction.getPreferencesEntry(key: PreferencesKeys.shortcutMessages())?.get(QuickReplyMessageShortcutsState.self) ?? QuickReplyMessageShortcutsState(shortcuts: []) + + for id in ids { + if let index = state.shortcuts.firstIndex(where: { $0.id == id }) { + state.shortcuts.remove(at: index) + } + } + transaction.setPreferencesEntry(key: PreferencesKeys.shortcutMessages(), value: PreferencesEntry(state)) + + for id in ids { + cloudChatAddClearHistoryOperation(transaction: transaction, peerId: account.peerId, threadId: Int64(id), explicitTopMessageId: nil, minTimestamp: nil, maxTimestamp: nil, type: .quickReplyMessages) + } + } + |> ignoreValues +} + +func _internal_reorderMessageShortcuts(account: Account, ids: [Int32], localCompletion: @escaping () -> Void) -> Signal { + let remoteApply = account.network.request(Api.functions.messages.reorderQuickReplies(order: ids)) + |> `catch` { _ -> Signal in + return .single(.boolFalse) + } + |> mapToSignal { _ -> Signal in + return .complete() + } + + return account.postbox.transaction { transaction in + var state = transaction.getPreferencesEntry(key: PreferencesKeys.shortcutMessages())?.get(QuickReplyMessageShortcutsState.self) ?? QuickReplyMessageShortcutsState(shortcuts: []) + + let previousShortcuts = state.shortcuts + state.shortcuts.removeAll() + for id in ids { + if let index = previousShortcuts.firstIndex(where: { $0.id == id }) { + state.shortcuts.append(previousShortcuts[index]) + } + } + for shortcut in previousShortcuts { + if !state.shortcuts.contains(where: { $0.id == shortcut.id }) { + state.shortcuts.append(shortcut) + } + } + + transaction.setPreferencesEntry(key: PreferencesKeys.shortcutMessages(), value: PreferencesEntry(state)) + } + |> ignoreValues + |> afterCompleted { + localCompletion() + } + |> then(remoteApply) +} + +func _internal_sendMessageShortcut(account: Account, peerId: PeerId, id: Int32) -> Signal { + return account.postbox.transaction { transaction -> Peer? in + return transaction.getPeer(peerId) + } + |> mapToSignal { peer -> Signal in + guard let peer, let inputPeer = apiInputPeer(peer) else { + return .complete() + } + return account.network.request(Api.functions.messages.sendQuickReplyMessages(peer: inputPeer, shortcutId: id)) + |> map(Optional.init) + |> `catch` { _ -> Signal in + return .single(nil) + } + |> mapToSignal { result -> Signal in + if let result { + account.stateManager.addUpdates(result) + } + return .complete() + } + } +} + +func _internal_applySentQuickReplyMessage(transaction: Transaction, shortcut: String, quickReplyId: Int32) { + var state = transaction.getPreferencesEntry(key: PreferencesKeys.shortcutMessages())?.get(QuickReplyMessageShortcutsState.self) ?? QuickReplyMessageShortcutsState(shortcuts: []) + + if !state.shortcuts.contains(where: { $0.id == quickReplyId }) { + state.shortcuts.append(QuickReplyMessageShortcut(id: quickReplyId, shortcut: shortcut)) + transaction.setPreferencesEntry(key: PreferencesKeys.shortcutMessages(), value: PreferencesEntry(state)) + } +} + +public final class TelegramBusinessRecipients: Codable, Equatable { + public struct Categories: OptionSet { + public var rawValue: Int32 + + public init(rawValue: Int32) { + self.rawValue = rawValue + } + + public static let existingChats = Categories(rawValue: 1 << 0) + public static let newChats = Categories(rawValue: 1 << 1) + public static let contacts = Categories(rawValue: 1 << 2) + public static let nonContacts = Categories(rawValue: 1 << 3) + } + + private enum CodingKeys: String, CodingKey { + case categories + case additionalPeers + case exclude + } + + public let categories: Categories + public let additionalPeers: Set + public let exclude: Bool + + public init(categories: Categories, additionalPeers: Set, exclude: Bool) { + self.categories = categories + self.additionalPeers = additionalPeers + self.exclude = exclude + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + self.categories = Categories(rawValue: try container.decode(Int32.self, forKey: .categories)) + self.additionalPeers = Set(try container.decode([PeerId].self, forKey: .additionalPeers)) + self.exclude = try container.decode(Bool.self, forKey: .exclude) + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + + try container.encode(self.categories.rawValue, forKey: .categories) + try container.encode(Array(self.additionalPeers).sorted(), forKey: .additionalPeers) + try container.encode(self.exclude, forKey: .exclude) + } + + public static func ==(lhs: TelegramBusinessRecipients, rhs: TelegramBusinessRecipients) -> Bool { + if lhs === rhs { + return true + } + + if lhs.categories != rhs.categories { + return false + } + if lhs.additionalPeers != rhs.additionalPeers { + return false + } + if lhs.exclude != rhs.exclude { + return false + } + + return true + } +} + +public final class TelegramBusinessGreetingMessage: Codable, Equatable { + private enum CodingKeys: String, CodingKey { + case shortcutId + case recipients + case inactivityDays + } + + public let shortcutId: Int32 + public let recipients: TelegramBusinessRecipients + public let inactivityDays: Int + + public init(shortcutId: Int32, recipients: TelegramBusinessRecipients, inactivityDays: Int) { + self.shortcutId = shortcutId + self.recipients = recipients + self.inactivityDays = inactivityDays + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + self.shortcutId = try container.decode(Int32.self, forKey: .shortcutId) + self.recipients = try container.decode(TelegramBusinessRecipients.self, forKey: .recipients) + self.inactivityDays = Int(try container.decode(Int32.self, forKey: .inactivityDays)) + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + + try container.encode(self.shortcutId, forKey: .shortcutId) + try container.encode(self.recipients, forKey: .recipients) + try container.encode(Int32(clamping: self.inactivityDays), forKey: .inactivityDays) + } + + public static func ==(lhs: TelegramBusinessGreetingMessage, rhs: TelegramBusinessGreetingMessage) -> Bool { + if lhs === rhs { + return true + } + + if lhs.shortcutId != rhs.shortcutId { + return false + } + if lhs.recipients != rhs.recipients { + return false + } + if lhs.inactivityDays != rhs.inactivityDays { + return false + } + + return true + } +} + +extension TelegramBusinessGreetingMessage { + convenience init(apiGreetingMessage: Api.BusinessGreetingMessage) { + switch apiGreetingMessage { + case let .businessGreetingMessage(shortcutId, recipients, noActivityDays): + self.init( + shortcutId: shortcutId, + recipients: TelegramBusinessRecipients(apiValue: recipients), + inactivityDays: Int(noActivityDays) + ) + } + } +} + +public final class TelegramBusinessAwayMessage: Codable, Equatable { + public enum Schedule: Codable, Equatable { + private enum CodingKeys: String, CodingKey { + case discriminator + case customBeginTimestamp + case customEndTimestamp + } + + case always + case outsideWorkingHours + case custom(beginTimestamp: Int32, endTimestamp: Int32) + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + switch try container.decode(Int32.self, forKey: .discriminator) { + case 0: + self = .always + case 1: + self = .outsideWorkingHours + case 2: + self = .custom(beginTimestamp: try container.decode(Int32.self, forKey: .customBeginTimestamp), endTimestamp: try container.decode(Int32.self, forKey: .customEndTimestamp)) + default: + self = .always + } + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + + switch self { + case .always: + try container.encode(0 as Int32, forKey: .discriminator) + case .outsideWorkingHours: + try container.encode(1 as Int32, forKey: .discriminator) + case let .custom(beginTimestamp, endTimestamp): + try container.encode(2 as Int32, forKey: .discriminator) + try container.encode(beginTimestamp, forKey: .customBeginTimestamp) + try container.encode(endTimestamp, forKey: .customEndTimestamp) + } + } + } + + private enum CodingKeys: String, CodingKey { + case shortcutId + case recipients + case schedule + case sendWhenOffline + } + + public let shortcutId: Int32 + public let recipients: TelegramBusinessRecipients + public let schedule: Schedule + public let sendWhenOffline: Bool + + public init(shortcutId: Int32, recipients: TelegramBusinessRecipients, schedule: Schedule, sendWhenOffline: Bool) { + self.shortcutId = shortcutId + self.recipients = recipients + self.schedule = schedule + self.sendWhenOffline = sendWhenOffline + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + self.shortcutId = try container.decode(Int32.self, forKey: .shortcutId) + self.recipients = try container.decode(TelegramBusinessRecipients.self, forKey: .recipients) + self.schedule = try container.decode(Schedule.self, forKey: .schedule) + self.sendWhenOffline = try container.decodeIfPresent(Bool.self, forKey: .sendWhenOffline) ?? false + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + + try container.encode(self.shortcutId, forKey: .shortcutId) + try container.encode(self.recipients, forKey: .recipients) + try container.encode(self.schedule, forKey: .schedule) + try container.encode(self.sendWhenOffline, forKey: .sendWhenOffline) + } + + public static func ==(lhs: TelegramBusinessAwayMessage, rhs: TelegramBusinessAwayMessage) -> Bool { + if lhs === rhs { + return true + } + + if lhs.shortcutId != rhs.shortcutId { + return false + } + if lhs.recipients != rhs.recipients { + return false + } + if lhs.schedule != rhs.schedule { + return false + } + if lhs.sendWhenOffline != rhs.sendWhenOffline { + return false + } + + return true + } +} + +extension TelegramBusinessAwayMessage { + convenience init(apiAwayMessage: Api.BusinessAwayMessage) { + switch apiAwayMessage { + case let .businessAwayMessage(flags, shortcutId, schedule, recipients): + let mappedSchedule: Schedule + switch schedule { + case .businessAwayMessageScheduleAlways: + mappedSchedule = .always + case .businessAwayMessageScheduleOutsideWorkHours: + mappedSchedule = .outsideWorkingHours + case let .businessAwayMessageScheduleCustom(startDate, endDate): + mappedSchedule = .custom(beginTimestamp: startDate, endTimestamp: endDate) + } + + let sendWhenOffline = (flags & (1 << 0)) != 0 + + self.init( + shortcutId: shortcutId, + recipients: TelegramBusinessRecipients(apiValue: recipients), + schedule: mappedSchedule, + sendWhenOffline: sendWhenOffline + ) + } + } +} + +extension TelegramBusinessRecipients { + convenience init(apiValue: Api.BusinessRecipients) { + switch apiValue { + case let .businessRecipients(flags, users): + var categories: Categories = [] + if (flags & (1 << 0)) != 0 { + categories.insert(.existingChats) + } + if (flags & (1 << 1)) != 0 { + categories.insert(.newChats) + } + if (flags & (1 << 2)) != 0 { + categories.insert(.contacts) + } + if (flags & (1 << 3)) != 0 { + categories.insert(.nonContacts) + } + + self.init( + categories: categories, + additionalPeers: Set((users ?? []).map(PeerId.init)), + exclude: (flags & (1 << 5)) != 0 + ) + } + } + + func apiInputValue(additionalPeers: [Peer]) -> Api.InputBusinessRecipients { + var users: [Api.InputUser]? + if !additionalPeers.isEmpty { + users = additionalPeers.compactMap(apiInputUser) + } + + var flags: Int32 = 0 + + if self.categories.contains(.existingChats) { + flags |= 1 << 0 + } + if self.categories.contains(.newChats) { + flags |= 1 << 1 + } + if self.categories.contains(.contacts) { + flags |= 1 << 2 + } + if self.categories.contains(.nonContacts) { + flags |= 1 << 3 + } + if self.exclude { + flags |= 1 << 5 + } + if users != nil { + flags |= 1 << 4 + } + + return .inputBusinessRecipients(flags: flags, users: users) + } +} + +func _internal_updateBusinessGreetingMessage(account: Account, greetingMessage: TelegramBusinessGreetingMessage?) -> Signal { + let remoteApply = account.postbox.transaction { transaction -> [Peer] in + guard let greetingMessage else { + return [] + } + return greetingMessage.recipients.additionalPeers.compactMap(transaction.getPeer) + } + |> mapToSignal { additionalPeers in + var mappedMessage: Api.InputBusinessGreetingMessage? + if let greetingMessage { + mappedMessage = .inputBusinessGreetingMessage( + shortcutId: greetingMessage.shortcutId, + recipients: greetingMessage.recipients.apiInputValue(additionalPeers: additionalPeers), + noActivityDays: Int32(clamping: greetingMessage.inactivityDays) + ) + } + + var flags: Int32 = 0 + if mappedMessage != nil { + flags |= 1 << 0 + } + + return account.network.request(Api.functions.account.updateBusinessGreetingMessage(flags: flags, message: mappedMessage)) + |> `catch` { _ -> Signal in + return .single(.boolFalse) + } + |> mapToSignal { _ -> Signal in + return .complete() + } + } + + return account.postbox.transaction { transaction in + transaction.updatePeerCachedData(peerIds: Set([account.peerId]), update: { _, current in + var current = (current as? CachedUserData) ?? CachedUserData() + current = current.withUpdatedGreetingMessage(greetingMessage) + return current + }) + } + |> ignoreValues + |> then(remoteApply) +} + +func _internal_updateBusinessAwayMessage(account: Account, awayMessage: TelegramBusinessAwayMessage?) -> Signal { + let remoteApply = account.postbox.transaction { transaction -> [Peer] in + guard let awayMessage else { + return [] + } + return awayMessage.recipients.additionalPeers.compactMap(transaction.getPeer) + } + |> mapToSignal { additionalPeers in + var mappedMessage: Api.InputBusinessAwayMessage? + if let awayMessage { + let mappedSchedule: Api.BusinessAwayMessageSchedule + switch awayMessage.schedule { + case .always: + mappedSchedule = .businessAwayMessageScheduleAlways + case .outsideWorkingHours: + mappedSchedule = .businessAwayMessageScheduleOutsideWorkHours + case let .custom(beginTimestamp, endTimestamp): + mappedSchedule = .businessAwayMessageScheduleCustom(startDate: beginTimestamp, endDate: endTimestamp) + } + + var flags: Int32 = 0 + if awayMessage.sendWhenOffline { + flags |= 1 << 0 + } + + mappedMessage = .inputBusinessAwayMessage( + flags: flags, + shortcutId: awayMessage.shortcutId, + schedule: mappedSchedule, + recipients: awayMessage.recipients.apiInputValue(additionalPeers: additionalPeers) + ) + } + + var flags: Int32 = 0 + if mappedMessage != nil { + flags |= 1 << 0 + } + + return account.network.request(Api.functions.account.updateBusinessAwayMessage(flags: flags, message: mappedMessage)) + |> `catch` { _ -> Signal in + return .single(.boolFalse) + } + |> mapToSignal { _ -> Signal in + return .complete() + } + } + + return account.postbox.transaction { transaction in + transaction.updatePeerCachedData(peerIds: Set([account.peerId]), update: { _, current in + var current = (current as? CachedUserData) ?? CachedUserData() + current = current.withUpdatedAwayMessage(awayMessage) + return current + }) + } + |> ignoreValues + |> then(remoteApply) +} + +public final class TelegramAccountConnectedBot: Codable, Equatable { + public let id: PeerId + public let recipients: TelegramBusinessRecipients + public let canReply: Bool + + public init(id: PeerId, recipients: TelegramBusinessRecipients, canReply: Bool) { + self.id = id + self.recipients = recipients + self.canReply = canReply + } + + public static func ==(lhs: TelegramAccountConnectedBot, rhs: TelegramAccountConnectedBot) -> Bool { + if lhs === rhs { + return true + } + if lhs.id != rhs.id { + return false + } + if lhs.recipients != rhs.recipients { + return false + } + if lhs.canReply != rhs.canReply { + return false + } + return true + } +} + +public func _internal_setAccountConnectedBot(account: Account, bot: TelegramAccountConnectedBot?) -> Signal { + let remoteApply = account.postbox.transaction { transaction -> (Peer?, [Peer]) in + guard let bot else { + return (nil, []) + } + return (transaction.getPeer(bot.id), bot.recipients.additionalPeers.compactMap(transaction.getPeer)) + } + |> mapToSignal { botUser, additionalPeers in + var flags: Int32 = 0 + var mappedBot: Api.InputUser = .inputUserEmpty + var mappedRecipients: Api.InputBusinessRecipients = .inputBusinessRecipients(flags: 0, users: nil) + + if let bot, let inputBotUser = botUser.flatMap(apiInputUser) { + mappedBot = inputBotUser + if bot.canReply { + flags |= 1 << 0 + } + mappedRecipients = bot.recipients.apiInputValue(additionalPeers: additionalPeers) + } + + return account.network.request(Api.functions.account.updateConnectedBot(flags: flags, bot: mappedBot, recipients: mappedRecipients)) + |> map(Optional.init) + |> `catch` { _ -> Signal in + return .single(nil) + } + |> mapToSignal { result -> Signal in + if let result { + account.stateManager.addUpdates(result) + } + return .complete() + } + } + + return account.postbox.transaction { transaction in + transaction.updatePeerCachedData(peerIds: Set([account.peerId]), update: { _, current in + var current = (current as? CachedUserData) ?? CachedUserData() + current = current.withUpdatedConnectedBot(bot) + return current + }) + } + |> ignoreValues + |> then(remoteApply) +} diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/ReplyThreadHistory.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/ReplyThreadHistory.swift index 0695e05c0c6..4c57ab1f131 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/ReplyThreadHistory.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/ReplyThreadHistory.swift @@ -843,7 +843,7 @@ func _internal_fetchChannelReplyThreadMessage(account: Account, messageId: Messa count: 40, clipHoles: true, anchor: inputAnchor, - namespaces: .not(Namespaces.Message.allScheduled) + namespaces: .not(Namespaces.Message.allNonRegular) ) if !testView.isLoading || transaction.getMessageHistoryThreadInfo(peerId: threadMessageId.peerId, threadId: Int64(threadMessageId.id)) != nil { let initialAnchor: ChatReplyThreadMessage.Anchor diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/SearchMessages.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/SearchMessages.swift index 409791f28da..d64c77acc51 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/SearchMessages.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/SearchMessages.swift @@ -584,12 +584,18 @@ func _internal_downloadMessage(accountPeerId: PeerId, postbox: Postbox, network: } func fetchRemoteMessage(accountPeerId: PeerId, postbox: Postbox, source: FetchMessageHistoryHoleSource, message: MessageReference) -> Signal { - guard case let .message(peer, _, id, _, _, _) = message.content else { + guard case let .message(peer, _, id, _, _, _, threadId) = message.content else { return .single(nil) } let signal: Signal if id.namespace == Namespaces.Message.ScheduledCloud { signal = source.request(Api.functions.messages.getScheduledMessages(peer: peer.inputPeer, id: [id.id])) + } else if id.namespace == Namespaces.Message.QuickReplyCloud { + if let threadId { + signal = source.request(Api.functions.messages.getQuickReplyMessages(flags: 1 << 0, shortcutId: Int32(clamping: threadId), id: [id.id], hash: 0)) + } else { + signal = .never() + } } else if id.peerId.namespace == Namespaces.Peer.CloudChannel { if let channel = peer.inputChannel { signal = source.request(Api.functions.channels.getMessages(channel: channel, id: [Api.InputMessage.inputMessageID(id: id.id)])) diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/SparseMessageList.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/SparseMessageList.swift index 5f39622ea4e..f6f258c1546 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/SparseMessageList.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/SparseMessageList.swift @@ -192,7 +192,7 @@ public final class SparseMessageList { let location: ChatLocationInput = .peer(peerId: self.peerId, threadId: self.threadId) - self.topItemsDisposable.set((self.account.postbox.aroundMessageHistoryViewForLocation(location, anchor: .upperBound, ignoreMessagesInTimestampRange: nil, count: count, fixedCombinedReadStates: nil, topTaggedMessageIdNamespaces: Set(), tag: .tag(self.messageTag), appendMessagesFromTheSameGroup: false, namespaces: .not(Set(Namespaces.Message.allScheduled)), orderStatistics: []) + self.topItemsDisposable.set((self.account.postbox.aroundMessageHistoryViewForLocation(location, anchor: .upperBound, ignoreMessagesInTimestampRange: nil, count: count, fixedCombinedReadStates: nil, topTaggedMessageIdNamespaces: Set(), tag: .tag(self.messageTag), appendMessagesFromTheSameGroup: false, namespaces: .not(Namespaces.Message.allNonRegular), orderStatistics: []) |> deliverOn(self.queue)).start(next: { [weak self] view, updateType, _ in guard let strongSelf = self else { return diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift index e0334d215c3..8decbe638dc 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift @@ -1205,7 +1205,7 @@ public extension TelegramEngine { } var selectedMedia: EngineMedia - if let alternativeMedia = itemAndPeer.item.alternativeMedia.flatMap(EngineMedia.init), !preferHighQuality { + if let alternativeMedia = itemAndPeer.item.alternativeMedia.flatMap(EngineMedia.init), (!preferHighQuality && !itemAndPeer.item.isMy) { selectedMedia = alternativeMedia } else { selectedMedia = EngineMedia(media) diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/TimeZones.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/TimeZones.swift new file mode 100644 index 00000000000..5f231500db6 --- /dev/null +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/TimeZones.swift @@ -0,0 +1,106 @@ +import Foundation +import Postbox +import SwiftSignalKit +import TelegramApi +import MtProtoKit + +public final class TimeZoneList: Codable, Equatable { + public final class Item: Codable, Equatable { + public let id: String + public let title: String + public let utcOffset: Int32 + + public init(id: String, title: String, utcOffset: Int32) { + self.id = id + self.title = title + self.utcOffset = utcOffset + } + + public static func ==(lhs: Item, rhs: Item) -> Bool { + if lhs === rhs { + return true + } + if lhs.id != rhs.id { + return false + } + if lhs.title != rhs.title { + return false + } + if lhs.utcOffset != rhs.utcOffset { + return false + } + return true + } + } + + public let items: [Item] + public let hashValue: Int32 + + public init(items: [Item], hashValue: Int32) { + self.items = items + self.hashValue = hashValue + } + + public static func ==(lhs: TimeZoneList, rhs: TimeZoneList) -> Bool { + if lhs === rhs { + return true + } + if lhs.items != rhs.items { + return false + } + if lhs.hashValue != rhs.hashValue { + return false + } + return true + } +} + +func _internal_cachedTimeZoneList(account: Account) -> Signal { + let viewKey: PostboxViewKey = .preferences(keys: Set([PreferencesKeys.timezoneList()])) + return account.postbox.combinedView(keys: [viewKey]) + |> map { views -> TimeZoneList? in + guard let view = views.views[viewKey] as? PreferencesView else { + return nil + } + guard let value = view.values[PreferencesKeys.timezoneList()]?.get(TimeZoneList.self) else { + return nil + } + return value + } +} + +func _internal_keepCachedTimeZoneListUpdated(account: Account) -> Signal { + let updateSignal = _internal_cachedTimeZoneList(account: account) + |> take(1) + |> mapToSignal { list -> Signal in + return account.network.request(Api.functions.help.getTimezonesList(hash: list?.hashValue ?? 0)) + |> map(Optional.init) + |> `catch` { _ -> Signal in + return .single(nil) + } + |> mapToSignal { result -> Signal in + guard let result else { + return .complete() + } + + return account.postbox.transaction { transaction in + switch result { + case let .timezonesList(timezones, hash): + var items: [TimeZoneList.Item] = [] + for item in timezones { + switch item { + case let .timezone(id, name, utcOffset): + items.append(TimeZoneList.Item(id: id, title: name, utcOffset: utcOffset)) + } + } + transaction.setPreferencesEntry(key: PreferencesKeys.timezoneList(), value: PreferencesEntry(TimeZoneList(items: items, hashValue: hash))) + case .timezonesListNotModified: + break + } + } + |> ignoreValues + } + } + + return updateSignal +} diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/ChatListFiltering.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/ChatListFiltering.swift index cec7854829c..af88b9dffa1 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/ChatListFiltering.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/ChatListFiltering.swift @@ -306,7 +306,7 @@ extension ChatListFilter { switch apiFilter { case .dialogFilterDefault: self = .allChats - case let .dialogFilter(flags, id, title, emoticon, pinnedPeers, includePeers, excludePeers): + case let .dialogFilter(flags, id, title, emoticon, color, pinnedPeers, includePeers, excludePeers): self = .filter( id: id, title: title, @@ -353,10 +353,10 @@ extension ChatListFilter { return nil } }, - color: nil + color: color.flatMap(PeerNameColor.init(rawValue:)) ) ) - case let .dialogFilterChatlist(flags, id, title, emoticon, pinnedPeers, includePeers): + case let .dialogFilterChatlist(flags, id, title, emoticon, color, pinnedPeers, includePeers): self = .filter( id: id, title: title, @@ -392,7 +392,7 @@ extension ChatListFilter { } }), excludePeers: [], - color: nil + color: color.flatMap(PeerNameColor.init(rawValue:)) ) ) } @@ -408,7 +408,10 @@ extension ChatListFilter { if emoticon != nil { flags |= 1 << 25 } - return .dialogFilterChatlist(flags: flags, id: id, title: title, emoticon: emoticon, pinnedPeers: data.includePeers.pinnedPeers.compactMap { peerId -> Api.InputPeer? in + if data.color != nil { + flags |= 1 << 27 + } + return .dialogFilterChatlist(flags: flags, id: id, title: title, emoticon: emoticon, color: data.color?.rawValue, pinnedPeers: data.includePeers.pinnedPeers.compactMap { peerId -> Api.InputPeer? in return transaction.getPeer(peerId).flatMap(apiInputPeer) }, includePeers: data.includePeers.peers.compactMap { peerId -> Api.InputPeer? in if data.includePeers.pinnedPeers.contains(peerId) { @@ -431,7 +434,10 @@ extension ChatListFilter { if emoticon != nil { flags |= 1 << 25 } - return .dialogFilter(flags: flags, id: id, title: title, emoticon: emoticon, pinnedPeers: data.includePeers.pinnedPeers.compactMap { peerId -> Api.InputPeer? in + if data.color != nil { + flags |= 1 << 27 + } + return .dialogFilter(flags: flags, id: id, title: title, emoticon: emoticon, color: data.color?.rawValue, pinnedPeers: data.includePeers.pinnedPeers.compactMap { peerId -> Api.InputPeer? in return transaction.getPeer(peerId).flatMap(apiInputPeer) }, includePeers: data.includePeers.peers.compactMap { peerId -> Api.InputPeer? in if data.includePeers.pinnedPeers.contains(peerId) { @@ -488,111 +494,116 @@ private enum RequestChatListFiltersError { case generic } -private func requestChatListFilters(accountPeerId: PeerId, postbox: Postbox, network: Network) -> Signal<[ChatListFilter], RequestChatListFiltersError> { +private func requestChatListFilters(accountPeerId: PeerId, postbox: Postbox, network: Network) -> Signal<([ChatListFilter], Bool), RequestChatListFiltersError> { return network.request(Api.functions.messages.getDialogFilters()) |> mapError { _ -> RequestChatListFiltersError in return .generic } - |> mapToSignal { result -> Signal<[ChatListFilter], RequestChatListFiltersError> in - return postbox.transaction { transaction -> ([ChatListFilter], [Api.InputPeer], [Api.InputPeer]) in - var filters: [ChatListFilter] = [] - var missingPeers: [Api.InputPeer] = [] - var missingChats: [Api.InputPeer] = [] - var missingPeerIds = Set() - var missingChatIds = Set() - for apiFilter in result { - let filter = ChatListFilter(apiFilter: apiFilter) - filters.append(filter) - switch apiFilter { - case .dialogFilterDefault: - break - case let .dialogFilter(_, _, _, _, pinnedPeers, includePeers, excludePeers): - for peer in pinnedPeers + includePeers + excludePeers { - var peerId: PeerId? - switch peer { - case let .inputPeerUser(userId, _): - peerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(userId)) - case let .inputPeerChat(chatId): - peerId = PeerId(namespace: Namespaces.Peer.CloudGroup, id: PeerId.Id._internalFromInt64Value(chatId)) - case let .inputPeerChannel(channelId, _): - peerId = PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value(channelId)) - default: - break - } - if let peerId = peerId { - if transaction.getPeer(peerId) == nil && !missingPeerIds.contains(peerId) { - missingPeerIds.insert(peerId) - missingPeers.append(peer) + |> mapToSignal { result -> Signal<([ChatListFilter], Bool), RequestChatListFiltersError> in + return postbox.transaction { transaction -> ([ChatListFilter], [Api.InputPeer], [Api.InputPeer], Bool) in + switch result { + case let .dialogFilters(flags, apiFilters): + let tagsEnabled = (flags & (1 << 0)) != 0 + + var filters: [ChatListFilter] = [] + var missingPeers: [Api.InputPeer] = [] + var missingChats: [Api.InputPeer] = [] + var missingPeerIds = Set() + var missingChatIds = Set() + for apiFilter in apiFilters { + let filter = ChatListFilter(apiFilter: apiFilter) + filters.append(filter) + switch apiFilter { + case .dialogFilterDefault: + break + case let .dialogFilter(_, _, _, _, _, pinnedPeers, includePeers, excludePeers): + for peer in pinnedPeers + includePeers + excludePeers { + var peerId: PeerId? + switch peer { + case let .inputPeerUser(userId, _): + peerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(userId)) + case let .inputPeerChat(chatId): + peerId = PeerId(namespace: Namespaces.Peer.CloudGroup, id: PeerId.Id._internalFromInt64Value(chatId)) + case let .inputPeerChannel(channelId, _): + peerId = PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value(channelId)) + default: + break } - } - } - - for peer in pinnedPeers { - var peerId: PeerId? - switch peer { - case let .inputPeerUser(userId, _): - peerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(userId)) - case let .inputPeerChat(chatId): - peerId = PeerId(namespace: Namespaces.Peer.CloudGroup, id: PeerId.Id._internalFromInt64Value(chatId)) - case let .inputPeerChannel(channelId, _): - peerId = PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value(channelId)) - default: - break - } - if let peerId = peerId, !missingChatIds.contains(peerId) { - if transaction.getPeerChatListIndex(peerId) == nil { - missingChatIds.insert(peerId) - missingChats.append(peer) + if let peerId = peerId { + if transaction.getPeer(peerId) == nil && !missingPeerIds.contains(peerId) { + missingPeerIds.insert(peerId) + missingPeers.append(peer) + } } } - } - case let .dialogFilterChatlist(_, _, _, _, pinnedPeers, includePeers): - for peer in pinnedPeers + includePeers { - var peerId: PeerId? - switch peer { - case let .inputPeerUser(userId, _): - peerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(userId)) - case let .inputPeerChat(chatId): - peerId = PeerId(namespace: Namespaces.Peer.CloudGroup, id: PeerId.Id._internalFromInt64Value(chatId)) - case let .inputPeerChannel(channelId, _): - peerId = PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value(channelId)) - default: - break - } - if let peerId = peerId { - if transaction.getPeer(peerId) == nil && !missingPeerIds.contains(peerId) { - missingPeerIds.insert(peerId) - missingPeers.append(peer) + + for peer in pinnedPeers { + var peerId: PeerId? + switch peer { + case let .inputPeerUser(userId, _): + peerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(userId)) + case let .inputPeerChat(chatId): + peerId = PeerId(namespace: Namespaces.Peer.CloudGroup, id: PeerId.Id._internalFromInt64Value(chatId)) + case let .inputPeerChannel(channelId, _): + peerId = PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value(channelId)) + default: + break + } + if let peerId = peerId, !missingChatIds.contains(peerId) { + if transaction.getPeerChatListIndex(peerId) == nil { + missingChatIds.insert(peerId) + missingChats.append(peer) + } } } - } - - for peer in pinnedPeers { - var peerId: PeerId? - switch peer { - case let .inputPeerUser(userId, _): - peerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(userId)) - case let .inputPeerChat(chatId): - peerId = PeerId(namespace: Namespaces.Peer.CloudGroup, id: PeerId.Id._internalFromInt64Value(chatId)) - case let .inputPeerChannel(channelId, _): - peerId = PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value(channelId)) - default: - break + case let .dialogFilterChatlist(_, _, _, _, _, pinnedPeers, includePeers): + for peer in pinnedPeers + includePeers { + var peerId: PeerId? + switch peer { + case let .inputPeerUser(userId, _): + peerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(userId)) + case let .inputPeerChat(chatId): + peerId = PeerId(namespace: Namespaces.Peer.CloudGroup, id: PeerId.Id._internalFromInt64Value(chatId)) + case let .inputPeerChannel(channelId, _): + peerId = PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value(channelId)) + default: + break + } + if let peerId = peerId { + if transaction.getPeer(peerId) == nil && !missingPeerIds.contains(peerId) { + missingPeerIds.insert(peerId) + missingPeers.append(peer) + } + } } - if let peerId = peerId, !missingChatIds.contains(peerId) { - if transaction.getPeerChatListIndex(peerId) == nil { - missingChatIds.insert(peerId) - missingChats.append(peer) + + for peer in pinnedPeers { + var peerId: PeerId? + switch peer { + case let .inputPeerUser(userId, _): + peerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(userId)) + case let .inputPeerChat(chatId): + peerId = PeerId(namespace: Namespaces.Peer.CloudGroup, id: PeerId.Id._internalFromInt64Value(chatId)) + case let .inputPeerChannel(channelId, _): + peerId = PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value(channelId)) + default: + break + } + if let peerId = peerId, !missingChatIds.contains(peerId) { + if transaction.getPeerChatListIndex(peerId) == nil { + missingChatIds.insert(peerId) + missingChats.append(peer) + } } } } } + return (filters, missingPeers, missingChats, tagsEnabled) } - return (filters, missingPeers, missingChats) } |> castError(RequestChatListFiltersError.self) - |> mapToSignal { filtersAndMissingPeers -> Signal<[ChatListFilter], RequestChatListFiltersError> in - let (filters, missingPeers, missingChats) = filtersAndMissingPeers + |> mapToSignal { filtersAndMissingPeers -> Signal<([ChatListFilter], Bool), RequestChatListFiltersError> in + let (filters, missingPeers, missingChats, tagsEnabled) = filtersAndMissingPeers var missingUsers: [Api.InputUser] = [] var missingChannels: [Api.InputChannel] = [] @@ -700,13 +711,10 @@ private func requestChatListFilters(accountPeerId: PeerId, postbox: Postbox, net loadMissingChats ) |> castError(RequestChatListFiltersError.self) - |> mapToSignal { _ -> Signal<[ChatListFilter], RequestChatListFiltersError> in - #if swift(<5.1) - return .complete() - #endif + |> mapToSignal { _ -> Signal<([ChatListFilter], Bool), RequestChatListFiltersError> in } |> then( - .single(filters) + Signal<([ChatListFilter], Bool), RequestChatListFiltersError>.single((filters, tagsEnabled)) ) } } @@ -889,14 +897,16 @@ struct ChatListFiltersState: Codable, Equatable { var updates: [ChatListFilterUpdates] + var remoteDisplayTags: Bool? var displayTags: Bool - static var `default` = ChatListFiltersState(filters: [], remoteFilters: nil, updates: [], displayTags: false) + static var `default` = ChatListFiltersState(filters: [], remoteFilters: nil, updates: [], remoteDisplayTags: nil, displayTags: false) - fileprivate init(filters: [ChatListFilter], remoteFilters: [ChatListFilter]?, updates: [ChatListFilterUpdates], displayTags: Bool) { + fileprivate init(filters: [ChatListFilter], remoteFilters: [ChatListFilter]?, updates: [ChatListFilterUpdates], remoteDisplayTags: Bool?, displayTags: Bool) { self.filters = filters self.remoteFilters = remoteFilters self.updates = updates + self.remoteDisplayTags = remoteDisplayTags self.displayTags = displayTags } @@ -906,6 +916,7 @@ struct ChatListFiltersState: Codable, Equatable { self.filters = try container.decode([ChatListFilter].self, forKey: "filters") self.remoteFilters = try container.decodeIfPresent([ChatListFilter].self, forKey: "remoteFilters") self.updates = try container.decodeIfPresent([ChatListFilterUpdates].self, forKey: "updates") ?? [] + self.remoteDisplayTags = try container.decodeIfPresent(Bool.self, forKey: "remoteDisplayTags") self.displayTags = try container.decodeIfPresent(Bool.self, forKey: "displayTags") ?? false } @@ -915,6 +926,7 @@ struct ChatListFiltersState: Codable, Equatable { try container.encode(self.filters, forKey: "filters") try container.encodeIfPresent(self.remoteFilters, forKey: "remoteFilters") try container.encode(self.updates, forKey: "updates") + try container.encodeIfPresent(self.remoteDisplayTags, forKey: "remoteDisplayTags") try container.encode(self.displayTags, forKey: "displayTags") } @@ -959,6 +971,43 @@ func _internal_updateChatListFiltersInteractively(postbox: Postbox, _ f: @escapi } } +func _internal_updateChatListFiltersDisplayTagsInteractively(postbox: Postbox, displayTags: Bool) -> Signal { + return postbox.transaction { transaction -> Void in + var hasUpdates = false + transaction.updatePreferencesEntry(key: PreferencesKeys.chatListFilters, { entry in + var state = entry?.get(ChatListFiltersState.self) ?? ChatListFiltersState.default + if displayTags != state.displayTags { + state.displayTags = displayTags + + if state.displayTags { + for i in 0 ..< state.filters.count { + switch state.filters[i] { + case .allChats: + break + case let .filter(id, title, emoticon, data): + if data.color == nil { + var data = data + data.color = PeerNameColor(rawValue: Int32.random(in: 0 ... 7)) + state.filters[i] = .filter(id: id, title: title, emoticon: emoticon, data: data) + } + } + } + } + + hasUpdates = true + } + + state.normalize() + + return PreferencesEntry(state) + }) + if hasUpdates { + requestChatListFiltersSync(transaction: transaction) + } + } + |> ignoreValues +} + func _internal_updateChatListFiltersInteractively(transaction: Transaction, _ f: ([ChatListFilter]) -> [ChatListFilter]) { var hasUpdates = false transaction.updatePreferencesEntry(key: PreferencesKeys.chatListFilters, { entry in @@ -1366,18 +1415,22 @@ private func synchronizeChatListFilters(transaction: Transaction, accountPeerId: let settings = transaction.getPreferencesEntry(key: PreferencesKeys.chatListFilters)?.get(ChatListFiltersState.self) ?? ChatListFiltersState.default let localFilters = settings.filters let locallyKnownRemoteFilters = settings.remoteFilters ?? [] + let localDisplayTags = settings.displayTags + let locallyKnownRemoteDisplayTags = settings.remoteDisplayTags ?? false return requestChatListFilters(accountPeerId: accountPeerId, postbox: postbox, network: network) - |> `catch` { _ -> Signal<[ChatListFilter], NoError> in + |> `catch` { _ -> Signal<([ChatListFilter], Bool), NoError> in return .complete() } - |> mapToSignal { remoteFilters -> Signal in - if localFilters == locallyKnownRemoteFilters { + |> mapToSignal { remoteFilters, remoteTagsEnabled -> Signal in + if localFilters == locallyKnownRemoteFilters && localDisplayTags == locallyKnownRemoteDisplayTags { return postbox.transaction { transaction -> Void in let _ = updateChatListFiltersState(transaction: transaction, { state in var state = state state.filters = remoteFilters state.remoteFilters = state.filters + state.displayTags = remoteTagsEnabled + state.remoteDisplayTags = state.displayTags return state }) } @@ -1453,6 +1506,17 @@ private func synchronizeChatListFilters(transaction: Transaction, accountPeerId: reorderFilters = .complete() } + let updateTagsEnabled: Signal + if localDisplayTags != remoteTagsEnabled { + updateTagsEnabled = network.request(Api.functions.messages.toggleDialogFilterTags(enabled: localDisplayTags ? .boolTrue : .boolFalse)) + |> ignoreValues + |> `catch` { _ -> Signal in + return .complete() + } + } else { + updateTagsEnabled = .complete() + } + return deleteSignals |> then( addSignals @@ -1460,12 +1524,16 @@ private func synchronizeChatListFilters(transaction: Transaction, accountPeerId: |> then( reorderFilters ) + |> then( + updateTagsEnabled + ) |> then( postbox.transaction { transaction -> Void in let _ = updateChatListFiltersState(transaction: transaction, { state in var state = state state.filters = mergedFilters state.remoteFilters = state.filters + state.remoteDisplayTags = state.displayTags return state }) } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift index 0d1e3ea5d74..d0bc67b7258 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift @@ -589,13 +589,7 @@ public extension TelegramEngine { } public func updateChatListFiltersDisplayTags(isEnabled: Bool) { - let _ = self.account.postbox.transaction({ transaction in - updateChatListFiltersState(transaction: transaction, { state in - var state = state - state.displayTags = isEnabled - return state - }) - }).start() + let _ = _internal_updateChatListFiltersDisplayTagsInteractively(postbox: self.account.postbox, displayTags: isEnabled).startStandalone() } public func updatedChatListFilters() -> Signal<[ChatListFilter], NoError> { @@ -1082,7 +1076,7 @@ public extension TelegramEngine { transaction.setMessageHistoryThreadInfo(peerId: id, threadId: threadId, info: nil) - _internal_clearHistory(transaction: transaction, mediaBox: self.account.postbox.mediaBox, peerId: id, threadId: threadId, namespaces: .not(Namespaces.Message.allScheduled)) + _internal_clearHistory(transaction: transaction, mediaBox: self.account.postbox.mediaBox, peerId: id, threadId: threadId, namespaces: .not(Namespaces.Message.allNonRegular)) } |> ignoreValues } @@ -1094,7 +1088,7 @@ public extension TelegramEngine { transaction.setMessageHistoryThreadInfo(peerId: id, threadId: threadId, info: nil) - _internal_clearHistory(transaction: transaction, mediaBox: self.account.postbox.mediaBox, peerId: id, threadId: threadId, namespaces: .not(Namespaces.Message.allScheduled)) + _internal_clearHistory(transaction: transaction, mediaBox: self.account.postbox.mediaBox, peerId: id, threadId: threadId, namespaces: .not(Namespaces.Message.allNonRegular)) } } |> ignoreValues @@ -1373,7 +1367,7 @@ public extension TelegramEngine { return .single(false) } - return self.account.postbox.aroundMessageHistoryViewForLocation(.peer(peerId: id, threadId: nil), anchor: .upperBound, ignoreMessagesInTimestampRange: nil, count: 44, fixedCombinedReadStates: nil, topTaggedMessageIdNamespaces: Set(), tag: nil, appendMessagesFromTheSameGroup: false, namespaces: .not(Namespaces.Message.allScheduled), orderStatistics: []) + return self.account.postbox.aroundMessageHistoryViewForLocation(.peer(peerId: id, threadId: nil), anchor: .upperBound, ignoreMessagesInTimestampRange: nil, count: 44, fixedCombinedReadStates: nil, topTaggedMessageIdNamespaces: Set(), tag: nil, appendMessagesFromTheSameGroup: false, namespaces: .not(Namespaces.Message.allNonRegular), orderStatistics: []) |> map { view -> Bool in for entry in view.0.entries { if entry.message.flags.contains(.Incoming) { diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/UpdateCachedPeerData.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/UpdateCachedPeerData.swift index 81af3adfb1f..775b5d7c2e5 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/UpdateCachedPeerData.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/UpdateCachedPeerData.swift @@ -196,18 +196,28 @@ func _internal_fetchAndUpdateCachedPeerData(accountPeerId: PeerId, peerId rawPee } else { editableBotInfo = .single(nil) } + + var additionalConnectedBots: Signal = .single(nil) + if rawPeerId == accountPeerId { + additionalConnectedBots = network.request(Api.functions.account.getConnectedBots()) + |> map(Optional.init) + |> `catch` { _ -> Signal in + return .single(nil) + } + } return combineLatest( network.request(Api.functions.users.getFullUser(id: inputUser)) |> retryRequest, - editableBotInfo + editableBotInfo, + additionalConnectedBots ) - |> mapToSignal { result, editableBotInfo -> Signal in + |> mapToSignal { result, editableBotInfo, additionalConnectedBots -> Signal in return postbox.transaction { transaction -> Bool in switch result { case let .userFull(fullUser, chats, users): var accountUser: Api.User? - let parsedPeers = AccumulatedPeers(transaction: transaction, chats: chats, users: users) + var parsedPeers = AccumulatedPeers(transaction: transaction, chats: chats, users: users) for user in users { if user.peerId == accountPeerId { accountUser = user @@ -215,8 +225,28 @@ func _internal_fetchAndUpdateCachedPeerData(accountPeerId: PeerId, peerId rawPee } let _ = accountUser + var mappedConnectedBot: TelegramAccountConnectedBot? + + if let additionalConnectedBots { + switch additionalConnectedBots { + case let .connectedBots(connectedBots, users): + parsedPeers = parsedPeers.union(with: AccumulatedPeers(transaction: transaction, chats: [], users: users)) + + if let apiBot = connectedBots.first { + switch apiBot { + case let .connectedBot(flags, botId, recipients): + mappedConnectedBot = TelegramAccountConnectedBot( + id: PeerId(botId), + recipients: TelegramBusinessRecipients(apiValue: recipients), + canReply: (flags & (1 << 0)) != 0 + ) + } + } + } + } + switch fullUser { - case let .userFull(_, _, _, _, _, _, _, userFullNotifySettings, _, _, _, _, _, _, _, _, _, _, _, _): + case let .userFull(_, _, _, _, _, _, _, _, userFullNotifySettings, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _): updatePeers(transaction: transaction, accountPeerId: accountPeerId, peers: parsedPeers) transaction.updateCurrentPeerNotificationSettings([peerId: TelegramPeerNotificationSettings(apiSettings: userFullNotifySettings)]) } @@ -228,7 +258,7 @@ func _internal_fetchAndUpdateCachedPeerData(accountPeerId: PeerId, peerId rawPee previous = CachedUserData() } switch fullUser { - case let .userFull(userFullFlags, _, userFullAbout, userFullSettings, personalPhoto, profilePhoto, fallbackPhoto, _, userFullBotInfo, userFullPinnedMsgId, userFullCommonChatsCount, _, userFullTtlPeriod, userFullThemeEmoticon, _, _, _, userPremiumGiftOptions, userWallpaper, stories): + case let .userFull(userFullFlags, _, _, userFullAbout, userFullSettings, personalPhoto, profilePhoto, fallbackPhoto, _, userFullBotInfo, userFullPinnedMsgId, userFullCommonChatsCount, _, userFullTtlPeriod, userFullThemeEmoticon, _, _, _, userPremiumGiftOptions, userWallpaper, stories, businessWorkHours, businessLocation, greetingMessage, awayMessage): let _ = stories let botInfo = userFullBotInfo.flatMap(BotInfo.init(apiBotInfo:)) let isBlocked = (userFullFlags & (1 << 0)) != 0 @@ -286,6 +316,26 @@ func _internal_fetchAndUpdateCachedPeerData(accountPeerId: PeerId, peerId rawPee let wallpaper = userWallpaper.flatMap { TelegramWallpaper(apiWallpaper: $0) } + var mappedBusinessHours: TelegramBusinessHours? + if let businessWorkHours { + mappedBusinessHours = TelegramBusinessHours(apiWorkingHours: businessWorkHours) + } + + var mappedBusinessLocation: TelegramBusinessLocation? + if let businessLocation { + mappedBusinessLocation = TelegramBusinessLocation(apiLocation: businessLocation) + } + + var mappedGreetingMessage: TelegramBusinessGreetingMessage? + if let greetingMessage { + mappedGreetingMessage = TelegramBusinessGreetingMessage(apiGreetingMessage: greetingMessage) + } + + var mappedAwayMessage: TelegramBusinessAwayMessage? + if let awayMessage { + mappedAwayMessage = TelegramBusinessAwayMessage(apiAwayMessage: awayMessage) + } + return previous.withUpdatedAbout(userFullAbout) .withUpdatedBotInfo(botInfo) .withUpdatedEditableBotInfo(editableBotInfo) @@ -307,6 +357,11 @@ func _internal_fetchAndUpdateCachedPeerData(accountPeerId: PeerId, peerId rawPee .withUpdatedVoiceMessagesAvailable(voiceMessagesAvailable) .withUpdatedWallpaper(wallpaper) .withUpdatedFlags(flags) + .withUpdatedBusinessHours(mappedBusinessHours) + .withUpdatedBusinessLocation(mappedBusinessLocation) + .withUpdatedGreetingMessage(mappedGreetingMessage) + .withUpdatedAwayMessage(mappedAwayMessage) + .withUpdatedConnectedBot(mappedConnectedBot) } }) } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Stickers/TelegramEngineStickers.swift b/submodules/TelegramCore/Sources/TelegramEngine/Stickers/TelegramEngineStickers.swift index 8e607e3dcea..acc87028079 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Stickers/TelegramEngineStickers.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Stickers/TelegramEngineStickers.swift @@ -257,6 +257,12 @@ public extension TelegramEngine { return (items.map(\.file), isFinalResult) } } + + public func addRecentlyUsedSticker(file: TelegramMediaFile) { + let _ = self.account.postbox.transaction({ transaction -> Void in + TelegramCore.addRecentlyUsedSticker(transaction: transaction, fileReference: .standalone(media: file)) + }).start() + } } } diff --git a/submodules/TelegramCore/Sources/Utils/MessageUtils.swift b/submodules/TelegramCore/Sources/Utils/MessageUtils.swift index 22173d47cb6..53f630c40d4 100644 --- a/submodules/TelegramCore/Sources/Utils/MessageUtils.swift +++ b/submodules/TelegramCore/Sources/Utils/MessageUtils.swift @@ -181,6 +181,36 @@ func messagesIdsGroupedByPeerId(_ ids: ReferencedReplyMessageIds) -> [PeerId: Re return dict } +func messagesIdsGroupedByPeerId(_ ids: Set) -> [PeerAndThreadId: [MessageId]] { + var dict: [PeerAndThreadId: [MessageId]] = [:] + + for id in ids { + let peerAndThreadId = PeerAndThreadId(peerId: id.messageId.peerId, threadId: id.threadId) + if dict[peerAndThreadId] == nil { + dict[peerAndThreadId] = [id.messageId] + } else { + dict[peerAndThreadId]!.append(id.messageId) + } + } + + return dict +} + +func messagesIdsGroupedByPeerId(_ ids: [MessageAndThreadId]) -> [PeerAndThreadId: [MessageId]] { + var dict: [PeerAndThreadId: [MessageId]] = [:] + + for id in ids { + let peerAndThreadId = PeerAndThreadId(peerId: id.messageId.peerId, threadId: id.threadId) + if dict[peerAndThreadId] == nil { + dict[peerAndThreadId] = [id.messageId] + } else { + dict[peerAndThreadId]!.append(id.messageId) + } + } + + return dict +} + func locallyRenderedMessage(message: StoreMessage, peers: [PeerId: Peer], associatedThreadInfo: Message.AssociatedThreadInfo? = nil) -> Message? { guard case let .Id(id) = message.id else { return nil diff --git a/submodules/TelegramNotices/Sources/Notices.swift b/submodules/TelegramNotices/Sources/Notices.swift index 627928f5ac2..7eee7c482ba 100644 --- a/submodules/TelegramNotices/Sources/Notices.swift +++ b/submodules/TelegramNotices/Sources/Notices.swift @@ -199,6 +199,7 @@ private enum ApplicationSpecificGlobalNotice: Int32 { case savedMessageTagLabelSuggestion = 65 case dismissedLastSeenBadge = 66 case dismissedMessagePrivacyBadge = 67 + case dismissedBusinessBadge = 68 var key: ValueBoxKey { let v = ValueBoxKey(length: 4) @@ -529,6 +530,10 @@ private struct ApplicationSpecificNoticeKeys { static func dismissedMessagePrivacyBadge() -> NoticeEntryKey { return NoticeEntryKey(namespace: noticeNamespace(namespace: globalNamespace), key: ApplicationSpecificGlobalNotice.dismissedMessagePrivacyBadge.key) } + + static func dismissedBusinessBadge() -> NoticeEntryKey { + return NoticeEntryKey(namespace: noticeNamespace(namespace: globalNamespace), key: ApplicationSpecificGlobalNotice.dismissedBusinessBadge.key) + } } public struct ApplicationSpecificNotice { @@ -2223,4 +2228,25 @@ public struct ApplicationSpecificNotice { } |> take(1) } + + public static func setDismissedBusinessBadge(accountManager: AccountManager) -> Signal { + return accountManager.transaction { transaction -> Void in + if let entry = CodableEntry(ApplicationSpecificBoolNotice()) { + transaction.setNotice(ApplicationSpecificNoticeKeys.dismissedBusinessBadge(), entry) + } + } + |> ignoreValues + } + + public static func dismissedBusinessBadge(accountManager: AccountManager) -> Signal { + return accountManager.noticeEntry(key: ApplicationSpecificNoticeKeys.dismissedBusinessBadge()) + |> map { view -> Bool in + if let _ = view.value?.get(ApplicationSpecificBoolNotice.self) { + return true + } else { + return false + } + } + |> take(1) + } } diff --git a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesSettings.swift b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesSettings.swift index 7e54418f9bb..9dac210e626 100644 --- a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesSettings.swift +++ b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesSettings.swift @@ -11,10 +11,41 @@ private func drawBorder(context: CGContext, rect: CGRect) { context.strokePath() } -private func renderIcon(name: String) -> UIImage? { +private func addRoundedRectPath(context: CGContext, rect: CGRect, radius: CGFloat) { + context.saveGState() + context.translateBy(x: rect.minX, y: rect.minY) + context.scaleBy(x: radius, y: radius) + let fw = rect.width / radius + let fh = rect.height / radius + context.move(to: CGPoint(x: fw, y: fh / 2.0)) + context.addArc(tangent1End: CGPoint(x: fw, y: fh), tangent2End: CGPoint(x: fw/2, y: fh), radius: 1.0) + context.addArc(tangent1End: CGPoint(x: 0, y: fh), tangent2End: CGPoint(x: 0, y: fh/2), radius: 1) + context.addArc(tangent1End: CGPoint(x: 0, y: 0), tangent2End: CGPoint(x: fw/2, y: 0), radius: 1) + context.addArc(tangent1End: CGPoint(x: fw, y: 0), tangent2End: CGPoint(x: fw, y: fh/2), radius: 1) + context.closePath() + context.restoreGState() +} + +private func renderIcon(name: String, backgroundColors: [UIColor]? = nil) -> UIImage? { return generateImage(CGSize(width: 29.0, height: 29.0), contextGenerator: { size, context in let bounds = CGRect(origin: CGPoint(), size: size) context.clear(bounds) + + if let backgroundColors { + addRoundedRectPath(context: context, rect: CGRect(origin: CGPoint(), size: size), radius: 7.0) + context.clip() + + var locations: [CGFloat] = [0.0, 1.0] + let colors: [CGColor] = backgroundColors.map(\.cgColor) + + let colorSpace = CGColorSpaceCreateDeviceRGB() + let gradient = CGGradient(colorsSpace: colorSpace, colors: colors as CFArray, locations: &locations)! + + context.drawLinearGradient(gradient, start: CGPoint(x: size.width, y: size.height), end: CGPoint(x: 0.0, y: 0.0), options: CGGradientDrawingOptions()) + + context.resetClip() + } + if let image = UIImage(bundleImageName: name)?.cgImage { context.draw(image, in: bounds) } @@ -44,6 +75,7 @@ public struct PresentationResourcesSettings { public static let powerSaving = renderIcon(name: "Settings/Menu/PowerSaving") public static let stories = renderIcon(name: "Settings/Menu/Stories") public static let premiumGift = renderIcon(name: "Settings/Menu/Gift") + public static let business = renderIcon(name: "Settings/Menu/Business", backgroundColors: [UIColor(rgb: 0xA95CE3), UIColor(rgb: 0xF16B80)]) public static let premium = generateImage(CGSize(width: 29.0, height: 29.0), contextGenerator: { size, context in let bounds = CGRect(origin: CGPoint(), size: size) diff --git a/submodules/TelegramStringFormatting/Sources/DateFormat.swift b/submodules/TelegramStringFormatting/Sources/DateFormat.swift index 27479dca88f..fc3e439fa5e 100644 --- a/submodules/TelegramStringFormatting/Sources/DateFormat.swift +++ b/submodules/TelegramStringFormatting/Sources/DateFormat.swift @@ -2,7 +2,7 @@ import Foundation import TelegramPresentationData import TelegramUIPreferences -public func stringForShortTimestamp(hours: Int32, minutes: Int32, dateTimeFormat: PresentationDateTimeFormat) -> String { +public func stringForShortTimestamp(hours: Int32, minutes: Int32, dateTimeFormat: PresentationDateTimeFormat, formatAsPlainText: Bool = false) -> String { switch dateTimeFormat.timeFormat { case .regular: let hourString: String @@ -20,10 +20,18 @@ public func stringForShortTimestamp(hours: Int32, minutes: Int32, dateTimeFormat } else { periodString = "AM" } + + let spaceCharacter: String + if formatAsPlainText { + spaceCharacter = " " + } else { + spaceCharacter = "\u{00a0}" + } + if minutes >= 10 { - return "\(hourString):\(minutes) \(periodString)" + return "\(hourString):\(minutes)\(spaceCharacter)\(periodString)" } else { - return "\(hourString):0\(minutes) \(periodString)" + return "\(hourString):0\(minutes)\(spaceCharacter)\(periodString)" } case .military: return String(format: "%02d:%02d", arguments: [Int(hours), Int(minutes)]) diff --git a/submodules/TelegramStringFormatting/Sources/PresenceStrings.swift b/submodules/TelegramStringFormatting/Sources/PresenceStrings.swift index b8f5bbd3db9..8cdc962093d 100644 --- a/submodules/TelegramStringFormatting/Sources/PresenceStrings.swift +++ b/submodules/TelegramStringFormatting/Sources/PresenceStrings.swift @@ -107,6 +107,46 @@ public func stringForMonth(strings: PresentationStrings, month: Int32, ofYear ye } } +private func monthAtIndex(_ index: Int, strings: PresentationStrings) -> String { + switch index { + case 0: + return strings.Month_ShortJanuary + case 1: + return strings.Month_ShortFebruary + case 2: + return strings.Month_ShortMarch + case 3: + return strings.Month_ShortApril + case 4: + return strings.Month_ShortMay + case 5: + return strings.Month_ShortJune + case 6: + return strings.Month_ShortJuly + case 7: + return strings.Month_ShortAugust + case 8: + return strings.Month_ShortSeptember + case 9: + return strings.Month_ShortOctober + case 10: + return strings.Month_ShortNovember + case 11: + return strings.Month_ShortDecember + default: + return "" + } +} + +public func stringForCompactDate(timestamp: Int32, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat) -> String { + var t: time_t = time_t(timestamp) + var timeinfo: tm = tm() + localtime_r(&t, &timeinfo) + + //TODO:localize + return "\(shortStringForDayOfWeek(strings: strings, day: timeinfo.tm_wday)) \(timeinfo.tm_mday) \(monthAtIndex(Int(timeinfo.tm_mon), strings: strings))" +} + public enum RelativeTimestampFormatDay { case today case yesterday @@ -362,37 +402,6 @@ public func stringForRelativeLiveLocationUpdateTimestamp(strings: PresentationSt } } -private func monthAtIndex(_ index: Int, strings: PresentationStrings) -> String { - switch index { - case 0: - return strings.Month_GenJanuary - case 1: - return strings.Month_GenFebruary - case 2: - return strings.Month_GenMarch - case 3: - return strings.Month_GenApril - case 4: - return strings.Month_GenMay - case 5: - return strings.Month_GenJune - case 6: - return strings.Month_GenJuly - case 7: - return strings.Month_GenAugust - case 8: - return strings.Month_GenSeptember - case 9: - return strings.Month_GenOctober - case 10: - return strings.Month_GenNovember - case 11: - return strings.Month_GenDecember - default: - return "" - } -} - public func stringForRelativeActivityTimestamp(strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, preciseTime: Bool = false, relativeTimestamp: Int32, relativeTo timestamp: Int32) -> String { let difference = timestamp - relativeTimestamp if difference < 60 { diff --git a/submodules/TelegramUI/BUILD b/submodules/TelegramUI/BUILD index e4b131d26b2..104d0a258fd 100644 --- a/submodules/TelegramUI/BUILD +++ b/submodules/TelegramUI/BUILD @@ -20,6 +20,7 @@ NGDEPS = [ "//Nicegram/NGModels:NGModels", "//Nicegram/NGWrap:NGWrap", "@FirebaseSDK//:FirebaseCrashlytics", + "@swiftpkg_nicegram_assistant_ios//:FeatAvatarGeneratorUI", "@swiftpkg_nicegram_assistant_ios//:FeatCardUI", "@swiftpkg_nicegram_assistant_ios//:FeatChatBanner", "@swiftpkg_nicegram_assistant_ios//:FeatPremium", @@ -462,8 +463,11 @@ swift_library( "//submodules/TelegramUI/Components/Chat/TopMessageReactions", "//submodules/TelegramUI/Components/Chat/SavedTagNameAlertController", "//submodules/TelegramUI/Components/Chat/ChatInlineSearchResultsListComponent", - "//submodules/TelegramUI/Components/Settings/BusinessSetupScreen", "//submodules/TelegramUI/Components/Settings/ChatbotSetupScreen", + "//submodules/TelegramUI/Components/Settings/BusinessLocationSetupScreen", + "//submodules/TelegramUI/Components/Settings/BusinessHoursSetupScreen", + "//submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen", + "//submodules/TelegramUI/Components/Settings/QuickReplyNameAlertController", ] + select({ "@build_bazel_rules_apple//apple:ios_arm64": appcenter_targets, "//build-system:ios_sim_arm64": [], diff --git a/submodules/TelegramUI/Components/AudioTranscriptionButtonComponent/Sources/AudioTranscriptionButtonComponent.swift b/submodules/TelegramUI/Components/AudioTranscriptionButtonComponent/Sources/AudioTranscriptionButtonComponent.swift index b614222d278..3696b9ffeaa 100644 --- a/submodules/TelegramUI/Components/AudioTranscriptionButtonComponent/Sources/AudioTranscriptionButtonComponent.swift +++ b/submodules/TelegramUI/Components/AudioTranscriptionButtonComponent/Sources/AudioTranscriptionButtonComponent.swift @@ -17,6 +17,12 @@ public final class AudioTranscriptionButtonComponent: Component { } else { return false } + case let .custom(lhsBackgroundColor, lhsForegroundColor): + if case let .custom(rhsBackgroundColor, rhsForegroundColor) = rhs { + return lhsBackgroundColor == rhsBackgroundColor && lhsForegroundColor == rhsForegroundColor + } else { + return false + } case let .freeform(lhsFreeform, lhsForeground): if case let .freeform(rhsFreeform, rhsForeground) = rhs, lhsFreeform == rhsFreeform, lhsForeground == rhsForeground { return true @@ -27,6 +33,7 @@ public final class AudioTranscriptionButtonComponent: Component { } case bubble(PresentationThemePartedColors) + case custom(UIColor, UIColor) case freeform((UIColor, Bool), UIColor) } @@ -101,6 +108,9 @@ public final class AudioTranscriptionButtonComponent: Component { case let .bubble(theme): foregroundColor = theme.bubble.withWallpaper.reactionActiveBackground backgroundColor = theme.bubble.withWallpaper.reactionInactiveBackground + case let .custom(backgroundColorValue, foregroundColorValue): + foregroundColor = foregroundColorValue + backgroundColor = backgroundColorValue case let .freeform(colorAndBlur, color): foregroundColor = color backgroundColor = .clear diff --git a/submodules/TelegramUI/Components/CameraScreen/Sources/CameraScreen.swift b/submodules/TelegramUI/Components/CameraScreen/Sources/CameraScreen.swift index 84de31e7780..0eb3f6033e9 100644 --- a/submodules/TelegramUI/Components/CameraScreen/Sources/CameraScreen.swift +++ b/submodules/TelegramUI/Components/CameraScreen/Sources/CameraScreen.swift @@ -2384,6 +2384,7 @@ public class CameraScreen: ViewController { bottom: bottomInset, right: layout.safeInsets.right ), + additionalInsets: layout.additionalInsets, inputHeight: layout.inputHeight ?? 0.0, metrics: layout.metrics, deviceMetrics: layout.deviceMetrics, diff --git a/submodules/TelegramUI/Components/Chat/ChatInlineSearchResultsListComponent/Sources/ChatInlineSearchResultsListComponent.swift b/submodules/TelegramUI/Components/Chat/ChatInlineSearchResultsListComponent/Sources/ChatInlineSearchResultsListComponent.swift index 0340332567c..0900afd2ae3 100644 --- a/submodules/TelegramUI/Components/Chat/ChatInlineSearchResultsListComponent/Sources/ChatInlineSearchResultsListComponent.swift +++ b/submodules/TelegramUI/Components/Chat/ChatInlineSearchResultsListComponent/Sources/ChatInlineSearchResultsListComponent.swift @@ -81,6 +81,7 @@ public final class ChatInlineSearchResultsListComponent: Component { public let loadTagMessages: (MemoryBuffer, MessageIndex?) -> Signal? public let getSearchResult: () -> Signal? public let getSavedPeers: (String) -> Signal<[(EnginePeer, MessageIndex?)], NoError>? + public let loadMoreSearchResults: () -> Void public init( context: AccountContext, @@ -92,7 +93,8 @@ public final class ChatInlineSearchResultsListComponent: Component { peerSelected: @escaping (EnginePeer) -> Void, loadTagMessages: @escaping (MemoryBuffer, MessageIndex?) -> Signal?, getSearchResult: @escaping () -> Signal?, - getSavedPeers: @escaping (String) -> Signal<[(EnginePeer, MessageIndex?)], NoError>? + getSavedPeers: @escaping (String) -> Signal<[(EnginePeer, MessageIndex?)], NoError>?, + loadMoreSearchResults: @escaping () -> Void ) { self.context = context self.presentation = presentation @@ -104,6 +106,7 @@ public final class ChatInlineSearchResultsListComponent: Component { self.loadTagMessages = loadTagMessages self.getSearchResult = getSearchResult self.getSavedPeers = getSavedPeers + self.loadMoreSearchResults = loadMoreSearchResults } public static func ==(lhs: ChatInlineSearchResultsListComponent, rhs: ChatInlineSearchResultsListComponent) -> Bool { @@ -377,6 +380,19 @@ public final class ChatInlineSearchResultsListComponent: Component { break } } + } else if let (currentIndex, disposable) = self.searchContents { + if let loadAroundIndex, loadAroundIndex != currentIndex { + switch component.contents { + case .empty: + break + case .tag: + break + case .search: + self.searchContents = (loadAroundIndex, disposable) + + component.loadMoreSearchResults() + } + } } } @@ -511,7 +527,7 @@ public final class ChatInlineSearchResultsListComponent: Component { contentId: .search(query), entries: entries, messages: messages, - hasEarlier: false, + hasEarlier: !(result?.completed ?? true), hasLater: false ) if !self.isUpdating { @@ -617,6 +633,8 @@ public final class ChatInlineSearchResultsListComponent: Component { openStories: { _, _ in }, dismissNotice: { _ in + }, + editPeer: { _ in } ) self.chatListNodeInteraction = chatListNodeInteraction diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageAnimatedStickerItemNode/Sources/ChatMessageAnimatedStickerItemNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageAnimatedStickerItemNode/Sources/ChatMessageAnimatedStickerItemNode.swift index 3a4f48fb5b4..bf2ab06bd62 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageAnimatedStickerItemNode/Sources/ChatMessageAnimatedStickerItemNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageAnimatedStickerItemNode/Sources/ChatMessageAnimatedStickerItemNode.swift @@ -434,10 +434,10 @@ public class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { override public func setupItem(_ item: ChatMessageItem, synchronousLoad: Bool) { super.setupItem(item, synchronousLoad: synchronousLoad) - if item.message.id.namespace == Namespaces.Message.Local || item.message.id.namespace == Namespaces.Message.ScheduledLocal { + if item.message.id.namespace == Namespaces.Message.Local || item.message.id.namespace == Namespaces.Message.ScheduledLocal || item.message.id.namespace == Namespaces.Message.QuickReplyLocal { self.wasPending = true } - if self.wasPending && (item.message.id.namespace != Namespaces.Message.Local && item.message.id.namespace != Namespaces.Message.ScheduledLocal) { + if self.wasPending && (item.message.id.namespace != Namespaces.Message.Local && item.message.id.namespace != Namespaces.Message.ScheduledLocal && item.message.id.namespace != Namespaces.Message.QuickReplyLocal) { self.didChangeFromPendingToSent = true } @@ -818,8 +818,6 @@ public class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { if !isBroadcastChannel { hasAvatar = true - } else if case .feed = item.chatLocation { - hasAvatar = true } } } else if incoming { @@ -844,8 +842,8 @@ public class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { } else if incoming { hasAvatar = true } - case .feed: - hasAvatar = true + case .customChatContents: + hasAvatar = false } if hasAvatar { @@ -859,7 +857,7 @@ public class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { var needsShareButton = false if case .pinnedMessages = item.associatedData.subject { needsShareButton = true - } else if isFailed || Namespaces.Message.allScheduled.contains(item.message.id.namespace) { + } else if isFailed || Namespaces.Message.allNonRegular.contains(item.message.id.namespace) { needsShareButton = false } else if item.message.id.peerId.isRepliesOrSavedMessages(accountPeerId: item.context.account.peerId) { for attribute in item.content.firstMessage.attributes { @@ -1445,6 +1443,9 @@ public class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { let dateAndStatusFrame = CGRect(origin: CGPoint(x: max(displayLeftInset, updatedImageFrame.maxX - dateAndStatusSize.width - 4.0), y: updatedImageFrame.maxY - dateAndStatusSize.height - 4.0 + imageBottomPadding), size: dateAndStatusSize) animation.animator.updateFrame(layer: strongSelf.dateAndStatusNode.layer, frame: dateAndStatusFrame, completion: nil) dateAndStatusApply(animation) + if case .customChatContents = item.associatedData.subject { + strongSelf.dateAndStatusNode.isHidden = true + } if needsReplyBackground { if strongSelf.replyBackgroundContent == nil, let backgroundContent = item.controllerInteraction.presentationContext.backgroundNode?.makeBubbleBackground(for: .free) { diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageAttachedContentNode/Sources/ChatMessageAttachedContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageAttachedContentNode/Sources/ChatMessageAttachedContentNode.swift index 688e432d8d4..a6c634bea46 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageAttachedContentNode/Sources/ChatMessageAttachedContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageAttachedContentNode/Sources/ChatMessageAttachedContentNode.swift @@ -675,7 +675,8 @@ public final class ChatMessageAttachedContentNode: ASDisplayNode { } var statusLayoutAndContinue: (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation) -> ChatMessageDateAndStatusNode))? - if case let .linear(_, bottom) = position { + if case .customChatContents = associatedData.subject { + } else if case let .linear(_, bottom) = position { switch bottom { case .None, .Neighbour(_, .footer, _): if message.adAttribute == nil { @@ -727,7 +728,7 @@ public final class ChatMessageAttachedContentNode: ASDisplayNode { let contentFileSizeAndApply: (CGSize, ChatMessageInteractiveFileNode.Apply)? if let contentFileFinalizeLayout { - let (size, apply) = contentFileFinalizeLayout(resultingWidth - insets.left - insets.right) + let (size, apply) = contentFileFinalizeLayout(resultingWidth - insets.left - insets.right - 6.0) contentFileSizeAndApply = (size, apply) } else { contentFileSizeAndApply = nil @@ -845,7 +846,7 @@ public final class ChatMessageAttachedContentNode: ASDisplayNode { offsetY: actualSize.height )) - actualSize.height += contentFileSize.height + actualSize.height += contentFileSize.height + 9.0 } case .actionButton: if let (actionButtonSize, _) = actionButtonSizeAndApply { diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift index 3fb79cbdeeb..3a0b47b7d76 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift @@ -338,7 +338,7 @@ private func contentNodeMessagesAndClassesForItem(_ item: ChatMessageItem) -> ([ needReactions = false } - if !isAction && !hasSeparateCommentsButton && !Namespaces.Message.allScheduled.contains(firstMessage.id.namespace) && !hideAllAdditionalInfo { + if !isAction && !hasSeparateCommentsButton && !Namespaces.Message.allNonRegular.contains(firstMessage.id.namespace) && !hideAllAdditionalInfo { if hasCommentButton(item: item) { result.append((firstMessage, ChatMessageCommentFooterContentNode.self, ChatMessageEntryAttributes(), BubbleItemAttributes(isAttachment: true, neighborType: .footer, neighborSpacing: .default))) } @@ -1478,8 +1478,8 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI if !isBroadcastChannel { hasAvatar = incoming - } else if case .feed = item.chatLocation { - hasAvatar = true + } else if case .customChatContents = item.chatLocation { + hasAvatar = false } } } else if incoming { @@ -1563,7 +1563,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI } else if case let .replyThread(replyThreadMessage) = item.chatLocation, replyThreadMessage.effectiveTopId == item.message.id { needsShareButton = false allowFullWidth = true - } else if isFailed || Namespaces.Message.allScheduled.contains(item.message.id.namespace) { + } else if isFailed || Namespaces.Message.allNonRegular.contains(item.message.id.namespace) { needsShareButton = false } else if item.message.id.peerId == item.context.account.peerId { if let _ = sourceReference { @@ -2156,7 +2156,8 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI maximumNodeWidth = size.width - if mosaicRange.upperBound == contentPropertiesAndLayouts.count || contentPropertiesAndLayouts[contentPropertiesAndLayouts.count - 1].3.isAttachment { + if case .customChatContents = item.associatedData.subject { + } else if mosaicRange.upperBound == contentPropertiesAndLayouts.count || contentPropertiesAndLayouts[contentPropertiesAndLayouts.count - 1].3.isAttachment { let message = item.content.firstMessage var edited = false diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageContactBubbleContentNode/Sources/ChatMessageContactBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageContactBubbleContentNode/Sources/ChatMessageContactBubbleContentNode.swift index ff61a4319a2..f94d9c3786c 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageContactBubbleContentNode/Sources/ChatMessageContactBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageContactBubbleContentNode/Sources/ChatMessageContactBubbleContentNode.swift @@ -252,7 +252,10 @@ public class ChatMessageContactBubbleContentNode: ChatMessageBubbleContentNode { let dateText = stringForMessageTimestampStatus(accountPeerId: item.context.account.peerId, message: item.message, dateTimeFormat: item.presentationData.dateTimeFormat, nameDisplayOrder: item.presentationData.nameDisplayOrder, strings: item.presentationData.strings, associatedData: item.associatedData) let statusType: ChatMessageDateAndStatusType? - switch position { + if case .customChatContents = item.associatedData.subject { + statusType = nil + } else { + switch position { case .linear(_, .None), .linear(_, .Neighbour(true, _, _)): if incoming { statusType = .BubbleIncoming @@ -267,6 +270,7 @@ public class ChatMessageContactBubbleContentNode: ChatMessageBubbleContentNode { } default: statusType = nil + } } var statusSuggestedWidthAndContinue: (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation) -> Void))? diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageFileBubbleContentNode/Sources/ChatMessageFileBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageFileBubbleContentNode/Sources/ChatMessageFileBubbleContentNode.swift index 10d7db92832..c9ddf26366a 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageFileBubbleContentNode/Sources/ChatMessageFileBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageFileBubbleContentNode/Sources/ChatMessageFileBubbleContentNode.swift @@ -112,8 +112,11 @@ public class ChatMessageFileBubbleContentNode: ChatMessageBubbleContentNode { incoming = false } let statusType: ChatMessageDateAndStatusType? - switch preparePosition { - case .linear(_, .None), .linear(_, .Neighbour(true, _, _)): + if case .customChatContents = item.associatedData.subject { + statusType = nil + } else { + switch preparePosition { + case .linear(_, .None), .linear(_, .Neighbour(true, _, _)): if incoming { statusType = .BubbleIncoming } else { @@ -127,6 +130,7 @@ public class ChatMessageFileBubbleContentNode: ChatMessageBubbleContentNode { } default: statusType = nil + } } let automaticDownload = shouldDownloadMediaAutomatically(settings: item.controllerInteraction.automaticMediaDownloadSettings, peerType: item.associatedData.automaticDownloadPeerType, networkType: item.associatedData.automaticDownloadNetworkType, authorPeerId: item.message.author?.id, contactsPeerIds: item.associatedData.contactsPeerIds, media: selectedFile!) diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageInstantVideoBubbleContentNode/Sources/ChatMessageInstantVideoBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageInstantVideoBubbleContentNode/Sources/ChatMessageInstantVideoBubbleContentNode.swift index 9eee510b6d3..a010d655e00 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageInstantVideoBubbleContentNode/Sources/ChatMessageInstantVideoBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageInstantVideoBubbleContentNode/Sources/ChatMessageInstantVideoBubbleContentNode.swift @@ -201,21 +201,25 @@ public class ChatMessageInstantVideoBubbleContentNode: ChatMessageBubbleContentN } let statusType: ChatMessageDateAndStatusType? - switch preparePosition { - case .linear(_, .None), .linear(_, .Neighbour(true, _, _)): - if incoming { - statusType = .BubbleIncoming - } else { - if item.message.flags.contains(.Failed) { - statusType = .BubbleOutgoing(.Failed) - } else if (item.message.flags.isSending && !item.message.isSentOrAcknowledged) || item.attributes.updatingMedia != nil { - statusType = .BubbleOutgoing(.Sending) + if case .customChatContents = item.associatedData.subject { + statusType = nil + } else { + switch preparePosition { + case .linear(_, .None), .linear(_, .Neighbour(true, _, _)): + if incoming { + statusType = .BubbleIncoming } else { - statusType = .BubbleOutgoing(.Sent(read: item.read)) + if item.message.flags.contains(.Failed) { + statusType = .BubbleOutgoing(.Failed) + } else if (item.message.flags.isSending && !item.message.isSentOrAcknowledged) || item.attributes.updatingMedia != nil { + statusType = .BubbleOutgoing(.Sending) + } else { + statusType = .BubbleOutgoing(.Sent(read: item.read)) + } } + default: + statusType = nil } - default: - statusType = nil } let automaticDownload = shouldDownloadMediaAutomatically(settings: item.controllerInteraction.automaticMediaDownloadSettings, peerType: item.associatedData.automaticDownloadPeerType, networkType: item.associatedData.automaticDownloadNetworkType, authorPeerId: item.message.author?.id, contactsPeerIds: item.associatedData.contactsPeerIds, media: selectedFile!) diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageInstantVideoItemNode/Sources/ChatMessageInstantVideoItemNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageInstantVideoItemNode/Sources/ChatMessageInstantVideoItemNode.swift index e19651af743..c1ba69fe709 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageInstantVideoItemNode/Sources/ChatMessageInstantVideoItemNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageInstantVideoItemNode/Sources/ChatMessageInstantVideoItemNode.swift @@ -99,7 +99,7 @@ public class ChatMessageInstantVideoItemNode: ChatMessageItemView, UIGestureReco self.interactiveVideoNode.shouldOpen = { [weak self] in if let strongSelf = self { - if let item = strongSelf.item, (item.message.id.namespace == Namespaces.Message.Local || item.message.id.namespace == Namespaces.Message.ScheduledLocal) { + if let item = strongSelf.item, (item.message.id.namespace == Namespaces.Message.Local || item.message.id.namespace == Namespaces.Message.ScheduledLocal || item.message.id.namespace == Namespaces.Message.QuickReplyLocal) { return false } return !strongSelf.animatingHeight @@ -321,8 +321,8 @@ public class ChatMessageInstantVideoItemNode: ChatMessageItemView, UIGestureReco if !isBroadcastChannel { hasAvatar = true - } else if case .feed = item.chatLocation { - hasAvatar = true + } else if case .customChatContents = item.chatLocation { + hasAvatar = false } } } else if incoming { @@ -341,7 +341,7 @@ public class ChatMessageInstantVideoItemNode: ChatMessageItemView, UIGestureReco var needsShareButton = false if case .pinnedMessages = item.associatedData.subject { needsShareButton = true - } else if isFailed || Namespaces.Message.allScheduled.contains(item.message.id.namespace) { + } else if isFailed || Namespaces.Message.allNonRegular.contains(item.message.id.namespace) { needsShareButton = false } else if item.message.id.peerId == item.context.account.peerId { diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveFileNode/Sources/ChatMessageInteractiveFileNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveFileNode/Sources/ChatMessageInteractiveFileNode.swift index 4262768be25..7c11d7204f4 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveFileNode/Sources/ChatMessageInteractiveFileNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveFileNode/Sources/ChatMessageInteractiveFileNode.swift @@ -883,7 +883,9 @@ public final class ChatMessageInteractiveFileNode: ASDisplayNode { var updatedAudioTranscriptionState: AudioTranscriptionButtonComponent.TranscriptionState? var displayTranscribe = false - if arguments.message.id.peerId.namespace != Namespaces.Peer.SecretChat && !isViewOnceMessage && !arguments.presentationData.isPreview { + if Namespaces.Message.allNonRegular.contains(arguments.message.id.namespace) { + displayTranscribe = false + } else if arguments.message.id.peerId.namespace != Namespaces.Peer.SecretChat && !isViewOnceMessage && !arguments.presentationData.isPreview { let premiumConfiguration = PremiumConfiguration.with(appConfiguration: arguments.context.currentAppConfiguration.with { $0 }) if arguments.associatedData.isPremium { displayTranscribe = true @@ -1469,10 +1471,16 @@ public final class ChatMessageInteractiveFileNode: ASDisplayNode { strongSelf.view.addSubview(audioTranscriptionButton) added = true } + let buttonTheme: AudioTranscriptionButtonComponent.Theme + if let customTintColor = arguments.customTintColor { + buttonTheme = .custom(customTintColor.withMultipliedAlpha(0.1), customTintColor) + } else { + buttonTheme = .bubble(arguments.incoming ? arguments.presentationData.theme.theme.chat.message.incoming : arguments.presentationData.theme.theme.chat.message.outgoing) + } let audioTranscriptionButtonSize = audioTranscriptionButton.update( transition: animation.isAnimated ? .easeInOut(duration: 0.3) : .immediate, component: AnyComponent(AudioTranscriptionButtonComponent( - theme: .bubble(arguments.incoming ? arguments.presentationData.theme.theme.chat.message.incoming : arguments.presentationData.theme.theme.chat.message.outgoing), + theme: buttonTheme, transcriptionState: effectiveAudioTranscriptionState, pressed: { guard let strongSelf = self else { diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveInstantVideoNode/Sources/ChatMessageInteractiveInstantVideoNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveInstantVideoNode/Sources/ChatMessageInteractiveInstantVideoNode.swift index a3a48a3c5b1..e10b3d40c74 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveInstantVideoNode/Sources/ChatMessageInteractiveInstantVideoNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveInstantVideoNode/Sources/ChatMessageInteractiveInstantVideoNode.swift @@ -464,7 +464,7 @@ public class ChatMessageInteractiveInstantVideoNode: ASDisplayNode { break } } - if item.message.id.namespace == Namespaces.Message.Local || item.message.id.namespace == Namespaces.Message.ScheduledLocal { + if item.message.id.namespace == Namespaces.Message.Local || item.message.id.namespace == Namespaces.Message.ScheduledLocal || item.message.id.namespace == Namespaces.Message.QuickReplyLocal { notConsumed = true } @@ -949,6 +949,10 @@ public class ChatMessageInteractiveInstantVideoNode: ASDisplayNode { animation.animator.updateFrame(layer: strongSelf.dateAndStatusNode.layer, frame: dateAndStatusFrame, completion: nil) } + if case .customChatContents = item.associatedData.subject { + strongSelf.dateAndStatusNode.isHidden = true + } + if let videoNode = strongSelf.videoNode { videoNode.bounds = CGRect(origin: CGPoint(), size: videoFrame.size) if strongSelf.imageScale != imageScale { diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageItemImpl/Sources/ChatMessageDateHeader.swift b/submodules/TelegramUI/Components/Chat/ChatMessageItemImpl/Sources/ChatMessageDateHeader.swift index e1f4f5b65f8..297e1a4988c 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageItemImpl/Sources/ChatMessageDateHeader.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageItemImpl/Sources/ChatMessageDateHeader.swift @@ -524,7 +524,7 @@ public final class ChatMessageAvatarHeaderNodeImpl: ListViewItemHeaderNode, Chat return } var messageId: MessageId? - if let messageReference = messageReference, case let .message(_, _, id, _, _, _) = messageReference.content { + if let messageReference = messageReference, case let .message(_, _, id, _, _, _, _) = messageReference.content { messageId = id } strongSelf.controllerInteraction?.openPeerContextMenu(peer, messageId, strongSelf.containerNode, strongSelf.containerNode.bounds, gesture) @@ -751,7 +751,7 @@ public final class ChatMessageAvatarHeaderNodeImpl: ListViewItemHeaderNode, Chat @objc private func tapGesture(_ recognizer: ListViewTapGestureRecognizer) { if case .ended = recognizer.state { - if self.peerId.namespace == Namespaces.Peer.Empty, case let .message(_, _, id, _, _, _) = self.messageReference?.content { + if self.peerId.namespace == Namespaces.Peer.Empty, case let .message(_, _, id, _, _, _, _) = self.messageReference?.content { self.controllerInteraction?.displayMessageTooltip(id, self.presentationData.strings.Conversation_ForwardAuthorHiddenTooltip, self, self.avatarNode.frame) } else if let peer = self.peer { if let adMessageId = self.adMessageId { diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageItemImpl/Sources/ChatMessageItemImpl.swift b/submodules/TelegramUI/Components/Chat/ChatMessageItemImpl/Sources/ChatMessageItemImpl.swift index 809bc40416b..e6cd1aaaeae 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageItemImpl/Sources/ChatMessageItemImpl.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageItemImpl/Sources/ChatMessageItemImpl.swift @@ -372,7 +372,10 @@ public final class ChatMessageItemImpl: ChatMessageItem, CustomStringConvertible } self.avatarHeader = avatarHeader - var headers: [ListViewItemHeader] = [self.dateHeader] + var headers: [ListViewItemHeader] = [] + if !self.disableDate { + headers.append(self.dateHeader) + } if case .messageOptions = associatedData.subject { headers = [] } diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageMapBubbleContentNode/Sources/ChatMessageMapBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageMapBubbleContentNode/Sources/ChatMessageMapBubbleContentNode.swift index 18cf1765151..c1f43229378 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageMapBubbleContentNode/Sources/ChatMessageMapBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageMapBubbleContentNode/Sources/ChatMessageMapBubbleContentNode.swift @@ -223,7 +223,10 @@ public class ChatMessageMapBubbleContentNode: ChatMessageBubbleContentNode { let dateText = stringForMessageTimestampStatus(accountPeerId: item.context.account.peerId, message: item.message, dateTimeFormat: item.presentationData.dateTimeFormat, nameDisplayOrder: item.presentationData.nameDisplayOrder, strings: item.presentationData.strings, format: dateFormat, associatedData: item.associatedData) let statusType: ChatMessageDateAndStatusType? - switch position { + if case .customChatContents = item.associatedData.subject { + statusType = nil + } else { + switch position { case .linear(_, .None), .linear(_, .Neighbour(true, _, _)): if selectedMedia?.venue != nil || activeLiveBroadcastingTimeout != nil { if incoming { @@ -252,6 +255,7 @@ public class ChatMessageMapBubbleContentNode: ChatMessageBubbleContentNode { } default: statusType = nil + } } var statusSize = CGSize() diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageMediaBubbleContentNode/Sources/ChatMessageMediaBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageMediaBubbleContentNode/Sources/ChatMessageMediaBubbleContentNode.swift index 85a3503874d..61fe835f664 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageMediaBubbleContentNode/Sources/ChatMessageMediaBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageMediaBubbleContentNode/Sources/ChatMessageMediaBubbleContentNode.swift @@ -284,7 +284,10 @@ public class ChatMessageMediaBubbleContentNode: ChatMessageBubbleContentNode { let dateText = stringForMessageTimestampStatus(accountPeerId: item.context.account.peerId, message: item.message, dateTimeFormat: item.presentationData.dateTimeFormat, nameDisplayOrder: item.presentationData.nameDisplayOrder, strings: item.presentationData.strings, format: dateFormat, associatedData: item.associatedData) let statusType: ChatMessageDateAndStatusType? - switch preparePosition { + if case .customChatContents = item.associatedData.subject { + statusType = nil + } else { + switch preparePosition { case .linear(_, .None), .linear(_, .Neighbour(true, _, _)): if item.message.effectivelyIncoming(item.context.account.peerId) { statusType = .ImageIncoming @@ -301,6 +304,7 @@ public class ChatMessageMediaBubbleContentNode: ChatMessageBubbleContentNode { statusType = nil default: statusType = nil + } } var isReplyThread = false diff --git a/submodules/TelegramUI/Components/Chat/ChatMessagePollBubbleContentNode/Sources/ChatMessagePollBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessagePollBubbleContentNode/Sources/ChatMessagePollBubbleContentNode.swift index a0261a4de67..940ca9feb7b 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessagePollBubbleContentNode/Sources/ChatMessagePollBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessagePollBubbleContentNode/Sources/ChatMessagePollBubbleContentNode.swift @@ -494,7 +494,7 @@ private final class ChatMessagePollOptionNode: ASDisplayNode { let shouldHaveRadioNode = optionResult == nil let isSelectable: Bool - if shouldHaveRadioNode, case .poll(multipleAnswers: true) = poll.kind, !Namespaces.Message.allScheduled.contains(message.id.namespace) { + if shouldHaveRadioNode, case .poll(multipleAnswers: true) = poll.kind, !Namespaces.Message.allNonRegular.contains(message.id.namespace) { isSelectable = true } else { isSelectable = false @@ -905,7 +905,7 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode { } } if !hasSelection { - if !Namespaces.Message.allScheduled.contains(item.message.id.namespace) { + if !Namespaces.Message.allNonRegular.contains(item.message.id.namespace) { item.controllerInteraction.requestOpenMessagePollResults(item.message.id, pollId) } } else if !selectedOpaqueIdentifiers.isEmpty { @@ -986,7 +986,10 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode { let dateText = stringForMessageTimestampStatus(accountPeerId: item.context.account.peerId, message: item.message, dateTimeFormat: item.presentationData.dateTimeFormat, nameDisplayOrder: item.presentationData.nameDisplayOrder, strings: item.presentationData.strings, format: dateFormat, associatedData: item.associatedData) let statusType: ChatMessageDateAndStatusType? - switch position { + if case .customChatContents = item.associatedData.subject { + statusType = nil + } else { + switch position { case .linear(_, .None), .linear(_, .Neighbour(true, _, _)): if incoming { statusType = .BubbleIncoming @@ -1001,8 +1004,9 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode { } default: statusType = nil + } } - + var statusSuggestedWidthAndContinue: (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation) -> Void))? if let statusType = statusType { @@ -1207,7 +1211,7 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode { boundingSize.width = max(boundingSize.width, min(270.0, constrainedSize.width)) var canVote = false - if (item.message.id.namespace == Namespaces.Message.Cloud || Namespaces.Message.allScheduled.contains(item.message.id.namespace)), let poll = poll, poll.pollId.namespace == Namespaces.Media.CloudPoll, !isClosed { + if (item.message.id.namespace == Namespaces.Message.Cloud || Namespaces.Message.allNonRegular.contains(item.message.id.namespace)), let poll = poll, poll.pollId.namespace == Namespaces.Media.CloudPoll, !isClosed { var hasVoted = false if let voters = poll.results.voters { for voter in voters { @@ -1576,7 +1580,7 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode { self.buttonNode.isHidden = false } - if Namespaces.Message.allScheduled.contains(item.message.id.namespace) { + if Namespaces.Message.allNonRegular.contains(item.message.id.namespace) { self.buttonNode.isUserInteractionEnabled = false } else { self.buttonNode.isUserInteractionEnabled = true @@ -1639,7 +1643,7 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode { if optionNode.frame.contains(point), case .tap = gesture { if optionNode.isUserInteractionEnabled { return ChatMessageBubbleContentTapAction(content: .ignore) - } else if let result = optionNode.currentResult, let item = self.item, !Namespaces.Message.allScheduled.contains(item.message.id.namespace), let poll = self.poll, let option = optionNode.option, !isBotChat { + } else if let result = optionNode.currentResult, let item = self.item, !Namespaces.Message.allNonRegular.contains(item.message.id.namespace), let poll = self.poll, let option = optionNode.option, !isBotChat { switch poll.publicity { case .anonymous: let string: String diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageRestrictedBubbleContentNode/Sources/ChatMessageRestrictedBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageRestrictedBubbleContentNode/Sources/ChatMessageRestrictedBubbleContentNode.swift index 6dbf1521f66..5871ffeff55 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageRestrictedBubbleContentNode/Sources/ChatMessageRestrictedBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageRestrictedBubbleContentNode/Sources/ChatMessageRestrictedBubbleContentNode.swift @@ -77,21 +77,25 @@ public class ChatMessageRestrictedBubbleContentNode: ChatMessageBubbleContentNod let dateText = stringForMessageTimestampStatus(accountPeerId: item.context.account.peerId, message: item.message, dateTimeFormat: item.presentationData.dateTimeFormat, nameDisplayOrder: item.presentationData.nameDisplayOrder, strings: item.presentationData.strings, associatedData: item.associatedData) let statusType: ChatMessageDateAndStatusType? - switch position { - case .linear(_, .None), .linear(_, .Neighbour(true, _, _)): - if incoming { - statusType = .BubbleIncoming - } else { - if message.flags.contains(.Failed) { - statusType = .BubbleOutgoing(.Failed) - } else if message.flags.isSending && !message.isSentOrAcknowledged { - statusType = .BubbleOutgoing(.Sending) + if case .customChatContents = item.associatedData.subject { + statusType = nil + } else { + switch position { + case .linear(_, .None), .linear(_, .Neighbour(true, _, _)): + if incoming { + statusType = .BubbleIncoming } else { - statusType = .BubbleOutgoing(.Sent(read: item.read)) + if message.flags.contains(.Failed) { + statusType = .BubbleOutgoing(.Failed) + } else if message.flags.isSending && !message.isSentOrAcknowledged { + statusType = .BubbleOutgoing(.Sending) + } else { + statusType = .BubbleOutgoing(.Sent(read: item.read)) + } } + default: + statusType = nil } - default: - statusType = nil } let entities = [MessageTextEntity(range: 0.. Void public let openRecommendedChannelContextMenu: (EnginePeer, UIView, ContextGesture?) -> Void public let openGroupBoostInfo: (EnginePeer.Id?, Int) -> Void + public let openStickerEditor: () -> Void public let requestMessageUpdate: (MessageId, Bool) -> Void public let cancelInteractiveKeyboardGestures: () -> Void @@ -362,6 +363,7 @@ public final class ChatControllerInteraction: ChatControllerInteractionProtocol openPremiumStatusInfo: @escaping (EnginePeer.Id, UIView, Int64?, PeerNameColor) -> Void, openRecommendedChannelContextMenu: @escaping (EnginePeer, UIView, ContextGesture?) -> Void, openGroupBoostInfo: @escaping (EnginePeer.Id?, Int) -> Void, + openStickerEditor: @escaping () -> Void, requestMessageUpdate: @escaping (MessageId, Bool) -> Void, cancelInteractiveKeyboardGestures: @escaping () -> Void, dismissTextInput: @escaping () -> Void, @@ -468,6 +470,7 @@ public final class ChatControllerInteraction: ChatControllerInteractionProtocol self.openPremiumStatusInfo = openPremiumStatusInfo self.openRecommendedChannelContextMenu = openRecommendedChannelContextMenu self.openGroupBoostInfo = openGroupBoostInfo + self.openStickerEditor = openStickerEditor self.requestMessageUpdate = requestMessageUpdate self.cancelInteractiveKeyboardGestures = cancelInteractiveKeyboardGestures diff --git a/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/ChatEntityKeyboardInputNode.swift b/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/ChatEntityKeyboardInputNode.swift index 70f925a52e5..d6e954fdb2d 100644 --- a/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/ChatEntityKeyboardInputNode.swift +++ b/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/ChatEntityKeyboardInputNode.swift @@ -56,6 +56,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { let dismissTextInput: () -> Void let insertText: (NSAttributedString) -> Void let backwardsDeleteText: () -> Void + let openStickerEditor: () -> Void let presentController: (ViewController, Any?) -> Void let presentGlobalOverlayController: (ViewController, Any?) -> Void let getNavigationController: () -> NavigationController? @@ -72,6 +73,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { dismissTextInput: @escaping () -> Void, insertText: @escaping (NSAttributedString) -> Void, backwardsDeleteText: @escaping () -> Void, + openStickerEditor: @escaping () -> Void, presentController: @escaping (ViewController, Any?) -> Void, presentGlobalOverlayController: @escaping (ViewController, Any?) -> Void, getNavigationController: @escaping () -> NavigationController?, @@ -86,6 +88,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { self.dismissTextInput = dismissTextInput self.insertText = insertText self.backwardsDeleteText = backwardsDeleteText + self.openStickerEditor = openStickerEditor self.presentController = presentController self.presentGlobalOverlayController = presentGlobalOverlayController self.getNavigationController = getNavigationController @@ -106,6 +109,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { self.dismissTextInput = chatControllerInteraction.dismissTextInput self.insertText = panelInteraction.insertText self.backwardsDeleteText = panelInteraction.backwardsDeleteText + self.openStickerEditor = chatControllerInteraction.openStickerEditor self.presentController = chatControllerInteraction.presentController self.presentGlobalOverlayController = chatControllerInteraction.presentGlobalOverlayController self.getNavigationController = chatControllerInteraction.navigationController @@ -1148,6 +1152,9 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { return } guard let file = item.itemFile else { + if groupId == AnyHashable("recent"), case .icon(.add) = item.content { + interaction.openStickerEditor() + } return } diff --git a/submodules/TelegramUI/Components/ChatListHeaderComponent/Sources/ChatListHeaderComponent.swift b/submodules/TelegramUI/Components/ChatListHeaderComponent/Sources/ChatListHeaderComponent.swift index e41442d0ae3..ae0f24162d2 100644 --- a/submodules/TelegramUI/Components/ChatListHeaderComponent/Sources/ChatListHeaderComponent.swift +++ b/submodules/TelegramUI/Components/ChatListHeaderComponent/Sources/ChatListHeaderComponent.swift @@ -267,7 +267,9 @@ public final class ChatListHeaderComponent: Component { } func update(title: String, theme: PresentationTheme, availableSize: CGSize, transition: Transition) -> CGSize { - self.titleView.attributedText = NSAttributedString(string: title, font: Font.regular(17.0), textColor: theme.rootController.navigationBar.accentTextColor) + let titleText = NSAttributedString(string: title, font: Font.regular(17.0), textColor: theme.rootController.navigationBar.accentTextColor) + let titleTextUpdated = self.titleView.attributedText != titleText + self.titleView.attributedText = titleText let titleSize = self.titleView.updateLayout(CGSize(width: 100.0, height: 44.0)) self.accessibilityLabel = title @@ -287,7 +289,12 @@ public final class ChatListHeaderComponent: Component { transition.setPosition(view: self.arrowView, position: arrowFrame.center) transition.setBounds(view: self.arrowView, bounds: CGRect(origin: CGPoint(), size: arrowFrame.size)) - transition.setFrame(view: self.titleView, frame: CGRect(origin: CGPoint(x: iconOffset - 3.0 + arrowSize.width + iconSpacing, y: floor((availableSize.height - titleSize.height) / 2.0)), size: titleSize)) + let titleFrame = CGRect(origin: CGPoint(x: iconOffset - 3.0 + arrowSize.width + iconSpacing, y: floor((availableSize.height - titleSize.height) / 2.0)), size: titleSize) + if titleTextUpdated { + self.titleView.frame = titleFrame + } else { + transition.setFrame(view: self.titleView, frame: titleFrame) + } return CGSize(width: iconOffset + arrowSize.width + iconSpacing + titleSize.width, height: availableSize.height) } @@ -479,7 +486,9 @@ public final class ChatListHeaderComponent: Component { transition.setPosition(view: self.titleScaleContainer, position: CGPoint(x: size.width * 0.5, y: size.height * 0.5)) transition.setBounds(view: self.titleScaleContainer, bounds: CGRect(origin: self.titleScaleContainer.bounds.origin, size: size)) - self.titleTextView.attributedText = NSAttributedString(string: content.title, font: Font.semibold(17.0), textColor: theme.rootController.navigationBar.primaryTextColor) + let titleText = NSAttributedString(string: content.title, font: Font.semibold(17.0), textColor: theme.rootController.navigationBar.primaryTextColor) + let titleTextUpdated = self.titleTextView.attributedText != titleText + self.titleTextView.attributedText = titleText let buttonSpacing: CGFloat = 8.0 @@ -616,7 +625,11 @@ public final class ChatListHeaderComponent: Component { let titleTextSize = self.titleTextView.updateLayout(CGSize(width: remainingWidth, height: size.height)) let titleFrame = CGRect(origin: CGPoint(x: floor((size.width - titleTextSize.width) / 2.0) + sideContentWidth, y: floor((size.height - titleTextSize.height) / 2.0)), size: titleTextSize) - transition.setFrame(view: self.titleTextView, frame: titleFrame) + if titleTextUpdated { + self.titleTextView.frame = titleFrame + } else { + transition.setFrame(view: self.titleTextView, frame: titleFrame) + } if let titleComponent = content.titleComponent { var titleContentTransition = transition diff --git a/submodules/TelegramUI/Components/ChatListHeaderComponent/Sources/ChatListNavigationBar.swift b/submodules/TelegramUI/Components/ChatListHeaderComponent/Sources/ChatListNavigationBar.swift index 4f8e504ce4d..d59b4fe7729 100644 --- a/submodules/TelegramUI/Components/ChatListHeaderComponent/Sources/ChatListNavigationBar.swift +++ b/submodules/TelegramUI/Components/ChatListHeaderComponent/Sources/ChatListNavigationBar.swift @@ -27,6 +27,7 @@ public final class ChatListNavigationBar: Component { public let statusBarHeight: CGFloat public let sideInset: CGFloat public let isSearchActive: Bool + public let isSearchEnabled: Bool public let primaryContent: ChatListHeaderComponent.Content? public let secondaryContent: ChatListHeaderComponent.Content? public let secondaryTransition: CGFloat @@ -48,6 +49,7 @@ public final class ChatListNavigationBar: Component { statusBarHeight: CGFloat, sideInset: CGFloat, isSearchActive: Bool, + isSearchEnabled: Bool, primaryContent: ChatListHeaderComponent.Content?, secondaryContent: ChatListHeaderComponent.Content?, secondaryTransition: CGFloat, @@ -68,6 +70,7 @@ public final class ChatListNavigationBar: Component { self.statusBarHeight = statusBarHeight self.sideInset = sideInset self.isSearchActive = isSearchActive + self.isSearchEnabled = isSearchEnabled self.primaryContent = primaryContent self.secondaryContent = secondaryContent self.secondaryTransition = secondaryTransition @@ -102,6 +105,9 @@ public final class ChatListNavigationBar: Component { if lhs.isSearchActive != rhs.isSearchActive { return false } + if lhs.isSearchEnabled != rhs.isSearchEnabled { + return false + } if lhs.primaryContent != rhs.primaryContent { return false } @@ -316,6 +322,9 @@ public final class ChatListNavigationBar: Component { searchContentNode.updateLayout(size: searchSize, leftInset: component.sideInset, rightInset: component.sideInset, transition: transition.containedViewLayoutTransition) + transition.setAlpha(view: searchContentNode.view, alpha: component.isSearchEnabled ? 1.0 : 0.5) + searchContentNode.isUserInteractionEnabled = component.isSearchEnabled + let headerTransition = transition let storiesOffsetFraction: CGFloat @@ -520,6 +529,7 @@ public final class ChatListNavigationBar: Component { statusBarHeight: component.statusBarHeight, sideInset: component.sideInset, isSearchActive: component.isSearchActive, + isSearchEnabled: component.isSearchEnabled, primaryContent: component.primaryContent, secondaryContent: component.secondaryContent, secondaryTransition: component.secondaryTransition, diff --git a/submodules/TelegramUI/Components/EmptyStateIndicatorComponent/Sources/EmptyStateIndicatorComponent.swift b/submodules/TelegramUI/Components/EmptyStateIndicatorComponent/Sources/EmptyStateIndicatorComponent.swift index 4cb104ad66a..01ac92dbe0c 100644 --- a/submodules/TelegramUI/Components/EmptyStateIndicatorComponent/Sources/EmptyStateIndicatorComponent.swift +++ b/submodules/TelegramUI/Components/EmptyStateIndicatorComponent/Sources/EmptyStateIndicatorComponent.swift @@ -16,6 +16,8 @@ public final class EmptyStateIndicatorComponent: Component { public let text: String public let actionTitle: String? public let action: () -> Void + public let additionalActionTitle: String? + public let additionalAction: () -> Void public init( context: AccountContext, @@ -24,7 +26,9 @@ public final class EmptyStateIndicatorComponent: Component { title: String, text: String, actionTitle: String?, - action: @escaping () -> Void + action: @escaping () -> Void, + additionalActionTitle: String?, + additionalAction: @escaping () -> Void ) { self.context = context self.theme = theme @@ -33,6 +37,8 @@ public final class EmptyStateIndicatorComponent: Component { self.text = text self.actionTitle = actionTitle self.action = action + self.additionalActionTitle = additionalActionTitle + self.additionalAction = additionalAction } public static func ==(lhs: EmptyStateIndicatorComponent, rhs: EmptyStateIndicatorComponent) -> Bool { @@ -54,6 +60,9 @@ public final class EmptyStateIndicatorComponent: Component { if lhs.actionTitle != rhs.actionTitle { return false } + if lhs.additionalActionTitle != rhs.additionalActionTitle { + return false + } return true } @@ -65,6 +74,7 @@ public final class EmptyStateIndicatorComponent: Component { private let title = ComponentView() private let text = ComponentView() private var button: ComponentView? + private var additionalButton: ComponentView? override public init(frame: CGRect) { super.init(frame: frame) @@ -139,7 +149,7 @@ public final class EmptyStateIndicatorComponent: Component { } )), environment: {}, - containerSize: CGSize(width: 240.0, height: 50.0) + containerSize: CGSize(width: 260.0, height: 50.0) ) } else { if let button = self.button { @@ -148,14 +158,52 @@ public final class EmptyStateIndicatorComponent: Component { } } + var additionalButtonSize: CGSize? + if let additionalActionTitle = component.additionalActionTitle { + let additionalButton: ComponentView + if let current = self.additionalButton { + additionalButton = current + } else { + additionalButton = ComponentView() + self.additionalButton = additionalButton + } + + additionalButtonSize = additionalButton.update( + transition: transition, + component: AnyComponent(Button( + content: AnyComponent(Text( + text: additionalActionTitle, font: + Font.regular(17.0), + color: component.theme.list.itemAccentColor) + ), + action: { [weak self] in + guard let self, let component = self.component else { + return + } + component.additionalAction() + } + )), + environment: {}, + containerSize: CGSize(width: 262.0, height: 50.0) + ) + } else { + if let additionalButton = self.additionalButton { + self.additionalButton = nil + additionalButton.view?.removeFromSuperview() + } + } + let animationSpacing: CGFloat = 11.0 let titleSpacing: CGFloat = 17.0 - let buttonSpacing: CGFloat = 17.0 + let buttonSpacing: CGFloat = 21.0 var totalHeight: CGFloat = animationSize.height + animationSpacing + titleSize.height + titleSpacing + textSize.height if let buttonSize { totalHeight += buttonSpacing + buttonSize.height } + if let additionalButtonSize { + totalHeight += buttonSpacing + additionalButtonSize.height + } var contentY = floor((availableSize.height - totalHeight) * 0.5) @@ -185,7 +233,14 @@ public final class EmptyStateIndicatorComponent: Component { self.addSubview(buttonView) } transition.setFrame(view: buttonView, frame: CGRect(origin: CGPoint(x: floor((availableSize.width - buttonSize.width) * 0.5), y: contentY), size: buttonSize)) - contentY += buttonSize.height + contentY += buttonSize.height + buttonSpacing + } + if let additionalButtonSize, let additionalButtonView = self.additionalButton?.view { + if additionalButtonView.superview == nil { + self.addSubview(additionalButtonView) + } + transition.setFrame(view: additionalButtonView, frame: CGRect(origin: CGPoint(x: floor((availableSize.width - additionalButtonSize.width) * 0.5), y: contentY), size: additionalButtonSize)) + contentY += additionalButtonSize.height } return availableSize diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift index 13dc762e1f6..7c141f89eee 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift @@ -2519,6 +2519,7 @@ public final class EmojiPagerContentComponent: Component { case premiumStar case topic(String, Int32) case stop + case add } case animation(EntityKeyboardAnimationData) @@ -3559,6 +3560,15 @@ public final class EmojiPagerContentComponent: Component { let imageSize = image.size.aspectFitted(CGSize(width: size.width - 6.0, height: size.height - 6.0)) image.draw(in: CGRect(origin: CGPoint(x: floor((size.width - imageSize.width) / 2.0), y: floor((size.height - imageSize.height) / 2.0)), size: imageSize)) } + case .add: + context.setFillColor(UIColor.black.withAlphaComponent(0.08).cgColor) + context.fillEllipse(in: CGRect(origin: .zero, size: size).insetBy(dx: 8.0, dy: 8.0)) + context.setFillColor(UIColor.black.withAlphaComponent(0.16).cgColor) + + let plusSize = CGSize(width: 4.5, height: 31.5) + context.addPath(UIBezierPath(roundedRect: CGRect(x: floorToScreenPixels((size.width - plusSize.width) / 2.0), y: floorToScreenPixels((size.height - plusSize.height) / 2.0), width: plusSize.width, height: plusSize.height), cornerRadius: plusSize.width / 2.0).cgPath) + context.addPath(UIBezierPath(roundedRect: CGRect(x: floorToScreenPixels((size.width - plusSize.height) / 2.0), y: floorToScreenPixels((size.height - plusSize.width) / 2.0), width: plusSize.height, height: plusSize.width), cornerRadius: plusSize.width / 2.0).cgPath) + context.fillPath() } UIGraphicsPopContext() diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentSignals.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentSignals.swift index 7f7ec18d10c..4fbf2390c14 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentSignals.swift +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentSignals.swift @@ -1784,6 +1784,7 @@ public extension EmojiPagerContentComponent { } if let recentStickers = recentStickers { + let groupId = "recent" for item in recentStickers.items { guard let item = item.contents.get(RecentMediaItem.self) else { continue @@ -1807,7 +1808,6 @@ public extension EmojiPagerContentComponent { tintMode: tintMode ) - let groupId = "recent" if let groupIndex = itemGroupIndexById[groupId] { itemGroups[groupIndex].items.append(resultItem) } else { diff --git a/submodules/TelegramUI/Components/LegacyCamera/Sources/LegacyCamera.swift b/submodules/TelegramUI/Components/LegacyCamera/Sources/LegacyCamera.swift index a9c14e92e52..17eccfc3ea3 100644 --- a/submodules/TelegramUI/Components/LegacyCamera/Sources/LegacyCamera.swift +++ b/submodules/TelegramUI/Components/LegacyCamera/Sources/LegacyCamera.swift @@ -10,7 +10,7 @@ import ShareController import LegacyUI import LegacyMediaPickerUI -public func presentedLegacyCamera(context: AccountContext, peer: Peer, chatLocation: ChatLocation, cameraView: TGAttachmentCameraView?, menuController: TGMenuSheetController?, parentController: ViewController, attachmentController: ViewController? = nil, editingMedia: Bool, saveCapturedPhotos: Bool, mediaGrouping: Bool, initialCaption: NSAttributedString, hasSchedule: Bool, enablePhoto: Bool, enableVideo: Bool, sendMessagesWithSignals: @escaping ([Any]?, Bool, Int32) -> Void, recognizedQRCode: @escaping (String) -> Void = { _ in }, presentSchedulePicker: @escaping (Bool, @escaping (Int32) -> Void) -> Void, presentTimerPicker: @escaping (@escaping (Int32) -> Void) -> Void, getCaptionPanelView: @escaping () -> TGCaptionPanelView?, dismissedWithResult: @escaping () -> Void = {}, finishedTransitionIn: @escaping () -> Void = {}) { +public func presentedLegacyCamera(context: AccountContext, peer: Peer?, chatLocation: ChatLocation, cameraView: TGAttachmentCameraView?, menuController: TGMenuSheetController?, parentController: ViewController, attachmentController: ViewController? = nil, editingMedia: Bool, saveCapturedPhotos: Bool, mediaGrouping: Bool, initialCaption: NSAttributedString, hasSchedule: Bool, enablePhoto: Bool, enableVideo: Bool, sendMessagesWithSignals: @escaping ([Any]?, Bool, Int32) -> Void, recognizedQRCode: @escaping (String) -> Void = { _ in }, presentSchedulePicker: @escaping (Bool, @escaping (Int32) -> Void) -> Void, presentTimerPicker: @escaping (@escaping (Int32) -> Void) -> Void, getCaptionPanelView: @escaping () -> TGCaptionPanelView?, dismissedWithResult: @escaping () -> Void = {}, finishedTransitionIn: @escaping () -> Void = {}) { let presentationData = context.sharedContext.currentPresentationData.with { $0 } let legacyController = LegacyController(presentation: .custom, theme: presentationData.theme) legacyController.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .portrait, compactSize: .portrait) @@ -18,7 +18,7 @@ public func presentedLegacyCamera(context: AccountContext, peer: Peer, chatLocat legacyController.deferScreenEdgeGestures = [.top] - let isSecretChat = peer.id.namespace == Namespaces.Peer.SecretChat + let isSecretChat = peer?.id.namespace == Namespaces.Peer.SecretChat let controller: TGCameraController if let cameraView = cameraView, let previewView = cameraView.previewView() { @@ -87,15 +87,18 @@ public func presentedLegacyCamera(context: AccountContext, peer: Peer, chatLocat controller.allowCaptionEntities = true controller.allowGrouping = mediaGrouping controller.inhibitDocumentCaptions = false - controller.recipientName = EnginePeer(peer).displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) - if peer.id != context.account.peerId { - if peer is TelegramUser { - controller.hasTimer = hasSchedule + + if let peer { + controller.recipientName = EnginePeer(peer).displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) + if peer.id != context.account.peerId { + if peer is TelegramUser { + controller.hasTimer = hasSchedule + } + controller.hasSilentPosting = true } - controller.hasSilentPosting = true } controller.hasSchedule = hasSchedule - controller.reminder = peer.id == context.account.peerId + controller.reminder = peer?.id == context.account.peerId let screenSize = parentController.view.bounds.size var startFrame = CGRect(x: 0, y: screenSize.height, width: screenSize.width, height: screenSize.height) diff --git a/submodules/TelegramUI/Components/LegacyMessageInputPanel/Sources/LegacyMessageInputPanel.swift b/submodules/TelegramUI/Components/LegacyMessageInputPanel/Sources/LegacyMessageInputPanel.swift index 2df3092fd4a..2472f96dfca 100644 --- a/submodules/TelegramUI/Components/LegacyMessageInputPanel/Sources/LegacyMessageInputPanel.swift +++ b/submodules/TelegramUI/Components/LegacyMessageInputPanel/Sources/LegacyMessageInputPanel.swift @@ -57,7 +57,7 @@ public class LegacyMessageInputPanelNode: ASDisplayNode, TGCaptionPanelView { super.init() - self.state._updated = { [weak self] transition in + self.state._updated = { [weak self] transition, _ in if let self { self.update(transition: transition.containedViewLayoutTransition) } diff --git a/submodules/TelegramUI/Components/ListActionItemComponent/Sources/ListActionItemComponent.swift b/submodules/TelegramUI/Components/ListActionItemComponent/Sources/ListActionItemComponent.swift index b6c169f3886..0b0fe45934f 100644 --- a/submodules/TelegramUI/Components/ListActionItemComponent/Sources/ListActionItemComponent.swift +++ b/submodules/TelegramUI/Components/ListActionItemComponent/Sources/ListActionItemComponent.swift @@ -7,28 +7,84 @@ import ListSectionComponent import SwitchNode public final class ListActionItemComponent: Component { + public enum ToggleStyle { + case regular + case icons + } + + public struct Toggle: Equatable { + public var style: ToggleStyle + public var isOn: Bool + public var isInteractive: Bool + public var action: ((Bool) -> Void)? + + public init(style: ToggleStyle, isOn: Bool, isInteractive: Bool = true, action: ((Bool) -> Void)? = nil) { + self.style = style + self.isOn = isOn + self.isInteractive = isInteractive + self.action = action + } + + public static func ==(lhs: Toggle, rhs: Toggle) -> Bool { + if lhs.style != rhs.style { + return false + } + if lhs.isOn != rhs.isOn { + return false + } + if lhs.isInteractive != rhs.isInteractive { + return false + } + if (lhs.action == nil) != (rhs.action == nil) { + return false + } + return true + } + } + public enum Accessory: Equatable { case arrow - case toggle(Bool) + case toggle(Toggle) + case activity + } + + public enum IconInsets: Equatable { + case `default` + case custom(UIEdgeInsets) + } + + public struct Icon: Equatable { + public var component: AnyComponentWithIdentity + public var insets: IconInsets + public var allowUserInteraction: Bool + + public init(component: AnyComponentWithIdentity, insets: IconInsets = .default, allowUserInteraction: Bool = false) { + self.component = component + self.insets = insets + self.allowUserInteraction = allowUserInteraction + } } public let theme: PresentationTheme public let title: AnyComponent + public let contentInsets: UIEdgeInsets public let leftIcon: AnyComponentWithIdentity? - public let icon: AnyComponentWithIdentity? + public let icon: Icon? public let accessory: Accessory? public let action: ((UIView) -> Void)? public init( theme: PresentationTheme, title: AnyComponent, + contentInsets: UIEdgeInsets = UIEdgeInsets(top: 12.0, left: 0.0, bottom: 12.0, right: 0.0), leftIcon: AnyComponentWithIdentity? = nil, - icon: AnyComponentWithIdentity? = nil, + icon: Icon? = nil, accessory: Accessory? = .arrow, action: ((UIView) -> Void)? ) { self.theme = theme self.title = title + self.contentInsets = contentInsets self.leftIcon = leftIcon self.icon = icon self.accessory = accessory @@ -42,6 +98,9 @@ public final class ListActionItemComponent: Component { if lhs.title != rhs.title { return false } + if lhs.contentInsets != rhs.contentInsets { + return false + } if lhs.leftIcon != rhs.leftIcon { return false } @@ -63,7 +122,9 @@ public final class ListActionItemComponent: Component { private var icon: ComponentView? private var arrowView: UIImageView? - private var switchNode: IconSwitchNode? + private var switchNode: SwitchNode? + private var iconSwitchNode: IconSwitchNode? + private var activityIndicatorView: UIActivityIndicatorView? private var component: ListActionItemComponent? @@ -83,7 +144,10 @@ public final class ListActionItemComponent: Component { self.addTarget(self, action: #selector(self.pressed), for: .touchUpInside) self.internalHighligthedChanged = { [weak self] isHighlighted in - guard let self else { + guard let self, let component = self.component, component.action != nil else { + return + } + if case .toggle = component.accessory, component.action == nil { return } if let customUpdateIsHighlighted = self.customUpdateIsHighlighted { @@ -97,32 +161,44 @@ public final class ListActionItemComponent: Component { } @objc private func pressed() { - self.component?.action?(self) + guard let component, let action = component.action else { + return + } + action(self) + } + + override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + guard let result = super.hitTest(point, with: event) else { + return nil + } + return result } func update(component: ListActionItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { let previousComponent = self.component self.component = component - self.isEnabled = component.action != nil - let themeUpdated = component.theme !== previousComponent?.theme - let verticalInset: CGFloat = 12.0 - var contentLeftInset: CGFloat = 16.0 let contentRightInset: CGFloat switch component.accessory { case .none: - contentRightInset = 16.0 + if let _ = component.icon { + contentRightInset = 42.0 + } else { + contentRightInset = 16.0 + } case .arrow: contentRightInset = 30.0 case .toggle: - contentRightInset = 42.0 + contentRightInset = 76.0 + case .activity: + contentRightInset = 76.0 } var contentHeight: CGFloat = 0.0 - contentHeight += verticalInset + contentHeight += component.contentInsets.top if component.leftIcon != nil { contentLeftInset += 46.0 @@ -134,7 +210,7 @@ public final class ListActionItemComponent: Component { environment: {}, containerSize: CGSize(width: availableSize.width - contentLeftInset - contentRightInset, height: availableSize.height) ) - let titleFrame = CGRect(origin: CGPoint(x: contentLeftInset, y: verticalInset), size: titleSize) + let titleFrame = CGRect(origin: CGPoint(x: contentLeftInset, y: contentHeight), size: titleSize) if let titleView = self.title.view { if titleView.superview == nil { titleView.isUserInteractionEnabled = false @@ -144,10 +220,10 @@ public final class ListActionItemComponent: Component { } contentHeight += titleSize.height - contentHeight += verticalInset + contentHeight += component.contentInsets.bottom if let iconValue = component.icon { - if previousComponent?.icon?.id != iconValue.id, let icon = self.icon { + if previousComponent?.icon?.component.id != iconValue.component.id, let icon = self.icon { self.icon = nil if let iconView = icon.view { transition.setAlpha(view: iconView, alpha: 0.0, completion: { [weak iconView] _ in @@ -168,17 +244,23 @@ public final class ListActionItemComponent: Component { let iconSize = icon.update( transition: iconTransition, - component: iconValue.component, + component: iconValue.component.component, environment: {}, containerSize: CGSize(width: availableSize.width, height: availableSize.height) ) - let iconFrame = CGRect(origin: CGPoint(x: availableSize.width - contentRightInset - iconSize.width, y: floor((contentHeight - iconSize.height) * 0.5)), size: iconSize) + + var iconOffset: CGFloat = 0.0 + if case .none = component.accessory { + iconOffset = 26.0 + } + + let iconFrame = CGRect(origin: CGPoint(x: availableSize.width - contentRightInset - iconSize.width + iconOffset, y: floor((contentHeight - iconSize.height) * 0.5)), size: iconSize) if let iconView = icon.view { if iconView.superview == nil { - iconView.isUserInteractionEnabled = false self.addSubview(iconView) transition.animateAlpha(view: iconView, from: 0.0, to: 1.0) } + iconView.isUserInteractionEnabled = iconValue.allowUserInteraction iconTransition.setFrame(view: iconView, frame: iconFrame) } } else { @@ -243,15 +325,16 @@ public final class ListActionItemComponent: Component { var arrowTransition = transition if let current = self.arrowView { arrowView = current + if themeUpdated { + arrowView.image = PresentationResourcesItemList.disclosureArrowImage(component.theme) + } } else { arrowTransition = arrowTransition.withAnimation(.none) - arrowView = UIImageView(image: PresentationResourcesItemList.disclosureArrowImage(component.theme)?.withRenderingMode(.alwaysTemplate)) + arrowView = UIImageView(image: PresentationResourcesItemList.disclosureArrowImage(component.theme)) self.arrowView = arrowView self.addSubview(arrowView) } - - arrowView.tintColor = component.theme.list.disclosureArrowColor - + if let image = arrowView.image { let arrowFrame = CGRect(origin: CGPoint(x: availableSize.width - 7.0 - image.size.width, y: floor((contentHeight - image.size.height) * 0.5)), size: image.size) arrowTransition.setFrame(view: arrowView, frame: arrowFrame) @@ -263,32 +346,85 @@ public final class ListActionItemComponent: Component { } } - if case let .toggle(isOn) = component.accessory { - let switchNode: IconSwitchNode - var switchTransition = transition - var updateSwitchTheme = themeUpdated - if let current = self.switchNode { - switchNode = current - switchNode.setOn(isOn, animated: !transition.animation.isImmediate) - } else { - switchTransition = switchTransition.withAnimation(.none) - updateSwitchTheme = true - switchNode = IconSwitchNode() - switchNode.setOn(isOn, animated: false) - self.addSubview(switchNode.view) - } - - if updateSwitchTheme { - switchNode.frameColor = component.theme.list.itemSwitchColors.frameColor - switchNode.contentColor = component.theme.list.itemSwitchColors.contentColor - switchNode.handleColor = component.theme.list.itemSwitchColors.handleColor - switchNode.positiveContentColor = component.theme.list.itemSwitchColors.positiveColor - switchNode.negativeContentColor = component.theme.list.itemSwitchColors.negativeColor + if case let .toggle(toggle) = component.accessory { + switch toggle.style { + case .regular: + let switchNode: SwitchNode + var switchTransition = transition + var updateSwitchTheme = themeUpdated + if let current = self.switchNode { + switchNode = current + switchNode.setOn(toggle.isOn, animated: !transition.animation.isImmediate) + } else { + switchTransition = switchTransition.withAnimation(.none) + updateSwitchTheme = true + switchNode = SwitchNode() + switchNode.setOn(toggle.isOn, animated: false) + self.switchNode = switchNode + self.addSubview(switchNode.view) + + switchNode.valueUpdated = { [weak self] value in + guard let self, let component = self.component else { + return + } + if case let .toggle(toggle) = component.accessory, let action = toggle.action { + action(value) + } else { + component.action?(self) + } + } + } + switchNode.isUserInteractionEnabled = toggle.isInteractive + + if updateSwitchTheme { + switchNode.frameColor = component.theme.list.itemSwitchColors.frameColor + switchNode.contentColor = component.theme.list.itemSwitchColors.contentColor + switchNode.handleColor = component.theme.list.itemSwitchColors.handleColor + } + + let switchSize = CGSize(width: 51.0, height: 31.0) + let switchFrame = CGRect(origin: CGPoint(x: availableSize.width - 16.0 - switchSize.width, y: floor((min(60.0, contentHeight) - switchSize.height) * 0.5)), size: switchSize) + switchTransition.setFrame(view: switchNode.view, frame: switchFrame) + case .icons: + let switchNode: IconSwitchNode + var switchTransition = transition + var updateSwitchTheme = themeUpdated + if let current = self.iconSwitchNode { + switchNode = current + switchNode.setOn(toggle.isOn, animated: !transition.animation.isImmediate) + } else { + switchTransition = switchTransition.withAnimation(.none) + updateSwitchTheme = true + switchNode = IconSwitchNode() + switchNode.setOn(toggle.isOn, animated: false) + self.iconSwitchNode = switchNode + self.addSubview(switchNode.view) + + switchNode.valueUpdated = { [weak self] value in + guard let self, let component = self.component else { + return + } + if case let .toggle(toggle) = component.accessory, let action = toggle.action { + action(value) + } else { + component.action?(self) + } + } + } + switchNode.isUserInteractionEnabled = toggle.isInteractive + + if updateSwitchTheme { + switchNode.frameColor = component.theme.list.itemSwitchColors.frameColor + switchNode.contentColor = component.theme.list.itemSwitchColors.contentColor + switchNode.handleColor = component.theme.list.itemSwitchColors.handleColor + switchNode.positiveContentColor = component.theme.list.itemSwitchColors.positiveColor + switchNode.negativeContentColor = component.theme.list.itemSwitchColors.negativeColor + } + + let switchSize = CGSize(width: 51.0, height: 31.0) + let switchFrame = CGRect(origin: CGPoint(x: availableSize.width - 16.0 - switchSize.width, y: floor((min(60.0, contentHeight) - switchSize.height) * 0.5)), size: switchSize) + switchTransition.setFrame(view: switchNode.view, frame: switchFrame) } - - let switchSize = CGSize(width: 51.0, height: 31.0) - let switchFrame = CGRect(origin: CGPoint(x: availableSize.width - 16.0 - switchSize.width, y: floor((min(60.0, contentHeight) - switchSize.height) * 0.5)), size: switchSize) - switchTransition.setFrame(view: switchNode.view, frame: switchFrame) } else { if let switchNode = self.switchNode { self.switchNode = nil @@ -296,6 +432,40 @@ public final class ListActionItemComponent: Component { } } + if case .activity = component.accessory { + let activityIndicatorView: UIActivityIndicatorView + var activityIndicatorTransition = transition + if let current = self.activityIndicatorView { + activityIndicatorView = current + } else { + activityIndicatorTransition = activityIndicatorTransition.withAnimation(.none) + if #available(iOS 13.0, *) { + activityIndicatorView = UIActivityIndicatorView(style: .medium) + } else { + activityIndicatorView = UIActivityIndicatorView(style: .gray) + } + self.activityIndicatorView = activityIndicatorView + self.addSubview(activityIndicatorView) + activityIndicatorView.sizeToFit() + } + + let activityIndicatorSize = activityIndicatorView.bounds.size + let activityIndicatorFrame = CGRect(origin: CGPoint(x: availableSize.width - 16.0 - activityIndicatorSize.width, y: floor((min(60.0, contentHeight) - activityIndicatorSize.height) * 0.5)), size: activityIndicatorSize) + + activityIndicatorView.tintColor = component.theme.list.itemPrimaryTextColor.withMultipliedAlpha(0.5) + + activityIndicatorTransition.setFrame(view: activityIndicatorView, frame: activityIndicatorFrame) + + if !activityIndicatorView.isAnimating { + activityIndicatorView.startAnimating() + } + } else { + if let activityIndicatorView = self.activityIndicatorView { + self.activityIndicatorView = nil + activityIndicatorView.removeFromSuperview() + } + } + self.separatorInset = contentLeftInset return CGSize(width: availableSize.width, height: contentHeight) diff --git a/submodules/TelegramUI/Components/ListItemSliderSelectorComponent/BUILD b/submodules/TelegramUI/Components/ListItemSliderSelectorComponent/BUILD new file mode 100644 index 00000000000..9e20c794e64 --- /dev/null +++ b/submodules/TelegramUI/Components/ListItemSliderSelectorComponent/BUILD @@ -0,0 +1,23 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "ListItemSliderSelectorComponent", + module_name = "ListItemSliderSelectorComponent", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/Display", + "//submodules/TelegramPresentationData", + "//submodules/ComponentFlow", + "//submodules/Components/MultilineTextComponent", + "//submodules/TelegramUI/Components/SliderComponent", + "//submodules/TelegramUI/Components/ListSectionComponent", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Components/ListItemSliderSelectorComponent/Sources/ListItemSliderSelectorComponent.swift b/submodules/TelegramUI/Components/ListItemSliderSelectorComponent/Sources/ListItemSliderSelectorComponent.swift new file mode 100644 index 00000000000..80b67d862a0 --- /dev/null +++ b/submodules/TelegramUI/Components/ListItemSliderSelectorComponent/Sources/ListItemSliderSelectorComponent.swift @@ -0,0 +1,151 @@ +import Foundation +import UIKit +import Display +import AsyncDisplayKit +import TelegramPresentationData +import ComponentFlow +import MultilineTextComponent +import ListSectionComponent +import SliderComponent + +public final class ListItemSliderSelectorComponent: Component { + public let theme: PresentationTheme + public let values: [String] + public let selectedIndex: Int + public let selectedIndexUpdated: (Int) -> Void + + public init( + theme: PresentationTheme, + values: [String], + selectedIndex: Int, + selectedIndexUpdated: @escaping (Int) -> Void + ) { + self.theme = theme + self.values = values + self.selectedIndex = selectedIndex + self.selectedIndexUpdated = selectedIndexUpdated + } + + public static func ==(lhs: ListItemSliderSelectorComponent, rhs: ListItemSliderSelectorComponent) -> Bool { + if lhs.theme !== rhs.theme { + return false + } + if lhs.values != rhs.values { + return false + } + if lhs.selectedIndex != rhs.selectedIndex { + return false + } + return true + } + + public final class View: UIView, ListSectionComponent.ChildView { + private var titles: [ComponentView] = [] + private var slider = ComponentView() + + private var component: ListItemSliderSelectorComponent? + private weak var state: EmptyComponentState? + + public var customUpdateIsHighlighted: ((Bool) -> Void)? + public private(set) var separatorInset: CGFloat = 0.0 + + override public init(frame: CGRect) { + super.init(frame: frame) + } + + required public init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func update(component: ListItemSliderSelectorComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + self.component = component + self.state = state + + let sideInset: CGFloat = 13.0 + let titleSideInset: CGFloat = 20.0 + let titleClippingSideInset: CGFloat = 14.0 + + let titleAreaWidth: CGFloat = availableSize.width - titleSideInset * 2.0 + + for i in 0 ..< component.values.count { + var titleTransition = transition + let title: ComponentView + if self.titles.count > i { + title = self.titles[i] + } else { + titleTransition = titleTransition.withAnimation(.none) + title = ComponentView() + self.titles.append(title) + } + let titleSize = title.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: component.values[i], font: Font.regular(13.0), textColor: component.theme.list.itemSecondaryTextColor)) + )), + environment: {}, + containerSize: CGSize(width: 100.0, height: 100.0) + ) + var titleFrame = CGRect(origin: CGPoint(x: titleSideInset - floor(titleSize.width * 0.5), y: 14.0), size: titleSize) + if component.values.count > 1 { + titleFrame.origin.x += floor(CGFloat(i) / CGFloat(component.values.count - 1) * titleAreaWidth) + } + if titleFrame.minX < titleClippingSideInset { + titleFrame.origin.x = titleSideInset + } + if titleFrame.maxX > availableSize.width - titleClippingSideInset { + titleFrame.origin.x = availableSize.width - titleClippingSideInset - titleSize.width + } + if let titleView = title.view { + if titleView.superview == nil { + self.addSubview(titleView) + } + titleView.bounds = CGRect(origin: CGPoint(), size: titleFrame.size) + titleTransition.setPosition(view: titleView, position: titleFrame.center) + } + } + if self.titles.count > component.values.count { + for i in component.values.count ..< self.titles.count { + self.titles[i].view?.removeFromSuperview() + } + self.titles.removeLast(self.titles.count - component.values.count) + } + + let sliderSize = self.slider.update( + transition: transition, + component: AnyComponent(SliderComponent( + valueCount: component.values.count, + value: component.selectedIndex, + trackBackgroundColor: component.theme.list.controlSecondaryColor, + trackForegroundColor: component.theme.list.itemAccentColor, + valueUpdated: { [weak self] value in + guard let self, let component = self.component else { + return + } + component.selectedIndexUpdated(value) + } + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 100.0) + ) + let sliderFrame = CGRect(origin: CGPoint(x: sideInset, y: 36.0), size: sliderSize) + if let sliderView = self.slider.view { + if sliderView.superview == nil { + self.addSubview(sliderView) + } + transition.setFrame(view: sliderView, frame: sliderFrame) + } + + self.separatorInset = 16.0 + + return CGSize(width: availableSize.width, height: 88.0) + } + } + + public func makeView() -> View { + return View(frame: CGRect()) + } + + public func update(view: View, availableSize: CGSize, state: State, environment: Environment, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} diff --git a/submodules/TelegramUI/Components/ListItemSwipeOptionContainer/BUILD b/submodules/TelegramUI/Components/ListItemSwipeOptionContainer/BUILD new file mode 100644 index 00000000000..c5634af4055 --- /dev/null +++ b/submodules/TelegramUI/Components/ListItemSwipeOptionContainer/BUILD @@ -0,0 +1,21 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "ListItemSwipeOptionContainer", + module_name = "ListItemSwipeOptionContainer", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/Display", + "//submodules/ComponentFlow", + "//submodules/Components/MultilineTextComponent", + "//submodules/Components/ComponentDisplayAdapters", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Components/ListItemSwipeOptionContainer/Sources/ListItemSwipeOptionContainer.swift b/submodules/TelegramUI/Components/ListItemSwipeOptionContainer/Sources/ListItemSwipeOptionContainer.swift new file mode 100644 index 00000000000..eeec875a32a --- /dev/null +++ b/submodules/TelegramUI/Components/ListItemSwipeOptionContainer/Sources/ListItemSwipeOptionContainer.swift @@ -0,0 +1,905 @@ +import Foundation +import UIKit +import Display +import ComponentFlow +import ComponentDisplayAdapters +import AppBundle +import MultilineTextComponent + +private let titleFontWithIcon = Font.medium(13.0) +private let titleFontWithoutIcon = Font.regular(17.0) + +private final class SwipeOptionsGestureRecognizer: UIPanGestureRecognizer { + public var validatedGesture = false + public var firstLocation: CGPoint = CGPoint() + + public var allowAnyDirection = false + public var lastVelocity: CGPoint = CGPoint() + + override public init(target: Any?, action: Selector?) { + super.init(target: target, action: action) + + if #available(iOS 13.4, *) { + self.allowedScrollTypesMask = .continuous + } + + self.maximumNumberOfTouches = 1 + } + + override public func reset() { + super.reset() + + self.validatedGesture = false + } + + public func becomeCancelled() { + self.state = .cancelled + } + + override public func touchesBegan(_ touches: Set, with event: UIEvent) { + super.touchesBegan(touches, with: event) + + let touch = touches.first! + self.firstLocation = touch.location(in: self.view) + } + + override public func touchesMoved(_ touches: Set, with event: UIEvent) { + let location = touches.first!.location(in: self.view) + let translation = CGPoint(x: location.x - self.firstLocation.x, y: location.y - self.firstLocation.y) + + if !self.validatedGesture { + if !self.allowAnyDirection && translation.x > 0.0 { + self.state = .failed + } else if abs(translation.y) > 4.0 && abs(translation.y) > abs(translation.x) * 2.5 { + self.state = .failed + } else if abs(translation.x) > 4.0 && abs(translation.y) * 2.5 < abs(translation.x) { + self.validatedGesture = true + } + } + + if self.validatedGesture { + self.lastVelocity = self.velocity(in: self.view) + super.touchesMoved(touches, with: event) + } + } +} + +open class ListItemSwipeOptionContainer: UIView, UIGestureRecognizerDelegate { + public struct Option: Equatable { + public enum Icon: Equatable { + case none + case image(image: UIImage) + + public static func ==(lhs: Icon, rhs: Icon) -> Bool { + switch lhs { + case .none: + if case .none = rhs { + return true + } else { + return false + } + case let .image(lhsImage): + if case let .image(rhsImage) = rhs, lhsImage == rhsImage { + return true + } else { + return false + } + } + } + } + + public let key: AnyHashable + public let title: String + public let icon: Icon + public let color: UIColor + public let textColor: UIColor + + public init(key: AnyHashable, title: String, icon: Icon, color: UIColor, textColor: UIColor) { + self.key = key + self.title = title + self.icon = icon + self.color = color + self.textColor = textColor + } + + public static func ==(lhs: Option, rhs: Option) -> Bool { + if lhs.key != rhs.key { + return false + } + if lhs.title != rhs.title { + return false + } + if !lhs.color.isEqual(rhs.color) { + return false + } + if !lhs.textColor.isEqual(rhs.textColor) { + return false + } + if lhs.icon != rhs.icon { + return false + } + return true + } + } + + private enum OptionAlignment { + case left + case right + } + + private final class OptionView: UIView { + private let backgroundView: UIView + private let title = ComponentView() + private var iconView: UIImageView? + + private let titleString: String + private let textColor: UIColor + + private var titleSize: CGSize? + + var alignment: OptionAlignment? + var isExpanded: Bool = false + + init(title: String, icon: Option.Icon, color: UIColor, textColor: UIColor) { + self.titleString = title + self.textColor = textColor + + self.backgroundView = UIView() + + switch icon { + case let .image(image): + let iconView = UIImageView() + iconView.image = image.withRenderingMode(.alwaysTemplate) + iconView.tintColor = textColor + self.iconView = iconView + case .none: + self.iconView = nil + } + + super.init(frame: CGRect()) + + self.addSubview(self.backgroundView) + if let iconView = self.iconView { + self.addSubview(iconView) + } + self.backgroundView.backgroundColor = color + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func updateLayout( + isFirst: Bool, + isLeft: Bool, + baseSize: CGSize, + alignment: OptionAlignment, + isExpanded: Bool, + extendedWidth: CGFloat, + sideInset: CGFloat, + transition: Transition, + additive: Bool, + revealFactor: CGFloat, + animateIconMovement: Bool + ) { + var animateAdditive = false + if additive && !transition.animation.isImmediate && self.isExpanded != isExpanded { + animateAdditive = true + } + + let backgroundFrame: CGRect + if isFirst { + backgroundFrame = CGRect(origin: CGPoint(x: isLeft ? -400.0 : 0.0, y: 0.0), size: CGSize(width: extendedWidth + 400.0, height: baseSize.height)) + } else { + backgroundFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: extendedWidth, height: baseSize.height)) + } + let deltaX: CGFloat + if animateAdditive { + let previousFrame = self.backgroundView.frame + self.backgroundView.frame = backgroundFrame + if isLeft { + deltaX = previousFrame.width - backgroundFrame.width + } else { + deltaX = -(previousFrame.width - backgroundFrame.width) + } + if !animateIconMovement { + transition.animatePosition(view: self.backgroundView, from: CGPoint(x: deltaX, y: 0.0), to: CGPoint(), additive: true) + } + } else { + deltaX = 0.0 + transition.setFrame(view: self.backgroundView, frame: backgroundFrame) + } + + self.alignment = alignment + self.isExpanded = isExpanded + let titleSize = self.titleSize ?? CGSize(width: 32.0, height: 10.0) + var contentRect = CGRect(origin: CGPoint(), size: baseSize) + switch alignment { + case .left: + contentRect.origin.x = 0.0 + case .right: + contentRect.origin.x = extendedWidth - contentRect.width + } + + if let iconView = self.iconView, let imageSize = iconView.image?.size { + let iconOffset: CGFloat = -9.0 + let titleIconSpacing: CGFloat = 11.0 + let iconFrame = CGRect(origin: CGPoint(x: contentRect.minX + floor((baseSize.width - imageSize.width + sideInset) / 2.0), y: contentRect.midY - imageSize.height / 2.0 + iconOffset), size: imageSize) + if animateAdditive { + let iconOffsetX = animateIconMovement ? iconView.frame.minX - iconFrame.minX : deltaX + iconView.frame = iconFrame + transition.animatePosition(view: iconView, from: CGPoint(x: iconOffsetX, y: 0.0), to: CGPoint(), additive: true) + } else { + transition.setFrame(view: iconView, frame: iconFrame) + } + + let titleFrame = CGRect(origin: CGPoint(x: contentRect.minX + floor((baseSize.width - titleSize.width + sideInset) / 2.0), y: contentRect.midY + titleIconSpacing), size: titleSize) + if let titleView = self.title.view { + if titleView.superview == nil { + self.addSubview(titleView) + } + if animateAdditive { + let titleOffsetX = animateIconMovement ? titleView.frame.minX - titleFrame.minX : deltaX + titleView.frame = titleFrame + transition.animatePosition(view: titleView, from: CGPoint(x: titleOffsetX, y: 0.0), to: CGPoint(), additive: true) + } else { + transition.setFrame(view: titleView, frame: titleFrame) + } + } + } else { + let titleFrame = CGRect(origin: CGPoint(x: contentRect.minX + floor((baseSize.width - titleSize.width + sideInset) / 2.0), y: contentRect.minY + floor((baseSize.height - titleSize.height) / 2.0)), size: titleSize) + + if let titleView = self.title.view { + if titleView.superview == nil { + self.addSubview(titleView) + } + if animateAdditive { + let titleOffsetX = animateIconMovement ? titleView.frame.minX - titleFrame.minX : deltaX + titleView.frame = titleFrame + transition.animatePosition(view: titleView, from: CGPoint(x: titleOffsetX, y: 0.0), to: CGPoint(), additive: true) + } else { + transition.setFrame(view: titleView, frame: titleFrame) + } + } + } + } + + func calculateSize(_ constrainedSize: CGSize) -> CGSize { + let titleSize = self.title.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: self.titleString, font: self.iconView == nil ? titleFontWithoutIcon : titleFontWithIcon, textColor: self.textColor)) + )), + environment: {}, + containerSize: CGSize(width: 200.0, height: 100.0) + ) + self.titleSize = titleSize + + var maxWidth = titleSize.width + if let iconView = self.iconView, let image = iconView.image { + maxWidth = max(image.size.width, maxWidth) + } + return CGSize(width: max(74.0, maxWidth + 20.0), height: constrainedSize.height) + } + } + + public final class OptionsView: UIView { + private let optionSelected: (Option) -> Void + private let tapticAction: () -> Void + + private var options: [Option] = [] + private var isLeft: Bool = false + + private var optionViews: [OptionView] = [] + private var revealOffset: CGFloat = 0.0 + private var sideInset: CGFloat = 0.0 + + public init(optionSelected: @escaping (Option) -> Void, tapticAction: @escaping () -> Void) { + self.optionSelected = optionSelected + self.tapticAction = tapticAction + + super.init(frame: CGRect()) + + let gestureRecognizer = TapLongTapOrDoubleTapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:))) + gestureRecognizer.tapActionAtPoint = { _ in + return .waitForSingleTap + } + self.addGestureRecognizer(gestureRecognizer) + } + + required public init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + public func setOptions(_ options: [Option], isLeft: Bool) { + if self.options != options || self.isLeft != isLeft { + self.options = options + self.isLeft = isLeft + for optionView in self.optionViews { + optionView.removeFromSuperview() + } + self.optionViews = options.map { option in + return OptionView(title: option.title, icon: option.icon, color: option.color, textColor: option.textColor) + } + if isLeft { + for optionView in self.optionViews.reversed() { + self.addSubview(optionView) + } + } else { + for optionView in self.optionViews { + self.addSubview(optionView) + } + } + } + } + + func calculateSize(_ constrainedSize: CGSize) -> CGSize { + var maxWidth: CGFloat = 0.0 + for optionView in self.optionViews { + let nodeSize = optionView.calculateSize(constrainedSize) + maxWidth = max(nodeSize.width, maxWidth) + } + return CGSize(width: maxWidth * CGFloat(self.optionViews.count), height: constrainedSize.height) + } + + public func updateRevealOffset(offset: CGFloat, sideInset: CGFloat, transition: Transition) { + self.revealOffset = offset + self.sideInset = sideInset + self.updateNodesLayout(transition: transition) + } + + private func updateNodesLayout(transition: Transition) { + let size = self.bounds.size + if size.width.isLessThanOrEqualTo(0.0) || self.optionViews.isEmpty { + return + } + let basicNodeWidth = floor((size.width - abs(self.sideInset)) / CGFloat(self.optionViews.count)) + let lastNodeWidth = size.width - basicNodeWidth * CGFloat(self.optionViews.count - 1) + let revealFactor = self.revealOffset / size.width + let boundaryRevealFactor: CGFloat + if self.optionViews.count > 2 { + boundaryRevealFactor = 1.0 + 16.0 / size.width + } else { + boundaryRevealFactor = 1.0 + basicNodeWidth / size.width + } + let startingOffset: CGFloat + if self.isLeft { + startingOffset = size.width + max(0.0, abs(revealFactor) - 1.0) * size.width + } else { + startingOffset = 0.0 + } + + var completionCount = self.optionViews.count + let intermediateCompletion = { + } + + var i = self.isLeft ? (self.optionViews.count - 1) : 0 + while i >= 0 && i < self.optionViews.count { + let optionView = self.optionViews[i] + let nodeWidth = i == (self.optionViews.count - 1) ? lastNodeWidth : basicNodeWidth + var nodeTransition = transition + var isExpanded = false + if (self.isLeft && i == 0) || (!self.isLeft && i == self.optionViews.count - 1) { + if abs(revealFactor) > boundaryRevealFactor { + isExpanded = true + } + } + if let _ = optionView.alignment, optionView.isExpanded != isExpanded { + nodeTransition = !transition.animation.isImmediate ? transition : .easeInOut(duration: 0.2) + if transition.animation.isImmediate { + self.tapticAction() + } + } + + var sideInset: CGFloat = 0.0 + if i == self.optionViews.count - 1 { + sideInset = self.sideInset + } + + let extendedWidth: CGFloat + let nodeLeftOffset: CGFloat + if isExpanded { + nodeLeftOffset = 0.0 + extendedWidth = size.width * max(1.0, abs(revealFactor)) + } else if self.isLeft { + let offset = basicNodeWidth * CGFloat(self.optionViews.count - 1 - i) + extendedWidth = (size.width - offset) * max(1.0, abs(revealFactor)) + nodeLeftOffset = startingOffset - extendedWidth - floorToScreenPixels(offset * abs(revealFactor)) + } else { + let offset = basicNodeWidth * CGFloat(i) + extendedWidth = (size.width - offset) * max(1.0, abs(revealFactor)) + nodeLeftOffset = startingOffset + floorToScreenPixels(offset * abs(revealFactor)) + } + + transition.setFrame(view: optionView, frame: CGRect(origin: CGPoint(x: nodeLeftOffset, y: 0.0), size: CGSize(width: extendedWidth, height: size.height)), completion: { _ in + completionCount -= 1 + intermediateCompletion() + }) + + var nodeAlignment: OptionAlignment + if (self.optionViews.count > 1) { + nodeAlignment = self.isLeft ? .right : .left + } else { + if self.isLeft { + nodeAlignment = isExpanded ? .right : .left + } else { + nodeAlignment = isExpanded ? .left : .right + } + } + let animateIconMovement = self.optionViews.count == 1 + optionView.updateLayout(isFirst: (self.isLeft && i == 0) || (!self.isLeft && i == self.optionViews.count - 1), isLeft: self.isLeft, baseSize: CGSize(width: nodeWidth, height: size.height), alignment: nodeAlignment, isExpanded: isExpanded, extendedWidth: extendedWidth, sideInset: sideInset, transition: nodeTransition, additive: transition.animation.isImmediate, revealFactor: revealFactor, animateIconMovement: animateIconMovement) + + if self.isLeft { + i -= 1 + } else { + i += 1 + } + } + } + + @objc private func tapGesture(_ recognizer: TapLongTapOrDoubleTapGestureRecognizer) { + if case .ended = recognizer.state, let gesture = recognizer.lastRecognizedGestureAndLocation?.0, case .tap = gesture { + let location = recognizer.location(in: self) + var selectedOption: Int? + + var i = self.isLeft ? 0 : (self.optionViews.count - 1) + while i >= 0 && i < self.optionViews.count { + if self.optionViews[i].frame.contains(location) { + selectedOption = i + break + } + if self.isLeft { + i += 1 + } else { + i -= 1 + } + } + if let selectedOption { + self.optionSelected(self.options[selectedOption]) + } + } + } + + public func isDisplayingExtendedAction() -> Bool { + return self.optionViews.contains(where: { $0.isExpanded }) + } + } + + private var validLayout: (size: CGSize, leftInset: CGFloat, reftInset: CGFloat)? + + private var leftRevealView: OptionsView? + private var rightRevealView: OptionsView? + private var revealOptions: (left: [Option], right: [Option]) = ([], []) + + private var initialRevealOffset: CGFloat = 0.0 + public private(set) var revealOffset: CGFloat = 0.0 + + private var recognizer: SwipeOptionsGestureRecognizer? + private var tapRecognizer: UITapGestureRecognizer? + private var hapticFeedback: HapticFeedback? + + private var allowAnyDirection: Bool = false + + public var updateRevealOffset: ((CGFloat, Transition) -> Void)? + public var revealOptionsInteractivelyOpened: (() -> Void)? + public var revealOptionsInteractivelyClosed: (() -> Void)? + public var revealOptionSelected: ((Option, Bool) -> Void)? + + open var controlsContainer: UIView { + return self + } + + public var isDisplayingRevealedOptions: Bool { + return !self.revealOffset.isZero + } + + override public init(frame: CGRect) { + super.init(frame: frame) + + let recognizer = SwipeOptionsGestureRecognizer(target: self, action: #selector(self.revealGesture(_:))) + self.recognizer = recognizer + recognizer.delegate = self + recognizer.allowAnyDirection = self.allowAnyDirection + self.addGestureRecognizer(recognizer) + + let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.revealTapGesture(_:))) + self.tapRecognizer = tapRecognizer + tapRecognizer.delegate = self + self.addGestureRecognizer(tapRecognizer) + + self.disablesInteractiveTransitionGestureRecognizer = self.allowAnyDirection + + self.disablesInteractiveTransitionGestureRecognizerNow = { [weak self] in + guard let self else { + return false + } + if !self.revealOffset.isZero { + return true + } + return false + } + } + + required public init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + open func setRevealOptions(_ options: (left: [Option], right: [Option])) { + if self.revealOptions == options { + return + } + let previousOptions = self.revealOptions + let wasEmpty = self.revealOptions.left.isEmpty && self.revealOptions.right.isEmpty + self.revealOptions = options + let isEmpty = options.left.isEmpty && options.right.isEmpty + if options.left.isEmpty { + if let _ = self.leftRevealView { + self.recognizer?.becomeCancelled() + self.updateRevealOffsetInternal(offset: 0.0, transition: .spring(duration: 0.3)) + } + } else if previousOptions.left != options.left { + } + if options.right.isEmpty { + if let _ = self.rightRevealView { + self.recognizer?.becomeCancelled() + self.updateRevealOffsetInternal(offset: 0.0, transition: .spring(duration: 0.3)) + } + } else if previousOptions.right != options.right { + if let _ = self.rightRevealView { + } + } + if wasEmpty != isEmpty { + self.recognizer?.isEnabled = !isEmpty + } + let allowAnyDirection = !options.left.isEmpty || !self.revealOffset.isZero + if allowAnyDirection != self.allowAnyDirection { + self.allowAnyDirection = allowAnyDirection + self.recognizer?.allowAnyDirection = allowAnyDirection + self.disablesInteractiveTransitionGestureRecognizer = allowAnyDirection + } + } + + override open func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { + if let recognizer = self.recognizer, gestureRecognizer == self.tapRecognizer { + return abs(self.revealOffset) > 0.0 && !recognizer.validatedGesture + } else if let recognizer = self.recognizer, gestureRecognizer == self.recognizer, recognizer.numberOfTouches == 0 { + let translation = recognizer.velocity(in: recognizer.view) + if abs(translation.y) > 4.0 && abs(translation.y) > abs(translation.x) * 2.5 { + return false + } + } + return true + } + + open func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { + if let recognizer = self.recognizer, otherGestureRecognizer == recognizer { + return true + } else { + return false + } + } + + open func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldBeRequiredToFailBy otherGestureRecognizer: UIGestureRecognizer) -> Bool { + /*if gestureRecognizer === self.recognizer && otherGestureRecognizer is InteractiveTransitionGestureRecognizer { + return true + }*/ + return false + } + + @objc private func revealTapGesture(_ recognizer: UITapGestureRecognizer) { + if case .ended = recognizer.state { + self.updateRevealOffsetInternal(offset: 0.0, transition: .spring(duration: 0.3)) + self.revealOptionsInteractivelyClosed?() + } + } + + @objc private func revealGesture(_ recognizer: SwipeOptionsGestureRecognizer) { + guard let (size, _, _) = self.validLayout else { + return + } + switch recognizer.state { + case .began: + if let leftRevealView = self.leftRevealView { + let revealSize = leftRevealView.bounds.size + let location = recognizer.location(in: self) + if location.x < revealSize.width { + recognizer.becomeCancelled() + } else { + self.initialRevealOffset = self.revealOffset + } + } else if let rightRevealView = self.rightRevealView { + let revealSize = rightRevealView.bounds.size + let location = recognizer.location(in: self) + if location.x > size.width - revealSize.width { + recognizer.becomeCancelled() + } else { + self.initialRevealOffset = self.revealOffset + } + } else { + if self.revealOptions.left.isEmpty && self.revealOptions.right.isEmpty { + recognizer.becomeCancelled() + } + self.initialRevealOffset = self.revealOffset + } + case .changed: + var translation = recognizer.translation(in: self) + translation.x += self.initialRevealOffset + if self.revealOptions.left.isEmpty { + translation.x = min(0.0, translation.x) + } + if self.leftRevealView == nil && CGFloat(0.0).isLess(than: translation.x) { + self.setupAndAddLeftRevealNode() + self.revealOptionsInteractivelyOpened?() + } else if self.rightRevealView == nil && translation.x.isLess(than: 0.0) { + self.setupAndAddRightRevealNode() + self.revealOptionsInteractivelyOpened?() + } + self.updateRevealOffsetInternal(offset: translation.x, transition: .immediate) + if self.leftRevealView == nil && self.rightRevealView == nil { + self.revealOptionsInteractivelyClosed?() + } + case .ended, .cancelled: + guard let recognizer = self.recognizer else { + break + } + + if let leftRevealView = self.leftRevealView { + let velocity = recognizer.velocity(in: self) + let revealSize = leftRevealView.bounds.size + var reveal = false + if abs(velocity.x) < 100.0 { + if self.initialRevealOffset.isZero && self.revealOffset > 0.0 { + reveal = true + } else if self.revealOffset > revealSize.width { + reveal = true + } else { + reveal = false + } + } else { + if velocity.x > 0.0 { + reveal = true + } else { + reveal = false + } + } + + var selectedOption: Option? + if reveal && leftRevealView.isDisplayingExtendedAction() { + reveal = false + selectedOption = self.revealOptions.left.first + } else { + self.updateRevealOffsetInternal(offset: reveal ? revealSize.width : 0.0, transition: .spring(duration: 0.3)) + } + + if let selectedOption = selectedOption { + self.revealOptionSelected?(selectedOption, true) + } else { + if !reveal { + self.revealOptionsInteractivelyClosed?() + } + } + } else if let rightRevealView = self.rightRevealView { + let velocity = recognizer.velocity(in: self) + let revealSize = rightRevealView.bounds.size + var reveal = false + if abs(velocity.x) < 100.0 { + if self.initialRevealOffset.isZero && self.revealOffset < 0.0 { + reveal = true + } else if self.revealOffset < -revealSize.width { + reveal = true + } else { + reveal = false + } + } else { + if velocity.x < 0.0 { + reveal = true + } else { + reveal = false + } + } + + var selectedOption: Option? + if reveal && rightRevealView.isDisplayingExtendedAction() { + reveal = false + selectedOption = self.revealOptions.right.last + } else { + self.updateRevealOffsetInternal(offset: reveal ? -revealSize.width : 0.0, transition: .spring(duration: 0.3)) + } + + if let selectedOption = selectedOption { + self.revealOptionSelected?(selectedOption, true) + } else { + if !reveal { + self.revealOptionsInteractivelyClosed?() + } + } + } + default: + break + } + } + + private func setupAndAddLeftRevealNode() { + if !self.revealOptions.left.isEmpty { + let revealView = OptionsView(optionSelected: { [weak self] option in + self?.revealOptionSelected?(option, false) + }, tapticAction: { [weak self] in + self?.hapticImpact() + }) + revealView.setOptions(self.revealOptions.left, isLeft: true) + self.leftRevealView = revealView + + if let (size, leftInset, _) = self.validLayout { + var revealSize = revealView.calculateSize(CGSize(width: CGFloat.greatestFiniteMagnitude, height: size.height)) + revealSize.width += leftInset + + revealView.frame = CGRect(origin: CGPoint(x: min(self.revealOffset - revealSize.width, 0.0), y: 0.0), size: revealSize) + revealView.updateRevealOffset(offset: 0.0, sideInset: leftInset, transition: .immediate) + } + + self.controlsContainer.addSubview(revealView) + } + } + + private func setupAndAddRightRevealNode() { + if !self.revealOptions.right.isEmpty { + let revealView = OptionsView(optionSelected: { [weak self] option in + self?.revealOptionSelected?(option, false) + }, tapticAction: { [weak self] in + self?.hapticImpact() + }) + revealView.setOptions(self.revealOptions.right, isLeft: false) + self.rightRevealView = revealView + + if let (size, _, rightInset) = self.validLayout { + var revealSize = revealView.calculateSize(CGSize(width: CGFloat.greatestFiniteMagnitude, height: size.height)) + revealSize.width += rightInset + + revealView.frame = CGRect(origin: CGPoint(x: size.width + max(self.revealOffset, -revealSize.width), y: 0.0), size: revealSize) + revealView.updateRevealOffset(offset: 0.0, sideInset: -rightInset, transition: .immediate) + } + + self.controlsContainer.addSubview(revealView) + } + } + + public func updateLayout(size: CGSize, leftInset: CGFloat, rightInset: CGFloat) { + self.validLayout = (size, leftInset, rightInset) + + if let leftRevealView = self.leftRevealView { + var revealSize = leftRevealView.calculateSize(CGSize(width: CGFloat.greatestFiniteMagnitude, height: size.height)) + revealSize.width += leftInset + leftRevealView.frame = CGRect(origin: CGPoint(x: min(self.revealOffset - revealSize.width, 0.0), y: 0.0), size: revealSize) + } + + if let rightRevealView = self.rightRevealView { + var revealSize = rightRevealView.calculateSize(CGSize(width: CGFloat.greatestFiniteMagnitude, height: size.height)) + revealSize.width += rightInset + rightRevealView.frame = CGRect(origin: CGPoint(x: size.width + max(self.revealOffset, -revealSize.width), y: 0.0), size: revealSize) + } + } + + open func updateRevealOffsetInternal(offset: CGFloat, transition: Transition, completion: (() -> Void)? = nil) { + self.revealOffset = offset + guard let (size, leftInset, rightInset) = self.validLayout else { + return + } + + var leftRevealCompleted = true + var rightRevealCompleted = true + let intermediateCompletion = { + if leftRevealCompleted && rightRevealCompleted { + completion?() + } + } + + if let leftRevealView = self.leftRevealView { + leftRevealCompleted = false + + let revealSize = leftRevealView.bounds.size + + let revealFrame = CGRect(origin: CGPoint(x: min(self.revealOffset - revealSize.width, 0.0), y: 0.0), size: revealSize) + let revealNodeOffset = -self.revealOffset + leftRevealView.updateRevealOffset(offset: revealNodeOffset, sideInset: leftInset, transition: transition) + + if CGFloat(offset).isLessThanOrEqualTo(0.0) { + self.leftRevealView = nil + transition.setFrame(view: leftRevealView, frame: revealFrame, completion: { [weak leftRevealView] _ in + leftRevealView?.removeFromSuperview() + + leftRevealCompleted = true + intermediateCompletion() + }) + } else { + transition.setFrame(view: leftRevealView, frame: revealFrame, completion: { _ in + leftRevealCompleted = true + intermediateCompletion() + }) + } + } + if let rightRevealView = self.rightRevealView { + rightRevealCompleted = false + + let revealSize = rightRevealView.bounds.size + + let revealFrame = CGRect(origin: CGPoint(x: min(size.width, size.width + self.revealOffset), y: 0.0), size: revealSize) + let revealNodeOffset = -self.revealOffset + rightRevealView.updateRevealOffset(offset: revealNodeOffset, sideInset: -rightInset, transition: transition) + + if CGFloat(0.0).isLessThanOrEqualTo(offset) { + self.rightRevealView = nil + transition.setFrame(view: rightRevealView, frame: revealFrame, completion: { [weak rightRevealView] _ in + rightRevealView?.removeFromSuperview() + + rightRevealCompleted = true + intermediateCompletion() + }) + } else { + transition.setFrame(view: rightRevealView, frame: revealFrame, completion: { _ in + rightRevealCompleted = true + intermediateCompletion() + }) + } + } + let allowAnyDirection = !self.revealOptions.left.isEmpty || !offset.isZero + if allowAnyDirection != self.allowAnyDirection { + self.allowAnyDirection = allowAnyDirection + self.recognizer?.allowAnyDirection = allowAnyDirection + self.disablesInteractiveTransitionGestureRecognizer = allowAnyDirection + } + + self.updateRevealOffset?(offset, transition) + } + + open func setRevealOptionsOpened(_ value: Bool, animated: Bool) { + if value != !self.revealOffset.isZero { + if !self.revealOffset.isZero { + self.recognizer?.becomeCancelled() + } + let transition: Transition + if animated { + transition = .spring(duration: 0.3) + } else { + transition = .immediate + } + if value { + if self.rightRevealView == nil { + self.setupAndAddRightRevealNode() + if let rightRevealView = self.rightRevealView, let validLayout = self.validLayout { + let revealSize = rightRevealView.calculateSize(CGSize(width: CGFloat.greatestFiniteMagnitude, height: validLayout.size.height)) + self.updateRevealOffsetInternal(offset: -revealSize.width, transition: transition) + } + } + } else if !self.revealOffset.isZero { + self.updateRevealOffsetInternal(offset: 0.0, transition: transition) + } + } + } + + open func animateRevealOptionsFill(completion: (() -> Void)? = nil) { + if let validLayout = self.validLayout { + self.layer.allowsGroupOpacity = true + self.updateRevealOffsetInternal(offset: -validLayout.0.width - 74.0, transition: .spring(duration: 0.3), completion: { + self.layer.allowsGroupOpacity = false + completion?() + }) + } + } + + open var preventsTouchesToOtherItems: Bool { + return self.isDisplayingRevealedOptions + } + + open func touchesToOtherItemsPrevented() { + if self.isDisplayingRevealedOptions { + self.setRevealOptionsOpened(false, animated: true) + } + } + + private func hapticImpact() { + if self.hapticFeedback == nil { + self.hapticFeedback = HapticFeedback() + } + self.hapticFeedback?.impact(.medium) + } +} diff --git a/submodules/TelegramUI/Components/ListMultilineTextFieldItemComponent/BUILD b/submodules/TelegramUI/Components/ListMultilineTextFieldItemComponent/BUILD new file mode 100644 index 00000000000..cab35652152 --- /dev/null +++ b/submodules/TelegramUI/Components/ListMultilineTextFieldItemComponent/BUILD @@ -0,0 +1,24 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "ListMultilineTextFieldItemComponent", + module_name = "ListMultilineTextFieldItemComponent", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/Display", + "//submodules/ComponentFlow", + "//submodules/TelegramPresentationData", + "//submodules/Components/MultilineTextComponent", + "//submodules/TelegramUI/Components/ListSectionComponent", + "//submodules/TelegramUI/Components/TextFieldComponent", + "//submodules/AccountContext", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Components/ListMultilineTextFieldItemComponent/Sources/ListMultilineTextFieldItemComponent.swift b/submodules/TelegramUI/Components/ListMultilineTextFieldItemComponent/Sources/ListMultilineTextFieldItemComponent.swift new file mode 100644 index 00000000000..d2c964545d2 --- /dev/null +++ b/submodules/TelegramUI/Components/ListMultilineTextFieldItemComponent/Sources/ListMultilineTextFieldItemComponent.swift @@ -0,0 +1,281 @@ +import Foundation +import UIKit +import Display +import ComponentFlow +import TelegramPresentationData +import MultilineTextComponent +import ListSectionComponent +import TextFieldComponent +import AccountContext + +public final class ListMultilineTextFieldItemComponent: Component { + public final class ExternalState { + public fileprivate(set) var hasText: Bool = false + + public init() { + } + } + + public final class ResetText: Equatable { + public let value: String + + public init(value: String) { + self.value = value + } + + public static func ==(lhs: ResetText, rhs: ResetText) -> Bool { + return lhs === rhs + } + } + + public let externalState: ExternalState? + public let context: AccountContext + public let theme: PresentationTheme + public let strings: PresentationStrings + public let initialText: String + public let resetText: ResetText? + public let placeholder: String + public let autocapitalizationType: UITextAutocapitalizationType + public let autocorrectionType: UITextAutocorrectionType + public let characterLimit: Int? + public let allowEmptyLines: Bool + public let updated: ((String) -> Void)? + public let textUpdateTransition: Transition + public let tag: AnyObject? + + public init( + externalState: ExternalState? = nil, + context: AccountContext, + theme: PresentationTheme, + strings: PresentationStrings, + initialText: String, + resetText: ResetText? = nil, + placeholder: String, + autocapitalizationType: UITextAutocapitalizationType = .sentences, + autocorrectionType: UITextAutocorrectionType = .default, + characterLimit: Int? = nil, + allowEmptyLines: Bool = true, + updated: ((String) -> Void)?, + textUpdateTransition: Transition = .immediate, + tag: AnyObject? = nil + ) { + self.externalState = externalState + self.context = context + self.theme = theme + self.strings = strings + self.initialText = initialText + self.resetText = resetText + self.placeholder = placeholder + self.autocapitalizationType = autocapitalizationType + self.autocorrectionType = autocorrectionType + self.characterLimit = characterLimit + self.allowEmptyLines = allowEmptyLines + self.updated = updated + self.textUpdateTransition = textUpdateTransition + self.tag = tag + } + + public static func ==(lhs: ListMultilineTextFieldItemComponent, rhs: ListMultilineTextFieldItemComponent) -> Bool { + if lhs.externalState !== rhs.externalState { + return false + } + if lhs.context !== rhs.context { + return false + } + if lhs.theme !== rhs.theme { + return false + } + if lhs.strings !== rhs.strings { + return false + } + if lhs.initialText != rhs.initialText { + return false + } + if lhs.resetText != rhs.resetText { + return false + } + if lhs.placeholder != rhs.placeholder { + return false + } + if lhs.autocapitalizationType != rhs.autocapitalizationType { + return false + } + if lhs.autocorrectionType != rhs.autocorrectionType { + return false + } + if lhs.characterLimit != rhs.characterLimit { + return false + } + if lhs.allowEmptyLines != rhs.allowEmptyLines { + return false + } + if (lhs.updated == nil) != (rhs.updated == nil) { + return false + } + return true + } + + private final class TextField: UITextField { + var sideInset: CGFloat = 0.0 + + override func textRect(forBounds bounds: CGRect) -> CGRect { + return CGRect(origin: CGPoint(x: self.sideInset, y: 0.0), size: CGSize(width: bounds.width - self.sideInset * 2.0, height: bounds.height)) + } + + override func editingRect(forBounds bounds: CGRect) -> CGRect { + return CGRect(origin: CGPoint(x: self.sideInset, y: 0.0), size: CGSize(width: bounds.width - self.sideInset * 2.0, height: bounds.height)) + } + } + + public final class View: UIView, UITextFieldDelegate, ListSectionComponent.ChildView, ComponentTaggedView { + private let textField = ComponentView() + private let textFieldExternalState = TextFieldComponent.ExternalState() + + private let placeholder = ComponentView() + + private var component: ListMultilineTextFieldItemComponent? + private weak var state: EmptyComponentState? + private var isUpdating: Bool = false + + public var currentText: String { + if let textFieldView = self.textField.view as? TextFieldComponent.View { + return textFieldView.inputState.inputText.string + } else { + return "" + } + } + + public var customUpdateIsHighlighted: ((Bool) -> Void)? + public private(set) var separatorInset: CGFloat = 0.0 + + public override init(frame: CGRect) { + super.init(frame: CGRect()) + } + + required public init?(coder: NSCoder) { + preconditionFailure() + } + + public func textFieldShouldReturn(_ textField: UITextField) -> Bool { + return true + } + + @objc private func textDidChange() { + if !self.isUpdating { + self.state?.updated(transition: self.component?.textUpdateTransition ?? .immediate) + } + self.component?.updated?(self.currentText) + } + + public func setText(text: String, updateState: Bool) { + if let textFieldView = self.textField.view as? TextFieldComponent.View { + //TODO + let _ = textFieldView + } + + if updateState { + self.component?.updated?(self.currentText) + } else { + self.state?.updated(transition: .immediate, isLocal: true) + } + } + + public func matches(tag: Any) -> Bool { + if let component = self.component, let componentTag = component.tag { + let tag = tag as AnyObject + if componentTag === tag { + return true + } + } + return false + } + + func update(component: ListMultilineTextFieldItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + self.isUpdating = true + defer { + self.isUpdating = false + } + + self.component = component + self.state = state + + let verticalInset: CGFloat = 12.0 + let sideInset: CGFloat = 16.0 + + let textFieldSize = self.textField.update( + transition: transition, + component: AnyComponent(TextFieldComponent( + context: component.context, + strings: component.strings, + externalState: self.textFieldExternalState, + fontSize: 17.0, + textColor: component.theme.list.itemPrimaryTextColor, + insets: UIEdgeInsets(top: verticalInset, left: sideInset - 8.0, bottom: verticalInset, right: sideInset - 8.0), + hideKeyboard: false, + customInputView: nil, + resetText: component.resetText.flatMap { resetText in + return NSAttributedString(string: resetText.value, font: Font.regular(17.0), textColor: component.theme.list.itemPrimaryTextColor) + }, + isOneLineWhenUnfocused: false, + characterLimit: component.characterLimit, + allowEmptyLines: component.allowEmptyLines, + formatMenuAvailability: .none, + lockedFormatAction: { + }, + present: { _ in + }, + paste: { _ in + } + )), + environment: {}, + containerSize: availableSize + ) + + let size = CGSize(width: textFieldSize.width, height: textFieldSize.height - 1.0) + let textFieldFrame = CGRect(origin: CGPoint(), size: textFieldSize) + + if let textFieldView = self.textField.view { + if textFieldView.superview == nil { + self.addSubview(textFieldView) + self.textField.parentState = state + } + transition.setFrame(view: textFieldView, frame: textFieldFrame) + } + + let placeholderSize = self.placeholder.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: component.placeholder.isEmpty ? " " : component.placeholder, font: Font.regular(17.0), textColor: component.theme.list.itemPlaceholderTextColor)) + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 100.0) + ) + let placeholderFrame = CGRect(origin: CGPoint(x: sideInset, y: verticalInset), size: placeholderSize) + if let placeholderView = self.placeholder.view { + if placeholderView.superview == nil { + placeholderView.layer.anchorPoint = CGPoint() + placeholderView.isUserInteractionEnabled = false + self.insertSubview(placeholderView, at: 0) + } + transition.setPosition(view: placeholderView, position: placeholderFrame.origin) + placeholderView.bounds = CGRect(origin: CGPoint(), size: placeholderFrame.size) + + placeholderView.isHidden = self.textFieldExternalState.hasText + } + + self.separatorInset = 16.0 + + component.externalState?.hasText = self.textFieldExternalState.hasText + + return size + } + } + + public func makeView() -> View { + return View(frame: CGRect()) + } + + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} diff --git a/submodules/TelegramUI/Components/ListSectionComponent/Sources/ListSectionComponent.swift b/submodules/TelegramUI/Components/ListSectionComponent/Sources/ListSectionComponent.swift index ce6b71a1ef3..714250a551e 100644 --- a/submodules/TelegramUI/Components/ListSectionComponent/Sources/ListSectionComponent.swift +++ b/submodules/TelegramUI/Components/ListSectionComponent/Sources/ListSectionComponent.swift @@ -217,6 +217,7 @@ public final class ListSectionComponent: Component { itemTransition = itemTransition.withAnimation(.none) itemView = ItemView() self.itemViews[itemId] = itemView + itemView.contents.parentState = state } let itemSize = itemView.contents.update( diff --git a/submodules/TelegramUI/Components/ListTextFieldItemComponent/BUILD b/submodules/TelegramUI/Components/ListTextFieldItemComponent/BUILD index c0256f3093c..bbb2f2118f7 100644 --- a/submodules/TelegramUI/Components/ListTextFieldItemComponent/BUILD +++ b/submodules/TelegramUI/Components/ListTextFieldItemComponent/BUILD @@ -14,6 +14,9 @@ swift_library( "//submodules/ComponentFlow", "//submodules/TelegramPresentationData", "//submodules/Components/MultilineTextComponent", + "//submodules/TelegramUI/Components/ListSectionComponent", + "//submodules/TelegramUI/Components/PlainButtonComponent", + "//submodules/Components/BundleIconComponent", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/ListTextFieldItemComponent/Sources/ListTextFieldItemComponent.swift b/submodules/TelegramUI/Components/ListTextFieldItemComponent/Sources/ListTextFieldItemComponent.swift index c91f9b94147..3f6e7c42655 100644 --- a/submodules/TelegramUI/Components/ListTextFieldItemComponent/Sources/ListTextFieldItemComponent.swift +++ b/submodules/TelegramUI/Components/ListTextFieldItemComponent/Sources/ListTextFieldItemComponent.swift @@ -4,23 +4,50 @@ import Display import ComponentFlow import TelegramPresentationData import MultilineTextComponent +import ListSectionComponent +import PlainButtonComponent +import BundleIconComponent public final class ListTextFieldItemComponent: Component { + public final class ResetText: Equatable { + public let value: String + + public init(value: String) { + self.value = value + } + + public static func ==(lhs: ResetText, rhs: ResetText) -> Bool { + return lhs === rhs + } + } + public let theme: PresentationTheme public let initialText: String + public let resetText: ResetText? public let placeholder: String + public let autocapitalizationType: UITextAutocapitalizationType + public let autocorrectionType: UITextAutocorrectionType public let updated: ((String) -> Void)? + public let tag: AnyObject? public init( theme: PresentationTheme, initialText: String, + resetText: ResetText? = nil, placeholder: String, - updated: ((String) -> Void)? + autocapitalizationType: UITextAutocapitalizationType = .sentences, + autocorrectionType: UITextAutocorrectionType = .default, + updated: ((String) -> Void)?, + tag: AnyObject? = nil ) { self.theme = theme self.initialText = initialText + self.resetText = resetText self.placeholder = placeholder + self.autocapitalizationType = autocapitalizationType + self.autocorrectionType = autocorrectionType self.updated = updated + self.tag = tag } public static func ==(lhs: ListTextFieldItemComponent, rhs: ListTextFieldItemComponent) -> Bool { @@ -30,9 +57,18 @@ public final class ListTextFieldItemComponent: Component { if lhs.initialText != rhs.initialText { return false } + if lhs.resetText !== rhs.resetText { + return false + } if lhs.placeholder != rhs.placeholder { return false } + if lhs.autocapitalizationType != rhs.autocapitalizationType { + return false + } + if lhs.autocorrectionType != rhs.autocorrectionType { + return false + } if (lhs.updated == nil) != (rhs.updated == nil) { return false } @@ -51,9 +87,10 @@ public final class ListTextFieldItemComponent: Component { } } - public final class View: UIView, UITextFieldDelegate { + public final class View: UIView, UITextFieldDelegate, ListSectionComponent.ChildView, ComponentTaggedView { private let textField: TextField private let placeholder = ComponentView() + private let clearButton = ComponentView() private var component: ListTextFieldItemComponent? private weak var state: EmptyComponentState? @@ -63,6 +100,9 @@ public final class ListTextFieldItemComponent: Component { return self.textField.text ?? "" } + public var customUpdateIsHighlighted: ((Bool) -> Void)? + public private(set) var separatorInset: CGFloat = 0.0 + public override init(frame: CGRect) { self.textField = TextField() @@ -81,6 +121,27 @@ public final class ListTextFieldItemComponent: Component { if !self.isUpdating { self.state?.updated(transition: .immediate) } + self.component?.updated?(self.currentText) + } + + public func setText(text: String, updateState: Bool) { + self.textField.text = text + if updateState { + self.state?.updated(transition: .immediate, isLocal: true) + self.component?.updated?(self.currentText) + } else { + self.state?.updated(transition: .immediate, isLocal: true) + } + } + + public func matches(tag: Any) -> Bool { + if let component = self.component, let componentTag = component.tag { + let tag = tag as AnyObject + if componentTag === tag { + return true + } + } + return false } func update(component: ListTextFieldItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { @@ -101,6 +162,16 @@ public final class ListTextFieldItemComponent: Component { self.textField.delegate = self self.textField.addTarget(self, action: #selector(self.textDidChange), for: .editingChanged) } + if let resetText = component.resetText, previousComponent?.resetText !== component.resetText { + self.textField.text = resetText.value + } + + if self.textField.autocapitalizationType != component.autocapitalizationType { + self.textField.autocapitalizationType = component.autocapitalizationType + } + if self.textField.autocorrectionType != component.autocorrectionType { + self.textField.autocorrectionType = component.autocorrectionType + } let themeUpdated = component.theme !== previousComponent?.theme @@ -120,7 +191,7 @@ public final class ListTextFieldItemComponent: Component { text: .plain(NSAttributedString(string: component.placeholder.isEmpty ? " " : component.placeholder, font: Font.regular(17.0), textColor: component.theme.list.itemPlaceholderTextColor)) )), environment: {}, - containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 100.0) + containerSize: CGSize(width: availableSize.width - sideInset * 2.0 - 30.0, height: 100.0) ) let contentHeight: CGFloat = placeholderSize.height + verticalInset * 2.0 let placeholderFrame = CGRect(origin: CGPoint(x: sideInset, y: floor((contentHeight - placeholderSize.height) * 0.5)), size: placeholderSize) @@ -138,6 +209,37 @@ public final class ListTextFieldItemComponent: Component { transition.setFrame(view: self.textField, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: availableSize.width, height: contentHeight))) + let clearButtonSize = self.clearButton.update( + transition: transition, + component: AnyComponent(PlainButtonComponent( + content: AnyComponent(BundleIconComponent( + name: "Components/Search Bar/Clear", + tintColor: component.theme.list.itemPrimaryTextColor.withMultipliedAlpha(0.4) + )), + effectAlignment: .center, + minSize: CGSize(width: 44.0, height: 44.0), + action: { [weak self] in + guard let self else { + return + } + self.setText(text: "", updateState: true) + }, + animateAlpha: false, + animateScale: true + )), + environment: {}, + containerSize: CGSize(width: 44.0, height: 44.0) + ) + if let clearButtonView = self.clearButton.view { + if clearButtonView.superview == nil { + self.addSubview(clearButtonView) + } + transition.setFrame(view: clearButtonView, frame: CGRect(origin: CGPoint(x: availableSize.width - 0.0 - clearButtonSize.width, y: floor((contentHeight - clearButtonSize.height) * 0.5)), size: clearButtonSize)) + clearButtonView.isHidden = self.currentText.isEmpty + } + + self.separatorInset = 16.0 + return CGSize(width: availableSize.width, height: contentHeight) } } diff --git a/submodules/TelegramUI/Components/MediaEditor/MetalResources/EditorDefault.metal b/submodules/TelegramUI/Components/MediaEditor/MetalResources/EditorDefault.metal index d03c2f7e8f5..6559709cc87 100644 --- a/submodules/TelegramUI/Components/MediaEditor/MetalResources/EditorDefault.metal +++ b/submodules/TelegramUI/Components/MediaEditor/MetalResources/EditorDefault.metal @@ -25,8 +25,8 @@ vertex RasterizerData defaultVertexShader(uint vertexID [[vertex_id]], fragment half4 defaultFragmentShader(RasterizerData in [[stage_in]], texture2d texture [[texture(0)]]) { constexpr sampler samplr(filter::linear, mag_filter::linear, min_filter::linear); - half3 color = texture.sample(samplr, in.texCoord).rgb; - return half4(color, 1.0); + half4 color = texture.sample(samplr, in.texCoord); + return color; } fragment half histogramPrepareFragmentShader(RasterizerData in [[stage_in]], @@ -39,12 +39,12 @@ fragment half histogramPrepareFragmentShader(RasterizerData in [[stage_in]], } typedef struct { - float3 topColor; - float3 bottomColor; + float4 topColor; + float4 bottomColor; } GradientColors; fragment half4 gradientFragmentShader(RasterizerData in [[stage_in]], constant GradientColors& colors [[buffer(0)]]) { - return half4(half3(mix(colors.topColor, colors.bottomColor, in.texCoord.y)), 1.0); + return half4(half3(mix(colors.topColor.rgb, colors.bottomColor.rgb, in.texCoord.y)), 1.0); } diff --git a/submodules/TelegramUI/Components/MediaEditor/MetalResources/EditorDual.metal b/submodules/TelegramUI/Components/MediaEditor/MetalResources/EditorDual.metal index c45b5ec9ffc..a4fa037c62d 100644 --- a/submodules/TelegramUI/Components/MediaEditor/MetalResources/EditorDual.metal +++ b/submodules/TelegramUI/Components/MediaEditor/MetalResources/EditorDual.metal @@ -3,6 +3,14 @@ using namespace metal; +typedef struct { + float2 dimensions; + float roundness; + float alpha; + float isOpaque; + float empty; +} VideoEncodeParameters; + typedef struct { float4 pos; float2 texCoord; @@ -17,11 +25,10 @@ float sdfRoundedRectangle(float2 uv, float2 position, float2 size, float radius) fragment half4 dualFragmentShader(RasterizerData in [[stage_in]], texture2d texture [[texture(0)]], - constant uint2 &resolution[[buffer(0)]], - constant float &roundness[[buffer(1)]], - constant float &alpha[[buffer(2)]] + texture2d mask [[texture(1)]], + constant VideoEncodeParameters& adjustments [[buffer(0)]] ) { - float2 R = float2(resolution.x, resolution.y); + float2 R = float2(adjustments.dimensions.x, adjustments.dimensions.y); float2 uv = (in.localPos - float2(0.5, 0.5)) * 2.0; if (R.x > R.y) { @@ -33,10 +40,11 @@ fragment half4 dualFragmentShader(RasterizerData in [[stage_in]], constexpr sampler samplr(filter::linear, mag_filter::linear, min_filter::linear); half3 color = texture.sample(samplr, in.texCoord).rgb; + float colorAlpha = min(1.0, adjustments.isOpaque + mask.sample(samplr, in.texCoord).r); - float t = 1.0 / resolution.y; + float t = 1.0 / adjustments.dimensions.y; float side = 1.0 * aspectRatio; - float distance = smoothstep(t, -t, sdfRoundedRectangle(uv, float2(0.0, 0.0), float2(side, mix(1.0, side, roundness)), side * roundness)); + float distance = smoothstep(t, -t, sdfRoundedRectangle(uv, float2(0.0, 0.0), float2(side, mix(1.0, side, adjustments.roundness)), side * adjustments.roundness)); - return mix(half4(color, 0.0), half4(color, 1.0 * alpha), distance); + return mix(half4(color, 0.0), half4(color, colorAlpha * adjustments.alpha), distance); } diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/ImageObjectSeparation.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/ImageObjectSeparation.swift new file mode 100644 index 00000000000..fe2f1100f02 --- /dev/null +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/ImageObjectSeparation.swift @@ -0,0 +1,153 @@ +import Foundation +import UIKit +import Vision +import CoreImage +import CoreImage.CIFilterBuiltins +import SwiftSignalKit +import VideoToolbox + +private let queue = Queue() + +public func cutoutStickerImage(from image: UIImage, onlyCheck: Bool = false) -> Signal { + if #available(iOS 17.0, *) { + guard let cgImage = image.cgImage else { + return .single(nil) + } + return Signal { subscriber in + let ciContext = CIContext(options: nil) + let inputImage = CIImage(cgImage: cgImage) + let handler = VNImageRequestHandler(cgImage: cgImage, options: [:]) + let request = VNGenerateForegroundInstanceMaskRequest { [weak handler] request, error in + guard let handler, let result = request.results?.first as? VNInstanceMaskObservation else { + subscriber.putNext(nil) + subscriber.putCompletion() + return + } + if onlyCheck { + subscriber.putNext(UIImage()) + subscriber.putCompletion() + } else { + let instances = instances(atPoint: nil, inObservation: result) + if let mask = try? result.generateScaledMaskForImage(forInstances: instances, from: handler) { + let filter = CIFilter.blendWithMask() + filter.inputImage = inputImage + filter.backgroundImage = CIImage(color: .clear) + filter.maskImage = CIImage(cvPixelBuffer: mask) + if let output = filter.outputImage, let cgImage = ciContext.createCGImage(output, from: inputImage.extent) { + let image = UIImage(cgImage: cgImage) + subscriber.putNext(image) + subscriber.putCompletion() + return + } + } + subscriber.putNext(nil) + subscriber.putCompletion() + } + } + try? handler.perform([request]) + return ActionDisposable { + request.cancel() + } + } + |> runOn(queue) + } else { + return .single(nil) + } +} + +public enum CutoutResult { + case image(UIImage) + case pixelBuffer(CVPixelBuffer) +} + +public func cutoutImage(from image: UIImage, atPoint point: CGPoint?, asImage: Bool) -> Signal { + if #available(iOS 17.0, *) { + guard let cgImage = image.cgImage else { + return .single(nil) + } + return Signal { subscriber in + let ciContext = CIContext(options: nil) + let inputImage = CIImage(cgImage: cgImage) + let handler = VNImageRequestHandler(cgImage: cgImage, options: [:]) + let request = VNGenerateForegroundInstanceMaskRequest { [weak handler] request, error in + guard let handler, let result = request.results?.first as? VNInstanceMaskObservation else { + subscriber.putNext(nil) + subscriber.putCompletion() + return + } + + let instances = IndexSet(instances(atPoint: point, inObservation: result).prefix(1)) + if let mask = try? result.generateScaledMaskForImage(forInstances: instances, from: handler) { + if asImage { + let filter = CIFilter.blendWithMask() + filter.inputImage = inputImage + filter.backgroundImage = CIImage(color: .clear) + filter.maskImage = CIImage(cvPixelBuffer: mask) + if let output = filter.outputImage, let cgImage = ciContext.createCGImage(output, from: inputImage.extent) { + let image = UIImage(cgImage: cgImage) + subscriber.putNext(.image(image)) + subscriber.putCompletion() + return + } + } else { + let filter = CIFilter.blendWithMask() + filter.inputImage = CIImage(color: .white) + filter.backgroundImage = CIImage(color: .black) + filter.maskImage = CIImage(cvPixelBuffer: mask) + if let output = filter.outputImage, let cgImage = ciContext.createCGImage(output, from: inputImage.extent) { + let image = UIImage(cgImage: cgImage) + subscriber.putNext(.image(image)) + subscriber.putCompletion() + return + } +// subscriber.putNext(.pixelBuffer(mask)) +// subscriber.putCompletion() + } + } + subscriber.putNext(nil) + subscriber.putCompletion() + } + + try? handler.perform([request]) + return ActionDisposable { + request.cancel() + } + } + |> runOn(queue) + } else { + return .single(nil) + } +} + +@available(iOS 17.0, *) +private func instances(atPoint maybePoint: CGPoint?, inObservation observation: VNInstanceMaskObservation) -> IndexSet { + guard let point = maybePoint else { + return observation.allInstances + } + + let instanceMap = observation.instanceMask + let coords = VNImagePointForNormalizedPoint(point, CVPixelBufferGetWidth(instanceMap) - 1, CVPixelBufferGetHeight(instanceMap) - 1) + + CVPixelBufferLockBaseAddress(instanceMap, .readOnly) + guard let pixels = CVPixelBufferGetBaseAddress(instanceMap) else { + fatalError() + } + let bytesPerRow = CVPixelBufferGetBytesPerRow(instanceMap) + let instanceLabel = pixels.load(fromByteOffset: Int(coords.y) * bytesPerRow + Int(coords.x), as: UInt8.self) + CVPixelBufferUnlockBaseAddress(instanceMap, .readOnly) + + return instanceLabel == 0 ? observation.allInstances : [Int(instanceLabel)] +} + +private extension UIImage { + convenience init?(pixelBuffer: CVPixelBuffer) { + var cgImage: CGImage? + VTCreateCGImageFromCVPixelBuffer(pixelBuffer, options: nil, imageOut: &cgImage) + + guard let cgImage = cgImage else { + return nil + } + + self.init(cgImage: cgImage) + } +} diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditor.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditor.swift index 6dba11a9ea3..183b2405e3b 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditor.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditor.swift @@ -94,6 +94,11 @@ public final class MediaEditor { } } + public enum Mode { + case `default` + case sticker + } + public enum Subject { case image(UIImage, PixelDimensions) case video(String, UIImage?, Bool, String?, PixelDimensions, Double) @@ -116,6 +121,7 @@ public final class MediaEditor { } private let context: AccountContext + private let mode: Mode private let subject: Subject private let clock = CMClockGetHostTimeClock() @@ -182,6 +188,10 @@ public final class MediaEditor { } } + public private(set) var canCutout: Bool = false + public var canCutoutUpdated: (Bool) -> Void = { _ in } + public var isCutoutUpdated: (Bool) -> Void = { _ in } + private var textureCache: CVMetalTextureCache! public var hasPortraitMask: Bool { @@ -391,8 +401,9 @@ public final class MediaEditor { } } - public init(context: AccountContext, subject: Subject, values: MediaEditorValues? = nil, hasHistogram: Bool = false) { + public init(context: AccountContext, mode: Mode, subject: Subject, values: MediaEditorValues? = nil, hasHistogram: Bool = false) { self.context = context + self.mode = mode self.subject = subject if let values { self.values = values @@ -668,6 +679,19 @@ public final class MediaEditor { } else { textureSource.setMainInput(.image(image)) } + + + if case .sticker = self.mode { + let _ = (cutoutStickerImage(from: image, onlyCheck: true) + |> deliverOnMainQueue).start(next: { [weak self] result in + guard let self, result != nil else { + return + } + self.canCutout = true + self.canCutoutUpdated(true) + }) + } + } if let player, let playerItem = player.currentItem, !textureSourceResult.playerIsReference { textureSource.setMainInput(.video(playerItem)) @@ -677,7 +701,12 @@ public final class MediaEditor { } self.renderer.textureSource = textureSource - self.setGradientColors(textureSourceResult.gradientColors) + switch self.mode { + case .default: + self.setGradientColors(textureSourceResult.gradientColors) + case .sticker: + self.setGradientColors(GradientColors(top: .clear, bottom: .clear)) + } if let _ = textureSourceResult.player { self.updateRenderChain() @@ -1615,7 +1644,7 @@ public final class MediaEditor { public func setGradientColors(_ gradientColors: GradientColors) { self.gradientColorsPromise.set(.single(gradientColors)) - self.updateValues(mode: .skipRendering) { values in + self.updateValues(mode: self.sourceIsVideo ? .skipRendering : .generic) { values in return values.withUpdatedGradientColors(gradientColors: gradientColors.array) } } @@ -1654,6 +1683,51 @@ public final class MediaEditor { self.renderer.renderFrame() } + public func getSeparatedImage(point: CGPoint?) -> Signal { + guard let textureSource = self.renderer.textureSource as? UniversalTextureSource, let image = textureSource.mainImage else { + return .single(nil) + } + return cutoutImage(from: image, atPoint: point, asImage: true) + |> map { result in + if let result, case let .image(image) = result { + return image + } else { + return nil + } + } + } + + public func removeSeparationMask() { + self.isCutoutUpdated(false) + + self.renderer.currentMainInputMask = nil + if !self.skipRendering { + self.updateRenderChain() + } + } + + public func setSeparationMask(point: CGPoint?) { + guard let renderTarget = self.previewView, let device = renderTarget.mtlDevice else { + return + } + guard let textureSource = self.renderer.textureSource as? UniversalTextureSource, let image = textureSource.mainImage else { + return + } + self.isCutoutUpdated(true) + + let _ = (cutoutImage(from: image, atPoint: point, asImage: false) + |> deliverOnMainQueue).start(next: { [weak self] result in + guard let self, let result, case let .image(image) = result else { + return + } + //TODO:replace with pixelbuffer + self.renderer.currentMainInputMask = loadTexture(image: image, device: device) + if !self.skipRendering { + self.updateRenderChain() + } + }) + } + private func maybeGeneratePersonSegmentation(_ image: UIImage?) { if #available(iOS 15.0, *), let cgImage = image?.cgImage { let faceRequest = VNDetectFaceRectanglesRequest { [weak self] request, _ in diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorComposer.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorComposer.swift index eb276b8a087..d97aa11d7d0 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorComposer.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorComposer.swift @@ -178,7 +178,7 @@ final class MediaEditorComposer { } } -public func makeEditorImageComposition(context: CIContext, postbox: Postbox, inputImage: UIImage, dimensions: CGSize, values: MediaEditorValues, time: CMTime, textScale: CGFloat, completion: @escaping (UIImage?) -> Void) { +public func makeEditorImageComposition(context: CIContext, postbox: Postbox, inputImage: UIImage, dimensions: CGSize, outputDimensions: CGSize? = nil, values: MediaEditorValues, time: CMTime, textScale: CGFloat, completion: @escaping (UIImage?) -> Void) { let colorSpace = CGColorSpaceCreateDeviceRGB() let inputImage = CIImage(image: inputImage, options: [.colorSpace: colorSpace])! var drawingImage: CIImage? @@ -192,7 +192,7 @@ public func makeEditorImageComposition(context: CIContext, postbox: Postbox, inp entities.append(contentsOf: composerEntitiesForDrawingEntity(postbox: postbox, textScale: textScale, entity: entity.entity, colorSpace: colorSpace)) } - makeEditorImageFrameComposition(context: context, inputImage: inputImage, drawingImage: drawingImage, dimensions: dimensions, outputDimensions: dimensions, values: values, entities: entities, time: time, textScale: textScale, completion: { ciImage in + makeEditorImageFrameComposition(context: context, inputImage: inputImage, drawingImage: drawingImage, dimensions: dimensions, outputDimensions: outputDimensions ?? dimensions, values: values, entities: entities, time: time, textScale: textScale, completion: { ciImage in if let ciImage { if let cgImage = context.createCGImage(ciImage, from: CGRect(origin: .zero, size: ciImage.extent.size)) { Queue.mainQueue().async { @@ -206,20 +206,19 @@ public func makeEditorImageComposition(context: CIContext, postbox: Postbox, inp } private func makeEditorImageFrameComposition(context: CIContext, inputImage: CIImage, drawingImage: CIImage?, dimensions: CGSize, outputDimensions: CGSize, values: MediaEditorValues, entities: [MediaEditorComposerEntity], time: CMTime, textScale: CGFloat = 1.0, completion: @escaping (CIImage?) -> Void) { - var resultImage = CIImage(color: .black).cropped(to: CGRect(origin: .zero, size: dimensions)).transformed(by: CGAffineTransform(translationX: -dimensions.width / 2.0, y: -dimensions.height / 2.0)) + var isClear = false + if let gradientColor = values.gradientColors?.first, gradientColor.alpha.isZero { + isClear = true + } - var mediaImage = inputImage.samplingLinear().transformed(by: CGAffineTransform(translationX: -inputImage.extent.midX, y: -inputImage.extent.midY)) + var resultImage = CIImage(color: isClear ? .clear : .black).cropped(to: CGRect(origin: .zero, size: dimensions)).transformed(by: CGAffineTransform(translationX: -dimensions.width / 2.0, y: -dimensions.height / 2.0)) - var initialScale: CGFloat - if mediaImage.extent.height > mediaImage.extent.width && values.isStory { - initialScale = max(dimensions.width / mediaImage.extent.width, dimensions.height / mediaImage.extent.height) - } else { - initialScale = dimensions.width / mediaImage.extent.width - } + var mediaImage = inputImage.samplingLinear().transformed(by: CGAffineTransform(translationX: -inputImage.extent.midX, y: -inputImage.extent.midY)) if values.isStory { resultImage = mediaImage.samplingLinear().composited(over: resultImage) } else { + let initialScale = dimensions.width / mediaImage.extent.width var horizontalScale = initialScale if values.cropMirroring { horizontalScale *= -1.0 diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorRenderer.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorRenderer.swift index 0242d8dc452..c4bddd47257 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorRenderer.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorRenderer.swift @@ -98,6 +98,7 @@ final class MediaEditorRenderer { } private var currentMainInput: Input? + var currentMainInputMask: MTLTexture? private var currentAdditionalInput: Input? private(set) var resultTexture: MTLTexture? @@ -202,7 +203,7 @@ final class MediaEditorRenderer { } if let mainTexture { - return self.videoFinishPass.process(input: mainTexture, secondInput: additionalTexture, timestamp: mainInput.timestamp, device: device, commandBuffer: commandBuffer) + return self.videoFinishPass.process(input: mainTexture, inputMask: self.currentMainInputMask, secondInput: additionalTexture, timestamp: mainInput.timestamp, device: device, commandBuffer: commandBuffer) } else { return nil } diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorValues.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorValues.swift index cbd042f9759..fcf114e4dfb 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorValues.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorValues.swift @@ -634,6 +634,10 @@ public final class MediaEditorValues: Codable, Equatable { return MediaEditorValues(peerId: self.peerId, originalDimensions: self.originalDimensions, cropOffset: offset, cropRect: self.cropRect, cropScale: scale, cropRotation: rotation, cropMirroring: mirroring, cropOrientation: self.cropOrientation, gradientColors: self.gradientColors, videoTrimRange: self.videoTrimRange, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, videoVolume: self.videoVolume, additionalVideoPath: self.additionalVideoPath, additionalVideoIsDual: self.additionalVideoIsDual, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, additionalVideoTrimRange: self.additionalVideoTrimRange, additionalVideoOffset: self.additionalVideoOffset, additionalVideoVolume: self.additionalVideoVolume, nightTheme: self.nightTheme, drawing: self.drawing, entities: self.entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackOffset: self.audioTrackOffset, audioTrackVolume: self.audioTrackVolume, audioTrackSamples: self.audioTrackSamples, qualityPreset: self.qualityPreset) } + public func withUpdatedCropRect(cropRect: CGRect, rotation: CGFloat, mirroring: Bool) -> MediaEditorValues { + return MediaEditorValues(peerId: self.peerId, originalDimensions: self.originalDimensions, cropOffset: .zero, cropRect: cropRect, cropScale: 1.0, cropRotation: rotation, cropMirroring: mirroring, cropOrientation: self.cropOrientation, gradientColors: self.gradientColors, videoTrimRange: self.videoTrimRange, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, videoVolume: self.videoVolume, additionalVideoPath: self.additionalVideoPath, additionalVideoIsDual: self.additionalVideoIsDual, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, additionalVideoTrimRange: self.additionalVideoTrimRange, additionalVideoOffset: self.additionalVideoOffset, additionalVideoVolume: self.additionalVideoVolume, nightTheme: self.nightTheme, drawing: self.drawing, entities: self.entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackOffset: self.audioTrackOffset, audioTrackVolume: self.audioTrackVolume, audioTrackSamples: self.audioTrackSamples, qualityPreset: self.qualityPreset) + } + func withUpdatedGradientColors(gradientColors: [UIColor]) -> MediaEditorValues { return MediaEditorValues(peerId: self.peerId, originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropRect: self.cropRect, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, cropOrientation: self.cropOrientation, gradientColors: gradientColors, videoTrimRange: self.videoTrimRange, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, videoVolume: self.videoVolume, additionalVideoPath: self.additionalVideoPath, additionalVideoIsDual: self.additionalVideoIsDual, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, additionalVideoTrimRange: self.additionalVideoTrimRange, additionalVideoOffset: self.additionalVideoOffset, additionalVideoVolume: self.additionalVideoVolume, nightTheme: self.nightTheme, drawing: self.drawing, entities: self.entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackOffset: self.audioTrackOffset, audioTrackVolume: self.audioTrackVolume, audioTrackSamples: self.audioTrackSamples, qualityPreset: self.qualityPreset) } @@ -716,6 +720,10 @@ public final class MediaEditorValues: Codable, Equatable { return MediaEditorValues(peerId: self.peerId, originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropRect: self.cropRect, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, cropOrientation: self.cropOrientation, gradientColors: self.gradientColors, videoTrimRange: videoTrimRange, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, videoVolume: self.videoVolume, additionalVideoPath: self.additionalVideoPath, additionalVideoIsDual: self.additionalVideoIsDual, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, additionalVideoTrimRange: self.additionalVideoTrimRange, additionalVideoOffset: self.additionalVideoOffset, additionalVideoVolume: self.additionalVideoVolume, nightTheme: self.nightTheme, drawing: self.drawing, entities: entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackOffset: self.audioTrackOffset, audioTrackVolume: self.audioTrackVolume, audioTrackSamples: self.audioTrackSamples, qualityPreset: self.qualityPreset) } + public func withUpdatedQualityPreset(_ qualityPreset: MediaQualityPreset?) -> MediaEditorValues { + return MediaEditorValues(peerId: self.peerId, originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropRect: self.cropRect, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, cropOrientation: self.cropOrientation, gradientColors: self.gradientColors, videoTrimRange: videoTrimRange, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, videoVolume: self.videoVolume, additionalVideoPath: self.additionalVideoPath, additionalVideoIsDual: self.additionalVideoIsDual, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, additionalVideoTrimRange: self.additionalVideoTrimRange, additionalVideoOffset: self.additionalVideoOffset, additionalVideoVolume: self.additionalVideoVolume, nightTheme: self.nightTheme, drawing: self.drawing, entities: self.entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackOffset: self.audioTrackOffset, audioTrackVolume: self.audioTrackVolume, audioTrackSamples: self.audioTrackSamples, qualityPreset: qualityPreset) + } + public var resultDimensions: PixelDimensions { if self.videoIsFullHd { return PixelDimensions(width: 1080, height: 1920) diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/RenderPass.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/RenderPass.swift index 4611bc5f57f..54a849f09e5 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/RenderPass.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/RenderPass.swift @@ -165,6 +165,7 @@ final class OutputRenderPass: DefaultRenderPass { renderPassDescriptor.colorAttachments[0].texture = (drawable as? CAMetalDrawable)?.texture renderPassDescriptor.colorAttachments[0].loadAction = .clear renderPassDescriptor.colorAttachments[0].storeAction = .store + renderPassDescriptor.colorAttachments[0].clearColor = MTLClearColor(red: 0.0, green: 0.0, blue: 0.0, alpha: 0.0) let drawableSize = renderTarget.drawableSize diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/UniversalTextureSource.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/UniversalTextureSource.swift index 737dd4ee674..4e7a97f844e 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/UniversalTextureSource.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/UniversalTextureSource.swift @@ -42,6 +42,13 @@ final class UniversalTextureSource: TextureSource { ) } + var mainImage: UIImage? { + if let mainInput = self.mainInputContext?.input, case let .image(image) = mainInput { + return image + } + return nil + } + func setMainInput(_ input: Input) { guard let renderTarget = self.renderTarget else { return @@ -128,15 +135,6 @@ final class UniversalTextureSource: TextureSource { self.update() } } -// -// private func setupDisplayLink(frameRate: Int) { -// self.displayLink?.invalidate() -// self.displayLink = nil -// -// if self.playerItemOutput != nil { - -// } -// } } private protocol InputContext { diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/VideoFinishPass.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/VideoFinishPass.swift index 45f313016f3..d6e3fcbe436 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/VideoFinishPass.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/VideoFinishPass.swift @@ -144,6 +144,14 @@ private var transitionDuration = 0.5 private var apperanceDuration = 0.2 private var videoRemovalDuration: Double = 0.2 +struct VideoEncodeParameters { + var dimensions: simd_float2 + var roundness: simd_float1 + var alpha: simd_float1 + var isOpaque: simd_float1 + var empty: simd_float1 +} + final class VideoFinishPass: RenderPass { private var cachedTexture: MTLTexture? @@ -195,6 +203,7 @@ final class VideoFinishPass: RenderPass { containerSize: CGSize, texture: MTLTexture, textureRotation: TextureRotation, + maskTexture: MTLTexture?, position: VideoPosition, roundness: Float, alpha: Float, @@ -202,6 +211,11 @@ final class VideoFinishPass: RenderPass { device: MTLDevice ) { encoder.setFragmentTexture(texture, index: 0) + if let maskTexture { + encoder.setFragmentTexture(maskTexture, index: 1) + } else { + encoder.setFragmentTexture(texture, index: 1) + } let center = CGPoint( x: position.position.x - containerSize.width / 2.0, @@ -220,20 +234,31 @@ final class VideoFinishPass: RenderPass { options: []) encoder.setVertexBuffer(buffer, offset: 0, index: 0) - var resolution = simd_uint2(UInt32(size.width), UInt32(size.height)) - encoder.setFragmentBytes(&resolution, length: MemoryLayout.size * 2, index: 0) - - var roundness = roundness - encoder.setFragmentBytes(&roundness, length: MemoryLayout.size, index: 1) - - var alpha = alpha - encoder.setFragmentBytes(&alpha, length: MemoryLayout.size, index: 2) + var parameters = VideoEncodeParameters( + dimensions: simd_float2(Float(size.width), Float(size.height)), + roundness: roundness, + alpha: alpha, + isOpaque: maskTexture == nil ? 1.0 : 0.0, + empty: 0 + ) + encoder.setFragmentBytes(¶meters, length: MemoryLayout.size, index: 0) +// var resolution = simd_uint2(UInt32(size.width), UInt32(size.height)) +// encoder.setFragmentBytes(&resolution, length: MemoryLayout.size * 2, index: 0) +// +// var roundness = roundness +// encoder.setFragmentBytes(&roundness, length: MemoryLayout.size, index: 1) +// +// var alpha = alpha +// encoder.setFragmentBytes(&alpha, length: MemoryLayout.size, index: 2) +// +// var isOpaque = maskTexture == nil ? 1.0 : 0.0 +// encoder.setFragmentBytes(&isOpaque, length: MemoryLayout.size, index: 3) encoder.drawPrimitives(type: .triangleStrip, vertexStart: 0, vertexCount: 4) } private let canvasSize = CGSize(width: 1080.0, height: 1920.0) - private var gradientColors = GradientColors(topColor: simd_float3(0.0, 0.0, 0.0), bottomColor: simd_float3(0.0, 0.0, 0.0)) + private var gradientColors = GradientColors(topColor: simd_float4(0.0, 0.0, 0.0, 0.0), bottomColor: simd_float4(0.0, 0.0, 0.0, 0.0)) func update(values: MediaEditorValues, videoDuration: Double?, additionalVideoDuration: Double?) { let position = CGPoint( x: canvasSize.width / 2.0 + values.cropOffset.x, @@ -241,6 +266,7 @@ final class VideoFinishPass: RenderPass { ) self.isStory = values.isStory + self.isSticker = values.gradientColors?.first?.alpha == 0.0 self.mainPosition = VideoFinishPass.VideoPosition(position: position, size: self.mainPosition.size, scale: values.cropScale, rotation: values.cropRotation, baseScale: self.mainPosition.baseScale) if let position = values.additionalVideoPosition, let scale = values.additionalVideoScale, let rotation = values.additionalVideoRotation { @@ -262,12 +288,12 @@ final class VideoFinishPass: RenderPass { } if let gradientColors = values.gradientColors, let top = gradientColors.first, let bottom = gradientColors.last { - let (topRed, topGreen, topBlue, _) = top.components - let (bottomRed, bottomGreen, bottomBlue, _) = bottom.components + let (topRed, topGreen, topBlue, topAlpha) = top.components + let (bottomRed, bottomGreen, bottomBlue, bottomAlpha) = bottom.components self.gradientColors = GradientColors( - topColor: simd_float3(Float(topRed), Float(topGreen), Float(topBlue)), - bottomColor: simd_float3(Float(bottomRed), Float(bottomGreen), Float(bottomBlue)) + topColor: simd_float4(Float(topRed), Float(topGreen), Float(topBlue), Float(topAlpha)), + bottomColor: simd_float4(Float(bottomRed), Float(bottomGreen), Float(bottomBlue), Float(bottomAlpha)) ) } } @@ -289,6 +315,7 @@ final class VideoFinishPass: RenderPass { ) private var isStory = true + private var isSticker = true private var videoPositionChanges: [VideoPositionChange] = [] private var videoDuration: Double? private var additionalVideoDuration: Double? @@ -476,16 +503,31 @@ final class VideoFinishPass: RenderPass { return (backgroundVideoState, foregroundVideoState, disappearingVideoState) } - func process(input: MTLTexture, secondInput: MTLTexture?, timestamp: CMTime, device: MTLDevice, commandBuffer: MTLCommandBuffer) -> MTLTexture? { + func process( + input: MTLTexture, + inputMask: MTLTexture?, + secondInput: MTLTexture?, + timestamp: CMTime, + device: MTLDevice, + commandBuffer: MTLCommandBuffer + ) -> MTLTexture? { if !self.isStory { return input } let baseScale: CGFloat - if input.height > input.width { - baseScale = max(canvasSize.width / CGFloat(input.width), canvasSize.height / CGFloat(input.height)) + if !self.isSticker { + if input.height > input.width { + baseScale = max(canvasSize.width / CGFloat(input.width), canvasSize.height / CGFloat(input.height)) + } else { + baseScale = canvasSize.width / CGFloat(input.width) + } } else { - baseScale = canvasSize.width / CGFloat(input.width) + if input.height > input.width { + baseScale = canvasSize.width / CGFloat(input.width) + } else { + baseScale = canvasSize.width / CGFloat(input.height) + } } self.mainPosition = self.mainPosition.with(size: CGSize(width: input.width, height: input.height), baseScale: baseScale) @@ -508,9 +550,13 @@ final class VideoFinishPass: RenderPass { let renderPassDescriptor = MTLRenderPassDescriptor() renderPassDescriptor.colorAttachments[0].texture = self.cachedTexture! - renderPassDescriptor.colorAttachments[0].loadAction = .dontCare + if self.gradientColors.topColor.w > 0.0 { + renderPassDescriptor.colorAttachments[0].loadAction = .dontCare + } else { + renderPassDescriptor.colorAttachments[0].loadAction = .clear + } renderPassDescriptor.colorAttachments[0].storeAction = .store - renderPassDescriptor.colorAttachments[0].clearColor = MTLClearColor(red: 0.0, green: 0.0, blue: 0.0, alpha: 1.0) + renderPassDescriptor.colorAttachments[0].clearColor = MTLClearColor(red: 0.0, green: 0.0, blue: 0.0, alpha: 0.0) guard let renderCommandEncoder = commandBuffer.makeRenderCommandEncoder(descriptor: renderPassDescriptor) else { return input } @@ -521,12 +567,13 @@ final class VideoFinishPass: RenderPass { znear: -1.0, zfar: 1.0) ) - renderCommandEncoder.setRenderPipelineState(self.gradientPipelineState!) - self.encodeGradient( - using: renderCommandEncoder, - containerSize: containerSize, - device: device - ) + if self.gradientColors.topColor.w > 0.0 { + self.encodeGradient( + using: renderCommandEncoder, + containerSize: containerSize, + device: device + ) + } renderCommandEncoder.setRenderPipelineState(self.mainPipelineState!) @@ -538,6 +585,7 @@ final class VideoFinishPass: RenderPass { containerSize: containerSize, texture: transitionVideoState.texture, textureRotation: transitionVideoState.textureRotation, + maskTexture: nil, position: transitionVideoState.position, roundness: transitionVideoState.roundness, alpha: transitionVideoState.alpha, @@ -551,6 +599,7 @@ final class VideoFinishPass: RenderPass { containerSize: containerSize, texture: mainVideoState.texture, textureRotation: mainVideoState.textureRotation, + maskTexture: inputMask, position: mainVideoState.position, roundness: mainVideoState.roundness, alpha: mainVideoState.alpha, @@ -564,6 +613,7 @@ final class VideoFinishPass: RenderPass { containerSize: containerSize, texture: additionalVideoState.texture, textureRotation: additionalVideoState.textureRotation, + maskTexture: nil, position: additionalVideoState.position, roundness: additionalVideoState.roundness, alpha: additionalVideoState.alpha, @@ -578,8 +628,8 @@ final class VideoFinishPass: RenderPass { } struct GradientColors { - var topColor: simd_float3 - var bottomColor: simd_float3 + var topColor: simd_float4 + var bottomColor: simd_float4 } func encodeGradient( @@ -587,6 +637,7 @@ final class VideoFinishPass: RenderPass { containerSize: CGSize, device: MTLDevice ) { + encoder.setRenderPipelineState(self.gradientPipelineState!) let vertices = verticesDataForRotation(.rotate0Degrees) let buffer = device.makeBuffer( diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/BUILD b/submodules/TelegramUI/Components/MediaEditorScreen/BUILD index e0bf2ad2537..88ab5ba5efb 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/BUILD +++ b/submodules/TelegramUI/Components/MediaEditorScreen/BUILD @@ -50,6 +50,8 @@ swift_library( "//submodules/TelegramUI/Components/Stories/ForwardInfoPanelComponent", "//submodules/TelegramUI/Components/ContextReferenceButtonComponent", "//submodules/TelegramUI/Components/MediaScrubberComponent", + "//submodules/Components/BlurredBackgroundComponent", + "//submodules/TelegramUI/Components/DustEffect", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaCutoutScreen.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaCutoutScreen.swift new file mode 100644 index 00000000000..fd68d35fbbb --- /dev/null +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaCutoutScreen.swift @@ -0,0 +1,437 @@ +import Foundation +import UIKit +import Display +import AsyncDisplayKit +import ComponentFlow +import SwiftSignalKit +import ViewControllerComponent +import ComponentDisplayAdapters +import TelegramPresentationData +import AccountContext +import TelegramCore +import MultilineTextComponent +import DrawingUI +import MediaEditor +import Photos +import LottieAnimationComponent +import MessageInputPanelComponent +import DustEffect + +private final class MediaCutoutScreenComponent: Component { + typealias EnvironmentType = ViewControllerComponentContainer.Environment + + let context: AccountContext + let mediaEditor: MediaEditor + + init( + context: AccountContext, + mediaEditor: MediaEditor + ) { + self.context = context + self.mediaEditor = mediaEditor + } + + static func ==(lhs: MediaCutoutScreenComponent, rhs: MediaCutoutScreenComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + return true + } + + public final class View: UIView { + private let buttonsContainerView = UIView() + private let buttonsBackgroundView = UIView() + private let previewContainerView = UIView() + private let cancelButton = ComponentView() + private let label = ComponentView() + private let doneButton = ComponentView() + + private let fadeView = UIView() + private let separatedImageView = UIImageView() + + private var component: MediaCutoutScreenComponent? + private weak var state: State? + private var environment: ViewControllerComponentContainer.Environment? + + override init(frame: CGRect) { + self.buttonsContainerView.clipsToBounds = true + + self.fadeView.alpha = 0.0 + self.fadeView.backgroundColor = UIColor(rgb: 0x000000, alpha: 0.6) + + self.separatedImageView.contentMode = .scaleAspectFit + + super.init(frame: frame) + + self.backgroundColor = .clear + + self.addSubview(self.buttonsContainerView) + self.buttonsContainerView.addSubview(self.buttonsBackgroundView) + + self.addSubview(self.fadeView) + self.addSubview(self.separatedImageView) + self.addSubview(self.previewContainerView) + + self.previewContainerView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.previewTap(_:)))) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + @objc private func previewTap(_ gestureRecognizer: UITapGestureRecognizer) { + guard let component = self.component else { + return + } + let location = gestureRecognizer.location(in: gestureRecognizer.view) + + let point = CGPoint( + x: location.x / self.previewContainerView.frame.width, + y: location.y / self.previewContainerView.frame.height + ) + component.mediaEditor.setSeparationMask(point: point) + + self.playDissolveAnimation() + } + + func animateInFromEditor() { + self.buttonsBackgroundView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + self.label.view?.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + } + + private var animatingOut = false + func animateOutToEditor(completion: @escaping () -> Void) { + self.animatingOut = true + + self.cancelButton.view?.isHidden = true + + self.fadeView.layer.animateAlpha(from: self.fadeView.alpha, to: 0.0, duration: 0.2, removeOnCompletion: false) + self.buttonsBackgroundView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { _ in + completion() + }) + self.label.view?.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false) + + self.state?.updated() + } + + public func playDissolveAnimation() { + guard let component = self.component, let resultImage = component.mediaEditor.resultImage, let environment = self.environment, let controller = environment.controller() as? MediaCutoutScreen else { + return + } + let previewView = controller.previewView + + let dustEffectLayer = DustEffectLayer() + dustEffectLayer.position = previewView.center + dustEffectLayer.bounds = previewView.bounds + previewView.superview?.layer.insertSublayer(dustEffectLayer, below: previewView.layer) + + dustEffectLayer.animationSpeed = 2.2 + dustEffectLayer.becameEmpty = { [weak dustEffectLayer] in + dustEffectLayer?.removeFromSuperlayer() + } + + dustEffectLayer.addItem(frame: previewView.bounds, image: resultImage) + + controller.requestDismiss(animated: true) + } + + func update(component: MediaCutoutScreenComponent, availableSize: CGSize, state: State, environment: Environment, transition: Transition) -> CGSize { + let environment = environment[ViewControllerComponentContainer.Environment.self].value + self.environment = environment + + let isFirstTime = self.component == nil + self.component = component + self.state = state + + let isTablet: Bool + if case .regular = environment.metrics.widthClass { + isTablet = true + } else { + isTablet = false + } + + let buttonSideInset: CGFloat + let buttonBottomInset: CGFloat = 8.0 + var controlsBottomInset: CGFloat = 0.0 + let previewSize: CGSize + var topInset: CGFloat = environment.statusBarHeight + 5.0 + if isTablet { + let previewHeight = availableSize.height - topInset - 75.0 + previewSize = CGSize(width: floorToScreenPixels(previewHeight / 1.77778), height: previewHeight) + buttonSideInset = 30.0 + } else { + previewSize = CGSize(width: availableSize.width, height: floorToScreenPixels(availableSize.width * 1.77778)) + buttonSideInset = 10.0 + if availableSize.height < previewSize.height + 30.0 { + topInset = 0.0 + controlsBottomInset = -75.0 + } else { + self.buttonsBackgroundView.backgroundColor = .clear + } + } + + let previewContainerFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - previewSize.width) / 2.0), y: environment.safeInsets.top), size: CGSize(width: previewSize.width, height: availableSize.height - environment.safeInsets.top - environment.safeInsets.bottom + controlsBottomInset)) + let buttonsContainerFrame = CGRect(origin: CGPoint(x: 0.0, y: availableSize.height - environment.safeInsets.bottom + controlsBottomInset), size: CGSize(width: availableSize.width, height: environment.safeInsets.bottom - controlsBottomInset)) + + let cancelButtonSize = self.cancelButton.update( + transition: transition, + component: AnyComponent(Button( + content: AnyComponent( + LottieAnimationComponent( + animation: LottieAnimationComponent.AnimationItem( + name: "media_backToCancel", + mode: .animating(loop: false), + range: self.animatingOut ? (0.5, 1.0) : (0.0, 0.5) + ), + colors: ["__allcolors__": .white], + size: CGSize(width: 33.0, height: 33.0) + ) + ), + action: { + guard let controller = environment.controller() as? MediaCutoutScreen else { + return + } + controller.requestDismiss(animated: true) + } + )), + environment: {}, + containerSize: CGSize(width: 44.0, height: 44.0) + ) + let cancelButtonFrame = CGRect( + origin: CGPoint(x: buttonSideInset, y: buttonBottomInset), + size: cancelButtonSize + ) + if let cancelButtonView = self.cancelButton.view { + if cancelButtonView.superview == nil { + self.buttonsContainerView.addSubview(cancelButtonView) + } + transition.setFrame(view: cancelButtonView, frame: cancelButtonFrame) + } + + let labelSize = self.label.update( + transition: transition, + component: AnyComponent(Text(text: "Tap an object to cut it out", font: Font.regular(17.0), color: .white)), + environment: {}, + containerSize: CGSize(width: availableSize.width - 88.0, height: 44.0) + ) + let labelFrame = CGRect( + origin: CGPoint(x: floorToScreenPixels((availableSize.width - labelSize.width) / 2.0), y: buttonBottomInset + 4.0), + size: labelSize + ) + if let labelView = self.label.view { + if labelView.superview == nil { + self.buttonsContainerView.addSubview(labelView) + } + transition.setFrame(view: labelView, frame: labelFrame) + } + + transition.setFrame(view: self.buttonsContainerView, frame: buttonsContainerFrame) + transition.setFrame(view: self.buttonsBackgroundView, frame: CGRect(origin: .zero, size: buttonsContainerFrame.size)) + + transition.setFrame(view: self.previewContainerView, frame: previewContainerFrame) + transition.setFrame(view: self.separatedImageView, frame: previewContainerFrame) + + let frameWidth = floor(previewContainerFrame.width * 0.97) + + self.fadeView.frame = CGRect(x: floorToScreenPixels((previewContainerFrame.width - frameWidth) / 2.0), y: previewContainerFrame.minY + floorToScreenPixels((previewContainerFrame.height - frameWidth) / 2.0), width: frameWidth, height: frameWidth) + self.fadeView.layer.cornerRadius = frameWidth / 8.0 + + if isFirstTime { + let _ = (component.mediaEditor.getSeparatedImage(point: nil) + |> deliverOnMainQueue).start(next: { [weak self] image in + guard let self else { + return + } + self.separatedImageView.image = image + self.state?.updated(transition: .easeInOut(duration: 0.2)) + }) + } else { + if let _ = self.separatedImageView.image { + transition.setAlpha(view: self.fadeView, alpha: 1.0) + } else { + transition.setAlpha(view: self.fadeView, alpha: 0.0) + } + } + return availableSize + } + } + + func makeView() -> View { + return View() + } + + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} + +public final class MediaCutoutScreen: ViewController { + fileprivate final class Node: ViewControllerTracingNode, UIGestureRecognizerDelegate { + private weak var controller: MediaCutoutScreen? + private let context: AccountContext + + fileprivate let componentHost: ComponentView + + private var presentationData: PresentationData + private var validLayout: ContainerViewLayout? + + init(controller: MediaCutoutScreen) { + self.controller = controller + self.context = controller.context + + self.presentationData = self.context.sharedContext.currentPresentationData.with { $0 } + + self.componentHost = ComponentView() + + super.init() + + self.backgroundColor = .clear + } + + override func didLoad() { + super.didLoad() + + self.view.disablesInteractiveModalDismiss = true + self.view.disablesInteractiveKeyboardGestureRecognizer = true + } + + @objc func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { + return true + } + + func animateInFromEditor() { + if let view = self.componentHost.view as? MediaCutoutScreenComponent.View { + view.animateInFromEditor() + } + } + + func animateOutToEditor(completion: @escaping () -> Void) { + if let mediaEditor = self.controller?.mediaEditor { + mediaEditor.play() + } + if let view = self.componentHost.view as? MediaCutoutScreenComponent.View { + view.animateOutToEditor(completion: completion) + } + } + + func containerLayoutUpdated(layout: ContainerViewLayout, forceUpdate: Bool = false, animateOut: Bool = false, transition: Transition) { + guard let controller = self.controller else { + return + } + let isFirstTime = self.validLayout == nil + self.validLayout = layout + + let isTablet = layout.metrics.isTablet + + let previewSize: CGSize + let topInset: CGFloat = (layout.statusBarHeight ?? 0.0) + 5.0 + if isTablet { + let previewHeight = layout.size.height - topInset - 75.0 + previewSize = CGSize(width: floorToScreenPixels(previewHeight / 1.77778), height: previewHeight) + } else { + previewSize = CGSize(width: layout.size.width, height: floorToScreenPixels(layout.size.width * 1.77778)) + } + let bottomInset = layout.size.height - previewSize.height - topInset + + let environment = ViewControllerComponentContainer.Environment( + statusBarHeight: layout.statusBarHeight ?? 0.0, + navigationHeight: 0.0, + safeInsets: UIEdgeInsets( + top: topInset, + left: layout.safeInsets.left, + bottom: bottomInset, + right: layout.safeInsets.right + ), + additionalInsets: layout.additionalInsets, + inputHeight: layout.inputHeight ?? 0.0, + metrics: layout.metrics, + deviceMetrics: layout.deviceMetrics, + orientation: nil, + isVisible: true, + theme: self.presentationData.theme, + strings: self.presentationData.strings, + dateTimeFormat: self.presentationData.dateTimeFormat, + controller: { [weak self] in + return self?.controller + } + ) + + let componentSize = self.componentHost.update( + transition: transition, + component: AnyComponent( + MediaCutoutScreenComponent( + context: self.context, + mediaEditor: controller.mediaEditor + ) + ), + environment: { + environment + }, + forceUpdate: forceUpdate || animateOut, + containerSize: layout.size + ) + if let componentView = self.componentHost.view { + if componentView.superview == nil { + self.view.insertSubview(componentView, at: 3) + componentView.clipsToBounds = true + } + let componentFrame = CGRect(origin: .zero, size: componentSize) + transition.setFrame(view: componentView, frame: CGRect(origin: componentFrame.origin, size: CGSize(width: componentFrame.width, height: componentFrame.height))) + } + + if isFirstTime { + self.animateInFromEditor() + } + } + } + + fileprivate var node: Node { + return self.displayNode as! Node + } + + fileprivate let context: AccountContext + fileprivate let mediaEditor: MediaEditor + fileprivate let previewView: MediaEditorPreviewView + + public var dismissed: () -> Void = {} + + private var initialValues: MediaEditorValues + + public init(context: AccountContext, mediaEditor: MediaEditor, previewView: MediaEditorPreviewView) { + self.context = context + self.mediaEditor = mediaEditor + self.previewView = previewView + self.initialValues = mediaEditor.values.makeCopy() + + super.init(navigationBarPresentationData: nil) + self.navigationPresentation = .flatModal + + self.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait) + + self.statusBar.statusBarStyle = .White + } + + required public init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override public func loadDisplayNode() { + self.displayNode = Node(controller: self) + + super.displayNodeDidLoad() + } + + func requestDismiss(animated: Bool) { + self.dismissed() + + self.node.animateOutToEditor(completion: { + self.dismiss() + }) + } + + override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { + super.containerLayoutUpdated(layout, transition: transition) + + (self.displayNode as! Node).containerLayoutUpdated(layout: layout, transition: Transition(transition)) + } +} diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift index 52e1f0ef041..cfd9e042dfe 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift @@ -40,6 +40,7 @@ import TelegramStringFormatting import ForwardInfoPanelComponent import ContextReferenceButtonComponent import MediaScrubberComponent +import BlurredBackgroundComponent private let playbackButtonTag = GenericComponentViewTag() private let muteButtonTag = GenericComponentViewTag() @@ -78,6 +79,7 @@ final class MediaEditorScreenComponent: Component { let entityViewForEntity: (DrawingEntity) -> DrawingEntityView? let openDrawing: (DrawingScreenType) -> Void let openTools: () -> Void + let openCutout: () -> Void init( context: AccountContext, @@ -93,7 +95,8 @@ final class MediaEditorScreenComponent: Component { selectedEntity: DrawingEntity?, entityViewForEntity: @escaping (DrawingEntity) -> DrawingEntityView?, openDrawing: @escaping (DrawingScreenType) -> Void, - openTools: @escaping () -> Void + openTools: @escaping () -> Void, + openCutout: @escaping () -> Void ) { self.context = context self.externalState = externalState @@ -109,6 +112,7 @@ final class MediaEditorScreenComponent: Component { self.entityViewForEntity = entityViewForEntity self.openDrawing = openDrawing self.openTools = openTools + self.openCutout = openCutout } static func ==(lhs: MediaEditorScreenComponent, rhs: MediaEditorScreenComponent) -> Bool { @@ -149,6 +153,8 @@ final class MediaEditorScreenComponent: Component { case sticker case tools case done + case cutout + case undo } private var cachedImages: [ImageKey: UIImage] = [:] func image(_ key: ImageKey) -> UIImage { @@ -165,6 +171,10 @@ final class MediaEditorScreenComponent: Component { image = generateTintedImage(image: UIImage(bundleImageName: "Media Editor/AddSticker"), color: .white)! case .tools: image = generateTintedImage(image: UIImage(bundleImageName: "Media Editor/Tools"), color: .white)! + case .cutout: + image = generateTintedImage(image: UIImage(bundleImageName: "Media Editor/Cutout"), color: .white)! + case .undo: + image = generateTintedImage(image: UIImage(bundleImageName: "Media Editor/CutoutUndo"), color: .white)! case .done: image = generateImage(CGSize(width: 33.0, height: 33.0), rotatedContext: { size, context in context.clear(CGRect(origin: CGPoint(), size: size)) @@ -254,6 +264,7 @@ final class MediaEditorScreenComponent: Component { private let stickerButton = ComponentView() private let toolsButton = ComponentView() private let doneButton = ComponentView() + private let cutoutButton = ComponentView() private let fadeView = UIButton() @@ -385,6 +396,7 @@ final class MediaEditorScreenComponent: Component { self.inputPanelExternalState.deleteBackward() } }, + openStickerEditor: {}, presentController: { [weak self] c, a in if let self { self.environment?.controller()?.present(c, in: .window(.root), with: a) @@ -580,7 +592,7 @@ final class MediaEditorScreenComponent: Component { } } - func animateOutToTool(transition: Transition) { + func animateOutToTool(inPlace: Bool, transition: Transition) { if let view = self.cancelButton.view { view.alpha = 0.0 } @@ -592,7 +604,9 @@ final class MediaEditorScreenComponent: Component { ] for button in buttons { if let view = button.view { - view.layer.animatePosition(from: .zero, to: CGPoint(x: 0.0, y: -44.0), duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, additive: true) + if !inPlace { + view.layer.animatePosition(from: .zero, to: CGPoint(x: 0.0, y: -44.0), duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, additive: true) + } view.layer.animateScale(from: 1.0, to: 0.1, duration: 0.2) } } @@ -607,7 +621,7 @@ final class MediaEditorScreenComponent: Component { } } - func animateInFromTool(transition: Transition) { + func animateInFromTool(inPlace: Bool, transition: Transition) { if let view = self.cancelButton.view { view.alpha = 1.0 } @@ -622,7 +636,9 @@ final class MediaEditorScreenComponent: Component { ] for button in buttons { if let view = button.view { - view.layer.animatePosition(from: CGPoint(x: 0.0, y: -44.0), to: .zero, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, additive: true) + if !inPlace { + view.layer.animatePosition(from: CGPoint(x: 0.0, y: -44.0), to: .zero, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, additive: true) + } view.layer.animateScale(from: 0.1, to: 1.0, duration: 0.2) } } @@ -685,6 +701,7 @@ final class MediaEditorScreenComponent: Component { let openDrawing = component.openDrawing let openTools = component.openTools + let openCutout = component.openCutout let buttonSideInset: CGFloat let buttonBottomInset: CGFloat = 8.0 @@ -748,33 +765,48 @@ final class MediaEditorScreenComponent: Component { transition.setAlpha(view: cancelButtonView, alpha: buttonsAreHidden ? 0.0 : bottomButtonsAlpha) } - let doneButtonTitle = isEditingStory ? environment.strings.Story_Editor_Done : environment.strings.Story_Editor_Next + var doneButtonTitle: String? + var doneButtonIcon: UIImage + switch controller.mode { + case .storyEditor: + doneButtonTitle = isEditingStory ? environment.strings.Story_Editor_Done.uppercased() : environment.strings.Story_Editor_Next.uppercased() + doneButtonIcon = UIImage(bundleImageName: "Media Editor/Next")! + case .stickerEditor: + doneButtonTitle = nil + doneButtonIcon = generateTintedImage(image: UIImage(bundleImageName: "Media Editor/Apply"), color: .white)! + } + let doneButtonSize = self.doneButton.update( transition: transition, component: AnyComponent(PlainButtonComponent( content: AnyComponent(DoneButtonContentComponent( backgroundColor: UIColor(rgb: 0x007aff), - icon: UIImage(bundleImageName: "Media Editor/Next")!, - title: doneButtonTitle.uppercased())), + icon: doneButtonIcon, + title: doneButtonTitle)), effectAlignment: .center, action: { [weak self] in guard let environment = self?.environment, let controller = environment.controller() as? MediaEditorScreen else { return } - guard !controller.node.recording.isActive else { - return - } - guard controller.checkCaptionLimit() else { - return - } - if controller.isEditingStory { - controller.requestCompletion(animated: true) - } else { - if controller.checkIfCompletionIsAllowed() { - controller.openPrivacySettings(completion: { [weak controller] in - controller?.requestCompletion(animated: true) - }) + switch controller.mode { + case .storyEditor: + guard !controller.node.recording.isActive else { + return + } + guard controller.checkCaptionLimit() else { + return + } + if controller.isEditingStory { + controller.requestStoryCompletion(animated: true) + } else { + if controller.checkIfCompletionIsAllowed() { + controller.openPrivacySettings(completion: { [weak controller] in + controller?.requestStoryCompletion(animated: true) + }) + } } + case .stickerEditor: + controller.requestStickerCompletion(animated: true) } } )), @@ -827,7 +859,7 @@ final class MediaEditorScreenComponent: Component { let drawButtonFrame = CGRect( origin: CGPoint(x: buttonsLeftOffset + floorToScreenPixels(buttonsAvailableWidth / 5.0 - drawButtonSize.width / 2.0 - 3.0), y: availableSize.height - environment.safeInsets.bottom + buttonBottomInset + controlsBottomInset + 1.0), size: drawButtonSize - ) + ) if let drawButtonView = self.drawButton.view { if drawButtonView.superview == nil { self.addSubview(drawButtonView) @@ -839,6 +871,7 @@ final class MediaEditorScreenComponent: Component { } } + let textButtonSize = self.textButton.update( transition: transition, component: AnyComponent(Button( @@ -950,6 +983,42 @@ final class MediaEditorScreenComponent: Component { } } + if controller.node.canCutout { + let isCutout = controller.node.isCutout + let cutoutButtonSize = self.cutoutButton.update( + transition: transition, + component: AnyComponent(PlainButtonComponent( + content: AnyComponent(CutoutButtonContentComponent( + backgroundColor: UIColor(rgb: 0xffffff, alpha: 0.18), + icon: state.image(isCutout ? .undo : .cutout), + title: isCutout ? "Undo Cut Out" : "Cut Out an Object" + )), + effectAlignment: .center, + action: { + openCutout() + } + )), + environment: {}, + containerSize: CGSize(width: availableSize.width, height: 44.0) + ) + let cutoutButtonFrame = CGRect( + origin: CGPoint(x: floorToScreenPixels((availableSize.width - cutoutButtonSize.width) / 2.0), y: availableSize.height - environment.safeInsets.bottom + buttonBottomInset + controlsBottomInset - cutoutButtonSize.height - 14.0), + size: cutoutButtonSize + ) + if let cutoutButtonView = self.cutoutButton.view { + if cutoutButtonView.superview == nil { + self.addSubview(cutoutButtonView) + + cutoutButtonView.layer.animatePosition(from: CGPoint(x: 0.0, y: 64.0), to: .zero, duration: 0.3, delay: 0.0, timingFunction: kCAMediaTimingFunctionSpring, additive: true) + cutoutButtonView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2, delay: 0.0) + cutoutButtonView.layer.animateScale(from: 0.1, to: 1.0, duration: 0.2, delay: 0.0) + } + transition.setPosition(view: cutoutButtonView, position: cutoutButtonFrame.center) + transition.setBounds(view: cutoutButtonView, bounds: CGRect(origin: .zero, size: cutoutButtonFrame.size)) + transition.setAlpha(view: cutoutButtonView, alpha: buttonsAreHidden ? 0.0 : bottomButtonsAlpha) + } + } + let mediaEditor = controller.node.mediaEditor var timeoutValue: String @@ -1083,224 +1152,6 @@ final class MediaEditorScreenComponent: Component { ) } - let nextInputMode: MessageInputPanelComponent.InputMode - switch self.currentInputMode { - case .text: - nextInputMode = .emoji - case .emoji: - nextInputMode = .text - default: - nextInputMode = .emoji - } - - var canRecordVideo = true - if let subject = controller.node.subject { - if case let .video(_, _, _, additionalPath, _, _, _, _, _) = subject, additionalPath != nil { - canRecordVideo = false - } - } - - self.inputPanel.parentState = state - let inputPanelSize = self.inputPanel.update( - transition: transition, - component: AnyComponent(MessageInputPanelComponent( - externalState: self.inputPanelExternalState, - context: component.context, - theme: environment.theme, - strings: environment.strings, - style: .editor, - placeholder: .plain(environment.strings.Story_Editor_InputPlaceholderAddCaption), - maxLength: Int(component.context.userLimits.maxStoryCaptionLength), - queryTypes: [.mention], - alwaysDarkWhenHasText: false, - resetInputContents: nil, - nextInputMode: { _ in return nextInputMode }, - areVoiceMessagesAvailable: false, - presentController: { [weak controller] c in - guard let controller else { - return - } - controller.present(c, in: .window(.root)) - }, - presentInGlobalOverlay: { [weak controller] c in - guard let controller else { - return - } - controller.presentInGlobalOverlay(c) - }, - sendMessageAction: { [weak self] in - guard let self else { - return - } - self.deactivateInput() - }, - sendMessageOptionsAction: nil, - sendStickerAction: { _ in }, - setMediaRecordingActive: canRecordVideo ? { [weak controller] isActive, _, finished, sourceView in - guard let controller else { - return - } - controller.node.recording.setMediaRecordingActive(isActive, finished: finished, sourceView: sourceView) - } : nil, - lockMediaRecording: { [weak controller, weak self] in - guard let controller, let self else { - return - } - controller.node.recording.isLocked = true - self.state?.updated(transition: .easeInOut(duration: 0.2)) - }, - stopAndPreviewMediaRecording: { [weak controller] in - guard let controller else { - return - } - controller.node.recording.setMediaRecordingActive(false, finished: true, sourceView: nil) - }, - discardMediaRecordingPreview: nil, - attachmentAction: nil, - myReaction: nil, - likeAction: nil, - likeOptionsAction: nil, - inputModeAction: { [weak self] in - if let self { - switch self.currentInputMode { - case .text: - self.currentInputMode = .emoji - case .emoji: - self.currentInputMode = .text - default: - self.currentInputMode = .emoji - } - if self.currentInputMode == .text { - self.activateInput() - } else { - self.state?.updated(transition: .immediate) - } - } - }, - timeoutAction: isEditingStory ? nil : { [weak controller] view, gesture in - guard let controller else { - return - } - controller.presentTimeoutSetup(sourceView: view, gesture: gesture) - }, - forwardAction: nil, - moreAction: nil, - presentVoiceMessagesUnavailableTooltip: nil, - presentTextLengthLimitTooltip: { [weak controller] in - guard let controller else { - return - } - controller.presentCaptionLimitPremiumSuggestion(isPremium: controller.context.isPremium) - }, - presentTextFormattingTooltip: { [weak controller] in - guard let controller else { - return - } - controller.presentCaptionEntitiesPremiumSuggestion() - }, - paste: { [weak self, weak controller] data in - guard let self, let controller else { - return - } - switch data { - case let .sticker(image, _): - if max(image.size.width, image.size.height) > 1.0 { - let entity = DrawingStickerEntity(content: .image(image, .sticker)) - controller.node.interaction?.insertEntity(entity, scale: 1.0) - self.deactivateInput() - } - case let .images(images): - if images.count == 1, let image = images.first, max(image.size.width, image.size.height) > 1.0 { - let entity = DrawingStickerEntity(content: .image(image, .rectangle)) - controller.node.interaction?.insertEntity(entity, scale: 2.5) - self.deactivateInput() - } - case .text: - Queue.mainQueue().after(0.1) { - let text = self.getInputText() - if text.length > component.context.userLimits.maxStoryCaptionLength { - controller.presentCaptionLimitPremiumSuggestion(isPremium: self.state?.isPremium ?? false) - } - } - default: - break - } - }, - audioRecorder: nil, - videoRecordingStatus: controller.node.recording.status, - isRecordingLocked: controller.node.recording.isLocked, - hasRecordedVideo: mediaEditor?.values.additionalVideoPath != nil, - recordedAudioPreview: nil, - hasRecordedVideoPreview: false, - wasRecordingDismissed: false, - timeoutValue: timeoutValue, - timeoutSelected: false, - displayGradient: false, - bottomInset: 0.0, - isFormattingLocked: !state.isPremium, - hideKeyboard: self.currentInputMode == .emoji, - customInputView: nil, - forceIsEditing: self.currentInputMode == .emoji, - disabledPlaceholder: nil, - header: header, - isChannel: false, - storyItem: nil, - chatLocation: nil - )), - environment: {}, - containerSize: CGSize(width: inputPanelAvailableWidth, height: inputPanelAvailableHeight) - ) - - if self.inputPanelExternalState.isEditing && controller.node.entitiesView.hasSelection { - Queue.mainQueue().justDispatch { - controller.node.entitiesView.selectEntity(nil) - } - } - - if self.inputPanelExternalState.isEditing { - if self.currentInputMode == .emoji || (inputHeight.isZero && keyboardWasHidden) { - inputHeight = max(inputHeight, environment.deviceMetrics.standardInputHeight(inLandscape: false)) - } - } - keyboardHeight = inputHeight - - let fadeTransition = Transition(animation: .curve(duration: 0.3, curve: .easeInOut)) - if self.inputPanelExternalState.isEditing { - fadeTransition.setAlpha(view: self.fadeView, alpha: 1.0) - } else { - fadeTransition.setAlpha(view: self.fadeView, alpha: 0.0) - } - transition.setFrame(view: self.fadeView, frame: CGRect(origin: .zero, size: availableSize)) - - let isEditingCaption = self.inputPanelExternalState.isEditing - if self.isEditingCaption != isEditingCaption { - self.isEditingCaption = isEditingCaption - - if isEditingCaption { - controller.dismissAllTooltips() - mediaEditor?.maybePauseVideo() - } else { - mediaEditor?.maybeUnpauseVideo() - } - } - - let inputPanelBackgroundSize = self.inputPanelBackground.update( - transition: transition, - component: AnyComponent(BlurredGradientComponent(position: .bottom, tag: nil)), - environment: {}, - containerSize: CGSize(width: availableSize.width, height: keyboardHeight + 60.0) - ) - if let inputPanelBackgroundView = self.inputPanelBackground.view { - if inputPanelBackgroundView.superview == nil { - self.addSubview(inputPanelBackgroundView) - } - let isVisible = isEditingCaption && inputHeight > 44.0 - transition.setFrame(view: inputPanelBackgroundView, frame: CGRect(origin: CGPoint(x: 0.0, y: isVisible ? availableSize.height - inputPanelBackgroundSize.height : availableSize.height), size: inputPanelBackgroundSize)) - if !self.animatingButtons { - transition.setAlpha(view: inputPanelBackgroundView, alpha: isVisible ? 1.0 : 0.0, delay: isVisible ? 0.0 : 0.4) - } - } - var isEditingTextEntity = false var sizeSliderVisible = false var sizeValue: CGFloat? @@ -1310,562 +1161,783 @@ final class MediaEditorScreenComponent: Component { sizeValue = textEntity.fontSize } - var inputPanelBottomInset: CGFloat = -controlsBottomInset - if inputHeight > 0.0 { - inputPanelBottomInset = inputHeight - environment.safeInsets.bottom - } - let inputPanelFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - inputPanelSize.width) / 2.0), y: availableSize.height - environment.safeInsets.bottom - inputPanelBottomInset - inputPanelSize.height - 3.0), size: inputPanelSize) - if let inputPanelView = self.inputPanel.view { - if inputPanelView.superview == nil { - self.addSubview(inputPanelView) - } - transition.setFrame(view: inputPanelView, frame: inputPanelFrame) - transition.setAlpha(view: inputPanelView, alpha: isEditingTextEntity || component.isDisplayingTool || component.isDismissing || component.isInteractingWithEntities ? 0.0 : 1.0) - } - - if let playerState = state.playerState { - let scrubberInset: CGFloat = 9.0 - - let minDuration: Double - let maxDuration: Double - if playerState.isAudioOnly { - minDuration = 5.0 - maxDuration = 15.0 - } else { - minDuration = 1.0 - maxDuration = storyMaxVideoDuration - } - - let previousTrackCount = self.currentVisibleTracks?.count - let visibleTracks = playerState.tracks.filter { $0.visibleInTimeline }.map { MediaScrubberComponent.Track($0) } - self.currentVisibleTracks = visibleTracks - - var scrubberTransition = transition - if let previousTrackCount, previousTrackCount != visibleTracks.count { - scrubberTransition = .easeInOut(duration: 0.2) + if case .storyEditor = controller.mode { + let nextInputMode: MessageInputPanelComponent.InputMode + switch self.currentInputMode { + case .text: + nextInputMode = .emoji + case .emoji: + nextInputMode = .text + default: + nextInputMode = .emoji } - let isAudioOnly = playerState.isAudioOnly - let hasMainVideoTrack = playerState.tracks.contains(where: { $0.id == 0 }) - - let scrubber: ComponentView - if let current = self.scrubber { - scrubber = current - } else { - scrubber = ComponentView() - self.scrubber = scrubber + var canRecordVideo = true + if let subject = controller.node.subject { + if case let .video(_, _, _, additionalPath, _, _, _, _, _) = subject, additionalPath != nil { + canRecordVideo = false + } } - let scrubberSize = scrubber.update( - transition: scrubberTransition, - component: AnyComponent(MediaScrubberComponent( + self.inputPanel.parentState = state + let inputPanelSize = self.inputPanel.update( + transition: transition, + component: AnyComponent(MessageInputPanelComponent( + externalState: self.inputPanelExternalState, context: component.context, - style: .editor, theme: environment.theme, - generationTimestamp: playerState.generationTimestamp, - position: playerState.position, - minDuration: minDuration, - maxDuration: maxDuration, - isPlaying: playerState.isPlaying, - tracks: visibleTracks, - positionUpdated: { [weak mediaEditor] position, apply in - if let mediaEditor { - mediaEditor.seek(position, andPlay: apply) + strings: environment.strings, + style: .editor, + placeholder: .plain(environment.strings.Story_Editor_InputPlaceholderAddCaption), + maxLength: Int(component.context.userLimits.maxStoryCaptionLength), + queryTypes: [.mention], + alwaysDarkWhenHasText: false, + resetInputContents: nil, + nextInputMode: { _ in return nextInputMode }, + areVoiceMessagesAvailable: false, + presentController: { [weak controller] c in + guard let controller else { + return } + controller.present(c, in: .window(.root)) }, - trackTrimUpdated: { [weak mediaEditor] trackId, start, end, updatedEnd, apply in - guard let mediaEditor else { + presentInGlobalOverlay: { [weak controller] c in + guard let controller else { return } - let trimRange = start..= upperBound { - start = lowerBound - } else if start < lowerBound { - start = lowerBound - } - } - - mediaEditor.seek(start, andPlay: true) - mediaEditor.play() - } else { - mediaEditor.stop() - } - } - } else if trackId == 1 { - mediaEditor.setAdditionalVideoOffset(offset, apply: apply) + controller.presentTimeoutSetup(sourceView: view, gesture: gesture) + }, + forwardAction: nil, + moreAction: nil, + presentVoiceMessagesUnavailableTooltip: nil, + presentTextLengthLimitTooltip: { [weak controller] in + guard let controller else { + return } + controller.presentCaptionLimitPremiumSuggestion(isPremium: controller.context.isPremium) }, - trackLongPressed: { [weak self] trackId, sourceView in - guard let self, let controller = self.environment?.controller() as? MediaEditorScreen else { + presentTextFormattingTooltip: { [weak controller] in + guard let controller else { return } - controller.node.presentTrackOptions(trackId: trackId, sourceView: sourceView) - } + controller.presentCaptionEntitiesPremiumSuggestion() + }, + paste: { [weak self, weak controller] data in + guard let self, let controller else { + return + } + switch data { + case let .sticker(image, _): + if max(image.size.width, image.size.height) > 1.0 { + let entity = DrawingStickerEntity(content: .image(image, .sticker)) + controller.node.interaction?.insertEntity(entity, scale: 1.0) + self.deactivateInput() + } + case let .images(images): + if images.count == 1, let image = images.first, max(image.size.width, image.size.height) > 1.0 { + let entity = DrawingStickerEntity(content: .image(image, .rectangle)) + controller.node.interaction?.insertEntity(entity, scale: 2.5) + self.deactivateInput() + } + case .text: + Queue.mainQueue().after(0.1) { + let text = self.getInputText() + if text.length > component.context.userLimits.maxStoryCaptionLength { + controller.presentCaptionLimitPremiumSuggestion(isPremium: self.state?.isPremium ?? false) + } + } + default: + break + } + }, + audioRecorder: nil, + videoRecordingStatus: controller.node.recording.status, + isRecordingLocked: controller.node.recording.isLocked, + hasRecordedVideo: mediaEditor?.values.additionalVideoPath != nil, + recordedAudioPreview: nil, + hasRecordedVideoPreview: false, + wasRecordingDismissed: false, + timeoutValue: timeoutValue, + timeoutSelected: false, + displayGradient: false, + bottomInset: 0.0, + isFormattingLocked: !state.isPremium, + hideKeyboard: self.currentInputMode == .emoji, + customInputView: nil, + forceIsEditing: self.currentInputMode == .emoji, + disabledPlaceholder: nil, + header: header, + isChannel: false, + storyItem: nil, + chatLocation: nil )), environment: {}, - containerSize: CGSize(width: previewSize.width - scrubberInset * 2.0, height: availableSize.height) + containerSize: CGSize(width: inputPanelAvailableWidth, height: inputPanelAvailableHeight) ) - let scrubberFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - scrubberSize.width) / 2.0), y: availableSize.height - environment.safeInsets.bottom - scrubberSize.height + controlsBottomInset - inputPanelSize.height + 3.0), size: scrubberSize) - if let scrubberView = scrubber.view { - var animateIn = false - if scrubberView.superview == nil { - animateIn = true - if let inputPanelBackgroundView = self.inputPanelBackground.view, inputPanelBackgroundView.superview != nil { - self.insertSubview(scrubberView, belowSubview: inputPanelBackgroundView) - } else { - self.addSubview(scrubberView) - } + if self.inputPanelExternalState.isEditing && controller.node.entitiesView.hasSelection { + Queue.mainQueue().justDispatch { + controller.node.entitiesView.selectEntity(nil) } - if animateIn { - scrubberView.frame = scrubberFrame + } + + if self.inputPanelExternalState.isEditing { + if self.currentInputMode == .emoji || (inputHeight.isZero && keyboardWasHidden) { + inputHeight = max(inputHeight, environment.deviceMetrics.standardInputHeight(inLandscape: false)) + } + } + keyboardHeight = inputHeight + + let fadeTransition = Transition(animation: .curve(duration: 0.3, curve: .easeInOut)) + if self.inputPanelExternalState.isEditing { + fadeTransition.setAlpha(view: self.fadeView, alpha: 1.0) + } else { + fadeTransition.setAlpha(view: self.fadeView, alpha: 0.0) + } + transition.setFrame(view: self.fadeView, frame: CGRect(origin: .zero, size: availableSize)) + + let isEditingCaption = self.inputPanelExternalState.isEditing + if self.isEditingCaption != isEditingCaption { + self.isEditingCaption = isEditingCaption + + if isEditingCaption { + controller.dismissAllTooltips() + mediaEditor?.maybePauseVideo() } else { - scrubberTransition.setFrame(view: scrubberView, frame: scrubberFrame) + mediaEditor?.maybeUnpauseVideo() + } + } + + let inputPanelBackgroundSize = self.inputPanelBackground.update( + transition: transition, + component: AnyComponent(BlurredGradientComponent(position: .bottom, tag: nil)), + environment: {}, + containerSize: CGSize(width: availableSize.width, height: keyboardHeight + 60.0) + ) + if let inputPanelBackgroundView = self.inputPanelBackground.view { + if inputPanelBackgroundView.superview == nil { + self.addSubview(inputPanelBackgroundView) } - if !self.animatingButtons && !(!hasMainVideoTrack && animateIn) { - transition.setAlpha(view: scrubberView, alpha: component.isDisplayingTool || component.isDismissing || component.isInteractingWithEntities || isEditingCaption || isRecordingAdditionalVideo || isEditingTextEntity ? 0.0 : 1.0) - } else if animateIn { - scrubberView.layer.animatePosition(from: CGPoint(x: 0.0, y: 44.0), to: .zero, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, additive: true) - scrubberView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) - scrubberView.layer.animateScale(from: 0.6, to: 1.0, duration: 0.2) + let isVisible = isEditingCaption && inputHeight > 44.0 + transition.setFrame(view: inputPanelBackgroundView, frame: CGRect(origin: CGPoint(x: 0.0, y: isVisible ? availableSize.height - inputPanelBackgroundSize.height : availableSize.height), size: inputPanelBackgroundSize)) + if !self.animatingButtons { + transition.setAlpha(view: inputPanelBackgroundView, alpha: isVisible ? 1.0 : 0.0, delay: isVisible ? 0.0 : 0.4) } } - } else { - if let scrubber = self.scrubber { - self.scrubber = nil - if let scrubberView = scrubber.view { - scrubberView.layer.animatePosition(from: .zero, to: CGPoint(x: 0.0, y: 44.0), duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, additive: true) - scrubberView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { _ in - scrubberView.removeFromSuperview() - }) - scrubberView.layer.animateScale(from: 1.0, to: 0.6, duration: 0.2, removeOnCompletion: false) + + var inputPanelBottomInset: CGFloat = -controlsBottomInset + if inputHeight > 0.0 { + inputPanelBottomInset = inputHeight - environment.safeInsets.bottom + } + let inputPanelFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - inputPanelSize.width) / 2.0), y: availableSize.height - environment.safeInsets.bottom - inputPanelBottomInset - inputPanelSize.height - 3.0), size: inputPanelSize) + if let inputPanelView = self.inputPanel.view { + if inputPanelView.superview == nil { + self.addSubview(inputPanelView) } + transition.setFrame(view: inputPanelView, frame: inputPanelFrame) + transition.setAlpha(view: inputPanelView, alpha: isEditingTextEntity || component.isDisplayingTool || component.isDismissing || component.isInteractingWithEntities ? 0.0 : 1.0) } - } - - let displayTopButtons = !(self.inputPanelExternalState.isEditing || isEditingTextEntity || component.isDisplayingTool) - let saveContentComponent: AnyComponentWithIdentity - if component.hasAppeared { - saveContentComponent = AnyComponentWithIdentity( - id: "animatedIcon", - component: AnyComponent( - LottieAnimationComponent( - animation: LottieAnimationComponent.AnimationItem( - name: "anim_storysave", - mode: .still(position: .begin), - range: nil - ), - colors: ["__allcolors__": .white], - size: CGSize(width: 30.0, height: 30.0) - ).tagged(saveButtonTag) - ) - ) - } else { - saveContentComponent = AnyComponentWithIdentity( - id: "staticIcon", - component: AnyComponent( - BundleIconComponent( - name: "Media Editor/SaveIcon", - tintColor: nil - ) + if let playerState = state.playerState { + let scrubberInset: CGFloat = 9.0 + + let minDuration: Double + let maxDuration: Double + if playerState.isAudioOnly { + minDuration = 5.0 + maxDuration = 15.0 + } else { + minDuration = 1.0 + maxDuration = storyMaxVideoDuration + } + + let previousTrackCount = self.currentVisibleTracks?.count + let visibleTracks = playerState.tracks.filter { $0.visibleInTimeline }.map { MediaScrubberComponent.Track($0) } + self.currentVisibleTracks = visibleTracks + + var scrubberTransition = transition + if let previousTrackCount, previousTrackCount != visibleTracks.count { + scrubberTransition = .easeInOut(duration: 0.2) + } + + let isAudioOnly = playerState.isAudioOnly + let hasMainVideoTrack = playerState.tracks.contains(where: { $0.id == 0 }) + + let scrubber: ComponentView + if let current = self.scrubber { + scrubber = current + } else { + scrubber = ComponentView() + self.scrubber = scrubber + } + + let scrubberSize = scrubber.update( + transition: scrubberTransition, + component: AnyComponent(MediaScrubberComponent( + context: component.context, + style: .editor, + theme: environment.theme, + generationTimestamp: playerState.generationTimestamp, + position: playerState.position, + minDuration: minDuration, + maxDuration: maxDuration, + isPlaying: playerState.isPlaying, + tracks: visibleTracks, + positionUpdated: { [weak mediaEditor] position, apply in + if let mediaEditor { + mediaEditor.seek(position, andPlay: apply) + } + }, + trackTrimUpdated: { [weak mediaEditor] trackId, start, end, updatedEnd, apply in + guard let mediaEditor else { + return + } + let trimRange = start..= upperBound { + start = lowerBound + } else if start < lowerBound { + start = lowerBound + } + } + + mediaEditor.seek(start, andPlay: true) + mediaEditor.play() + } else { + mediaEditor.stop() + } + } + } else if trackId == 1 { + mediaEditor.setAdditionalVideoOffset(offset, apply: apply) + } + }, + trackLongPressed: { [weak self] trackId, sourceView in + guard let self, let controller = self.environment?.controller() as? MediaEditorScreen else { + return + } + controller.node.presentTrackOptions(trackId: trackId, sourceView: sourceView) + } + )), + environment: {}, + containerSize: CGSize(width: previewSize.width - scrubberInset * 2.0, height: availableSize.height) ) - ) - } - - let saveButtonSize = self.saveButton.update( - transition: transition, - component: AnyComponent(CameraButton( - content: saveContentComponent, - action: { [weak self] in - guard let environment = self?.environment, let controller = environment.controller() as? MediaEditorScreen else { - return + + let scrubberFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - scrubberSize.width) / 2.0), y: availableSize.height - environment.safeInsets.bottom - scrubberSize.height + controlsBottomInset - inputPanelSize.height + 3.0), size: scrubberSize) + if let scrubberView = scrubber.view { + var animateIn = false + if scrubberView.superview == nil { + animateIn = true + if let inputPanelBackgroundView = self.inputPanelBackground.view, inputPanelBackgroundView.superview != nil { + self.insertSubview(scrubberView, belowSubview: inputPanelBackgroundView) + } else { + self.addSubview(scrubberView) + } } - guard !controller.node.recording.isActive else { - return + if animateIn { + scrubberView.frame = scrubberFrame + } else { + scrubberTransition.setFrame(view: scrubberView, frame: scrubberFrame) } - if let view = self?.saveButton.findTaggedView(tag: saveButtonTag) as? LottieAnimationComponent.View { - view.playOnce() + if !self.animatingButtons && !(!hasMainVideoTrack && animateIn) { + transition.setAlpha(view: scrubberView, alpha: component.isDisplayingTool || component.isDismissing || component.isInteractingWithEntities || isEditingCaption || isRecordingAdditionalVideo || isEditingTextEntity ? 0.0 : 1.0) + } else if animateIn { + scrubberView.layer.animatePosition(from: CGPoint(x: 0.0, y: 44.0), to: .zero, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, additive: true) + scrubberView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + scrubberView.layer.animateScale(from: 0.6, to: 1.0, duration: 0.2) + } + } + } else { + if let scrubber = self.scrubber { + self.scrubber = nil + if let scrubberView = scrubber.view { + scrubberView.layer.animatePosition(from: .zero, to: CGPoint(x: 0.0, y: 44.0), duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, additive: true) + scrubberView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { _ in + scrubberView.removeFromSuperview() + }) + scrubberView.layer.animateScale(from: 1.0, to: 0.6, duration: 0.2, removeOnCompletion: false) } - controller.requestSave() } - )), - environment: {}, - containerSize: CGSize(width: 44.0, height: 44.0) - ) - let saveButtonFrame = CGRect( - origin: CGPoint(x: availableSize.width - 20.0 - saveButtonSize.width, y: max(environment.statusBarHeight + 10.0, environment.safeInsets.top + 20.0)), - size: saveButtonSize - ) - if let saveButtonView = self.saveButton.view { - if saveButtonView.superview == nil { - setupButtonShadow(saveButtonView) - self.addSubview(saveButtonView) } - - let saveButtonAlpha = component.isSavingAvailable ? topButtonsAlpha : 0.3 - saveButtonView.isUserInteractionEnabled = component.isSavingAvailable - - transition.setPosition(view: saveButtonView, position: saveButtonFrame.center) - transition.setBounds(view: saveButtonView, bounds: CGRect(origin: .zero, size: saveButtonFrame.size)) - transition.setScale(view: saveButtonView, scale: displayTopButtons ? 1.0 : 0.01) - transition.setAlpha(view: saveButtonView, alpha: displayTopButtons && !component.isDismissing && !component.isInteractingWithEntities ? saveButtonAlpha : 0.0) - } - - var topButtonOffsetX: CGFloat = 0.0 - - if let subject = controller.node.subject, case .message = subject { - let isNightTheme = mediaEditor?.values.nightTheme == true - let dayNightContentComponent: AnyComponentWithIdentity + let displayTopButtons = !(self.inputPanelExternalState.isEditing || isEditingTextEntity || component.isDisplayingTool) + + let saveContentComponent: AnyComponentWithIdentity if component.hasAppeared { - dayNightContentComponent = AnyComponentWithIdentity( + saveContentComponent = AnyComponentWithIdentity( id: "animatedIcon", component: AnyComponent( LottieAnimationComponent( animation: LottieAnimationComponent.AnimationItem( - name: isNightTheme ? "anim_sun" : "anim_sun_reverse", - mode: state.dayNightDidChange ? .animating(loop: false) : .still(position: .end) + name: "anim_storysave", + mode: .still(position: .begin), + range: nil ), colors: ["__allcolors__": .white], size: CGSize(width: 30.0, height: 30.0) - ).tagged(dayNightButtonTag) + ).tagged(saveButtonTag) ) ) } else { - dayNightContentComponent = AnyComponentWithIdentity( + saveContentComponent = AnyComponentWithIdentity( id: "staticIcon", component: AnyComponent( BundleIconComponent( - name: "Media Editor/MuteIcon", + name: "Media Editor/SaveIcon", tintColor: nil ) ) ) } - let dayNightButtonSize = self.dayNightButton.update( + let saveButtonSize = self.saveButton.update( transition: transition, component: AnyComponent(CameraButton( - content: dayNightContentComponent, - action: { [weak self, weak state, weak mediaEditor] in + content: saveContentComponent, + action: { [weak self] in guard let environment = self?.environment, let controller = environment.controller() as? MediaEditorScreen else { return } guard !controller.node.recording.isActive else { return } - - if let mediaEditor { - state?.dayNightDidChange = true - - if let snapshotView = controller.node.previewContainerView.snapshotView(afterScreenUpdates: false) { - controller.node.previewContainerView.addSubview(snapshotView) - - snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, delay: 0.1, removeOnCompletion: false, completion: { _ in - snapshotView.removeFromSuperview() - }) - } - - Queue.mainQueue().after(0.1) { - mediaEditor.toggleNightTheme() - controller.node.entitiesView.eachView { view in - if let stickerEntityView = view as? DrawingStickerEntityView { - stickerEntityView.isNightTheme = mediaEditor.values.nightTheme - } - } - } + if let view = self?.saveButton.findTaggedView(tag: saveButtonTag) as? LottieAnimationComponent.View { + view.playOnce() } + controller.requestSave() } )), environment: {}, containerSize: CGSize(width: 44.0, height: 44.0) ) - let dayNightButtonFrame = CGRect( - origin: CGPoint(x: availableSize.width - 20.0 - dayNightButtonSize.width - 50.0, y: max(environment.statusBarHeight + 10.0, environment.safeInsets.top + 20.0)), - size: dayNightButtonSize + let saveButtonFrame = CGRect( + origin: CGPoint(x: availableSize.width - 20.0 - saveButtonSize.width, y: max(environment.statusBarHeight + 10.0, environment.safeInsets.top + 20.0)), + size: saveButtonSize ) - if let dayNightButtonView = self.dayNightButton.view { - if dayNightButtonView.superview == nil { - setupButtonShadow(dayNightButtonView) - self.addSubview(dayNightButtonView) - - dayNightButtonView.layer.animateAlpha(from: 0.0, to: dayNightButtonView.alpha, duration: self.animatingButtons ? 0.1 : 0.2) - dayNightButtonView.layer.animateScale(from: 0.4, to: 1.0, duration: self.animatingButtons ? 0.1 : 0.2) + if let saveButtonView = self.saveButton.view { + if saveButtonView.superview == nil { + setupButtonShadow(saveButtonView) + self.addSubview(saveButtonView) } - transition.setPosition(view: dayNightButtonView, position: dayNightButtonFrame.center) - transition.setBounds(view: dayNightButtonView, bounds: CGRect(origin: .zero, size: dayNightButtonFrame.size)) - transition.setScale(view: dayNightButtonView, scale: displayTopButtons ? 1.0 : 0.01) - transition.setAlpha(view: dayNightButtonView, alpha: displayTopButtons && !component.isDismissing && !component.isInteractingWithEntities ? topButtonsAlpha : 0.0) - } - - topButtonOffsetX += 50.0 - } else { - if let dayNightButtonView = self.dayNightButton.view, dayNightButtonView.superview != nil { - dayNightButtonView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak dayNightButtonView] _ in - dayNightButtonView?.removeFromSuperview() - }) - dayNightButtonView.layer.animateScale(from: 1.0, to: 0.01, duration: 0.2, removeOnCompletion: false) + + let saveButtonAlpha = component.isSavingAvailable ? topButtonsAlpha : 0.3 + saveButtonView.isUserInteractionEnabled = component.isSavingAvailable + + transition.setPosition(view: saveButtonView, position: saveButtonFrame.center) + transition.setBounds(view: saveButtonView, bounds: CGRect(origin: .zero, size: saveButtonFrame.size)) + transition.setScale(view: saveButtonView, scale: displayTopButtons ? 1.0 : 0.01) + transition.setAlpha(view: saveButtonView, alpha: displayTopButtons && !component.isDismissing && !component.isInteractingWithEntities ? saveButtonAlpha : 0.0) } - } - - if let playerState = state.playerState, playerState.hasAudio { - let isVideoMuted = mediaEditor?.values.videoIsMuted ?? false + + var topButtonOffsetX: CGFloat = 0.0 - let muteContentComponent: AnyComponentWithIdentity - if component.hasAppeared { - muteContentComponent = AnyComponentWithIdentity( - id: "animatedIcon", - component: AnyComponent( - LottieAnimationComponent( - animation: LottieAnimationComponent.AnimationItem( - name: "anim_storymute", - mode: state.muteDidChange ? .animating(loop: false) : .still(position: .begin), - range: isVideoMuted ? (0.0, 0.5) : (0.5, 1.0) - ), - colors: ["__allcolors__": .white], - size: CGSize(width: 30.0, height: 30.0) - ).tagged(muteButtonTag) + if let subject = controller.node.subject, case .message = subject { + let isNightTheme = mediaEditor?.values.nightTheme == true + + let dayNightContentComponent: AnyComponentWithIdentity + if component.hasAppeared { + dayNightContentComponent = AnyComponentWithIdentity( + id: "animatedIcon", + component: AnyComponent( + LottieAnimationComponent( + animation: LottieAnimationComponent.AnimationItem( + name: isNightTheme ? "anim_sun" : "anim_sun_reverse", + mode: state.dayNightDidChange ? .animating(loop: false) : .still(position: .end) + ), + colors: ["__allcolors__": .white], + size: CGSize(width: 30.0, height: 30.0) + ).tagged(dayNightButtonTag) + ) ) - ) - } else { - muteContentComponent = AnyComponentWithIdentity( - id: "staticIcon", - component: AnyComponent( - BundleIconComponent( - name: "Media Editor/MuteIcon", - tintColor: nil + } else { + dayNightContentComponent = AnyComponentWithIdentity( + id: "staticIcon", + component: AnyComponent( + BundleIconComponent( + name: "Media Editor/MuteIcon", + tintColor: nil + ) ) ) + } + + let dayNightButtonSize = self.dayNightButton.update( + transition: transition, + component: AnyComponent(CameraButton( + content: dayNightContentComponent, + action: { [weak self, weak state, weak mediaEditor] in + guard let environment = self?.environment, let controller = environment.controller() as? MediaEditorScreen else { + return + } + guard !controller.node.recording.isActive else { + return + } + + if let mediaEditor { + state?.dayNightDidChange = true + + if let snapshotView = controller.node.previewContainerView.snapshotView(afterScreenUpdates: false) { + controller.node.previewContainerView.addSubview(snapshotView) + + snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, delay: 0.1, removeOnCompletion: false, completion: { _ in + snapshotView.removeFromSuperview() + }) + } + + Queue.mainQueue().after(0.1) { + mediaEditor.toggleNightTheme() + controller.node.entitiesView.eachView { view in + if let stickerEntityView = view as? DrawingStickerEntityView { + stickerEntityView.isNightTheme = mediaEditor.values.nightTheme + } + } + } + } + } + )), + environment: {}, + containerSize: CGSize(width: 44.0, height: 44.0) + ) + let dayNightButtonFrame = CGRect( + origin: CGPoint(x: availableSize.width - 20.0 - dayNightButtonSize.width - 50.0, y: max(environment.statusBarHeight + 10.0, environment.safeInsets.top + 20.0)), + size: dayNightButtonSize ) + if let dayNightButtonView = self.dayNightButton.view { + if dayNightButtonView.superview == nil { + setupButtonShadow(dayNightButtonView) + self.addSubview(dayNightButtonView) + + dayNightButtonView.layer.animateAlpha(from: 0.0, to: dayNightButtonView.alpha, duration: self.animatingButtons ? 0.1 : 0.2) + dayNightButtonView.layer.animateScale(from: 0.4, to: 1.0, duration: self.animatingButtons ? 0.1 : 0.2) + } + transition.setPosition(view: dayNightButtonView, position: dayNightButtonFrame.center) + transition.setBounds(view: dayNightButtonView, bounds: CGRect(origin: .zero, size: dayNightButtonFrame.size)) + transition.setScale(view: dayNightButtonView, scale: displayTopButtons ? 1.0 : 0.01) + transition.setAlpha(view: dayNightButtonView, alpha: displayTopButtons && !component.isDismissing && !component.isInteractingWithEntities ? topButtonsAlpha : 0.0) + } + + topButtonOffsetX += 50.0 + } else { + if let dayNightButtonView = self.dayNightButton.view, dayNightButtonView.superview != nil { + dayNightButtonView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak dayNightButtonView] _ in + dayNightButtonView?.removeFromSuperview() + }) + dayNightButtonView.layer.animateScale(from: 1.0, to: 0.01, duration: 0.2, removeOnCompletion: false) + } } - let muteButtonSize = self.muteButton.update( - transition: transition, - component: AnyComponent(CameraButton( - content: muteContentComponent, - action: { [weak self, weak state, weak mediaEditor] in - guard let environment = self?.environment, let controller = environment.controller() as? MediaEditorScreen else { - return - } - guard !controller.node.recording.isActive else { - return - } - - if let mediaEditor { - state?.muteDidChange = true - let isMuted = !mediaEditor.values.videoIsMuted - mediaEditor.setVideoIsMuted(isMuted) - state?.updated() + if let playerState = state.playerState, playerState.hasAudio { + let isVideoMuted = mediaEditor?.values.videoIsMuted ?? false + + let muteContentComponent: AnyComponentWithIdentity + if component.hasAppeared { + muteContentComponent = AnyComponentWithIdentity( + id: "animatedIcon", + component: AnyComponent( + LottieAnimationComponent( + animation: LottieAnimationComponent.AnimationItem( + name: "anim_storymute", + mode: state.muteDidChange ? .animating(loop: false) : .still(position: .begin), + range: isVideoMuted ? (0.0, 0.5) : (0.5, 1.0) + ), + colors: ["__allcolors__": .white], + size: CGSize(width: 30.0, height: 30.0) + ).tagged(muteButtonTag) + ) + ) + } else { + muteContentComponent = AnyComponentWithIdentity( + id: "staticIcon", + component: AnyComponent( + BundleIconComponent( + name: "Media Editor/MuteIcon", + tintColor: nil + ) + ) + ) + } + + let muteButtonSize = self.muteButton.update( + transition: transition, + component: AnyComponent(CameraButton( + content: muteContentComponent, + action: { [weak self, weak state, weak mediaEditor] in + guard let environment = self?.environment, let controller = environment.controller() as? MediaEditorScreen else { + return + } + guard !controller.node.recording.isActive else { + return + } - controller.node.presentMutedTooltip() + if let mediaEditor { + state?.muteDidChange = true + let isMuted = !mediaEditor.values.videoIsMuted + mediaEditor.setVideoIsMuted(isMuted) + state?.updated() + + controller.node.presentMutedTooltip() + } } - } - )), - environment: {}, - containerSize: CGSize(width: 44.0, height: 44.0) - ) - let muteButtonFrame = CGRect( - origin: CGPoint(x: availableSize.width - 20.0 - muteButtonSize.width - 50.0 - topButtonOffsetX, y: max(environment.statusBarHeight + 10.0, environment.safeInsets.top + 20.0)), - size: muteButtonSize - ) - if let muteButtonView = self.muteButton.view { - if muteButtonView.superview == nil { - setupButtonShadow(muteButtonView) - self.addSubview(muteButtonView) - - muteButtonView.layer.animateAlpha(from: 0.0, to: muteButtonView.alpha, duration: self.animatingButtons ? 0.1 : 0.2) - muteButtonView.layer.animateScale(from: 0.4, to: 1.0, duration: self.animatingButtons ? 0.1 : 0.2) + )), + environment: {}, + containerSize: CGSize(width: 44.0, height: 44.0) + ) + let muteButtonFrame = CGRect( + origin: CGPoint(x: availableSize.width - 20.0 - muteButtonSize.width - 50.0 - topButtonOffsetX, y: max(environment.statusBarHeight + 10.0, environment.safeInsets.top + 20.0)), + size: muteButtonSize + ) + if let muteButtonView = self.muteButton.view { + if muteButtonView.superview == nil { + setupButtonShadow(muteButtonView) + self.addSubview(muteButtonView) + + muteButtonView.layer.animateAlpha(from: 0.0, to: muteButtonView.alpha, duration: self.animatingButtons ? 0.1 : 0.2) + muteButtonView.layer.animateScale(from: 0.4, to: 1.0, duration: self.animatingButtons ? 0.1 : 0.2) + } + transition.setPosition(view: muteButtonView, position: muteButtonFrame.center) + transition.setBounds(view: muteButtonView, bounds: CGRect(origin: .zero, size: muteButtonFrame.size)) + transition.setScale(view: muteButtonView, scale: displayTopButtons ? 1.0 : 0.01) + transition.setAlpha(view: muteButtonView, alpha: displayTopButtons && !component.isDismissing && !component.isInteractingWithEntities ? topButtonsAlpha : 0.0) + } + + topButtonOffsetX += 50.0 + } else { + if let muteButtonView = self.muteButton.view, muteButtonView.superview != nil { + muteButtonView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak muteButtonView] _ in + muteButtonView?.removeFromSuperview() + }) + muteButtonView.layer.animateScale(from: 1.0, to: 0.01, duration: 0.2, removeOnCompletion: false) } - transition.setPosition(view: muteButtonView, position: muteButtonFrame.center) - transition.setBounds(view: muteButtonView, bounds: CGRect(origin: .zero, size: muteButtonFrame.size)) - transition.setScale(view: muteButtonView, scale: displayTopButtons ? 1.0 : 0.01) - transition.setAlpha(view: muteButtonView, alpha: displayTopButtons && !component.isDismissing && !component.isInteractingWithEntities ? topButtonsAlpha : 0.0) } - topButtonOffsetX += 50.0 - } else { - if let muteButtonView = self.muteButton.view, muteButtonView.superview != nil { - muteButtonView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak muteButtonView] _ in - muteButtonView?.removeFromSuperview() - }) - muteButtonView.layer.animateScale(from: 1.0, to: 0.01, duration: 0.2, removeOnCompletion: false) - } - } - - if let playerState = state.playerState { - let playbackContentComponent: AnyComponentWithIdentity - if component.hasAppeared { - playbackContentComponent = AnyComponentWithIdentity( - id: "animatedIcon", - component: AnyComponent( - LottieAnimationComponent( - animation: LottieAnimationComponent.AnimationItem( - name: "anim_storyplayback", - mode: state.playbackDidChange ? .animating(loop: false) : .still(position: .end), - range: playerState.isPlaying ? (0.5, 1.0) : (0.0, 0.5) - ), - colors: ["__allcolors__": .white], - size: CGSize(width: 30.0, height: 30.0) - ).tagged(playbackButtonTag) + if let playerState = state.playerState { + let playbackContentComponent: AnyComponentWithIdentity + if component.hasAppeared { + playbackContentComponent = AnyComponentWithIdentity( + id: "animatedIcon", + component: AnyComponent( + LottieAnimationComponent( + animation: LottieAnimationComponent.AnimationItem( + name: "anim_storyplayback", + mode: state.playbackDidChange ? .animating(loop: false) : .still(position: .end), + range: playerState.isPlaying ? (0.5, 1.0) : (0.0, 0.5) + ), + colors: ["__allcolors__": .white], + size: CGSize(width: 30.0, height: 30.0) + ).tagged(playbackButtonTag) + ) ) - ) - } else { - playbackContentComponent = AnyComponentWithIdentity( - id: "staticIcon", - component: AnyComponent( - BundleIconComponent( - name: playerState.isPlaying ? "Media Editor/Pause" : "Media Editor/Play", - tintColor: nil + } else { + playbackContentComponent = AnyComponentWithIdentity( + id: "staticIcon", + component: AnyComponent( + BundleIconComponent( + name: playerState.isPlaying ? "Media Editor/Pause" : "Media Editor/Play", + tintColor: nil + ) ) ) + } + + let playbackButtonSize = self.playbackButton.update( + transition: transition, + component: AnyComponent(CameraButton( + content: playbackContentComponent, + action: { [weak self, weak mediaEditor, weak state] in + guard let environment = self?.environment, let controller = environment.controller() as? MediaEditorScreen else { + return + } + guard !controller.node.recording.isActive else { + return + } + if let mediaEditor { + state?.playbackDidChange = true + mediaEditor.togglePlayback() + } + } + )), + environment: {}, + containerSize: CGSize(width: 44.0, height: 44.0) + ) + let playbackButtonFrame = CGRect( + origin: CGPoint(x: availableSize.width - 20.0 - playbackButtonSize.width - 50.0 - topButtonOffsetX, y: max(environment.statusBarHeight + 10.0, environment.safeInsets.top + 20.0)), + size: playbackButtonSize ) + if let playbackButtonView = self.playbackButton.view { + if playbackButtonView.superview == nil { + setupButtonShadow(playbackButtonView) + self.addSubview(playbackButtonView) + + playbackButtonView.layer.animateAlpha(from: 0.0, to: playbackButtonView.alpha, duration: self.animatingButtons ? 0.1 : 0.2) + playbackButtonView.layer.animateScale(from: 0.4, to: 1.0, duration: self.animatingButtons ? 0.1 : 0.2) + } + transition.setPosition(view: playbackButtonView, position: playbackButtonFrame.center) + transition.setBounds(view: playbackButtonView, bounds: CGRect(origin: .zero, size: playbackButtonFrame.size)) + transition.setScale(view: playbackButtonView, scale: displayTopButtons ? 1.0 : 0.01) + transition.setAlpha(view: playbackButtonView, alpha: displayTopButtons && !component.isDismissing && !component.isInteractingWithEntities ? topButtonsAlpha : 0.0) + } + + topButtonOffsetX += 50.0 + } else { + if let playbackButtonView = self.playbackButton.view, playbackButtonView.superview != nil { + playbackButtonView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak playbackButtonView] _ in + playbackButtonView?.removeFromSuperview() + }) + playbackButtonView.layer.animateScale(from: 1.0, to: 0.01, duration: 0.2, removeOnCompletion: false) + } } - let playbackButtonSize = self.playbackButton.update( + let switchCameraButtonSize = self.switchCameraButton.update( transition: transition, - component: AnyComponent(CameraButton( - content: playbackContentComponent, - action: { [weak self, weak mediaEditor, weak state] in - guard let environment = self?.environment, let controller = environment.controller() as? MediaEditorScreen else { - return - } - guard !controller.node.recording.isActive else { - return - } - if let mediaEditor { - state?.playbackDidChange = true - mediaEditor.togglePlayback() + component: AnyComponent(Button( + content: AnyComponent( + FlipButtonContentComponent(tag: switchCameraButtonTag) + ), + action: { [weak self] in + if let self, let environment = self.environment, let controller = environment.controller() as? MediaEditorScreen { + controller.node.recording.togglePosition() + + if let view = self.switchCameraButton.findTaggedView(tag: switchCameraButtonTag) as? FlipButtonContentComponent.View { + view.playAnimation() + } } } - )), + ).withIsExclusive(false)), environment: {}, - containerSize: CGSize(width: 44.0, height: 44.0) + containerSize: CGSize(width: 48.0, height: 48.0) ) - let playbackButtonFrame = CGRect( - origin: CGPoint(x: availableSize.width - 20.0 - playbackButtonSize.width - 50.0 - topButtonOffsetX, y: max(environment.statusBarHeight + 10.0, environment.safeInsets.top + 20.0)), - size: playbackButtonSize + let switchCameraButtonFrame = CGRect( + origin: CGPoint(x: 12.0, y: max(environment.statusBarHeight + 10.0, inputPanelFrame.minY - switchCameraButtonSize.height - 3.0)), + size: switchCameraButtonSize ) - if let playbackButtonView = self.playbackButton.view { - if playbackButtonView.superview == nil { - setupButtonShadow(playbackButtonView) - self.addSubview(playbackButtonView) - - playbackButtonView.layer.animateAlpha(from: 0.0, to: playbackButtonView.alpha, duration: self.animatingButtons ? 0.1 : 0.2) - playbackButtonView.layer.animateScale(from: 0.4, to: 1.0, duration: self.animatingButtons ? 0.1 : 0.2) + if let switchCameraButtonView = self.switchCameraButton.view { + if switchCameraButtonView.superview == nil { + self.addSubview(switchCameraButtonView) } - transition.setPosition(view: playbackButtonView, position: playbackButtonFrame.center) - transition.setBounds(view: playbackButtonView, bounds: CGRect(origin: .zero, size: playbackButtonFrame.size)) - transition.setScale(view: playbackButtonView, scale: displayTopButtons ? 1.0 : 0.01) - transition.setAlpha(view: playbackButtonView, alpha: displayTopButtons && !component.isDismissing && !component.isInteractingWithEntities ? topButtonsAlpha : 0.0) + transition.setPosition(view: switchCameraButtonView, position: switchCameraButtonFrame.center) + transition.setBounds(view: switchCameraButtonView, bounds: CGRect(origin: .zero, size: switchCameraButtonFrame.size)) + transition.setScale(view: switchCameraButtonView, scale: isRecordingAdditionalVideo ? 1.0 : 0.01) + transition.setAlpha(view: switchCameraButtonView, alpha: isRecordingAdditionalVideo ? 1.0 : 0.0) } - topButtonOffsetX += 50.0 - } else { - if let playbackButtonView = self.playbackButton.view, playbackButtonView.superview != nil { - playbackButtonView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak playbackButtonView] _ in - playbackButtonView?.removeFromSuperview() - }) - playbackButtonView.layer.animateScale(from: 1.0, to: 0.01, duration: 0.2, removeOnCompletion: false) - } - } - - let switchCameraButtonSize = self.switchCameraButton.update( - transition: transition, - component: AnyComponent(Button( - content: AnyComponent( - FlipButtonContentComponent(tag: switchCameraButtonTag) - ), - action: { [weak self] in - if let self, let environment = self.environment, let controller = environment.controller() as? MediaEditorScreen { - controller.node.recording.togglePosition() - - if let view = self.switchCameraButton.findTaggedView(tag: switchCameraButtonTag) as? FlipButtonContentComponent.View { - view.playAnimation() - } - } - } - ).withIsExclusive(false)), - environment: {}, - containerSize: CGSize(width: 48.0, height: 48.0) - ) - let switchCameraButtonFrame = CGRect( - origin: CGPoint(x: 12.0, y: max(environment.statusBarHeight + 10.0, inputPanelFrame.minY - switchCameraButtonSize.height - 3.0)), - size: switchCameraButtonSize - ) - if let switchCameraButtonView = self.switchCameraButton.view { - if switchCameraButtonView.superview == nil { - self.addSubview(switchCameraButtonView) - } - transition.setPosition(view: switchCameraButtonView, position: switchCameraButtonFrame.center) - transition.setBounds(view: switchCameraButtonView, bounds: CGRect(origin: .zero, size: switchCameraButtonFrame.size)) - transition.setScale(view: switchCameraButtonView, scale: isRecordingAdditionalVideo ? 1.0 : 0.01) - transition.setAlpha(view: switchCameraButtonView, alpha: isRecordingAdditionalVideo ? 1.0 : 0.0) } let textCancelButtonSize = self.textCancelButton.update( @@ -1977,6 +2049,11 @@ let storyDimensions = CGSize(width: 1080.0, height: 1920.0) let storyMaxVideoDuration: Double = 60.0 public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate { + public enum Mode { + case storyEditor + case stickerEditor + } + public enum TransitionIn { public final class GalleryTransitionIn { public weak var sourceView: UIView? @@ -2057,6 +2134,8 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate private let gradientView: UIImageView private var gradientColorsDisposable: Disposable? + private let stickerTransparentView: UIImageView + fileprivate let entitiesContainerView: UIView let entitiesView: DrawingEntitiesView fileprivate let selectionContainerView: DrawingSelectionContainerView @@ -2085,6 +2164,9 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate private var isDismissed = false private var isDismissBySwipeSuppressed = false + fileprivate var canCutout = false + fileprivate var isCutout = false + private (set) var hasAnyChanges = false private var playbackPositionDisposable: Disposable? @@ -2122,9 +2204,11 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate } self.gradientView = UIImageView() + self.stickerTransparentView = UIImageView() + self.stickerTransparentView.clipsToBounds = true self.entitiesContainerView = UIView(frame: CGRect(origin: .zero, size: storyDimensions)) - self.entitiesView = DrawingEntitiesView(context: controller.context, size: storyDimensions, hasBin: true) + self.entitiesView = DrawingEntitiesView(context: controller.context, size: storyDimensions, hasBin: true, isStickerEditor: controller.mode == .stickerEditor) self.entitiesView.getEntityCenterPosition = { return CGPoint(x: storyDimensions.width / 2.0, y: storyDimensions.height / 2.0) } @@ -2132,6 +2216,10 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate return UIEdgeInsets(top: 160.0, left: 36.0, bottom: storyDimensions.height - 160.0, right: storyDimensions.width - 36.0) } self.previewView = MediaEditorPreviewView(frame: .zero) + if case .stickerEditor = controller.mode { + self.previewView.isOpaque = false + self.previewView.backgroundColor = .clear + } self.drawingView = DrawingView(size: storyDimensions) self.drawingView.isUserInteractionEnabled = false @@ -2147,7 +2235,31 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate self.view.addSubview(self.backgroundDimView) self.view.addSubview(self.containerView) self.containerView.addSubview(self.previewContainerView) - self.previewContainerView.addSubview(self.gradientView) + + if case .stickerEditor = controller.mode { + let rowsCount = 40 + self.stickerTransparentView.image = generateImage(CGSize(width: rowsCount, height: rowsCount), opaque: true, scale: 1.0, rotatedContext: { size, context in + context.setFillColor(UIColor.black.cgColor) + context.fill(CGRect(origin: .zero, size: size)) + context.setFillColor(UIColor(rgb: 0x2b2b2d).cgColor) + + for row in 0 ..< rowsCount { + for column in 0 ..< rowsCount { + if (row + column).isMultiple(of: 2) { + context.addRect(CGRect(x: column, y: row, width: 1, height: 1)) + } + } + } + context.fillPath() + }) + self.stickerTransparentView.layer.magnificationFilter = .nearest + self.stickerTransparentView.layer.shouldRasterize = true + self.stickerTransparentView.layer.rasterizationScale = UIScreenScale + self.previewContainerView.addSubview(self.stickerTransparentView) + } else { + self.previewContainerView.addSubview(self.gradientView) + } + self.previewContainerView.addSubview(self.previewView) self.previewContainerView.addSubview(self.entitiesContainerView) self.entitiesContainerView.addSubview(self.entitiesView) @@ -2275,8 +2387,12 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate } private func setup(with subject: MediaEditorScreen.Subject) { - self.actualSubject = subject + guard let controller = self.controller else { + return + } + self.actualSubject = subject + var effectiveSubject = subject if case let .draft(draft, _ ) = subject { for entity in draft.values.entities { @@ -2288,10 +2404,6 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate } self.subject = effectiveSubject - guard let controller = self.controller else { - return - } - Queue.mainQueue().justDispatch { controller.setupAudioSessionIfNeeded() } @@ -2321,10 +2433,19 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate let fittedSize = mediaDimensions.cgSize.fitted(CGSize(width: maxSide, height: maxSide)) let mediaEntity = DrawingMediaEntity(size: fittedSize) mediaEntity.position = CGPoint(x: storyDimensions.width / 2.0, y: storyDimensions.height / 2.0) - if fittedSize.height > fittedSize.width { - mediaEntity.scale = max(storyDimensions.width / fittedSize.width, storyDimensions.height / fittedSize.height) - } else { - mediaEntity.scale = storyDimensions.width / fittedSize.width + switch controller.mode { + case .storyEditor: + if fittedSize.height > fittedSize.width { + mediaEntity.scale = max(storyDimensions.width / fittedSize.width, storyDimensions.height / fittedSize.height) + } else { + mediaEntity.scale = storyDimensions.width / fittedSize.width + } + case .stickerEditor: + if fittedSize.height > fittedSize.width { + mediaEntity.scale = storyDimensions.width / fittedSize.width + } else { + mediaEntity.scale = storyDimensions.width / fittedSize.height + } } let initialPosition = mediaEntity.position @@ -2370,7 +2491,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate } } - let mediaEditor = MediaEditor(context: self.context, subject: effectiveSubject.editorSubject, values: initialValues, hasHistogram: true) + let mediaEditor = MediaEditor(context: self.context, mode: controller.mode == .stickerEditor ? .sticker : .default, subject: effectiveSubject.editorSubject, values: initialValues, hasHistogram: true) if let initialVideoPosition = controller.initialVideoPosition { mediaEditor.seek(initialVideoPosition, andPlay: true) } @@ -2389,6 +2510,20 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate } controller.requestLayout(transition: .animated(duration: 0.25, curve: .easeInOut)) } + } + mediaEditor.canCutoutUpdated = { [weak self] canCutout in + guard let self, let controller = self.controller else { + return + } + self.canCutout = canCutout + controller.requestLayout(transition: .animated(duration: 0.25, curve: .easeInOut)) + } + mediaEditor.isCutoutUpdated = { [weak self] isCutout in + guard let self else { + return + } + self.isCutout = isCutout + self.requestLayout(forceUpdate: true, transition: .immediate) } if case .message = effectiveSubject { @@ -2765,6 +2900,12 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate hasSwipeToDismiss = true } } + + var hasSwipeToEnhance = true + if case .stickerEditor = controller.mode { + hasSwipeToDismiss = false + hasSwipeToEnhance = false + } let translation = gestureRecognizer.translation(in: self.view) let velocity = gestureRecognizer.velocity(in: self.view) @@ -2776,7 +2917,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate self.isDismissBySwipeSuppressed = controller.isEligibleForDraft() controller.requestLayout(transition: .animated(duration: 0.25, curve: .easeInOut)) } - } else if abs(translation.x) > 10.0 && !self.isDismissing && !self.isEnhancing && self.canEnhance { + } else if abs(translation.x) > 10.0 && !self.isDismissing && !self.isEnhancing && self.canEnhance && hasSwipeToEnhance { self.isEnhancing = true controller.requestLayout(transition: .animated(duration: 0.3, curve: .easeInOut)) } @@ -2855,7 +2996,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate } @objc func handleTap(_ gestureRecognizer: UITapGestureRecognizer) { - guard !self.recording.isActive else { + guard !self.recording.isActive, let controller = self.controller else { return } let location = gestureRecognizer.location(in: self.view) @@ -2872,7 +3013,9 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate if let layout = self.validLayout, (layout.inputHeight ?? 0.0) > 0.0 { self.view.endEditing(true) } else { - self.insertTextEntity() + if case .storyEditor = controller.mode { + self.insertTextEntity() + } } } } @@ -2900,16 +3043,29 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate } private func setupTransitionImage(_ image: UIImage) { + guard let controller = self.controller else { + return + } self.previewContainerView.alpha = 1.0 let transitionInView = UIImageView(image: image) transitionInView.contentMode = .scaleAspectFill var initialScale: CGFloat - if image.size.height > image.size.width { - initialScale = max(self.previewContainerView.bounds.width / image.size.width, self.previewContainerView.bounds.height / image.size.height) - } else { - initialScale = self.previewContainerView.bounds.width / image.size.width + switch controller.mode { + case .storyEditor: + if image.size.height > image.size.width { + initialScale = max(self.previewContainerView.bounds.width / image.size.width, self.previewContainerView.bounds.height / image.size.height) + } else { + initialScale = self.previewContainerView.bounds.width / image.size.width + } + case .stickerEditor: + if image.size.height > image.size.width { + initialScale = self.previewContainerView.bounds.width / image.size.width + } else { + initialScale = self.previewContainerView.bounds.width / image.size.height + } } + transitionInView.center = CGPoint(x: self.previewContainerView.bounds.width / 2.0, y: self.previewContainerView.bounds.height / 2.0) transitionInView.transform = CGAffineTransformMakeScale(initialScale, initialScale) self.previewContainerView.addSubview(transitionInView) @@ -3199,22 +3355,22 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate } } - func animateOutToTool() { + func animateOutToTool(inPlace: Bool = false) { self.isDisplayingTool = true let transition: Transition = .easeInOut(duration: 0.2) if let view = self.componentHost.view as? MediaEditorScreenComponent.View { - view.animateOutToTool(transition: transition) + view.animateOutToTool(inPlace: inPlace, transition: transition) } self.requestUpdate(transition: transition) } - func animateInFromTool() { + func animateInFromTool(inPlace: Bool = false) { self.isDisplayingTool = false let transition: Transition = .easeInOut(duration: 0.2) if let view = self.componentHost.view as? MediaEditorScreenComponent.View { - view.animateInFromTool(transition: transition) + view.animateInFromTool(inPlace: inPlace, transition: transition) } self.requestUpdate(transition: transition) } @@ -3378,7 +3534,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate let entity = DrawingStickerEntity(content: .image(updatedImage, .rectangle)) entity.canCutOut = false - let _ = (cutoutStickerImage(from: image) + let _ = (cutoutStickerImage(from: image, onlyCheck: true) |> deliverOnMainQueue).start(next: { [weak entity] result in if result != nil, let entity { entity.canCutOut = true @@ -3853,6 +4009,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate bottom: bottomInset, right: layout.safeInsets.right ), + additionalInsets: layout.additionalInsets, inputHeight: layoutInputHeight, metrics: layout.metrics, deviceMetrics: layout.deviceMetrics, @@ -4079,6 +4236,33 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate self.controller?.present(controller, in: .window(.root)) self.animateOutToTool() } + }, + openCutout: { [weak self] in + if let self, let mediaEditor = self.mediaEditor { + if self.entitiesView.hasSelection { + self.entitiesView.selectEntity(nil) + } + + if controller.node.isCutout { + let snapshotView = self.previewView.snapshotView(afterScreenUpdates: false) + if let snapshotView { + self.previewView.superview?.addSubview(snapshotView) + } + self.previewView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3, completion: { _ in + snapshotView?.removeFromSuperview() + }) + mediaEditor.removeSeparationMask() + } else { + let controller = MediaCutoutScreen(context: self.context, mediaEditor: mediaEditor, previewView: self.previewView) + controller.dismissed = { [weak self] in + if let self { + self.animateInFromTool(inPlace: true) + } + } + self.controller?.present(controller, in: .window(.root)) + self.animateOutToTool(inPlace: true) + } + } } ) ), @@ -4172,6 +4356,10 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate transition.setFrame(view: self.selectionContainerView, frame: CGRect(origin: .zero, size: previewFrame.size)) + let stickerFrameWidth = floor(previewSize.width * 0.97) + transition.setFrame(view: self.stickerTransparentView, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((previewSize.width - stickerFrameWidth) / 2.0), y: floorToScreenPixels((previewSize.height - stickerFrameWidth) / 2.0)), size: CGSize(width: stickerFrameWidth, height: stickerFrameWidth))) + self.stickerTransparentView.layer.cornerRadius = stickerFrameWidth / 8.0 + self.interaction?.containerLayoutUpdated(layout: layout, transition: transition) var layout = layout @@ -4292,6 +4480,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate } let context: AccountContext + let mode: Mode let subject: Signal let isEditingStory: Bool fileprivate let customTarget: EnginePeer.Id? @@ -4322,6 +4511,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate public init( context: AccountContext, + mode: Mode, subject: Signal, customTarget: EnginePeer.Id? = nil, isEditing: Bool = false, @@ -4335,6 +4525,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate completion: @escaping (MediaEditorScreen.Result, @escaping (@escaping () -> Void) -> Void) -> Void ) { self.context = context + self.mode = mode self.subject = subject self.customTarget = customTarget self.isEditingStory = isEditing @@ -4916,35 +5107,46 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate } let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } - let title: String - let save: String - if case .draft = self.node.actualSubject { - title = presentationData.strings.Story_Editor_DraftDiscardDraft - save = presentationData.strings.Story_Editor_DraftKeepDraft - } else { + var title: String + var text: String + var save: String? + switch self.mode { + case .storyEditor: + if case .draft = self.node.actualSubject { + title = presentationData.strings.Story_Editor_DraftDiscardDraft + save = presentationData.strings.Story_Editor_DraftKeepDraft + } else { + title = presentationData.strings.Story_Editor_DraftDiscardMedia + save = presentationData.strings.Story_Editor_DraftKeepMedia + } + text = presentationData.strings.Story_Editor_DraftDiscaedText + case .stickerEditor: title = presentationData.strings.Story_Editor_DraftDiscardMedia - save = presentationData.strings.Story_Editor_DraftKeepMedia + text = presentationData.strings.Story_Editor_DiscardText + } + + var actions: [TextAlertAction] = [] + actions.append(TextAlertAction(type: .destructiveAction, title: presentationData.strings.Story_Editor_DraftDiscard, action: { [weak self] in + if let self { + self.requestDismiss(saveDraft: false, animated: true) + } + })) + if let save { + actions.append(TextAlertAction(type: .genericAction, title: save, action: { [weak self] in + if let self { + self.requestDismiss(saveDraft: true, animated: true) + } + })) } + actions.append(TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: { + + })) let controller = textAlertController( context: self.context, forceTheme: defaultDarkPresentationTheme, title: title, - text: presentationData.strings.Story_Editor_DraftDiscaedText, - actions: [ - TextAlertAction(type: .destructiveAction, title: presentationData.strings.Story_Editor_DraftDiscard, action: { [weak self] in - if let self { - self.requestDismiss(saveDraft: false, animated: true) - } - }), - TextAlertAction(type: .genericAction, title: save, action: { [weak self] in - if let self { - self.requestDismiss(saveDraft: true, animated: true) - } - }), - TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: { - - }) - ], + text: text, + actions: actions, actionLayout: .vertical ) self.present(controller, in: .window(.root)) @@ -5012,7 +5214,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate } private var didComplete = false - func requestCompletion(animated: Bool) { + func requestStoryCompletion(animated: Bool) { guard let mediaEditor = self.node.mediaEditor, let subject = self.node.subject, let actualSubject = self.node.actualSubject, !self.didComplete else { return } @@ -5364,6 +5566,55 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate } } + func requestStickerCompletion(animated: Bool) { + guard let mediaEditor = self.node.mediaEditor, !self.didComplete else { + return + } + + self.didComplete = true + + self.dismissAllTooltips() + + mediaEditor.stop() + mediaEditor.invalidate() + self.node.entitiesView.invalidate() + + if let navigationController = self.navigationController as? NavigationController { + navigationController.updateRootContainerTransitionOffset(0.0, transition: .immediate) + } + + let entities = self.node.entitiesView.entities.filter { !($0 is DrawingMediaEntity) } + let codableEntities = DrawingEntitiesView.encodeEntities(entities, entitiesView: self.node.entitiesView) + mediaEditor.setDrawingAndEntities(data: nil, image: mediaEditor.values.drawing, entities: codableEntities) + + if let image = mediaEditor.resultImage { + makeEditorImageComposition(context: self.node.ciContext, postbox: self.context.account.postbox, inputImage: image, dimensions: storyDimensions, values: mediaEditor.values, time: .zero, textScale: 2.0, completion: { [weak self] resultImage in + if let self, let resultImage { + Logger.shared.log("MediaEditor", "Completed with image \(resultImage)") + + let scaledImage = generateImage(CGSize(width: 512, height: 512), contextGenerator: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + + context.addPath(CGPath(roundedRect: CGRect(origin: .zero, size: size), cornerWidth: size.width / 8.0, cornerHeight: size.width / 8.0, transform: nil)) + context.clip() + + let scaledSize = resultImage.size.aspectFilled(size) + context.draw(resultImage.cgImage!, in: CGRect(origin: CGPoint(x: floor((size.width - scaledSize.width) / 2.0), y: floor((size.height - scaledSize.height) / 2.0)), size: scaledSize)) + }, opaque: false, scale: 1.0)! + + self.completion(MediaEditorScreen.Result(media: .image(image: scaledImage, dimensions: PixelDimensions(scaledImage.size)), mediaAreas: [], caption: NSAttributedString(), options: MediaEditorResultPrivacy(sendAsPeerId: nil, privacy: EngineStoryPrivacy(base: .everyone, additionallyIncludePeers: []), timeout: 0, isForwardingDisabled: false, pin: false), stickers: [], randomId: 0), { [weak self] finished in + self?.node.animateOut(finished: true, saveDraft: false, completion: { [weak self] in + self?.dismiss() + Queue.mainQueue().justDispatch { + finished() + } + }) + }) + } + }) + } + } + private var videoExport: MediaEditorVideoExport? private var exportDisposable = MetaDisposable() @@ -5615,7 +5866,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate } } -final class DoneButtonContentComponent: CombinedComponent { +private final class DoneButtonContentComponent: CombinedComponent { let backgroundColor: UIColor let icon: UIImage let title: String? @@ -5646,8 +5897,9 @@ final class DoneButtonContentComponent: CombinedComponent { let text = Child(Text.self) return { context in + let iconSize = context.component.icon.size let icon = icon.update( - component: Image(image: context.component.icon, tintColor: .white, size: CGSize(width: 10.0, height: 16.0)), + component: Image(image: context.component.icon, tintColor: .white, size: iconSize), availableSize: CGSize(width: 180.0, height: 100.0), transition: .immediate ) @@ -5705,6 +5957,91 @@ final class DoneButtonContentComponent: CombinedComponent { } } +private final class CutoutButtonContentComponent: CombinedComponent { + let backgroundColor: UIColor + let icon: UIImage + let title: String? + + init( + backgroundColor: UIColor, + icon: UIImage, + title: String? + ) { + self.backgroundColor = backgroundColor + self.icon = icon + self.title = title + } + + static func ==(lhs: CutoutButtonContentComponent, rhs: CutoutButtonContentComponent) -> Bool { + if lhs.backgroundColor != rhs.backgroundColor { + return false + } + if lhs.title != rhs.title { + return false + } + return true + } + + static var body: Body { + let background = Child(BlurredBackgroundComponent.self) + let icon = Child(Image.self) + let text = Child(Text.self) + + return { context in + let iconSize = context.component.icon.size + let icon = icon.update( + component: Image(image: context.component.icon, tintColor: .white, size: iconSize), + availableSize: CGSize(width: 180.0, height: 40.0), + transition: .immediate + ) + + let backgroundHeight: CGFloat = 40.0 + var backgroundSize = CGSize(width: backgroundHeight, height: backgroundHeight) + + let textSpacing: CGFloat = 8.0 + + var title: _UpdatedChildComponent? + if let titleText = context.component.title { + title = text.update( + component: Text( + text: titleText, + font: Font.with(size: 17.0, weight: .semibold), + color: .white + ), + availableSize: CGSize(width: 240.0, height: 100.0), + transition: .immediate + ) + + let updatedBackgroundWidth = backgroundSize.width + textSpacing + title!.size.width + backgroundSize.width = updatedBackgroundWidth + 32.0 + } + + let background = background.update( + component: BlurredBackgroundComponent(color: context.component.backgroundColor, tintContainerView: nil, cornerRadius: backgroundHeight / 2.0), + availableSize: backgroundSize, + transition: .immediate + ) + context.add(background + .position(CGPoint(x: backgroundSize.width / 2.0, y: backgroundSize.height / 2.0)) + .cornerRadius(min(backgroundSize.width, backgroundSize.height) / 2.0) + .clipsToBounds(true) + ) + + if let title { + context.add(title + .position(CGPoint(x: title.size.width / 2.0 + 54.0, y: backgroundHeight / 2.0)) + ) + } + + context.add(icon + .position(CGPoint(x: 36.0, y: backgroundSize.height / 2.0)) + ) + + return backgroundSize + } + } +} + private final class HeaderContextReferenceContentSource: ContextReferenceContentSource { private let controller: ViewController diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaToolsScreen.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaToolsScreen.swift index 250f26efe5c..11f3086dcb7 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaToolsScreen.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaToolsScreen.swift @@ -1031,6 +1031,7 @@ public final class MediaToolsScreen: ViewController { bottom: bottomInset, right: layout.safeInsets.right ), + additionalInsets: layout.additionalInsets, inputHeight: layout.inputHeight ?? 0.0, metrics: layout.metrics, deviceMetrics: layout.deviceMetrics, diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/SaveProgressScreen.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/SaveProgressScreen.swift index 7de762ec791..2aeb3f1dfae 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/SaveProgressScreen.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/SaveProgressScreen.swift @@ -448,6 +448,7 @@ public final class SaveProgressScreen: ViewController { bottom: topInset, right: layout.safeInsets.right ), + additionalInsets: layout.additionalInsets, inputHeight: layout.inputHeight ?? 0.0, metrics: layout.metrics, deviceMetrics: layout.deviceMetrics, diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/BUILD b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/BUILD index 2b772973384..b0a86ff36e6 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/BUILD +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/BUILD @@ -150,6 +150,7 @@ swift_library( "//submodules/TelegramUI/Components/Settings/BoostLevelIconComponent", "//submodules/Components/MultilineTextComponent", "//submodules/TelegramUI/Components/Settings/PeerNameColorItem", + "//submodules/TelegramUI/Components/PlainButtonComponent", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenAddressItem.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenAddressItem.swift index 0b1f06d683c..c6042821136 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenAddressItem.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenAddressItem.swift @@ -13,7 +13,7 @@ final class PeerInfoScreenAddressItem: PeerInfoScreenItem { let text: String let imageSignal: Signal<(TransformImageArguments) -> DrawingContext?, NoError>? let action: (() -> Void)? - let longTapAction: (() -> Void)? + let longTapAction: ((ASDisplayNode, String) -> Void)? let linkItemAction: ((TextLinkItemActionType, TextLinkItem) -> Void)? init( @@ -22,7 +22,7 @@ final class PeerInfoScreenAddressItem: PeerInfoScreenItem { text: String, imageSignal: Signal<(TransformImageArguments) -> DrawingContext?, NoError>?, action: (() -> Void)?, - longTapAction: (() -> Void)? = nil, + longTapAction: ((ASDisplayNode, String) -> Void)? = nil, linkItemAction: ((TextLinkItemActionType, TextLinkItem) -> Void)? = nil ) { self.id = id @@ -66,6 +66,16 @@ private final class PeerInfoScreenAddressItemNode: PeerInfoScreenItemNode { self.addSubnode(self.bottomSeparatorNode) self.addSubnode(self.selectionNode) self.addSubnode(self.maskNode) + + self.view.addGestureRecognizer(UILongPressGestureRecognizer(target: self, action: #selector(self.longPressGesture(_:)))) + } + + @objc private func longPressGesture(_ recognizer: UILongPressGestureRecognizer) { + if case .began = recognizer.state { + if let item = self.item { + item.longTapAction?(self, item.text) + } + } } override func update(width: CGFloat, safeInsets: UIEdgeInsets, presentationData: PresentationData, item: PeerInfoScreenItem, topItem: PeerInfoScreenItem?, bottomItem: PeerInfoScreenItem?, hasCorners: Bool, transition: ContainedViewLayoutTransition) -> CGFloat { @@ -81,7 +91,7 @@ private final class PeerInfoScreenAddressItemNode: PeerInfoScreenItemNode { self.bottomSeparatorNode.backgroundColor = presentationData.theme.list.itemBlocksSeparatorColor - let addressItem = ItemListAddressItem(theme: presentationData.theme, label: item.label, text: item.text, imageSignal: item.imageSignal, sectionId: 0, style: .blocks, displayDecorations: false, action: nil, longTapAction: item.longTapAction, linkItemAction: item.linkItemAction) + let addressItem = ItemListAddressItem(theme: presentationData.theme, label: item.label, text: item.text, imageSignal: item.imageSignal, sectionId: 0, style: .blocks, displayDecorations: false, action: nil, longTapAction: nil, linkItemAction: item.linkItemAction) let params = ListViewItemLayoutParams(width: width, leftInset: safeInsets.left, rightInset: safeInsets.right, availableHeight: 1000.0) @@ -90,7 +100,7 @@ private final class PeerInfoScreenAddressItemNode: PeerInfoScreenItemNode { itemNode = current addressItem.updateNode(async: { $0() }, node: { return itemNode - }, params: params, previousItem: nil, nextItem: nil, animation: .None, completion: { (layout, apply) in + }, params: params, previousItem: topItem != nil ? addressItem : nil, nextItem: bottomItem != nil ? addressItem : nil, animation: .None, completion: { (layout, apply) in let nodeFrame = CGRect(origin: CGPoint(), size: CGSize(width: width, height: layout.size.height)) itemNode.contentSize = layout.contentSize @@ -119,7 +129,7 @@ private final class PeerInfoScreenAddressItemNode: PeerInfoScreenItemNode { self.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(presentationData.theme, top: hasTopCorners, bottom: hasBottomCorners) : nil self.maskNode.frame = CGRect(origin: CGPoint(x: safeInsets.left, y: 0.0), size: CGSize(width: width - safeInsets.left - safeInsets.right, height: height)) - self.bottomSeparatorNode.isHidden = hasBottomCorners + self.bottomSeparatorNode.isHidden = hasBottomCorners || bottomItem != nil transition.updateFrame(node: itemNode, frame: CGRect(origin: CGPoint(), size: itemNode.bounds.size)) diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenBusinessHoursItem.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenBusinessHoursItem.swift new file mode 100644 index 00000000000..9e43592add3 --- /dev/null +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenBusinessHoursItem.swift @@ -0,0 +1,624 @@ +import AsyncDisplayKit +import Display +import TelegramPresentationData +import AccountContext +import TextFormat +import UIKit +import AppBundle +import TelegramStringFormatting +import ContextUI +import TelegramCore +import ComponentFlow +import MultilineTextComponent +import BundleIconComponent +import PlainButtonComponent + +private func dayBusinessHoursText(presentationData: PresentationData, day: TelegramBusinessHours.WeekDay, offsetMinutes: Int, formatAsPlainText: Bool = false) -> String { + var businessHoursText: String = "" + switch day { + case .open: + businessHoursText += presentationData.strings.PeerInfo_BusinessHours_DayOpen24h + case .closed: + businessHoursText += presentationData.strings.PeerInfo_BusinessHours_DayClosed + case let .intervals(intervals): + func clipMinutes(_ value: Int) -> Int { + var value = value + if value < 0 { + value = 24 * 60 + value + } + return value % (24 * 60) + } + + var resultText: String = "" + for range in intervals { + let range = TelegramBusinessHours.WorkingTimeInterval(startMinute: range.startMinute + offsetMinutes, endMinute: range.endMinute + offsetMinutes) + + if !resultText.isEmpty { + if formatAsPlainText { + resultText.append(", ") + } else { + resultText.append("\n") + } + } + let startHours = clipMinutes(range.startMinute) / 60 + let startMinutes = clipMinutes(range.startMinute) % 60 + let startText = stringForShortTimestamp(hours: Int32(startHours), minutes: Int32(startMinutes), dateTimeFormat: presentationData.dateTimeFormat, formatAsPlainText: formatAsPlainText) + let endHours = clipMinutes(range.endMinute) / 60 + let endMinutes = clipMinutes(range.endMinute) % 60 + let endText = stringForShortTimestamp(hours: Int32(endHours), minutes: Int32(endMinutes), dateTimeFormat: presentationData.dateTimeFormat, formatAsPlainText: formatAsPlainText) + resultText.append("\(startText) - \(endText)") + } + businessHoursText += resultText + } + + return businessHoursText +} + +final class PeerInfoScreenBusinessHoursItem: PeerInfoScreenItem { + let id: AnyHashable + let label: String + let businessHours: TelegramBusinessHours + let requestLayout: (Bool) -> Void + let longTapAction: (ASDisplayNode, String) -> Void + + init( + id: AnyHashable, + label: String, + businessHours: TelegramBusinessHours, + requestLayout: @escaping (Bool) -> Void, + longTapAction: @escaping (ASDisplayNode, String) -> Void + ) { + self.id = id + self.label = label + self.businessHours = businessHours + self.requestLayout = requestLayout + self.longTapAction = longTapAction + } + + func node() -> PeerInfoScreenItemNode { + return PeerInfoScreenBusinessHoursItemNode() + } +} + +private final class PeerInfoScreenBusinessHoursItemNode: PeerInfoScreenItemNode { + private let containerNode: ContextControllerSourceNode + private let contextSourceNode: ContextExtractedContentContainingNode + + private let extractedBackgroundImageNode: ASImageNode + + private var extractedRect: CGRect? + private var nonExtractedRect: CGRect? + + private let maskNode: ASImageNode + private let labelNode: ImmediateTextNode + private let currentStatusText = ComponentView() + private let currentDayText = ComponentView() + private var timezoneSwitchButton: ComponentView? + private var dayTitles: [ComponentView] = [] + private var dayValues: [ComponentView] = [] + private let arrowIcon = ComponentView() + + private let bottomSeparatorNode: ASDisplayNode + + private let activateArea: AccessibilityAreaNode + + private var item: PeerInfoScreenBusinessHoursItem? + private var presentationData: PresentationData? + private var theme: PresentationTheme? + + private var currentTimezone: TimeZone + private var displayLocalTimezone: Bool = false + private var cachedDays: [TelegramBusinessHours.WeekDay] = [] + private var cachedWeekMinuteSet = IndexSet() + + private var isExpanded: Bool = false + + override init() { + self.contextSourceNode = ContextExtractedContentContainingNode() + self.containerNode = ContextControllerSourceNode() + + self.extractedBackgroundImageNode = ASImageNode() + self.extractedBackgroundImageNode.displaysAsynchronously = false + self.extractedBackgroundImageNode.alpha = 0.0 + + self.maskNode = ASImageNode() + self.maskNode.isUserInteractionEnabled = false + + self.labelNode = ImmediateTextNode() + self.labelNode.displaysAsynchronously = false + self.labelNode.isUserInteractionEnabled = false + + self.bottomSeparatorNode = ASDisplayNode() + self.bottomSeparatorNode.isLayerBacked = true + + self.activateArea = AccessibilityAreaNode() + + self.currentTimezone = TimeZone.current + + super.init() + + self.addSubnode(self.bottomSeparatorNode) + + self.containerNode.addSubnode(self.contextSourceNode) + self.containerNode.targetNodeForActivationProgress = self.contextSourceNode.contentNode + self.addSubnode(self.containerNode) + + self.addSubnode(self.maskNode) + + self.contextSourceNode.contentNode.clipsToBounds = true + + self.contextSourceNode.contentNode.addSubnode(self.extractedBackgroundImageNode) + self.contextSourceNode.contentNode.addSubnode(self.labelNode) + + self.addSubnode(self.activateArea) + + self.containerNode.isGestureEnabled = false + + self.contextSourceNode.willUpdateIsExtractedToContextPreview = { [weak self] isExtracted, transition in + guard let strongSelf = self, let theme = strongSelf.theme else { + return + } + + if isExtracted { + strongSelf.extractedBackgroundImageNode.image = generateStretchableFilledCircleImage(diameter: 28.0, color: theme.list.plainBackgroundColor) + } + + if let extractedRect = strongSelf.extractedRect, let nonExtractedRect = strongSelf.nonExtractedRect { + let rect = isExtracted ? extractedRect : nonExtractedRect + transition.updateFrame(node: strongSelf.extractedBackgroundImageNode, frame: rect) + } + + transition.updateAlpha(node: strongSelf.extractedBackgroundImageNode, alpha: isExtracted ? 1.0 : 0.0, completion: { _ in + if !isExtracted { + self?.extractedBackgroundImageNode.image = nil + } + }) + } + } + + override func didLoad() { + super.didLoad() + + let recognizer = TapLongTapOrDoubleTapGestureRecognizer(target: self, action: #selector(self.tapLongTapOrDoubleTapGesture(_:))) + recognizer.tapActionAtPoint = { _ in + return .waitForSingleTap + } + recognizer.highlight = { [weak self] point in + guard let strongSelf = self else { + return + } + strongSelf.updateTouchesAtPoint(point) + } + self.view.addGestureRecognizer(recognizer) + } + + @objc private func tapLongTapOrDoubleTapGesture(_ recognizer: TapLongTapOrDoubleTapGestureRecognizer) { + switch recognizer.state { + case .ended: + if let (gesture, _) = recognizer.lastRecognizedGestureAndLocation { + switch gesture { + case .tap: + self.isExpanded = !self.isExpanded + self.item?.requestLayout(true) + case .longTap: + if let item = self.item, let presentationData = self.presentationData { + var text = "" + + var timezoneOffsetMinutes: Int = 0 + if self.displayLocalTimezone { + var currentCalendar = Calendar(identifier: .gregorian) + currentCalendar.timeZone = TimeZone(identifier: item.businessHours.timezoneId) ?? TimeZone.current + + timezoneOffsetMinutes = (self.currentTimezone.secondsFromGMT() - currentCalendar.timeZone.secondsFromGMT()) / 60 + } + + let businessDays: [TelegramBusinessHours.WeekDay] = self.cachedDays + + for i in 0 ..< businessDays.count { + let dayTitleValue: String + switch i { + case 0: + dayTitleValue = presentationData.strings.Weekday_Monday + case 1: + dayTitleValue = presentationData.strings.Weekday_Tuesday + case 2: + dayTitleValue = presentationData.strings.Weekday_Wednesday + case 3: + dayTitleValue = presentationData.strings.Weekday_Thursday + case 4: + dayTitleValue = presentationData.strings.Weekday_Friday + case 5: + dayTitleValue = presentationData.strings.Weekday_Saturday + case 6: + dayTitleValue = presentationData.strings.Weekday_Sunday + default: + dayTitleValue = " " + } + + let businessHoursText = dayBusinessHoursText(presentationData: presentationData, day: businessDays[i], offsetMinutes: timezoneOffsetMinutes, formatAsPlainText: true) + + if !text.isEmpty { + text.append("\n") + } + text.append("\(dayTitleValue): \(businessHoursText)") + } + + item.longTapAction(self, text) + } + default: + break + } + } + default: + break + } + } + + override func update(width: CGFloat, safeInsets: UIEdgeInsets, presentationData: PresentationData, item: PeerInfoScreenItem, topItem: PeerInfoScreenItem?, bottomItem: PeerInfoScreenItem?, hasCorners: Bool, transition: ContainedViewLayoutTransition) -> CGFloat { + guard let item = item as? PeerInfoScreenBusinessHoursItem else { + return 10.0 + } + + let businessDays: [TelegramBusinessHours.WeekDay] + if self.item?.businessHours != item.businessHours { + businessDays = item.businessHours.splitIntoWeekDays() + self.cachedDays = businessDays + self.cachedWeekMinuteSet = item.businessHours.weekMinuteSet() + } else { + businessDays = self.cachedDays + } + + self.item = item + self.presentationData = presentationData + self.theme = presentationData.theme + + let sideInset: CGFloat = 16.0 + safeInsets.left + + self.bottomSeparatorNode.backgroundColor = presentationData.theme.list.itemBlocksSeparatorColor + + self.labelNode.attributedText = NSAttributedString(string: item.label, font: Font.regular(14.0), textColor: presentationData.theme.list.itemPrimaryTextColor) + + let labelSize = self.labelNode.updateLayout(CGSize(width: width - sideInset * 2.0, height: .greatestFiniteMagnitude)) + + var topOffset = 10.0 + let labelFrame = CGRect(origin: CGPoint(x: sideInset, y: topOffset), size: labelSize) + if labelSize.height > 0.0 { + topOffset += labelSize.height + topOffset += 3.0 + } + + let arrowIconSize = self.arrowIcon.update( + transition: .immediate, + component: AnyComponent(BundleIconComponent( + name: "Item List/DownArrow", + tintColor: presentationData.theme.list.disclosureArrowColor + )), + environment: {}, + containerSize: CGSize(width: 100.0, height: 100.0) + ) + let arrowIconFrame = CGRect(origin: CGPoint(x: width - sideInset + 1.0 - arrowIconSize.width, y: topOffset + 5.0), size: arrowIconSize) + if let arrowIconView = self.arrowIcon.view { + if arrowIconView.superview == nil { + self.contextSourceNode.contentNode.view.addSubview(arrowIconView) + arrowIconView.frame = arrowIconFrame + } + transition.updatePosition(layer: arrowIconView.layer, position: arrowIconFrame.center) + transition.updateBounds(layer: arrowIconView.layer, bounds: CGRect(origin: CGPoint(), size: arrowIconFrame.size)) + transition.updateTransformRotation(view: arrowIconView, angle: self.isExpanded ? CGFloat.pi * 1.0 : CGFloat.pi * 0.0) + } + + var currentCalendar = Calendar(identifier: .gregorian) + currentCalendar.timeZone = TimeZone(identifier: item.businessHours.timezoneId) ?? TimeZone.current + let currentDate = Date() + var currentDayIndex = currentCalendar.component(.weekday, from: currentDate) + if currentDayIndex == 1 { + currentDayIndex = 6 + } else { + currentDayIndex -= 2 + } + + let currentMinute = currentCalendar.component(.minute, from: currentDate) + let currentHour = currentCalendar.component(.hour, from: currentDate) + let currentWeekMinute = currentDayIndex * 24 * 60 + currentHour * 60 + currentMinute + + var timezoneOffsetMinutes: Int = 0 + if self.displayLocalTimezone { + timezoneOffsetMinutes = (self.currentTimezone.secondsFromGMT() - currentCalendar.timeZone.secondsFromGMT()) / 60 + } + + let isOpen = self.cachedWeekMinuteSet.contains(currentWeekMinute) + let openStatusText = isOpen ? presentationData.strings.PeerInfo_BusinessHours_StatusOpen : presentationData.strings.PeerInfo_BusinessHours_StatusClosed + + var currentDayStatusText = currentDayIndex >= 0 && currentDayIndex < businessDays.count ? dayBusinessHoursText(presentationData: presentationData, day: businessDays[currentDayIndex], offsetMinutes: timezoneOffsetMinutes) : " " + + if !isOpen { + for range in self.cachedWeekMinuteSet.rangeView { + if range.lowerBound > currentWeekMinute { + let openInMinutes = range.lowerBound - currentWeekMinute + if openInMinutes < 60 { + currentDayStatusText = presentationData.strings.PeerInfo_BusinessHours_StatusOpensInMinutes(Int32(openInMinutes)) + } else if openInMinutes < 6 * 60 { + currentDayStatusText = presentationData.strings.PeerInfo_BusinessHours_StatusOpensInHours(Int32(openInMinutes / 60)) + } else { + let openDate = currentDate.addingTimeInterval(Double(openInMinutes * 60)) + let openTimestamp = Int32(openDate.timeIntervalSince1970) + Int32(currentCalendar.timeZone.secondsFromGMT() - TimeZone.current.secondsFromGMT()) + + let dateText = humanReadableStringForTimestamp(strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, timestamp: openTimestamp, alwaysShowTime: true, allowYesterday: false, format: HumanReadableStringFormat( + dateFormatString: { value in + let text = PresentationStrings.FormattedString(string: presentationData.strings.Chat_MessageSeenTimestamp_Date(value).string, ranges: []) + return presentationData.strings.PeerInfo_BusinessHours_StatusOpensOnDate(text.string) + }, + tomorrowFormatString: { value in + return PresentationStrings.FormattedString(string: presentationData.strings.PeerInfo_BusinessHours_StatusOpensTomorrowAt(value).string, ranges: []) + }, + todayFormatString: { value in + return PresentationStrings.FormattedString(string: presentationData.strings.PeerInfo_BusinessHours_StatusOpensTodayAt(value).string, ranges: []) + }, + yesterdayFormatString: { value in + return PresentationStrings.FormattedString(string: presentationData.strings.PeerInfo_BusinessHours_StatusOpensTodayAt(value).string, ranges: []) + } + )).string + currentDayStatusText = dateText + } + break + } + } + } + + let currentStatusTextSize = self.currentStatusText.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: openStatusText, font: Font.regular(15.0), textColor: isOpen ? presentationData.theme.list.freeTextSuccessColor : presentationData.theme.list.itemDestructiveColor)) + )), + environment: {}, + containerSize: CGSize(width: width - sideInset * 2.0, height: 100.0) + ) + let currentStatusTextFrame = CGRect(origin: CGPoint(x: sideInset, y: topOffset), size: currentStatusTextSize) + if let currentStatusTextView = self.currentStatusText.view { + if currentStatusTextView.superview == nil { + currentStatusTextView.layer.anchorPoint = CGPoint() + self.contextSourceNode.contentNode.view.addSubview(currentStatusTextView) + } + transition.updatePosition(layer: currentStatusTextView.layer, position: currentStatusTextFrame.origin) + currentStatusTextView.bounds = CGRect(origin: CGPoint(), size: currentStatusTextFrame.size) + } + + let dayRightInset = sideInset + 17.0 + + let currentDayTextSize = self.currentDayText.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: currentDayStatusText, font: Font.regular(15.0), textColor: presentationData.theme.list.itemSecondaryTextColor)), + horizontalAlignment: .right, + maximumNumberOfLines: 0 + )), + environment: {}, + containerSize: CGSize(width: width - sideInset - dayRightInset, height: 100.0) + ) + + var timezoneSwitchButtonSize: CGSize? + if item.businessHours.timezoneId != self.currentTimezone.identifier { + let timezoneSwitchButton: ComponentView + if let current = self.timezoneSwitchButton { + timezoneSwitchButton = current + } else { + timezoneSwitchButton = ComponentView() + self.timezoneSwitchButton = timezoneSwitchButton + } + let timezoneSwitchTitle: String + if self.displayLocalTimezone { + timezoneSwitchTitle = presentationData.strings.PeerInfo_BusinessHours_TimezoneSwitchMy + } else { + timezoneSwitchTitle = presentationData.strings.PeerInfo_BusinessHours_TimezoneSwitchBusiness + } + timezoneSwitchButtonSize = timezoneSwitchButton.update( + transition: .immediate, + component: AnyComponent(PlainButtonComponent( + content: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: timezoneSwitchTitle, font: Font.regular(12.0), textColor: presentationData.theme.list.itemAccentColor)) + )), + background: AnyComponent(RoundedRectangle( + color: presentationData.theme.list.itemAccentColor.withMultipliedAlpha(0.1), + cornerRadius: nil + )), + effectAlignment: .center, + contentInsets: UIEdgeInsets(top: 1.0, left: 7.0, bottom: 2.0, right: 7.0), + action: { [weak self] in + guard let self else { + return + } + self.displayLocalTimezone = !self.displayLocalTimezone + self.item?.requestLayout(false) + }, + animateAlpha: true, + animateScale: false, + animateContents: false + )), + environment: {}, + containerSize: CGSize(width: 100.0, height: 100.0) + ) + } else { + if let timezoneSwitchButton = self.timezoneSwitchButton { + self.timezoneSwitchButton = nil + timezoneSwitchButton.view?.removeFromSuperview() + } + } + + let timezoneSwitchButtonSpacing: CGFloat = 3.0 + if timezoneSwitchButtonSize != nil { + topOffset -= 20.0 + } + + let currentDayTextFrame = CGRect(origin: CGPoint(x: width - dayRightInset - currentDayTextSize.width, y: topOffset), size: currentDayTextSize) + if let currentDayTextView = self.currentDayText.view { + if currentDayTextView.superview == nil { + currentDayTextView.layer.anchorPoint = CGPoint() + self.contextSourceNode.contentNode.view.addSubview(currentDayTextView) + } + transition.updatePosition(layer: currentDayTextView.layer, position: currentDayTextFrame.origin) + currentDayTextView.bounds = CGRect(origin: CGPoint(), size: currentDayTextFrame.size) + } + + topOffset += max(currentStatusTextSize.height, currentDayTextSize.height) + + if let timezoneSwitchButtonView = self.timezoneSwitchButton?.view, let timezoneSwitchButtonSize { + topOffset += timezoneSwitchButtonSpacing + + var timezoneSwitchButtonTransition = transition + if timezoneSwitchButtonView.superview == nil { + timezoneSwitchButtonTransition = .immediate + self.contextSourceNode.contentNode.view.addSubview(timezoneSwitchButtonView) + } + let timezoneSwitchButtonFrame = CGRect(origin: CGPoint(x: width - dayRightInset - timezoneSwitchButtonSize.width, y: topOffset), size: timezoneSwitchButtonSize) + timezoneSwitchButtonTransition.updateFrame(view: timezoneSwitchButtonView, frame: timezoneSwitchButtonFrame) + + topOffset += timezoneSwitchButtonSize.height + } + + let daySpacing: CGFloat = 15.0 + + var dayHeights: CGFloat = 0.0 + + for rawI in 0 ..< businessDays.count { + if rawI == 0 { + //skip current day + continue + } + let i = (rawI + currentDayIndex) % businessDays.count + + dayHeights += daySpacing + + var dayTransition = transition + let dayTitle: ComponentView + if self.dayTitles.count > i { + dayTitle = self.dayTitles[i] + } else { + dayTransition = .immediate + dayTitle = ComponentView() + self.dayTitles.append(dayTitle) + } + + let dayValue: ComponentView + if self.dayValues.count > i { + dayValue = self.dayValues[i] + } else { + dayValue = ComponentView() + self.dayValues.append(dayValue) + } + + let dayTitleValue: String + switch i { + case 0: + dayTitleValue = presentationData.strings.Weekday_Monday + case 1: + dayTitleValue = presentationData.strings.Weekday_Tuesday + case 2: + dayTitleValue = presentationData.strings.Weekday_Wednesday + case 3: + dayTitleValue = presentationData.strings.Weekday_Thursday + case 4: + dayTitleValue = presentationData.strings.Weekday_Friday + case 5: + dayTitleValue = presentationData.strings.Weekday_Saturday + case 6: + dayTitleValue = presentationData.strings.Weekday_Sunday + default: + dayTitleValue = " " + } + + let businessHoursText = dayBusinessHoursText(presentationData: presentationData, day: businessDays[i], offsetMinutes: timezoneOffsetMinutes) + + let dayTitleSize = dayTitle.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: dayTitleValue, font: Font.regular(15.0), textColor: presentationData.theme.list.itemPrimaryTextColor)) + )), + environment: {}, + containerSize: CGSize(width: width - sideInset * 2.0, height: 100.0) + ) + let dayTitleFrame = CGRect(origin: CGPoint(x: sideInset, y: topOffset + dayHeights), size: dayTitleSize) + if let dayTitleView = dayTitle.view { + if dayTitleView.superview == nil { + dayTitleView.layer.anchorPoint = CGPoint() + self.contextSourceNode.contentNode.view.addSubview(dayTitleView) + dayTitleView.alpha = 0.0 + } + dayTransition.updatePosition(layer: dayTitleView.layer, position: dayTitleFrame.origin) + dayTitleView.bounds = CGRect(origin: CGPoint(), size: dayTitleFrame.size) + + transition.updateAlpha(layer: dayTitleView.layer, alpha: self.isExpanded ? 1.0 : 0.0) + } + + let dayValueSize = dayValue.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: businessHoursText, font: Font.regular(15.0), textColor: presentationData.theme.list.itemSecondaryTextColor, paragraphAlignment: .right)), + horizontalAlignment: .right, + maximumNumberOfLines: 0 + )), + environment: {}, + containerSize: CGSize(width: width - sideInset - dayRightInset, height: 100.0) + ) + let dayValueFrame = CGRect(origin: CGPoint(x: width - dayRightInset - dayValueSize.width, y: topOffset + dayHeights), size: dayValueSize) + if let dayValueView = dayValue.view { + if dayValueView.superview == nil { + dayValueView.layer.anchorPoint = CGPoint(x: 1.0, y: 0.0) + self.contextSourceNode.contentNode.view.addSubview(dayValueView) + dayValueView.alpha = 0.0 + } + dayTransition.updatePosition(layer: dayValueView.layer, position: CGPoint(x: dayValueFrame.maxX, y: dayValueFrame.minY)) + dayValueView.bounds = CGRect(origin: CGPoint(), size: dayValueFrame.size) + + transition.updateAlpha(layer: dayValueView.layer, alpha: self.isExpanded ? 1.0 : 0.0) + } + + dayHeights += max(dayTitleSize.height, dayValueSize.height) + } + + if self.isExpanded { + topOffset += dayHeights + } + + topOffset += 11.0 + + transition.updateFrame(node: self.labelNode, frame: labelFrame) + + let height = topOffset + + transition.updateFrame(node: self.bottomSeparatorNode, frame: CGRect(origin: CGPoint(x: sideInset, y: height - UIScreenPixel), size: CGSize(width: width - sideInset, height: UIScreenPixel))) + transition.updateAlpha(node: self.bottomSeparatorNode, alpha: bottomItem == nil ? 0.0 : 1.0) + + let hasCorners = hasCorners && (topItem == nil || bottomItem == nil) + let hasTopCorners = hasCorners && topItem == nil + let hasBottomCorners = hasCorners && bottomItem == nil + + self.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(presentationData.theme, top: hasTopCorners, bottom: hasBottomCorners) : nil + self.maskNode.frame = CGRect(origin: CGPoint(x: safeInsets.left, y: 0.0), size: CGSize(width: width - safeInsets.left - safeInsets.right, height: height)) + self.bottomSeparatorNode.isHidden = hasBottomCorners + + self.activateArea.frame = CGRect(origin: CGPoint(), size: CGSize(width: width, height: height)) + self.activateArea.accessibilityLabel = item.label + + let contentSize = CGSize(width: width, height: height) + self.containerNode.frame = CGRect(origin: CGPoint(), size: contentSize) + self.contextSourceNode.frame = CGRect(origin: CGPoint(), size: contentSize) + transition.updateFrame(node: self.contextSourceNode.contentNode, frame: CGRect(origin: CGPoint(), size: contentSize)) + + let nonExtractedRect = CGRect(origin: CGPoint(), size: CGSize(width: contentSize.width, height: contentSize.height)) + let extractedRect = nonExtractedRect + self.extractedRect = extractedRect + self.nonExtractedRect = nonExtractedRect + + if self.contextSourceNode.isExtractedToContextPreview { + self.extractedBackgroundImageNode.frame = extractedRect + } else { + self.extractedBackgroundImageNode.frame = nonExtractedRect + } + self.contextSourceNode.contentRect = extractedRect + + return height + } + + private func updateTouchesAtPoint(_ point: CGPoint?) { + } +} diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift index 8db7a04c952..7730007aced 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift @@ -360,6 +360,8 @@ final class PeerInfoSelectionPanelNode: ASDisplayNode { }, sendContextResult: { _, _, _, _ in return false }, sendBotCommand: { _, _ in + }, sendShortcut: { _ in + }, openEditShortcuts: { }, sendBotStart: { _ in }, botSwitchChatWithPayload: { _, _ in }, beginMediaRecording: { _ in @@ -495,6 +497,8 @@ private enum PeerInfoContextSubject { case ngId(String) case regDate(String) case link(customLink: String?) + case businessHours(String) + case businessLocation(String) } private enum PeerInfoSettingsSection { @@ -1002,10 +1006,13 @@ private func settingsItems(data: PeerInfoScreenData?, context: AccountContext, p items[.payment]!.append(PeerInfoScreenDisclosureItem(id: 100, label: .text(""), text: presentationData.strings.Settings_Premium, icon: PresentationResourcesSettings.premium, action: { interaction.openSettings(.premium) })) + items[.payment]!.append(PeerInfoScreenDisclosureItem(id: 101, label: .text(""), additionalBadgeLabel: presentationData.strings.Settings_New, text: presentationData.strings.Settings_Business, icon: PresentationResourcesSettings.business, action: { + interaction.openSettings(.businessSetup) + })) // MARK: Nicegram, comment this item (hide "Gift Premium") /* - items[.payment]!.append(PeerInfoScreenDisclosureItem(id: 102, label: .text(""), additionalBadgeLabel: presentationData.strings.Settings_New, text: presentationData.strings.Settings_PremiumGift, icon: PresentationResourcesSettings.premiumGift, action: { + items[.payment]!.append(PeerInfoScreenDisclosureItem(id: 102, label: .text(""), text: presentationData.strings.Settings_PremiumGift, icon: PresentationResourcesSettings.premiumGift, action: { interaction.openSettings(.premiumGift) })) */ @@ -1262,6 +1269,49 @@ private func infoItems(data: PeerInfoScreenData?, context: AccountContext, prese interaction.requestLayout(false) })) } + + if let businessHours = cachedData.businessHours { + items[.peerInfo]!.append(PeerInfoScreenBusinessHoursItem(id: 300, label: presentationData.strings.PeerInfo_BusinessHours_Label, businessHours: businessHours, requestLayout: { animated in + interaction.requestLayout(animated) + }, longTapAction: { sourceNode, text in + if !text.isEmpty { + interaction.openPeerInfoContextMenu(.businessHours(text), sourceNode, nil) + } + })) + } + + if let businessLocation = cachedData.businessLocation { + if let coordinates = businessLocation.coordinates { + let imageSignal = chatMapSnapshotImage(engine: context.engine, resource: MapSnapshotMediaResource(latitude: coordinates.latitude, longitude: coordinates.longitude, width: 90, height: 90)) + items[.peerInfo]!.append(PeerInfoScreenAddressItem( + id: 301, + label: presentationData.strings.PeerInfo_Location_Label, + text: businessLocation.address, + imageSignal: imageSignal, + action: { + interaction.openLocation() + }, + longTapAction: { sourceNode, text in + if !text.isEmpty { + interaction.openPeerInfoContextMenu(.businessLocation(text), sourceNode, nil) + } + } + )) + } else { + items[.peerInfo]!.append(PeerInfoScreenAddressItem( + id: 301, + label: presentationData.strings.PeerInfo_Location_Label, + text: businessLocation.address, + imageSignal: nil, + action: nil, + longTapAction: { sourceNode, text in + if !text.isEmpty { + interaction.openPeerInfoContextMenu(.businessLocation(text), sourceNode, nil) + } + } + )) + } + } } if let reactionSourceMessageId = reactionSourceMessageId, !data.isContact { items[.peerInfo]!.append(PeerInfoScreenActionItem(id: 3, text: presentationData.strings.UserInfo_SendMessage, action: { @@ -3210,6 +3260,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro }, openPremiumStatusInfo: { _, _, _, _ in }, openRecommendedChannelContextMenu: { _, _, _ in }, openGroupBoostInfo: { _, _ in + }, openStickerEditor: { }, requestMessageUpdate: { _, _ in }, cancelInteractiveKeyboardGestures: { }, dismissTextInput: { @@ -5918,16 +5969,24 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro } } else if let channel = peer as? TelegramChannel { if let cachedData = strongSelf.data?.cachedData as? CachedChannelData { - if case .group = channel.info { - items.append(.action(ContextMenuActionItem(text: presentationData.strings.PeerInfo_Group_Boost, badge: ContextMenuActionBadge(value: presentationData.strings.Settings_New, color: .accent, style: .label), icon: { theme in - generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Boost"), color: theme.contextMenu.primaryColor) - }, action: { [weak self] _, f in - f(.dismissWithoutContent) - - self?.openBoost() - }))) + let boostTitle: String + var isNew = false + switch channel.info { + case .group: + boostTitle = presentationData.strings.PeerInfo_Group_Boost + isNew = true + case .broadcast: + boostTitle = presentationData.strings.PeerInfo_Channel_Boost } + items.append(.action(ContextMenuActionItem(text: boostTitle, badge: isNew ? ContextMenuActionBadge(value: presentationData.strings.Settings_New, color: .accent, style: .label) : nil, icon: { theme in + generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Boost"), color: theme.contextMenu.primaryColor) + }, action: { [weak self] _, f in + f(.dismissWithoutContent) + + self?.openBoost() + }))) + if channel.hasPermission(.editStories) { items.append(.action(ContextMenuActionItem(text: presentationData.strings.PeerInfo_Channel_ArchivedStories, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Archive"), color: theme.contextMenu.primaryColor) @@ -7786,9 +7845,21 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro } private func openLocation() { - guard let data = self.data, let peer = data.peer, let cachedData = data.cachedData as? CachedChannelData, let location = cachedData.peerGeoLocation else { + guard let data = self.data, let peer = data.peer else { + return + } + + var location: PeerGeoLocation? + if let cachedData = data.cachedData as? CachedChannelData, let locationValue = cachedData.peerGeoLocation { + location = locationValue + } else if let cachedData = data.cachedData as? CachedUserData, let businessLocation = cachedData.businessLocation, let coordinates = businessLocation.coordinates { + location = PeerGeoLocation(latitude: coordinates.latitude, longitude: coordinates.longitude, address: businessLocation.address) + } + + guard let location else { return } + let context = self.context let presentationData = self.presentationData let map = TelegramMediaMap(latitude: location.latitude, longitude: location.longitude, heading: nil, accuracyRadius: nil, geoPlace: nil, venue: MapVenue(title: EnginePeer(peer).displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder), address: location.address, provider: nil, id: nil, type: nil), liveBroadcastingTimeout: nil, liveProximityNotificationRadius: nil) @@ -8112,6 +8183,27 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro return nil } })) + case .businessHours(let text), .businessLocation(let text): + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + + let actions: [ContextMenuAction] = [ContextMenuAction(content: .text(title: presentationData.strings.Conversation_ContextMenuCopy, accessibilityLabel: presentationData.strings.Conversation_ContextMenuCopy), action: { [weak self] in + UIPasteboard.general.string = text + + self?.controller?.present(UndoOverlayController(presentationData: presentationData, content: .copy(text: presentationData.strings.Conversation_TextCopied), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .current) + })] + + let contextMenuController = makeContextMenuController(actions: actions) + controller.present(contextMenuController, in: .window(.root), with: ContextMenuControllerPresentationArguments(sourceNodeAndRect: { [weak self, weak sourceNode] in + if let controller = self?.controller, let sourceNode = sourceNode { + var rect = sourceNode.bounds.insetBy(dx: 0.0, dy: 2.0) + if let sourceRect = sourceRect { + rect = sourceRect.insetBy(dx: 0.0, dy: 2.0) + } + return (sourceNode, rect, controller.displayNode, controller.view.bounds) + } else { + return nil + } + })) } } @@ -9037,7 +9129,8 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro } }) case .chatFolders: - push(chatListFilterPresetListController(context: self.context, mode: .default)) + let controller = self.context.sharedContext.makeFilterSettingsController(context: self.context, modal: false, scrollToTags: false, dismissed: nil) + push(controller) case .notificationsAndSounds: if let settings = self.data?.globalSettings { push(notificationsAndSoundsController(context: self.context, exceptionsList: settings.notificationExceptions)) diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoStoryGridScreen/Sources/PeerInfoStoryGridScreen.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoStoryGridScreen/Sources/PeerInfoStoryGridScreen.swift index 8dba1993637..1ae74c8100a 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoStoryGridScreen/Sources/PeerInfoStoryGridScreen.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoStoryGridScreen/Sources/PeerInfoStoryGridScreen.swift @@ -295,7 +295,23 @@ final class PeerInfoStoryGridScreenComponent: Component { let _ = paneNode.scrollToTop() } + func openCreateStory() { + guard let component = self.component else { + return + } + if let rootController = component.context.sharedContext.mainWindow?.viewController as? TelegramRootControllerInterface { + let coordinator = rootController.openStoryCamera(customTarget: nil, transitionIn: nil, transitionedIn: {}, transitionOut: { _, _ in return nil }) + coordinator?.animateIn() + } + } + + private var isUpdating = false func update(component: PeerInfoStoryGridScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + self.isUpdating = true + defer { + self.isUpdating = false + } + self.component = component self.state = state @@ -313,7 +329,7 @@ final class PeerInfoStoryGridScreenComponent: Component { var bottomInset: CGFloat = environment.safeInsets.bottom - if self.selectedCount != 0 { + if self.selectedCount != 0 || (component.scope == .saved && self.paneNode?.isEmpty == false) { let selectionPanel: ComponentView var selectionPanelTransition = transition if let current = self.selectionPanel { @@ -327,7 +343,7 @@ final class PeerInfoStoryGridScreenComponent: Component { let buttonText: String switch component.scope { case .saved: - buttonText = environment.strings.ChatList_Context_Archive + buttonText = self.selectedCount > 0 ? environment.strings.ChatList_Context_Archive : environment.strings.StoryList_SavedAddAction case .archive: buttonText = environment.strings.StoryList_SaveToProfile } @@ -344,7 +360,7 @@ final class PeerInfoStoryGridScreenComponent: Component { guard let self, let component = self.component, let environment = self.environment else { return } - guard let paneNode = self.paneNode, !paneNode.selectedIds.isEmpty else { + guard let paneNode = self.paneNode else { return } @@ -361,21 +377,25 @@ final class PeerInfoStoryGridScreenComponent: Component { switch component.scope { case .saved: let selectedCount = paneNode.selectedItems.count - let _ = component.context.engine.messages.updateStoriesArePinned(peerId: component.peerId, ids: paneNode.selectedItems, isPinned: false).start() - - paneNode.setIsSelectionModeActive(false) - (self.environment?.controller() as? PeerInfoStoryGridScreen)?.updateTitle() - - let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: environment.theme) - - let title: String = presentationData.strings.StoryList_TooltipStoriesSavedToProfile(Int32(selectedCount)) - environment.controller()?.present(UndoOverlayController( - presentationData: presentationData, - content: .info(title: nil, text: title, timeout: nil, customUndoText: nil), - elevatedLayout: false, - animateInAsReplacement: false, - action: { _ in return false } - ), in: .current) + if selectedCount == 0 { + self.openCreateStory() + } else { + let _ = component.context.engine.messages.updateStoriesArePinned(peerId: component.peerId, ids: paneNode.selectedItems, isPinned: false).start() + + paneNode.setIsSelectionModeActive(false) + (self.environment?.controller() as? PeerInfoStoryGridScreen)?.updateTitle() + + let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: environment.theme) + + let title: String = presentationData.strings.StoryList_TooltipStoriesSavedToProfile(Int32(selectedCount)) + environment.controller()?.present(UndoOverlayController( + presentationData: presentationData, + content: .info(title: nil, text: title, timeout: nil, customUndoText: nil), + elevatedLayout: false, + animateInAsReplacement: false, + action: { _ in return false } + ), in: .current) + } case .archive: let _ = component.context.engine.messages.updateStoriesArePinned(peerId: component.peerId, ids: paneNode.selectedItems, isPinned: true).start() @@ -449,10 +469,28 @@ final class PeerInfoStoryGridScreenComponent: Component { }, listContext: nil ) + paneNode.isEmptyUpdated = { [weak self] _ in + guard let self else { + return + } + if !self.isUpdating { + self.state?.updated(transition: .immediate) + } + } self.paneNode = paneNode - self.addSubview(paneNode.view) + if let selectionPanelView = self.selectionPanel?.view { + self.insertSubview(paneNode.view, belowSubview: selectionPanelView) + } else { + self.addSubview(paneNode.view) + } paneNode.emptyAction = { [weak self] in + guard let self else { + return + } + self.openCreateStory() + } + paneNode.additionalEmptyAction = { [weak self] in guard let self, let component = self.component else { return } diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoStoryPaneNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoStoryPaneNode.swift index b7d525ded01..99d64be4437 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoStoryPaneNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoStoryPaneNode.swift @@ -948,6 +948,8 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr } } + public var isEmptyUpdated: (Bool) -> Void = { _ in } + public private(set) var isSelectionModeActive: Bool private var currentParams: (size: CGSize, topInset: CGFloat, sideInset: CGFloat, bottomInset: CGFloat, deviceMetrics: DeviceMetrics, visibleHeight: CGFloat, isScrollingLockedAtTop: Bool, expandProgress: CGFloat, navigationHeight: CGFloat, presentationData: PresentationData)? @@ -985,6 +987,7 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr public var openCurrentDate: (() -> Void)? public var paneDidScroll: (() -> Void)? public var emptyAction: (() -> Void)? + public var additionalEmptyAction: (() -> Void)? public var ensureRectVisible: ((UIView, CGRect) -> Void)? @@ -1729,6 +1732,7 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr private func updateHistory(items: SparseItemGrid.Items, synchronous: Bool, reloadAtTop: Bool) { self.items = items + self.isEmptyUpdated(self.isEmpty) if let (size, topInset, sideInset, bottomInset, deviceMetrics, visibleHeight, isScrollingLockedAtTop, expandProgress, navigationHeight, presentationData) = self.currentParams { var gridSnapshot: UIView? @@ -2027,14 +2031,21 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr context: self.context, theme: presentationData.theme, animationName: "StoryListEmpty", - title: self.isArchive ? presentationData.strings.StoryList_ArchivedEmptyState_Title : presentationData.strings.StoryList_SavedEmptyState_Title, - text: self.isArchive ? presentationData.strings.StoryList_ArchivedEmptyState_Text : presentationData.strings.StoryList_SavedEmptyState_Text, - actionTitle: self.isArchive ? nil : presentationData.strings.StoryList_SavedEmptyAction, + title: self.isArchive ? presentationData.strings.StoryList_ArchivedEmptyState_Title : presentationData.strings.StoryList_SavedEmptyPosts_Title, + text: self.isArchive ? presentationData.strings.StoryList_ArchivedEmptyState_Text : presentationData.strings.StoryList_SavedEmptyPosts_Text, + actionTitle: self.isArchive ? nil : presentationData.strings.StoryList_SavedAddAction, action: { [weak self] in guard let self else { return } self.emptyAction?() + }, + additionalActionTitle: self.isArchive ? nil : presentationData.strings.StoryList_SavedEmptyAction, + additionalAction: { [weak self] in + guard let self else { + return + } + self.additionalEmptyAction?() } )), environment: {}, diff --git a/submodules/TelegramUI/Components/PeerSelectionController/Sources/PeerSelectionControllerNode.swift b/submodules/TelegramUI/Components/PeerSelectionController/Sources/PeerSelectionControllerNode.swift index fa7762a1894..f57a661601f 100644 --- a/submodules/TelegramUI/Components/PeerSelectionController/Sources/PeerSelectionControllerNode.swift +++ b/submodules/TelegramUI/Components/PeerSelectionController/Sources/PeerSelectionControllerNode.swift @@ -591,6 +591,8 @@ final class PeerSelectionControllerNode: ASDisplayNode { }, sendContextResult: { _, _, _, _ in return false }, sendBotCommand: { _, _ in + }, sendShortcut: { _ in + }, openEditShortcuts: { }, sendBotStart: { _ in }, botSwitchChatWithPayload: { _, _ in }, beginMediaRecording: { _ in diff --git a/submodules/TelegramUI/Components/PlainButtonComponent/Sources/PlainButtonComponent.swift b/submodules/TelegramUI/Components/PlainButtonComponent/Sources/PlainButtonComponent.swift index cfce4784d2a..f91488ec89c 100644 --- a/submodules/TelegramUI/Components/PlainButtonComponent/Sources/PlainButtonComponent.swift +++ b/submodules/TelegramUI/Components/PlainButtonComponent/Sources/PlainButtonComponent.swift @@ -11,6 +11,7 @@ public final class PlainButtonComponent: Component { } public let content: AnyComponent + public let background: AnyComponent? public let effectAlignment: EffectAlignment public let minSize: CGSize? public let contentInsets: UIEdgeInsets @@ -23,6 +24,7 @@ public final class PlainButtonComponent: Component { public init( content: AnyComponent, + background: AnyComponent? = nil, effectAlignment: EffectAlignment, minSize: CGSize? = nil, contentInsets: UIEdgeInsets = UIEdgeInsets(), @@ -34,6 +36,7 @@ public final class PlainButtonComponent: Component { tag: AnyObject? = nil ) { self.content = content + self.background = background self.effectAlignment = effectAlignment self.minSize = minSize self.contentInsets = contentInsets @@ -49,6 +52,9 @@ public final class PlainButtonComponent: Component { if lhs.content != rhs.content { return false } + if lhs.background != rhs.background { + return false + } if lhs.effectAlignment != rhs.effectAlignment { return false } @@ -92,6 +98,7 @@ public final class PlainButtonComponent: Component { private let contentContainer = UIView() private let content = ComponentView() + private var background: ComponentView? public var contentView: UIView? { return self.content.view @@ -220,7 +227,12 @@ public final class PlainButtonComponent: Component { } let contentFrame = CGRect(origin: CGPoint(x: component.contentInsets.left + floor((size.width - component.contentInsets.left - component.contentInsets.right - contentSize.width) * 0.5), y: component.contentInsets.top + floor((size.height - component.contentInsets.top - component.contentInsets.bottom - contentSize.height) * 0.5)), size: contentSize) - contentTransition.setPosition(view: contentView, position: CGPoint(x: contentFrame.minX + contentFrame.width * contentView.layer.anchorPoint.x, y: contentFrame.minY + contentFrame.height * contentView.layer.anchorPoint.y)) + let contentPosition = CGPoint(x: contentFrame.minX + contentFrame.width * contentView.layer.anchorPoint.x, y: contentFrame.minY + contentFrame.height * contentView.layer.anchorPoint.y) + if !component.animateContents && (abs(contentView.center.x - contentPosition.x) <= 2.0 && abs(contentView.center.y - contentPosition.y) <= 2.0){ + contentView.center = contentPosition + } else { + contentTransition.setPosition(view: contentView, position: contentPosition) + } if component.animateContents { contentTransition.setBounds(view: contentView, bounds: CGRect(origin: CGPoint(), size: contentFrame.size)) @@ -243,6 +255,33 @@ public final class PlainButtonComponent: Component { transition.setBounds(view: self.contentContainer, bounds: CGRect(origin: CGPoint(), size: size)) transition.setPosition(view: self.contentContainer, position: CGPoint(x: size.width * anchorX, y: size.height * 0.5)) + if let backgroundValue = component.background { + var backgroundTransition = transition + let background: ComponentView + if let current = self.background { + background = current + } else { + backgroundTransition = .immediate + background = ComponentView() + self.background = background + } + let _ = background.update( + transition: backgroundTransition, + component: backgroundValue, + environment: {}, + containerSize: size + ) + if let backgroundView = background.view { + if backgroundView.superview == nil { + self.contentContainer.insertSubview(backgroundView, at: 0) + } + backgroundTransition.setFrame(view: backgroundView, frame: CGRect(origin: CGPoint(), size: size)) + } + } else if let background = self.background { + self.background = nil + background.view?.removeFromSuperview() + } + return size } } diff --git a/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/BUILD b/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/BUILD new file mode 100644 index 00000000000..b7f87546ebf --- /dev/null +++ b/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/BUILD @@ -0,0 +1,55 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "AutomaticBusinessMessageSetupScreen", + module_name = "AutomaticBusinessMessageSetupScreen", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/AsyncDisplayKit", + "//submodules/Display", + "//submodules/Postbox", + "//submodules/TelegramCore", + "//submodules/SSignalKit/SwiftSignalKit", + "//submodules/TelegramPresentationData", + "//submodules/AccountContext", + "//submodules/PresentationDataUtils", + "//submodules/Markdown", + "//submodules/ComponentFlow", + "//submodules/Components/ViewControllerComponent", + "//submodules/Components/BundleIconComponent", + "//submodules/Components/MultilineTextComponent", + "//submodules/Components/BalancedTextComponent", + "//submodules/TelegramUI/Components/AnimatedTextComponent", + "//submodules/TelegramUI/Components/ButtonComponent", + "//submodules/TelegramUI/Components/BackButtonComponent", + "//submodules/TelegramUI/Components/ListSectionComponent", + "//submodules/TelegramUI/Components/ListActionItemComponent", + "//submodules/TelegramUI/Components/ListTextFieldItemComponent", + "//submodules/TelegramUI/Components/LottieComponent", + "//submodules/AvatarNode", + "//submodules/TelegramUI/Components/PlainButtonComponent", + "//submodules/TelegramUI/Components/Stories/PeerListItemComponent", + "//submodules/TelegramUI/Components/ListItemSliderSelectorComponent", + "//submodules/ShimmerEffect", + "//submodules/ChatListUI", + "//submodules/MergeLists", + "//submodules/Components/ComponentDisplayAdapters", + "//submodules/ItemListPeerActionItem", + "//submodules/ItemListUI", + "//submodules/TelegramUI/Components/Settings/QuickReplyNameAlertController", + "//submodules/DateSelectionUI", + "//submodules/TelegramStringFormatting", + "//submodules/TelegramUI/Components/TimeSelectionActionSheet", + "//submodules/TelegramUI/Components/ChatListHeaderComponent", + "//submodules/AttachmentUI", + "//submodules/SearchBarNode", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/AutomaticBusinessMessageListItemComponent.swift b/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/AutomaticBusinessMessageListItemComponent.swift new file mode 100644 index 00000000000..ad53e21b11b --- /dev/null +++ b/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/AutomaticBusinessMessageListItemComponent.swift @@ -0,0 +1,318 @@ +import Foundation +import UIKit +import Display +import ComponentFlow +import ListSectionComponent +import TelegramPresentationData +import AppBundle +import ChatListUI +import AccountContext +import Postbox +import TelegramCore + +final class GreetingMessageListItemComponent: Component { + let context: AccountContext + let theme: PresentationTheme + let strings: PresentationStrings + let accountPeer: EnginePeer + let message: EngineMessage + let count: Int + let action: (() -> Void)? + + init( + context: AccountContext, + theme: PresentationTheme, + strings: PresentationStrings, + accountPeer: EnginePeer, + message: EngineMessage, + count: Int, + action: (() -> Void)? = nil + ) { + self.context = context + self.theme = theme + self.strings = strings + self.accountPeer = accountPeer + self.message = message + self.count = count + self.action = action + } + + static func ==(lhs: GreetingMessageListItemComponent, rhs: GreetingMessageListItemComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.theme !== rhs.theme { + return false + } + if lhs.strings !== rhs.strings { + return false + } + if lhs.accountPeer != rhs.accountPeer { + return false + } + if lhs.message != rhs.message { + return false + } + if lhs.count != rhs.count { + return false + } + if (lhs.action == nil) != (rhs.action == nil) { + return false + } + return true + } + + final class View: HighlightTrackingButton, ListSectionComponent.ChildView { + private var component: GreetingMessageListItemComponent? + private weak var componentState: EmptyComponentState? + + private var chatListPresentationData: ChatListPresentationData? + private var chatListNodeInteraction: ChatListNodeInteraction? + + private var itemNode: ListViewItemNode? + + var customUpdateIsHighlighted: ((Bool) -> Void)? + private(set) var separatorInset: CGFloat = 0.0 + + override init(frame: CGRect) { + super.init(frame: frame) + + self.addTarget(self, action: #selector(self.pressed), for: .touchUpInside) + self.internalHighligthedChanged = { [weak self] isHighlighted in + guard let self, let component = self.component, component.action != nil else { + return + } + if let customUpdateIsHighlighted = self.customUpdateIsHighlighted { + customUpdateIsHighlighted(isHighlighted) + } + } + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + @objc private func pressed() { + self.component?.action?() + } + + func update(component: GreetingMessageListItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + let previousComponent = self.component + self.component = component + self.componentState = state + + self.isEnabled = component.action != nil + + let chatListPresentationData: ChatListPresentationData + if let current = self.chatListPresentationData, let previousComponent, previousComponent.theme === component.theme { + chatListPresentationData = current + } else { + let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 }) + chatListPresentationData = ChatListPresentationData( + theme: component.theme, + fontSize: presentationData.listsFontSize, + strings: component.strings, + dateTimeFormat: presentationData.dateTimeFormat, + nameSortOrder: presentationData.nameSortOrder, + nameDisplayOrder: presentationData.nameDisplayOrder, + disableAnimations: false + ) + self.chatListPresentationData = chatListPresentationData + } + + let chatListNodeInteraction: ChatListNodeInteraction + if let current = self.chatListNodeInteraction { + chatListNodeInteraction = current + } else { + chatListNodeInteraction = ChatListNodeInteraction( + context: component.context, + animationCache: component.context.animationCache, + animationRenderer: component.context.animationRenderer, + activateSearch: { + }, + peerSelected: { _, _, _, _ in + }, + disabledPeerSelected: { _, _, _ in + }, + togglePeerSelected: { _, _ in + }, + togglePeersSelection: { _, _ in + }, + additionalCategorySelected: { _ in + }, + messageSelected: { _, _, _, _ in + }, + groupSelected: { _ in + }, + addContact: { _ in + }, + setPeerIdWithRevealedOptions: { _, _ in + }, + setItemPinned: { _, _ in + }, + setPeerMuted: { _, _ in + }, + setPeerThreadMuted: { _, _, _ in + }, + deletePeer: { _, _ in + }, + deletePeerThread: { _, _ in + }, + setPeerThreadStopped: { _, _, _ in + }, + setPeerThreadPinned: { _, _, _ in + }, + setPeerThreadHidden: { _, _, _ in + }, + updatePeerGrouping: { _, _ in + }, + togglePeerMarkedUnread: { _, _ in + }, + toggleArchivedFolderHiddenByDefault: { + }, + toggleThreadsSelection: { _, _ in + }, + hidePsa: { _ in + }, + activateChatPreview: { _, _, _, _, _ in + }, + present: { _ in + }, + openForumThread: { _, _ in + }, + openStorageManagement: { + }, + openPasswordSetup: { + }, + openPremiumIntro: { + }, + openPremiumGift: { + }, + openActiveSessions: { + }, + performActiveSessionAction: { _, _ in + }, + openChatFolderUpdates: { + }, + hideChatFolderUpdates: { + }, + openStories: { _, _ in + }, + dismissNotice: { _ in + }, + editPeer: { _ in + } + ) + self.chatListNodeInteraction = chatListNodeInteraction + } + + let chatListItem = ChatListItem( + presentationData: chatListPresentationData, + context: component.context, + chatListLocation: .chatList(groupId: .root), + filterData: nil, + index: EngineChatList.Item.Index.chatList(ChatListIndex(pinningIndex: nil, messageIndex: component.message.index)), + content: .peer(ChatListItemContent.PeerData( + messages: [component.message], + peer: EngineRenderedPeer(peer: component.accountPeer), + threadInfo: nil, + combinedReadState: nil, + isRemovedFromTotalUnreadCount: false, + presence: nil, + hasUnseenMentions: false, + hasUnseenReactions: false, + draftState: nil, + mediaDraftContentType: nil, + inputActivities: nil, + promoInfo: nil, + ignoreUnreadBadge: false, + displayAsMessage: false, + hasFailedMessages: false, + forumTopicData: nil, + topForumTopicItems: [], + autoremoveTimeout: nil, + storyState: nil, + requiresPremiumForMessaging: false, + displayAsTopicList: false, + tags: [], + customMessageListData: ChatListItemContent.CustomMessageListData( + commandPrefix: nil, + searchQuery: nil, + messageCount: component.count, + hideSeparator: true + ) + )), + editing: false, + hasActiveRevealControls: false, + selected: false, + header: nil, + enableContextActions: false, + hiddenOffset: false, + interaction: chatListNodeInteraction + ) + var itemNode: ListViewItemNode? + let params = ListViewItemLayoutParams(width: availableSize.width, leftInset: 0.0, rightInset: 0.0, availableHeight: 1000.0) + if let current = self.itemNode { + itemNode = current + chatListItem.updateNode( + async: { f in f () }, + node: { + return current + }, + params: params, + previousItem: nil, + nextItem: nil, animation: .None, + completion: { layout, apply in + let nodeFrame = CGRect(origin: current.frame.origin, size: CGSize(width: layout.size.width, height: layout.size.height)) + + current.contentSize = layout.contentSize + current.insets = layout.insets + current.frame = nodeFrame + + apply(ListViewItemApply(isOnScreen: true)) + }) + } else { + var outItemNode: ListViewItemNode? + chatListItem.nodeConfiguredForParams( + async: { f in f() }, + params: params, + synchronousLoads: true, + previousItem: nil, + nextItem: nil, + completion: { node, apply in + outItemNode = node + apply().1(ListViewItemApply(isOnScreen: true)) + } + ) + itemNode = outItemNode + } + + let size = CGSize(width: availableSize.width, height: itemNode?.contentSize.height ?? 44.0) + + if self.itemNode !== itemNode { + self.itemNode?.removeFromSupernode() + + self.itemNode = itemNode + if let itemNode { + itemNode.isUserInteractionEnabled = false + self.addSubview(itemNode.view) + } + } + if let itemNode = self.itemNode { + itemNode.frame = CGRect(origin: CGPoint(), size: size) + } + + self.separatorInset = 76.0 + + return size + } + } + + func makeView() -> View { + return View(frame: CGRect()) + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} diff --git a/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/AutomaticBusinessMessageSetupChatContents.swift b/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/AutomaticBusinessMessageSetupChatContents.swift new file mode 100644 index 00000000000..b002aa3cc56 --- /dev/null +++ b/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/AutomaticBusinessMessageSetupChatContents.swift @@ -0,0 +1,252 @@ +import Foundation +import UIKit +import SwiftSignalKit +import Postbox +import TelegramCore +import AccountContext + +final class AutomaticBusinessMessageSetupChatContents: ChatCustomContentsProtocol { + private final class PendingMessageContext { + let disposable = MetaDisposable() + var message: Message? + + init() { + } + } + + private final class Impl { + let queue: Queue + let context: AccountContext + + private var shortcut: String + private var shortcutId: Int32? + + private(set) var mergedHistoryView: MessageHistoryView? + private var sourceHistoryView: MessageHistoryView? + + private var pendingMessages: [PendingMessageContext] = [] + private var historyViewDisposable: Disposable? + private var pendingHistoryViewDisposable: Disposable? + let historyViewStream = ValuePipe<(MessageHistoryView, ViewUpdateType)>() + private var nextUpdateIsHoleFill: Bool = false + + init(queue: Queue, context: AccountContext, shortcut: String, shortcutId: Int32?) { + self.queue = queue + self.context = context + self.shortcut = shortcut + self.shortcutId = shortcutId + + self.updateHistoryViewRequest(reload: false) + } + + deinit { + for context in self.pendingMessages { + context.disposable.dispose() + } + self.historyViewDisposable?.dispose() + self.pendingHistoryViewDisposable?.dispose() + } + + private func updateHistoryViewRequest(reload: Bool) { + if let shortcutId = self.shortcutId { + self.pendingHistoryViewDisposable?.dispose() + self.pendingHistoryViewDisposable = nil + + if self.historyViewDisposable == nil || reload { + self.historyViewDisposable?.dispose() + + self.historyViewDisposable = (self.context.account.viewTracker.quickReplyMessagesViewForLocation(quickReplyId: shortcutId) + |> deliverOn(self.queue)).start(next: { [weak self] view, update, _ in + guard let self else { + return + } + if update == .FillHole { + self.nextUpdateIsHoleFill = true + self.updateHistoryViewRequest(reload: true) + return + } + + let nextUpdateIsHoleFill = self.nextUpdateIsHoleFill + self.nextUpdateIsHoleFill = false + + self.sourceHistoryView = view + + if !view.entries.contains(where: { $0.message.id.namespace == Namespaces.Message.QuickReplyCloud }) { + self.shortcutId = nil + } + + self.updateHistoryView(updateType: nextUpdateIsHoleFill ? .FillHole : .Generic) + }) + } + } else { + self.historyViewDisposable?.dispose() + self.historyViewDisposable = nil + + self.pendingHistoryViewDisposable = (self.context.account.viewTracker.pendingQuickReplyMessagesViewForLocation(shortcut: self.shortcut) + |> deliverOn(self.queue)).start(next: { [weak self] view, _, _ in + guard let self else { + return + } + + let nextUpdateIsHoleFill = self.nextUpdateIsHoleFill + self.nextUpdateIsHoleFill = false + + self.sourceHistoryView = view + + self.updateHistoryView(updateType: nextUpdateIsHoleFill ? .FillHole : .Generic) + }) + + /*if self.sourceHistoryView == nil { + let sourceHistoryView = MessageHistoryView(tag: nil, namespaces: .just(Namespaces.Message.allQuickReply), entries: [], holeEarlier: false, holeLater: false, isLoading: false) + self.sourceHistoryView = sourceHistoryView + self.updateHistoryView(updateType: .Initial) + }*/ + } + } + + private func updateHistoryView(updateType: ViewUpdateType) { + var entries = self.sourceHistoryView?.entries ?? [] + for pendingMessage in self.pendingMessages { + if let message = pendingMessage.message { + if !entries.contains(where: { $0.message.stableId == message.stableId }) { + entries.append(MessageHistoryEntry( + message: message, + isRead: true, + location: nil, + monthLocation: nil, + attributes: MutableMessageHistoryEntryAttributes( + authorIsContact: false + ) + )) + } + } + } + entries.sort(by: { $0.message.index < $1.message.index }) + + let mergedHistoryView = MessageHistoryView(tag: nil, namespaces: .just(Namespaces.Message.allQuickReply), entries: entries, holeEarlier: false, holeLater: false, isLoading: false) + self.mergedHistoryView = mergedHistoryView + + self.historyViewStream.putNext((mergedHistoryView, updateType)) + } + + func enqueueMessages(messages: [EnqueueMessage]) { + let threadId = self.shortcutId.flatMap(Int64.init) + let _ = (TelegramCore.enqueueMessages(account: self.context.account, peerId: self.context.account.peerId, messages: messages.map { message in + return message.withUpdatedThreadId(threadId).withUpdatedAttributes { attributes in + var attributes = attributes + attributes.removeAll(where: { $0 is OutgoingQuickReplyMessageAttribute }) + attributes.append(OutgoingQuickReplyMessageAttribute(shortcut: self.shortcut)) + return attributes + } + }) + |> deliverOn(self.queue)).startStandalone(next: { [weak self] result in + guard let self else { + return + } + if self.shortcutId != nil { + return + } + for id in result { + if let id { + let pendingMessage = PendingMessageContext() + self.pendingMessages.append(pendingMessage) + pendingMessage.disposable.set(( + self.context.account.postbox.messageView(id) + |> deliverOn(self.queue) + ).startStrict(next: { [weak self, weak pendingMessage] messageView in + guard let self else { + return + } + guard let pendingMessage else { + return + } + pendingMessage.message = messageView.message + if let message = pendingMessage.message, message.id.namespace == Namespaces.Message.QuickReplyCloud, let threadId = message.threadId { + self.shortcutId = Int32(clamping: threadId) + self.updateHistoryViewRequest(reload: true) + } else { + self.updateHistoryView(updateType: .Generic) + } + })) + } + } + }) + } + + func deleteMessages(ids: [EngineMessage.Id]) { + let _ = self.context.engine.messages.deleteMessagesInteractively(messageIds: ids, type: .forEveryone).startStandalone() + } + + func editMessage(id: EngineMessage.Id, text: String, media: RequestEditMessageMedia, entities: TextEntitiesMessageAttribute?, webpagePreviewAttribute: WebpagePreviewMessageAttribute?, disableUrlPreview: Bool) { + } + + func quickReplyUpdateShortcut(value: String) { + self.shortcut = value + if let shortcutId = self.shortcutId { + self.context.engine.accountData.editMessageShortcut(id: shortcutId, shortcut: value) + } + } + } + + var kind: ChatCustomContentsKind + + var historyView: Signal<(MessageHistoryView, ViewUpdateType), NoError> { + return self.impl.signalWith({ impl, subscriber in + if let mergedHistoryView = impl.mergedHistoryView { + subscriber.putNext((mergedHistoryView, .Initial)) + } + return impl.historyViewStream.signal().start(next: subscriber.putNext) + }) + } + + var messageLimit: Int? { + return 20 + } + + private let queue: Queue + private let impl: QueueLocalObject + + init(context: AccountContext, kind: ChatCustomContentsKind, shortcutId: Int32?) { + self.kind = kind + + let initialShortcut: String + switch kind { + case let .quickReplyMessageInput(shortcut, _): + initialShortcut = shortcut + } + + let queue = Queue() + self.queue = queue + self.impl = QueueLocalObject(queue: queue, generate: { + return Impl(queue: queue, context: context, shortcut: initialShortcut, shortcutId: shortcutId) + }) + } + + func enqueueMessages(messages: [EnqueueMessage]) { + self.impl.with { impl in + impl.enqueueMessages(messages: messages) + } + } + + func deleteMessages(ids: [EngineMessage.Id]) { + self.impl.with { impl in + impl.deleteMessages(ids: ids) + } + } + + func editMessage(id: EngineMessage.Id, text: String, media: RequestEditMessageMedia, entities: TextEntitiesMessageAttribute?, webpagePreviewAttribute: WebpagePreviewMessageAttribute?, disableUrlPreview: Bool) { + self.impl.with { impl in + impl.editMessage(id: id, text: text, media: media, entities: entities, webpagePreviewAttribute: webpagePreviewAttribute, disableUrlPreview: disableUrlPreview) + } + } + + func quickReplyUpdateShortcut(value: String) { + switch self.kind { + case let .quickReplyMessageInput(_, shortcutType): + self.kind = .quickReplyMessageInput(shortcut: value, shortcutType: shortcutType) + self.impl.with { impl in + impl.quickReplyUpdateShortcut(value: value) + } + } + } +} diff --git a/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/AutomaticBusinessMessageSetupScreen.swift b/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/AutomaticBusinessMessageSetupScreen.swift new file mode 100644 index 00000000000..7e9fe2c3f2a --- /dev/null +++ b/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/AutomaticBusinessMessageSetupScreen.swift @@ -0,0 +1,1667 @@ +import Foundation +import UIKit +import Photos +import Display +import AsyncDisplayKit +import SwiftSignalKit +import Postbox +import TelegramCore +import TelegramPresentationData +import TelegramUIPreferences +import PresentationDataUtils +import AccountContext +import ComponentFlow +import ViewControllerComponent +import MultilineTextComponent +import BalancedTextComponent +import BackButtonComponent +import ListSectionComponent +import ListActionItemComponent +import ListTextFieldItemComponent +import BundleIconComponent +import LottieComponent +import Markdown +import PeerListItemComponent +import AvatarNode +import ListItemSliderSelectorComponent +import DateSelectionUI +import PlainButtonComponent +import TelegramStringFormatting +import TimeSelectionActionSheet + +private let checkIcon: UIImage = { + return generateImage(CGSize(width: 12.0, height: 10.0), rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + context.setStrokeColor(UIColor.white.cgColor) + context.setLineWidth(1.98) + context.setLineCap(.round) + context.setLineJoin(.round) + context.translateBy(x: 1.0, y: 1.0) + + let _ = try? drawSvgPath(context, path: "M0.215053763,4.36080467 L3.31621263,7.70466293 L3.31621263,7.70466293 C3.35339229,7.74475231 3.41603123,7.74711109 3.45612061,7.70993143 C3.45920681,7.70706923 3.46210733,7.70401312 3.46480451,7.70078171 L9.89247312,0 S ") + })!.withRenderingMode(.alwaysTemplate) +}() + +final class AutomaticBusinessMessageSetupScreenComponent: Component { + typealias EnvironmentType = ViewControllerComponentContainer.Environment + + let context: AccountContext + let initialData: AutomaticBusinessMessageSetupScreen.InitialData + let mode: AutomaticBusinessMessageSetupScreen.Mode + + init( + context: AccountContext, + initialData: AutomaticBusinessMessageSetupScreen.InitialData, + mode: AutomaticBusinessMessageSetupScreen.Mode + ) { + self.context = context + self.initialData = initialData + self.mode = mode + } + + static func ==(lhs: AutomaticBusinessMessageSetupScreenComponent, rhs: AutomaticBusinessMessageSetupScreenComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.mode != rhs.mode { + return false + } + + return true + } + + private final class ScrollView: UIScrollView { + override func touchesShouldCancel(in view: UIView) -> Bool { + return true + } + } + + struct AdditionalPeerList { + enum Category: Int { + case newChats = 0 + case existingChats = 1 + case contacts = 2 + case nonContacts = 3 + } + + struct Peer { + var peer: EnginePeer + var isContact: Bool + + init(peer: EnginePeer, isContact: Bool) { + self.peer = peer + self.isContact = isContact + } + } + + var categories: Set + var peers: [Peer] + + init(categories: Set, peers: [Peer]) { + self.categories = categories + self.peers = peers + } + } + + private enum Schedule { + case always + case outsideBusinessHours + case custom + } + + final class View: UIView, UIScrollViewDelegate { + private let topOverscrollLayer = SimpleLayer() + private let scrollView: ScrollView + + private let navigationTitle = ComponentView() + private let icon = ComponentView() + private let subtitle = ComponentView() + private let generalSection = ComponentView() + private let messagesSection = ComponentView() + private let scheduleSection = ComponentView() + private let customScheduleSection = ComponentView() + private let sendWhenOfflineSection = ComponentView() + private let accessSection = ComponentView() + private let excludedSection = ComponentView() + private let periodSection = ComponentView() + + private var ignoreScrolling: Bool = false + private var isUpdating: Bool = false + + private var component: AutomaticBusinessMessageSetupScreenComponent? + private(set) weak var state: EmptyComponentState? + private var environment: EnvironmentType? + + private var isOn: Bool = false + private var accountPeer: EnginePeer? + private var currentShortcut: ShortcutMessageList.Item? + private var currentShortcutDisposable: Disposable? + + private var schedule: Schedule = .always + private var customScheduleStart: Date? + private var customScheduleEnd: Date? + + private var sendWhenOffline: Bool = true + + private var hasAccessToAllChatsByDefault: Bool = true + private var additionalPeerList = AdditionalPeerList( + categories: Set(), + peers: [] + ) + + private var replyToMessages: Bool = true + + private var inactivityDays: Int = 7 + + override init(frame: CGRect) { + self.scrollView = ScrollView() + self.scrollView.showsVerticalScrollIndicator = true + self.scrollView.showsHorizontalScrollIndicator = false + self.scrollView.scrollsToTop = false + self.scrollView.delaysContentTouches = false + self.scrollView.canCancelContentTouches = true + self.scrollView.contentInsetAdjustmentBehavior = .never + if #available(iOS 13.0, *) { + self.scrollView.automaticallyAdjustsScrollIndicatorInsets = false + } + self.scrollView.alwaysBounceVertical = true + + super.init(frame: frame) + + self.scrollView.delegate = self + self.addSubview(self.scrollView) + + self.scrollView.layer.addSublayer(self.topOverscrollLayer) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + self.currentShortcutDisposable?.dispose() + } + + func scrollToTop() { + self.scrollView.setContentOffset(CGPoint(), animated: true) + } + + func attemptNavigation(complete: @escaping () -> Void) -> Bool { + guard let component = self.component else { + return true + } + + let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } + + if self.isOn { + if !self.hasAccessToAllChatsByDefault && self.additionalPeerList.categories.isEmpty && self.additionalPeerList.peers.isEmpty { + self.environment?.controller()?.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: nil, text: presentationData.strings.BusinessMessageSetup_ErrorNoRecipients_Text, actions: [ + TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: { + }), + TextAlertAction(type: .defaultAction, title: presentationData.strings.BusinessMessageSetup_ErrorNoRecipients_ResetAction, action: { + complete() + }) + ]), in: .window(.root)) + + return false + } + + if case .away = component.mode, case .custom = self.schedule { + var errorText: String? + if let customScheduleStart = self.customScheduleStart, let customScheduleEnd = self.customScheduleEnd { + if customScheduleStart >= customScheduleEnd { + errorText = presentationData.strings.BusinessMessageSetup_ErrorScheduleEndTimeBeforeStartTime_Text + } + } else { + if self.customScheduleStart == nil && self.customScheduleEnd == nil { + errorText = presentationData.strings.BusinessMessageSetup_ErrorScheduleTimeMissing_Text + } else if self.customScheduleStart == nil { + errorText = presentationData.strings.BusinessMessageSetup_ErrorScheduleStartTimeMissing_Text + } else { + errorText = presentationData.strings.BusinessMessageSetup_ErrorScheduleEndTimeMissing_Text + } + } + + if let errorText { + self.environment?.controller()?.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: nil, text: errorText, actions: [ + TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: { + }), + TextAlertAction(type: .defaultAction, title: presentationData.strings.BusinessMessageSetup_ErrorScheduleTime_ResetAction, action: { + complete() + }) + ]), in: .window(.root)) + return false + } + } + } + + var mappedCategories: TelegramBusinessRecipients.Categories = [] + if self.additionalPeerList.categories.contains(.existingChats) { + mappedCategories.insert(.existingChats) + } + if self.additionalPeerList.categories.contains(.newChats) { + mappedCategories.insert(.newChats) + } + if self.additionalPeerList.categories.contains(.contacts) { + mappedCategories.insert(.contacts) + } + if self.additionalPeerList.categories.contains(.nonContacts) { + mappedCategories.insert(.nonContacts) + } + let recipients = TelegramBusinessRecipients( + categories: mappedCategories, + additionalPeers: Set(self.additionalPeerList.peers.map(\.peer.id)), + exclude: self.hasAccessToAllChatsByDefault + ) + + switch component.mode { + case .greeting: + var greetingMessage: TelegramBusinessGreetingMessage? + if self.isOn, let currentShortcut = self.currentShortcut, let shortcutId = currentShortcut.id { + greetingMessage = TelegramBusinessGreetingMessage( + shortcutId: shortcutId, + recipients: recipients, + inactivityDays: self.inactivityDays + ) + } + let _ = component.context.engine.accountData.updateBusinessGreetingMessage(greetingMessage: greetingMessage).startStandalone() + case .away: + var awayMessage: TelegramBusinessAwayMessage? + if self.isOn, let currentShortcut = self.currentShortcut, let shortcutId = currentShortcut.id { + let mappedSchedule: TelegramBusinessAwayMessage.Schedule + switch self.schedule { + case .always: + mappedSchedule = .always + case .outsideBusinessHours: + mappedSchedule = .outsideWorkingHours + case .custom: + if let customScheduleStart = self.customScheduleStart, let customScheduleEnd = self.customScheduleEnd { + mappedSchedule = .custom(beginTimestamp: Int32(customScheduleStart.timeIntervalSince1970), endTimestamp: Int32(customScheduleEnd.timeIntervalSince1970)) + } else { + return false + } + } + awayMessage = TelegramBusinessAwayMessage( + shortcutId: shortcutId, + recipients: recipients, + schedule: mappedSchedule, + sendWhenOffline: self.sendWhenOffline + ) + } + let _ = component.context.engine.accountData.updateBusinessAwayMessage(awayMessage: awayMessage).startStandalone() + } + + return true + } + + func scrollViewDidScroll(_ scrollView: UIScrollView) { + if !self.ignoreScrolling { + self.updateScrolling(transition: .immediate) + } + } + + var scrolledUp = true + private func updateScrolling(transition: Transition) { + let navigationRevealOffsetY: CGFloat = 0.0 + + let navigationAlphaDistance: CGFloat = 16.0 + let navigationAlpha: CGFloat = max(0.0, min(1.0, (self.scrollView.contentOffset.y - navigationRevealOffsetY) / navigationAlphaDistance)) + if let controller = self.environment?.controller(), let navigationBar = controller.navigationBar { + transition.setAlpha(layer: navigationBar.backgroundNode.layer, alpha: navigationAlpha) + transition.setAlpha(layer: navigationBar.stripeNode.layer, alpha: navigationAlpha) + } + + var scrolledUp = false + if navigationAlpha < 0.5 { + scrolledUp = true + } else if navigationAlpha > 0.5 { + scrolledUp = false + } + + if self.scrolledUp != scrolledUp { + self.scrolledUp = scrolledUp + if !self.isUpdating { + self.state?.updated() + } + } + + if let navigationTitleView = self.navigationTitle.view { + transition.setAlpha(view: navigationTitleView, alpha: 1.0) + } + } + + private func openAdditionalPeerListSetup() { + guard let component = self.component, let enviroment = self.environment else { + return + } + + enum AdditionalCategoryId: Int { + case existingChats + case newChats + case contacts + case nonContacts + } + + let additionalCategories: [ChatListNodeAdditionalCategory] = [ + ChatListNodeAdditionalCategory( + id: self.hasAccessToAllChatsByDefault ? AdditionalCategoryId.existingChats.rawValue : AdditionalCategoryId.newChats.rawValue, + icon: generateAvatarImage(size: CGSize(width: 40.0, height: 40.0), icon: generateTintedImage(image: UIImage(bundleImageName: self.hasAccessToAllChatsByDefault ? "Chat List/Filters/Chats" : "Chat List/Filters/NewChats"), color: .white), cornerRadius: 12.0, color: .purple), + smallIcon: generateAvatarImage(size: CGSize(width: 22.0, height: 22.0), icon: generateTintedImage(image: UIImage(bundleImageName: self.hasAccessToAllChatsByDefault ? "Chat List/Filters/Chats" : "Chat List/Filters/NewChats"), color: .white), iconScale: 0.6, cornerRadius: 6.0, circleCorners: true, color: .purple), + title: self.hasAccessToAllChatsByDefault ? enviroment.strings.BusinessMessageSetup_Recipients_CategoryExistingChats : enviroment.strings.BusinessMessageSetup_Recipients_CategoryNewChats + ), + ChatListNodeAdditionalCategory( + id: AdditionalCategoryId.contacts.rawValue, + icon: generateAvatarImage(size: CGSize(width: 40.0, height: 40.0), icon: generateTintedImage(image: UIImage(bundleImageName: "Chat List/Filters/Contact"), color: .white), cornerRadius: 12.0, color: .blue), + smallIcon: generateAvatarImage(size: CGSize(width: 22.0, height: 22.0), icon: generateTintedImage(image: UIImage(bundleImageName: "Chat List/Filters/Contact"), color: .white), iconScale: 0.6, cornerRadius: 6.0, circleCorners: true, color: .blue), + title: enviroment.strings.BusinessMessageSetup_Recipients_CategoryContacts + ), + ChatListNodeAdditionalCategory( + id: AdditionalCategoryId.nonContacts.rawValue, + icon: generateAvatarImage(size: CGSize(width: 40.0, height: 40.0), icon: generateTintedImage(image: UIImage(bundleImageName: "Chat List/Filters/User"), color: .white), cornerRadius: 12.0, color: .yellow), + smallIcon: generateAvatarImage(size: CGSize(width: 22.0, height: 22.0), icon: generateTintedImage(image: UIImage(bundleImageName: "Chat List/Filters/User"), color: .white), iconScale: 0.6, cornerRadius: 6.0, circleCorners: true, color: .yellow), + title: enviroment.strings.BusinessMessageSetup_Recipients_CategoryNonContacts + ) + ] + var selectedCategories = Set() + for category in self.additionalPeerList.categories { + switch category { + case .existingChats: + selectedCategories.insert(AdditionalCategoryId.existingChats.rawValue) + case .newChats: + selectedCategories.insert(AdditionalCategoryId.newChats.rawValue) + case .contacts: + selectedCategories.insert(AdditionalCategoryId.contacts.rawValue) + case .nonContacts: + selectedCategories.insert(AdditionalCategoryId.nonContacts.rawValue) + } + } + + let controller = component.context.sharedContext.makeContactMultiselectionController(ContactMultiselectionControllerParams(context: component.context, mode: .chatSelection(ContactMultiselectionControllerMode.ChatSelection( + title: self.hasAccessToAllChatsByDefault ? enviroment.strings.BusinessMessageSetup_Recipients_ExcludeSearchTitle : enviroment.strings.BusinessMessageSetup_Recipients_IncludeSearchTitle, + searchPlaceholder: enviroment.strings.ChatListFilter_AddChatsSearchPlaceholder, + selectedChats: Set(self.additionalPeerList.peers.map(\.peer.id)), + additionalCategories: ContactMultiselectionControllerAdditionalCategories(categories: additionalCategories, selectedCategories: selectedCategories), + chatListFilters: nil, + onlyUsers: true + )), options: [], filters: [], alwaysEnabled: true, limit: 100, reachedLimit: { _ in + })) + controller.navigationPresentation = .modal + + let _ = (controller.result + |> take(1) + |> deliverOnMainQueue).startStandalone(next: { [weak self, weak controller] result in + guard let self, let component = self.component, case let .result(rawPeerIds, additionalCategoryIds) = result else { + controller?.dismiss() + return + } + + let peerIds = rawPeerIds.compactMap { id -> EnginePeer.Id? in + switch id { + case let .peer(id): + return id + case .deviceContact: + return nil + } + } + + let _ = (component.context.engine.data.get( + EngineDataMap( + peerIds.map(TelegramEngine.EngineData.Item.Peer.Peer.init(id:)) + ), + EngineDataMap( + peerIds.map(TelegramEngine.EngineData.Item.Peer.IsContact.init(id:)) + ) + ) + |> deliverOnMainQueue).start(next: { [weak self] peerMap, isContactMap in + guard let self else { + return + } + + let mappedCategories = additionalCategoryIds.compactMap { item -> AdditionalPeerList.Category? in + switch item { + case AdditionalCategoryId.existingChats.rawValue: + return .existingChats + case AdditionalCategoryId.newChats.rawValue: + return .newChats + case AdditionalCategoryId.contacts.rawValue: + return .contacts + case AdditionalCategoryId.nonContacts.rawValue: + return .nonContacts + default: + return nil + } + } + + self.additionalPeerList.categories = Set(mappedCategories) + + self.additionalPeerList.peers.removeAll() + for id in peerIds { + guard let maybePeer = peerMap[id], let peer = maybePeer else { + continue + } + self.additionalPeerList.peers.append(AdditionalPeerList.Peer( + peer: peer, + isContact: isContactMap[id] ?? false + )) + } + self.additionalPeerList.peers.sort(by: { lhs, rhs in + return lhs.peer.debugDisplayTitle < rhs.peer.debugDisplayTitle + }) + self.state?.updated(transition: .immediate) + + controller?.dismiss() + }) + }) + + self.environment?.controller()?.push(controller) + } + + private func openMessageList() { + guard let component = self.component else { + return + } + + let shortcutName: String + let shortcutType: ChatQuickReplyShortcutType + switch component.mode { + case .greeting: + shortcutName = "hello" + shortcutType = .greeting + case .away: + shortcutName = "away" + shortcutType = .away + } + + let contents = AutomaticBusinessMessageSetupChatContents( + context: component.context, + kind: .quickReplyMessageInput(shortcut: shortcutName, shortcutType: shortcutType), + shortcutId: self.currentShortcut?.id + ) + let chatController = component.context.sharedContext.makeChatController( + context: component.context, + chatLocation: .customChatContents, + subject: .customChatContents(contents: contents), + botStart: nil, + mode: .standard(.default) + ) + chatController.navigationPresentation = .modal + self.environment?.controller()?.push(chatController) + } + + private func openCustomScheduleDateSetup(isStartTime: Bool, isDate: Bool) { + guard let component = self.component else { + return + } + + let currentValue: Date = (isStartTime ? self.customScheduleStart : self.customScheduleEnd) ?? Date() + + if isDate { + var calendar = Calendar(identifier: .gregorian) + calendar.timeZone = TimeZone(secondsFromGMT: 0)! + let components = calendar.dateComponents([.year, .month, .day], from: currentValue) + guard let clippedDate = calendar.date(from: components) else { + return + } + + let controller = DateSelectionActionSheetController( + context: component.context, + title: nil, + currentValue: Int32(clippedDate.timeIntervalSince1970), + minimumDate: nil, + maximumDate: nil, + emptyTitle: nil, + applyValue: { [weak self] value in + guard let self else { + return + } + guard let value else { + return + } + let updatedDate = Date(timeIntervalSince1970: Double(value)) + let calendar = Calendar.current + var updatedComponents = calendar.dateComponents([.year, .month, .day], from: updatedDate) + let currentComponents = calendar.dateComponents([.hour, .minute], from: currentValue) + updatedComponents.hour = currentComponents.hour + updatedComponents.minute = currentComponents.minute + guard let updatedClippedDate = calendar.date(from: updatedComponents) else { + return + } + + if isStartTime { + self.customScheduleStart = updatedClippedDate + } else { + self.customScheduleEnd = updatedClippedDate + } + self.state?.updated(transition: .immediate) + } + ) + self.environment?.controller()?.present(controller, in: .window(.root)) + } else { + let calendar = Calendar.current + let components = calendar.dateComponents([.hour, .minute], from: currentValue) + let hour = components.hour ?? 0 + let minute = components.minute ?? 0 + + let controller = TimeSelectionActionSheet(context: component.context, currentValue: Int32(hour * 60 * 60 + minute * 60), applyValue: { [weak self] value in + guard let self else { + return + } + guard let value else { + return + } + + let updatedHour = value / (60 * 60) + let updatedMinute = (value % (60 * 60)) / 60 + + let calendar = Calendar.current + var updatedComponents = calendar.dateComponents([.year, .month, .day], from: currentValue) + updatedComponents.hour = Int(updatedHour) + updatedComponents.minute = Int(updatedMinute) + + guard let updatedClippedDate = calendar.date(from: updatedComponents) else { + return + } + + if isStartTime { + self.customScheduleStart = updatedClippedDate + } else { + self.customScheduleEnd = updatedClippedDate + } + self.state?.updated(transition: .immediate) + }) + self.environment?.controller()?.present(controller, in: .window(.root)) + } + } + + func update(component: AutomaticBusinessMessageSetupScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + self.isUpdating = true + defer { + self.isUpdating = false + } + + if self.component == nil { + self.accountPeer = component.initialData.accountPeer + + var initialRecipients: TelegramBusinessRecipients? + + let shortcutName: String + switch component.mode { + case .greeting: + shortcutName = "hello" + + if let greetingMessage = component.initialData.greetingMessage { + self.isOn = true + initialRecipients = greetingMessage.recipients + + self.inactivityDays = greetingMessage.inactivityDays + } + case .away: + shortcutName = "away" + + if let awayMessage = component.initialData.awayMessage { + self.isOn = true + + self.sendWhenOffline = awayMessage.sendWhenOffline + + initialRecipients = awayMessage.recipients + + switch awayMessage.schedule { + case .always: + self.schedule = .always + case let .custom(beginTimestamp, endTimestamp): + self.schedule = .custom + self.customScheduleStart = Date(timeIntervalSince1970: Double(beginTimestamp)) + self.customScheduleEnd = Date(timeIntervalSince1970: Double(endTimestamp)) + case .outsideWorkingHours: + if component.initialData.businessHours != nil { + self.schedule = .outsideBusinessHours + } else { + self.schedule = .always + } + } + } + } + + if let initialRecipients { + var mappedCategories = Set() + if initialRecipients.categories.contains(.existingChats) { + mappedCategories.insert(.existingChats) + } + if initialRecipients.categories.contains(.newChats) { + mappedCategories.insert(.newChats) + } + if initialRecipients.categories.contains(.contacts) { + mappedCategories.insert(.contacts) + } + if initialRecipients.categories.contains(.nonContacts) { + mappedCategories.insert(.nonContacts) + } + + var additionalPeers: [AdditionalPeerList.Peer] = [] + for peerId in initialRecipients.additionalPeers { + if let peer = component.initialData.additionalPeers[peerId] { + additionalPeers.append(peer) + } + } + + self.additionalPeerList = AdditionalPeerList( + categories: mappedCategories, + peers: additionalPeers + ) + + self.hasAccessToAllChatsByDefault = initialRecipients.exclude + } + + self.currentShortcut = component.initialData.shortcutMessageList.items.first(where: { $0.shortcut == shortcutName }) + + self.currentShortcutDisposable = (component.context.engine.accountData.shortcutMessageList(onlyRemote: false) + |> deliverOnMainQueue).start(next: { [weak self] shortcutMessageList in + guard let self else { + return + } + let shortcut = shortcutMessageList.items.first(where: { $0.shortcut == shortcutName }) + if shortcut != self.currentShortcut { + self.currentShortcut = shortcut + self.state?.updated(transition: .immediate) + } + }) + } + + let environment = environment[EnvironmentType.self].value + let themeUpdated = self.environment?.theme !== environment.theme + self.environment = environment + + self.component = component + self.state = state + + let alphaTransition: Transition = transition.animation.isImmediate ? transition : transition.withAnimation(.curve(duration: 0.25, curve: .easeInOut)) + + if themeUpdated { + self.backgroundColor = environment.theme.list.blocksBackgroundColor + } + + let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } + + let navigationTitleSize = self.navigationTitle.update( + transition: transition, + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: component.mode == .greeting ? environment.strings.BusinessMessageSetup_TitleGreetingMessage : environment.strings.BusinessMessageSetup_TitleAwayMessage, font: Font.semibold(17.0), textColor: environment.theme.rootController.navigationBar.primaryTextColor)), + horizontalAlignment: .center + )), + environment: {}, + containerSize: CGSize(width: availableSize.width, height: 100.0) + ) + let navigationTitleFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - navigationTitleSize.width) / 2.0), y: environment.statusBarHeight + floor((environment.navigationHeight - environment.statusBarHeight - navigationTitleSize.height) / 2.0)), size: navigationTitleSize) + if let navigationTitleView = self.navigationTitle.view { + if navigationTitleView.superview == nil { + if let controller = self.environment?.controller(), let navigationBar = controller.navigationBar { + navigationBar.view.addSubview(navigationTitleView) + } + } + transition.setFrame(view: navigationTitleView, frame: navigationTitleFrame) + } + + let bottomContentInset: CGFloat = 24.0 + let sideInset: CGFloat = 16.0 + environment.safeInsets.left + let sectionSpacing: CGFloat = 32.0 + + var contentHeight: CGFloat = 0.0 + + contentHeight += environment.navigationHeight + + let iconSize = self.icon.update( + transition: .immediate, + component: AnyComponent(LottieComponent( + content: LottieComponent.AppBundleContent(name: component.mode == .greeting ? "HandWaveEmoji" : "ZzzEmoji"), + loop: false + )), + environment: {}, + containerSize: CGSize(width: 100.0, height: 100.0) + ) + let iconFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - iconSize.width) * 0.5), y: contentHeight + 8.0), size: iconSize) + if let iconView = self.icon.view as? LottieComponent.View { + if iconView.superview == nil { + self.scrollView.addSubview(iconView) + iconView.playOnce() + } + transition.setPosition(view: iconView, position: iconFrame.center) + iconView.bounds = CGRect(origin: CGPoint(), size: iconFrame.size) + } + + contentHeight += 124.0 + + let subtitleString = NSMutableAttributedString(attributedString: parseMarkdownIntoAttributedString(component.mode == .greeting ? environment.strings.BusinessMessageSetup_TextGreetingMessage : environment.strings.BusinessMessageSetup_TextAwayMessage, attributes: MarkdownAttributes( + body: MarkdownAttributeSet(font: Font.regular(15.0), textColor: environment.theme.list.freeTextColor), + bold: MarkdownAttributeSet(font: Font.semibold(15.0), textColor: environment.theme.list.freeTextColor), + link: MarkdownAttributeSet(font: Font.regular(15.0), textColor: environment.theme.list.itemAccentColor), + linkAttribute: { attributes in + return ("URL", "") + }), textAlignment: .center + )) + + let subtitleSize = self.subtitle.update( + transition: .immediate, + component: AnyComponent(BalancedTextComponent( + text: .plain(subtitleString), + horizontalAlignment: .center, + maximumNumberOfLines: 0, + lineSpacing: 0.25, + highlightColor: environment.theme.list.itemAccentColor.withMultipliedAlpha(0.1), + highlightAction: { attributes in + if let _ = attributes[NSAttributedString.Key(rawValue: "URL")] { + return NSAttributedString.Key(rawValue: "URL") + } else { + return nil + } + }, + tapAction: { [weak self] _, _ in + guard let self, let component = self.component else { + return + } + let _ = component + } + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 1000.0) + ) + let subtitleFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - subtitleSize.width) * 0.5), y: contentHeight), size: subtitleSize) + if let subtitleView = self.subtitle.view { + if subtitleView.superview == nil { + self.scrollView.addSubview(subtitleView) + } + transition.setPosition(view: subtitleView, position: subtitleFrame.center) + subtitleView.bounds = CGRect(origin: CGPoint(), size: subtitleFrame.size) + } + contentHeight += subtitleSize.height + contentHeight += 27.0 + + var generalSectionItems: [AnyComponentWithIdentity] = [] + generalSectionItems.append(AnyComponentWithIdentity(id: 0, component: AnyComponent(ListActionItemComponent( + theme: environment.theme, + title: AnyComponent(VStack([ + AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: component.mode == .greeting ? environment.strings.BusinessMessageSetup_ToggleGreetingMessage : environment.strings.BusinessMessageSetup_ToggleAwayMessage, + font: Font.regular(presentationData.listsFontSize.baseDisplaySize), + textColor: environment.theme.list.itemPrimaryTextColor + )), + maximumNumberOfLines: 1 + ))), + ], alignment: .left, spacing: 2.0)), + accessory: .toggle(ListActionItemComponent.Toggle(style: .regular, isOn: self.isOn, action: { [weak self] _ in + guard let self else { + return + } + self.isOn = !self.isOn + self.state?.updated(transition: .spring(duration: 0.4)) + })), + action: nil + )))) + + let generalSectionSize = self.generalSection.update( + transition: transition, + component: AnyComponent(ListSectionComponent( + theme: environment.theme, + header: nil, + footer: nil, + items: generalSectionItems + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 10000.0) + ) + let generalSectionFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: generalSectionSize) + if let generalSectionView = self.generalSection.view { + if generalSectionView.superview == nil { + self.scrollView.addSubview(generalSectionView) + } + transition.setFrame(view: generalSectionView, frame: generalSectionFrame) + } + contentHeight += generalSectionSize.height + contentHeight += sectionSpacing + + var otherSectionsHeight: CGFloat = 0.0 + + var messagesSectionItems: [AnyComponentWithIdentity] = [] + if let currentShortcut = self.currentShortcut { + if let accountPeer = self.accountPeer { + messagesSectionItems.append(AnyComponentWithIdentity(id: 1, component: AnyComponent(GreetingMessageListItemComponent( + context: component.context, + theme: environment.theme, + strings: environment.strings, + accountPeer: accountPeer, + message: currentShortcut.topMessage, + count: currentShortcut.totalCount, + action: { [weak self] in + guard let self else { + return + } + self.openMessageList() + } + )))) + } + } else { + messagesSectionItems.append(AnyComponentWithIdentity(id: 0, component: AnyComponent(ListActionItemComponent( + theme: environment.theme, + title: AnyComponent(VStack([ + AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: component.mode == .greeting ? environment.strings.BusinessMessageSetup_CreateGreetingMessage : environment.strings.BusinessMessageSetup_CreateAwayMessage, + font: Font.regular(presentationData.listsFontSize.baseDisplaySize), + textColor: environment.theme.list.itemAccentColor + )), + maximumNumberOfLines: 1 + ))), + ], alignment: .left, spacing: 2.0)), + leftIcon: AnyComponentWithIdentity(id: 0, component: AnyComponent(BundleIconComponent( + name: "Chat List/ComposeIcon", + tintColor: environment.theme.list.itemAccentColor + ))), + accessory: nil, + action: { [weak self] _ in + guard let self else { + return + } + self.openMessageList() + } + )))) + } + let messagesSectionSize = self.messagesSection.update( + transition: transition, + component: AnyComponent(ListSectionComponent( + theme: environment.theme, + header: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: component.mode == .greeting ? environment.strings.BusinessMessageSetup_GreetingMessageSectionHeader : environment.strings.BusinessMessageSetup_AwayMessageSectionHeader, + font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize), + textColor: environment.theme.list.freeTextColor + )), + maximumNumberOfLines: 0 + )), + footer: nil, + items: messagesSectionItems + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 10000.0) + ) + let messagesSectionFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight + otherSectionsHeight), size: messagesSectionSize) + if let messagesSectionView = self.messagesSection.view { + if messagesSectionView.superview == nil { + messagesSectionView.layer.allowsGroupOpacity = true + self.scrollView.addSubview(messagesSectionView) + } + transition.setFrame(view: messagesSectionView, frame: messagesSectionFrame) + alphaTransition.setAlpha(view: messagesSectionView, alpha: self.isOn ? 1.0 : 0.0) + } + otherSectionsHeight += messagesSectionSize.height + otherSectionsHeight += sectionSpacing + + if case .away = component.mode { + var scheduleSectionItems: [AnyComponentWithIdentity] = [] + optionLoop: for i in 0 ..< 3 { + let title: String + let schedule: Schedule + switch i { + case 0: + title = environment.strings.BusinessMessageSetup_ScheduleAlways + schedule = .always + case 1: + if component.initialData.businessHours == nil { + continue optionLoop + } + + title = environment.strings.BusinessMessageSetup_ScheduleOutsideBusinessHours + schedule = .outsideBusinessHours + default: + title = environment.strings.BusinessMessageSetup_ScheduleCustom + schedule = .custom + } + let isSelected = self.schedule == schedule + scheduleSectionItems.append(AnyComponentWithIdentity(id: scheduleSectionItems.count, component: AnyComponent(ListActionItemComponent( + theme: environment.theme, + title: AnyComponent(VStack([ + AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: title, + font: Font.regular(presentationData.listsFontSize.baseDisplaySize), + textColor: environment.theme.list.itemPrimaryTextColor + )), + maximumNumberOfLines: 1 + ))), + ], alignment: .left, spacing: 2.0)), + leftIcon: AnyComponentWithIdentity(id: 0, component: AnyComponent(Image( + image: checkIcon, + tintColor: !isSelected ? .clear : environment.theme.list.itemAccentColor, + contentMode: .center + ))), + accessory: nil, + action: { [weak self] _ in + guard let self else { + return + } + + if self.schedule != schedule { + self.schedule = schedule + self.state?.updated(transition: .immediate) + } + } + )))) + } + let scheduleSectionSize = self.scheduleSection.update( + transition: transition, + component: AnyComponent(ListSectionComponent( + theme: environment.theme, + header: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: environment.strings.BusinessMessageSetup_ScheduleSectionHeader, + font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize), + textColor: environment.theme.list.freeTextColor + )), + maximumNumberOfLines: 0 + )), + footer: nil, + items: scheduleSectionItems + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 10000.0) + ) + let scheduleSectionFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight + otherSectionsHeight), size: scheduleSectionSize) + if let scheduleSectionView = self.scheduleSection.view { + if scheduleSectionView.superview == nil { + scheduleSectionView.layer.allowsGroupOpacity = true + self.scrollView.addSubview(scheduleSectionView) + } + transition.setFrame(view: scheduleSectionView, frame: scheduleSectionFrame) + alphaTransition.setAlpha(view: scheduleSectionView, alpha: self.isOn ? 1.0 : 0.0) + } + otherSectionsHeight += scheduleSectionSize.height + otherSectionsHeight += sectionSpacing + + var customScheduleSectionsHeight: CGFloat = 0.0 + var customScheduleSectionItems: [AnyComponentWithIdentity] = [] + for i in 0 ..< 2 { + let title: String + let itemDate: Date? + let isStartTime: Bool + switch i { + case 0: + title = environment.strings.BusinessMessageSetup_ScheduleStartTime + itemDate = self.customScheduleStart + isStartTime = true + default: + title = environment.strings.BusinessMessageSetup_ScheduleEndTime + itemDate = self.customScheduleEnd + isStartTime = false + } + + var icon: ListActionItemComponent.Icon? + var accessory: ListActionItemComponent.Accessory? + if let itemDate { + let calendar = Calendar.current + let hours = calendar.component(.hour, from: itemDate) + let minutes = calendar.component(.minute, from: itemDate) + + let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 }) + + let timeText = stringForShortTimestamp(hours: Int32(hours), minutes: Int32(minutes), dateTimeFormat: presentationData.dateTimeFormat) + + let dateFormatter = DateFormatter() + dateFormatter.timeStyle = .none + dateFormatter.dateStyle = .medium + let dateText = stringForCompactDate(timestamp: Int32(itemDate.timeIntervalSince1970), strings: environment.strings, dateTimeFormat: presentationData.dateTimeFormat) + + icon = ListActionItemComponent.Icon(component: AnyComponentWithIdentity(id: 0, component: AnyComponent(HStack([ + AnyComponentWithIdentity(id: 0, component: AnyComponent(PlainButtonComponent( + content: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: dateText, font: Font.regular(17.0), textColor: environment.theme.list.itemPrimaryTextColor)) + )), + background: AnyComponent(RoundedRectangle(color: environment.theme.list.itemPrimaryTextColor.withMultipliedAlpha(0.1), cornerRadius: 6.0)), + effectAlignment: .center, + minSize: nil, + contentInsets: UIEdgeInsets(top: 7.0, left: 8.0, bottom: 7.0, right: 8.0), + action: { [weak self] in + guard let self else { + return + } + self.openCustomScheduleDateSetup(isStartTime: isStartTime, isDate: true) + }, + animateAlpha: true, + animateScale: false + ))), + AnyComponentWithIdentity(id: 1, component: AnyComponent(PlainButtonComponent( + content: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: timeText, font: Font.regular(17.0), textColor: environment.theme.list.itemPrimaryTextColor)) + )), + background: AnyComponent(RoundedRectangle(color: environment.theme.list.itemPrimaryTextColor.withMultipliedAlpha(0.1), cornerRadius: 6.0)), + effectAlignment: .center, + minSize: nil, + contentInsets: UIEdgeInsets(top: 7.0, left: 8.0, bottom: 7.0, right: 8.0), + action: { [weak self] in + guard let self else { + return + } + self.openCustomScheduleDateSetup(isStartTime: isStartTime, isDate: false) + }, + animateAlpha: true, + animateScale: false + ))) + ], spacing: 4.0))), insets: .custom(UIEdgeInsets(top: 4.0, left: 0.0, bottom: 4.0, right: 0.0)), allowUserInteraction: true) + } else { + icon = ListActionItemComponent.Icon(component: AnyComponentWithIdentity(id: 1, component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: environment.strings.BusinessMessageSetup_ScheduleTimePlaceholder, + font: Font.regular(presentationData.listsFontSize.baseDisplaySize), + textColor: environment.theme.list.itemSecondaryTextColor + )), + maximumNumberOfLines: 1 + )))) + accessory = .arrow + } + + customScheduleSectionItems.append(AnyComponentWithIdentity(id: customScheduleSectionItems.count, component: AnyComponent(ListActionItemComponent( + theme: environment.theme, + title: AnyComponent(VStack([ + AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: title, + font: Font.regular(presentationData.listsFontSize.baseDisplaySize), + textColor: environment.theme.list.itemPrimaryTextColor + )), + maximumNumberOfLines: 1 + ))), + ], alignment: .left, spacing: 2.0)), + icon: icon, + accessory: accessory, + action: itemDate != nil ? nil : { [weak self] _ in + guard let self else { + return + } + self.openCustomScheduleDateSetup(isStartTime: isStartTime, isDate: true) + } + )))) + } + let customScheduleSectionSize = self.customScheduleSection.update( + transition: transition, + component: AnyComponent(ListSectionComponent( + theme: environment.theme, + header: nil, + footer: nil, + items: customScheduleSectionItems + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 10000.0) + ) + let customScheduleSectionFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight + otherSectionsHeight + customScheduleSectionsHeight), size: customScheduleSectionSize) + if let customScheduleSectionView = self.customScheduleSection.view { + if customScheduleSectionView.superview == nil { + customScheduleSectionView.layer.allowsGroupOpacity = true + self.scrollView.addSubview(customScheduleSectionView) + } + transition.setFrame(view: customScheduleSectionView, frame: customScheduleSectionFrame) + alphaTransition.setAlpha(view: customScheduleSectionView, alpha: (self.isOn && self.schedule == .custom) ? 1.0 : 0.0) + } + customScheduleSectionsHeight += customScheduleSectionSize.height + customScheduleSectionsHeight += sectionSpacing + + if self.schedule == .custom { + otherSectionsHeight += customScheduleSectionsHeight + } + } + + if case .away = component.mode { + let sendWhenOfflineSectionSize = self.sendWhenOfflineSection.update( + transition: transition, + component: AnyComponent(ListSectionComponent( + theme: environment.theme, + header: nil, + footer: AnyComponent(MultilineTextComponent( + text: .markdown( + text: environment.strings.BusinessMessageSetup_SendWhenOfflineFooter, + attributes: MarkdownAttributes( + body: MarkdownAttributeSet(font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize), textColor: environment.theme.list.freeTextColor), + bold: MarkdownAttributeSet(font: Font.semibold(presentationData.listsFontSize.itemListBaseHeaderFontSize), textColor: environment.theme.list.freeTextColor), + link: MarkdownAttributeSet(font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize), textColor: environment.theme.list.itemAccentColor), + linkAttribute: { _ in + return nil + } + ) + ), + maximumNumberOfLines: 0 + )), + items: [ + AnyComponentWithIdentity(id: 0, component: AnyComponent(ListActionItemComponent( + theme: environment.theme, + title: AnyComponent(VStack([ + AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: environment.strings.BusinessMessageSetup_SendWhenOffline, + font: Font.regular(presentationData.listsFontSize.baseDisplaySize), + textColor: environment.theme.list.itemPrimaryTextColor + )), + maximumNumberOfLines: 1 + ))), + ], alignment: .left, spacing: 2.0)), + leftIcon: nil, + accessory: .toggle(ListActionItemComponent.Toggle( + style: .regular, + isOn: self.sendWhenOffline, + action: { [weak self] value in + guard let self else { + return + } + self.sendWhenOffline = value + self.state?.updated(transition: .spring(duration: 0.4)) + } + )), + action: nil + ))) + ] + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 10000.0) + ) + let sendWhenOfflineSectionFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight + otherSectionsHeight), size: sendWhenOfflineSectionSize) + if let sendWhenOfflineSectionView = self.sendWhenOfflineSection.view { + if sendWhenOfflineSectionView.superview == nil { + sendWhenOfflineSectionView.layer.allowsGroupOpacity = true + self.scrollView.addSubview(sendWhenOfflineSectionView) + } + transition.setFrame(view: sendWhenOfflineSectionView, frame: sendWhenOfflineSectionFrame) + alphaTransition.setAlpha(view: sendWhenOfflineSectionView, alpha: self.isOn ? 1.0 : 0.0) + } + otherSectionsHeight += sendWhenOfflineSectionSize.height + otherSectionsHeight += sectionSpacing + } + + let accessSectionSize = self.accessSection.update( + transition: transition, + component: AnyComponent(ListSectionComponent( + theme: environment.theme, + header: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: environment.strings.BusinessMessageSetup_RecipientsSectionHeader, + font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize), + textColor: environment.theme.list.freeTextColor + )), + maximumNumberOfLines: 0 + )), + footer: nil, + items: [ + AnyComponentWithIdentity(id: 0, component: AnyComponent(ListActionItemComponent( + theme: environment.theme, + title: AnyComponent(VStack([ + AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: environment.strings.BusinessMessageSetup_RecipientsOptionAllExcept, + font: Font.regular(presentationData.listsFontSize.baseDisplaySize), + textColor: environment.theme.list.itemPrimaryTextColor + )), + maximumNumberOfLines: 1 + ))), + ], alignment: .left, spacing: 2.0)), + leftIcon: AnyComponentWithIdentity(id: 0, component: AnyComponent(Image( + image: checkIcon, + tintColor: !self.hasAccessToAllChatsByDefault ? .clear : environment.theme.list.itemAccentColor, + contentMode: .center + ))), + accessory: nil, + action: { [weak self] _ in + guard let self else { + return + } + if !self.hasAccessToAllChatsByDefault { + self.hasAccessToAllChatsByDefault = true + self.additionalPeerList.categories.removeAll() + self.additionalPeerList.peers.removeAll() + self.state?.updated(transition: .immediate) + } + } + ))), + AnyComponentWithIdentity(id: 1, component: AnyComponent(ListActionItemComponent( + theme: environment.theme, + title: AnyComponent(VStack([ + AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: environment.strings.BusinessMessageSetup_RecipientsOptionOnly, + font: Font.regular(presentationData.listsFontSize.baseDisplaySize), + textColor: environment.theme.list.itemPrimaryTextColor + )), + maximumNumberOfLines: 1 + ))), + ], alignment: .left, spacing: 2.0)), + leftIcon: AnyComponentWithIdentity(id: 0, component: AnyComponent(Image( + image: checkIcon, + tintColor: self.hasAccessToAllChatsByDefault ? .clear : environment.theme.list.itemAccentColor, + contentMode: .center + ))), + accessory: nil, + action: { [weak self] _ in + guard let self else { + return + } + if self.hasAccessToAllChatsByDefault { + self.hasAccessToAllChatsByDefault = false + self.additionalPeerList.categories.removeAll() + self.additionalPeerList.peers.removeAll() + self.state?.updated(transition: .immediate) + } + } + ))) + ] + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 10000.0) + ) + let accessSectionFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight + otherSectionsHeight), size: accessSectionSize) + if let accessSectionView = self.accessSection.view { + if accessSectionView.superview == nil { + accessSectionView.layer.allowsGroupOpacity = true + self.scrollView.addSubview(accessSectionView) + } + transition.setFrame(view: accessSectionView, frame: accessSectionFrame) + alphaTransition.setAlpha(view: accessSectionView, alpha: self.isOn ? 1.0 : 0.0) + } + otherSectionsHeight += accessSectionSize.height + otherSectionsHeight += sectionSpacing + + var excludedSectionItems: [AnyComponentWithIdentity] = [] + excludedSectionItems.append(AnyComponentWithIdentity(id: 0, component: AnyComponent(ListActionItemComponent( + theme: environment.theme, + title: AnyComponent(VStack([ + AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: self.hasAccessToAllChatsByDefault ? environment.strings.BusinessMessageSetup_Recipients_AddExclude : environment.strings.BusinessMessageSetup_Recipients_AddInclude, + font: Font.regular(presentationData.listsFontSize.baseDisplaySize), + textColor: environment.theme.list.itemAccentColor + )), + maximumNumberOfLines: 1 + ))), + ], alignment: .left, spacing: 2.0)), + leftIcon: AnyComponentWithIdentity(id: 0, component: AnyComponent(BundleIconComponent( + name: "Chat List/AddIcon", + tintColor: environment.theme.list.itemAccentColor + ))), + accessory: nil, + action: { [weak self] _ in + guard let self else { + return + } + self.openAdditionalPeerListSetup() + } + )))) + for category in self.additionalPeerList.categories.sorted(by: { $0.rawValue < $1.rawValue }) { + let title: String + let icon: String + let color: AvatarBackgroundColor + switch category { + case .newChats: + title = environment.strings.BusinessMessageSetup_Recipients_CategoryNewChats + icon = "Chat List/Filters/NewChats" + color = .purple + case .existingChats: + title = environment.strings.BusinessMessageSetup_Recipients_CategoryExistingChats + icon = "Chat List/Filters/Chats" + color = .purple + case .contacts: + title = environment.strings.BusinessMessageSetup_Recipients_CategoryContacts + icon = "Chat List/Filters/Contact" + color = .blue + case .nonContacts: + title = environment.strings.BusinessMessageSetup_Recipients_CategoryNonContacts + icon = "Chat List/Filters/User" + color = .yellow + } + excludedSectionItems.append(AnyComponentWithIdentity(id: category, component: AnyComponent(PeerListItemComponent( + context: component.context, + theme: environment.theme, + strings: environment.strings, + style: .generic, + sideInset: 0.0, + title: title, + avatar: PeerListItemComponent.Avatar( + icon: icon, + color: color, + clipStyle: .roundedRect + ), + peer: nil, + subtitle: nil, + subtitleAccessory: .none, + presence: nil, + selectionState: .none, + hasNext: false, + action: { peer, _, _ in + }, + inlineActions: PeerListItemComponent.InlineActionsState( + actions: [PeerListItemComponent.InlineAction( + id: AnyHashable(0), + title: environment.strings.Common_Delete, + color: .destructive, + action: { [weak self] in + guard let self else { + return + } + self.additionalPeerList.categories.remove(category) + self.state?.updated(transition: .spring(duration: 0.4)) + } + )] + ) + )))) + } + for peer in self.additionalPeerList.peers { + excludedSectionItems.append(AnyComponentWithIdentity(id: peer.peer.id, component: AnyComponent(PeerListItemComponent( + context: component.context, + theme: environment.theme, + strings: environment.strings, + style: .generic, + sideInset: 0.0, + title: peer.peer.displayTitle(strings: environment.strings, displayOrder: .firstLast), + peer: peer.peer, + subtitle: peer.isContact ? environment.strings.ChatList_PeerTypeContact : environment.strings.ChatList_PeerTypeNonContact, + subtitleAccessory: .none, + presence: nil, + selectionState: .none, + hasNext: false, + action: { peer, _, _ in + }, + inlineActions: PeerListItemComponent.InlineActionsState( + actions: [PeerListItemComponent.InlineAction( + id: AnyHashable(0), + title: environment.strings.Common_Delete, + color: .destructive, + action: { [weak self] in + guard let self else { + return + } + self.additionalPeerList.peers.removeAll(where: { $0.peer.id == peer.peer.id }) + self.state?.updated(transition: .spring(duration: 0.4)) + } + )] + ) + )))) + } + + let excludedSectionSize = self.excludedSection.update( + transition: transition, + component: AnyComponent(ListSectionComponent( + theme: environment.theme, + header: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: self.hasAccessToAllChatsByDefault ? environment.strings.BusinessMessageSetup_Recipients_ExcludedSectionHeader : environment.strings.BusinessMessageSetup_Recipients_IncludedSectionHeader, + font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize), + textColor: environment.theme.list.freeTextColor + )), + maximumNumberOfLines: 0 + )), + footer: AnyComponent(MultilineTextComponent( + text: .markdown( + text: component.mode == .greeting ? environment.strings.BusinessMessageSetup_Recipients_GreetingMessageFooter : environment.strings.BusinessMessageSetup_Recipients_AwayMessageFooter, + attributes: MarkdownAttributes( + body: MarkdownAttributeSet(font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize), textColor: environment.theme.list.freeTextColor), + bold: MarkdownAttributeSet(font: Font.semibold(presentationData.listsFontSize.itemListBaseHeaderFontSize), textColor: environment.theme.list.freeTextColor), + link: MarkdownAttributeSet(font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize), textColor: environment.theme.list.itemAccentColor), + linkAttribute: { _ in + return nil + } + ) + ), + maximumNumberOfLines: 0 + )), + items: excludedSectionItems + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 10000.0) + ) + let excludedSectionFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight + otherSectionsHeight), size: excludedSectionSize) + if let excludedSectionView = self.excludedSection.view { + if excludedSectionView.superview == nil { + excludedSectionView.layer.allowsGroupOpacity = true + self.scrollView.addSubview(excludedSectionView) + } + transition.setFrame(view: excludedSectionView, frame: excludedSectionFrame) + alphaTransition.setAlpha(view: excludedSectionView, alpha: self.isOn ? 1.0 : 0.0) + } + otherSectionsHeight += excludedSectionSize.height + otherSectionsHeight += sectionSpacing + + if case .greeting = component.mode { + var selectedInactivityIndex = 0 + let valueList: [Int] = [ + 7, + 14, + 21, + 28 + ] + for i in 0 ..< valueList.count { + if valueList[i] <= self.inactivityDays { + selectedInactivityIndex = i + } + } + + let periodSectionSize = self.periodSection.update( + transition: transition, + component: AnyComponent(ListSectionComponent( + theme: environment.theme, + header: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: environment.strings.BusinessMessageSetup_InactivitySectionHeader, + font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize), + textColor: environment.theme.list.freeTextColor + )), + maximumNumberOfLines: 0 + )), + footer: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: environment.strings.BusinessMessageSetup_InactivitySectionFooter, + font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize), + textColor: environment.theme.list.freeTextColor + )), + maximumNumberOfLines: 0 + )), + items: [ + AnyComponentWithIdentity(id: 0, component: AnyComponent(ListItemSliderSelectorComponent( + theme: environment.theme, + values: valueList.map { item in + return environment.strings.MessageTimer_Days(Int32(item)) + }, + selectedIndex: selectedInactivityIndex, + selectedIndexUpdated: { [weak self] index in + guard let self else { + return + } + let index = max(0, min(valueList.count - 1, index)) + self.inactivityDays = valueList[index] + self.state?.updated(transition: .immediate) + } + ))) + ] + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 10000.0) + ) + let periodSectionFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight + otherSectionsHeight), size: periodSectionSize) + if let periodSectionView = self.periodSection.view { + if periodSectionView.superview == nil { + periodSectionView.layer.allowsGroupOpacity = true + self.scrollView.addSubview(periodSectionView) + } + transition.setFrame(view: periodSectionView, frame: periodSectionFrame) + alphaTransition.setAlpha(view: periodSectionView, alpha: self.isOn ? 1.0 : 0.0) + } + otherSectionsHeight += periodSectionSize.height + otherSectionsHeight += sectionSpacing + } + + if self.isOn { + contentHeight += otherSectionsHeight + } + + contentHeight += bottomContentInset + contentHeight += environment.safeInsets.bottom + + let previousBounds = self.scrollView.bounds + + let contentSize = CGSize(width: availableSize.width, height: contentHeight) + self.ignoreScrolling = true + if self.scrollView.frame != CGRect(origin: CGPoint(), size: availableSize) { + self.scrollView.frame = CGRect(origin: CGPoint(), size: availableSize) + } + if self.scrollView.contentSize != contentSize { + self.scrollView.contentSize = contentSize + } + let scrollInsets = UIEdgeInsets(top: environment.navigationHeight, left: 0.0, bottom: 0.0, right: 0.0) + if self.scrollView.scrollIndicatorInsets != scrollInsets { + self.scrollView.scrollIndicatorInsets = scrollInsets + } + self.ignoreScrolling = false + + if !previousBounds.isEmpty, !transition.animation.isImmediate { + let bounds = self.scrollView.bounds + if bounds.maxY != previousBounds.maxY { + let offsetY = previousBounds.maxY - bounds.maxY + transition.animateBoundsOrigin(view: self.scrollView, from: CGPoint(x: 0.0, y: offsetY), to: CGPoint(), additive: true) + } + } + + self.topOverscrollLayer.frame = CGRect(origin: CGPoint(x: 0.0, y: -3000.0), size: CGSize(width: availableSize.width, height: 3000.0)) + + self.updateScrolling(transition: transition) + + return availableSize + } + } + + func makeView() -> View { + return View() + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} + +public final class AutomaticBusinessMessageSetupScreen: ViewControllerComponentContainer { + public final class InitialData: AutomaticBusinessMessageSetupScreenInitialData { + fileprivate let accountPeer: EnginePeer? + fileprivate let shortcutMessageList: ShortcutMessageList + fileprivate let greetingMessage: TelegramBusinessGreetingMessage? + fileprivate let awayMessage: TelegramBusinessAwayMessage? + fileprivate let additionalPeers: [EnginePeer.Id: AutomaticBusinessMessageSetupScreenComponent.AdditionalPeerList.Peer] + fileprivate let businessHours: TelegramBusinessHours? + + fileprivate init( + accountPeer: EnginePeer?, + shortcutMessageList: ShortcutMessageList, + greetingMessage: TelegramBusinessGreetingMessage?, + awayMessage: TelegramBusinessAwayMessage?, + additionalPeers: [EnginePeer.Id: AutomaticBusinessMessageSetupScreenComponent.AdditionalPeerList.Peer], + businessHours: TelegramBusinessHours? + ) { + self.accountPeer = accountPeer + self.shortcutMessageList = shortcutMessageList + self.greetingMessage = greetingMessage + self.awayMessage = awayMessage + self.additionalPeers = additionalPeers + self.businessHours = businessHours + } + } + + public enum Mode { + case greeting + case away + } + + private let context: AccountContext + + public init(context: AccountContext, initialData: InitialData, mode: Mode) { + self.context = context + + super.init(context: context, component: AutomaticBusinessMessageSetupScreenComponent( + context: context, + initialData: initialData, + mode: mode + ), navigationBarAppearance: .default, theme: .default, updatedPresentationData: nil) + + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + self.title = "" + self.navigationItem.backBarButtonItem = UIBarButtonItem(title: presentationData.strings.Common_Back, style: .plain, target: nil, action: nil) + + self.scrollToTop = { [weak self] in + guard let self, let componentView = self.node.hostView.componentView as? AutomaticBusinessMessageSetupScreenComponent.View else { + return + } + componentView.scrollToTop() + } + + self.attemptNavigation = { [weak self] complete in + guard let self, let componentView = self.node.hostView.componentView as? AutomaticBusinessMessageSetupScreenComponent.View else { + return true + } + + return componentView.attemptNavigation(complete: complete) + } + } + + required public init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + } + + @objc private func cancelPressed() { + self.dismiss() + } + + override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { + super.containerLayoutUpdated(layout, transition: transition) + } + + public static func initialData(context: AccountContext) -> Signal { + return combineLatest( + context.engine.data.get( + TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId), + TelegramEngine.EngineData.Item.Peer.BusinessGreetingMessage(id: context.account.peerId), + TelegramEngine.EngineData.Item.Peer.BusinessAwayMessage(id: context.account.peerId), + TelegramEngine.EngineData.Item.Peer.BusinessHours(id: context.account.peerId) + ), + context.engine.accountData.shortcutMessageList(onlyRemote: true) + |> take(1) + ) + |> mapToSignal { data, shortcutMessageList -> Signal in + let (accountPeer, greetingMessage, awayMessage, businessHours) = data + + var additionalPeerIds = Set() + if let greetingMessage { + additionalPeerIds.formUnion(greetingMessage.recipients.additionalPeers) + } + if let awayMessage { + additionalPeerIds.formUnion(awayMessage.recipients.additionalPeers) + } + + return context.engine.data.get( + EngineDataMap(additionalPeerIds.map(TelegramEngine.EngineData.Item.Peer.Peer.init(id:))), + EngineDataMap(additionalPeerIds.map(TelegramEngine.EngineData.Item.Peer.IsContact.init(id:))) + ) + |> map { peers, isContacts -> AutomaticBusinessMessageSetupScreenInitialData in + var additionalPeers: [EnginePeer.Id: AutomaticBusinessMessageSetupScreenComponent.AdditionalPeerList.Peer] = [:] + for id in additionalPeerIds { + guard let peer = peers[id], let peer else { + continue + } + additionalPeers[id] = AutomaticBusinessMessageSetupScreenComponent.AdditionalPeerList.Peer( + peer: peer, + isContact: isContacts[id] ?? false + ) + } + + return InitialData( + accountPeer: accountPeer, + shortcutMessageList: shortcutMessageList, + greetingMessage: greetingMessage, + awayMessage: awayMessage, + additionalPeers: additionalPeers, + businessHours: businessHours + ) + } + } + } +} diff --git a/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/BottomPanelComponent.swift b/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/BottomPanelComponent.swift new file mode 100644 index 00000000000..2dd0f3bae71 --- /dev/null +++ b/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/BottomPanelComponent.swift @@ -0,0 +1,117 @@ +import Foundation +import UIKit +import Display +import ComponentFlow +import TelegramPresentationData +import ComponentDisplayAdapters + +final class BottomPanelComponent: Component { + let theme: PresentationTheme + let content: AnyComponentWithIdentity + let insets: UIEdgeInsets + + init( + theme: PresentationTheme, + content: AnyComponentWithIdentity, + insets: UIEdgeInsets + ) { + self.theme = theme + self.content = content + self.insets = insets + } + + static func ==(lhs: BottomPanelComponent, rhs: BottomPanelComponent) -> Bool { + if lhs.theme !== rhs.theme { + return false + } + if lhs.content != rhs.content { + return false + } + if lhs.insets != rhs.insets { + return false + } + return true + } + + final class View: UIView { + private let separatorLayer: SimpleLayer + private let backgroundView: BlurredBackgroundView + private var content = ComponentView() + + private var component: BottomPanelComponent? + private weak var componentState: EmptyComponentState? + + override init(frame: CGRect) { + self.separatorLayer = SimpleLayer() + self.backgroundView = BlurredBackgroundView(color: .clear, enableBlur: true) + + super.init(frame: frame) + + self.addSubview(self.backgroundView) + self.layer.addSublayer(self.separatorLayer) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func update(component: BottomPanelComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + let previousComponent = self.component + self.component = component + self.componentState = state + + let themeUpdated = previousComponent?.theme !== component.theme + + var contentHeight: CGFloat = 0.0 + + contentHeight += component.insets.top + + var contentTransition = transition + if let previousComponent, previousComponent.content.id != component.content.id { + contentTransition = contentTransition.withAnimation(.none) + self.content.view?.removeFromSuperview() + self.content = ComponentView() + } + + let contentSize = self.content.update( + transition: contentTransition, + component: component.content.component, + environment: {}, + containerSize: CGSize(width: availableSize.width - component.insets.left - component.insets.right, height: availableSize.height - component.insets.top - component.insets.bottom) + ) + let contentFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - contentSize.width) * 0.5), y: contentHeight), size: contentSize) + if let contentView = self.content.view { + if contentView.superview == nil { + self.addSubview(contentView) + } + contentTransition.setFrame(view: contentView, frame: contentFrame) + } + contentHeight += contentSize.height + + contentHeight += component.insets.bottom + + let size = CGSize(width: availableSize.width, height: contentHeight) + + if themeUpdated { + self.backgroundView.updateColor(color: component.theme.rootController.navigationBar.blurredBackgroundColor, transition: .immediate) + self.separatorLayer.backgroundColor = component.theme.rootController.navigationBar.separatorColor.cgColor + } + + let backgroundFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: size) + transition.setFrame(view: self.backgroundView, frame: backgroundFrame) + self.backgroundView.update(size: backgroundFrame.size, transition: transition.containedViewLayoutTransition) + + transition.setFrame(layer: self.separatorLayer, frame: CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: availableSize.width, height: UIScreenPixel))) + + return size + } + } + + func makeView() -> View { + return View(frame: CGRect()) + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} diff --git a/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/QuickReplyEmptyStateComponent.swift b/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/QuickReplyEmptyStateComponent.swift new file mode 100644 index 00000000000..4582101ed6f --- /dev/null +++ b/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/QuickReplyEmptyStateComponent.swift @@ -0,0 +1,186 @@ +import Foundation +import UIKit +import Display +import ComponentFlow +import TelegramPresentationData +import AppBundle +import ButtonComponent +import MultilineTextComponent +import BalancedTextComponent +import LottieComponent + +final class QuickReplyEmptyStateComponent: Component { + let theme: PresentationTheme + let strings: PresentationStrings + let insets: UIEdgeInsets + let action: () -> Void + + init( + theme: PresentationTheme, + strings: PresentationStrings, + insets: UIEdgeInsets, + action: @escaping () -> Void + ) { + self.theme = theme + self.strings = strings + self.insets = insets + self.action = action + } + + static func ==(lhs: QuickReplyEmptyStateComponent, rhs: QuickReplyEmptyStateComponent) -> Bool { + if lhs.theme !== rhs.theme { + return false + } + if lhs.strings !== rhs.strings { + return false + } + if lhs.insets != rhs.insets { + return false + } + return true + } + + final class View: UIView { + private let icon = ComponentView() + private let title = ComponentView() + private let text = ComponentView() + private let button = ComponentView() + + private var component: QuickReplyEmptyStateComponent? + private weak var componentState: EmptyComponentState? + + override init(frame: CGRect) { + super.init(frame: frame) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func update(component: QuickReplyEmptyStateComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + let previousComponent = self.component + self.component = component + self.componentState = state + + let _ = previousComponent + + let buttonSize = self.button.update( + transition: transition, + component: AnyComponent(ButtonComponent( + background: ButtonComponent.Background( + color: component.theme.list.itemCheckColors.fillColor, + foreground: component.theme.list.itemCheckColors.foregroundColor, + pressedColor: component.theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.9) + ), + content: AnyComponentWithIdentity( + id: AnyHashable(0), + component: AnyComponent(ButtonTextContentComponent( + text: component.strings.QuickReplies_EmptyState_AddButton, + badge: 0, + textColor: component.theme.list.itemCheckColors.foregroundColor, + badgeBackground: component.theme.list.itemCheckColors.foregroundColor, + badgeForeground: component.theme.list.itemCheckColors.fillColor + )) + ), + isEnabled: true, + displaysProgress: false, + action: { [weak self] in + guard let self, let component = self.component else { + return + } + component.action() + } + )), + environment: {}, + containerSize: CGSize(width: min(availableSize.width - 16.0 * 2.0, 280.0), height: 50.0) + ) + let buttonFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - buttonSize.width) * 0.5), y: availableSize.height - component.insets.bottom - 14.0 - buttonSize.height), size: buttonSize) + if let buttonView = self.button.view { + if buttonView.superview == nil { + self.addSubview(buttonView) + } + transition.setFrame(view: buttonView, frame: buttonFrame) + } + + let iconTitleSpacing: CGFloat = 13.0 + let titleTextSpacing: CGFloat = 9.0 + + let iconSize = self.icon.update( + transition: .immediate, + component: AnyComponent(LottieComponent( + content: LottieComponent.AppBundleContent(name: "WriteEmoji"), + loop: false + )), + environment: {}, + containerSize: CGSize(width: 120.0, height: 120.0) + ) + + let titleSize = self.title.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: component.strings.QuickReplies_EmptyState_Title, font: Font.semibold(17.0), textColor: component.theme.rootController.navigationBar.primaryTextColor)), + horizontalAlignment: .center + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - 16.0 * 2.0, height: 100.0) + ) + + let textSize = self.text.update( + transition: .immediate, + component: AnyComponent(BalancedTextComponent( + text: .plain(NSAttributedString(string: component.strings.QuickReplies_EmptyState_Text, font: Font.regular(15.0), textColor: component.theme.list.itemSecondaryTextColor)), + horizontalAlignment: .center, + maximumNumberOfLines: 20, + lineSpacing: 0.2 + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - 16.0 * 2.0, height: 100.0) + ) + + let topInset: CGFloat = component.insets.top + + let centralContentsHeight: CGFloat = iconSize.height + iconTitleSpacing + titleSize.height + titleTextSpacing + var centralContentsY: CGFloat = topInset + floor((buttonFrame.minY - topInset - centralContentsHeight) * 0.426) + + let iconFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - iconSize.width) * 0.5), y: centralContentsY), size: iconSize) + + if let iconView = self.icon.view as? LottieComponent.View { + if iconView.superview == nil { + self.addSubview(iconView) + iconView.playOnce() + } + transition.setFrame(view: iconView, frame: iconFrame) + } + centralContentsY += iconSize.height + iconTitleSpacing + + let titleFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - titleSize.width) * 0.5), y: centralContentsY), size: titleSize) + if let titleView = self.title.view { + if titleView.superview == nil { + self.addSubview(titleView) + } + titleView.bounds = CGRect(origin: CGPoint(), size: titleFrame.size) + transition.setPosition(view: titleView, position: titleFrame.center) + } + centralContentsY += titleSize.height + titleTextSpacing + + let textFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - textSize.width) * 0.5), y: centralContentsY), size: textSize) + if let textView = self.text.view { + if textView.superview == nil { + self.addSubview(textView) + } + textView.bounds = CGRect(origin: CGPoint(), size: textFrame.size) + transition.setPosition(view: textView, position: textFrame.center) + } + + return availableSize + } + } + + func makeView() -> View { + return View(frame: CGRect()) + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} diff --git a/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/QuickReplySetupScreen.swift b/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/QuickReplySetupScreen.swift new file mode 100644 index 00000000000..22d8f7e0cae --- /dev/null +++ b/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/QuickReplySetupScreen.swift @@ -0,0 +1,1384 @@ +import Foundation +import UIKit +import Photos +import Display +import AsyncDisplayKit +import SwiftSignalKit +import Postbox +import TelegramCore +import TelegramPresentationData +import TelegramUIPreferences +import PresentationDataUtils +import AccountContext +import ComponentFlow +import ViewControllerComponent +import MergeLists +import ComponentDisplayAdapters +import ItemListPeerActionItem +import ItemListUI +import ChatListUI +import QuickReplyNameAlertController +import ChatListHeaderComponent +import PlainButtonComponent +import MultilineTextComponent +import AttachmentUI +import SearchBarNode +import BalancedTextComponent + +final class QuickReplySetupScreenComponent: Component { + typealias EnvironmentType = ViewControllerComponentContainer.Environment + + let context: AccountContext + let initialData: QuickReplySetupScreen.InitialData + let mode: QuickReplySetupScreen.Mode + + init( + context: AccountContext, + initialData: QuickReplySetupScreen.InitialData, + mode: QuickReplySetupScreen.Mode + ) { + self.context = context + self.initialData = initialData + self.mode = mode + } + + static func ==(lhs: QuickReplySetupScreenComponent, rhs: QuickReplySetupScreenComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + + return true + } + + private enum ContentEntry: Comparable, Identifiable { + enum Id: Hashable { + case add + case item(Int32) + case pendingItem(String) + } + + var stableId: Id { + switch self { + case .add: + return .add + case let .item(item, _, _, _, _): + if let itemId = item.id { + return .item(itemId) + } else { + return .pendingItem(item.shortcut) + } + } + } + + case add + case item(item: ShortcutMessageList.Item, accountPeer: EnginePeer, sortIndex: Int, isEditing: Bool, isSelected: Bool) + + static func <(lhs: ContentEntry, rhs: ContentEntry) -> Bool { + switch lhs { + case .add: + return false + case let .item(lhsItem, _, lhsSortIndex, _, _): + switch rhs { + case .add: + return false + case let .item(rhsItem, _, rhsSortIndex, _, _): + if lhsSortIndex != rhsSortIndex { + return lhsSortIndex < rhsSortIndex + } + return lhsItem.shortcut < rhsItem.shortcut + } + } + } + + func item(listNode: ContentListNode) -> ListViewItem { + switch self { + case .add: + return ItemListPeerActionItem( + presentationData: ItemListPresentationData(listNode.presentationData), + icon: PresentationResourcesItemList.plusIconImage(listNode.presentationData.theme), + iconSignal: nil, + title: listNode.presentationData.strings.QuickReply_InlineCreateAction, + additionalBadgeIcon: nil, + alwaysPlain: true, + hasSeparator: true, + sectionId: 0, + height: .generic, + color: .accent, + editing: false, + action: { [weak listNode] in + guard let listNode, let parentView = listNode.parentView else { + return + } + parentView.openQuickReplyChat(shortcut: nil, shortcutId: nil) + } + ) + case let .item(item, accountPeer, _, isEditing, isSelected): + let chatListNodeInteraction = ChatListNodeInteraction( + context: listNode.context, + animationCache: listNode.context.animationCache, + animationRenderer: listNode.context.animationRenderer, + activateSearch: { + }, + peerSelected: { [weak listNode] _, _, _, _ in + guard let listNode, let parentView = listNode.parentView else { + return + } + parentView.openQuickReplyChat(shortcut: item.shortcut, shortcutId: item.id) + }, + disabledPeerSelected: { _, _, _ in + }, + togglePeerSelected: { [weak listNode] _, _ in + guard let listNode, let parentView = listNode.parentView else { + return + } + if let itemId = item.id { + parentView.toggleShortcutSelection(id: itemId) + } + }, + togglePeersSelection: { [weak listNode] _, _ in + guard let listNode, let parentView = listNode.parentView else { + return + } + if let itemId = item.id { + parentView.toggleShortcutSelection(id: itemId) + } + }, + additionalCategorySelected: { _ in + }, + messageSelected: { [weak listNode] _, _, _, _ in + guard let listNode, let parentView = listNode.parentView else { + return + } + parentView.openQuickReplyChat(shortcut: item.shortcut, shortcutId: item.id) + }, + groupSelected: { _ in + }, + addContact: { _ in + }, + setPeerIdWithRevealedOptions: { _, _ in + }, + setItemPinned: { _, _ in + }, + setPeerMuted: { _, _ in + }, + setPeerThreadMuted: { _, _, _ in + }, + deletePeer: { [weak listNode] _, _ in + guard let listNode, let parentView = listNode.parentView else { + return + } + if let itemId = item.id { + parentView.openDeleteShortcuts(ids: [itemId]) + } + }, + deletePeerThread: { _, _ in + }, + setPeerThreadStopped: { _, _, _ in + }, + setPeerThreadPinned: { _, _, _ in + }, + setPeerThreadHidden: { _, _, _ in + }, + updatePeerGrouping: { _, _ in + }, + togglePeerMarkedUnread: { _, _ in + }, + toggleArchivedFolderHiddenByDefault: { + }, + toggleThreadsSelection: { _, _ in + }, + hidePsa: { _ in + }, + activateChatPreview: { _, _, _, _, _ in + }, + present: { _ in + }, + openForumThread: { _, _ in + }, + openStorageManagement: { + }, + openPasswordSetup: { + }, + openPremiumIntro: { + }, + openPremiumGift: { + }, + openActiveSessions: { + }, + performActiveSessionAction: { _, _ in + }, + openChatFolderUpdates: { + }, + hideChatFolderUpdates: { + }, + openStories: { _, _ in + }, + dismissNotice: { _ in + }, + editPeer: { [weak listNode] _ in + guard let listNode, let parentView = listNode.parentView else { + return + } + if let itemId = item.id { + parentView.openEditShortcut(id: itemId, currentValue: item.shortcut) + } + } + ) + + let presentationData = listNode.context.sharedContext.currentPresentationData.with({ $0 }) + let chatListPresentationData = ChatListPresentationData( + theme: presentationData.theme, + fontSize: presentationData.listsFontSize, + strings: presentationData.strings, + dateTimeFormat: presentationData.dateTimeFormat, + nameSortOrder: presentationData.nameSortOrder, + nameDisplayOrder: presentationData.nameDisplayOrder, + disableAnimations: false + ) + + return ChatListItem( + presentationData: chatListPresentationData, + context: listNode.context, + chatListLocation: .chatList(groupId: .root), + filterData: nil, + index: EngineChatList.Item.Index.chatList(ChatListIndex(pinningIndex: nil, messageIndex: MessageIndex(id: MessageId(peerId: listNode.context.account.peerId, namespace: 0, id: 0), timestamp: 0))), + content: .peer(ChatListItemContent.PeerData( + messages: [item.topMessage], + peer: EngineRenderedPeer(peer: accountPeer), + threadInfo: nil, + combinedReadState: nil, + isRemovedFromTotalUnreadCount: false, + presence: nil, + hasUnseenMentions: false, + hasUnseenReactions: false, + draftState: nil, + mediaDraftContentType: nil, + inputActivities: nil, + promoInfo: nil, + ignoreUnreadBadge: false, + displayAsMessage: false, + hasFailedMessages: false, + forumTopicData: nil, + topForumTopicItems: [], + autoremoveTimeout: nil, + storyState: nil, + requiresPremiumForMessaging: false, + displayAsTopicList: false, + tags: [], + customMessageListData: ChatListItemContent.CustomMessageListData( + commandPrefix: "/\(item.shortcut)", + searchQuery: nil, + messageCount: item.totalCount, + hideSeparator: false + ) + )), + editing: isEditing, + hasActiveRevealControls: false, + selected: isSelected, + header: nil, + enableContextActions: true, + hiddenOffset: false, + interaction: chatListNodeInteraction + ) + } + } + } + + private final class ContentListNode: ListView { + weak var parentView: View? + let context: AccountContext + var presentationData: PresentationData + private var currentEntries: [ContentEntry] = [] + private var originalEntries: [ContentEntry] = [] + private var tempOrder: [Int32]? + private var pendingRemoveItems: [Int32]? + private var resetTempOrderOnNextUpdate: Bool = false + + init(parentView: View, context: AccountContext) { + self.parentView = parentView + self.context = context + self.presentationData = context.sharedContext.currentPresentationData.with({ $0 }) + + super.init() + + self.reorderBegan = { [weak self] in + guard let self else { + return + } + self.tempOrder = nil + } + self.reorderCompleted = { [weak self] _ in + guard let self, let tempOrder = self.tempOrder else { + return + } + self.resetTempOrderOnNextUpdate = true + self.context.engine.accountData.reorderMessageShortcuts(ids: tempOrder, completion: {}) + } + self.reorderItem = { [weak self] fromIndex, toIndex, transactionOpaqueState -> Signal in + guard let self else { + return .single(false) + } + guard fromIndex >= 0 && fromIndex < self.currentEntries.count && toIndex >= 0 && toIndex < self.currentEntries.count else { + return .single(false) + } + + let fromEntry = self.currentEntries[fromIndex] + let toEntry = self.currentEntries[toIndex] + + var referenceId: Int32? + var beforeAll = false + switch toEntry { + case let .item(item, _, _, _, _): + referenceId = item.id + case .add: + beforeAll = true + } + + if case let .item(item, _, _, _, _) = fromEntry { + var itemIds = self.currentEntries.compactMap { entry -> Int32? in + switch entry { + case .add: + return nil + case let .item(item, _, _, _, _): + return item.id + } + } + let itemId: Int32? = item.id + + if let itemId { + itemIds = itemIds.filter({ $0 != itemId }) + if let referenceId { + var inserted = false + for i in 0 ..< itemIds.count { + if itemIds[i] == referenceId { + if fromIndex < toIndex { + itemIds.insert(itemId, at: i + 1) + } else { + itemIds.insert(itemId, at: i) + } + inserted = true + break + } + } + if !inserted { + itemIds.append(itemId) + } + } else if beforeAll { + itemIds.insert(itemId, at: 0) + } else { + itemIds.append(itemId) + } + if self.tempOrder != itemIds { + self.tempOrder = itemIds + self.setEntries(entries: self.originalEntries, animated: true) + } + + return .single(true) + } else { + return .single(false) + } + } else { + return .single(false) + } + } + } + + func update(size: CGSize, insets: UIEdgeInsets, transition: Transition) { + let (listViewDuration, listViewCurve) = listViewAnimationDurationAndCurve(transition: transition.containedViewLayoutTransition) + self.transaction( + deleteIndices: [], + insertIndicesAndItems: [], + updateIndicesAndItems: [], + options: [.Synchronous, .LowLatency, .PreferSynchronousResourceLoading], + additionalScrollDistance: 0.0, + updateSizeAndInsets: ListViewUpdateSizeAndInsets(size: size, insets: insets, duration: listViewDuration, curve: listViewCurve), + updateOpaqueState: nil + ) + } + + func setPendingRemoveItems(itemIds: [Int32]) { + self.pendingRemoveItems = itemIds + self.setEntries(entries: self.originalEntries, animated: true) + } + + func setEntries(entries: [ContentEntry], animated: Bool) { + if self.resetTempOrderOnNextUpdate { + self.resetTempOrderOnNextUpdate = false + self.tempOrder = nil + } + let pendingRemoveItems = self.pendingRemoveItems + self.pendingRemoveItems = nil + + self.originalEntries = entries + + var entries = entries + if let pendingRemoveItems { + entries = entries.filter { entry in + switch entry.stableId { + case .add: + return true + case let .item(id): + return !pendingRemoveItems.contains(id) + case .pendingItem: + return true + } + } + } + + if let tempOrder = self.tempOrder { + let originalList = entries + entries.removeAll() + + if let entry = originalList.first(where: { entry in + if case .add = entry { + return true + } else { + return false + } + }) { + entries.append(entry) + } + + for id in tempOrder { + if let entry = originalList.first(where: { entry in + if case let .item(listId) = entry.stableId, listId == id { + return true + } else { + return false + } + }) { + entries.append(entry) + } + } + for entry in originalList { + if !entries.contains(where: { listEntry in + listEntry.stableId == entry.stableId + }) { + entries.append(entry) + } + } + } + + let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: self.currentEntries, rightList: entries) + self.currentEntries = entries + + let deletions = deleteIndices.map { ListViewDeleteItem(index: $0, directionHint: nil) } + let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(listNode: self), directionHint: nil) } + let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(listNode: self), directionHint: nil) } + + var options: ListViewDeleteAndInsertOptions = [.Synchronous, .LowLatency] + if animated { + options.insert(.AnimateInsertion) + } else { + options.insert(.PreferSynchronousResourceLoading) + } + + self.transaction( + deleteIndices: deletions, + insertIndicesAndItems: insertions, + updateIndicesAndItems: updates, + options: options, + scrollToItem: nil, + stationaryItemRange: nil, + updateOpaqueState: nil, + completion: { _ in + } + ) + } + } + + final class View: UIView { + private var emptyState: ComponentView? + private var contentListNode: ContentListNode? + private var emptySearchState: ComponentView? + + private let navigationBarView = ComponentView() + private var navigationHeight: CGFloat? + + private var searchBarNode: SearchBarNode? + + private var selectionPanel: ComponentView? + + private var isUpdating: Bool = false + + private var component: QuickReplySetupScreenComponent? + private(set) weak var state: EmptyComponentState? + private var environment: EnvironmentType? + + private var shortcutMessageList: ShortcutMessageList? + private var shortcutMessageListDisposable: Disposable? + private var keepUpdatedDisposable: Disposable? + + private var selectedIds = Set() + + private var isEditing: Bool = false + private var isSearchDisplayControllerActive: Bool = false + private var searchQuery: String = "" + private let searchQueryComponentSeparationCharacterSet: CharacterSet + + private var accountPeer: EnginePeer? + + override init(frame: CGRect) { + self.searchQueryComponentSeparationCharacterSet = CharacterSet(charactersIn: " _.:/") + + super.init(frame: frame) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + self.shortcutMessageListDisposable?.dispose() + self.keepUpdatedDisposable?.dispose() + } + + func scrollToTop() { + } + + func attemptNavigation(complete: @escaping () -> Void) -> Bool { + return true + } + + func openQuickReplyChat(shortcut: String?, shortcutId: Int32?) { + guard let component = self.component, let environment = self.environment else { + return + } + + if case let .select(completion) = component.mode { + if let shortcutId { + completion(shortcutId) + } + return + } + + if let shortcut { + let shortcutType: ChatQuickReplyShortcutType + if shortcut == "hello" { + shortcutType = .greeting + } else if shortcut == "away" { + shortcutType = .away + } else { + shortcutType = .generic + } + + let contents = AutomaticBusinessMessageSetupChatContents( + context: component.context, + kind: .quickReplyMessageInput(shortcut: shortcut, shortcutType: shortcutType), + shortcutId: shortcutId + ) + let chatController = component.context.sharedContext.makeChatController( + context: component.context, + chatLocation: .customChatContents, + subject: .customChatContents(contents: contents), + botStart: nil, + mode: .standard(.default) + ) + chatController.navigationPresentation = .modal + self.environment?.controller()?.push(chatController) + } else { + var completion: ((String?) -> Void)? + let alertController = quickReplyNameAlertController( + context: component.context, + text: environment.strings.QuickReply_CreateShortcutTitle, + subtext: environment.strings.QuickReply_CreateShortcutText, + value: "", + characterLimit: 32, + apply: { value in + completion?(value) + } + ) + completion = { [weak self, weak alertController] value in + guard let self, let environment = self.environment else { + alertController?.dismissAnimated() + return + } + if let value, !value.isEmpty { + guard let shortcutMessageList = self.shortcutMessageList else { + alertController?.dismissAnimated() + return + } + + if shortcutMessageList.items.contains(where: { $0.shortcut.lowercased() == value.lowercased() }) { + if let contentNode = alertController?.contentNode as? QuickReplyNameAlertContentNode { + contentNode.setErrorText(errorText: environment.strings.QuickReply_ShortcutExistsInlineError) + } + return + } + + alertController?.view.endEditing(true) + alertController?.dismissAnimated() + self.openQuickReplyChat(shortcut: value, shortcutId: nil) + } + } + self.environment?.controller()?.present(alertController, in: .window(.root)) + } + + self.contentListNode?.clearHighlightAnimated(true) + } + + func openEditShortcut(id: Int32, currentValue: String) { + guard let component = self.component, let environment = self.environment else { + return + } + + var completion: ((String?) -> Void)? + let alertController = quickReplyNameAlertController( + context: component.context, + text: environment.strings.QuickReply_EditShortcutTitle, + subtext: environment.strings.QuickReply_EditShortcutText, + value: currentValue, + characterLimit: 32, + apply: { value in + completion?(value) + } + ) + completion = { [weak self, weak alertController] value in + guard let self, let component = self.component, let environment = self.environment else { + alertController?.dismissAnimated() + return + } + if let value, !value.isEmpty { + if value == currentValue { + alertController?.dismissAnimated() + return + } + guard let shortcutMessageList = self.shortcutMessageList else { + alertController?.dismissAnimated() + return + } + + if shortcutMessageList.items.contains(where: { $0.shortcut.lowercased() == value.lowercased() }) { + if let contentNode = alertController?.contentNode as? QuickReplyNameAlertContentNode { + contentNode.setErrorText(errorText: environment.strings.QuickReply_ShortcutExistsInlineError) + } + } else { + component.context.engine.accountData.editMessageShortcut(id: id, shortcut: value) + + alertController?.view.endEditing(true) + alertController?.dismissAnimated() + } + } + } + self.environment?.controller()?.present(alertController, in: .window(.root)) + } + + func toggleShortcutSelection(id: Int32) { + if self.selectedIds.contains(id) { + self.selectedIds.remove(id) + } else { + self.selectedIds.insert(id) + } + self.state?.updated(transition: .spring(duration: 0.4)) + } + + func openDeleteShortcuts(ids: [Int32]) { + guard let component = self.component else { + return + } + + let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } + let actionSheet = ActionSheetController(presentationData: presentationData) + var items: [ActionSheetItem] = [] + + items.append(ActionSheetButtonItem(title: ids.count == 1 ? presentationData.strings.QuickReply_DeleteConfirmationSingle : presentationData.strings.QuickReply_DeleteConfirmationMultiple, color: .destructive, action: { [weak self, weak actionSheet] in + actionSheet?.dismissAnimated() + guard let self, let component = self.component else { + return + } + + for id in ids { + self.selectedIds.remove(id) + } + self.contentListNode?.setPendingRemoveItems(itemIds: ids) + component.context.engine.accountData.deleteMessageShortcuts(ids: ids) + self.state?.updated(transition: .spring(duration: 0.4)) + })) + + actionSheet.setItemGroups([ + ActionSheetItemGroup(items: items), + ActionSheetItemGroup(items: [ + ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + }) + ]) + ]) + self.environment?.controller()?.present(actionSheet, in: .window(.root)) + } + + private func updateNavigationBar( + component: QuickReplySetupScreenComponent, + theme: PresentationTheme, + strings: PresentationStrings, + size: CGSize, + insets: UIEdgeInsets, + statusBarHeight: CGFloat, + isModal: Bool, + transition: Transition, + deferScrollApplication: Bool + ) -> CGFloat { + var rightButtons: [AnyComponentWithIdentity] = [] + if let shortcutMessageList = self.shortcutMessageList, !shortcutMessageList.items.isEmpty { + if self.isEditing { + rightButtons.append(AnyComponentWithIdentity(id: "done", component: AnyComponent(NavigationButtonComponent( + content: .text(title: strings.Common_Done, isBold: true), + pressed: { [weak self] _ in + guard let self else { + return + } + self.isEditing = false + self.selectedIds.removeAll() + self.state?.updated(transition: .spring(duration: 0.4)) + } + )))) + } else { + rightButtons.append(AnyComponentWithIdentity(id: "edit", component: AnyComponent(NavigationButtonComponent( + content: .text(title: strings.Common_Edit, isBold: false), + pressed: { [weak self] _ in + guard let self else { + return + } + self.isEditing = true + self.state?.updated(transition: .spring(duration: 0.4)) + } + )))) + } + } + + let titleText: String + if !self.selectedIds.isEmpty { + titleText = strings.QuickReply_SelectedTitle(Int32(self.selectedIds.count)) + } else { + titleText = strings.QuickReply_Title + } + + let closeTitle: String + switch component.mode { + case .manage: + closeTitle = strings.Common_Close + case .select: + closeTitle = strings.Common_Cancel + } + let headerContent: ChatListHeaderComponent.Content? = ChatListHeaderComponent.Content( + title: titleText, + navigationBackTitle: nil, + titleComponent: nil, + chatListTitle: nil, + leftButton: isModal ? AnyComponentWithIdentity(id: "close", component: AnyComponent(NavigationButtonComponent( + content: .text(title: closeTitle, isBold: false), + pressed: { [weak self] _ in + guard let self else { + return + } + if self.attemptNavigation(complete: {}) { + self.environment?.controller()?.dismiss() + } + } + ))) : nil, + rightButtons: rightButtons, + backTitle: isModal ? nil : strings.Common_Back, + backPressed: { [weak self] in + guard let self else { + return + } + + if self.attemptNavigation(complete: {}) { + self.environment?.controller()?.dismiss() + } + } + ) + + let navigationBarSize = self.navigationBarView.update( + transition: transition, + component: AnyComponent(ChatListNavigationBar( + context: component.context, + theme: theme, + strings: strings, + statusBarHeight: statusBarHeight, + sideInset: insets.left, + isSearchActive: self.isSearchDisplayControllerActive, + isSearchEnabled: !self.isEditing, + primaryContent: headerContent, + secondaryContent: nil, + secondaryTransition: 0.0, + storySubscriptions: nil, + storiesIncludeHidden: false, + uploadProgress: [:], + tabsNode: nil, + tabsNodeIsSearch: false, + accessoryPanelContainer: nil, + accessoryPanelContainerHeight: 0.0, + activateSearch: { [weak self] _ in + guard let self else { + return + } + + self.isSearchDisplayControllerActive = true + self.state?.updated(transition: .spring(duration: 0.4)) + }, + openStatusSetup: { _ in + }, + allowAutomaticOrder: { + } + )), + environment: {}, + containerSize: size + ) + if let navigationBarComponentView = self.navigationBarView.view as? ChatListNavigationBar.View { + if deferScrollApplication { + navigationBarComponentView.deferScrollApplication = true + } + + if navigationBarComponentView.superview == nil { + self.addSubview(navigationBarComponentView) + } + transition.setFrame(view: navigationBarComponentView, frame: CGRect(origin: CGPoint(), size: navigationBarSize)) + + return navigationBarSize.height + } else { + return 0.0 + } + } + + private func updateNavigationScrolling(navigationHeight: CGFloat, transition: Transition) { + var mainOffset: CGFloat + if let shortcutMessageList = self.shortcutMessageList, !shortcutMessageList.items.isEmpty { + if let contentListNode = self.contentListNode { + switch contentListNode.visibleContentOffset() { + case .none: + mainOffset = 0.0 + case .unknown: + mainOffset = navigationHeight + case let .known(value): + mainOffset = value + } + } else { + mainOffset = navigationHeight + } + } else { + mainOffset = navigationHeight + } + + mainOffset = min(mainOffset, ChatListNavigationBar.searchScrollHeight) + if abs(mainOffset) < 0.1 { + mainOffset = 0.0 + } + + let resultingOffset = mainOffset + + var offset = resultingOffset + if self.isSearchDisplayControllerActive { + offset = 0.0 + } + + if let navigationBarComponentView = self.navigationBarView.view as? ChatListNavigationBar.View { + navigationBarComponentView.applyScroll(offset: offset, allowAvatarsExpansion: false, forceUpdate: false, transition: transition.withUserData(ChatListNavigationBar.AnimationHint( + disableStoriesAnimations: false, + crossfadeStoryPeers: false + ))) + } + } + + func update(component: QuickReplySetupScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + self.isUpdating = true + defer { + self.isUpdating = false + } + + if self.component == nil { + self.accountPeer = component.initialData.accountPeer + self.shortcutMessageList = component.initialData.shortcutMessageList + + self.shortcutMessageListDisposable = (component.context.engine.accountData.shortcutMessageList(onlyRemote: false) + |> deliverOnMainQueue).startStrict(next: { [weak self] shortcutMessageList in + guard let self else { + return + } + self.shortcutMessageList = shortcutMessageList + if !self.isUpdating { + self.state?.updated(transition: .immediate) + } + }) + + self.keepUpdatedDisposable = component.context.engine.accountData.keepShortcutMessageListUpdated().startStrict() + } + + let environment = environment[EnvironmentType.self].value + let themeUpdated = self.environment?.theme !== environment.theme + self.environment = environment + + self.component = component + self.state = state + + let alphaTransition: Transition = transition.animation.isImmediate ? transition : transition.withAnimation(.curve(duration: 0.25, curve: .easeInOut)) + let _ = alphaTransition + + if themeUpdated { + self.backgroundColor = environment.theme.list.plainBackgroundColor + } + + if let shortcutMessageList = self.shortcutMessageList, !shortcutMessageList.items.isEmpty { + if let emptyState = self.emptyState { + self.emptyState = nil + emptyState.view?.removeFromSuperview() + } + } else { + let emptyState: ComponentView + var emptyStateTransition = transition + if let current = self.emptyState { + emptyState = current + } else { + emptyState = ComponentView() + self.emptyState = emptyState + emptyStateTransition = emptyStateTransition.withAnimation(.none) + } + + let emptyStateFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: availableSize.width, height: availableSize.height)) + let _ = emptyState.update( + transition: emptyStateTransition, + component: AnyComponent(QuickReplyEmptyStateComponent( + theme: environment.theme, + strings: environment.strings, + insets: UIEdgeInsets(top: environment.navigationHeight, left: environment.safeInsets.left, bottom: environment.safeInsets.bottom + environment.additionalInsets.bottom, right: environment.safeInsets.right), + action: { [weak self] in + guard let self else { + return + } + self.openQuickReplyChat(shortcut: nil, shortcutId: nil) + } + )), + environment: {}, + containerSize: emptyStateFrame.size + ) + if let emptyStateView = emptyState.view { + if emptyStateView.superview == nil { + if let navigationBarComponentView = self.navigationBarView.view { + self.insertSubview(emptyStateView, belowSubview: navigationBarComponentView) + } else { + self.addSubview(emptyStateView) + } + } + emptyStateTransition.setFrame(view: emptyStateView, frame: emptyStateFrame) + } + } + + var isModal = false + if let controller = environment.controller(), controller.navigationPresentation == .modal { + isModal = true + } + if case .select = component.mode { + isModal = true + } + + var statusBarHeight = environment.statusBarHeight + if isModal { + statusBarHeight = max(statusBarHeight, 1.0) + } + + var listBottomInset = environment.safeInsets.bottom + environment.additionalInsets.bottom + let navigationHeight = self.updateNavigationBar( + component: component, + theme: environment.theme, + strings: environment.strings, + size: availableSize, + insets: environment.safeInsets, + statusBarHeight: statusBarHeight, + isModal: isModal, + transition: transition, + deferScrollApplication: true + ) + self.navigationHeight = navigationHeight + + var removedSearchBar: SearchBarNode? + if self.isSearchDisplayControllerActive { + let searchBarNode: SearchBarNode + var searchBarTransition = transition + if let current = self.searchBarNode { + searchBarNode = current + } else { + searchBarTransition = .immediate + let searchBarTheme = SearchBarNodeTheme(theme: environment.theme, hasSeparator: false) + searchBarNode = SearchBarNode( + theme: searchBarTheme, + strings: environment.strings, + fieldStyle: .modern, + displayBackground: false + ) + searchBarNode.placeholderString = NSAttributedString(string: environment.strings.Common_Search, font: Font.regular(17.0), textColor: searchBarTheme.placeholder) + self.searchBarNode = searchBarNode + searchBarNode.cancel = { [weak self] in + guard let self else { + return + } + self.isSearchDisplayControllerActive = false + self.state?.updated(transition: .spring(duration: 0.4)) + } + searchBarNode.textUpdated = { [weak self] query, _ in + guard let self else { + return + } + if self.searchQuery != query { + self.searchQuery = query.lowercased().trimmingCharacters(in: .whitespacesAndNewlines) + self.state?.updated(transition: .immediate) + } + } + DispatchQueue.main.async { [weak self, weak searchBarNode] in + guard let self, let searchBarNode, self.searchBarNode === searchBarNode else { + return + } + searchBarNode.activate() + + if let controller = self.environment?.controller() as? QuickReplySetupScreen { + controller.requestAttachmentMenuExpansion() + } + } + } + + var searchBarFrame = CGRect(origin: CGPoint(x: 0.0, y: navigationHeight - 54.0 + 2.0), size: CGSize(width: availableSize.width, height: 54.0)) + if isModal { + searchBarFrame.origin.y += 2.0 + } + searchBarNode.updateLayout(boundingSize: searchBarFrame.size, leftInset: environment.safeInsets.left + 6.0, rightInset: environment.safeInsets.right, transition: searchBarTransition.containedViewLayoutTransition) + searchBarTransition.setFrame(view: searchBarNode.view, frame: searchBarFrame) + if searchBarNode.view.superview == nil { + self.addSubview(searchBarNode.view) + + if case let .curve(duration, curve) = transition.animation, let navigationBarView = self.navigationBarView.view as? ChatListNavigationBar.View, let placeholderNode = navigationBarView.searchContentNode?.placeholderNode { + let timingFunction: String + switch curve { + case .easeInOut: + timingFunction = CAMediaTimingFunctionName.easeOut.rawValue + case .linear: + timingFunction = CAMediaTimingFunctionName.linear.rawValue + case .spring: + timingFunction = kCAMediaTimingFunctionSpring + case .custom: + timingFunction = kCAMediaTimingFunctionSpring + } + + searchBarNode.animateIn(from: placeholderNode, duration: duration, timingFunction: timingFunction) + } + } + } else { + self.searchQuery = "" + if let searchBarNode = self.searchBarNode { + self.searchBarNode = nil + removedSearchBar = searchBarNode + } + } + + if !self.selectedIds.isEmpty { + let selectionPanel: ComponentView + var selectionPanelTransition = transition + if let current = self.selectionPanel { + selectionPanel = current + } else { + selectionPanelTransition = selectionPanelTransition.withAnimation(.none) + selectionPanel = ComponentView() + self.selectionPanel = selectionPanel + } + + let buttonTitle: String = environment.strings.QuickReply_DeleteAction(Int32(self.selectedIds.count)) + + let selectionPanelSize = selectionPanel.update( + transition: selectionPanelTransition, + component: AnyComponent(BottomPanelComponent( + theme: environment.theme, + content: AnyComponentWithIdentity(id: 0, component: AnyComponent(PlainButtonComponent( + content: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: buttonTitle, font: Font.regular(17.0), textColor: environment.theme.list.itemDestructiveColor)) + )), + background: nil, + effectAlignment: .center, + minSize: CGSize(width: availableSize.width - environment.safeInsets.left - environment.safeInsets.right, height: 44.0), + contentInsets: UIEdgeInsets(), + action: { [weak self] in + guard let self else { + return + } + if self.selectedIds.isEmpty { + return + } + self.openDeleteShortcuts(ids: Array(self.selectedIds)) + }, + animateAlpha: true, + animateScale: false, + animateContents: false + ))), + insets: UIEdgeInsets(top: 4.0, left: environment.safeInsets.left, bottom: environment.safeInsets.bottom + environment.additionalInsets.bottom, right: environment.safeInsets.right) + )), + environment: {}, + containerSize: availableSize + ) + let selectionPanelFrame = CGRect(origin: CGPoint(x: 0.0, y: availableSize.height - selectionPanelSize.height), size: selectionPanelSize) + listBottomInset = selectionPanelSize.height + if let selectionPanelView = selectionPanel.view { + var animateIn = false + if selectionPanelView.superview == nil { + animateIn = true + self.addSubview(selectionPanelView) + } + selectionPanelTransition.setFrame(view: selectionPanelView, frame: selectionPanelFrame) + if animateIn { + transition.animatePosition(view: selectionPanelView, from: CGPoint(x: 0.0, y: selectionPanelFrame.height), to: CGPoint(), additive: true) + } + } + } else { + if let selectionPanel = self.selectionPanel { + self.selectionPanel = nil + if let selectionPanelView = selectionPanel.view { + transition.setPosition(view: selectionPanelView, position: CGPoint(x: selectionPanelView.center.x, y: availableSize.height + selectionPanelView.bounds.height * 0.5), completion: { [weak selectionPanelView] _ in + selectionPanelView?.removeFromSuperview() + }) + } + } + } + + let contentListNode: ContentListNode + if let current = self.contentListNode { + contentListNode = current + } else { + contentListNode = ContentListNode(parentView: self, context: component.context) + self.contentListNode = contentListNode + + contentListNode.visibleContentOffsetChanged = { [weak self] offset in + guard let self else { + return + } + guard let navigationHeight = self.navigationHeight else { + return + } + + self.updateNavigationScrolling(navigationHeight: navigationHeight, transition: .immediate) + } + + if let selectionPanelView = self.selectionPanel?.view { + self.insertSubview(contentListNode.view, belowSubview: selectionPanelView) + } else if let navigationBarComponentView = self.navigationBarView.view { + self.insertSubview(contentListNode.view, belowSubview: navigationBarComponentView) + } else { + self.addSubview(contentListNode.view) + } + } + + transition.setFrame(view: contentListNode.view, frame: CGRect(origin: CGPoint(), size: availableSize)) + contentListNode.update(size: availableSize, insets: UIEdgeInsets(top: navigationHeight, left: environment.safeInsets.left, bottom: listBottomInset, right: environment.safeInsets.right), transition: transition) + + var entries: [ContentEntry] = [] + if let shortcutMessageList = self.shortcutMessageList, let accountPeer = self.accountPeer { + switch component.mode { + case .manage: + if self.searchQuery.isEmpty { + entries.append(.add) + } + case .select: + break + } + for item in shortcutMessageList.items { + if !self.searchQuery.isEmpty { + var matches = false + inner: for nameComponent in item.shortcut.lowercased().components(separatedBy: self.searchQueryComponentSeparationCharacterSet) { + if nameComponent.lowercased().hasPrefix(self.searchQuery) { + matches = true + break inner + } + } + if !matches { + continue + } + } + var isItemSelected = false + if let itemId = item.id { + isItemSelected = self.selectedIds.contains(itemId) + } + entries.append(.item(item: item, accountPeer: accountPeer, sortIndex: entries.count, isEditing: self.isEditing, isSelected: isItemSelected)) + } + } + contentListNode.setEntries(entries: entries, animated: !transition.animation.isImmediate) + + if !self.searchQuery.isEmpty && entries.isEmpty { + var emptySearchStateTransition = transition + let emptySearchState: ComponentView + if let current = self.emptySearchState { + emptySearchState = current + } else { + emptySearchStateTransition = emptySearchStateTransition.withAnimation(.none) + emptySearchState = ComponentView() + self.emptySearchState = emptySearchState + } + let emptySearchStateSize = emptySearchState.update( + transition: .immediate, + component: AnyComponent(BalancedTextComponent( + text: .plain(NSAttributedString(string: environment.strings.Conversation_SearchNoResults, font: Font.regular(17.0), textColor: environment.theme.list.freeTextColor, paragraphAlignment: .center)), + horizontalAlignment: .center, + maximumNumberOfLines: 0 + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - 16.0 * 2.0, height: availableSize.height) + ) + var emptySearchStateBottomInset = listBottomInset + emptySearchStateBottomInset = max(emptySearchStateBottomInset, environment.inputHeight) + let emptySearchStateFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - emptySearchStateSize.width) * 0.5), y: navigationHeight + floor((availableSize.height - emptySearchStateBottomInset - navigationHeight) * 0.5)), size: emptySearchStateSize) + if let emptySearchStateView = emptySearchState.view { + if emptySearchStateView.superview == nil { + if let navigationBarComponentView = self.navigationBarView.view { + self.insertSubview(emptySearchStateView, belowSubview: navigationBarComponentView) + } else { + self.addSubview(emptySearchStateView) + } + } + emptySearchStateTransition.containedViewLayoutTransition.updatePosition(layer: emptySearchStateView.layer, position: emptySearchStateFrame.center) + emptySearchStateView.bounds = CGRect(origin: CGPoint(), size: emptySearchStateFrame.size) + } + } else if let emptySearchState = self.emptySearchState { + self.emptySearchState = nil + emptySearchState.view?.removeFromSuperview() + } + + if let shortcutMessageList = self.shortcutMessageList, !shortcutMessageList.items.isEmpty { + contentListNode.isHidden = false + } else { + contentListNode.isHidden = true + } + + self.updateNavigationScrolling(navigationHeight: navigationHeight, transition: transition) + + if let navigationBarComponentView = self.navigationBarView.view as? ChatListNavigationBar.View { + navigationBarComponentView.deferScrollApplication = false + navigationBarComponentView.applyCurrentScroll(transition: transition) + } + + if let removedSearchBar { + if !transition.animation.isImmediate, let navigationBarView = self.navigationBarView.view as? ChatListNavigationBar.View, let placeholderNode = + navigationBarView.searchContentNode?.placeholderNode { + removedSearchBar.transitionOut(to: placeholderNode, transition: transition.containedViewLayoutTransition, completion: { [weak removedSearchBar] in + removedSearchBar?.view.removeFromSuperview() + }) + } else { + removedSearchBar.view.removeFromSuperview() + } + } + + return availableSize + } + } + + func makeView() -> View { + return View() + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} + +public final class QuickReplySetupScreen: ViewControllerComponentContainer, AttachmentContainable { + public final class InitialData: QuickReplySetupScreenInitialData { + let accountPeer: EnginePeer? + let shortcutMessageList: ShortcutMessageList + + init( + accountPeer: EnginePeer?, + shortcutMessageList: ShortcutMessageList + ) { + self.accountPeer = accountPeer + self.shortcutMessageList = shortcutMessageList + } + } + + public enum Mode { + case manage + case select(completion: (Int32) -> Void) + } + + private let context: AccountContext + + public var requestAttachmentMenuExpansion: () -> Void = { + } + public var updateNavigationStack: (@escaping ([AttachmentContainable]) -> ([AttachmentContainable], AttachmentMediaPickerContext?)) -> Void = { _ in + } + public var updateTabBarAlpha: (CGFloat, ContainedViewLayoutTransition) -> Void = { _, _ in + } + public var cancelPanGesture: () -> Void = { + } + public var isContainerPanning: () -> Bool = { + return false + } + public var isContainerExpanded: () -> Bool = { + return false + } + public var mediaPickerContext: AttachmentMediaPickerContext? + + public init(context: AccountContext, initialData: InitialData, mode: Mode) { + self.context = context + + super.init(context: context, component: QuickReplySetupScreenComponent( + context: context, + initialData: initialData, + mode: mode + ), navigationBarAppearance: .none, theme: .default, updatedPresentationData: nil) + + self.scrollToTop = { [weak self] in + guard let self, let componentView = self.node.hostView.componentView as? QuickReplySetupScreenComponent.View else { + return + } + componentView.scrollToTop() + } + + self.attemptNavigation = { [weak self] complete in + guard let self, let componentView = self.node.hostView.componentView as? QuickReplySetupScreenComponent.View else { + return true + } + + return componentView.attemptNavigation(complete: complete) + } + } + + required public init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + } + + @objc private func cancelPressed() { + self.dismiss() + } + + override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { + super.containerLayoutUpdated(layout, transition: transition) + } + + public static func initialData(context: AccountContext) -> Signal { + return combineLatest( + context.engine.data.get( + TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId) + ), + context.engine.accountData.shortcutMessageList(onlyRemote: false) + |> take(1) + ) + |> map { accountPeer, shortcutMessageList -> QuickReplySetupScreenInitialData in + return InitialData( + accountPeer: accountPeer, + shortcutMessageList: shortcutMessageList + ) + } + } + + public func isContainerPanningUpdated(_ panning: Bool) { + } + + public func resetForReuse() { + } + + public func prepareForReuse() { + } + + public func requestDismiss(completion: @escaping () -> Void) { + completion() + } + + public func shouldDismissImmediately() -> Bool { + return true + } +} diff --git a/submodules/TelegramUI/Components/Settings/BusinessHoursSetupScreen/BUILD b/submodules/TelegramUI/Components/Settings/BusinessHoursSetupScreen/BUILD new file mode 100644 index 00000000000..426f4b106b8 --- /dev/null +++ b/submodules/TelegramUI/Components/Settings/BusinessHoursSetupScreen/BUILD @@ -0,0 +1,43 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "BusinessHoursSetupScreen", + module_name = "BusinessHoursSetupScreen", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/AsyncDisplayKit", + "//submodules/Display", + "//submodules/Postbox", + "//submodules/TelegramCore", + "//submodules/SSignalKit/SwiftSignalKit", + "//submodules/TelegramPresentationData", + "//submodules/TelegramUIPreferences", + "//submodules/AccountContext", + "//submodules/PresentationDataUtils", + "//submodules/Markdown", + "//submodules/ComponentFlow", + "//submodules/Components/ViewControllerComponent", + "//submodules/Components/BundleIconComponent", + "//submodules/Components/MultilineTextComponent", + "//submodules/Components/BalancedTextComponent", + "//submodules/TelegramUI/Components/ListSectionComponent", + "//submodules/TelegramUI/Components/ListActionItemComponent", + "//submodules/TelegramUI/Components/ListTextFieldItemComponent", + "//submodules/TelegramUI/Components/LottieComponent", + "//submodules/TelegramUI/Components/PlainButtonComponent", + "//submodules/LocationUI", + "//submodules/AppBundle", + "//submodules/TelegramStringFormatting", + "//submodules/UIKitRuntimeUtils", + "//submodules/TelegramUI/Components/Settings/TimezoneSelectionScreen", + "//submodules/TelegramUI/Components/TimeSelectionActionSheet", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Components/Settings/BusinessHoursSetupScreen/Sources/BusinessDaySetupScreen.swift b/submodules/TelegramUI/Components/Settings/BusinessHoursSetupScreen/Sources/BusinessDaySetupScreen.swift new file mode 100644 index 00000000000..e663082df39 --- /dev/null +++ b/submodules/TelegramUI/Components/Settings/BusinessHoursSetupScreen/Sources/BusinessDaySetupScreen.swift @@ -0,0 +1,677 @@ +import Foundation +import UIKit +import Display +import AsyncDisplayKit +import SwiftSignalKit +import Postbox +import TelegramCore +import TelegramPresentationData +import TelegramUIPreferences +import PresentationDataUtils +import AccountContext +import ComponentFlow +import ViewControllerComponent +import MultilineTextComponent +import BalancedTextComponent +import ListSectionComponent +import ListActionItemComponent +import BundleIconComponent +import LottieComponent +import Markdown +import LocationUI +import TelegramStringFormatting +import PlainButtonComponent +import TimeSelectionActionSheet + +func clipMinutes(_ value: Int) -> Int { + return value % (24 * 60) +} + +final class BusinessDaySetupScreenComponent: Component { + typealias EnvironmentType = ViewControllerComponentContainer.Environment + + let context: AccountContext + let dayIndex: Int + let day: BusinessHoursSetupScreenComponent.Day + + init( + context: AccountContext, + dayIndex: Int, + day: BusinessHoursSetupScreenComponent.Day + ) { + self.context = context + self.dayIndex = dayIndex + self.day = day + } + + static func ==(lhs: BusinessDaySetupScreenComponent, rhs: BusinessDaySetupScreenComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.dayIndex != rhs.dayIndex { + return false + } + if lhs.day != rhs.day { + return false + } + + return true + } + + private final class ScrollView: UIScrollView { + override func touchesShouldCancel(in view: UIView) -> Bool { + return true + } + } + + final class View: UIView, UIScrollViewDelegate { + private let topOverscrollLayer = SimpleLayer() + private let scrollView: ScrollView + + private let navigationTitle = ComponentView() + private let generalSection = ComponentView() + private var rangeSections: [Int: ComponentView] = [:] + private let addSection = ComponentView() + + private var isUpdating: Bool = false + + private var component: BusinessDaySetupScreenComponent? + private(set) weak var state: EmptyComponentState? + private var environment: EnvironmentType? + + private(set) var isOpen: Bool = false + private(set) var ranges: [BusinessHoursSetupScreenComponent.WorkingHourRange] = [] + private var intersectingRanges = Set() + private var nextRangeId: Int = 0 + + override init(frame: CGRect) { + self.scrollView = ScrollView() + self.scrollView.showsVerticalScrollIndicator = true + self.scrollView.showsHorizontalScrollIndicator = false + self.scrollView.scrollsToTop = false + self.scrollView.delaysContentTouches = false + self.scrollView.canCancelContentTouches = true + self.scrollView.contentInsetAdjustmentBehavior = .never + if #available(iOS 13.0, *) { + self.scrollView.automaticallyAdjustsScrollIndicatorInsets = false + } + self.scrollView.alwaysBounceVertical = true + + super.init(frame: frame) + + self.scrollView.delegate = self + self.addSubview(self.scrollView) + + self.scrollView.layer.addSublayer(self.topOverscrollLayer) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + } + + func scrollToTop() { + self.scrollView.setContentOffset(CGPoint(), animated: true) + } + + func attemptNavigation(complete: @escaping () -> Void) -> Bool { + guard let component = self.component, let enviroment = self.environment else { + return true + } + + if self.isOpen { + if self.intersectingRanges.isEmpty { + return true + } + } + + let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } + self.environment?.controller()?.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: nil, text: enviroment.strings.BusinessHoursSetup_ErrorIntersectingHours_Text, actions: [ + TextAlertAction(type: .genericAction, title: enviroment.strings.Common_Cancel, action: { + }), + TextAlertAction(type: .defaultAction, title: enviroment.strings.BusinessHoursSetup_ErrorIntersectingHours_ResetAction, action: { + complete() + }) + ]), in: .window(.root)) + + return false + } + + func scrollViewDidScroll(_ scrollView: UIScrollView) { + self.updateScrolling(transition: .immediate) + } + + var scrolledUp = true + private func updateScrolling(transition: Transition) { + let navigationRevealOffsetY: CGFloat = 0.0 + + let navigationAlphaDistance: CGFloat = 16.0 + let navigationAlpha: CGFloat = max(0.0, min(1.0, (self.scrollView.contentOffset.y - navigationRevealOffsetY) / navigationAlphaDistance)) + if let controller = self.environment?.controller(), let navigationBar = controller.navigationBar { + transition.setAlpha(layer: navigationBar.backgroundNode.layer, alpha: navigationAlpha) + transition.setAlpha(layer: navigationBar.stripeNode.layer, alpha: navigationAlpha) + } + + var scrolledUp = false + if navigationAlpha < 0.5 { + scrolledUp = true + } else if navigationAlpha > 0.5 { + scrolledUp = false + } + + if self.scrolledUp != scrolledUp { + self.scrolledUp = scrolledUp + if !self.isUpdating { + self.state?.updated() + } + } + + if let navigationTitleView = self.navigationTitle.view { + transition.setAlpha(view: navigationTitleView, alpha: 1.0) + } + } + + private func openRangeDateSetup(rangeId: Int, isStartTime: Bool) { + guard let component = self.component else { + return + } + guard let range = self.ranges.first(where: { $0.id == rangeId }) else { + return + } + + let controller = TimeSelectionActionSheet(context: component.context, currentValue: Int32(isStartTime ? clipMinutes(range.startMinute) : clipMinutes(range.endMinute)) * 60, applyValue: { [weak self] value in + guard let self else { + return + } + guard let value else { + return + } + if let index = self.ranges.firstIndex(where: { $0.id == rangeId }) { + var startMinute = range.startMinute + var endMinute = range.endMinute + if isStartTime { + startMinute = Int(value) / 60 + } else { + endMinute = Int(value) / 60 + } + if endMinute < startMinute { + endMinute = endMinute + 24 * 60 + } + self.ranges[index].startMinute = startMinute + self.ranges[index].endMinute = endMinute + self.validateRanges() + + self.state?.updated(transition: .immediate) + } + }) + self.environment?.controller()?.present(controller, in: .window(.root)) + } + + private func validateRanges() { + self.ranges.sort(by: { $0.startMinute < $1.startMinute }) + + self.intersectingRanges.removeAll() + for i in 0 ..< self.ranges.count { + var minuteSet = IndexSet() + inner: for j in 0 ..< self.ranges.count { + if i == j { + continue inner + } + let range = self.ranges[j] + let rangeMinutes = range.startMinute ..< range.endMinute + minuteSet.insert(integersIn: rangeMinutes) + } + + let range = self.ranges[i] + let rangeMinutes = range.startMinute ..< range.endMinute + + if minuteSet.intersects(integersIn: rangeMinutes) { + self.intersectingRanges.insert(range.id) + } + } + } + + func update(component: BusinessDaySetupScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + self.isUpdating = true + defer { + self.isUpdating = false + } + + let environment = environment[EnvironmentType.self].value + let themeUpdated = self.environment?.theme !== environment.theme + self.environment = environment + + if self.component == nil { + self.isOpen = component.day.ranges != nil + self.ranges = component.day.ranges ?? [] + self.nextRangeId = (self.ranges.map(\.id).max() ?? 0) + 1 + self.validateRanges() + } + + self.component = component + self.state = state + + if themeUpdated { + self.backgroundColor = environment.theme.list.blocksBackgroundColor + } + + let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } + + let title: String + switch component.dayIndex { + case 0: + title = environment.strings.Weekday_Monday + case 1: + title = environment.strings.Weekday_Tuesday + case 2: + title = environment.strings.Weekday_Wednesday + case 3: + title = environment.strings.Weekday_Thursday + case 4: + title = environment.strings.Weekday_Friday + case 5: + title = environment.strings.Weekday_Saturday + case 6: + title = environment.strings.Weekday_Sunday + default: + title = " " + } + let navigationTitleSize = self.navigationTitle.update( + transition: transition, + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: title, font: Font.semibold(17.0), textColor: environment.theme.rootController.navigationBar.primaryTextColor)), + horizontalAlignment: .center + )), + environment: {}, + containerSize: CGSize(width: availableSize.width, height: 100.0) + ) + let navigationTitleFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - navigationTitleSize.width) / 2.0), y: environment.statusBarHeight + floor((environment.navigationHeight - environment.statusBarHeight - navigationTitleSize.height) / 2.0)), size: navigationTitleSize) + if let navigationTitleView = self.navigationTitle.view { + if navigationTitleView.superview == nil { + if let controller = self.environment?.controller(), let navigationBar = controller.navigationBar { + navigationBar.view.addSubview(navigationTitleView) + } + } + transition.setFrame(view: navigationTitleView, frame: navigationTitleFrame) + } + + let bottomContentInset: CGFloat = 24.0 + let sideInset: CGFloat = 16.0 + environment.safeInsets.left + let sectionSpacing: CGFloat = 24.0 + + let _ = bottomContentInset + let _ = sectionSpacing + + var contentHeight: CGFloat = 0.0 + + contentHeight += environment.navigationHeight + contentHeight += 16.0 + + let generalSectionSize = self.generalSection.update( + transition: transition, + component: AnyComponent(ListSectionComponent( + theme: environment.theme, + header: nil, + footer: nil, + items: [ + AnyComponentWithIdentity(id: 0, component: AnyComponent(ListActionItemComponent( + theme: environment.theme, + title: AnyComponent(VStack([ + AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: environment.strings.BusinessHoursSetup_DaySwitch, + font: Font.regular(presentationData.listsFontSize.baseDisplaySize), + textColor: environment.theme.list.itemPrimaryTextColor + )), + maximumNumberOfLines: 1 + ))), + ], alignment: .left, spacing: 2.0)), + accessory: .toggle(ListActionItemComponent.Toggle(style: .regular, isOn: self.isOpen, action: { [weak self] _ in + guard let self else { + return + } + self.isOpen = !self.isOpen + self.state?.updated(transition: .spring(duration: 0.4)) + })), + action: nil + ))) + ] + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 10000.0) + ) + let generalSectionFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: generalSectionSize) + if let generalSectionView = self.generalSection.view { + if generalSectionView.superview == nil { + self.scrollView.addSubview(generalSectionView) + } + transition.setFrame(view: generalSectionView, frame: generalSectionFrame) + } + contentHeight += generalSectionSize.height + contentHeight += sectionSpacing + + var rangesSectionsHeight: CGFloat = 0.0 + for range in self.ranges { + let rangeId = range.id + var rangeSectionTransition = transition + let rangeSection: ComponentView + if let current = self.rangeSections[range.id] { + rangeSection = current + } else { + rangeSection = ComponentView() + self.rangeSections[range.id] = rangeSection + rangeSectionTransition = rangeSectionTransition.withAnimation(.none) + } + + let startHours = clipMinutes(range.startMinute) / 60 + let startMinutes = clipMinutes(range.startMinute) % 60 + let startText = stringForShortTimestamp(hours: Int32(startHours), minutes: Int32(startMinutes), dateTimeFormat: PresentationDateTimeFormat()) + let endHours = clipMinutes(range.endMinute) / 60 + let endMinutes = clipMinutes(range.endMinute) % 60 + let endText = stringForShortTimestamp(hours: Int32(endHours), minutes: Int32(endMinutes), dateTimeFormat: PresentationDateTimeFormat()) + + var rangeSectionItems: [AnyComponentWithIdentity] = [] + for i in 0 ..< 2 { + let isOpenTime = i == 0 + rangeSectionItems.append(AnyComponentWithIdentity(id: rangeSectionItems.count, component: AnyComponent(ListActionItemComponent( + theme: environment.theme, + title: AnyComponent(VStack([ + AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: isOpenTime ? environment.strings.BusinessHoursSetup_DayIntervalStart : environment.strings.BusinessHoursSetup_DayIntervalEnd, + font: Font.regular(presentationData.listsFontSize.baseDisplaySize), + textColor: environment.theme.list.itemPrimaryTextColor + )), + maximumNumberOfLines: 1 + ))), + ], alignment: .left, spacing: 2.0)), + icon: ListActionItemComponent.Icon(component: AnyComponentWithIdentity(id: 0, component: AnyComponent(PlainButtonComponent( + content: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: isOpenTime ? startText : endText, font: Font.regular(17.0), textColor: self.intersectingRanges.contains(range.id) ? environment.theme.list.itemDestructiveColor : environment.theme.list.itemPrimaryTextColor)) + )), + background: AnyComponent(RoundedRectangle(color: environment.theme.list.itemPrimaryTextColor.withMultipliedAlpha(0.1), cornerRadius: 6.0)), + effectAlignment: .center, + minSize: nil, + contentInsets: UIEdgeInsets(top: 7.0, left: 8.0, bottom: 7.0, right: 8.0), + action: { [weak self] in + guard let self else { + return + } + self.openRangeDateSetup(rangeId: rangeId, isStartTime: isOpenTime) + }, + animateAlpha: true, + animateScale: false + ))), insets: .custom(UIEdgeInsets(top: 4.0, left: 0.0, bottom: 4.0, right: 0.0)), allowUserInteraction: true), + accessory: nil, + action: { [weak self] _ in + guard let self else { + return + } + self.openRangeDateSetup(rangeId: rangeId, isStartTime: isOpenTime) + } + )))) + } + + rangeSectionItems.append(AnyComponentWithIdentity(id: rangeSectionItems.count, component: AnyComponent(ListActionItemComponent( + theme: environment.theme, + title: AnyComponent(VStack([ + AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: environment.strings.BusinessHoursSetup_DayIntervalRemove, + font: Font.regular(presentationData.listsFontSize.baseDisplaySize), + textColor: environment.theme.list.itemDestructiveColor + )), + maximumNumberOfLines: 1 + ))), + ], alignment: .left, spacing: 2.0)), + accessory: nil, + action: { [weak self] _ in + guard let self else { + return + } + self.ranges.removeAll(where: { $0.id == rangeId }) + self.validateRanges() + self.state?.updated(transition: .spring(duration: 0.4)) + } + )))) + + let rangeSectionSize = rangeSection.update( + transition: rangeSectionTransition, + component: AnyComponent(ListSectionComponent( + theme: environment.theme, + header: nil, + footer: nil, + items: rangeSectionItems + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 10000.0) + ) + let rangeSectionFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight + rangesSectionsHeight), size: rangeSectionSize) + if let rangeSectionView = rangeSection.view { + var animateIn = false + if rangeSectionView.superview == nil { + animateIn = true + rangeSectionView.layer.allowsGroupOpacity = true + self.scrollView.addSubview(rangeSectionView) + } + rangeSectionTransition.setFrame(view: rangeSectionView, frame: rangeSectionFrame) + + let alphaTransition = transition.animation.isImmediate ? transition : .easeInOut(duration: 0.25) + if self.isOpen { + if animateIn { + if !transition.animation.isImmediate { + alphaTransition.animateAlpha(view: rangeSectionView, from: 0.0, to: 1.0) + transition.animateScale(view: rangeSectionView, from: 0.001, to: 1.0) + } + } else { + alphaTransition.setAlpha(view: rangeSectionView, alpha: 1.0) + } + } else { + alphaTransition.setAlpha(view: rangeSectionView, alpha: 0.0) + } + } + + rangesSectionsHeight += rangeSectionSize.height + rangesSectionsHeight += sectionSpacing + } + var removeRangeSectionIds: [Int] = [] + for (id, rangeSection) in self.rangeSections { + if !self.ranges.contains(where: { $0.id == id }) { + removeRangeSectionIds.append(id) + + if let rangeSectionView = rangeSection.view { + if !transition.animation.isImmediate { + Transition.easeInOut(duration: 0.2).setAlpha(view: rangeSectionView, alpha: 0.0, completion: { [weak rangeSectionView] _ in + rangeSectionView?.removeFromSuperview() + }) + transition.setScale(view: rangeSectionView, scale: 0.001) + } else { + rangeSectionView.removeFromSuperview() + } + } + } + } + for id in removeRangeSectionIds { + self.rangeSections.removeValue(forKey: id) + } + + var canAddRanges = true + if let lastRange = self.ranges.last, lastRange.endMinute >= 24 * 60 * 2 - 1 { + canAddRanges = false + } + + let addSectionSize = self.addSection.update( + transition: transition, + component: AnyComponent(ListSectionComponent( + theme: environment.theme, + header: nil, + footer: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: environment.strings.BusinessHoursSetup_AddSectionFooter, + font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize), + textColor: environment.theme.list.freeTextColor + )), + maximumNumberOfLines: 0 + )), + items: [ + AnyComponentWithIdentity(id: 0, component: AnyComponent(ListActionItemComponent( + theme: environment.theme, + title: AnyComponent(VStack([ + AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: environment.strings.BusinessHoursSetup_AddAction, + font: Font.regular(presentationData.listsFontSize.baseDisplaySize), + textColor: environment.theme.list.itemAccentColor + )), + maximumNumberOfLines: 1 + ))), + ], alignment: .left, spacing: 2.0)), + leftIcon: AnyComponentWithIdentity(id: 0, component: AnyComponent(BundleIconComponent( + name: "Item List/AddTimeIcon", + tintColor: environment.theme.list.itemAccentColor + ))), + accessory: nil, + action: { [weak self] _ in + guard let self else { + return + } + + var rangeStart = 9 * 60 + if let lastRange = self.ranges.last { + rangeStart = lastRange.endMinute + 1 + } + if rangeStart >= 2 * 24 * 60 - 1 { + return + } + let rangeEnd = min(rangeStart + 9 * 60, 2 * 24 * 60) + + let rangeId = self.nextRangeId + self.nextRangeId += 1 + + self.ranges.append(BusinessHoursSetupScreenComponent.WorkingHourRange( + id: rangeId, startMinute: rangeStart, endMinute: rangeEnd)) + self.validateRanges() + self.state?.updated(transition: .spring(duration: 0.4)) + } + ))) + ] + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 10000.0) + ) + let addSectionFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight + rangesSectionsHeight), size: addSectionSize) + if let addSectionView = self.addSection.view { + if addSectionView.superview == nil { + self.scrollView.addSubview(addSectionView) + } + transition.setFrame(view: addSectionView, frame: addSectionFrame) + + let alphaTransition = transition.animation.isImmediate ? transition : .easeInOut(duration: 0.25) + alphaTransition.setAlpha(view: addSectionView, alpha: (self.isOpen && canAddRanges) ? 1.0 : 0.0) + } + if canAddRanges { + rangesSectionsHeight += addSectionSize.height + } + + if self.isOpen { + contentHeight += rangesSectionsHeight + } + + contentHeight += bottomContentInset + contentHeight += environment.safeInsets.bottom + + let previousBounds = self.scrollView.bounds + + let contentSize = CGSize(width: availableSize.width, height: contentHeight) + if self.scrollView.frame != CGRect(origin: CGPoint(), size: availableSize) { + self.scrollView.frame = CGRect(origin: CGPoint(), size: availableSize) + } + if self.scrollView.contentSize != contentSize { + self.scrollView.contentSize = contentSize + } + let scrollInsets = UIEdgeInsets(top: environment.navigationHeight, left: 0.0, bottom: 0.0, right: 0.0) + if self.scrollView.scrollIndicatorInsets != scrollInsets { + self.scrollView.scrollIndicatorInsets = scrollInsets + } + + if !previousBounds.isEmpty, !transition.animation.isImmediate { + let bounds = self.scrollView.bounds + if bounds.maxY != previousBounds.maxY { + let offsetY = previousBounds.maxY - bounds.maxY + transition.animateBoundsOrigin(view: self.scrollView, from: CGPoint(x: 0.0, y: offsetY), to: CGPoint(), additive: true) + } + } + + self.topOverscrollLayer.frame = CGRect(origin: CGPoint(x: 0.0, y: -3000.0), size: CGSize(width: availableSize.width, height: 3000.0)) + + self.updateScrolling(transition: transition) + + return availableSize + } + } + + func makeView() -> View { + return View() + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} + +final class BusinessDaySetupScreen: ViewControllerComponentContainer { + private let context: AccountContext + fileprivate let updateDay: (BusinessHoursSetupScreenComponent.Day) -> Void + + init(context: AccountContext, dayIndex: Int, day: BusinessHoursSetupScreenComponent.Day, updateDay: @escaping (BusinessHoursSetupScreenComponent.Day) -> Void) { + self.context = context + self.updateDay = updateDay + + super.init(context: context, component: BusinessDaySetupScreenComponent( + context: context, + dayIndex: dayIndex, + day: day + ), navigationBarAppearance: .default, theme: .default, updatedPresentationData: nil) + + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + self.title = "" + self.navigationItem.backBarButtonItem = UIBarButtonItem(title: presentationData.strings.Common_Back, style: .plain, target: nil, action: nil) + + self.scrollToTop = { [weak self] in + guard let self, let componentView = self.node.hostView.componentView as? BusinessDaySetupScreenComponent.View else { + return + } + componentView.scrollToTop() + } + + self.attemptNavigation = { [weak self] complete in + guard let self, let componentView = self.node.hostView.componentView as? BusinessDaySetupScreenComponent.View else { + return true + } + + if componentView.attemptNavigation(complete: complete) { + self.updateDay(BusinessHoursSetupScreenComponent.Day(ranges: componentView.isOpen ? componentView.ranges : nil)) + return true + } else { + return false + } + } + } + + required public init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + } + + @objc private func cancelPressed() { + self.dismiss() + } + + override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { + super.containerLayoutUpdated(layout, transition: transition) + } +} diff --git a/submodules/TelegramUI/Components/Settings/BusinessHoursSetupScreen/Sources/BusinessHoursSetupScreen.swift b/submodules/TelegramUI/Components/Settings/BusinessHoursSetupScreen/Sources/BusinessHoursSetupScreen.swift new file mode 100644 index 00000000000..7166538490d --- /dev/null +++ b/submodules/TelegramUI/Components/Settings/BusinessHoursSetupScreen/Sources/BusinessHoursSetupScreen.swift @@ -0,0 +1,871 @@ +import Foundation +import UIKit +import Display +import AsyncDisplayKit +import SwiftSignalKit +import Postbox +import TelegramCore +import TelegramPresentationData +import TelegramUIPreferences +import PresentationDataUtils +import AccountContext +import ComponentFlow +import ViewControllerComponent +import MultilineTextComponent +import BalancedTextComponent +import ListSectionComponent +import ListActionItemComponent +import BundleIconComponent +import LottieComponent +import Markdown +import LocationUI +import TelegramStringFormatting +import TimezoneSelectionScreen + +private func wrappedMinuteRange(range: Range, dayIndexOffset: Int = 0) -> IndexSet { + let mappedRange = (range.lowerBound + dayIndexOffset * 24 * 60) ..< (range.upperBound + dayIndexOffset * 24 * 60) + + var result = IndexSet() + if mappedRange.upperBound > 7 * 24 * 60 { + if mappedRange.lowerBound < 7 * 24 * 60 { + result.insert(integersIn: mappedRange.lowerBound ..< 7 * 24 * 60) + } + result.insert(integersIn: 0 ..< (mappedRange.upperBound - 7 * 24 * 60)) + } else { + result.insert(integersIn: mappedRange) + } + return result +} + +private func getDayRanges(days: [BusinessHoursSetupScreenComponent.Day], index: Int) -> [BusinessHoursSetupScreenComponent.WorkingHourRange] { + let day = days[index] + if let ranges = day.ranges { + if ranges.isEmpty { + return [BusinessHoursSetupScreenComponent.WorkingHourRange(id: 0, startMinute: 0, endMinute: 24 * 60)] + } else { + return ranges + } + } else { + return [] + } +} + +final class BusinessHoursSetupScreenComponent: Component { + typealias EnvironmentType = ViewControllerComponentContainer.Environment + + let context: AccountContext + let initialValue: TelegramBusinessHours? + let completion: (TelegramBusinessHours?) -> Void + + init( + context: AccountContext, + initialValue: TelegramBusinessHours?, + completion: @escaping (TelegramBusinessHours?) -> Void + ) { + self.context = context + self.initialValue = initialValue + self.completion = completion + } + + static func ==(lhs: BusinessHoursSetupScreenComponent, rhs: BusinessHoursSetupScreenComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.initialValue != rhs.initialValue { + return false + } + + return true + } + + private final class ScrollView: UIScrollView { + override func touchesShouldCancel(in view: UIView) -> Bool { + return true + } + } + + struct WorkingHourRange: Equatable { + var id: Int + var startMinute: Int + var endMinute: Int + + init(id: Int, startMinute: Int, endMinute: Int) { + self.id = id + self.startMinute = startMinute + self.endMinute = endMinute + } + } + + struct DayRangeIndex: Hashable { + var day: Int + var id: Int + + init(day: Int, id: Int) { + self.day = day + self.id = id + } + } + + struct Day: Equatable { + var ranges: [WorkingHourRange]? + + init(ranges: [WorkingHourRange]?) { + self.ranges = ranges + } + } + + struct DaysState: Equatable { + enum ValidationError: Error { + case intersectingRanges + } + + var timezoneId: String + private(set) var days: [Day] + private(set) var intersectingRanges = Set() + + init(timezoneId: String, days: [Day]) { + self.timezoneId = timezoneId + self.days = days + + self.validate() + } + + init(businessHours: TelegramBusinessHours) { + self.timezoneId = businessHours.timezoneId + + self.days = businessHours.splitIntoWeekDays().map { day in + switch day { + case .closed: + return Day(ranges: nil) + case .open: + return Day(ranges: []) + case let .intervals(intervals): + var nextIntervalId = 0 + return Day(ranges: intervals.map { interval in + let intervalId = nextIntervalId + nextIntervalId += 1 + return WorkingHourRange(id: intervalId, startMinute: interval.startMinute, endMinute: interval.endMinute) + }) + } + } + + if let value = try? self.asBusinessHours() { + if value != businessHours { + assertionFailure("Inconsistent representation") + } + } + + self.validate() + } + + mutating func validate() { + self.intersectingRanges.removeAll() + + for dayIndex in 0 ..< self.days.count { + var otherDaysMinutes = IndexSet() + inner: for otherDayIndex in 0 ..< self.days.count { + if dayIndex == otherDayIndex { + continue inner + } + for range in getDayRanges(days: self.days, index: otherDayIndex) { + otherDaysMinutes.formUnion(wrappedMinuteRange(range: range.startMinute ..< range.endMinute, dayIndexOffset: otherDayIndex)) + } + } + + let dayRanges = getDayRanges(days: self.days, index: dayIndex) + for i in 0 ..< dayRanges.count { + var currentDayOtherMinutes = IndexSet() + inner: for j in 0 ..< dayRanges.count { + if i == j { + continue inner + } + currentDayOtherMinutes.formUnion(wrappedMinuteRange(range: dayRanges[j].startMinute ..< dayRanges[j].endMinute, dayIndexOffset: dayIndex)) + } + + let currentDayIndices = wrappedMinuteRange(range: dayRanges[i].startMinute ..< dayRanges[i].endMinute, dayIndexOffset: dayIndex) + if !otherDaysMinutes.intersection(currentDayIndices).isEmpty || !currentDayOtherMinutes.intersection(currentDayIndices).isEmpty { + self.intersectingRanges.insert(DayRangeIndex(day: dayIndex, id: dayRanges[i].id)) + } + } + } + } + + mutating func update(days: [Day]) { + self.days = days + self.validate() + } + + func asBusinessHours() throws -> TelegramBusinessHours { + var mappedIntervals: [TelegramBusinessHours.WorkingTimeInterval] = [] + + var filledMinutes = IndexSet() + for i in 0 ..< self.days.count { + let dayStartMinute = i * 24 * 60 + guard var effectiveRanges = self.days[i].ranges else { + continue + } + if effectiveRanges.isEmpty { + effectiveRanges = [WorkingHourRange(id: 0, startMinute: 0, endMinute: 24 * 60)] + } + for range in effectiveRanges { + let minuteRange: Range = (dayStartMinute + range.startMinute) ..< (dayStartMinute + range.endMinute) + + let wrappedMinutes = wrappedMinuteRange(range: minuteRange) + + if !filledMinutes.intersection(wrappedMinutes).isEmpty { + throw ValidationError.intersectingRanges + } + filledMinutes.formUnion(wrappedMinutes) + mappedIntervals.append(TelegramBusinessHours.WorkingTimeInterval(startMinute: minuteRange.lowerBound, endMinute: minuteRange.upperBound)) + } + } + + var mergedIntervals: [TelegramBusinessHours.WorkingTimeInterval] = [] + for interval in mappedIntervals { + if mergedIntervals.isEmpty { + mergedIntervals.append(interval) + } else { + if mergedIntervals[mergedIntervals.count - 1].endMinute >= interval.startMinute { + mergedIntervals[mergedIntervals.count - 1] = TelegramBusinessHours.WorkingTimeInterval(startMinute: mergedIntervals[mergedIntervals.count - 1].startMinute, endMinute: interval.endMinute) + } else { + mergedIntervals.append(interval) + } + } + } + + return TelegramBusinessHours(timezoneId: self.timezoneId, weeklyTimeIntervals: mergedIntervals) + } + } + + final class View: UIView, UIScrollViewDelegate { + private let topOverscrollLayer = SimpleLayer() + private let scrollView: ScrollView + + private let navigationTitle = ComponentView() + private let icon = ComponentView() + private let subtitle = ComponentView() + private let generalSection = ComponentView() + private let daysSection = ComponentView() + private let timezoneSection = ComponentView() + + private var ignoreScrolling: Bool = false + private var isUpdating: Bool = false + + private var component: BusinessHoursSetupScreenComponent? + private(set) weak var state: EmptyComponentState? + private var environment: EnvironmentType? + + private var showHours: Bool = false + private var daysState = DaysState(timezoneId: "", days: []) + + private var timeZoneList: TimeZoneList? + private var timezonesDisposable: Disposable? + private var keepTimezonesUpdatedDisposable: Disposable? + + override init(frame: CGRect) { + self.scrollView = ScrollView() + self.scrollView.showsVerticalScrollIndicator = true + self.scrollView.showsHorizontalScrollIndicator = false + self.scrollView.scrollsToTop = false + self.scrollView.delaysContentTouches = false + self.scrollView.canCancelContentTouches = true + self.scrollView.contentInsetAdjustmentBehavior = .never + if #available(iOS 13.0, *) { + self.scrollView.automaticallyAdjustsScrollIndicatorInsets = false + } + self.scrollView.alwaysBounceVertical = true + + super.init(frame: frame) + + self.scrollView.delegate = self + self.addSubview(self.scrollView) + + self.scrollView.layer.addSublayer(self.topOverscrollLayer) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + self.timezonesDisposable?.dispose() + self.keepTimezonesUpdatedDisposable?.dispose() + } + + func scrollToTop() { + self.scrollView.setContentOffset(CGPoint(), animated: true) + } + + func attemptNavigation(complete: @escaping () -> Void) -> Bool { + guard let component = self.component, let environment = self.environment else { + return true + } + + if self.showHours { + do { + let businessHours = try self.daysState.asBusinessHours() + let _ = component.context.engine.accountData.updateAccountBusinessHours(businessHours: businessHours).startStandalone() + return true + } catch _ { + let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } + self.environment?.controller()?.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: nil, text: environment.strings.BusinessHoursSetup_ErrorIntersectingDays_Text, actions: [ + TextAlertAction(type: .genericAction, title: environment.strings.Common_Cancel, action: { + }), + TextAlertAction(type: .defaultAction, title: environment.strings.BusinessHoursSetup_ErrorIntersectingDays_ResetAction, action: { [weak self] in + guard let self else { + return + } + let _ = self + complete() + }) + ]), in: .window(.root)) + + return false + } + } else { + if component.initialValue != nil { + let _ = component.context.engine.accountData.updateAccountBusinessHours(businessHours: nil).startStandalone() + return true + } else { + return true + } + } + } + + func scrollViewDidScroll(_ scrollView: UIScrollView) { + if !self.ignoreScrolling { + self.updateScrolling(transition: .immediate) + } + } + + var scrolledUp = true + private func updateScrolling(transition: Transition) { + let navigationRevealOffsetY: CGFloat = 0.0 + + let navigationAlphaDistance: CGFloat = 16.0 + let navigationAlpha: CGFloat = max(0.0, min(1.0, (self.scrollView.contentOffset.y - navigationRevealOffsetY) / navigationAlphaDistance)) + if let controller = self.environment?.controller(), let navigationBar = controller.navigationBar { + transition.setAlpha(layer: navigationBar.backgroundNode.layer, alpha: navigationAlpha) + transition.setAlpha(layer: navigationBar.stripeNode.layer, alpha: navigationAlpha) + } + + var scrolledUp = false + if navigationAlpha < 0.5 { + scrolledUp = true + } else if navigationAlpha > 0.5 { + scrolledUp = false + } + + if self.scrolledUp != scrolledUp { + self.scrolledUp = scrolledUp + if !self.isUpdating { + self.state?.updated() + } + } + + if let navigationTitleView = self.navigationTitle.view { + transition.setAlpha(view: navigationTitleView, alpha: 1.0) + } + } + + func update(component: BusinessHoursSetupScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + self.isUpdating = true + defer { + self.isUpdating = false + } + + if self.component == nil { + if let initialValue = component.initialValue { + self.showHours = true + self.daysState = DaysState(businessHours: initialValue) + } else { + self.showHours = false + self.daysState.timezoneId = TimeZone.current.identifier + self.daysState.update(days: (0 ..< 7).map { _ in + return Day(ranges: []) + }) + } + + self.timezonesDisposable = (component.context.engine.accountData.cachedTimeZoneList() + |> deliverOnMainQueue).start(next: { [weak self] timeZoneList in + guard let self else { + return + } + self.timeZoneList = timeZoneList + self.state?.updated(transition: .immediate) + }) + self.keepTimezonesUpdatedDisposable = component.context.engine.accountData.keepCachedTimeZoneListUpdated().startStrict() + } + + let environment = environment[EnvironmentType.self].value + let themeUpdated = self.environment?.theme !== environment.theme + self.environment = environment + + self.component = component + self.state = state + + if themeUpdated { + self.backgroundColor = environment.theme.list.blocksBackgroundColor + } + + let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } + + let navigationTitleSize = self.navigationTitle.update( + transition: transition, + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: environment.strings.BusinessHoursSetup_Title, font: Font.semibold(17.0), textColor: environment.theme.rootController.navigationBar.primaryTextColor)), + horizontalAlignment: .center + )), + environment: {}, + containerSize: CGSize(width: availableSize.width, height: 100.0) + ) + let navigationTitleFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - navigationTitleSize.width) / 2.0), y: environment.statusBarHeight + floor((environment.navigationHeight - environment.statusBarHeight - navigationTitleSize.height) / 2.0)), size: navigationTitleSize) + if let navigationTitleView = self.navigationTitle.view { + if navigationTitleView.superview == nil { + if let controller = self.environment?.controller(), let navigationBar = controller.navigationBar { + navigationBar.view.addSubview(navigationTitleView) + } + } + transition.setFrame(view: navigationTitleView, frame: navigationTitleFrame) + } + + let bottomContentInset: CGFloat = 24.0 + let sideInset: CGFloat = 16.0 + environment.safeInsets.left + let sectionSpacing: CGFloat = 30.0 + + let _ = bottomContentInset + let _ = sectionSpacing + + var contentHeight: CGFloat = 0.0 + + contentHeight += environment.navigationHeight + + let iconSize = self.icon.update( + transition: .immediate, + component: AnyComponent(LottieComponent( + content: LottieComponent.AppBundleContent(name: "BusinessHoursEmoji"), + loop: false + )), + environment: {}, + containerSize: CGSize(width: 100.0, height: 100.0) + ) + let iconFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - iconSize.width) * 0.5), y: contentHeight + 10.0), size: iconSize) + if let iconView = self.icon.view as? LottieComponent.View { + if iconView.superview == nil { + self.scrollView.addSubview(iconView) + iconView.playOnce() + } + transition.setPosition(view: iconView, position: iconFrame.center) + iconView.bounds = CGRect(origin: CGPoint(), size: iconFrame.size) + } + + contentHeight += 126.0 + + let subtitleString = NSMutableAttributedString(attributedString: parseMarkdownIntoAttributedString(environment.strings.BusinessHoursSetup_Text, attributes: MarkdownAttributes( + body: MarkdownAttributeSet(font: Font.regular(15.0), textColor: environment.theme.list.freeTextColor), + bold: MarkdownAttributeSet(font: Font.semibold(15.0), textColor: environment.theme.list.freeTextColor), + link: MarkdownAttributeSet(font: Font.regular(15.0), textColor: environment.theme.list.itemAccentColor), + linkAttribute: { attributes in + return ("URL", "") + }), textAlignment: .center + )) + + let subtitleSize = self.subtitle.update( + transition: .immediate, + component: AnyComponent(BalancedTextComponent( + text: .plain(subtitleString), + horizontalAlignment: .center, + maximumNumberOfLines: 0, + lineSpacing: 0.25, + highlightColor: environment.theme.list.itemAccentColor.withMultipliedAlpha(0.1), + highlightAction: { attributes in + if let _ = attributes[NSAttributedString.Key(rawValue: "URL")] { + return NSAttributedString.Key(rawValue: "URL") + } else { + return nil + } + }, + tapAction: { [weak self] _, _ in + guard let self, let component = self.component else { + return + } + let _ = component + } + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 1000.0) + ) + let subtitleFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - subtitleSize.width) * 0.5), y: contentHeight), size: subtitleSize) + if let subtitleView = self.subtitle.view { + if subtitleView.superview == nil { + self.scrollView.addSubview(subtitleView) + } + transition.setPosition(view: subtitleView, position: subtitleFrame.center) + subtitleView.bounds = CGRect(origin: CGPoint(), size: subtitleFrame.size) + } + contentHeight += subtitleSize.height + contentHeight += 27.0 + + let generalSectionSize = self.generalSection.update( + transition: transition, + component: AnyComponent(ListSectionComponent( + theme: environment.theme, + header: nil, + footer: nil, + items: [ + AnyComponentWithIdentity(id: 0, component: AnyComponent(ListActionItemComponent( + theme: environment.theme, + title: AnyComponent(VStack([ + AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: environment.strings.BusinessHoursSetup_MainToggle, + font: Font.regular(presentationData.listsFontSize.baseDisplaySize), + textColor: environment.theme.list.itemPrimaryTextColor + )), + maximumNumberOfLines: 1 + ))), + ], alignment: .left, spacing: 2.0)), + accessory: .toggle(ListActionItemComponent.Toggle(style: .regular, isOn: self.showHours, action: { [weak self] _ in + guard let self else { + return + } + self.showHours = !self.showHours + self.state?.updated(transition: .spring(duration: 0.4)) + })), + action: nil + ))) + ] + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 10000.0) + ) + let generalSectionFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: generalSectionSize) + if let generalSectionView = self.generalSection.view { + if generalSectionView.superview == nil { + self.scrollView.addSubview(generalSectionView) + } + transition.setFrame(view: generalSectionView, frame: generalSectionFrame) + } + contentHeight += generalSectionSize.height + contentHeight += sectionSpacing + + var daysContentHeight: CGFloat = 0.0 + + var daysSectionItems: [AnyComponentWithIdentity] = [] + for day in self.daysState.days { + let dayIndex = daysSectionItems.count + + let title: String + switch dayIndex { + case 0: + title = environment.strings.Weekday_Monday + case 1: + title = environment.strings.Weekday_Tuesday + case 2: + title = environment.strings.Weekday_Wednesday + case 3: + title = environment.strings.Weekday_Thursday + case 4: + title = environment.strings.Weekday_Friday + case 5: + title = environment.strings.Weekday_Saturday + case 6: + title = environment.strings.Weekday_Sunday + default: + title = " " + } + + let subtitle = NSMutableAttributedString() + + var invalidIndices: [Int] = [] + let effectiveDayRanges = getDayRanges(days: self.daysState.days, index: dayIndex) + for range in effectiveDayRanges { + if self.daysState.intersectingRanges.contains(DayRangeIndex(day: dayIndex, id: range.id)) { + invalidIndices.append(range.id) + } + } + + let subtitleFont = Font.regular(floor(presentationData.listsFontSize.baseDisplaySize * 15.0 / 17.0)) + + if let ranges = self.daysState.days[dayIndex].ranges { + if ranges.isEmpty { + subtitle.append(NSAttributedString(string: environment.strings.BusinessHoursSetup_DayOpen24h, font: subtitleFont, textColor: invalidIndices.contains(0) ? environment.theme.list.itemDestructiveColor : environment.theme.list.itemAccentColor)) + } else { + for i in 0 ..< ranges.count { + let range = ranges[i] + + let startHours = clipMinutes(range.startMinute) / 60 + let startMinutes = clipMinutes(range.startMinute) % 60 + let startText = stringForShortTimestamp(hours: Int32(startHours), minutes: Int32(startMinutes), dateTimeFormat: presentationData.dateTimeFormat) + let endHours = clipMinutes(range.endMinute) / 60 + let endMinutes = clipMinutes(range.endMinute) % 60 + let endText = stringForShortTimestamp(hours: Int32(endHours), minutes: Int32(endMinutes), dateTimeFormat: presentationData.dateTimeFormat) + + var rangeString = "\(startText)\u{00a0}- \(endText)" + if i != ranges.count - 1 { + rangeString.append(", ") + } + + subtitle.append(NSAttributedString(string: rangeString, font: subtitleFont, textColor: invalidIndices.contains(range.id) ? environment.theme.list.itemDestructiveColor : environment.theme.list.itemAccentColor)) + } + } + } else { + subtitle.append(NSAttributedString(string: environment.strings.BusinessHoursSetup_DayClosed, font: subtitleFont, textColor: environment.theme.list.itemAccentColor)) + } + + daysSectionItems.append(AnyComponentWithIdentity(id: dayIndex, component: AnyComponent(ListActionItemComponent( + theme: environment.theme, + title: AnyComponent(VStack([ + AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: title, + font: Font.regular(presentationData.listsFontSize.baseDisplaySize), + textColor: environment.theme.list.itemPrimaryTextColor + )), + maximumNumberOfLines: 1 + ))), + AnyComponentWithIdentity(id: AnyHashable(1), component: AnyComponent(MultilineTextComponent( + text: .plain(subtitle), + maximumNumberOfLines: 20 + ))) + ], alignment: .left, spacing: 3.0)), + contentInsets: UIEdgeInsets(top: 9.0, left: 0.0, bottom: 10.0, right: 0.0), + accessory: .toggle(ListActionItemComponent.Toggle(style: .regular, isOn: day.ranges != nil, action: { [weak self] _ in + guard let self else { + return + } + if dayIndex < self.daysState.days.count { + var days = self.daysState.days + if days[dayIndex].ranges == nil { + days[dayIndex].ranges = [] + } else { + days[dayIndex].ranges = nil + } + self.daysState.update(days: days) + } + self.state?.updated(transition: .immediate) + })), + action: { [weak self] _ in + guard let self, let component = self.component else { + return + } + self.environment?.controller()?.push(BusinessDaySetupScreen( + context: component.context, + dayIndex: dayIndex, + day: self.daysState.days[dayIndex], + updateDay: { [weak self] day in + guard let self else { + return + } + if self.daysState.days[dayIndex] != day { + var days = self.daysState.days + days[dayIndex] = day + self.daysState.update(days: days) + self.state?.updated(transition: .immediate) + } + } + )) + } + )))) + } + let daysSectionSize = self.daysSection.update( + transition: transition, + component: AnyComponent(ListSectionComponent( + theme: environment.theme, + header: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: environment.strings.BusinessHoursSetup_DaysSectionTitle, + font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize), + textColor: environment.theme.list.freeTextColor + )), + maximumNumberOfLines: 0 + )), + footer: nil, + items: daysSectionItems + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 10000.0) + ) + let daysSectionFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight + daysContentHeight), size: daysSectionSize) + if let daysSectionView = self.daysSection.view { + if daysSectionView.superview == nil { + daysSectionView.layer.allowsGroupOpacity = true + self.scrollView.addSubview(daysSectionView) + } + transition.setFrame(view: daysSectionView, frame: daysSectionFrame) + + let alphaTransition = transition.animation.isImmediate ? transition : .easeInOut(duration: 0.25) + alphaTransition.setAlpha(view: daysSectionView, alpha: self.showHours ? 1.0 : 0.0) + } + daysContentHeight += daysSectionSize.height + daysContentHeight += sectionSpacing + + let timezoneValueText: String + if let timeZoneList = self.timeZoneList { + if let item = timeZoneList.items.first(where: { $0.id == self.daysState.timezoneId }) { + timezoneValueText = item.title + } else { + timezoneValueText = TimeZone(identifier: self.daysState.timezoneId)?.localizedName(for: .shortStandard, locale: Locale.current) ?? " " + } + } else { + timezoneValueText = "..." + } + + let timezoneSectionSize = self.timezoneSection.update( + transition: transition, + component: AnyComponent(ListSectionComponent( + theme: environment.theme, + header: nil, + footer: nil, + items: [ + AnyComponentWithIdentity(id: 0, component: AnyComponent(ListActionItemComponent( + theme: environment.theme, + title: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: environment.strings.BusinessHoursSetup_TimeZone, + font: Font.regular(presentationData.listsFontSize.baseDisplaySize), + textColor: environment.theme.list.itemPrimaryTextColor + )), + maximumNumberOfLines: 1 + )), + icon: ListActionItemComponent.Icon(component: AnyComponentWithIdentity(id: 0, component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: timezoneValueText, + font: Font.regular(presentationData.listsFontSize.baseDisplaySize), + textColor: environment.theme.list.itemSecondaryTextColor + )), + maximumNumberOfLines: 1 + )))), + accessory: .arrow, + action: { [weak self] _ in + guard let self, let component = self.component else { + return + } + var completed: ((String) -> Void)? + let controller = TimezoneSelectionScreen(context: component.context, completed: { timezoneId in + completed?(timezoneId) + }) + controller.navigationPresentation = .modal + self.environment?.controller()?.push(controller) + completed = { [weak self, weak controller] timezoneId in + guard let self else { + controller?.dismiss() + return + } + self.daysState.timezoneId = timezoneId + self.state?.updated(transition: .immediate) + controller?.dismiss() + } + } + ))) + ] + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 10000.0) + ) + let timezoneSectionFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight + daysContentHeight), size: timezoneSectionSize) + if let timezoneSectionView = self.timezoneSection.view { + if timezoneSectionView.superview == nil { + self.scrollView.addSubview(timezoneSectionView) + } + transition.setFrame(view: timezoneSectionView, frame: timezoneSectionFrame) + let alphaTransition = transition.animation.isImmediate ? transition : .easeInOut(duration: 0.25) + alphaTransition.setAlpha(view: timezoneSectionView, alpha: self.showHours ? 1.0 : 0.0) + } + daysContentHeight += timezoneSectionSize.height + + if self.showHours { + contentHeight += daysContentHeight + } + + contentHeight += bottomContentInset + contentHeight += environment.safeInsets.bottom + + self.ignoreScrolling = true + let previousBounds = self.scrollView.bounds + + let contentSize = CGSize(width: availableSize.width, height: contentHeight) + if self.scrollView.frame != CGRect(origin: CGPoint(), size: availableSize) { + self.scrollView.frame = CGRect(origin: CGPoint(), size: availableSize) + } + if self.scrollView.contentSize != contentSize { + self.scrollView.contentSize = contentSize + } + let scrollInsets = UIEdgeInsets(top: environment.navigationHeight, left: 0.0, bottom: 0.0, right: 0.0) + if self.scrollView.scrollIndicatorInsets != scrollInsets { + self.scrollView.scrollIndicatorInsets = scrollInsets + } + self.ignoreScrolling = false + + if !previousBounds.isEmpty, !transition.animation.isImmediate { + let bounds = self.scrollView.bounds + if bounds.maxY != previousBounds.maxY { + let offsetY = previousBounds.maxY - bounds.maxY + transition.animateBoundsOrigin(view: self.scrollView, from: CGPoint(x: 0.0, y: offsetY), to: CGPoint(), additive: true) + } + } + + self.topOverscrollLayer.frame = CGRect(origin: CGPoint(x: 0.0, y: -3000.0), size: CGSize(width: availableSize.width, height: 3000.0)) + + self.updateScrolling(transition: transition) + + return availableSize + } + } + + func makeView() -> View { + return View() + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} + +public final class BusinessHoursSetupScreen: ViewControllerComponentContainer { + private let context: AccountContext + + public init(context: AccountContext, initialValue: TelegramBusinessHours?, completion: @escaping (TelegramBusinessHours?) -> Void) { + self.context = context + + super.init(context: context, component: BusinessHoursSetupScreenComponent( + context: context, + initialValue: initialValue, + completion: completion + ), navigationBarAppearance: .default, theme: .default, updatedPresentationData: nil) + + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + self.title = "" + self.navigationItem.backBarButtonItem = UIBarButtonItem(title: presentationData.strings.Common_Back, style: .plain, target: nil, action: nil) + + self.scrollToTop = { [weak self] in + guard let self, let componentView = self.node.hostView.componentView as? BusinessHoursSetupScreenComponent.View else { + return + } + componentView.scrollToTop() + } + + self.attemptNavigation = { [weak self] complete in + guard let self, let componentView = self.node.hostView.componentView as? BusinessHoursSetupScreenComponent.View else { + return true + } + + return componentView.attemptNavigation(complete: complete) + } + } + + required public init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + } + + @objc private func cancelPressed() { + self.dismiss() + } + + override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { + super.containerLayoutUpdated(layout, transition: transition) + } +} diff --git a/submodules/TelegramUI/Components/Settings/BusinessSetupScreen/BUILD b/submodules/TelegramUI/Components/Settings/BusinessLocationSetupScreen/BUILD similarity index 66% rename from submodules/TelegramUI/Components/Settings/BusinessSetupScreen/BUILD rename to submodules/TelegramUI/Components/Settings/BusinessLocationSetupScreen/BUILD index 6866f506c47..83cfb9c9c7c 100644 --- a/submodules/TelegramUI/Components/Settings/BusinessSetupScreen/BUILD +++ b/submodules/TelegramUI/Components/Settings/BusinessLocationSetupScreen/BUILD @@ -1,8 +1,8 @@ load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") swift_library( - name = "BusinessSetupScreen", - module_name = "BusinessSetupScreen", + name = "BusinessLocationSetupScreen", + module_name = "BusinessLocationSetupScreen", srcs = glob([ "Sources/**/*.swift", ]), @@ -10,24 +10,28 @@ swift_library( "-warnings-as-errors", ], deps = [ - "//submodules/AsyncDisplayKit", "//submodules/Display", "//submodules/Postbox", "//submodules/TelegramCore", "//submodules/SSignalKit/SwiftSignalKit", "//submodules/TelegramPresentationData", + "//submodules/TelegramUIPreferences", "//submodules/AccountContext", "//submodules/PresentationDataUtils", + "//submodules/Markdown", "//submodules/ComponentFlow", "//submodules/Components/ViewControllerComponent", "//submodules/Components/BundleIconComponent", - "//submodules/TelegramUI/Components/AnimatedTextComponent", "//submodules/Components/MultilineTextComponent", "//submodules/Components/BalancedTextComponent", - "//submodules/TelegramUI/Components/ButtonComponent", - "//submodules/TelegramUI/Components/BackButtonComponent", "//submodules/TelegramUI/Components/ListSectionComponent", "//submodules/TelegramUI/Components/ListActionItemComponent", + "//submodules/TelegramUI/Components/ListMultilineTextFieldItemComponent", + "//submodules/TelegramUI/Components/LottieComponent", + "//submodules/TelegramUI/Components/PlainButtonComponent", + "//submodules/LocationUI", + "//submodules/AppBundle", + "//submodules/Geocoding", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/Settings/BusinessLocationSetupScreen/Sources/BusinessLocationSetupScreen.swift b/submodules/TelegramUI/Components/Settings/BusinessLocationSetupScreen/Sources/BusinessLocationSetupScreen.swift new file mode 100644 index 00000000000..95f51e3f2c7 --- /dev/null +++ b/submodules/TelegramUI/Components/Settings/BusinessLocationSetupScreen/Sources/BusinessLocationSetupScreen.swift @@ -0,0 +1,682 @@ +import Foundation +import UIKit +import Display +import AsyncDisplayKit +import SwiftSignalKit +import Postbox +import TelegramCore +import TelegramPresentationData +import TelegramUIPreferences +import PresentationDataUtils +import AccountContext +import ComponentFlow +import ViewControllerComponent +import MultilineTextComponent +import BalancedTextComponent +import ListSectionComponent +import ListActionItemComponent +import ListMultilineTextFieldItemComponent +import BundleIconComponent +import LottieComponent +import Markdown +import LocationUI +import CoreLocation +import Geocoding + +final class BusinessLocationSetupScreenComponent: Component { + typealias EnvironmentType = ViewControllerComponentContainer.Environment + + let context: AccountContext + let initialValue: TelegramBusinessLocation? + let completion: (TelegramBusinessLocation?) -> Void + + init( + context: AccountContext, + initialValue: TelegramBusinessLocation?, + completion: @escaping (TelegramBusinessLocation?) -> Void + ) { + self.context = context + self.initialValue = initialValue + self.completion = completion + } + + static func ==(lhs: BusinessLocationSetupScreenComponent, rhs: BusinessLocationSetupScreenComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.initialValue != rhs.initialValue { + return false + } + + return true + } + + private final class ScrollView: UIScrollView { + override func touchesShouldCancel(in view: UIView) -> Bool { + return true + } + } + + final class View: UIView, UIScrollViewDelegate { + private let topOverscrollLayer = SimpleLayer() + private let scrollView: ScrollView + + private let navigationTitle = ComponentView() + private let icon = ComponentView() + private let subtitle = ComponentView() + private let addressSection = ComponentView() + private let mapSection = ComponentView() + private let deleteSection = ComponentView() + + private var isUpdating: Bool = false + + private var component: BusinessLocationSetupScreenComponent? + private(set) weak var state: EmptyComponentState? + private var environment: EnvironmentType? + + private let addressTextInputState = ListMultilineTextFieldItemComponent.ExternalState() + private let textFieldTag = NSObject() + private var resetAddressText: String? + + private var isLoadingGeocodedAddress: Bool = false + private var geocodeDisposable: Disposable? + + private var mapCoordinates: TelegramBusinessLocation.Coordinates? + private var mapCoordinatesManuallySet: Bool = false + + private var applyButtonItem: UIBarButtonItem? + + override init(frame: CGRect) { + self.scrollView = ScrollView() + self.scrollView.showsVerticalScrollIndicator = true + self.scrollView.showsHorizontalScrollIndicator = false + self.scrollView.scrollsToTop = false + self.scrollView.delaysContentTouches = false + self.scrollView.canCancelContentTouches = true + self.scrollView.contentInsetAdjustmentBehavior = .never + if #available(iOS 13.0, *) { + self.scrollView.automaticallyAdjustsScrollIndicatorInsets = false + } + self.scrollView.alwaysBounceVertical = true + + super.init(frame: frame) + + self.scrollView.delegate = self + self.addSubview(self.scrollView) + + self.scrollView.layer.addSublayer(self.topOverscrollLayer) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + self.geocodeDisposable?.dispose() + } + + func scrollToTop() { + self.scrollView.setContentOffset(CGPoint(), animated: true) + } + + func attemptNavigation(complete: @escaping () -> Void) -> Bool { + guard let component = self.component, let environment = self.environment else { + return true + } + + let businessLocation = self.currentBusinessLocation() + + if businessLocation != component.initialValue { + let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } + self.environment?.controller()?.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: nil, text: environment.strings.BusinessLocationSetup_AlertUnsavedChanges_Text, actions: [ + TextAlertAction(type: .genericAction, title: environment.strings.Common_Cancel, action: { + }), + TextAlertAction(type: .destructiveAction, title: environment.strings.BusinessLocationSetup_AlertUnsavedChanges_ResetAction, action: { + complete() + }) + ]), in: .window(.root)) + + return false + } + + return true + } + + func scrollViewDidScroll(_ scrollView: UIScrollView) { + self.updateScrolling(transition: .immediate) + } + + var scrolledUp = true + private func updateScrolling(transition: Transition) { + let navigationRevealOffsetY: CGFloat = 0.0 + + let navigationAlphaDistance: CGFloat = 16.0 + let navigationAlpha: CGFloat = max(0.0, min(1.0, (self.scrollView.contentOffset.y - navigationRevealOffsetY) / navigationAlphaDistance)) + if let controller = self.environment?.controller(), let navigationBar = controller.navigationBar { + transition.setAlpha(layer: navigationBar.backgroundNode.layer, alpha: navigationAlpha) + transition.setAlpha(layer: navigationBar.stripeNode.layer, alpha: navigationAlpha) + } + + var scrolledUp = false + if navigationAlpha < 0.5 { + scrolledUp = true + } else if navigationAlpha > 0.5 { + scrolledUp = false + } + + if self.scrolledUp != scrolledUp { + self.scrolledUp = scrolledUp + if !self.isUpdating { + self.state?.updated() + } + } + + if let navigationTitleView = self.navigationTitle.view { + transition.setAlpha(view: navigationTitleView, alpha: 1.0) + } + } + + private func currentBusinessLocation() -> TelegramBusinessLocation? { + var address = "" + if let textView = self.addressSection.findTaggedView(tag: self.textFieldTag) as? ListMultilineTextFieldItemComponent.View { + address = textView.currentText + } + + var businessLocation: TelegramBusinessLocation? + if !address.isEmpty || self.mapCoordinates != nil { + businessLocation = TelegramBusinessLocation(address: address, coordinates: self.mapCoordinates) + } + return businessLocation + } + + private func openLocationPicker() { + var initialLocation: CLLocationCoordinate2D? + var initialGeocodedLocation: String? + if let mapCoordinates = self.mapCoordinates { + initialLocation = CLLocationCoordinate2D(latitude: mapCoordinates.latitude, longitude: mapCoordinates.longitude) + } else if let textView = self.addressSection.findTaggedView(tag: self.textFieldTag) as? ListMultilineTextFieldItemComponent.View, textView.currentText.count >= 2 { + initialGeocodedLocation = textView.currentText + } + + if let initialGeocodedLocation { + self.isLoadingGeocodedAddress = true + self.state?.updated(transition: .immediate) + + self.geocodeDisposable?.dispose() + self.geocodeDisposable = (geocodeLocation(address: initialGeocodedLocation) + |> deliverOnMainQueue).startStrict(next: { [weak self] venues in + guard let self else { + return + } + self.isLoadingGeocodedAddress = false + self.state?.updated(transition: .immediate) + self.presentLocationPicker(initialLocation: venues?.first?.location?.coordinate) + }) + } else { + self.presentLocationPicker(initialLocation: initialLocation) + } + } + + private func presentLocationPicker(initialLocation: CLLocationCoordinate2D?) { + guard let component = self.component else { + return + } + let controller = LocationPickerController(context: component.context, updatedPresentationData: nil, mode: .pick, initialLocation: initialLocation, completion: { [weak self] location, _, _, address, _ in + guard let self else { + return + } + + self.mapCoordinates = TelegramBusinessLocation.Coordinates(latitude: location.latitude, longitude: location.longitude) + self.mapCoordinatesManuallySet = true + if let textView = self.addressSection.findTaggedView(tag: self.textFieldTag) as? ListMultilineTextFieldItemComponent.View, textView.currentText.isEmpty { + self.resetAddressText = address + } + + self.state?.updated(transition: .immediate) + }) + self.environment?.controller()?.push(controller) + } + + @objc private func savePressed() { + guard let component = self.component, let environment = self.environment else { + return + } + + var address = "" + if let textView = self.addressSection.findTaggedView(tag: self.textFieldTag) as? ListMultilineTextFieldItemComponent.View { + address = textView.currentText + } + + let businessLocation = self.currentBusinessLocation() + + if businessLocation != nil && address.isEmpty { + let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } + self.environment?.controller()?.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: nil, text: environment.strings.BusinessLocationSetup_ErrorAddressEmpty_Text, actions: [ + TextAlertAction(type: .genericAction, title: environment.strings.Common_OK, action: { + }) + ]), in: .window(.root)) + + return + } + + let _ = component.context.engine.accountData.updateAccountBusinessLocation(businessLocation: businessLocation).startStandalone() + environment.controller()?.dismiss() + } + + func update(component: BusinessLocationSetupScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + self.isUpdating = true + defer { + self.isUpdating = false + } + + if self.component == nil { + if let initialValue = component.initialValue { + self.mapCoordinates = initialValue.coordinates + if self.mapCoordinates != nil { + self.mapCoordinatesManuallySet = true + } + self.resetAddressText = initialValue.address + } + } + + let environment = environment[EnvironmentType.self].value + let themeUpdated = self.environment?.theme !== environment.theme + self.environment = environment + + self.component = component + self.state = state + + let alphaTransition: Transition + if !transition.animation.isImmediate { + alphaTransition = .easeInOut(duration: 0.25) + } else { + alphaTransition = .immediate + } + + if themeUpdated { + self.backgroundColor = environment.theme.list.blocksBackgroundColor + } + + let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } + + let navigationTitleSize = self.navigationTitle.update( + transition: transition, + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: environment.strings.BusinessLocationSetup_Title, font: Font.semibold(17.0), textColor: environment.theme.rootController.navigationBar.primaryTextColor)), + horizontalAlignment: .center + )), + environment: {}, + containerSize: CGSize(width: availableSize.width, height: 100.0) + ) + let navigationTitleFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - navigationTitleSize.width) / 2.0), y: environment.statusBarHeight + floor((environment.navigationHeight - environment.statusBarHeight - navigationTitleSize.height) / 2.0)), size: navigationTitleSize) + if let navigationTitleView = self.navigationTitle.view { + if navigationTitleView.superview == nil { + if let controller = self.environment?.controller(), let navigationBar = controller.navigationBar { + navigationBar.view.addSubview(navigationTitleView) + } + } + transition.setFrame(view: navigationTitleView, frame: navigationTitleFrame) + } + + let bottomContentInset: CGFloat = 24.0 + let sideInset: CGFloat = 16.0 + environment.safeInsets.left + let sectionSpacing: CGFloat = 24.0 + + var contentHeight: CGFloat = 0.0 + + contentHeight += environment.navigationHeight + + let iconSize = self.icon.update( + transition: .immediate, + component: AnyComponent(LottieComponent( + content: LottieComponent.AppBundleContent(name: "MapEmoji"), + loop: false + )), + environment: {}, + containerSize: CGSize(width: 100.0, height: 100.0) + ) + let iconFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - iconSize.width) * 0.5), y: contentHeight + 11.0), size: iconSize) + if let iconView = self.icon.view as? LottieComponent.View { + if iconView.superview == nil { + self.scrollView.addSubview(iconView) + iconView.playOnce() + } + transition.setPosition(view: iconView, position: iconFrame.center) + iconView.bounds = CGRect(origin: CGPoint(), size: iconFrame.size) + } + + contentHeight += 129.0 + + let subtitleString = NSMutableAttributedString(attributedString: parseMarkdownIntoAttributedString(environment.strings.BusinessLocationSetup_Text, attributes: MarkdownAttributes( + body: MarkdownAttributeSet(font: Font.regular(15.0), textColor: environment.theme.list.freeTextColor), + bold: MarkdownAttributeSet(font: Font.semibold(15.0), textColor: environment.theme.list.freeTextColor), + link: MarkdownAttributeSet(font: Font.regular(15.0), textColor: environment.theme.list.itemAccentColor), + linkAttribute: { attributes in + return ("URL", "") + }), textAlignment: .center + )) + + let subtitleSize = self.subtitle.update( + transition: .immediate, + component: AnyComponent(BalancedTextComponent( + text: .plain(subtitleString), + horizontalAlignment: .center, + maximumNumberOfLines: 0, + lineSpacing: 0.25, + highlightColor: environment.theme.list.itemAccentColor.withMultipliedAlpha(0.1), + highlightAction: { attributes in + if let _ = attributes[NSAttributedString.Key(rawValue: "URL")] { + return NSAttributedString.Key(rawValue: "URL") + } else { + return nil + } + }, + tapAction: { [weak self] _, _ in + guard let self, let component = self.component else { + return + } + let _ = component + } + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 1000.0) + ) + let subtitleFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - subtitleSize.width) * 0.5), y: contentHeight), size: subtitleSize) + if let subtitleView = self.subtitle.view { + if subtitleView.superview == nil { + self.scrollView.addSubview(subtitleView) + } + transition.setPosition(view: subtitleView, position: subtitleFrame.center) + subtitleView.bounds = CGRect(origin: CGPoint(), size: subtitleFrame.size) + } + contentHeight += subtitleSize.height + contentHeight += 27.0 + + var addressSectionItems: [AnyComponentWithIdentity] = [] + addressSectionItems.append(AnyComponentWithIdentity(id: 0, component: AnyComponent(ListMultilineTextFieldItemComponent( + externalState: self.addressTextInputState, + context: component.context, + theme: environment.theme, + strings: environment.strings, + initialText: "", + resetText: self.resetAddressText.flatMap { resetAddressText in + return ListMultilineTextFieldItemComponent.ResetText(value: resetAddressText) + }, + placeholder: environment.strings.BusinessLocationSetup_AddressPlaceholder, + autocapitalizationType: .none, + autocorrectionType: .no, + characterLimit: 256, + allowEmptyLines: false, + updated: { _ in + }, + textUpdateTransition: .spring(duration: 0.4), + tag: self.textFieldTag + )))) + self.resetAddressText = nil + + let addressSectionSize = self.addressSection.update( + transition: transition, + component: AnyComponent(ListSectionComponent( + theme: environment.theme, + header: nil, + footer: nil, + items: addressSectionItems + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 10000.0) + ) + let addressSectionFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: addressSectionSize) + if let addressSectionView = self.addressSection.view { + if addressSectionView.superview == nil { + self.scrollView.addSubview(addressSectionView) + self.addressSection.parentState = state + } + transition.setFrame(view: addressSectionView, frame: addressSectionFrame) + } + contentHeight += addressSectionSize.height + contentHeight += sectionSpacing + + var mapSectionItems: [AnyComponentWithIdentity] = [] + + let mapSelectionAccessory: ListActionItemComponent.Accessory? + if self.isLoadingGeocodedAddress { + mapSelectionAccessory = .activity + } else { + mapSelectionAccessory = .toggle(ListActionItemComponent.Toggle(style: .regular, isOn: self.mapCoordinates != nil, isInteractive: self.mapCoordinates != nil)) + } + + mapSectionItems.append(AnyComponentWithIdentity(id: 0, component: AnyComponent(ListActionItemComponent( + theme: environment.theme, + title: AnyComponent(VStack([ + AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: environment.strings.BusinessLocationSetup_SetLocationOnMap, + font: Font.regular(presentationData.listsFontSize.baseDisplaySize), + textColor: environment.theme.list.itemPrimaryTextColor + )), + maximumNumberOfLines: 1 + ))), + ], alignment: .left, spacing: 2.0)), + accessory: mapSelectionAccessory, + action: { [weak self] _ in + guard let self else { + return + } + if self.mapCoordinates == nil { + self.openLocationPicker() + } else { + self.mapCoordinates = nil + self.mapCoordinatesManuallySet = false + self.state?.updated(transition: .spring(duration: 0.4)) + } + } + )))) + if let mapCoordinates = self.mapCoordinates { + mapSectionItems.append(AnyComponentWithIdentity(id: 1, component: AnyComponent(MapPreviewComponent( + theme: environment.theme, + location: MapPreviewComponent.Location( + latitude: mapCoordinates.latitude, + longitude: mapCoordinates.longitude + ), + action: { [weak self] in + guard let self else { + return + } + self.openLocationPicker() + } + )))) + } + + let mapSectionSize = self.mapSection.update( + transition: transition, + component: AnyComponent(ListSectionComponent( + theme: environment.theme, + header: nil, + footer: nil, + items: mapSectionItems, + displaySeparators: false + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 10000.0) + ) + let mapSectionFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: mapSectionSize) + if let mapSectionView = self.mapSection.view { + if mapSectionView.superview == nil { + self.scrollView.addSubview(mapSectionView) + } + transition.setFrame(view: mapSectionView, frame: mapSectionFrame) + } + contentHeight += mapSectionSize.height + + var deleteSectionHeight: CGFloat = 0.0 + + deleteSectionHeight += sectionSpacing + let deleteSectionSize = self.deleteSection.update( + transition: transition, + component: AnyComponent(ListSectionComponent( + theme: environment.theme, + header: nil, + footer: nil, + items: [ + AnyComponentWithIdentity(id: 0, component: AnyComponent(ListActionItemComponent( + theme: environment.theme, + title: AnyComponent(VStack([ + AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: environment.strings.BusinessLocationSetup_DeleteLocation, + font: Font.regular(presentationData.listsFontSize.baseDisplaySize), + textColor: environment.theme.list.itemDestructiveColor + )), + maximumNumberOfLines: 1 + ))), + ], alignment: .left, spacing: 2.0)), + accessory: nil, + action: { [weak self] _ in + guard let self else { + return + } + + self.resetAddressText = "" + self.mapCoordinates = nil + self.mapCoordinatesManuallySet = false + self.state?.updated(transition: .spring(duration: 0.4)) + } + ))) + ], + displaySeparators: false + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 10000.0) + ) + let deleteSectionFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight + deleteSectionHeight), size: deleteSectionSize) + if let deleteSectionView = self.deleteSection.view { + if deleteSectionView.superview == nil { + self.scrollView.addSubview(deleteSectionView) + } + transition.setFrame(view: deleteSectionView, frame: deleteSectionFrame) + + if self.mapCoordinates != nil || self.addressTextInputState.hasText { + alphaTransition.setAlpha(view: deleteSectionView, alpha: 1.0) + } else { + alphaTransition.setAlpha(view: deleteSectionView, alpha: 0.0) + } + } + deleteSectionHeight += deleteSectionSize.height + + if self.mapCoordinates != nil || self.addressTextInputState.hasText { + contentHeight += deleteSectionHeight + } + + contentHeight += bottomContentInset + contentHeight += environment.safeInsets.bottom + + let previousBounds = self.scrollView.bounds + + let contentSize = CGSize(width: availableSize.width, height: contentHeight) + if self.scrollView.frame != CGRect(origin: CGPoint(), size: availableSize) { + self.scrollView.frame = CGRect(origin: CGPoint(), size: availableSize) + } + if self.scrollView.contentSize != contentSize { + self.scrollView.contentSize = contentSize + } + let scrollInsets = UIEdgeInsets(top: environment.navigationHeight, left: 0.0, bottom: 0.0, right: 0.0) + if self.scrollView.scrollIndicatorInsets != scrollInsets { + self.scrollView.scrollIndicatorInsets = scrollInsets + } + + if !previousBounds.isEmpty, !transition.animation.isImmediate { + let bounds = self.scrollView.bounds + if bounds.maxY != previousBounds.maxY { + let offsetY = previousBounds.maxY - bounds.maxY + transition.animateBoundsOrigin(view: self.scrollView, from: CGPoint(x: 0.0, y: offsetY), to: CGPoint(), additive: true) + } + } + + self.topOverscrollLayer.frame = CGRect(origin: CGPoint(x: 0.0, y: -3000.0), size: CGSize(width: availableSize.width, height: 3000.0)) + + self.updateScrolling(transition: transition) + + if let controller = environment.controller() as? BusinessLocationSetupScreen { + let businessLocation = self.currentBusinessLocation() + + if businessLocation == component.initialValue { + if controller.navigationItem.rightBarButtonItem != nil { + controller.navigationItem.setRightBarButton(nil, animated: true) + } + } else { + let applyButtonItem: UIBarButtonItem + if let current = self.applyButtonItem { + applyButtonItem = current + } else { + applyButtonItem = UIBarButtonItem(title: environment.strings.Common_Save, style: .done, target: self, action: #selector(self.savePressed)) + } + if controller.navigationItem.rightBarButtonItem !== applyButtonItem { + controller.navigationItem.setRightBarButton(applyButtonItem, animated: true) + } + } + } + + return availableSize + } + } + + func makeView() -> View { + return View() + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} + +public final class BusinessLocationSetupScreen: ViewControllerComponentContainer { + private let context: AccountContext + + public init( + context: AccountContext, + initialValue: TelegramBusinessLocation?, + completion: @escaping (TelegramBusinessLocation?) -> Void + ) { + self.context = context + + super.init(context: context, component: BusinessLocationSetupScreenComponent( + context: context, + initialValue: initialValue, + completion: completion + ), navigationBarAppearance: .default, theme: .default, updatedPresentationData: nil) + + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + self.title = "" + self.navigationItem.backBarButtonItem = UIBarButtonItem(title: presentationData.strings.Common_Back, style: .plain, target: nil, action: nil) + + self.scrollToTop = { [weak self] in + guard let self, let componentView = self.node.hostView.componentView as? BusinessLocationSetupScreenComponent.View else { + return + } + componentView.scrollToTop() + } + + self.attemptNavigation = { [weak self] complete in + guard let self, let componentView = self.node.hostView.componentView as? BusinessLocationSetupScreenComponent.View else { + return true + } + + return componentView.attemptNavigation(complete: complete) + } + } + + required public init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + } + + @objc private func cancelPressed() { + self.dismiss() + } + + override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { + super.containerLayoutUpdated(layout, transition: transition) + } +} diff --git a/submodules/TelegramUI/Components/Settings/BusinessLocationSetupScreen/Sources/MapPreviewComponent.swift b/submodules/TelegramUI/Components/Settings/BusinessLocationSetupScreen/Sources/MapPreviewComponent.swift new file mode 100644 index 00000000000..9e887c62796 --- /dev/null +++ b/submodules/TelegramUI/Components/Settings/BusinessLocationSetupScreen/Sources/MapPreviewComponent.swift @@ -0,0 +1,139 @@ +import Foundation +import UIKit +import Display +import ComponentFlow +import ListSectionComponent +import MapKit +import TelegramPresentationData +import AppBundle + +final class MapPreviewComponent: Component { + struct Location: Equatable { + var latitude: Double + var longitude: Double + + init(latitude: Double, longitude: Double) { + self.latitude = latitude + self.longitude = longitude + } + } + + let theme: PresentationTheme + let location: Location + let action: (() -> Void)? + + init( + theme: PresentationTheme, + location: Location, + action: (() -> Void)? = nil + ) { + self.theme = theme + self.location = location + self.action = action + } + + static func ==(lhs: MapPreviewComponent, rhs: MapPreviewComponent) -> Bool { + if lhs.theme !== rhs.theme { + return false + } + if lhs.location != rhs.location { + return false + } + if (lhs.action == nil) != (rhs.action == nil) { + return false + } + return true + } + + final class View: HighlightTrackingButton, ListSectionComponent.ChildView { + private var component: MapPreviewComponent? + private weak var componentState: EmptyComponentState? + + private var mapView: MKMapView? + + private let pinShadowView: UIImageView + private let pinView: UIImageView + private let pinForegroundView: UIImageView + + var customUpdateIsHighlighted: ((Bool) -> Void)? + private(set) var separatorInset: CGFloat = 0.0 + + override init(frame: CGRect) { + self.pinShadowView = UIImageView() + self.pinView = UIImageView() + self.pinForegroundView = UIImageView() + + super.init(frame: frame) + + self.addSubview(self.pinShadowView) + self.addSubview(self.pinView) + self.addSubview(self.pinForegroundView) + + self.pinShadowView.image = UIImage(bundleImageName: "Chat/Message/LocationPinShadow") + self.pinView.image = UIImage(bundleImageName: "Chat/Message/LocationPinBackground")?.withRenderingMode(.alwaysTemplate) + self.pinForegroundView.image = UIImage(bundleImageName: "Chat/Message/LocationPinForeground")?.withRenderingMode(.alwaysTemplate) + + self.addTarget(self, action: #selector(self.pressed), for: .touchUpInside) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + @objc private func pressed() { + self.component?.action?() + } + + func update(component: MapPreviewComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + let previousComponent = self.component + self.component = component + self.componentState = state + + self.isEnabled = component.action != nil + + let size = CGSize(width: availableSize.width, height: 160.0) + + let mapView: MKMapView + if let current = self.mapView { + mapView = current + } else { + mapView = MKMapView() + mapView.isUserInteractionEnabled = false + self.mapView = mapView + self.insertSubview(mapView, at: 0) + } + transition.setFrame(view: mapView, frame: CGRect(origin: CGPoint(), size: size)) + + let defaultMapSpan = MKCoordinateSpan(latitudeDelta: 0.016, longitudeDelta: 0.016) + + let region = MKCoordinateRegion(center: CLLocationCoordinate2D(latitude: component.location.latitude, longitude: component.location.longitude), span: defaultMapSpan) + if previousComponent?.location != component.location { + mapView.setRegion(region, animated: false) + mapView.setVisibleMapRect(mapView.visibleMapRect, edgePadding: UIEdgeInsets(top: 70.0, left: 0.0, bottom: 0.0, right: 0.0), animated: true) + } + + let pinImageSize = self.pinView.image?.size ?? CGSize(width: 62.0, height: 74.0) + let pinFrame = CGRect(origin: CGPoint(x: floor((size.width - pinImageSize.width) * 0.5), y: floor((size.height - pinImageSize.height) * 0.5)), size: pinImageSize) + transition.setFrame(view: self.pinShadowView, frame: pinFrame) + + transition.setFrame(view: self.pinView, frame: pinFrame) + self.pinView.tintColor = component.theme.list.itemCheckColors.fillColor + + if let image = pinForegroundView.image { + let pinIconFrame = CGRect(origin: CGPoint(x: pinFrame.minX + floor((pinFrame.width - image.size.width) * 0.5), y: pinFrame.minY + 15.0), size: image.size) + transition.setFrame(view: self.pinForegroundView, frame: pinIconFrame) + self.pinForegroundView.tintColor = component.theme.list.itemCheckColors.foregroundColor + } + + return size + } + } + + func makeView() -> View { + return View(frame: CGRect()) + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} diff --git a/submodules/TelegramUI/Components/Settings/BusinessSetupScreen/Sources/BusinessSetupScreen.swift b/submodules/TelegramUI/Components/Settings/BusinessSetupScreen/Sources/BusinessSetupScreen.swift deleted file mode 100644 index 26e1df5d667..00000000000 --- a/submodules/TelegramUI/Components/Settings/BusinessSetupScreen/Sources/BusinessSetupScreen.swift +++ /dev/null @@ -1,426 +0,0 @@ -import Foundation -import UIKit -import Photos -import Display -import AsyncDisplayKit -import SwiftSignalKit -import Postbox -import TelegramCore -import TelegramPresentationData -import TelegramUIPreferences -import PresentationDataUtils -import AccountContext -import ComponentFlow -import ViewControllerComponent -import MultilineTextComponent -import BalancedTextComponent -import BackButtonComponent -import ListSectionComponent -import ListActionItemComponent -import BundleIconComponent - -final class BusinessSetupScreenComponent: Component { - typealias EnvironmentType = ViewControllerComponentContainer.Environment - - let context: AccountContext - - init( - context: AccountContext - ) { - self.context = context - } - - static func ==(lhs: BusinessSetupScreenComponent, rhs: BusinessSetupScreenComponent) -> Bool { - if lhs.context !== rhs.context { - return false - } - - return true - } - - private final class ScrollView: UIScrollView { - override func touchesShouldCancel(in view: UIView) -> Bool { - return true - } - } - - final class View: UIView, UIScrollViewDelegate { - private let topOverscrollLayer = SimpleLayer() - private let scrollView: ScrollView - - private let navigationTitle = ComponentView() - private let title = ComponentView() - private let subtitle = ComponentView() - private let actionsSection = ComponentView() - - private var isUpdating: Bool = false - - private var component: BusinessSetupScreenComponent? - private(set) weak var state: EmptyComponentState? - private var environment: EnvironmentType? - - override init(frame: CGRect) { - self.scrollView = ScrollView() - self.scrollView.showsVerticalScrollIndicator = true - self.scrollView.showsHorizontalScrollIndicator = false - self.scrollView.scrollsToTop = false - self.scrollView.delaysContentTouches = false - self.scrollView.canCancelContentTouches = true - self.scrollView.contentInsetAdjustmentBehavior = .never - if #available(iOS 13.0, *) { - self.scrollView.automaticallyAdjustsScrollIndicatorInsets = false - } - self.scrollView.alwaysBounceVertical = true - - super.init(frame: frame) - - self.scrollView.delegate = self - self.addSubview(self.scrollView) - - self.scrollView.layer.addSublayer(self.topOverscrollLayer) - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - deinit { - } - - func scrollToTop() { - self.scrollView.setContentOffset(CGPoint(), animated: true) - } - - func attemptNavigation(complete: @escaping () -> Void) -> Bool { - return true - } - - func scrollViewDidScroll(_ scrollView: UIScrollView) { - self.updateScrolling(transition: .immediate) - } - - var scrolledUp = true - private func updateScrolling(transition: Transition) { - guard let environment = self.environment else { - return - } - - let navigationRevealOffsetY: CGFloat = -(environment.statusBarHeight + (environment.navigationHeight - environment.statusBarHeight) * 0.5) + (self.title.view?.frame.midY ?? 0.0) - - let navigationAlphaDistance: CGFloat = 16.0 - let navigationAlpha: CGFloat = max(0.0, min(1.0, (self.scrollView.contentOffset.y - navigationRevealOffsetY) / navigationAlphaDistance)) - if let controller = self.environment?.controller(), let navigationBar = controller.navigationBar { - transition.setAlpha(layer: navigationBar.backgroundNode.layer, alpha: navigationAlpha) - transition.setAlpha(layer: navigationBar.stripeNode.layer, alpha: navigationAlpha) - } - - var scrolledUp = false - if navigationAlpha < 0.5 { - scrolledUp = true - } else if navigationAlpha > 0.5 { - scrolledUp = false - } - - if self.scrolledUp != scrolledUp { - self.scrolledUp = scrolledUp - if !self.isUpdating { - self.state?.updated() - } - } - - if let navigationTitleView = self.navigationTitle.view { - transition.setAlpha(view: navigationTitleView, alpha: navigationAlpha) - } - } - - func update(component: BusinessSetupScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { - self.isUpdating = true - defer { - self.isUpdating = false - } - - let environment = environment[EnvironmentType.self].value - let themeUpdated = self.environment?.theme !== environment.theme - self.environment = environment - - self.component = component - self.state = state - - if themeUpdated { - self.backgroundColor = environment.theme.list.blocksBackgroundColor - } - - let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } - - //TODO:localize - let navigationTitleSize = self.navigationTitle.update( - transition: transition, - component: AnyComponent(MultilineTextComponent( - text: .plain(NSAttributedString(string: "Telegram Business", font: Font.semibold(17.0), textColor: environment.theme.rootController.navigationBar.primaryTextColor)), - horizontalAlignment: .center - )), - environment: {}, - containerSize: CGSize(width: availableSize.width, height: 100.0) - ) - let navigationTitleFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - navigationTitleSize.width) / 2.0), y: environment.statusBarHeight + floor((environment.navigationHeight - environment.statusBarHeight - navigationTitleSize.height) / 2.0)), size: navigationTitleSize) - if let navigationTitleView = self.navigationTitle.view { - if navigationTitleView.superview == nil { - if let controller = self.environment?.controller(), let navigationBar = controller.navigationBar { - navigationBar.view.addSubview(navigationTitleView) - } - } - transition.setFrame(view: navigationTitleView, frame: navigationTitleFrame) - } - - let bottomContentInset: CGFloat = 24.0 - let sideInset: CGFloat = 16.0 + environment.safeInsets.left - let sectionSpacing: CGFloat = 32.0 - - let _ = bottomContentInset - let _ = sectionSpacing - - var contentHeight: CGFloat = 0.0 - - contentHeight += environment.navigationHeight - contentHeight += 81.0 - - //TODO:localize - let titleSize = self.title.update( - transition: .immediate, - component: AnyComponent(MultilineTextComponent( - text: .plain(NSAttributedString(string: "Telegram Business", font: Font.bold(29.0), textColor: environment.theme.list.itemPrimaryTextColor)), - horizontalAlignment: .center, - maximumNumberOfLines: 1 - )), - environment: {}, - containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 1000.0) - ) - let titleFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - titleSize.width) * 0.5), y: contentHeight), size: titleSize) - if let titleView = self.title.view { - if titleView.superview == nil { - self.scrollView.addSubview(titleView) - } - transition.setPosition(view: titleView, position: titleFrame.center) - titleView.bounds = CGRect(origin: CGPoint(), size: titleFrame.size) - } - contentHeight += titleSize.height - contentHeight += 17.0 - - //TODO:localize - let subtitleSize = self.subtitle.update( - transition: .immediate, - component: AnyComponent(BalancedTextComponent( - text: .plain(NSAttributedString(string: "You have now unlocked these additional business features.", font: Font.regular(15.0), textColor: environment.theme.list.itemPrimaryTextColor)), - horizontalAlignment: .center, - maximumNumberOfLines: 0, - lineSpacing: 0.25 - )), - environment: {}, - containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 1000.0) - ) - let subtitleFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - subtitleSize.width) * 0.5), y: contentHeight), size: subtitleSize) - if let subtitleView = self.subtitle.view { - if subtitleView.superview == nil { - self.scrollView.addSubview(subtitleView) - } - transition.setPosition(view: subtitleView, position: subtitleFrame.center) - subtitleView.bounds = CGRect(origin: CGPoint(), size: subtitleFrame.size) - } - contentHeight += subtitleSize.height - contentHeight += 21.0 - - struct Item { - var icon: String - var title: String - var subtitle: String - var action: () -> Void - } - var items: [Item] = [] - //TODO:localize - items.append(Item( - icon: "Settings/Menu/AddAccount", - title: "Location", - subtitle: "Display the location of your business on your account.", - action: { - } - )) - items.append(Item( - icon: "Settings/Menu/DataVoice", - title: "Opening Hours", - subtitle: "Show to your customers when you are open for business.", - action: { - } - )) - items.append(Item( - icon: "Settings/Menu/Photos", - title: "Quick Replies", - subtitle: "Set up shortcuts with rich text and media to respond to messages faster.", - action: { - } - )) - items.append(Item( - icon: "Settings/Menu/Stories", - title: "Greeting Messages", - subtitle: "Create greetings that will be automatically sent to new customers.", - action: { - } - )) - items.append(Item( - icon: "Settings/Menu/Trending", - title: "Away Messages", - subtitle: "Define messages that are automatically sent when you are off.", - action: { - } - )) - items.append(Item( - icon: "Settings/Menu/DataStickers", - title: "Chatbots", - subtitle: "Add any third-party chatbots that will process customer interactions.", - action: { [weak self] in - guard let self, let component = self.component, let environment = self.environment else { - return - } - environment.controller()?.push(component.context.sharedContext.makeChatbotSetupScreen(context: component.context)) - } - )) - - var actionsSectionItems: [AnyComponentWithIdentity] = [] - for item in items { - actionsSectionItems.append(AnyComponentWithIdentity(id: actionsSectionItems.count, component: AnyComponent(ListActionItemComponent( - theme: environment.theme, - title: AnyComponent(VStack([ - AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent( - text: .plain(NSAttributedString( - string: item.title, - font: Font.regular(presentationData.listsFontSize.baseDisplaySize), - textColor: environment.theme.list.itemPrimaryTextColor - )), - maximumNumberOfLines: 0 - ))), - AnyComponentWithIdentity(id: AnyHashable(1), component: AnyComponent(MultilineTextComponent( - text: .plain(NSAttributedString( - string: item.subtitle, - font: Font.regular(floor(presentationData.listsFontSize.baseDisplaySize * 13.0 / 17.0)), - textColor: environment.theme.list.itemSecondaryTextColor - )), - maximumNumberOfLines: 0, - lineSpacing: 0.18 - ))) - ], alignment: .left, spacing: 2.0)), - leftIcon: AnyComponentWithIdentity(id: 0, component: AnyComponent(BundleIconComponent( - name: item.icon, - tintColor: nil - ))), - action: { _ in - item.action() - } - )))) - } - - let actionsSectionSize = self.actionsSection.update( - transition: transition, - component: AnyComponent(ListSectionComponent( - theme: environment.theme, - header: nil, - footer: nil, - items: actionsSectionItems - )), - environment: {}, - containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 10000.0) - ) - let actionsSectionFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: actionsSectionSize) - if let actionsSectionView = self.actionsSection.view { - if actionsSectionView.superview == nil { - self.scrollView.addSubview(actionsSectionView) - } - transition.setFrame(view: actionsSectionView, frame: actionsSectionFrame) - } - contentHeight += actionsSectionSize.height - - contentHeight += bottomContentInset - contentHeight += environment.safeInsets.bottom - - let previousBounds = self.scrollView.bounds - - let contentSize = CGSize(width: availableSize.width, height: contentHeight) - if self.scrollView.frame != CGRect(origin: CGPoint(), size: availableSize) { - self.scrollView.frame = CGRect(origin: CGPoint(), size: availableSize) - } - if self.scrollView.contentSize != contentSize { - self.scrollView.contentSize = contentSize - } - let scrollInsets = UIEdgeInsets(top: environment.navigationHeight, left: 0.0, bottom: 0.0, right: 0.0) - if self.scrollView.scrollIndicatorInsets != scrollInsets { - self.scrollView.scrollIndicatorInsets = scrollInsets - } - - if !previousBounds.isEmpty, !transition.animation.isImmediate { - let bounds = self.scrollView.bounds - if bounds.maxY != previousBounds.maxY { - let offsetY = previousBounds.maxY - bounds.maxY - transition.animateBoundsOrigin(view: self.scrollView, from: CGPoint(x: 0.0, y: offsetY), to: CGPoint(), additive: true) - } - } - - self.topOverscrollLayer.frame = CGRect(origin: CGPoint(x: 0.0, y: -3000.0), size: CGSize(width: availableSize.width, height: 3000.0)) - - self.updateScrolling(transition: transition) - - return availableSize - } - } - - func makeView() -> View { - return View() - } - - func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { - return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) - } -} - -public final class BusinessSetupScreen: ViewControllerComponentContainer { - private let context: AccountContext - - public init(context: AccountContext) { - self.context = context - - super.init(context: context, component: BusinessSetupScreenComponent( - context: context - ), navigationBarAppearance: .default, theme: .default, updatedPresentationData: nil) - - let presentationData = context.sharedContext.currentPresentationData.with { $0 } - self.title = "" - self.navigationItem.backBarButtonItem = UIBarButtonItem(title: presentationData.strings.Common_Back, style: .plain, target: nil, action: nil) - - self.scrollToTop = { [weak self] in - guard let self, let componentView = self.node.hostView.componentView as? BusinessSetupScreenComponent.View else { - return - } - componentView.scrollToTop() - } - - self.attemptNavigation = { [weak self] complete in - guard let self, let componentView = self.node.hostView.componentView as? BusinessSetupScreenComponent.View else { - return true - } - - return componentView.attemptNavigation(complete: complete) - } - } - - required public init(coder aDecoder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - deinit { - } - - @objc private func cancelPressed() { - self.dismiss() - } - - override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { - super.containerLayoutUpdated(layout, transition: transition) - } -} diff --git a/submodules/TelegramUI/Components/Settings/ChatbotSetupScreen/BUILD b/submodules/TelegramUI/Components/Settings/ChatbotSetupScreen/BUILD index 9cc671937c2..c83d5538a0e 100644 --- a/submodules/TelegramUI/Components/Settings/ChatbotSetupScreen/BUILD +++ b/submodules/TelegramUI/Components/Settings/ChatbotSetupScreen/BUILD @@ -31,6 +31,10 @@ swift_library( "//submodules/TelegramUI/Components/ListActionItemComponent", "//submodules/TelegramUI/Components/ListTextFieldItemComponent", "//submodules/TelegramUI/Components/LottieComponent", + "//submodules/AvatarNode", + "//submodules/TelegramUI/Components/PlainButtonComponent", + "//submodules/TelegramUI/Components/Stories/PeerListItemComponent", + "//submodules/ShimmerEffect", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/Settings/ChatbotSetupScreen/Sources/ChatbotSearchResultItemComponent.swift b/submodules/TelegramUI/Components/Settings/ChatbotSetupScreen/Sources/ChatbotSearchResultItemComponent.swift new file mode 100644 index 00000000000..c9b085c8ed0 --- /dev/null +++ b/submodules/TelegramUI/Components/Settings/ChatbotSetupScreen/Sources/ChatbotSearchResultItemComponent.swift @@ -0,0 +1,402 @@ +import Foundation +import UIKit +import Display +import ComponentFlow +import MultilineTextComponent +import AvatarNode +import BundleIconComponent +import TelegramPresentationData +import TelegramCore +import AccountContext +import ListSectionComponent +import PlainButtonComponent +import ShimmerEffect + +final class ChatbotSearchResultItemComponent: Component { + enum Content: Equatable { + case searching + case found(peer: EnginePeer, isInstalled: Bool) + case notFound + } + + let context: AccountContext + let theme: PresentationTheme + let strings: PresentationStrings + let content: Content + let installAction: () -> Void + let removeAction: () -> Void + + init( + context: AccountContext, + theme: PresentationTheme, + strings: PresentationStrings, + content: Content, + installAction: @escaping () -> Void, + removeAction: @escaping () -> Void + ) { + self.context = context + self.theme = theme + self.strings = strings + self.content = content + self.installAction = installAction + self.removeAction = removeAction + } + + static func ==(lhs: ChatbotSearchResultItemComponent, rhs: ChatbotSearchResultItemComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.theme !== rhs.theme { + return false + } + if lhs.strings !== rhs.strings { + return false + } + if lhs.content != rhs.content { + return false + } + return true + } + + final class View: UIView, ListSectionComponent.ChildView { + private var notFoundLabel: ComponentView? + private let titleLabel = ComponentView() + private let subtitleLabel = ComponentView() + + private var shimmerEffectNode: ShimmerEffectNode? + + private var avatarNode: AvatarNode? + + private var addButton: ComponentView? + private var removeButton: ComponentView? + + private var component: ChatbotSearchResultItemComponent? + private weak var state: EmptyComponentState? + + var customUpdateIsHighlighted: ((Bool) -> Void)? + private(set) var separatorInset: CGFloat = 0.0 + + override init(frame: CGRect) { + super.init(frame: frame) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func update(component: ChatbotSearchResultItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + self.component = component + self.state = state + + let sideInset: CGFloat = 10.0 + let avatarDiameter: CGFloat = 40.0 + let avatarTextSpacing: CGFloat = 12.0 + let titleSubtitleSpacing: CGFloat = 1.0 + let verticalInset: CGFloat = 11.0 + + let maxTextWidth: CGFloat = availableSize.width - sideInset * 2.0 - avatarDiameter - avatarTextSpacing + + var addButtonSize: CGSize? + if case .found(_, false) = component.content { + let addButton: ComponentView + var addButtonTransition = transition + if let current = self.addButton { + addButton = current + } else { + addButtonTransition = addButtonTransition.withAnimation(.none) + addButton = ComponentView() + self.addButton = addButton + } + + addButtonSize = addButton.update( + transition: addButtonTransition, + component: AnyComponent(PlainButtonComponent( + content: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: component.strings.ChatbotSetup_BotAddAction, font: Font.semibold(15.0), textColor: component.theme.list.itemCheckColors.foregroundColor)) + )), + background: AnyComponent(RoundedRectangle(color: component.theme.list.itemCheckColors.fillColor, cornerRadius: nil)), + effectAlignment: .center, + minSize: nil, + contentInsets: UIEdgeInsets(top: 4.0, left: 8.0, bottom: 4.0, right: 8.0), + action: { [weak self] in + guard let self, let component = self.component else { + return + } + component.installAction() + }, + animateAlpha: true, + animateScale: false + )), + environment: {}, + containerSize: CGSize(width: 100.0, height: 100.0) + ) + } else { + if let addButton = self.addButton { + self.addButton = nil + if let addButtonView = addButton.view { + if !transition.animation.isImmediate { + transition.setScale(view: addButtonView, scale: 0.001) + Transition.easeInOut(duration: 0.2).setAlpha(view: addButtonView, alpha: 0.0, completion: { [weak addButtonView] _ in + addButtonView?.removeFromSuperview() + }) + } else { + addButtonView.removeFromSuperview() + } + } + } + } + + var removeButtonSize: CGSize? + if case .found(_, true) = component.content { + let removeButton: ComponentView + var removeButtonTransition = transition + if let current = self.removeButton { + removeButton = current + } else { + removeButtonTransition = removeButtonTransition.withAnimation(.none) + removeButton = ComponentView() + self.removeButton = removeButton + } + + removeButtonSize = removeButton.update( + transition: removeButtonTransition, + component: AnyComponent(PlainButtonComponent( + content: AnyComponent(BundleIconComponent( + name: "Chat/Message/SideCloseIcon", + tintColor: component.theme.list.controlSecondaryColor + )), + effectAlignment: .center, + minSize: nil, + contentInsets: UIEdgeInsets(top: 4.0, left: 4.0, bottom: 4.0, right: 4.0), + action: { [weak self] in + guard let self, let component = self.component else { + return + } + component.removeAction() + }, + animateAlpha: true, + animateScale: false + )), + environment: {}, + containerSize: CGSize(width: 100.0, height: 100.0) + ) + } else { + if let removeButton = self.removeButton { + self.removeButton = nil + if let removeButtonView = removeButton.view { + if !transition.animation.isImmediate { + transition.setScale(view: removeButtonView, scale: 0.001) + Transition.easeInOut(duration: 0.2).setAlpha(view: removeButtonView, alpha: 0.0, completion: { [weak removeButtonView] _ in + removeButtonView?.removeFromSuperview() + }) + } else { + removeButtonView.removeFromSuperview() + } + } + } + } + + let titleValue: String + let subtitleValue: String + let isTextVisible: Bool + switch component.content { + case .searching, .notFound: + isTextVisible = false + titleValue = "AAAAAAAAA" + subtitleValue = component.strings.Bot_GenericBotStatus + case let .found(peer, _): + isTextVisible = true + titleValue = peer.displayTitle(strings: component.strings, displayOrder: .firstLast) + subtitleValue = component.strings.Bot_GenericBotStatus + } + + let titleSize = self.titleLabel.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: titleValue, font: Font.semibold(17.0), textColor: component.theme.list.itemPrimaryTextColor)), + maximumNumberOfLines: 1 + )), + environment: {}, + containerSize: CGSize(width: maxTextWidth, height: 100.0) + ) + let subtitleSize = self.subtitleLabel.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: subtitleValue, font: Font.regular(15.0), textColor: component.theme.list.itemSecondaryTextColor)), + maximumNumberOfLines: 1 + )), + environment: {}, + containerSize: CGSize(width: maxTextWidth, height: 100.0) + ) + + let size = CGSize(width: availableSize.width, height: verticalInset * 2.0 + titleSize.height + titleSubtitleSpacing + subtitleSize.height) + + let titleFrame = CGRect(origin: CGPoint(x: sideInset + avatarDiameter + avatarTextSpacing, y: verticalInset), size: titleSize) + if let titleView = self.titleLabel.view { + var titleTransition = transition + if titleView.superview == nil { + titleTransition = .immediate + titleView.layer.anchorPoint = CGPoint() + self.addSubview(titleView) + } + if titleView.isHidden != !isTextVisible { + titleTransition = .immediate + } + + titleView.bounds = CGRect(origin: CGPoint(), size: titleFrame.size) + titleTransition.setPosition(view: titleView, position: titleFrame.origin) + titleView.isHidden = !isTextVisible + } + + let subtitleFrame = CGRect(origin: CGPoint(x: sideInset + avatarDiameter + avatarTextSpacing, y: verticalInset + titleSize.height + titleSubtitleSpacing), size: subtitleSize) + if let subtitleView = self.subtitleLabel.view { + var subtitleTransition = transition + if subtitleView.superview == nil { + subtitleTransition = .immediate + subtitleView.layer.anchorPoint = CGPoint() + self.addSubview(subtitleView) + } + if subtitleView.isHidden != !isTextVisible { + subtitleTransition = .immediate + } + + subtitleView.bounds = CGRect(origin: CGPoint(), size: subtitleFrame.size) + subtitleTransition.setPosition(view: subtitleView, position: subtitleFrame.origin) + subtitleView.isHidden = !isTextVisible + } + + let avatarFrame = CGRect(origin: CGPoint(x: sideInset, y: floor((size.height - avatarDiameter) * 0.5)), size: CGSize(width: avatarDiameter, height: avatarDiameter)) + + if case let .found(peer, _) = component.content { + var avatarTransition = transition + let avatarNode: AvatarNode + if let current = self.avatarNode { + avatarNode = current + } else { + avatarTransition = .immediate + avatarNode = AvatarNode(font: avatarPlaceholderFont(size: 17.0)) + self.avatarNode = avatarNode + self.addSubview(avatarNode.view) + } + avatarTransition.setFrame(view: avatarNode.view, frame: avatarFrame) + avatarNode.setPeer(context: component.context, theme: component.theme, peer: peer, synchronousLoad: true, displayDimensions: avatarFrame.size) + avatarNode.updateSize(size: avatarFrame.size) + } else { + if let avatarNode = self.avatarNode { + self.avatarNode = nil + avatarNode.view.removeFromSuperview() + } + } + + if case .notFound = component.content { + let notFoundLabel: ComponentView + if let current = self.notFoundLabel { + notFoundLabel = current + } else { + notFoundLabel = ComponentView() + self.notFoundLabel = notFoundLabel + } + let notFoundLabelSize = notFoundLabel.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: component.strings.ChatbotSetup_BotNotFoundStatus, font: Font.regular(13.0), textColor: component.theme.list.itemSecondaryTextColor)) + )), + environment: {}, + containerSize: CGSize(width: maxTextWidth, height: 100.0) + ) + let notFoundLabelFrame = CGRect(origin: CGPoint(x: floor((size.width - notFoundLabelSize.width) * 0.5), y: floor((size.height - notFoundLabelSize.height) * 0.5)), size: notFoundLabelSize) + if let notFoundLabelView = notFoundLabel.view { + var notFoundLabelTransition = transition + if notFoundLabelView.superview == nil { + notFoundLabelTransition = .immediate + self.addSubview(notFoundLabelView) + } + notFoundLabelTransition.setPosition(view: notFoundLabelView, position: notFoundLabelFrame.center) + notFoundLabelView.bounds = CGRect(origin: CGPoint(), size: notFoundLabelFrame.size) + } + } else { + if let notFoundLabel = self.notFoundLabel { + self.notFoundLabel = nil + notFoundLabel.view?.removeFromSuperview() + } + } + + if let addButton = self.addButton, let addButtonSize { + var addButtonTransition = transition + let addButtonFrame = CGRect(origin: CGPoint(x: availableSize.width - sideInset - addButtonSize.width, y: floor((size.height - addButtonSize.height) * 0.5)), size: addButtonSize) + if let addButtonView = addButton.view { + if addButtonView.superview == nil { + addButtonTransition = addButtonTransition.withAnimation(.none) + self.addSubview(addButtonView) + if !transition.animation.isImmediate { + transition.animateScale(view: addButtonView, from: 0.001, to: 1.0) + Transition.easeInOut(duration: 0.2).animateAlpha(view: addButtonView, from: 0.0, to: 1.0) + } + } + addButtonTransition.setFrame(view: addButtonView, frame: addButtonFrame) + } + } + + if let removeButton = self.removeButton, let removeButtonSize { + var removeButtonTransition = transition + let removeButtonFrame = CGRect(origin: CGPoint(x: availableSize.width - sideInset - removeButtonSize.width, y: floor((size.height - removeButtonSize.height) * 0.5)), size: removeButtonSize) + if let removeButtonView = removeButton.view { + if removeButtonView.superview == nil { + removeButtonTransition = removeButtonTransition.withAnimation(.none) + self.addSubview(removeButtonView) + if !transition.animation.isImmediate { + transition.animateScale(view: removeButtonView, from: 0.001, to: 1.0) + Transition.easeInOut(duration: 0.2).animateAlpha(view: removeButtonView, from: 0.0, to: 1.0) + } + } + removeButtonTransition.setFrame(view: removeButtonView, frame: removeButtonFrame) + } + } + + if case .searching = component.content { + let shimmerEffectNode: ShimmerEffectNode + if let current = self.shimmerEffectNode { + shimmerEffectNode = current + } else { + shimmerEffectNode = ShimmerEffectNode() + self.shimmerEffectNode = shimmerEffectNode + self.addSubview(shimmerEffectNode.view) + } + + shimmerEffectNode.frame = CGRect(origin: CGPoint(), size: size) + shimmerEffectNode.updateAbsoluteRect(CGRect(origin: CGPoint(), size: size), within: size) + + var shapes: [ShimmerEffectNode.Shape] = [] + + let titleLineWidth: CGFloat = titleFrame.width + let subtitleLineWidth: CGFloat = subtitleFrame.width + let lineDiameter: CGFloat = 10.0 + + shapes.append(.circle(avatarFrame)) + + shapes.append(.roundedRectLine(startPoint: CGPoint(x: titleFrame.minX, y: titleFrame.minY + floor((titleFrame.height - lineDiameter) / 2.0)), width: titleLineWidth, diameter: lineDiameter)) + + shapes.append(.roundedRectLine(startPoint: CGPoint(x: subtitleFrame.minX, y: subtitleFrame.minY + floor((subtitleFrame.height - lineDiameter) / 2.0)), width: subtitleLineWidth, diameter: lineDiameter)) + + shimmerEffectNode.update(backgroundColor: component.theme.list.itemBlocksBackgroundColor, foregroundColor: component.theme.list.mediaPlaceholderColor, shimmeringColor: component.theme.list.itemBlocksBackgroundColor.withAlphaComponent(0.4), shapes: shapes, size: size) + } else { + if let shimmerEffectNode = self.shimmerEffectNode { + self.shimmerEffectNode = nil + shimmerEffectNode.view.removeFromSuperview() + } + } + + self.separatorInset = 16.0 + + return size + } + } + + func makeView() -> View { + return View(frame: CGRect()) + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} diff --git a/submodules/TelegramUI/Components/Settings/ChatbotSetupScreen/Sources/ChatbotSetupScreen.swift b/submodules/TelegramUI/Components/Settings/ChatbotSetupScreen/Sources/ChatbotSetupScreen.swift index 44b119f8950..02dc866138f 100644 --- a/submodules/TelegramUI/Components/Settings/ChatbotSetupScreen/Sources/ChatbotSetupScreen.swift +++ b/submodules/TelegramUI/Components/Settings/ChatbotSetupScreen/Sources/ChatbotSetupScreen.swift @@ -21,6 +21,8 @@ import ListTextFieldItemComponent import BundleIconComponent import LottieComponent import Markdown +import PeerListItemComponent +import AvatarNode private let checkIcon: UIImage = { return generateImage(CGSize(width: 12.0, height: 10.0), rotatedContext: { size, context in @@ -39,11 +41,14 @@ final class ChatbotSetupScreenComponent: Component { typealias EnvironmentType = ViewControllerComponentContainer.Environment let context: AccountContext + let initialData: ChatbotSetupScreen.InitialData init( - context: AccountContext + context: AccountContext, + initialData: ChatbotSetupScreen.InitialData ) { self.context = context + self.initialData = initialData } static func ==(lhs: ChatbotSetupScreenComponent, rhs: ChatbotSetupScreenComponent) -> Bool { @@ -60,6 +65,49 @@ final class ChatbotSetupScreenComponent: Component { } } + private struct BotResolutionState: Equatable { + enum State: Equatable { + case searching + case notFound + case found(peer: EnginePeer, isInstalled: Bool) + } + + var query: String + var state: State + + init(query: String, state: State) { + self.query = query + self.state = state + } + } + + struct AdditionalPeerList { + enum Category: Int { + case newChats = 0 + case existingChats = 1 + case contacts = 2 + case nonContacts = 3 + } + + struct Peer { + var peer: EnginePeer + var isContact: Bool + + init(peer: EnginePeer, isContact: Bool) { + self.peer = peer + self.isContact = isContact + } + } + + var categories: Set + var peers: [Peer] + + init(categories: Set, peers: [Peer]) { + self.categories = categories + self.peers = peers + } + } + final class View: UIView, UIScrollViewDelegate { private let topOverscrollLayer = SimpleLayer() private let scrollView: ScrollView @@ -79,6 +127,19 @@ final class ChatbotSetupScreenComponent: Component { private var environment: EnvironmentType? private var chevronImage: UIImage? + private let textFieldTag = NSObject() + + private var botResolutionState: BotResolutionState? + private var botResolutionDisposable: Disposable? + private var resetQueryText: String? + + private var hasAccessToAllChatsByDefault: Bool = true + private var additionalPeerList = AdditionalPeerList( + categories: Set(), + peers: [] + ) + + private var replyToMessages: Bool = true override init(frame: CGRect) { self.scrollView = ScrollView() @@ -113,6 +174,39 @@ final class ChatbotSetupScreenComponent: Component { } func attemptNavigation(complete: @escaping () -> Void) -> Bool { + guard let component = self.component else { + return true + } + + var mappedCategories: TelegramBusinessRecipients.Categories = [] + if self.additionalPeerList.categories.contains(.existingChats) { + mappedCategories.insert(.existingChats) + } + if self.additionalPeerList.categories.contains(.newChats) { + mappedCategories.insert(.newChats) + } + if self.additionalPeerList.categories.contains(.contacts) { + mappedCategories.insert(.contacts) + } + if self.additionalPeerList.categories.contains(.nonContacts) { + mappedCategories.insert(.nonContacts) + } + let recipients = TelegramBusinessRecipients( + categories: mappedCategories, + additionalPeers: Set(self.additionalPeerList.peers.map(\.peer.id)), + exclude: self.hasAccessToAllChatsByDefault + ) + + if let botResolutionState = self.botResolutionState, case let .found(peer, isInstalled) = botResolutionState.state, isInstalled { + let _ = component.context.engine.accountData.setAccountConnectedBot(bot: TelegramAccountConnectedBot( + id: peer.id, + recipients: recipients, + canReply: self.replyToMessages + )).startStandalone() + } else { + let _ = component.context.engine.accountData.setAccountConnectedBot(bot: nil).startStandalone() + } + return true } @@ -150,12 +244,228 @@ final class ChatbotSetupScreenComponent: Component { } } + private func updateBotQuery(query: String) { + guard let component = self.component else { + return + } + + if !query.isEmpty { + if self.botResolutionState?.query != query { + let previousState = self.botResolutionState?.state + self.botResolutionState = BotResolutionState( + query: query, + state: self.botResolutionState?.state ?? .searching + ) + self.botResolutionDisposable?.dispose() + + if previousState != self.botResolutionState?.state { + self.state?.updated(transition: .spring(duration: 0.35)) + } + + self.botResolutionDisposable = (component.context.engine.peers.resolvePeerByName(name: query) + |> deliverOnMainQueue).start(next: { [weak self] result in + guard let self else { + return + } + switch result { + case .progress: + break + case let .result(peer): + let previousState = self.botResolutionState?.state + if let peer { + self.botResolutionState?.state = .found(peer: peer, isInstalled: false) + } else { + self.botResolutionState?.state = .notFound + } + if previousState != self.botResolutionState?.state { + self.state?.updated(transition: .spring(duration: 0.35)) + } + } + }) + } + } else { + if let botResolutionDisposable = self.botResolutionDisposable { + self.botResolutionDisposable = nil + botResolutionDisposable.dispose() + } + if self.botResolutionState != nil { + self.botResolutionState = nil + self.state?.updated(transition: .spring(duration: 0.35)) + } + } + } + + private func openAdditionalPeerListSetup() { + guard let component = self.component, let environment = self.environment else { + return + } + + enum AdditionalCategoryId: Int { + case existingChats + case newChats + case contacts + case nonContacts + } + + let additionalCategories: [ChatListNodeAdditionalCategory] = [ + ChatListNodeAdditionalCategory( + id: self.hasAccessToAllChatsByDefault ? AdditionalCategoryId.existingChats.rawValue : AdditionalCategoryId.newChats.rawValue, + icon: generateAvatarImage(size: CGSize(width: 40.0, height: 40.0), icon: generateTintedImage(image: UIImage(bundleImageName: self.hasAccessToAllChatsByDefault ? "Chat List/Filters/Chats" : "Chat List/Filters/NewChats"), color: .white), cornerRadius: 12.0, color: .purple), + smallIcon: generateAvatarImage(size: CGSize(width: 22.0, height: 22.0), icon: generateTintedImage(image: UIImage(bundleImageName: self.hasAccessToAllChatsByDefault ? "Chat List/Filters/Chats" : "Chat List/Filters/NewChats"), color: .white), iconScale: 0.6, cornerRadius: 6.0, circleCorners: true, color: .purple), + title: self.hasAccessToAllChatsByDefault ? environment.strings.BusinessMessageSetup_Recipients_CategoryExistingChats : environment.strings.BusinessMessageSetup_Recipients_CategoryNewChats + ), + ChatListNodeAdditionalCategory( + id: AdditionalCategoryId.contacts.rawValue, + icon: generateAvatarImage(size: CGSize(width: 40.0, height: 40.0), icon: generateTintedImage(image: UIImage(bundleImageName: "Chat List/Filters/Contact"), color: .white), cornerRadius: 12.0, color: .blue), + smallIcon: generateAvatarImage(size: CGSize(width: 22.0, height: 22.0), icon: generateTintedImage(image: UIImage(bundleImageName: "Chat List/Filters/Contact"), color: .white), iconScale: 0.6, cornerRadius: 6.0, circleCorners: true, color: .blue), + title: environment.strings.BusinessMessageSetup_Recipients_CategoryContacts + ), + ChatListNodeAdditionalCategory( + id: AdditionalCategoryId.nonContacts.rawValue, + icon: generateAvatarImage(size: CGSize(width: 40.0, height: 40.0), icon: generateTintedImage(image: UIImage(bundleImageName: "Chat List/Filters/User"), color: .white), cornerRadius: 12.0, color: .yellow), + smallIcon: generateAvatarImage(size: CGSize(width: 22.0, height: 22.0), icon: generateTintedImage(image: UIImage(bundleImageName: "Chat List/Filters/User"), color: .white), iconScale: 0.6, cornerRadius: 6.0, circleCorners: true, color: .yellow), + title: environment.strings.BusinessMessageSetup_Recipients_CategoryNonContacts + ) + ] + var selectedCategories = Set() + for category in self.additionalPeerList.categories { + switch category { + case .existingChats: + selectedCategories.insert(AdditionalCategoryId.existingChats.rawValue) + case .newChats: + selectedCategories.insert(AdditionalCategoryId.newChats.rawValue) + case .contacts: + selectedCategories.insert(AdditionalCategoryId.contacts.rawValue) + case .nonContacts: + selectedCategories.insert(AdditionalCategoryId.nonContacts.rawValue) + } + } + + let controller = component.context.sharedContext.makeContactMultiselectionController(ContactMultiselectionControllerParams(context: component.context, mode: .chatSelection(ContactMultiselectionControllerMode.ChatSelection( + title: self.hasAccessToAllChatsByDefault ? environment.strings.BusinessMessageSetup_Recipients_ExcludeSearchTitle : environment.strings.BusinessMessageSetup_Recipients_IncludeSearchTitle, + searchPlaceholder: environment.strings.ChatListFilter_AddChatsSearchPlaceholder, + selectedChats: Set(self.additionalPeerList.peers.map(\.peer.id)), + additionalCategories: ContactMultiselectionControllerAdditionalCategories(categories: additionalCategories, selectedCategories: selectedCategories), + chatListFilters: nil, + onlyUsers: true + )), options: [], filters: [], alwaysEnabled: true, limit: 100, reachedLimit: { _ in + })) + controller.navigationPresentation = .modal + + let _ = (controller.result + |> take(1) + |> deliverOnMainQueue).startStandalone(next: { [weak self, weak controller] result in + guard let self, let component = self.component, case let .result(rawPeerIds, additionalCategoryIds) = result else { + controller?.dismiss() + return + } + + let peerIds = rawPeerIds.compactMap { id -> EnginePeer.Id? in + switch id { + case let .peer(id): + return id + case .deviceContact: + return nil + } + } + + let _ = (component.context.engine.data.get( + EngineDataMap( + peerIds.map(TelegramEngine.EngineData.Item.Peer.Peer.init(id:)) + ), + EngineDataMap( + peerIds.map(TelegramEngine.EngineData.Item.Peer.IsContact.init(id:)) + ) + ) + |> deliverOnMainQueue).start(next: { [weak self] peerMap, isContactMap in + guard let self else { + return + } + + let mappedCategories = additionalCategoryIds.compactMap { item -> AdditionalPeerList.Category? in + switch item { + case AdditionalCategoryId.existingChats.rawValue: + return .existingChats + case AdditionalCategoryId.newChats.rawValue: + return .newChats + case AdditionalCategoryId.contacts.rawValue: + return .contacts + case AdditionalCategoryId.nonContacts.rawValue: + return .nonContacts + default: + return nil + } + } + + self.additionalPeerList.categories = Set(mappedCategories) + + self.additionalPeerList.peers.removeAll() + for id in peerIds { + guard let maybePeer = peerMap[id], let peer = maybePeer else { + continue + } + self.additionalPeerList.peers.append(AdditionalPeerList.Peer( + peer: peer, + isContact: isContactMap[id] ?? false + )) + } + self.additionalPeerList.peers.sort(by: { lhs, rhs in + return lhs.peer.debugDisplayTitle < rhs.peer.debugDisplayTitle + }) + self.state?.updated(transition: .immediate) + + controller?.dismiss() + }) + }) + + self.environment?.controller()?.push(controller) + } + func update(component: ChatbotSetupScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { self.isUpdating = true defer { self.isUpdating = false } + if self.component == nil { + if let bot = component.initialData.bot, let botPeer = component.initialData.botPeer, let addressName = botPeer.addressName { + self.botResolutionState = BotResolutionState(query: addressName, state: .found(peer: botPeer, isInstalled: true)) + self.resetQueryText = addressName.lowercased() + + self.replyToMessages = bot.canReply + + let initialRecipients = bot.recipients + + var mappedCategories = Set() + if initialRecipients.categories.contains(.existingChats) { + mappedCategories.insert(.existingChats) + } + if initialRecipients.categories.contains(.newChats) { + mappedCategories.insert(.newChats) + } + if initialRecipients.categories.contains(.contacts) { + mappedCategories.insert(.contacts) + } + if initialRecipients.categories.contains(.nonContacts) { + mappedCategories.insert(.nonContacts) + } + + var additionalPeers: [AdditionalPeerList.Peer] = [] + for peerId in initialRecipients.additionalPeers { + if let peer = component.initialData.additionalPeers[peerId] { + additionalPeers.append(peer) + } + } + + self.additionalPeerList = AdditionalPeerList( + categories: mappedCategories, + peers: additionalPeers + ) + + self.hasAccessToAllChatsByDefault = initialRecipients.exclude + } + } + let environment = environment[EnvironmentType.self].value let themeUpdated = self.environment?.theme !== environment.theme self.environment = environment @@ -169,11 +479,10 @@ final class ChatbotSetupScreenComponent: Component { let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } - //TODO:localize let navigationTitleSize = self.navigationTitle.update( transition: transition, component: AnyComponent(MultilineTextComponent( - text: .plain(NSAttributedString(string: "Chatbots", font: Font.semibold(17.0), textColor: environment.theme.rootController.navigationBar.primaryTextColor)), + text: .plain(NSAttributedString(string: environment.strings.ChatbotSetup_Title, font: Font.semibold(17.0), textColor: environment.theme.rootController.navigationBar.primaryTextColor)), horizontalAlignment: .center )), environment: {}, @@ -204,15 +513,16 @@ final class ChatbotSetupScreenComponent: Component { transition: .immediate, component: AnyComponent(LottieComponent( content: LottieComponent.AppBundleContent(name: "BotEmoji"), - loop: true + loop: false )), environment: {}, containerSize: CGSize(width: 100.0, height: 100.0) ) - let iconFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - iconSize.width) * 0.5), y: contentHeight + 2.0), size: iconSize) - if let iconView = self.icon.view { + let iconFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - iconSize.width) * 0.5), y: contentHeight + 8.0), size: iconSize) + if let iconView = self.icon.view as? LottieComponent.View { if iconView.superview == nil { self.scrollView.addSubview(iconView) + iconView.playOnce() } transition.setPosition(view: iconView, position: iconFrame.center) iconView.bounds = CGRect(origin: CGPoint(), size: iconFrame.size) @@ -220,8 +530,7 @@ final class ChatbotSetupScreenComponent: Component { contentHeight += 129.0 - //TODO:localize - let subtitleString = NSMutableAttributedString(attributedString: parseMarkdownIntoAttributedString("Add a bot to your account to help you automatically process and respond to the messages you receive. [Learn More>]()", attributes: MarkdownAttributes( + let subtitleString = NSMutableAttributedString(attributedString: parseMarkdownIntoAttributedString(environment.strings.ChatbotSetup_Text, attributes: MarkdownAttributes( body: MarkdownAttributeSet(font: Font.regular(15.0), textColor: environment.theme.list.freeTextColor), bold: MarkdownAttributeSet(font: Font.semibold(15.0), textColor: environment.theme.list.freeTextColor), link: MarkdownAttributeSet(font: Font.regular(15.0), textColor: environment.theme.list.itemAccentColor), @@ -236,10 +545,9 @@ final class ChatbotSetupScreenComponent: Component { subtitleString.addAttribute(.attachment, value: chevronImage, range: NSRange(range, in: subtitleString.string)) } - //TODO:localize let subtitleSize = self.subtitle.update( transition: .immediate, - component: AnyComponent(MultilineTextComponent( + component: AnyComponent(BalancedTextComponent( text: .plain(subtitleString), horizontalAlignment: .center, maximumNumberOfLines: 0, @@ -253,10 +561,10 @@ final class ChatbotSetupScreenComponent: Component { } }, tapAction: { [weak self] _, _ in - guard let self, let component = self.component else { + guard let self, let component = self.component, let environment = self.environment else { return } - let _ = component + component.context.sharedContext.applicationBindings.openUrl(environment.strings.ChatbotSetup_TextLink) } )), environment: {}, @@ -273,7 +581,69 @@ final class ChatbotSetupScreenComponent: Component { contentHeight += subtitleSize.height contentHeight += 27.0 - //TODO:localize + let resetQueryText = self.resetQueryText + self.resetQueryText = nil + var nameSectionItems: [AnyComponentWithIdentity] = [] + nameSectionItems.append(AnyComponentWithIdentity(id: 0, component: AnyComponent(ListTextFieldItemComponent( + theme: environment.theme, + initialText: "", + resetText: resetQueryText.flatMap { ListTextFieldItemComponent.ResetText(value: $0) }, + placeholder: environment.strings.ChatbotSetup_BotSearchPlaceholder, + autocapitalizationType: .none, + autocorrectionType: .no, + updated: { [weak self] value in + guard let self else { + return + } + self.updateBotQuery(query: value) + }, + tag: self.textFieldTag + )))) + if let botResolutionState = self.botResolutionState { + let mappedContent: ChatbotSearchResultItemComponent.Content + switch botResolutionState.state { + case .searching: + mappedContent = .searching + case .notFound: + mappedContent = .notFound + case let .found(peer, isInstalled): + mappedContent = .found(peer: peer, isInstalled: isInstalled) + } + nameSectionItems.append(AnyComponentWithIdentity(id: 1, component: AnyComponent(ChatbotSearchResultItemComponent( + context: component.context, + theme: environment.theme, + strings: environment.strings, + content: mappedContent, + installAction: { [weak self] in + guard let self else { + return + } + if var botResolutionState = self.botResolutionState, case let .found(peer, isInstalled) = botResolutionState.state, !isInstalled { + botResolutionState.state = .found(peer: peer, isInstalled: true) + self.botResolutionState = botResolutionState + self.state?.updated(transition: .spring(duration: 0.3)) + } + }, + removeAction: { [weak self] in + guard let self else { + return + } + if let botResolutionState = self.botResolutionState, case let .found(_, isInstalled) = botResolutionState.state, isInstalled { + self.botResolutionState = nil + if let botResolutionDisposable = self.botResolutionDisposable { + self.botResolutionDisposable = nil + botResolutionDisposable.dispose() + } + + if let textFieldView = self.nameSection.findTaggedView(tag: self.textFieldTag) as? ListTextFieldItemComponent.View { + textFieldView.setText(text: "", updateState: false) + } + self.state?.updated(transition: .spring(duration: 0.3)) + } + } + )))) + } + let nameSectionSize = self.nameSection.update( transition: transition, component: AnyComponent(ListSectionComponent( @@ -281,21 +651,13 @@ final class ChatbotSetupScreenComponent: Component { header: nil, footer: AnyComponent(MultilineTextComponent( text: .plain(NSAttributedString( - string: "Enter the username or URL of the Telegram bot that you want to automatically process your chats.", + string: environment.strings.ChatbotSetup_BotSectionFooter, font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize), textColor: environment.theme.list.freeTextColor )), maximumNumberOfLines: 0 )), - items: [ - AnyComponentWithIdentity(id: 0, component: AnyComponent(ListTextFieldItemComponent( - theme: environment.theme, - initialText: "", - placeholder: "Bot Username", - updated: { value in - } - ))) - ] + items: nameSectionItems )), environment: {}, containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 10000.0) @@ -310,14 +672,13 @@ final class ChatbotSetupScreenComponent: Component { contentHeight += nameSectionSize.height contentHeight += sectionSpacing - //TODO:localize let accessSectionSize = self.accessSection.update( transition: transition, component: AnyComponent(ListSectionComponent( theme: environment.theme, header: AnyComponent(MultilineTextComponent( text: .plain(NSAttributedString( - string: "CHATS ACCESSIBLE FOR THE BOT", + string: environment.strings.ChatbotSetup_RecipientsSectionHeader, font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize), textColor: environment.theme.list.freeTextColor )), @@ -330,7 +691,7 @@ final class ChatbotSetupScreenComponent: Component { title: AnyComponent(VStack([ AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent( text: .plain(NSAttributedString( - string: "All 1-to-1 Chats Except...", + string: environment.strings.BusinessMessageSetup_RecipientsOptionAllExcept, font: Font.regular(presentationData.listsFontSize.baseDisplaySize), textColor: environment.theme.list.itemPrimaryTextColor )), @@ -339,11 +700,20 @@ final class ChatbotSetupScreenComponent: Component { ], alignment: .left, spacing: 2.0)), leftIcon: AnyComponentWithIdentity(id: 0, component: AnyComponent(Image( image: checkIcon, - tintColor: environment.theme.list.itemAccentColor, + tintColor: !self.hasAccessToAllChatsByDefault ? .clear : environment.theme.list.itemAccentColor, contentMode: .center ))), accessory: nil, - action: { _ in + action: { [weak self] _ in + guard let self else { + return + } + if !self.hasAccessToAllChatsByDefault { + self.hasAccessToAllChatsByDefault = true + self.additionalPeerList.categories.removeAll() + self.additionalPeerList.peers.removeAll() + self.state?.updated(transition: .immediate) + } } ))), AnyComponentWithIdentity(id: 1, component: AnyComponent(ListActionItemComponent( @@ -351,7 +721,7 @@ final class ChatbotSetupScreenComponent: Component { title: AnyComponent(VStack([ AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent( text: .plain(NSAttributedString( - string: "Only Selected Chats", + string: environment.strings.BusinessMessageSetup_RecipientsOptionOnly, font: Font.regular(presentationData.listsFontSize.baseDisplaySize), textColor: environment.theme.list.itemPrimaryTextColor )), @@ -360,11 +730,20 @@ final class ChatbotSetupScreenComponent: Component { ], alignment: .left, spacing: 2.0)), leftIcon: AnyComponentWithIdentity(id: 0, component: AnyComponent(Image( image: checkIcon, - tintColor: .clear, + tintColor: self.hasAccessToAllChatsByDefault ? .clear : environment.theme.list.itemAccentColor, contentMode: .center ))), accessory: nil, - action: { _ in + action: { [weak self] _ in + guard let self else { + return + } + if self.hasAccessToAllChatsByDefault { + self.hasAccessToAllChatsByDefault = false + self.additionalPeerList.categories.removeAll() + self.additionalPeerList.peers.removeAll() + self.state?.updated(transition: .immediate) + } } ))) ] @@ -382,49 +761,149 @@ final class ChatbotSetupScreenComponent: Component { contentHeight += accessSectionSize.height contentHeight += sectionSpacing - //TODO:localize + var excludedSectionItems: [AnyComponentWithIdentity] = [] + excludedSectionItems.append(AnyComponentWithIdentity(id: 0, component: AnyComponent(ListActionItemComponent( + theme: environment.theme, + title: AnyComponent(VStack([ + AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: self.hasAccessToAllChatsByDefault ? environment.strings.BusinessMessageSetup_Recipients_AddExclude : environment.strings.BusinessMessageSetup_Recipients_AddInclude, + font: Font.regular(presentationData.listsFontSize.baseDisplaySize), + textColor: environment.theme.list.itemAccentColor + )), + maximumNumberOfLines: 1 + ))), + ], alignment: .left, spacing: 2.0)), + leftIcon: AnyComponentWithIdentity(id: 0, component: AnyComponent(BundleIconComponent( + name: "Chat List/AddIcon", + tintColor: environment.theme.list.itemAccentColor + ))), + accessory: nil, + action: { [weak self] _ in + guard let self else { + return + } + self.openAdditionalPeerListSetup() + } + )))) + for category in self.additionalPeerList.categories.sorted(by: { $0.rawValue < $1.rawValue }) { + let title: String + let icon: String + let color: AvatarBackgroundColor + switch category { + case .newChats: + title = environment.strings.BusinessMessageSetup_Recipients_CategoryNewChats + icon = "Chat List/Filters/NewChats" + color = .purple + case .existingChats: + title = environment.strings.BusinessMessageSetup_Recipients_CategoryExistingChats + icon = "Chat List/Filters/Chats" + color = .purple + case .contacts: + title = environment.strings.BusinessMessageSetup_Recipients_CategoryContacts + icon = "Chat List/Filters/Contact" + color = .blue + case .nonContacts: + title = environment.strings.BusinessMessageSetup_Recipients_CategoryNonContacts + icon = "Chat List/Filters/User" + color = .yellow + } + excludedSectionItems.append(AnyComponentWithIdentity(id: category, component: AnyComponent(PeerListItemComponent( + context: component.context, + theme: environment.theme, + strings: environment.strings, + style: .generic, + sideInset: 0.0, + title: title, + avatar: PeerListItemComponent.Avatar( + icon: icon, + color: color, + clipStyle: .roundedRect + ), + peer: nil, + subtitle: nil, + subtitleAccessory: .none, + presence: nil, + selectionState: .none, + hasNext: false, + action: { peer, _, _ in + }, + inlineActions: PeerListItemComponent.InlineActionsState( + actions: [PeerListItemComponent.InlineAction( + id: AnyHashable(0), + title: environment.strings.Common_Delete, + color: .destructive, + action: { [weak self] in + guard let self else { + return + } + self.additionalPeerList.categories.remove(category) + self.state?.updated(transition: .spring(duration: 0.4)) + } + )] + ) + )))) + } + for peer in self.additionalPeerList.peers { + excludedSectionItems.append(AnyComponentWithIdentity(id: peer.peer.id, component: AnyComponent(PeerListItemComponent( + context: component.context, + theme: environment.theme, + strings: environment.strings, + style: .generic, + sideInset: 0.0, + title: peer.peer.displayTitle(strings: environment.strings, displayOrder: .firstLast), + peer: peer.peer, + subtitle: peer.isContact ? environment.strings.ChatList_PeerTypeContact : environment.strings.ChatList_PeerTypeNonContact, + subtitleAccessory: .none, + presence: nil, + selectionState: .none, + hasNext: false, + action: { peer, _, _ in + }, + inlineActions: PeerListItemComponent.InlineActionsState( + actions: [PeerListItemComponent.InlineAction( + id: AnyHashable(0), + title: environment.strings.Common_Delete, + color: .destructive, + action: { [weak self] in + guard let self else { + return + } + self.additionalPeerList.peers.removeAll(where: { $0.peer.id == peer.peer.id }) + self.state?.updated(transition: .spring(duration: 0.4)) + } + )] + ) + )))) + } + let excludedSectionSize = self.excludedSection.update( transition: transition, component: AnyComponent(ListSectionComponent( theme: environment.theme, header: AnyComponent(MultilineTextComponent( text: .plain(NSAttributedString( - string: "EXCLUDED CHATS", + string: self.hasAccessToAllChatsByDefault ? environment.strings.BusinessMessageSetup_Recipients_ExcludedSectionHeader : environment.strings.BusinessMessageSetup_Recipients_IncludedSectionHeader, font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize), textColor: environment.theme.list.freeTextColor )), maximumNumberOfLines: 0 )), footer: AnyComponent(MultilineTextComponent( - text: .plain(NSAttributedString( - string: "Select chats or entire chat categories which the bot WILL NOT have access to.", - font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize), - textColor: environment.theme.list.freeTextColor - )), + text: .markdown( + text: self.hasAccessToAllChatsByDefault ? environment.strings.ChatbotSetup_Recipients_ExcludedSectionFooter : environment.strings.ChatbotSetup_Recipients_IncludedSectionFooter, + attributes: MarkdownAttributes( + body: MarkdownAttributeSet(font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize), textColor: environment.theme.list.freeTextColor), + bold: MarkdownAttributeSet(font: Font.semibold(presentationData.listsFontSize.itemListBaseHeaderFontSize), textColor: environment.theme.list.freeTextColor), + link: MarkdownAttributeSet(font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize), textColor: environment.theme.list.itemAccentColor), + linkAttribute: { _ in + return nil + } + ) + ), maximumNumberOfLines: 0 )), - items: [ - AnyComponentWithIdentity(id: 0, component: AnyComponent(ListActionItemComponent( - theme: environment.theme, - title: AnyComponent(VStack([ - AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent( - text: .plain(NSAttributedString( - string: "Exclude Chats...", - font: Font.regular(presentationData.listsFontSize.baseDisplaySize), - textColor: environment.theme.list.itemAccentColor - )), - maximumNumberOfLines: 1 - ))), - ], alignment: .left, spacing: 2.0)), - leftIcon: AnyComponentWithIdentity(id: 0, component: AnyComponent(BundleIconComponent( - name: "Chat List/AddIcon", - tintColor: environment.theme.list.itemAccentColor - ))), - accessory: nil, - action: { _ in - } - ))), - ] + items: excludedSectionItems )), environment: {}, containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 10000.0) @@ -439,14 +918,13 @@ final class ChatbotSetupScreenComponent: Component { contentHeight += excludedSectionSize.height contentHeight += sectionSpacing - //TODO:localize let permissionsSectionSize = self.permissionsSection.update( transition: transition, component: AnyComponent(ListSectionComponent( theme: environment.theme, header: AnyComponent(MultilineTextComponent( text: .plain(NSAttributedString( - string: "BOT PERMISSIONS", + string: environment.strings.ChatbotSetup_PermissionsSectionHeader, font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize), textColor: environment.theme.list.freeTextColor )), @@ -454,7 +932,7 @@ final class ChatbotSetupScreenComponent: Component { )), footer: AnyComponent(MultilineTextComponent( text: .plain(NSAttributedString( - string: "The bot will be able to view all new incoming messages, but not the messages that had been sent before you added the bot.", + string: environment.strings.ChatbotSetup_PermissionsSectionFooter, font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize), textColor: environment.theme.list.freeTextColor )), @@ -466,14 +944,20 @@ final class ChatbotSetupScreenComponent: Component { title: AnyComponent(VStack([ AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent( text: .plain(NSAttributedString( - string: "Reply to Messages", + string: environment.strings.ChatbotSetup_Permission_ReplyToMessages, font: Font.regular(presentationData.listsFontSize.baseDisplaySize), textColor: environment.theme.list.itemPrimaryTextColor )), maximumNumberOfLines: 1 ))), ], alignment: .left, spacing: 2.0)), - accessory: .toggle(true), + accessory: .toggle(ListActionItemComponent.Toggle(style: .icons, isOn: self.replyToMessages, action: { [weak self] _ in + guard let self else { + return + } + self.replyToMessages = !self.replyToMessages + self.state?.updated(transition: .spring(duration: 0.4)) + })), action: nil ))), ] @@ -533,13 +1017,30 @@ final class ChatbotSetupScreenComponent: Component { } public final class ChatbotSetupScreen: ViewControllerComponentContainer { + public final class InitialData: ChatbotSetupScreenInitialData { + fileprivate let bot: TelegramAccountConnectedBot? + fileprivate let botPeer: EnginePeer? + fileprivate let additionalPeers: [EnginePeer.Id: ChatbotSetupScreenComponent.AdditionalPeerList.Peer] + + fileprivate init( + bot: TelegramAccountConnectedBot?, + botPeer: EnginePeer?, + additionalPeers: [EnginePeer.Id: ChatbotSetupScreenComponent.AdditionalPeerList.Peer] + ) { + self.bot = bot + self.botPeer = botPeer + self.additionalPeers = additionalPeers + } + } + private let context: AccountContext - public init(context: AccountContext) { + public init(context: AccountContext, initialData: InitialData) { self.context = context super.init(context: context, component: ChatbotSetupScreenComponent( - context: context + context: context, + initialData: initialData ), navigationBarAppearance: .default, theme: .default, updatedPresentationData: nil) let presentationData = context.sharedContext.currentPresentationData.with { $0 } @@ -576,4 +1077,48 @@ public final class ChatbotSetupScreen: ViewControllerComponentContainer { override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { super.containerLayoutUpdated(layout, transition: transition) } + + public static func initialData(context: AccountContext) -> Signal { + return context.engine.data.get( + TelegramEngine.EngineData.Item.Peer.BusinessConnectedBot(id: context.account.peerId) + ) + |> mapToSignal { connectedBot -> Signal in + guard let connectedBot else { + return .single( + InitialData( + bot: nil, + botPeer: nil, + additionalPeers: [:] + ) + ) + } + + var additionalPeerIds = Set() + additionalPeerIds.formUnion(connectedBot.recipients.additionalPeers) + + return context.engine.data.get( + TelegramEngine.EngineData.Item.Peer.Peer(id: connectedBot.id), + EngineDataMap(additionalPeerIds.map(TelegramEngine.EngineData.Item.Peer.Peer.init(id:))), + EngineDataMap(additionalPeerIds.map(TelegramEngine.EngineData.Item.Peer.IsContact.init(id:))) + ) + |> map { botPeer, peers, isContacts -> ChatbotSetupScreenInitialData in + var additionalPeers: [EnginePeer.Id: ChatbotSetupScreenComponent.AdditionalPeerList.Peer] = [:] + for id in additionalPeerIds { + guard let peer = peers[id], let peer else { + continue + } + additionalPeers[id] = ChatbotSetupScreenComponent.AdditionalPeerList.Peer( + peer: peer, + isContact: isContacts[id] ?? false + ) + } + + return InitialData( + bot: connectedBot, + botPeer: botPeer, + additionalPeers: additionalPeers + ) + } + } + } } diff --git a/submodules/TelegramUI/Components/Settings/PeerNameColorItem/Sources/PeerNameColorItem.swift b/submodules/TelegramUI/Components/Settings/PeerNameColorItem/Sources/PeerNameColorItem.swift index 0df4e747c14..95cfb9911da 100644 --- a/submodules/TelegramUI/Components/Settings/PeerNameColorItem/Sources/PeerNameColorItem.swift +++ b/submodules/TelegramUI/Components/Settings/PeerNameColorItem/Sources/PeerNameColorItem.swift @@ -13,33 +13,35 @@ import AccountContext import ListItemComponentAdaptor private class PeerNameColorIconItem { - let index: PeerNameColor - let colors: PeerNameColors.Colors + let index: PeerNameColor? + let colors: PeerNameColors.Colors? let isDark: Bool let selected: Bool - let action: (PeerNameColor) -> Void + let isLocked: Bool + let action: (PeerNameColor?) -> Void - public init(index: PeerNameColor, colors: PeerNameColors.Colors, isDark: Bool, selected: Bool, action: @escaping (PeerNameColor) -> Void) { + public init(index: PeerNameColor?, colors: PeerNameColors.Colors?, isDark: Bool, selected: Bool, isLocked: Bool, action: @escaping (PeerNameColor?) -> Void) { self.index = index self.colors = colors self.isDark = isDark self.selected = selected + self.isLocked = isLocked self.action = action } } -private func generateRingImage(nameColor: PeerNameColors.Colors, size: CGSize = CGSize(width: 40.0, height: 40.0)) -> UIImage? { +private func generateRingImage(color: UIColor, size: CGSize = CGSize(width: 40.0, height: 40.0)) -> UIImage? { return generateImage(size, rotatedContext: { size, context in let bounds = CGRect(origin: CGPoint(), size: size) context.clear(bounds) - context.setStrokeColor(nameColor.main.cgColor) + context.setStrokeColor(color.cgColor) context.setLineWidth(2.0) context.strokeEllipse(in: bounds.insetBy(dx: 1.0, dy: 1.0)) }) } -public func generatePeerNameColorImage(nameColor: PeerNameColors.Colors, isDark: Bool, bounds: CGSize = CGSize(width: 40.0, height: 40.0), size: CGSize = CGSize(width: 40.0, height: 40.0)) -> UIImage? { +public func generatePeerNameColorImage(nameColor: PeerNameColors.Colors?, isDark: Bool, isLocked: Bool = false, isEmpty: Bool = false, bounds: CGSize = CGSize(width: 40.0, height: 40.0), size: CGSize = CGSize(width: 40.0, height: 40.0)) -> UIImage? { return generateImage(bounds, rotatedContext: { contextSize, context in let bounds = CGRect(origin: CGPoint(), size: contextSize) context.clear(bounds) @@ -48,7 +50,7 @@ public func generatePeerNameColorImage(nameColor: PeerNameColors.Colors, isDark: context.addEllipse(in: circleBounds) context.clip() - if let secondColor = nameColor.secondary { + if let nameColor, let secondColor = nameColor.secondary { var firstColor = nameColor.main var secondColor = secondColor if isDark, nameColor.tertiary == nil { @@ -84,9 +86,51 @@ public func generatePeerNameColorImage(nameColor: PeerNameColors.Colors, isDark: context.setFillColor(firstColor.cgColor) context.fillPath() } - } else { + } else if let nameColor { context.setFillColor(nameColor.main.cgColor) context.fill(circleBounds) + } else { + context.setFillColor(UIColor(rgb: 0x798896).cgColor) + context.fill(circleBounds) + } + + if isLocked { + if let image = UIImage(bundleImageName: "Chat/Input/Accessory Panels/TextLockIcon") { + let scaleFactor: CGFloat = 1.58 + let imageSize = CGSize(width: floor(image.size.width * scaleFactor), height: floor(image.size.height * scaleFactor)) + var imageFrame = CGRect(origin: CGPoint(x: circleBounds.minX + floor((circleBounds.width - imageSize.width) * 0.5), y: circleBounds.minY + floor((circleBounds.height - imageSize.height) * 0.5)), size: imageSize) + imageFrame.origin.y += -0.5 + + context.translateBy(x: imageFrame.midX, y: imageFrame.midY) + context.scaleBy(x: 1.0, y: -1.0) + context.translateBy(x: -imageFrame.midX, y: -imageFrame.midY) + + if let cgImage = image.cgImage { + context.clip(to: imageFrame, mask: cgImage) + context.setFillColor(UIColor.clear.cgColor) + context.setBlendMode(.copy) + context.fill(imageFrame) + } + } + } else if isEmpty { + if let image = UIImage(bundleImageName: "Chat/Message/SideCloseIcon") { + let scaleFactor: CGFloat = 1.0 + let imageSize = CGSize(width: floor(image.size.width * scaleFactor), height: floor(image.size.height * scaleFactor)) + var imageFrame = CGRect(origin: CGPoint(x: circleBounds.minX + floor((circleBounds.width - imageSize.width) * 0.5), y: circleBounds.minY + floor((circleBounds.height - imageSize.height) * 0.5)), size: imageSize) + imageFrame.origin.y += 0.5 + imageFrame.origin.x += 0.5 + + context.translateBy(x: imageFrame.midX, y: imageFrame.midY) + context.scaleBy(x: 1.0, y: -1.0) + context.translateBy(x: -imageFrame.midX, y: -imageFrame.midY) + + if let cgImage = image.cgImage { + context.clip(to: imageFrame, mask: cgImage) + context.setFillColor(UIColor.clear.cgColor) + context.setBlendMode(.copy) + context.fill(imageFrame) + } + } } }) } @@ -187,8 +231,8 @@ private final class PeerNameColorIconItemNode : ASDisplayNode { self.item = item if updatedAccentColor { - self.fillNode.image = generatePeerNameColorImage(nameColor: item.colors, isDark: item.isDark, bounds: size, size: size) - self.ringNode.image = generateRingImage(nameColor: item.colors, size: size) + self.fillNode.image = generatePeerNameColorImage(nameColor: item.colors, isDark: item.isDark, isLocked: item.selected && item.isLocked, isEmpty: item.colors == nil, bounds: size, size: size) + self.ringNode.image = generateRingImage(color: item.colors?.main ?? UIColor(rgb: 0x798896), size: size) } let center = CGPoint(x: size.width / 2.0, y: size.height / 2.0) @@ -208,19 +252,29 @@ private final class PeerNameColorIconItemNode : ASDisplayNode { } public final class PeerNameColorItem: ListViewItem, ItemListItem, ListItemComponentAdaptor.ItemGenerator { + public enum Mode { + case name + case profile + case folderTag + } + public var sectionId: ItemListSectionId public let theme: PresentationTheme public let colors: PeerNameColors - public let isProfile: Bool + public let mode: Mode + public let displayEmptyColor: Bool + public let isLocked: Bool public let currentColor: PeerNameColor? - public let updated: (PeerNameColor) -> Void + public let updated: (PeerNameColor?) -> Void public let tag: ItemListItemTag? - public init(theme: PresentationTheme, colors: PeerNameColors, isProfile: Bool, currentColor: PeerNameColor?, updated: @escaping (PeerNameColor) -> Void, tag: ItemListItemTag? = nil, sectionId: ItemListSectionId) { + public init(theme: PresentationTheme, colors: PeerNameColors, mode: Mode, displayEmptyColor: Bool = false, currentColor: PeerNameColor?, isLocked: Bool = false, updated: @escaping (PeerNameColor?) -> Void, tag: ItemListItemTag? = nil, sectionId: ItemListSectionId) { self.theme = theme self.colors = colors - self.isProfile = isProfile + self.mode = mode + self.displayEmptyColor = displayEmptyColor + self.isLocked = isLocked self.currentColor = currentColor self.updated = updated self.tag = tag @@ -271,7 +325,7 @@ public final class PeerNameColorItem: ListViewItem, ItemListItem, ListItemCompon if lhs.colors != rhs.colors { return false } - if lhs.isProfile != rhs.isProfile { + if lhs.mode != rhs.mode { return false } if lhs.currentColor != rhs.currentColor { @@ -334,15 +388,24 @@ public final class PeerNameColorItemNode: ListViewItemNode, ItemListItemNode { let itemsPerRow: Int let displayOrder: [Int32] - if item.isProfile { - displayOrder = item.colors.profileDisplayOrder - itemsPerRow = 8 - } else { + switch item.mode { + case .name: displayOrder = item.colors.displayOrder itemsPerRow = 7 + case .profile: + displayOrder = item.colors.profileDisplayOrder + itemsPerRow = 8 + case .folderTag: + displayOrder = item.colors.chatFolderTagDisplayOrder + itemsPerRow = 8 + } + + var numItems = displayOrder.count + if item.displayEmptyColor { + numItems += 1 } - let rowsCount = ceil(CGFloat(displayOrder.count) / CGFloat(itemsPerRow)) + let rowsCount = ceil(CGFloat(numItems) / CGFloat(itemsPerRow)) contentSize = CGSize(width: params.width, height: 48.0 * rowsCount) insets = itemListNeighborsGroupedInsets(neighbors, params) @@ -419,22 +482,30 @@ public final class PeerNameColorItemNode: ListViewItemNode, ItemListItemNode { strongSelf.maskNode.frame = strongSelf.backgroundNode.frame.insetBy(dx: params.leftInset, dy: 0.0) strongSelf.topStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: layoutSize.width, height: separatorHeight)) - let action: (PeerNameColor) -> Void = { color in + let action: (PeerNameColor?) -> Void = { color in item.updated(color) } var items: [PeerNameColorIconItem] = [] var i: Int = 0 + for index in displayOrder { let color = PeerNameColor(rawValue: index) let colors: PeerNameColors.Colors - if item.isProfile { - colors = item.colors.getProfile(color, dark: item.theme.overallDarkAppearance, subject: .palette) - } else { + switch item.mode { + case .name: colors = item.colors.get(color, dark: item.theme.overallDarkAppearance) + case .profile: + colors = item.colors.getProfile(color, dark: item.theme.overallDarkAppearance, subject: .palette) + case .folderTag: + colors = item.colors.getChatFolderTag(color, dark: item.theme.overallDarkAppearance) } - items.append(PeerNameColorIconItem(index: color, colors: colors, isDark: item.theme.overallDarkAppearance, selected: color == item.currentColor, action: action)) + items.append(PeerNameColorIconItem(index: color, colors: colors, isDark: item.theme.overallDarkAppearance, selected: color == item.currentColor, isLocked: item.isLocked, action: action)) + i += 1 + } + if item.displayEmptyColor { + items.append(PeerNameColorIconItem(index: nil, colors: nil, isDark: item.theme.overallDarkAppearance, selected: item.currentColor == nil, isLocked: item.isLocked, action: action)) i += 1 } strongSelf.items = items @@ -442,18 +513,24 @@ public final class PeerNameColorItemNode: ListViewItemNode, ItemListItemNode { let sideInset: CGFloat = params.leftInset + 10.0 let iconSize = CGSize(width: 32.0, height: 32.0) - let spacing = (params.width - sideInset * 2.0 - iconSize.width * CGFloat(itemsPerRow)) / CGFloat(itemsPerRow - 1) + let spacing = floorToScreenPixels((params.width - sideInset * 2.0 - iconSize.width * CGFloat(itemsPerRow)) / CGFloat(itemsPerRow - 1)) var origin = CGPoint(x: sideInset, y: 10.0) i = 0 for item in items { let iconItemNode: PeerNameColorIconItemNode - if let current = strongSelf.itemNodes[item.index.rawValue] { + let indexKey: Int32 + if let index = item.index { + indexKey = index.rawValue + } else { + indexKey = Int32.min + } + if let current = strongSelf.itemNodes[indexKey] { iconItemNode = current } else { iconItemNode = PeerNameColorIconItemNode() - strongSelf.itemNodes[item.index.rawValue] = iconItemNode + strongSelf.itemNodes[indexKey] = iconItemNode strongSelf.containerNode.addSubnode(iconItemNode) } diff --git a/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/ChannelAppearanceScreen.swift b/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/ChannelAppearanceScreen.swift index 1d7f49bec23..e805cbd4b79 100644 --- a/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/ChannelAppearanceScreen.swift +++ b/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/ChannelAppearanceScreen.swift @@ -1261,10 +1261,10 @@ final class ChannelAppearanceScreenComponent: Component { itemGenerator: PeerNameColorItem( theme: environment.theme, colors: component.context.peerNameColors, - isProfile: true, + mode: .profile, currentColor: profileColor, updated: { [weak self] value in - guard let self else { + guard let self, let value else { return } self.updatedPeerProfileColor = value @@ -1277,14 +1277,14 @@ final class ChannelAppearanceScreenComponent: Component { AnyComponentWithIdentity(id: 2, component: AnyComponent(ListActionItemComponent( theme: environment.theme, title: AnyComponent(HStack(profileLogoContents, spacing: 6.0)), - icon: AnyComponentWithIdentity(id: 0, component: AnyComponent(EmojiActionIconComponent( + icon: ListActionItemComponent.Icon(component: AnyComponentWithIdentity(id: 0, component: AnyComponent(EmojiActionIconComponent( context: component.context, color: profileColor.flatMap { profileColor in component.context.peerNameColors.getProfile(profileColor, dark: environment.theme.overallDarkAppearance, subject: .palette).main } ?? environment.theme.list.itemAccentColor, fileId: backgroundFileId, file: backgroundFileId.flatMap { self.cachedIconFiles[$0] } - ))), + )))), action: { [weak self] view in guard let self, let resolvedState = self.resolveState(), let view = view as? ListActionItemComponent.View, let iconView = view.iconView else { return @@ -1406,12 +1406,12 @@ final class ChannelAppearanceScreenComponent: Component { AnyComponentWithIdentity(id: 0, component: AnyComponent(ListActionItemComponent( theme: environment.theme, title: AnyComponent(HStack(emojiPackContents, spacing: 6.0)), - icon: AnyComponentWithIdentity(id: 0, component: AnyComponent(EmojiActionIconComponent( + icon: ListActionItemComponent.Icon(component: AnyComponentWithIdentity(id: 0, component: AnyComponent(EmojiActionIconComponent( context: component.context, color: environment.theme.list.itemAccentColor, fileId: emojiPack?.thumbnailFileId, file: emojiPackFile - ))), + )))), action: { [weak self] view in guard let self, let resolvedState = self.resolveState() else { return @@ -1470,12 +1470,12 @@ final class ChannelAppearanceScreenComponent: Component { AnyComponentWithIdentity(id: 0, component: AnyComponent(ListActionItemComponent( theme: environment.theme, title: AnyComponent(HStack(emojiStatusContents, spacing: 6.0)), - icon: AnyComponentWithIdentity(id: 0, component: AnyComponent(EmojiActionIconComponent( + icon: ListActionItemComponent.Icon(component: AnyComponentWithIdentity(id: 0, component: AnyComponent(EmojiActionIconComponent( context: component.context, color: environment.theme.list.itemAccentColor, fileId: statusFileId, file: statusFileId.flatMap { self.cachedIconFiles[$0] } - ))), + )))), action: { [weak self] view in guard let self, let resolvedState = self.resolveState(), let view = view as? ListActionItemComponent.View, let iconView = view.iconView else { return @@ -1578,10 +1578,10 @@ final class ChannelAppearanceScreenComponent: Component { itemGenerator: PeerNameColorItem( theme: environment.theme, colors: component.context.peerNameColors, - isProfile: false, + mode: .name, currentColor: resolvedState.nameColor, updated: { [weak self] value in - guard let self else { + guard let self, let value else { return } self.updatedPeerNameColor = value @@ -1594,12 +1594,12 @@ final class ChannelAppearanceScreenComponent: Component { AnyComponentWithIdentity(id: 2, component: AnyComponent(ListActionItemComponent( theme: environment.theme, title: AnyComponent(HStack(replyLogoContents, spacing: 6.0)), - icon: AnyComponentWithIdentity(id: 0, component: AnyComponent(EmojiActionIconComponent( + icon: ListActionItemComponent.Icon(component: AnyComponentWithIdentity(id: 0, component: AnyComponent(EmojiActionIconComponent( context: component.context, color: component.context.peerNameColors.get(resolvedState.nameColor, dark: environment.theme.overallDarkAppearance).main, fileId: replyFileId, file: replyFileId.flatMap { self.cachedIconFiles[$0] } - ))), + )))), action: { [weak self] view in guard let self, let resolvedState = self.resolveState(), let view = view as? ListActionItemComponent.View, let iconView = view.iconView else { return diff --git a/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/PeerNameColorScreen.swift b/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/PeerNameColorScreen.swift index 945edb7b487..72cfb99bc27 100644 --- a/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/PeerNameColorScreen.swift +++ b/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/PeerNameColorScreen.swift @@ -212,10 +212,12 @@ private enum PeerNameColorScreenEntry: ItemListNodeEntry { return PeerNameColorItem( theme: presentationData.theme, colors: colors, - isProfile: isProfile, + mode: isProfile ? .profile : .name, currentColor: currentColor, updated: { color in - arguments.updateNameColor(color) + if let color { + arguments.updateNameColor(color) + } }, sectionId: self.section ) diff --git a/submodules/TelegramUI/Components/Settings/QuickReplyNameAlertController/BUILD b/submodules/TelegramUI/Components/Settings/QuickReplyNameAlertController/BUILD new file mode 100644 index 00000000000..a9f96d6606b --- /dev/null +++ b/submodules/TelegramUI/Components/Settings/QuickReplyNameAlertController/BUILD @@ -0,0 +1,28 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "QuickReplyNameAlertController", + module_name = "QuickReplyNameAlertController", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit", + "//submodules/AsyncDisplayKit:AsyncDisplayKit", + "//submodules/Display:Display", + "//submodules/Postbox:Postbox", + "//submodules/TelegramCore:TelegramCore", + "//submodules/AccountContext:AccountContext", + "//submodules/TelegramPresentationData:TelegramPresentationData", + "//submodules/ComponentFlow", + "//submodules/Components/MultilineTextComponent", + "//submodules/Components/BalancedTextComponent", + "//submodules/TelegramUI/Components/EmojiStatusComponent", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Components/Settings/QuickReplyNameAlertController/Sources/QuickReplyNameAlertController.swift b/submodules/TelegramUI/Components/Settings/QuickReplyNameAlertController/Sources/QuickReplyNameAlertController.swift new file mode 100644 index 00000000000..9629f3f3bbe --- /dev/null +++ b/submodules/TelegramUI/Components/Settings/QuickReplyNameAlertController/Sources/QuickReplyNameAlertController.swift @@ -0,0 +1,543 @@ +import Foundation +import UIKit +import SwiftSignalKit +import AsyncDisplayKit +import Display +import Postbox +import TelegramCore +import TelegramPresentationData +import AccountContext +import ComponentFlow +import MultilineTextComponent +import BalancedTextComponent +import EmojiStatusComponent + +private final class PromptInputFieldNode: ASDisplayNode, ASEditableTextNodeDelegate { + private var theme: PresentationTheme + private let backgroundNode: ASImageNode + private let textInputNode: EditableTextNode + private let placeholderNode: ASTextNode + private let characterLimitView = ComponentView() + + private let characterLimit: Int + + var updateHeight: (() -> Void)? + var complete: (() -> Void)? + var textChanged: ((String) -> Void)? + + private let backgroundInsets = UIEdgeInsets(top: 8.0, left: 16.0, bottom: 15.0, right: 16.0) + private let inputInsets: UIEdgeInsets + + private let validCharacterSets: [CharacterSet] + + var text: String { + get { + return self.textInputNode.attributedText?.string ?? "" + } + set { + self.textInputNode.attributedText = NSAttributedString(string: newValue, font: Font.regular(13.0), textColor: self.theme.actionSheet.inputTextColor) + self.placeholderNode.isHidden = !newValue.isEmpty + } + } + + var placeholder: String = "" { + didSet { + self.placeholderNode.attributedText = NSAttributedString(string: self.placeholder, font: Font.regular(13.0), textColor: self.theme.actionSheet.inputPlaceholderColor) + } + } + + init(theme: PresentationTheme, placeholder: String, characterLimit: Int) { + self.theme = theme + self.characterLimit = characterLimit + + self.inputInsets = UIEdgeInsets(top: 9.0, left: 6.0, bottom: 9.0, right: 16.0) + + self.backgroundNode = ASImageNode() + self.backgroundNode.isLayerBacked = true + self.backgroundNode.displaysAsynchronously = false + self.backgroundNode.displayWithoutProcessing = true + self.backgroundNode.image = generateStretchableFilledCircleImage(diameter: 16.0, color: theme.actionSheet.inputHollowBackgroundColor, strokeColor: theme.actionSheet.inputBorderColor, strokeWidth: 1.0) + + self.textInputNode = EditableTextNode() + self.textInputNode.typingAttributes = [NSAttributedString.Key.font.rawValue: Font.regular(13.0), NSAttributedString.Key.foregroundColor.rawValue: theme.actionSheet.inputTextColor] + self.textInputNode.clipsToBounds = true + self.textInputNode.hitTestSlop = UIEdgeInsets(top: -5.0, left: -5.0, bottom: -5.0, right: -5.0) + self.textInputNode.textContainerInset = UIEdgeInsets(top: self.inputInsets.top, left: self.inputInsets.left, bottom: self.inputInsets.bottom, right: self.inputInsets.right) + self.textInputNode.keyboardAppearance = theme.rootController.keyboardColor.keyboardAppearance + self.textInputNode.keyboardType = .default + self.textInputNode.autocapitalizationType = .none + self.textInputNode.returnKeyType = .done + self.textInputNode.autocorrectionType = .no + self.textInputNode.tintColor = theme.actionSheet.controlAccentColor + + self.placeholderNode = ASTextNode() + self.placeholderNode.isUserInteractionEnabled = false + self.placeholderNode.displaysAsynchronously = false + self.placeholderNode.attributedText = NSAttributedString(string: placeholder, font: Font.regular(13.0), textColor: self.theme.actionSheet.inputPlaceholderColor) + + self.validCharacterSets = [ + CharacterSet.alphanumerics, + CharacterSet(charactersIn: "0123456789_"), + ] + + super.init() + + self.textInputNode.delegate = self + + self.addSubnode(self.backgroundNode) + self.addSubnode(self.textInputNode) + self.addSubnode(self.placeholderNode) + } + + func updateTheme(_ theme: PresentationTheme) { + self.theme = theme + + self.backgroundNode.image = generateStretchableFilledCircleImage(diameter: 16.0, color: self.theme.actionSheet.inputHollowBackgroundColor, strokeColor: self.theme.actionSheet.inputBorderColor, strokeWidth: 1.0) + self.textInputNode.keyboardAppearance = self.theme.rootController.keyboardColor.keyboardAppearance + self.placeholderNode.attributedText = NSAttributedString(string: self.placeholderNode.attributedText?.string ?? "", font: Font.regular(13.0), textColor: self.theme.actionSheet.inputPlaceholderColor) + self.textInputNode.tintColor = self.theme.actionSheet.controlAccentColor + } + + func updateLayout(width: CGFloat, transition: ContainedViewLayoutTransition) -> CGFloat { + let backgroundInsets = self.backgroundInsets + let inputInsets = self.inputInsets + + let textFieldHeight = self.calculateTextFieldMetrics(width: width) + let panelHeight = textFieldHeight + backgroundInsets.top + backgroundInsets.bottom + + let backgroundFrame = CGRect(origin: CGPoint(x: backgroundInsets.left, y: backgroundInsets.top), size: CGSize(width: width - backgroundInsets.left - backgroundInsets.right, height: panelHeight - backgroundInsets.top - backgroundInsets.bottom)) + transition.updateFrame(node: self.backgroundNode, frame: backgroundFrame) + + let placeholderSize = self.placeholderNode.measure(backgroundFrame.size) + transition.updateFrame(node: self.placeholderNode, frame: CGRect(origin: CGPoint(x: backgroundFrame.minX + inputInsets.left + 5.0, y: backgroundFrame.minY + floor((backgroundFrame.size.height - placeholderSize.height) / 2.0)), size: placeholderSize)) + + transition.updateFrame(node: self.textInputNode, frame: CGRect(origin: CGPoint(x: backgroundFrame.minX + inputInsets.left, y: backgroundFrame.minY), size: CGSize(width: backgroundFrame.size.width - inputInsets.left - inputInsets.right, height: backgroundFrame.size.height))) + + let characterLimitString: String + let characterLimitColor: UIColor + if self.text.count <= self.characterLimit { + let remaining = self.characterLimit - self.text.count + if remaining < 5 { + characterLimitString = "\(remaining)" + } else { + characterLimitString = " " + } + characterLimitColor = self.theme.list.itemPlaceholderTextColor + } else { + characterLimitString = "\(self.characterLimit - self.text.count)" + characterLimitColor = self.theme.list.itemDestructiveColor + } + + let characterLimitSize = self.characterLimitView.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: characterLimitString, font: Font.regular(13.0), textColor: characterLimitColor)) + )), + environment: {}, + containerSize: CGSize(width: 100.0, height: 100.0) + ) + if let characterLimitComponentView = self.characterLimitView.view { + if characterLimitComponentView.superview == nil { + self.view.addSubview(characterLimitComponentView) + } + characterLimitComponentView.frame = CGRect(origin: CGPoint(x: width - 23.0 - characterLimitSize.width, y: 18.0), size: characterLimitSize) + } + + return panelHeight + } + + func activateInput() { + self.textInputNode.becomeFirstResponder() + } + + func deactivateInput() { + self.textInputNode.resignFirstResponder() + } + + @objc func editableTextNodeDidUpdateText(_ editableTextNode: ASEditableTextNode) { + self.updateTextNodeText(animated: true) + self.textChanged?(editableTextNode.textView.text) + self.placeholderNode.isHidden = !(editableTextNode.textView.text ?? "").isEmpty + } + + func editableTextNode(_ editableTextNode: ASEditableTextNode, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool { + if text == "\n" { + self.complete?() + return false + } + if text.unicodeScalars.contains(where: { c in + return !self.validCharacterSets.contains(where: { set in + return set.contains(c) + }) + }) { + return false + } + return true + } + + private func calculateTextFieldMetrics(width: CGFloat) -> CGFloat { + let backgroundInsets = self.backgroundInsets + let inputInsets = self.inputInsets + + let unboundTextFieldHeight = max(34.0, ceil(self.textInputNode.measure(CGSize(width: width - backgroundInsets.left - backgroundInsets.right - inputInsets.left - inputInsets.right, height: CGFloat.greatestFiniteMagnitude)).height)) + + return min(61.0, max(34.0, unboundTextFieldHeight)) + } + + private func updateTextNodeText(animated: Bool) { + let backgroundInsets = self.backgroundInsets + + let textFieldHeight = self.calculateTextFieldMetrics(width: self.bounds.size.width) + + let panelHeight = textFieldHeight + backgroundInsets.top + backgroundInsets.bottom + if !self.bounds.size.height.isEqual(to: panelHeight) { + self.updateHeight?() + } + } + + @objc func clearPressed() { + self.textInputNode.attributedText = nil + self.deactivateInput() + } +} + +public final class QuickReplyNameAlertContentNode: AlertContentNode { + private let context: AccountContext + private var theme: AlertControllerTheme + private let strings: PresentationStrings + private let text: String + private let subtext: String + private let titleFont: PromptControllerTitleFont + + private let textView = ComponentView() + private let subtextView = ComponentView() + + fileprivate let inputFieldNode: PromptInputFieldNode + + private let actionNodesSeparator: ASDisplayNode + private let actionNodes: [TextAlertContentActionNode] + private let actionVerticalSeparators: [ASDisplayNode] + + private let disposable = MetaDisposable() + + private var validLayout: CGSize? + private var errorText: String? + + private let hapticFeedback = HapticFeedback() + + var complete: (() -> Void)? + + override public var dismissOnOutsideTap: Bool { + return self.isUserInteractionEnabled + } + + init(context: AccountContext, theme: AlertControllerTheme, ptheme: PresentationTheme, strings: PresentationStrings, actions: [TextAlertAction], text: String, subtext: String, titleFont: PromptControllerTitleFont, value: String?, characterLimit: Int) { + self.context = context + self.theme = theme + self.strings = strings + self.text = text + self.subtext = subtext + self.titleFont = titleFont + + self.inputFieldNode = PromptInputFieldNode(theme: ptheme, placeholder: strings.QuickReply_ShortcutPlaceholder, characterLimit: characterLimit) + self.inputFieldNode.text = value ?? "" + + self.actionNodesSeparator = ASDisplayNode() + self.actionNodesSeparator.isLayerBacked = true + + self.actionNodes = actions.map { action -> TextAlertContentActionNode in + return TextAlertContentActionNode(theme: theme, action: action) + } + + var actionVerticalSeparators: [ASDisplayNode] = [] + if actions.count > 1 { + for _ in 0 ..< actions.count - 1 { + let separatorNode = ASDisplayNode() + separatorNode.isLayerBacked = true + actionVerticalSeparators.append(separatorNode) + } + } + self.actionVerticalSeparators = actionVerticalSeparators + + super.init() + + self.addSubnode(self.inputFieldNode) + + self.addSubnode(self.actionNodesSeparator) + + for actionNode in self.actionNodes { + self.addSubnode(actionNode) + } + self.actionNodes.last?.actionEnabled = true + + for separatorNode in self.actionVerticalSeparators { + self.addSubnode(separatorNode) + } + + self.inputFieldNode.updateHeight = { [weak self] in + if let strongSelf = self { + if let _ = strongSelf.validLayout { + strongSelf.requestLayout?(.immediate) + } + } + } + + self.inputFieldNode.textChanged = { [weak self] text in + if let strongSelf = self, let lastNode = strongSelf.actionNodes.last { + lastNode.actionEnabled = text.count <= characterLimit + strongSelf.requestLayout?(.immediate) + } + } + + self.updateTheme(theme) + + self.inputFieldNode.complete = { [weak self] in + guard let self else { + return + } + if let lastNode = self.actionNodes.last, lastNode.actionEnabled { + self.complete?() + } + } + } + + deinit { + self.disposable.dispose() + } + + var value: String { + return self.inputFieldNode.text + } + + public func setErrorText(errorText: String?) { + if self.errorText != errorText { + self.errorText = errorText + self.requestLayout?(.immediate) + } + + if errorText != nil { + HapticFeedback().error() + self.inputFieldNode.layer.addShakeAnimation() + } + } + + override public func updateTheme(_ theme: AlertControllerTheme) { + self.theme = theme + + self.actionNodesSeparator.backgroundColor = theme.separatorColor + for actionNode in self.actionNodes { + actionNode.updateTheme(theme) + } + for separatorNode in self.actionVerticalSeparators { + separatorNode.backgroundColor = theme.separatorColor + } + + if let size = self.validLayout { + _ = self.updateLayout(size: size, transition: .immediate) + } + } + + override public func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize { + var size = size + size.width = min(size.width, 270.0) + let measureSize = CGSize(width: size.width - 16.0 * 2.0, height: CGFloat.greatestFiniteMagnitude) + + let hadValidLayout = self.validLayout != nil + + self.validLayout = size + + var origin: CGPoint = CGPoint(x: 0.0, y: 16.0) + let spacing: CGFloat = 5.0 + let subtextSpacing: CGFloat = -1.0 + + let textSize = self.textView.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: self.text, font: Font.semibold(17.0), textColor: self.theme.primaryColor)), + horizontalAlignment: .center, + maximumNumberOfLines: 0 + )), + environment: {}, + containerSize: CGSize(width: measureSize.width, height: 1000.0) + ) + let textFrame = CGRect(origin: CGPoint(x: floor((size.width - textSize.width) * 0.5), y: origin.y), size: textSize) + if let textComponentView = self.textView.view { + if textComponentView.superview == nil { + textComponentView.layer.anchorPoint = CGPoint() + self.view.addSubview(textComponentView) + } + textComponentView.bounds = CGRect(origin: CGPoint(), size: textFrame.size) + transition.updatePosition(layer: textComponentView.layer, position: textFrame.origin) + } + origin.y += textSize.height + 6.0 + subtextSpacing + + let subtextSize = self.subtextView.update( + transition: .immediate, + component: AnyComponent(BalancedTextComponent( + text: .plain(NSAttributedString(string: self.errorText ?? self.subtext, font: Font.regular(13.0), textColor: self.errorText != nil ? self.theme.destructiveColor : self.theme.primaryColor)), + horizontalAlignment: .center, + maximumNumberOfLines: 0 + )), + environment: {}, + containerSize: CGSize(width: measureSize.width, height: 1000.0) + ) + let subtextFrame = CGRect(origin: CGPoint(x: floor((size.width - subtextSize.width) * 0.5), y: origin.y), size: subtextSize) + if let subtextComponentView = self.subtextView.view { + if subtextComponentView.superview == nil { + subtextComponentView.layer.anchorPoint = CGPoint() + self.view.addSubview(subtextComponentView) + } + subtextComponentView.bounds = CGRect(origin: CGPoint(), size: subtextFrame.size) + transition.updatePosition(layer: subtextComponentView.layer, position: subtextFrame.origin) + } + origin.y += subtextSize.height + 6.0 + spacing + + let actionButtonHeight: CGFloat = 44.0 + var minActionsWidth: CGFloat = 0.0 + let maxActionWidth: CGFloat = floor(size.width / CGFloat(self.actionNodes.count)) + let actionTitleInsets: CGFloat = 8.0 + + var effectiveActionLayout = TextAlertContentActionLayout.horizontal + for actionNode in self.actionNodes { + let actionTitleSize = actionNode.titleNode.updateLayout(CGSize(width: maxActionWidth, height: actionButtonHeight)) + if case .horizontal = effectiveActionLayout, actionTitleSize.height > actionButtonHeight * 0.6667 { + effectiveActionLayout = .vertical + } + switch effectiveActionLayout { + case .horizontal: + minActionsWidth += actionTitleSize.width + actionTitleInsets + case .vertical: + minActionsWidth = max(minActionsWidth, actionTitleSize.width + actionTitleInsets) + } + } + + let insets = UIEdgeInsets(top: 18.0, left: 18.0, bottom: 9.0, right: 18.0) + + var contentWidth = max(textSize.width, minActionsWidth) + contentWidth = max(subtextSize.width, minActionsWidth) + contentWidth = max(contentWidth, 234.0) + + var actionsHeight: CGFloat = 0.0 + switch effectiveActionLayout { + case .horizontal: + actionsHeight = actionButtonHeight + case .vertical: + actionsHeight = actionButtonHeight * CGFloat(self.actionNodes.count) + } + + let resultWidth = contentWidth + insets.left + insets.right + + let inputFieldWidth = resultWidth + let inputFieldHeight = self.inputFieldNode.updateLayout(width: inputFieldWidth, transition: transition) + let inputHeight = inputFieldHeight + let inputFieldFrame = CGRect(x: 0.0, y: origin.y, width: resultWidth, height: inputFieldHeight) + transition.updateFrame(node: self.inputFieldNode, frame: inputFieldFrame) + transition.updateAlpha(node: self.inputFieldNode, alpha: inputHeight > 0.0 ? 1.0 : 0.0) + + let resultSize = CGSize(width: resultWidth, height: textSize.height + subtextSpacing + subtextSize.height + spacing + inputHeight + actionsHeight + insets.top + insets.bottom) + + transition.updateFrame(node: self.actionNodesSeparator, frame: CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight - UIScreenPixel), size: CGSize(width: resultSize.width, height: UIScreenPixel))) + + var actionOffset: CGFloat = 0.0 + let actionWidth: CGFloat = floor(resultSize.width / CGFloat(self.actionNodes.count)) + var separatorIndex = -1 + var nodeIndex = 0 + for actionNode in self.actionNodes { + if separatorIndex >= 0 { + let separatorNode = self.actionVerticalSeparators[separatorIndex] + switch effectiveActionLayout { + case .horizontal: + transition.updateFrame(node: separatorNode, frame: CGRect(origin: CGPoint(x: actionOffset - UIScreenPixel, y: resultSize.height - actionsHeight), size: CGSize(width: UIScreenPixel, height: actionsHeight - UIScreenPixel))) + case .vertical: + transition.updateFrame(node: separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight + actionOffset - UIScreenPixel), size: CGSize(width: resultSize.width, height: UIScreenPixel))) + } + } + separatorIndex += 1 + + let currentActionWidth: CGFloat + switch effectiveActionLayout { + case .horizontal: + if nodeIndex == self.actionNodes.count - 1 { + currentActionWidth = resultSize.width - actionOffset + } else { + currentActionWidth = actionWidth + } + case .vertical: + currentActionWidth = resultSize.width + } + + let actionNodeFrame: CGRect + switch effectiveActionLayout { + case .horizontal: + actionNodeFrame = CGRect(origin: CGPoint(x: actionOffset, y: resultSize.height - actionsHeight), size: CGSize(width: currentActionWidth, height: actionButtonHeight)) + actionOffset += currentActionWidth + case .vertical: + actionNodeFrame = CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight + actionOffset), size: CGSize(width: currentActionWidth, height: actionButtonHeight)) + actionOffset += actionButtonHeight + } + + transition.updateFrame(node: actionNode, frame: actionNodeFrame) + + nodeIndex += 1 + } + + if !hadValidLayout { + self.inputFieldNode.activateInput() + } + + return resultSize + } + + func animateError() { + self.inputFieldNode.layer.addShakeAnimation() + self.hapticFeedback.error() + } +} + +public enum PromptControllerTitleFont { + case regular + case bold +} + +public func quickReplyNameAlertController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, text: String, subtext: String, titleFont: PromptControllerTitleFont = .regular, value: String?, characterLimit: Int = 1000, apply: @escaping (String?) -> Void) -> AlertController { + let presentationData = updatedPresentationData?.initial ?? context.sharedContext.currentPresentationData.with { $0 } + + var dismissImpl: ((Bool) -> Void)? + var applyImpl: (() -> Void)? + + let actions: [TextAlertAction] = [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: { + dismissImpl?(true) + apply(nil) + }), TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_Done, action: { + applyImpl?() + })] + + let contentNode = QuickReplyNameAlertContentNode(context: context, theme: AlertControllerTheme(presentationData: presentationData), ptheme: presentationData.theme, strings: presentationData.strings, actions: actions, text: text, subtext: subtext, titleFont: titleFont, value: value, characterLimit: characterLimit) + contentNode.complete = { + applyImpl?() + } + applyImpl = { [weak contentNode] in + guard let contentNode = contentNode else { + return + } + apply(contentNode.value) + } + + let controller = AlertController(theme: AlertControllerTheme(presentationData: presentationData), contentNode: contentNode) + let presentationDataDisposable = (updatedPresentationData?.signal ?? context.sharedContext.presentationData).start(next: { [weak controller, weak contentNode] presentationData in + controller?.theme = AlertControllerTheme(presentationData: presentationData) + contentNode?.inputFieldNode.updateTheme(presentationData.theme) + }) + controller.dismissed = { _ in + presentationDataDisposable.dispose() + } + dismissImpl = { [weak controller] animated in + contentNode.inputFieldNode.deactivateInput() + if animated { + controller?.dismissAnimated() + } else { + controller?.dismiss() + } + } + return controller +} diff --git a/submodules/TelegramUI/Components/Settings/ThemeAccentColorScreen/Sources/ThemeAccentColorControllerNode.swift b/submodules/TelegramUI/Components/Settings/ThemeAccentColorScreen/Sources/ThemeAccentColorControllerNode.swift index 4944cbc942d..c18ef7ae63a 100644 --- a/submodules/TelegramUI/Components/Settings/ThemeAccentColorScreen/Sources/ThemeAccentColorControllerNode.swift +++ b/submodules/TelegramUI/Components/Settings/ThemeAccentColorScreen/Sources/ThemeAccentColorControllerNode.swift @@ -870,6 +870,7 @@ final class ThemeAccentColorControllerNode: ASDisplayNode, UIScrollViewDelegate }, openChatFolderUpdates: {}, hideChatFolderUpdates: { }, openStories: { _, _ in }, dismissNotice: { _ in + }, editPeer: { _ in }) let chatListPresentationData = ChatListPresentationData(theme: self.presentationData.theme, fontSize: self.presentationData.listsFontSize, strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat, nameSortOrder: self.presentationData.nameSortOrder, nameDisplayOrder: self.presentationData.nameDisplayOrder, disableAnimations: true) diff --git a/submodules/TelegramUI/Components/Settings/TimezoneSelectionScreen/BUILD b/submodules/TelegramUI/Components/Settings/TimezoneSelectionScreen/BUILD new file mode 100644 index 00000000000..38fdb23587f --- /dev/null +++ b/submodules/TelegramUI/Components/Settings/TimezoneSelectionScreen/BUILD @@ -0,0 +1,32 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "TimezoneSelectionScreen", + module_name = "TimezoneSelectionScreen", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/Display", + "//submodules/AsyncDisplayKit", + "//submodules/Postbox", + "//submodules/SSignalKit/SwiftSignalKit", + "//submodules/TelegramCore", + "//submodules/TelegramPresentationData", + "//submodules/AccountContext", + "//submodules/SearchUI", + "//submodules/MergeLists", + "//submodules/ItemListUI", + "//submodules/PresentationDataUtils", + "//submodules/SearchBarNode", + "//submodules/TelegramUIPreferences", + "//submodules/ComponentFlow", + "//submodules/Components/BalancedTextComponent", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Components/Settings/TimezoneSelectionScreen/Sources/TimezoneSelectionScreen.swift b/submodules/TelegramUI/Components/Settings/TimezoneSelectionScreen/Sources/TimezoneSelectionScreen.swift new file mode 100644 index 00000000000..d83ae3e0fbe --- /dev/null +++ b/submodules/TelegramUI/Components/Settings/TimezoneSelectionScreen/Sources/TimezoneSelectionScreen.swift @@ -0,0 +1,155 @@ +import Foundation +import UIKit +import Display +import AsyncDisplayKit +import Postbox +import SwiftSignalKit +import TelegramCore +import TelegramPresentationData +import AccountContext +import SearchUI + +public class TimezoneSelectionScreen: ViewController { + private let context: AccountContext + private let completed: (String) -> Void + + private var controllerNode: TimezoneSelectionScreenNode { + return self.displayNode as! TimezoneSelectionScreenNode + } + + private var _ready = Promise() + override public var ready: Promise { + return self._ready + } + + private var presentationData: PresentationData + private var presentationDataDisposable: Disposable? + + private var searchContentNode: NavigationBarSearchContentNode? + + private var previousContentOffset: ListViewVisibleContentOffset? + + public init(context: AccountContext, completed: @escaping (String) -> Void) { + self.context = context + self.completed = completed + + self.presentationData = context.sharedContext.currentPresentationData.with { $0 } + + super.init(navigationBarPresentationData: NavigationBarPresentationData(presentationData: self.presentationData)) + + self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBarStyle.style + + self.title = self.presentationData.strings.TimeZoneSelection_Title + + self.navigationItem.backBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Back, style: .plain, target: nil, action: nil) + + self.scrollToTop = { [weak self] in + if let strongSelf = self { + if let searchContentNode = strongSelf.searchContentNode { + searchContentNode.updateExpansionProgress(1.0, animated: true) + } + strongSelf.controllerNode.scrollToTop() + } + } + + self.presentationDataDisposable = (context.sharedContext.presentationData + |> deliverOnMainQueue).start(next: { [weak self] presentationData in + if let strongSelf = self { + let previousTheme = strongSelf.presentationData.theme + let previousStrings = strongSelf.presentationData.strings + + strongSelf.presentationData = presentationData + + if previousTheme !== presentationData.theme || previousStrings !== presentationData.strings { + strongSelf.updateThemeAndStrings() + } + } + }) + + self.searchContentNode = NavigationBarSearchContentNode(theme: self.presentationData.theme, placeholder: self.presentationData.strings.Common_Search, inline: true, activate: { [weak self] in + self?.activateSearch() + }) + self.navigationBar?.setContentNode(self.searchContentNode, animated: false) + } + + required public init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + self.presentationDataDisposable?.dispose() + } + + private func updateThemeAndStrings() { + self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBarStyle.style + self.navigationBar?.updatePresentationData(NavigationBarPresentationData(presentationData: self.presentationData)) + self.searchContentNode?.updateThemeAndPlaceholder(theme: self.presentationData.theme, placeholder: self.presentationData.strings.Common_Search) + self.title = self.presentationData.strings.Settings_AppLanguage + self.navigationItem.backBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Back, style: .plain, target: nil, action: nil) + self.controllerNode.updatePresentationData(self.presentationData) + } + + override public func loadDisplayNode() { + self.displayNode = TimezoneSelectionScreenNode(context: self.context, presentationData: self.presentationData, navigationBar: self.navigationBar!, requestActivateSearch: { [weak self] in + self?.activateSearch() + }, requestDeactivateSearch: { [weak self] in + self?.deactivateSearch() + }, action: { [weak self] id in + guard let self else { + return + } + self.completed(id) + }, present: { [weak self] c, a in + self?.present(c, in: .window(.root), with: a) + }, push: { [weak self] c in + self?.push(c) + }) + + self.controllerNode.listNode.visibleContentOffsetChanged = { [weak self] offset in + if let strongSelf = self { + if let searchContentNode = strongSelf.searchContentNode { + searchContentNode.updateListVisibleContentOffset(offset) + } + + strongSelf.previousContentOffset = offset + } + } + + self.controllerNode.listNode.didEndScrolling = { [weak self] _ in + if let strongSelf = self, let searchContentNode = strongSelf.searchContentNode { + let _ = fixNavigationSearchableListNodeScrolling(strongSelf.controllerNode.listNode, searchNode: searchContentNode) + } + } + + self._ready.set(self.controllerNode._ready.get()) + + self.displayNodeDidLoad() + } + + override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { + super.containerLayoutUpdated(layout, transition: transition) + + self.controllerNode.containerLayoutUpdated(layout, navigationBarHeight: self.cleanNavigationHeight, transition: transition) + } + + private func activateSearch() { + if self.displayNavigationBar { + if let scrollToTop = self.scrollToTop { + scrollToTop() + } + if let searchContentNode = self.searchContentNode { + self.controllerNode.activateSearch(placeholderNode: searchContentNode.placeholderNode) + } + self.setDisplayNavigationBar(false, transition: .animated(duration: 0.5, curve: .spring)) + } + } + + private func deactivateSearch() { + if !self.displayNavigationBar { + self.setDisplayNavigationBar(true, transition: .animated(duration: 0.5, curve: .spring)) + if let searchContentNode = self.searchContentNode { + self.controllerNode.deactivateSearch(placeholderNode: searchContentNode.placeholderNode) + } + } + } +} diff --git a/submodules/TelegramUI/Components/Settings/TimezoneSelectionScreen/Sources/TimezoneSelectionScreenNode.swift b/submodules/TelegramUI/Components/Settings/TimezoneSelectionScreen/Sources/TimezoneSelectionScreenNode.swift new file mode 100644 index 00000000000..e20cc58d886 --- /dev/null +++ b/submodules/TelegramUI/Components/Settings/TimezoneSelectionScreen/Sources/TimezoneSelectionScreenNode.swift @@ -0,0 +1,550 @@ +import Foundation +import UIKit +import Display +import AsyncDisplayKit +import Postbox +import TelegramCore +import SwiftSignalKit +import TelegramPresentationData +import MergeLists +import ItemListUI +import PresentationDataUtils +import AccountContext +import SearchBarNode +import SearchUI +import TelegramUIPreferences +import ComponentFlow +import BalancedTextComponent + +private struct TimezoneListEntry: Comparable, Identifiable { + var id: String + var offset: Int + var title: String + + var stableId: String { + return self.id + } + + static func <(lhs: TimezoneListEntry, rhs: TimezoneListEntry) -> Bool { + if lhs.offset != rhs.offset { + return lhs.offset < rhs.offset + } + if lhs.title != rhs.title { + return lhs.title < rhs.title + } + return lhs.id < rhs.id + } + + func item(presentationData: PresentationData, searchMode: Bool, action: @escaping (String) -> Void) -> ListViewItem { + let hours = abs(self.offset / (60 * 60)) + let minutes = abs(self.offset % (60 * 60)) / 60 + let offsetString: String + if minutes == 0 { + offsetString = "UTC \(self.offset >= 0 ? "+" : "-")\(hours)" + } else { + let minutesString: String + if minutes < 10 { + minutesString = "0\(minutes)" + } else { + minutesString = "\(minutes)" + } + offsetString = "UTC \(self.offset >= 0 ? "+" : "-")\(hours):\(minutesString)" + } + + return ItemListDisclosureItem(presentationData: ItemListPresentationData(presentationData), title: self.title, label: offsetString, sectionId: 0, style: .plain, disclosureStyle: .none, action: { + action(self.id) + }) + } +} + +private struct TimezoneListSearchContainerTransition { + let deletions: [ListViewDeleteItem] + let insertions: [ListViewInsertItem] + let updates: [ListViewUpdateItem] + let isSearching: Bool + let isEmptyResult: Bool +} + +private func preparedLanguageListSearchContainerTransition(presentationData: PresentationData, from fromEntries: [TimezoneListEntry], to toEntries: [TimezoneListEntry], action: @escaping (String) -> Void, isSearching: Bool, forceUpdate: Bool) -> TimezoneListSearchContainerTransition { + let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries, allUpdated: forceUpdate) + + let deletions = deleteIndices.map { ListViewDeleteItem(index: $0, directionHint: nil) } + let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(presentationData: presentationData, searchMode: true, action: action), directionHint: nil) } + let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(presentationData: presentationData, searchMode: true, action: action), directionHint: nil) } + + return TimezoneListSearchContainerTransition(deletions: deletions, insertions: insertions, updates: updates, isSearching: isSearching, isEmptyResult: isSearching && toEntries.isEmpty) +} + +private final class TimezoneListSearchContainerNode: SearchDisplayControllerContentNode { + private let timeZoneList: TimeZoneList + private let dimNode: ASDisplayNode + private let listNode: ListView + + private var notFoundText: ComponentView? + + private var enqueuedTransitions: [TimezoneListSearchContainerTransition] = [] + private var hasValidLayout = false + + private let searchQuery = Promise() + private let searchDisposable = MetaDisposable() + + private var presentationData: PresentationData + private var presentationDataDisposable: Disposable? + + private let presentationDataPromise: Promise + + private var isEmptyResult: Bool = false + private var currentLayout: (layout: ContainerViewLayout, navigationBarHeight: CGFloat)? + + public override var hasDim: Bool { + return true + } + + init(context: AccountContext, timeZoneList: TimeZoneList, action: @escaping (String) -> Void) { + self.timeZoneList = timeZoneList + + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + self.presentationData = presentationData + + self.presentationDataPromise = Promise(self.presentationData) + + self.dimNode = ASDisplayNode() + self.dimNode.backgroundColor = UIColor.black.withAlphaComponent(0.5) + + self.listNode = ListView() + self.listNode.accessibilityPageScrolledString = { row, count in + return presentationData.strings.VoiceOver_ScrollStatus(row, count).string + } + + super.init() + + self.listNode.backgroundColor = self.presentationData.theme.chatList.backgroundColor + self.listNode.isHidden = true + + self.addSubnode(self.dimNode) + self.addSubnode(self.listNode) + + let querySplitCharacterSet: CharacterSet = CharacterSet(charactersIn: " /.+") + + let foundItems = self.searchQuery.get() + |> mapToSignal { query -> Signal<[TimeZoneList.Item]?, NoError> in + if let query, !query.isEmpty { + let query = query.lowercased() + + return .single(timeZoneList.items.filter { item in + if item.id.lowercased().hasPrefix(query) { + return true + } + if item.title.lowercased().components(separatedBy: querySplitCharacterSet).contains(where: { $0.hasPrefix(query) }) { + return true + } + + return false + }) + } else { + return .single(nil) + } + } + + let previousEntriesHolder = Atomic<([TimezoneListEntry], PresentationTheme, PresentationStrings)?>(value: nil) + self.searchDisposable.set(combineLatest(queue: .mainQueue(), foundItems, self.presentationDataPromise.get()).start(next: { [weak self] items, presentationData in + guard let strongSelf = self else { + return + } + var entries: [TimezoneListEntry] = [] + if let items { + for item in items { + entries.append(TimezoneListEntry( + id: item.id, + offset: Int(item.utcOffset), + title: item.title + )) + } + } + entries.sort() + let previousEntriesAndPresentationData = previousEntriesHolder.swap((entries, presentationData.theme, presentationData.strings)) + let transition = preparedLanguageListSearchContainerTransition(presentationData: presentationData, from: previousEntriesAndPresentationData?.0 ?? [], to: entries, action: action, isSearching: items != nil, forceUpdate: previousEntriesAndPresentationData?.1 !== presentationData.theme || previousEntriesAndPresentationData?.2 !== presentationData.strings) + strongSelf.enqueueTransition(transition) + })) + + self.presentationDataDisposable = (context.sharedContext.presentationData + |> deliverOnMainQueue).start(next: { [weak self] presentationData in + if let strongSelf = self { + let previousTheme = strongSelf.presentationData.theme + let previousStrings = strongSelf.presentationData.strings + + strongSelf.presentationData = presentationData + + if previousTheme !== presentationData.theme || previousStrings !== presentationData.strings { + strongSelf.updateThemeAndStrings(theme: presentationData.theme, strings: presentationData.strings) + strongSelf.presentationDataPromise.set(.single(presentationData)) + } + } + }) + + self.listNode.beganInteractiveDragging = { [weak self] _ in + self?.dismissInput?() + } + } + + deinit { + self.searchDisposable.dispose() + self.presentationDataDisposable?.dispose() + } + + override func didLoad() { + super.didLoad() + + self.dimNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dimTapGesture(_:)))) + } + + func updateThemeAndStrings(theme: PresentationTheme, strings: PresentationStrings) { + self.listNode.backgroundColor = theme.chatList.backgroundColor + } + + override func searchTextUpdated(text: String) { + if text.isEmpty { + self.searchQuery.set(.single(nil)) + } else { + self.searchQuery.set(.single(text)) + } + } + + private func enqueueTransition(_ transition: TimezoneListSearchContainerTransition) { + self.enqueuedTransitions.append(transition) + + if self.hasValidLayout { + while !self.enqueuedTransitions.isEmpty { + self.dequeueTransitions() + } + } + } + + private func dequeueTransitions() { + if let transition = self.enqueuedTransitions.first { + self.enqueuedTransitions.remove(at: 0) + + var options = ListViewDeleteAndInsertOptions() + options.insert(.PreferSynchronousDrawing) + + let isSearching = transition.isSearching + let isEmptyResult = transition.isEmptyResult + + self.listNode.transaction(deleteIndices: transition.deletions, insertIndicesAndItems: transition.insertions, updateIndicesAndItems: transition.updates, options: options, updateSizeAndInsets: nil, updateOpaqueState: nil, completion: { [weak self] _ in + guard let self else { + return + } + self.listNode.isHidden = !isSearching + self.dimNode.isHidden = isSearching + self.isEmptyResult = isEmptyResult + + if let currentLayout = self.currentLayout { + self.containerLayoutUpdated(currentLayout.layout, navigationBarHeight: currentLayout.navigationBarHeight, transition: .immediate) + } + }) + } + } + + override func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) { + self.currentLayout = (layout, navigationBarHeight) + + super.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: transition) + + let topInset = navigationBarHeight + transition.updateFrame(node: self.dimNode, frame: CGRect(origin: CGPoint(x: 0.0, y: topInset), size: CGSize(width: layout.size.width, height: layout.size.height - topInset))) + + let (duration, curve) = listViewAnimationDurationAndCurve(transition: transition) + + self.listNode.frame = CGRect(origin: CGPoint(), size: layout.size) + self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous], scrollToItem: nil, updateSizeAndInsets: ListViewUpdateSizeAndInsets(size: layout.size, insets: UIEdgeInsets(top: navigationBarHeight, left: layout.safeInsets.left, bottom: layout.insets(options: [.input]).bottom, right: layout.safeInsets.right), duration: duration, curve: curve), stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) + + if !self.hasValidLayout { + self.hasValidLayout = true + while !self.enqueuedTransitions.isEmpty { + self.dequeueTransitions() + } + } + + if self.isEmptyResult { + let notFoundText: ComponentView + if let current = self.notFoundText { + notFoundText = current + } else { + notFoundText = ComponentView() + self.notFoundText = notFoundText + } + let notFoundTextSize = notFoundText.update( + transition: .immediate, + component: AnyComponent(BalancedTextComponent( + text: .plain(NSAttributedString(string: self.presentationData.strings.Conversation_SearchNoResults, font: Font.regular(17.0), textColor: self.presentationData.theme.list.freeTextColor, paragraphAlignment: .center)), + horizontalAlignment: .center, + maximumNumberOfLines: 0 + )), + environment: {}, + containerSize: CGSize(width: layout.size.width - 16.0 * 2.0, height: layout.size.height) + ) + let notFoundTextFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - notFoundTextSize.width) * 0.5), y: navigationBarHeight + floor((layout.size.height - navigationBarHeight - notFoundTextSize.height) * 0.5)), size: notFoundTextSize) + if let notFoundTextView = notFoundText.view { + if notFoundTextView.superview == nil { + self.view.addSubview(notFoundTextView) + } + notFoundTextView.frame = notFoundTextFrame + } + } else if let notFoundText = self.notFoundText { + self.notFoundText = nil + notFoundText.view?.removeFromSuperview() + } + } + + @objc func dimTapGesture(_ recognizer: UITapGestureRecognizer) { + if case .ended = recognizer.state { + self.cancel?() + } + } +} + +private struct TimezoneListNodeTransition { + let deletions: [ListViewDeleteItem] + let insertions: [ListViewInsertItem] + let updates: [ListViewUpdateItem] + let firstTime: Bool + let isLoading: Bool + let animated: Bool + let crossfade: Bool +} + +private func preparedTimezoneListNodeTransition(presentationData: PresentationData, from fromEntries: [TimezoneListEntry], to toEntries: [TimezoneListEntry], action: @escaping (String) -> Void, firstTime: Bool, isLoading: Bool, forceUpdate: Bool, animated: Bool, crossfade: Bool) -> TimezoneListNodeTransition { + let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries, allUpdated: forceUpdate) + + let deletions = deleteIndices.map { ListViewDeleteItem(index: $0, directionHint: nil) } + let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(presentationData: presentationData, searchMode: false, action: action), directionHint: nil) } + let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(presentationData: presentationData, searchMode: false, action: action), directionHint: nil) } + + return TimezoneListNodeTransition(deletions: deletions, insertions: insertions, updates: updates, firstTime: firstTime, isLoading: isLoading, animated: animated, crossfade: crossfade) +} + +private final class TimezoneData { + struct Item { + var id: String + var offset: Int + var title: String + + init(id: String, offset: Int, title: String) { + self.id = id + self.offset = offset + self.title = title + } + } + + let items: [Item] + + init() { + let locale = Locale.current + var items: [Item] = [] + for (key, value) in TimeZone.abbreviationDictionary { + guard let timezone = TimeZone(abbreviation: key) else { + continue + } + if items.contains(where: { $0.id == timezone.identifier }) { + continue + } + items.append(Item( + id: timezone.identifier, + offset: timezone.secondsFromGMT(), + title: timezone.localizedName(for: .standard, locale: locale) ?? value + )) + } + self.items = items + } +} + +final class TimezoneSelectionScreenNode: ViewControllerTracingNode { + private let context: AccountContext + private let action: (String) -> Void + private var presentationData: PresentationData + private weak var navigationBar: NavigationBar? + private let requestActivateSearch: () -> Void + private let requestDeactivateSearch: () -> Void + private let present: (ViewController, Any?) -> Void + private let push: (ViewController) -> Void + private var timeZoneList: TimeZoneList? + + private var didSetReady = false + let _ready = ValuePromise() + + private var containerLayout: (ContainerViewLayout, CGFloat)? + let listNode: ListView + private var queuedTransitions: [TimezoneListNodeTransition] = [] + private var searchDisplayController: SearchDisplayController? + + private let presentationDataValue = Promise() + + private var listDisposable: Disposable? + + init(context: AccountContext, presentationData: PresentationData, navigationBar: NavigationBar, requestActivateSearch: @escaping () -> Void, requestDeactivateSearch: @escaping () -> Void, action: @escaping (String) -> Void, present: @escaping (ViewController, Any?) -> Void, push: @escaping (ViewController) -> Void) { + self.context = context + self.action = action + self.presentationData = presentationData + self.presentationDataValue.set(.single(presentationData)) + self.navigationBar = navigationBar + self.requestActivateSearch = requestActivateSearch + self.requestDeactivateSearch = requestDeactivateSearch + self.present = present + self.push = push + + self.listNode = ListView() + self.listNode.accessibilityPageScrolledString = { row, count in + return presentationData.strings.VoiceOver_ScrollStatus(row, count).string + } + + super.init() + + self.backgroundColor = presentationData.theme.list.plainBackgroundColor + self.addSubnode(self.listNode) + + let previousEntriesHolder = Atomic<([TimezoneListEntry], PresentationTheme, PresentationStrings)?>(value: nil) + self.listDisposable = (combineLatest(queue: .mainQueue(), + self.presentationDataValue.get(), + context.engine.accountData.cachedTimeZoneList() + ) + |> deliverOnMainQueue).start(next: { [weak self] presentationData, timeZoneList in + guard let strongSelf = self else { + return + } + + strongSelf.timeZoneList = timeZoneList + + var entries: [TimezoneListEntry] = [] + if let timeZoneList { + for item in timeZoneList.items { + entries.append(TimezoneListEntry( + id: item.id, + offset: Int(item.utcOffset), + title: item.title + )) + } + } + entries.sort() + + let previousEntriesAndPresentationData = previousEntriesHolder.swap((entries, presentationData.theme, presentationData.strings)) + let transition = preparedTimezoneListNodeTransition(presentationData: presentationData, from: previousEntriesAndPresentationData?.0 ?? [], to: entries, action: action, firstTime: previousEntriesAndPresentationData == nil, isLoading: entries.isEmpty, forceUpdate: previousEntriesAndPresentationData?.1 !== presentationData.theme || previousEntriesAndPresentationData?.2 !== presentationData.strings, animated: false, crossfade: false) + strongSelf.enqueueTransition(transition) + }) + } + + deinit { + self.listDisposable?.dispose() + } + + func updatePresentationData(_ presentationData: PresentationData) { + let stringsUpdated = self.presentationData.strings !== presentationData.strings + self.presentationData = presentationData + + if stringsUpdated { + if let snapshotView = self.view.snapshotView(afterScreenUpdates: false) { + self.view.addSubview(snapshotView) + snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak snapshotView] _ in + snapshotView?.removeFromSuperview() + }) + } + } + + self.presentationDataValue.set(.single(presentationData)) + self.backgroundColor = presentationData.theme.list.plainBackgroundColor + self.searchDisplayController?.updatePresentationData(presentationData) + } + + func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) { + let hadValidLayout = self.containerLayout != nil + self.containerLayout = (layout, navigationBarHeight) + + var listInsets = layout.insets(options: [.input]) + listInsets.top += navigationBarHeight + + if let searchDisplayController = self.searchDisplayController { + searchDisplayController.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: transition) + } + + self.listNode.bounds = CGRect(x: 0.0, y: 0.0, width: layout.size.width, height: layout.size.height) + self.listNode.position = CGPoint(x: layout.size.width / 2.0, y: layout.size.height / 2.0) + + let (duration, curve) = listViewAnimationDurationAndCurve(transition: transition) + let updateSizeAndInsets = ListViewUpdateSizeAndInsets(size: layout.size, insets: listInsets, duration: duration, curve: curve) + + self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: nil, updateSizeAndInsets: updateSizeAndInsets, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) + + if !hadValidLayout { + self.dequeueTransitions() + } + } + + private func enqueueTransition(_ transition: TimezoneListNodeTransition) { + self.queuedTransitions.append(transition) + + if self.containerLayout != nil { + self.dequeueTransitions() + } + } + + private func dequeueTransitions() { + guard let _ = self.containerLayout else { + return + } + while !self.queuedTransitions.isEmpty { + let transition = self.queuedTransitions.removeFirst() + + var options = ListViewDeleteAndInsertOptions() + if transition.firstTime { + options.insert(.Synchronous) + options.insert(.LowLatency) + } else if transition.crossfade { + options.insert(.AnimateCrossfade) + } else if transition.animated { + options.insert(.AnimateInsertion) + } + self.listNode.transaction(deleteIndices: transition.deletions, insertIndicesAndItems: transition.insertions, updateIndicesAndItems: transition.updates, options: options, updateOpaqueState: nil, completion: { [weak self] _ in + if let strongSelf = self { + if !strongSelf.didSetReady { + strongSelf.didSetReady = true + strongSelf._ready.set(true) + } + } + }) + } + } + + func activateSearch(placeholderNode: SearchBarPlaceholderNode) { + guard let (containerLayout, navigationBarHeight) = self.containerLayout, self.searchDisplayController == nil else { + return + } + guard let timeZoneList = self.timeZoneList else { + return + } + + self.searchDisplayController = SearchDisplayController(presentationData: self.presentationData, contentNode: TimezoneListSearchContainerNode(context: self.context, timeZoneList: timeZoneList, action: self.action), inline: true, cancel: { [weak self] in + self?.requestDeactivateSearch() + }) + + self.searchDisplayController?.containerLayoutUpdated(containerLayout, navigationBarHeight: navigationBarHeight, transition: .immediate) + self.searchDisplayController?.activate(insertSubnode: { [weak self, weak placeholderNode] subnode, isSearchBar in + if let strongSelf = self, let strongPlaceholderNode = placeholderNode { + if isSearchBar { + strongPlaceholderNode.supernode?.insertSubnode(subnode, aboveSubnode: strongPlaceholderNode) + } else if let navigationBar = strongSelf.navigationBar { + strongSelf.insertSubnode(subnode, belowSubnode: navigationBar) + } + } + }, placeholder: placeholderNode) + } + + func deactivateSearch(placeholderNode: SearchBarPlaceholderNode) { + if let searchDisplayController = self.searchDisplayController { + searchDisplayController.deactivate(placeholder: placeholderNode) + self.searchDisplayController = nil + } + } + + func scrollToTop() { + self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: ListViewScrollToItem(index: 0, position: .top(0.0), animated: true, curve: .Default(duration: nil), directionHint: .Up), updateSizeAndInsets: nil, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) + } +} diff --git a/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/ShareWithPeersScreen.swift b/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/ShareWithPeersScreen.swift index f16f5ac5192..37f1d5c338a 100644 --- a/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/ShareWithPeersScreen.swift +++ b/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/ShareWithPeersScreen.swift @@ -427,6 +427,10 @@ final class ShareWithPeersScreenComponent: Component { } } + func scrollViewWillBeginDragging(_ scrollView: UIScrollView) { + self.endEditing(true) + } + func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer) { guard let itemLayout = self.itemLayout, let topOffsetDistance = self.topOffsetDistance else { return @@ -613,15 +617,15 @@ final class ShareWithPeersScreenComponent: Component { controller.present(alertController, in: .window(.root)) } - if groupTooLarge { - showCountLimitAlert() - return - } - var append = false if let index = self.selectedGroups.firstIndex(of: peer.id) { self.selectedGroups.remove(at: index) } else { + if groupTooLarge { + showCountLimitAlert() + return + } + self.selectedGroups.append(peer.id) append = true } @@ -801,7 +805,7 @@ final class ShareWithPeersScreenComponent: Component { } private func updateModalOverlayTransition(transition: Transition) { - guard let component = self.component, let environment = self.environment, let itemLayout = self.itemLayout else { + guard let component = self.component, let environment = self.environment, let itemLayout = self.itemLayout, !self.isDismissed else { return } @@ -2071,6 +2075,14 @@ final class ShareWithPeersScreenComponent: Component { self.selectedCategories.insert(.everyone) } self.state?.updated(transition: Transition(animation: .curve(duration: 0.35, curve: .spring))) + }, + isFocusedUpdated: { [weak self] isFocused in + guard let self else { + return + } + if isFocused { + self.scrollView.setContentOffset(CGPoint(x: 0.0, y: -self.scrollView.contentInset.top), animated: true) + } } )), environment: {}, @@ -2122,11 +2134,11 @@ final class ShareWithPeersScreenComponent: Component { transition.setFrame(view: self.dimView, frame: CGRect(origin: CGPoint(), size: availableSize)) if case .members = component.stateContext.subject { - self.dimView .isHidden = true + self.dimView.isHidden = true } else if case .channels = component.stateContext.subject { - self.dimView .isHidden = true + self.dimView.isHidden = true } else { - self.dimView .isHidden = false + self.dimView.isHidden = false } let categoryItemSize = self.categoryTemplateItem.update( diff --git a/submodules/TelegramUI/Components/SliderComponent/BUILD b/submodules/TelegramUI/Components/SliderComponent/BUILD new file mode 100644 index 00000000000..6ed98b159f0 --- /dev/null +++ b/submodules/TelegramUI/Components/SliderComponent/BUILD @@ -0,0 +1,22 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "SliderComponent", + module_name = "SliderComponent", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/AsyncDisplayKit", + "//submodules/Display", + "//submodules/TelegramPresentationData", + "//submodules/LegacyComponents", + "//submodules/ComponentFlow", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Components/SliderComponent/Sources/SliderComponent.swift b/submodules/TelegramUI/Components/SliderComponent/Sources/SliderComponent.swift new file mode 100644 index 00000000000..2494d74a76b --- /dev/null +++ b/submodules/TelegramUI/Components/SliderComponent/Sources/SliderComponent.swift @@ -0,0 +1,152 @@ +import Foundation +import UIKit +import Display +import AsyncDisplayKit +import TelegramPresentationData +import LegacyComponents +import ComponentFlow + +public final class SliderComponent: Component { + public let valueCount: Int + public let value: Int + public let trackBackgroundColor: UIColor + public let trackForegroundColor: UIColor + public let valueUpdated: (Int) -> Void + public let isTrackingUpdated: ((Bool) -> Void)? + + public init( + valueCount: Int, + value: Int, + trackBackgroundColor: UIColor, + trackForegroundColor: UIColor, + valueUpdated: @escaping (Int) -> Void, + isTrackingUpdated: ((Bool) -> Void)? = nil + ) { + self.valueCount = valueCount + self.value = value + self.trackBackgroundColor = trackBackgroundColor + self.trackForegroundColor = trackForegroundColor + self.valueUpdated = valueUpdated + self.isTrackingUpdated = isTrackingUpdated + } + + public static func ==(lhs: SliderComponent, rhs: SliderComponent) -> Bool { + if lhs.valueCount != rhs.valueCount { + return false + } + if lhs.value != rhs.value { + return false + } + if lhs.trackBackgroundColor != rhs.trackBackgroundColor { + return false + } + if lhs.trackForegroundColor != rhs.trackForegroundColor { + return false + } + return true + } + + public final class View: UIView { + private var sliderView: TGPhotoEditorSliderView? + + private var component: SliderComponent? + private weak var state: EmptyComponentState? + + override public init(frame: CGRect) { + super.init(frame: frame) + } + + required public init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func update(component: SliderComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + self.component = component + self.state = state + + let size = CGSize(width: availableSize.width, height: 44.0) + + var internalIsTrackingUpdated: ((Bool) -> Void)? + if let isTrackingUpdated = component.isTrackingUpdated { + internalIsTrackingUpdated = { [weak self] isTracking in + if let self { + if isTracking { + self.sliderView?.bordered = true + } else { + DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.1, execute: { [weak self] in + self?.sliderView?.bordered = false + }) + } + isTrackingUpdated(isTracking) + } + } + } + + let sliderView: TGPhotoEditorSliderView + if let current = self.sliderView { + sliderView = current + } else { + sliderView = TGPhotoEditorSliderView() + sliderView.enablePanHandling = true + sliderView.trackCornerRadius = 2.0 + sliderView.lineSize = 4.0 + sliderView.dotSize = 5.0 + sliderView.minimumValue = 0.0 + sliderView.startValue = 0.0 + sliderView.disablesInteractiveTransitionGestureRecognizer = true + sliderView.maximumValue = CGFloat(component.valueCount - 1) + sliderView.positionsCount = component.valueCount + sliderView.useLinesForPositions = true + + sliderView.backgroundColor = nil + sliderView.isOpaque = false + sliderView.backColor = component.trackBackgroundColor + sliderView.startColor = component.trackBackgroundColor + sliderView.trackColor = component.trackForegroundColor + sliderView.knobImage = generateImage(CGSize(width: 40.0, height: 40.0), rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + context.setShadow(offset: CGSize(width: 0.0, height: -3.0), blur: 12.0, color: UIColor(white: 0.0, alpha: 0.25).cgColor) + context.setFillColor(UIColor.white.cgColor) + context.fillEllipse(in: CGRect(origin: CGPoint(x: 6.0, y: 6.0), size: CGSize(width: 28.0, height: 28.0))) + }) + + sliderView.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: size) + sliderView.hitTestEdgeInsets = UIEdgeInsets(top: -sliderView.frame.minX, left: 0.0, bottom: 0.0, right: -sliderView.frame.minX) + + + sliderView.disablesInteractiveTransitionGestureRecognizer = true + sliderView.addTarget(self, action: #selector(self.sliderValueChanged), for: .valueChanged) + sliderView.layer.allowsGroupOpacity = true + self.sliderView = sliderView + self.addSubview(sliderView) + } + sliderView.value = CGFloat(component.value) + sliderView.interactionBegan = { + internalIsTrackingUpdated?(true) + } + sliderView.interactionEnded = { + internalIsTrackingUpdated?(false) + } + + transition.setFrame(view: sliderView, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: availableSize.width, height: 44.0))) + sliderView.hitTestEdgeInsets = UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: 0.0) + + return size + } + + @objc private func sliderValueChanged() { + guard let component = self.component, let sliderView = self.sliderView else { + return + } + component.valueUpdated(Int(sliderView.value)) + } + } + + public func makeView() -> View { + return View(frame: CGRect()) + } + + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} diff --git a/submodules/TelegramUI/Components/Stories/AvatarStoryIndicatorComponent/Sources/AvatarStoryIndicatorComponent.swift b/submodules/TelegramUI/Components/Stories/AvatarStoryIndicatorComponent/Sources/AvatarStoryIndicatorComponent.swift index ebe68f2d334..5cd33daa875 100644 --- a/submodules/TelegramUI/Components/Stories/AvatarStoryIndicatorComponent/Sources/AvatarStoryIndicatorComponent.swift +++ b/submodules/TelegramUI/Components/Stories/AvatarStoryIndicatorComponent/Sources/AvatarStoryIndicatorComponent.swift @@ -345,16 +345,17 @@ public final class AvatarStoryIndicatorComponent: Component { } let colorSpace = CGColorSpaceCreateDeviceRGB() - let gradient = CGGradient(colorsSpace: colorSpace, colors: colors as CFArray, locations: &locations)! - - context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: 0.0, y: size.height), options: CGGradientDrawingOptions()) + if let gradient = CGGradient(colorsSpace: colorSpace, colors: colors as CFArray, locations: &locations) { + context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: 0.0, y: size.height), options: CGGradientDrawingOptions()) + } } } } else { let lineWidth: CGFloat = component.hasUnseen ? component.activeLineWidth : component.inactiveLineWidth context.setLineWidth(lineWidth) if component.isRoundedRect { - context.addPath(UIBezierPath(roundedRect: CGRect(origin: CGPoint(x: size.width * 0.5 - diameter * 0.5, y: size.height * 0.5 - diameter * 0.5), size: size).insetBy(dx: lineWidth * 0.5, dy: lineWidth * 0.5), cornerRadius: floor(diameter * 0.25)).cgPath) + let path = UIBezierPath(roundedRect: CGRect(origin: CGPoint(x: size.width * 0.5 - diameter * 0.5, y: size.height * 0.5 - diameter * 0.5), size: size).insetBy(dx: lineWidth * 0.5, dy: lineWidth * 0.5), cornerRadius: floor(diameter * 0.27)) + context.addPath(path.cgPath) } else { context.addEllipse(in: CGRect(origin: CGPoint(x: size.width * 0.5 - diameter * 0.5, y: size.height * 0.5 - diameter * 0.5), size: size).insetBy(dx: lineWidth * 0.5, dy: lineWidth * 0.5)) } diff --git a/submodules/TelegramUI/Components/Stories/PeerListItemComponent/BUILD b/submodules/TelegramUI/Components/Stories/PeerListItemComponent/BUILD index 0ad1d287a5c..2841b2a498e 100644 --- a/submodules/TelegramUI/Components/Stories/PeerListItemComponent/BUILD +++ b/submodules/TelegramUI/Components/Stories/PeerListItemComponent/BUILD @@ -29,6 +29,8 @@ swift_library( "//submodules/ContextUI", "//submodules/TextFormat", "//submodules/PhotoResources", + "//submodules/TelegramUI/Components/ListSectionComponent", + "//submodules/TelegramUI/Components/ListItemSwipeOptionContainer", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/Stories/PeerListItemComponent/Sources/PeerListItemComponent.swift b/submodules/TelegramUI/Components/Stories/PeerListItemComponent/Sources/PeerListItemComponent.swift index c23e8cae70b..f23ef6eb2a6 100644 --- a/submodules/TelegramUI/Components/Stories/PeerListItemComponent/Sources/PeerListItemComponent.swift +++ b/submodules/TelegramUI/Components/Stories/PeerListItemComponent/Sources/PeerListItemComponent.swift @@ -1,4 +1,5 @@ import Foundation +import Foundation import UIKit import Display import AsyncDisplayKit @@ -19,6 +20,8 @@ import ContextUI import EmojiTextAttachmentView import TextFormat import PhotoResources +import ListSectionComponent +import ListItemSwipeOptionContainer private let avatarFont = avatarPlaceholderFont(size: 15.0) private let readIconImage: UIImage? = generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/MenuReadIcon"), color: .white)?.withRenderingMode(.alwaysTemplate) @@ -64,6 +67,70 @@ public final class PeerListItemComponent: Component { case check } + public struct Avatar: Equatable { + public var icon: String + public var color: AvatarBackgroundColor + public var clipStyle: AvatarNodeClipStyle + + public init(icon: String, color: AvatarBackgroundColor, clipStyle: AvatarNodeClipStyle) { + self.icon = icon + self.color = color + self.clipStyle = clipStyle + } + } + + public final class InlineAction: Equatable { + public enum Color: Equatable { + case destructive + } + + public let id: AnyHashable + public let title: String + public let color: Color + public let action: () -> Void + + public init(id: AnyHashable, title: String, color: Color, action: @escaping () -> Void) { + self.id = id + self.title = title + self.color = color + self.action = action + } + + public static func ==(lhs: InlineAction, rhs: InlineAction) -> Bool { + if lhs === rhs { + return true + } + if lhs.id != rhs.id { + return false + } + if lhs.title != rhs.title { + return false + } + if lhs.color != rhs.color { + return false + } + return true + } + } + + public final class InlineActionsState: Equatable { + public let actions: [InlineAction] + + public init(actions: [InlineAction]) { + self.actions = actions + } + + public static func ==(lhs: InlineActionsState, rhs: InlineActionsState) -> Bool { + if lhs === rhs { + return true + } + if lhs.actions != rhs.actions { + return false + } + return true + } + } + public final class Reaction: Equatable { public let reaction: MessageReaction.Reaction public let file: TelegramMediaFile? @@ -103,6 +170,7 @@ public final class PeerListItemComponent: Component { let style: Style let sideInset: CGFloat let title: String + let avatar: Avatar? let peer: EnginePeer? let storyStats: PeerStoryStats? let subtitle: String? @@ -117,6 +185,7 @@ public final class PeerListItemComponent: Component { let isEnabled: Bool let hasNext: Bool let action: (EnginePeer, EngineMessage.Id?, UIView?) -> Void + let inlineActions: InlineActionsState? let contextAction: ((EnginePeer, ContextExtractedContentContainingView, ContextGesture) -> Void)? let openStories: ((EnginePeer, AvatarNode) -> Void)? @@ -127,6 +196,7 @@ public final class PeerListItemComponent: Component { style: Style, sideInset: CGFloat, title: String, + avatar: Avatar? = nil, peer: EnginePeer?, storyStats: PeerStoryStats? = nil, subtitle: String?, @@ -141,6 +211,7 @@ public final class PeerListItemComponent: Component { isEnabled: Bool = true, hasNext: Bool, action: @escaping (EnginePeer, EngineMessage.Id?, UIView?) -> Void, + inlineActions: InlineActionsState? = nil, contextAction: ((EnginePeer, ContextExtractedContentContainingView, ContextGesture) -> Void)? = nil, openStories: ((EnginePeer, AvatarNode) -> Void)? = nil ) { @@ -150,6 +221,7 @@ public final class PeerListItemComponent: Component { self.style = style self.sideInset = sideInset self.title = title + self.avatar = avatar self.peer = peer self.storyStats = storyStats self.subtitle = subtitle @@ -164,6 +236,7 @@ public final class PeerListItemComponent: Component { self.isEnabled = isEnabled self.hasNext = hasNext self.action = action + self.inlineActions = inlineActions self.contextAction = contextAction self.openStories = openStories } @@ -187,6 +260,9 @@ public final class PeerListItemComponent: Component { if lhs.title != rhs.title { return false } + if lhs.avatar != rhs.avatar { + return false + } if lhs.peer != rhs.peer { return false } @@ -226,17 +302,23 @@ public final class PeerListItemComponent: Component { if lhs.hasNext != rhs.hasNext { return false } + if lhs.inlineActions != rhs.inlineActions { + return false + } return true } - public final class View: ContextControllerSourceView { + public final class View: ContextControllerSourceView, ListSectionComponent.ChildView { private let extractedContainerView: ContextExtractedContentContainingView private let containerButton: HighlightTrackingButton + private let swipeOptionContainer: ListItemSwipeOptionContainer + private let title = ComponentView() private let label = ComponentView() private let separatorLayer: SimpleLayer private let avatarNode: AvatarNode + private var avatarImageView: UIImageView? private let avatarButtonView: HighlightTrackingButton private var avatarIcon: ComponentView? @@ -278,13 +360,19 @@ public final class PeerListItemComponent: Component { private var isExtractedToContextMenu: Bool = false + public var customUpdateIsHighlighted: ((Bool) -> Void)? + public private(set) var separatorInset: CGFloat = 0.0 + override init(frame: CGRect) { self.separatorLayer = SimpleLayer() self.extractedContainerView = ContextExtractedContentContainingView() self.containerButton = HighlightTrackingButton() + self.containerButton.layer.anchorPoint = CGPoint() self.containerButton.isExclusiveTouch = true + self.swipeOptionContainer = ListItemSwipeOptionContainer(frame: CGRect()) + self.avatarNode = AvatarNode(font: avatarFont) self.avatarNode.isLayerBacked = false self.avatarNode.isUserInteractionEnabled = false @@ -296,7 +384,9 @@ public final class PeerListItemComponent: Component { self.addSubview(self.extractedContainerView) self.targetViewForActivationProgress = self.extractedContainerView.contentView - self.extractedContainerView.contentView.addSubview(self.containerButton) + self.extractedContainerView.contentView.addSubview(self.swipeOptionContainer) + + self.swipeOptionContainer.addSubview(self.containerButton) self.layer.addSublayer(self.separatorLayer) self.containerButton.layer.addSublayer(self.avatarNode.layer) @@ -336,6 +426,34 @@ public final class PeerListItemComponent: Component { } component.contextAction?(peer, self.extractedContainerView, gesture) } + + self.containerButton.highligthedChanged = { [weak self] highlighted in + guard let self else { + return + } + if let customUpdateIsHighlighted = self.customUpdateIsHighlighted { + customUpdateIsHighlighted(highlighted) + } + } + + self.swipeOptionContainer.updateRevealOffset = { [weak self] offset, transition in + guard let self else { + return + } + transition.setBounds(view: self.containerButton, bounds: CGRect(origin: CGPoint(x: -offset, y: 0.0), size: self.containerButton.bounds.size)) + } + self.swipeOptionContainer.revealOptionSelected = { [weak self] option, animated in + guard let self, let component = self.component else { + return + } + guard let inlineActions = component.inlineActions else { + return + } + self.swipeOptionContainer.setRevealOptionsOpened(false, animated: animated) + if let inlineAction = inlineActions.actions.first(where: { $0.id == option.key }) { + inlineAction.action() + } + } } required init?(coder: NSCoder) { @@ -560,6 +678,27 @@ public final class PeerListItemComponent: Component { transition.setFrame(view: self.avatarButtonView, frame: avatarFrame) var statusIcon: EmojiStatusComponent.Content? + + if let avatar = component.avatar { + let avatarImageView: UIImageView + if let current = self.avatarImageView { + avatarImageView = current + } else { + avatarImageView = UIImageView() + self.avatarImageView = avatarImageView + self.containerButton.addSubview(avatarImageView) + } + if previousComponent?.avatar != avatar { + avatarImageView.image = generateAvatarImage(size: avatarFrame.size, icon: generateTintedImage(image: UIImage(bundleImageName: avatar.icon), color: .white), cornerRadius: 12.0, color: avatar.color) + } + transition.setFrame(view: avatarImageView, frame: avatarFrame) + } else { + if let avatarImageView = self.avatarImageView { + self.avatarImageView = nil + avatarImageView.removeFromSuperview() + } + } + if let peer = component.peer { let clipStyle: AvatarNodeClipStyle if case let .channel(channel) = peer, channel.flags.contains(.isForum) { @@ -596,6 +735,7 @@ public final class PeerListItemComponent: Component { lineWidth: 1.33, inactiveLineWidth: 1.33 ), transition: transition) + self.avatarNode.isHidden = false if peer.isScam { statusIcon = .text(color: component.theme.chat.message.incoming.scamColor, string: component.strings.Message_ScamAccount.uppercased()) @@ -608,6 +748,8 @@ public final class PeerListItemComponent: Component { } else if peer.isPremium { statusIcon = .premium(color: component.theme.list.itemAccentColor) } + } else { + self.avatarNode.isHidden = true } let previousTitleFrame = self.title.view?.frame @@ -951,7 +1093,38 @@ public final class PeerListItemComponent: Component { self.extractedContainerView.contentRect = resultBounds let containerFrame = CGRect(origin: CGPoint(x: contextInset, y: verticalInset), size: CGSize(width: availableSize.width - contextInset * 2.0, height: height - verticalInset * 2.0)) - transition.setFrame(view: self.containerButton, frame: containerFrame) + + let swipeOptionContainerFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: availableSize.width, height: height)) + transition.setFrame(view: self.swipeOptionContainer, frame: swipeOptionContainerFrame) + + transition.setPosition(view: self.containerButton, position: containerFrame.origin) + transition.setBounds(view: self.containerButton, bounds: CGRect(origin: self.containerButton.bounds.origin, size: containerFrame.size)) + + self.separatorInset = leftInset + + self.swipeOptionContainer.updateLayout(size: swipeOptionContainerFrame.size, leftInset: 0.0, rightInset: 0.0) + + var rightOptions: [ListItemSwipeOptionContainer.Option] = [] + if let inlineActions = component.inlineActions { + rightOptions = inlineActions.actions.map { action in + let color: UIColor + let textColor: UIColor + switch action.color { + case .destructive: + color = component.theme.list.itemDisclosureActions.destructive.fillColor + textColor = component.theme.list.itemDisclosureActions.destructive.foregroundColor + } + + return ListItemSwipeOptionContainer.Option( + key: action.id, + title: action.title, + icon: .none, + color: color, + textColor: textColor + ) + } + } + self.swipeOptionContainer.setRevealOptions(([], rightOptions)) return CGSize(width: availableSize.width, height: height) } diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryAuthorInfoComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryAuthorInfoComponent.swift index 810596bab85..3f3fe6dbcbe 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryAuthorInfoComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryAuthorInfoComponent.swift @@ -134,6 +134,9 @@ final class StoryAuthorInfoComponent: Component { if timeString.count < 6 { combinedString.append(NSAttributedString(string: " • \(timeString)", font: Font.regular(11.0), textColor: subtitleColor)) } + if component.isEdited { + combinedString.append(NSAttributedString(string: " • \(component.strings.Story_HeaderEdited)", font: Font.regular(11.0), textColor: subtitleColor)) + } subtitle = combinedString subtitleTruncationType = .middle } else { diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryChatContent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryChatContent.swift index 1146bf3101c..dc8f056aabd 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryChatContent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryChatContent.swift @@ -990,7 +990,7 @@ public final class StoryContentContextImpl: StoryContentContext { } var selectedMedia: EngineMedia - if let slice = stateValue.slice, let alternativeMedia = item.alternativeMedia, !slice.additionalPeerData.preferHighQualityStories { + if let slice = stateValue.slice, let alternativeMedia = item.alternativeMedia, (!slice.additionalPeerData.preferHighQualityStories && !item.isMy) { selectedMedia = alternativeMedia } else { selectedMedia = item.media @@ -1642,7 +1642,7 @@ public final class PeerStoryListContentContextImpl: StoryContentContext { } var selectedMedia: EngineMedia - if let alternativeMedia = item.alternativeMedia, !preferHighQualityStories { + if let alternativeMedia = item.alternativeMedia, (!preferHighQualityStories && !item.isMy) { selectedMedia = alternativeMedia } else { selectedMedia = item.media @@ -2880,7 +2880,7 @@ public final class RepostStoriesContentContextImpl: StoryContentContext { } var selectedMedia: EngineMedia - if let slice = stateValue.slice, let alternativeMedia = item.alternativeMedia, !slice.additionalPeerData.preferHighQualityStories { + if let slice = stateValue.slice, let alternativeMedia = item.alternativeMedia, (!slice.additionalPeerData.preferHighQualityStories && !item.isMy) { selectedMedia = alternativeMedia } else { selectedMedia = item.media diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemContentComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemContentComponent.swift index 76fb1778b63..fed8cb12203 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemContentComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemContentComponent.swift @@ -593,7 +593,7 @@ final class StoryItemContentComponent: Component { let selectedMedia: EngineMedia var messageMedia: EngineMedia? - if !component.preferHighQuality, let alternativeMedia = component.item.alternativeMedia { + if !component.preferHighQuality, !component.item.isMy, let alternativeMedia = component.item.alternativeMedia { selectedMedia = alternativeMedia switch alternativeMedia { diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift index 14aea4a93b9..412e4c50ff3 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift @@ -5420,6 +5420,7 @@ public final class StoryItemSetContainerComponent: Component { var updateProgressImpl: ((Float) -> Void)? let controller = MediaEditorScreen( context: context, + mode: .storyEditor, subject: subject, isEditing: !repost, forwardSource: repost ? (component.slice.peer, item) : nil, @@ -6857,7 +6858,7 @@ public final class StoryItemSetContainerComponent: Component { }))) } - if case let .file(file) = component.slice.item.storyItem.media, file.isVideo { + if !component.slice.item.storyItem.isMy, case let .file(file) = component.slice.item.storyItem.media, file.isVideo { let isHq = component.slice.additionalPeerData.preferHighQualityStories items.append(.action(ContextMenuActionItem(text: isHq ? component.strings.Story_ContextMenuSD : component.strings.Story_ContextMenuHD, icon: { theme in if isHq { diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift index d374b63b525..d257ff5efa3 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift @@ -179,6 +179,7 @@ final class StoryItemSetContainerSendMessage { self.inputPanelExternalState?.deleteBackward() } }, + openStickerEditor: {}, presentController: { [weak self] c, a in if let self { self.view?.component?.controller()?.present(c, in: .window(.root), with: a) diff --git a/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/Sources/StoryPeerListComponent.swift b/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/Sources/StoryPeerListComponent.swift index d0af5a34fc6..0b79cb0ebb9 100644 --- a/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/Sources/StoryPeerListComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/Sources/StoryPeerListComponent.swift @@ -590,7 +590,7 @@ public final class StoryPeerListComponent: Component { case .premium: statusContent = .premium(color: component.theme.list.itemAccentColor) case let .emoji(emoji): - statusContent = .animation(content: .customEmoji(fileId: emoji.fileId), size: CGSize(width: 22.0, height: 22.0), placeholderColor: component.theme.list.mediaPlaceholderColor, themeColor: component.theme.list.itemAccentColor, loopMode: .count(2)) + statusContent = .animation(content: .customEmoji(fileId: emoji.fileId), size: CGSize(width: 44.0, height: 44.0), placeholderColor: component.theme.list.mediaPlaceholderColor, themeColor: component.theme.list.itemAccentColor, loopMode: .count(2)) } var animateStatusTransition = false diff --git a/submodules/TelegramUI/Components/TextFieldComponent/Sources/TextFieldComponent.swift b/submodules/TelegramUI/Components/TextFieldComponent/Sources/TextFieldComponent.swift index a4779139ada..7fa657812c9 100644 --- a/submodules/TelegramUI/Components/TextFieldComponent/Sources/TextFieldComponent.swift +++ b/submodules/TelegramUI/Components/TextFieldComponent/Sources/TextFieldComponent.swift @@ -96,6 +96,8 @@ public final class TextFieldComponent: Component { public let customInputView: UIView? public let resetText: NSAttributedString? public let isOneLineWhenUnfocused: Bool + public let characterLimit: Int? + public let allowEmptyLines: Bool public let formatMenuAvailability: FormatMenuAvailability public let lockedFormatAction: () -> Void public let present: (ViewController) -> Void @@ -112,6 +114,8 @@ public final class TextFieldComponent: Component { customInputView: UIView?, resetText: NSAttributedString?, isOneLineWhenUnfocused: Bool, + characterLimit: Int? = nil, + allowEmptyLines: Bool = true, formatMenuAvailability: FormatMenuAvailability, lockedFormatAction: @escaping () -> Void, present: @escaping (ViewController) -> Void, @@ -127,6 +131,8 @@ public final class TextFieldComponent: Component { self.customInputView = customInputView self.resetText = resetText self.isOneLineWhenUnfocused = isOneLineWhenUnfocused + self.characterLimit = characterLimit + self.allowEmptyLines = allowEmptyLines self.formatMenuAvailability = formatMenuAvailability self.lockedFormatAction = lockedFormatAction self.present = present @@ -161,6 +167,12 @@ public final class TextFieldComponent: Component { if lhs.isOneLineWhenUnfocused != rhs.isOneLineWhenUnfocused { return false } + if lhs.characterLimit != rhs.characterLimit { + return false + } + if lhs.allowEmptyLines != rhs.allowEmptyLines { + return false + } if lhs.formatMenuAvailability != rhs.formatMenuAvailability { return false } @@ -193,7 +205,7 @@ public final class TextFieldComponent: Component { private let ellipsisView = ComponentView() - private var inputState: InputState { + public var inputState: InputState { let selectionRange: Range = self.textView.selectedRange.location ..< (self.textView.selectedRange.location + self.textView.selectedRange.length) return InputState(inputText: stateAttributedStringForText(self.textView.attributedText ?? NSAttributedString()), selectionRange: selectionRange) } @@ -537,6 +549,24 @@ public final class TextFieldComponent: Component { } public func chatInputTextNode(shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool { + guard let component = self.component else { + return true + } + if let characterLimit = component.characterLimit { + let string = self.inputState.inputText.string as NSString + let updatedString = string.replacingCharacters(in: range, with: text) + if (updatedString as NSString).length > characterLimit { + return false + } + } + if !component.allowEmptyLines { + let string = self.inputState.inputText.string as NSString + let updatedString = string.replacingCharacters(in: range, with: text) + if updatedString.range(of: "\n\n") != nil { + return false + } + } + return true } diff --git a/submodules/TelegramUI/Components/TimeSelectionActionSheet/BUILD b/submodules/TelegramUI/Components/TimeSelectionActionSheet/BUILD new file mode 100644 index 00000000000..b4680d882db --- /dev/null +++ b/submodules/TelegramUI/Components/TimeSelectionActionSheet/BUILD @@ -0,0 +1,25 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "TimeSelectionActionSheet", + module_name = "TimeSelectionActionSheet", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/Display", + "//submodules/AsyncDisplayKit", + "//submodules/SSignalKit/SwiftSignalKit", + "//submodules/TelegramCore", + "//submodules/TelegramPresentationData", + "//submodules/TelegramStringFormatting", + "//submodules/AccountContext", + "//submodules/UIKitRuntimeUtils", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Components/TimeSelectionActionSheet/Sources/TimeSelectionActionSheet.swift b/submodules/TelegramUI/Components/TimeSelectionActionSheet/Sources/TimeSelectionActionSheet.swift new file mode 100644 index 00000000000..8ec3ff48af3 --- /dev/null +++ b/submodules/TelegramUI/Components/TimeSelectionActionSheet/Sources/TimeSelectionActionSheet.swift @@ -0,0 +1,132 @@ +import Foundation +import Display +import AsyncDisplayKit +import UIKit +import SwiftSignalKit +import TelegramCore +import TelegramPresentationData +import TelegramStringFormatting +import AccountContext +import UIKitRuntimeUtils + +public final class TimeSelectionActionSheet: ActionSheetController { + private var presentationDisposable: Disposable? + + private let _ready = Promise() + override public var ready: Promise { + return self._ready + } + + public init(context: AccountContext, currentValue: Int32, emptyTitle: String? = nil, applyValue: @escaping (Int32?) -> Void) { + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + let strings = presentationData.strings + + super.init(theme: ActionSheetControllerTheme(presentationData: presentationData)) + + self.presentationDisposable = context.sharedContext.presentationData.start(next: { [weak self] presentationData in + if let strongSelf = self { + strongSelf.theme = ActionSheetControllerTheme(presentationData: presentationData) + } + }) + + self._ready.set(.single(true)) + + var updatedValue = currentValue + var items: [ActionSheetItem] = [] + items.append(TimeSelectionActionSheetItem(strings: strings, currentValue: currentValue, valueChanged: { value in + updatedValue = value + })) + if let emptyTitle = emptyTitle { + items.append(ActionSheetButtonItem(title: emptyTitle, action: { [weak self] in + self?.dismissAnimated() + applyValue(nil) + })) + } + items.append(ActionSheetButtonItem(title: strings.Wallpaper_Set, action: { [weak self] in + self?.dismissAnimated() + applyValue(updatedValue) + })) + self.setItemGroups([ + ActionSheetItemGroup(items: items), + ActionSheetItemGroup(items: [ + ActionSheetButtonItem(title: strings.Common_Cancel, action: { [weak self] in + self?.dismissAnimated() + }), + ]) + ]) + } + + required public init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + self.presentationDisposable?.dispose() + } +} + +private final class TimeSelectionActionSheetItem: ActionSheetItem { + let strings: PresentationStrings + + let currentValue: Int32 + let valueChanged: (Int32) -> Void + + init(strings: PresentationStrings, currentValue: Int32, valueChanged: @escaping (Int32) -> Void) { + self.strings = strings + self.currentValue = currentValue + self.valueChanged = valueChanged + } + + func node(theme: ActionSheetControllerTheme) -> ActionSheetItemNode { + return TimeSelectionActionSheetItemNode(theme: theme, strings: self.strings, currentValue: self.currentValue, valueChanged: self.valueChanged) + } + + func updateNode(_ node: ActionSheetItemNode) { + } +} + +private final class TimeSelectionActionSheetItemNode: ActionSheetItemNode { + private let theme: ActionSheetControllerTheme + private let strings: PresentationStrings + + private let valueChanged: (Int32) -> Void + private let pickerView: UIDatePicker + + init(theme: ActionSheetControllerTheme, strings: PresentationStrings, currentValue: Int32, valueChanged: @escaping (Int32) -> Void) { + self.theme = theme + self.strings = strings + self.valueChanged = valueChanged + + UILabel.setDateLabel(theme.primaryTextColor) + + self.pickerView = UIDatePicker() + self.pickerView.datePickerMode = .countDownTimer + self.pickerView.datePickerMode = .time + self.pickerView.timeZone = TimeZone(secondsFromGMT: 0) + self.pickerView.date = Date(timeIntervalSince1970: Double(currentValue)) + self.pickerView.locale = Locale.current + if #available(iOS 13.4, *) { + self.pickerView.preferredDatePickerStyle = .wheels + } + self.pickerView.setValue(theme.primaryTextColor, forKey: "textColor") + + super.init(theme: theme) + + self.view.addSubview(self.pickerView) + self.pickerView.addTarget(self, action: #selector(self.datePickerUpdated), for: .valueChanged) + } + + public override func updateLayout(constrainedSize: CGSize, transition: ContainedViewLayoutTransition) -> CGSize { + let size = CGSize(width: constrainedSize.width, height: 216.0) + + self.pickerView.frame = CGRect(origin: CGPoint(), size: size) + + self.updateInternalLayout(size, constrainedSize: constrainedSize) + return size + } + + @objc private func datePickerUpdated() { + self.valueChanged(Int32(self.pickerView.date.timeIntervalSince1970)) + } +} + diff --git a/submodules/TelegramUI/Components/TokenListTextField/Sources/TokenListTextField.swift b/submodules/TelegramUI/Components/TokenListTextField/Sources/TokenListTextField.swift index a5c33584308..d46862dac97 100644 --- a/submodules/TelegramUI/Components/TokenListTextField/Sources/TokenListTextField.swift +++ b/submodules/TelegramUI/Components/TokenListTextField/Sources/TokenListTextField.swift @@ -88,6 +88,7 @@ public final class TokenListTextField: Component { public let tokens: [Token] public let sideInset: CGFloat public let deleteToken: (AnyHashable) -> Void + public let isFocusedUpdated: (Bool) -> Void public init( externalState: ExternalState, @@ -96,7 +97,8 @@ public final class TokenListTextField: Component { placeholder: String, tokens: [Token], sideInset: CGFloat, - deleteToken: @escaping (AnyHashable) -> Void + deleteToken: @escaping (AnyHashable) -> Void, + isFocusedUpdated: @escaping (Bool) -> Void = { _ in } ) { self.externalState = externalState self.context = context @@ -105,6 +107,7 @@ public final class TokenListTextField: Component { self.tokens = tokens self.sideInset = sideInset self.deleteToken = deleteToken + self.isFocusedUpdated = isFocusedUpdated } public static func ==(lhs: TokenListTextField, rhs: TokenListTextField) -> Bool { @@ -191,6 +194,7 @@ public final class TokenListTextField: Component { guard let self else { return } + self.component?.isFocusedUpdated(self.tokenListNode?.isFocused ?? false) self.componentState?.updated(transition: Transition(animation: .curve(duration: 0.35, curve: .spring))) } diff --git a/submodules/TelegramUI/Components/VideoMessageCameraScreen/Sources/VideoMessageCameraScreen.swift b/submodules/TelegramUI/Components/VideoMessageCameraScreen/Sources/VideoMessageCameraScreen.swift index 1e6a8f738ce..1f0abf27446 100644 --- a/submodules/TelegramUI/Components/VideoMessageCameraScreen/Sources/VideoMessageCameraScreen.swift +++ b/submodules/TelegramUI/Components/VideoMessageCameraScreen/Sources/VideoMessageCameraScreen.swift @@ -263,23 +263,25 @@ private final class VideoMessageCameraScreenComponent: CombinedComponent { controller.updateCameraState({ $0.updatedRecording(pressing ? .holding : .handsFree).updatedDuration(initialDuration) }, transition: .spring(duration: 0.4)) controller.node.withReadyCamera(isFirstTime: !controller.node.cameraIsActive) { - self.resultDisposable.set((camera.startRecording() - |> deliverOnMainQueue).start(next: { [weak self] recordingData in - let duration = initialDuration + recordingData.duration - if let self, let controller = self.getController() { - controller.updateCameraState({ $0.updatedDuration(duration) }, transition: .easeInOut(duration: 0.1)) - if isFirstRecording { - controller.node.setupLiveUpload(filePath: recordingData.filePath) + Queue.mainQueue().after(0.15) { + self.resultDisposable.set((camera.startRecording() + |> deliverOnMainQueue).start(next: { [weak self] recordingData in + let duration = initialDuration + recordingData.duration + if let self, let controller = self.getController() { + controller.updateCameraState({ $0.updatedDuration(duration) }, transition: .easeInOut(duration: 0.1)) + if isFirstRecording { + controller.node.setupLiveUpload(filePath: recordingData.filePath) + } + if duration > 59.5 { + controller.onStop() + } } - if duration > 59.5 { - controller.onStop() + }, error: { [weak self] _ in + if let self, let controller = self.getController() { + controller.completion(nil, nil, nil) } - } - }, error: { [weak self] _ in - if let self, let controller = self.getController() { - controller.completion(nil, nil, nil) - } - })) + })) + } } if initialDuration > 0.0 { @@ -1104,6 +1106,7 @@ public class VideoMessageCameraScreen: ViewController { bottom: 44.0, right: layout.safeInsets.right ), + additionalInsets: layout.additionalInsets, inputHeight: layout.inputHeight ?? 0.0, metrics: layout.metrics, deviceMetrics: layout.deviceMetrics, @@ -1693,9 +1696,9 @@ public class VideoMessageCameraScreen: ViewController { private func requestAudioSession() { let audioSessionType: ManagedAudioSessionType if self.context.sharedContext.currentMediaInputSettings.with({ $0 }).pauseMusicOnRecording { - audioSessionType = .record(speaker: false, video: true, withOthers: false) + audioSessionType = .record(speaker: false, video: false, withOthers: false) } else { - audioSessionType = .record(speaker: false, video: true, withOthers: true) + audioSessionType = .record(speaker: false, video: false, withOthers: true) } self.audioSessionDisposable = self.context.sharedContext.mediaManager.audioSession.push(audioSessionType: audioSessionType, activate: { [weak self] _ in diff --git a/submodules/TelegramUI/Images.xcassets/Chat List/Filters/Chats.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Chat List/Filters/Chats.imageset/Contents.json new file mode 100644 index 00000000000..640ffa7fd91 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chat List/Filters/Chats.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "existing.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Chat List/Filters/Chats.imageset/existing.pdf b/submodules/TelegramUI/Images.xcassets/Chat List/Filters/Chats.imageset/existing.pdf new file mode 100644 index 00000000000..455ed1a7c17 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Chat List/Filters/Chats.imageset/existing.pdf differ diff --git a/submodules/TelegramUI/Images.xcassets/Chat List/Filters/NewChats.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Chat List/Filters/NewChats.imageset/Contents.json new file mode 100644 index 00000000000..8cc615202a9 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chat List/Filters/NewChats.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "unread.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Chat List/Filters/NewChats.imageset/unread.pdf b/submodules/TelegramUI/Images.xcassets/Chat List/Filters/NewChats.imageset/unread.pdf new file mode 100644 index 00000000000..7aa7e23a5bb Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Chat List/Filters/NewChats.imageset/unread.pdf differ diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Attach Menu/Reply.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Chat/Attach Menu/Reply.imageset/Contents.json new file mode 100644 index 00000000000..358678e10dd --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chat/Attach Menu/Reply.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "replies.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Attach Menu/Reply.imageset/replies.pdf b/submodules/TelegramUI/Images.xcassets/Chat/Attach Menu/Reply.imageset/replies.pdf new file mode 100644 index 00000000000..6360561a074 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Chat/Attach Menu/Reply.imageset/replies.pdf differ diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Empty Chat/AwayShortcut.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Chat/Empty Chat/AwayShortcut.imageset/Contents.json new file mode 100644 index 00000000000..af693ca66b1 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chat/Empty Chat/AwayShortcut.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "awaydemo.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Empty Chat/AwayShortcut.imageset/awaydemo.pdf b/submodules/TelegramUI/Images.xcassets/Chat/Empty Chat/AwayShortcut.imageset/awaydemo.pdf new file mode 100644 index 00000000000..57391563d73 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Chat/Empty Chat/AwayShortcut.imageset/awaydemo.pdf differ diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Empty Chat/GreetingShortcut.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Chat/Empty Chat/GreetingShortcut.imageset/Contents.json new file mode 100644 index 00000000000..747557322ca --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chat/Empty Chat/GreetingShortcut.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "greetingdemo.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Empty Chat/GreetingShortcut.imageset/greetingdemo.pdf b/submodules/TelegramUI/Images.xcassets/Chat/Empty Chat/GreetingShortcut.imageset/greetingdemo.pdf new file mode 100644 index 00000000000..b1a13e2cc83 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Chat/Empty Chat/GreetingShortcut.imageset/greetingdemo.pdf differ diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Empty Chat/QuickReplies.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Chat/Empty Chat/QuickReplies.imageset/Contents.json new file mode 100644 index 00000000000..5146f4aea05 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chat/Empty Chat/QuickReplies.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "quickrepliesdemo.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Empty Chat/QuickReplies.imageset/quickrepliesdemo.pdf b/submodules/TelegramUI/Images.xcassets/Chat/Empty Chat/QuickReplies.imageset/quickrepliesdemo.pdf new file mode 100644 index 00000000000..7dd7ee40f28 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Chat/Empty Chat/QuickReplies.imageset/quickrepliesdemo.pdf differ diff --git a/submodules/TelegramUI/Images.xcassets/Item List/AddTimeIcon.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Item List/AddTimeIcon.imageset/Contents.json new file mode 100644 index 00000000000..fbc5573e6d6 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Item List/AddTimeIcon.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "addclock_30.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Item List/AddTimeIcon.imageset/addclock_30.pdf b/submodules/TelegramUI/Images.xcassets/Item List/AddTimeIcon.imageset/addclock_30.pdf new file mode 100644 index 00000000000..9ba3b68f937 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Item List/AddTimeIcon.imageset/addclock_30.pdf differ diff --git a/submodules/TelegramUI/Images.xcassets/Item List/DownArrow.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Item List/DownArrow.imageset/Contents.json new file mode 100644 index 00000000000..ea04ba34971 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Item List/DownArrow.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "arrow (1).pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Media Editor/Apply.imageset/check.pdf b/submodules/TelegramUI/Images.xcassets/Item List/DownArrow.imageset/arrow (1).pdf similarity index 53% rename from submodules/TelegramUI/Images.xcassets/Media Editor/Apply.imageset/check.pdf rename to submodules/TelegramUI/Images.xcassets/Item List/DownArrow.imageset/arrow (1).pdf index c4dfcbde1b3..2437bf015c4 100644 Binary files a/submodules/TelegramUI/Images.xcassets/Media Editor/Apply.imageset/check.pdf and b/submodules/TelegramUI/Images.xcassets/Item List/DownArrow.imageset/arrow (1).pdf differ diff --git a/submodules/TelegramUI/Images.xcassets/Media Editor/Apply.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Media Editor/Apply.imageset/Contents.json index 37accc5389e..cad3335c661 100644 --- a/submodules/TelegramUI/Images.xcassets/Media Editor/Apply.imageset/Contents.json +++ b/submodules/TelegramUI/Images.xcassets/Media Editor/Apply.imageset/Contents.json @@ -1,7 +1,7 @@ { "images" : [ { - "filename" : "check.pdf", + "filename" : "arrowhead_30.pdf", "idiom" : "universal" } ], diff --git a/submodules/TelegramUI/Images.xcassets/Media Editor/Apply.imageset/arrowhead_30.pdf b/submodules/TelegramUI/Images.xcassets/Media Editor/Apply.imageset/arrowhead_30.pdf new file mode 100644 index 00000000000..c646e62924a Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Media Editor/Apply.imageset/arrowhead_30.pdf differ diff --git a/submodules/TelegramUI/Images.xcassets/Media Editor/Cutout.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Media Editor/Cutout.imageset/Contents.json new file mode 100644 index 00000000000..bc673b85bd6 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Media Editor/Cutout.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "magic_30.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Media Editor/Cutout.imageset/magic_30.pdf b/submodules/TelegramUI/Images.xcassets/Media Editor/Cutout.imageset/magic_30.pdf new file mode 100644 index 00000000000..2cfdca5431b Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Media Editor/Cutout.imageset/magic_30.pdf differ diff --git a/submodules/TelegramUI/Images.xcassets/Media Editor/CutoutUndo.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Media Editor/CutoutUndo.imageset/Contents.json new file mode 100644 index 00000000000..4a8e6450d0f --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Media Editor/CutoutUndo.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "undo2_30.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Media Editor/CutoutUndo.imageset/undo2_30.pdf b/submodules/TelegramUI/Images.xcassets/Media Editor/CutoutUndo.imageset/undo2_30.pdf new file mode 100644 index 00000000000..e814c8183de Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Media Editor/CutoutUndo.imageset/undo2_30.pdf differ diff --git a/submodules/TelegramUI/Images.xcassets/Premium/Business/Away.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Premium/Business/Away.imageset/Contents.json new file mode 100644 index 00000000000..10d5d09d15e --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Premium/Business/Away.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "sleep_30.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Premium/Business/Away.imageset/sleep_30.pdf b/submodules/TelegramUI/Images.xcassets/Premium/Business/Away.imageset/sleep_30.pdf new file mode 100644 index 00000000000..ea3295cd4c0 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Premium/Business/Away.imageset/sleep_30.pdf differ diff --git a/submodules/TelegramUI/Images.xcassets/Premium/Business/Chatbots.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Premium/Business/Chatbots.imageset/Contents.json new file mode 100644 index 00000000000..e582efcfd30 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Premium/Business/Chatbots.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "bot_30.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Premium/Business/Chatbots.imageset/bot_30.pdf b/submodules/TelegramUI/Images.xcassets/Premium/Business/Chatbots.imageset/bot_30.pdf new file mode 100644 index 00000000000..04f4a2db48b Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Premium/Business/Chatbots.imageset/bot_30.pdf differ diff --git a/submodules/TelegramUI/Images.xcassets/Premium/Business/Contents.json b/submodules/TelegramUI/Images.xcassets/Premium/Business/Contents.json new file mode 100644 index 00000000000..6e965652df6 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Premium/Business/Contents.json @@ -0,0 +1,9 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "provides-namespace" : true + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Premium/Business/Greetings.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Premium/Business/Greetings.imageset/Contents.json new file mode 100644 index 00000000000..3f0cc65f028 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Premium/Business/Greetings.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "greeting_30.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Premium/Business/Greetings.imageset/greeting_30.pdf b/submodules/TelegramUI/Images.xcassets/Premium/Business/Greetings.imageset/greeting_30.pdf new file mode 100644 index 00000000000..d2717c45d4d Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Premium/Business/Greetings.imageset/greeting_30.pdf differ diff --git a/submodules/TelegramUI/Images.xcassets/Premium/Business/Hours.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Premium/Business/Hours.imageset/Contents.json new file mode 100644 index 00000000000..4782f126a7c --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Premium/Business/Hours.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "clock_30.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Premium/Business/Hours.imageset/clock_30.pdf b/submodules/TelegramUI/Images.xcassets/Premium/Business/Hours.imageset/clock_30.pdf new file mode 100644 index 00000000000..a90192a2f00 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Premium/Business/Hours.imageset/clock_30.pdf differ diff --git a/submodules/TelegramUI/Images.xcassets/Premium/Business/Location.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Premium/Business/Location.imageset/Contents.json new file mode 100644 index 00000000000..904d28263a3 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Premium/Business/Location.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "location_30.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Premium/Business/Location.imageset/location_30.pdf b/submodules/TelegramUI/Images.xcassets/Premium/Business/Location.imageset/location_30.pdf new file mode 100644 index 00000000000..25df154e349 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Premium/Business/Location.imageset/location_30.pdf differ diff --git a/submodules/TelegramUI/Images.xcassets/Premium/Business/Replies.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Premium/Business/Replies.imageset/Contents.json new file mode 100644 index 00000000000..5c235cd01de --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Premium/Business/Replies.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "arrowshape_30.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Premium/Business/Replies.imageset/arrowshape_30.pdf b/submodules/TelegramUI/Images.xcassets/Premium/Business/Replies.imageset/arrowshape_30.pdf new file mode 100644 index 00000000000..75fe22b4f73 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Premium/Business/Replies.imageset/arrowshape_30.pdf differ diff --git a/submodules/TelegramUI/Images.xcassets/Premium/BusinessPerk/Away.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Premium/BusinessPerk/Away.imageset/Contents.json new file mode 100644 index 00000000000..b9b3b2f4363 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Premium/BusinessPerk/Away.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "bubble_30.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Premium/BusinessPerk/Away.imageset/bubble_30.pdf b/submodules/TelegramUI/Images.xcassets/Premium/BusinessPerk/Away.imageset/bubble_30.pdf new file mode 100644 index 00000000000..23cc2b43c6d Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Premium/BusinessPerk/Away.imageset/bubble_30.pdf differ diff --git a/submodules/TelegramUI/Images.xcassets/Premium/BusinessPerk/Chatbots.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Premium/BusinessPerk/Chatbots.imageset/Contents.json new file mode 100644 index 00000000000..be5f42810cd --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Premium/BusinessPerk/Chatbots.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "bots_30.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Premium/BusinessPerk/Chatbots.imageset/bots_30.pdf b/submodules/TelegramUI/Images.xcassets/Premium/BusinessPerk/Chatbots.imageset/bots_30.pdf new file mode 100644 index 00000000000..34b8eee230e Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Premium/BusinessPerk/Chatbots.imageset/bots_30.pdf differ diff --git a/submodules/TelegramUI/Images.xcassets/Premium/BusinessPerk/Contents.json b/submodules/TelegramUI/Images.xcassets/Premium/BusinessPerk/Contents.json new file mode 100644 index 00000000000..6e965652df6 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Premium/BusinessPerk/Contents.json @@ -0,0 +1,9 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "provides-namespace" : true + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Premium/BusinessPerk/Greetings.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Premium/BusinessPerk/Greetings.imageset/Contents.json new file mode 100644 index 00000000000..93a1282c319 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Premium/BusinessPerk/Greetings.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "hand_30.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Premium/BusinessPerk/Greetings.imageset/hand_30.pdf b/submodules/TelegramUI/Images.xcassets/Premium/BusinessPerk/Greetings.imageset/hand_30.pdf new file mode 100644 index 00000000000..f0f8b105471 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Premium/BusinessPerk/Greetings.imageset/hand_30.pdf differ diff --git a/submodules/TelegramUI/Images.xcassets/Premium/BusinessPerk/Hours.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Premium/BusinessPerk/Hours.imageset/Contents.json new file mode 100644 index 00000000000..4782f126a7c --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Premium/BusinessPerk/Hours.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "clock_30.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Premium/BusinessPerk/Hours.imageset/clock_30.pdf b/submodules/TelegramUI/Images.xcassets/Premium/BusinessPerk/Hours.imageset/clock_30.pdf new file mode 100644 index 00000000000..0f46a28840b Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Premium/BusinessPerk/Hours.imageset/clock_30.pdf differ diff --git a/submodules/TelegramUI/Images.xcassets/Premium/BusinessPerk/Location.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Premium/BusinessPerk/Location.imageset/Contents.json new file mode 100644 index 00000000000..904d28263a3 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Premium/BusinessPerk/Location.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "location_30.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Premium/BusinessPerk/Location.imageset/location_30.pdf b/submodules/TelegramUI/Images.xcassets/Premium/BusinessPerk/Location.imageset/location_30.pdf new file mode 100644 index 00000000000..07360fc3226 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Premium/BusinessPerk/Location.imageset/location_30.pdf differ diff --git a/submodules/TelegramUI/Images.xcassets/Premium/BusinessPerk/Replies.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Premium/BusinessPerk/Replies.imageset/Contents.json new file mode 100644 index 00000000000..ac64efccdb7 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Premium/BusinessPerk/Replies.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "stories_30.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Premium/BusinessPerk/Replies.imageset/stories_30.pdf b/submodules/TelegramUI/Images.xcassets/Premium/BusinessPerk/Replies.imageset/stories_30.pdf new file mode 100644 index 00000000000..ba64f0a7547 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Premium/BusinessPerk/Replies.imageset/stories_30.pdf differ diff --git a/submodules/TelegramUI/Images.xcassets/Premium/BusinessPerk/Status.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Premium/BusinessPerk/Status.imageset/Contents.json new file mode 100644 index 00000000000..4cce7e34664 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Premium/BusinessPerk/Status.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "work_30.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Premium/BusinessPerk/Status.imageset/work_30.pdf b/submodules/TelegramUI/Images.xcassets/Premium/BusinessPerk/Status.imageset/work_30.pdf new file mode 100644 index 00000000000..0710349e369 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Premium/BusinessPerk/Status.imageset/work_30.pdf differ diff --git a/submodules/TelegramUI/Images.xcassets/Premium/BusinessPerk/Tag.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Premium/BusinessPerk/Tag.imageset/Contents.json new file mode 100644 index 00000000000..81d7a4af903 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Premium/BusinessPerk/Tag.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "tag_30 (3).pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Premium/BusinessPerk/Tag.imageset/tag_30 (3).pdf b/submodules/TelegramUI/Images.xcassets/Premium/BusinessPerk/Tag.imageset/tag_30 (3).pdf new file mode 100644 index 00000000000..f487557273a Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Premium/BusinessPerk/Tag.imageset/tag_30 (3).pdf differ diff --git a/submodules/TelegramUI/Images.xcassets/Premium/Perk/Business.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Premium/Perk/Business.imageset/Contents.json new file mode 100644 index 00000000000..2c8c5ec09d2 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Premium/Perk/Business.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "business.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Premium/Perk/Business.imageset/business.pdf b/submodules/TelegramUI/Images.xcassets/Premium/Perk/Business.imageset/business.pdf new file mode 100644 index 00000000000..4bcd61ee0f3 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Premium/Perk/Business.imageset/business.pdf differ diff --git a/submodules/TelegramUI/Images.xcassets/Settings/Menu/Business.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Settings/Menu/Business.imageset/Contents.json new file mode 100644 index 00000000000..d1c4d41aef4 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Settings/Menu/Business.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "business_30.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Settings/Menu/Business.imageset/business_30.pdf b/submodules/TelegramUI/Images.xcassets/Settings/Menu/Business.imageset/business_30.pdf new file mode 100644 index 00000000000..374eb7b7a72 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Settings/Menu/Business.imageset/business_30.pdf differ diff --git a/submodules/TelegramUI/Resources/Animations/BusinessHoursEmoji.tgs b/submodules/TelegramUI/Resources/Animations/BusinessHoursEmoji.tgs new file mode 100644 index 00000000000..9fbdb0c152b Binary files /dev/null and b/submodules/TelegramUI/Resources/Animations/BusinessHoursEmoji.tgs differ diff --git a/submodules/TelegramUI/Resources/Animations/HandWaveEmoji.tgs b/submodules/TelegramUI/Resources/Animations/HandWaveEmoji.tgs new file mode 100644 index 00000000000..dd1ab78d28f Binary files /dev/null and b/submodules/TelegramUI/Resources/Animations/HandWaveEmoji.tgs differ diff --git a/submodules/TelegramUI/Resources/Animations/MapEmoji.tgs b/submodules/TelegramUI/Resources/Animations/MapEmoji.tgs new file mode 100644 index 00000000000..32ba54f16cd Binary files /dev/null and b/submodules/TelegramUI/Resources/Animations/MapEmoji.tgs differ diff --git a/submodules/TelegramUI/Resources/Animations/WriteEmoji.tgs b/submodules/TelegramUI/Resources/Animations/WriteEmoji.tgs new file mode 100644 index 00000000000..47caac05a2f Binary files /dev/null and b/submodules/TelegramUI/Resources/Animations/WriteEmoji.tgs differ diff --git a/submodules/TelegramUI/Resources/Animations/ZzzEmoji.tgs b/submodules/TelegramUI/Resources/Animations/ZzzEmoji.tgs new file mode 100644 index 00000000000..b766d6e438e Binary files /dev/null and b/submodules/TelegramUI/Resources/Animations/ZzzEmoji.tgs differ diff --git a/submodules/TelegramUI/Sources/AccountContext.swift b/submodules/TelegramUI/Sources/AccountContext.swift index a7dce56b2ec..fd14a86226d 100644 --- a/submodules/TelegramUI/Sources/AccountContext.swift +++ b/submodules/TelegramUI/Sources/AccountContext.swift @@ -483,7 +483,7 @@ public final class AccountContextImpl: AccountContext { let context = chatLocationContext(holder: contextHolder, account: self.account, data: data) return .thread(peerId: data.peerId, threadId: data.threadId, data: context.state) } - case .feed: + case .customChatContents: preconditionFailure() } } @@ -509,7 +509,7 @@ public final class AccountContextImpl: AccountContext { } else { return .single(nil) } - case .feed: + case .customChatContents: return .single(nil) } } @@ -547,7 +547,7 @@ public final class AccountContextImpl: AccountContext { let context = chatLocationContext(holder: contextHolder, account: self.account, data: data) return context.unreadCount } - case .feed: + case .customChatContents: return .single(0) } } @@ -559,7 +559,7 @@ public final class AccountContextImpl: AccountContext { case let .replyThread(data): let context = chatLocationContext(holder: contextHolder, account: self.account, data: data) context.applyMaxReadIndex(messageIndex: messageIndex) - case .feed: + case .customChatContents: break } } diff --git a/submodules/TelegramUI/Sources/AppDelegate.swift b/submodules/TelegramUI/Sources/AppDelegate.swift index 93a5c63a840..f4eafc5bb7e 100644 --- a/submodules/TelegramUI/Sources/AppDelegate.swift +++ b/submodules/TelegramUI/Sources/AppDelegate.swift @@ -1,5 +1,6 @@ // MARK: Nicegram imports import AppLovinAdProvider +import FeatNicegramHub import FeatTasks import NGAiChat import NGAnalytics @@ -14,6 +15,7 @@ import NGOnboarding import NGRemoteConfig import NGRepoTg import NGRepoUser +import NGStats import NGStealthMode import NGStrings import SubscriptionAnalytics @@ -1210,34 +1212,36 @@ private class UserInterfaceStyleObserverWindow: UIWindow { }) // MARK: Nicegram - if #available(iOS 13.0, *) { - let _ = self.context.get().start(next: { context in - if let context = context { - TasksContainer.shared.channelSubscriptionChecker.register { - ChannelSubscriptionCheckerImpl(context: context.context) - } - } - }) - } - let _ = self.context.get().start(next: { context in if let context = context { + let accountContext = context.context + CoreContainer.shared.urlOpener.register { - UrlOpenerImpl(accountContext: context.context) + UrlOpenerImpl(accountContext: accountContext) } - } - }) - - if #available(iOS 13.0, *) { - let _ = self.context.get().start(next: { context in - if let context = context { - let accountContext = context.context + + if #available(iOS 13.0, *) { + NicegramHubContainer.shared.stickersDataProvider.register { + StickersDataProviderImpl(context: accountContext) + } + RepoTgHelper.setTelegramId( accountContext.account.peerId.id._internalGetInt64Value() ) + + TasksContainer.shared.channelSubscriptionChecker.register { + ChannelSubscriptionCheckerImpl(context: accountContext) + } } - }) - } + + if #available(iOS 13.0, *) { + Task { + let shareStickersUseCase = NicegramHubContainer.shared.shareStickersUseCase() + await shareStickersUseCase() + } + } + } + }) let _ = self.sharedContextPromise.get().start(next: { sharedContext in NGStealthMode.initialize( diff --git a/submodules/TelegramUI/Sources/Chat/ChatControllerNavigateToMessage.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerNavigateToMessage.swift index 7ce0929b1ef..092847181da 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatControllerNavigateToMessage.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerNavigateToMessage.swift @@ -104,6 +104,9 @@ extension ChatControllerImpl { if case let .peer(peerId) = self.chatLocation, messageLocation.peerId == peerId, !isPinnedMessages, !isScheduledMessages { forceInCurrentChat = true } + if case .customChatContents = self.chatLocation { + forceInCurrentChat = true + } if isPinnedMessages, let messageId = messageLocation.messageId { let _ = (combineLatest( @@ -139,7 +142,7 @@ extension ChatControllerImpl { completion?() }) - } else if case let .peer(peerId) = self.chatLocation, let messageId = messageLocation.messageId, (messageId.peerId != peerId && !forceInCurrentChat) || (isScheduledMessages && messageId.id != 0 && !Namespaces.Message.allScheduled.contains(messageId.namespace)) { + } else if case let .peer(peerId) = self.chatLocation, let messageId = messageLocation.messageId, (messageId.peerId != peerId && !forceInCurrentChat) || (isScheduledMessages && messageId.id != 0 && !Namespaces.Message.allNonRegular.contains(messageId.namespace)) { let _ = (self.context.engine.data.get( TelegramEngine.EngineData.Item.Peer.Peer(id: messageId.peerId), TelegramEngine.EngineData.Item.Messages.Message(id: messageId) @@ -205,6 +208,7 @@ extension ChatControllerImpl { if case let .id(_, params) = messageLocation { quote = params.quote.flatMap { quote in (string: quote.string, offset: quote.offset) } } + self.chatDisplayNode.historyNode.scrollToMessage(from: scrollFromIndex, to: message.index, animated: animated, quote: quote, scrollPosition: scrollPosition) if delayCompletion { diff --git a/submodules/TelegramUI/Sources/Chat/ChatControllerOpenMessageContextMenu.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerOpenMessageContextMenu.swift index a396eef37f3..eacaa102761 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatControllerOpenMessageContextMenu.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerOpenMessageContextMenu.swift @@ -290,7 +290,7 @@ extension ChatControllerImpl { if let location = location { source = .location(ChatMessageContextLocationContentSource(controller: self, location: node.view.convert(node.bounds, to: nil).origin.offsetBy(dx: location.x, dy: location.y))) } else { - source = .extracted(ChatMessageContextExtractedContentSource(chatNode: self.chatDisplayNode, engine: self.context.engine, message: message, selectAll: selectAll)) + source = .extracted(ChatMessageContextExtractedContentSource(chatController: self, chatNode: self.chatDisplayNode, engine: self.context.engine, message: message, selectAll: selectAll)) } self.canReadHistory.set(false) diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index 2be0fb4f156..cfd15f3af68 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -151,7 +151,7 @@ public final class ChatControllerOverlayPresentationData { enum ChatLocationInfoData { case peer(Promise) case replyThread(Promise) - case feed + case customChatContents } enum ChatRecordingActivity { @@ -661,10 +661,10 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G promise.set(.single(nil)) } self.chatLocationInfoData = .replyThread(promise) - case .feed: + case .customChatContents: locationBroadcastPanelSource = .none groupCallPanelSource = .none - self.chatLocationInfoData = .feed + self.chatLocationInfoData = .customChatContents } self.presentationData = context.sharedContext.currentPresentationData.with { $0 } @@ -733,6 +733,37 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G return false } + if case let .customChatContents(customChatContents) = strongSelf.presentationInterfaceState.subject { + switch customChatContents.kind { + case let .quickReplyMessageInput(_, shortcutType): + if let historyView = strongSelf.chatDisplayNode.historyNode.originalHistoryView, historyView.entries.isEmpty { + + let titleString: String + let textString: String + switch shortcutType { + case .generic: + titleString = strongSelf.presentationData.strings.QuickReply_ChatRemoveGeneric_Title + textString = strongSelf.presentationData.strings.QuickReply_ChatRemoveGeneric_Text + case .greeting: + titleString = strongSelf.presentationData.strings.QuickReply_ChatRemoveGreetingMessage_Title + textString = strongSelf.presentationData.strings.QuickReply_ChatRemoveGreetingMessage_Text + case .away: + titleString = strongSelf.presentationData.strings.QuickReply_ChatRemoveAwayMessage_Title + textString = strongSelf.presentationData.strings.QuickReply_ChatRemoveAwayMessage_Text + } + + strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: titleString, text: textString, actions: [ + TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_Cancel, action: {}), + TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.QuickReply_ChatRemoveGeneric_DeleteAction, action: { [weak strongSelf] in + strongSelf?.dismiss() + }) + ]), in: .window(.root)) + + return false + } + } + } + return true } @@ -1129,7 +1160,12 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G chatFilterTag = value } - return context.sharedContext.openChatMessage(OpenChatMessageParams(context: context, updatedPresentationData: strongSelf.updatedPresentationData, chatLocation: openChatLocation, chatFilterTag: chatFilterTag, chatLocationContextHolder: strongSelf.chatLocationContextHolder, message: message, standalone: false, reverseMessageGalleryOrder: false, mode: mode, navigationController: strongSelf.effectiveNavigationController, dismissInput: { + var standalone = false + if case .customChatContents = strongSelf.chatLocation { + standalone = true + } + + return context.sharedContext.openChatMessage(OpenChatMessageParams(context: context, updatedPresentationData: strongSelf.updatedPresentationData, chatLocation: openChatLocation, chatFilterTag: chatFilterTag, chatLocationContextHolder: strongSelf.chatLocationContextHolder, message: message, standalone: standalone, reverseMessageGalleryOrder: false, mode: mode, navigationController: strongSelf.effectiveNavigationController, dismissInput: { self?.chatDisplayNode.dismissInput() }, present: { c, a in self?.present(c, in: .window(.root), with: a, blockInteraction: true) @@ -2320,7 +2356,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } case .replyThread: postAsReply = true - case .feed: + case .customChatContents: postAsReply = true } @@ -2926,7 +2962,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G case let .replyThread(replyThreadMessage): let peerId = replyThreadMessage.peerId strongSelf.navigateToMessage(from: nil, to: .index(MessageIndex(id: MessageId(peerId: peerId, namespace: 0, id: 0), timestamp: timestamp - Int32(NSTimeZone.local.secondsFromGMT()))), scrollPosition: .bottom(0.0), rememberInStack: false, forceInCurrentChat: true, animated: true, completion: nil) - case .feed: + case .customChatContents: break } }, requestRedeliveryOfFailedMessages: { [weak self] id in @@ -2967,7 +3003,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G strongSelf.chatDisplayNode.messageTransitionNode.dismissMessageReactionContexts() - let controller = ContextController(presentationData: strongSelf.presentationData, source: .extracted(ChatMessageContextExtractedContentSource(chatNode: strongSelf.chatDisplayNode, engine: strongSelf.context.engine, message: message._asMessage(), selectAll: true)), items: .single(ContextController.Items(content: .list(actions))), recognizer: nil) + let controller = ContextController(presentationData: strongSelf.presentationData, source: .extracted(ChatMessageContextExtractedContentSource(chatController: strongSelf, chatNode: strongSelf.chatDisplayNode, engine: strongSelf.context.engine, message: message._asMessage(), selectAll: true)), items: .single(ContextController.Items(content: .list(actions))), recognizer: nil) strongSelf.currentContextController = controller strongSelf.forEachController({ controller in if let controller = controller as? TooltipScreen { @@ -3045,7 +3081,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G strongSelf.chatDisplayNode.messageTransitionNode.dismissMessageReactionContexts() - let controller = ContextController(presentationData: strongSelf.presentationData, source: .extracted(ChatMessageContextExtractedContentSource(chatNode: strongSelf.chatDisplayNode, engine: strongSelf.context.engine, message: topMessage, selectAll: true)), items: .single(ContextController.Items(content: .list(actions))), recognizer: nil) + let controller = ContextController(presentationData: strongSelf.presentationData, source: .extracted(ChatMessageContextExtractedContentSource(chatController: strongSelf, chatNode: strongSelf.chatDisplayNode, engine: strongSelf.context.engine, message: topMessage, selectAll: true)), items: .single(ContextController.Items(content: .list(actions))), recognizer: nil) strongSelf.currentContextController = controller strongSelf.forEachController({ controller in if let controller = controller as? TooltipScreen { @@ -4571,6 +4607,11 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G ) self.push(boostController) }) + }, openStickerEditor: { [weak self] in + guard let self else { + return + } + self.openStickerEditor() }, requestMessageUpdate: { [weak self] id, scroll in if let self { self.chatDisplayNode.historyNode.requestMessageUpdate(id, andScrollToItem: scroll) @@ -4820,7 +4861,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G chatInfoButtonItem = UIBarButtonItem(customDisplayNode: avatarNode)! self.avatarNode = avatarNode - case .feed: + case .customChatContents: chatInfoButtonItem = UIBarButtonItem(title: "", style: .plain, target: nil, action: nil) } chatInfoButtonItem.target = self @@ -5702,7 +5743,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G replyThreadType = .replies } } - case .feed: + case .customChatContents: replyThreadType = .replies } @@ -6135,6 +6176,13 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } } + var appliedBoosts: Int32? + var boostsToUnrestrict: Int32? + if let cachedChannelData = peerView.cachedData as? CachedChannelData { + appliedBoosts = cachedChannelData.appliedBoosts + boostsToUnrestrict = cachedChannelData.boostsToUnrestrict + } + strongSelf.updateChatPresentationInterfaceState(animated: animated, interactive: false, { return $0.updatedPeer { _ in return renderedPeer @@ -6143,6 +6191,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G .updatedHasSearchTags(hasSearchTags) .updatedIsPremiumRequiredForMessaging(isPremiumRequiredForMessaging) .updatedHasSavedChats(hasSavedChats) + .updatedAppliedBoosts(appliedBoosts) + .updatedBoostsToUnrestrict(boostsToUnrestrict) .updatedInterfaceState { interfaceState in var interfaceState = interfaceState @@ -6182,11 +6232,25 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } } })) - } else if case .feed = self.chatLocationInfoData { + } else if case .customChatContents = self.chatLocationInfoData { self.reportIrrelvantGeoNoticePromise.set(.single(nil)) self.titleDisposable.set(nil) - self.chatTitleView?.titleContent = .custom("Feed", nil, false) + if case let .customChatContents(customChatContents) = self.subject { + switch customChatContents.kind { + case let .quickReplyMessageInput(shortcut, shortcutType): + switch shortcutType { + case .generic: + self.chatTitleView?.titleContent = .custom("\(shortcut)", nil, false) + case .greeting: + self.chatTitleView?.titleContent = .custom(self.presentationData.strings.QuickReply_TitleGreetingMessage, nil, false) + case .away: + self.chatTitleView?.titleContent = .custom(self.presentationData.strings.QuickReply_TitleAwayMessage, nil, false) + } + } + } else { + self.chatTitleView?.titleContent = .custom(" ", nil, false) + } if !self.didSetChatLocationInfoReady { self.didSetChatLocationInfoReady = true @@ -6340,7 +6404,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G activitySpace = PeerActivitySpace(peerId: peerId, category: .global) case let .replyThread(replyThreadMessage): activitySpace = PeerActivitySpace(peerId: replyThreadMessage.peerId, category: .thread(replyThreadMessage.threadId)) - case .feed: + case .customChatContents: activitySpace = nil } @@ -7796,7 +7860,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G case .peer: pinnedMessageId = topPinnedMessage?.message.id pinnedMessage = topPinnedMessage - case .feed: + case .customChatContents: pinnedMessageId = nil pinnedMessage = nil } @@ -7967,7 +8031,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G $0.updatedChatHistoryState(state) }) - if let botStart = strongSelf.botStart, case let .loaded(isEmpty) = state { + if let botStart = strongSelf.botStart, case let .loaded(isEmpty, _) = state { strongSelf.botStart = nil if !isEmpty { strongSelf.startBot(botStart.payload) @@ -8204,20 +8268,24 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } self.chatDisplayNode.sendMessages = { [weak self] messages, silentPosting, scheduleTime, isAnyMessageTextPartitioned in - if let strongSelf = self, let peerId = strongSelf.chatLocation.peerId { - var correlationIds: [Int64] = [] - for message in messages { - switch message { - case let .message(_, _, _, _, _, _, _, _, correlationId, _): - if let correlationId = correlationId { - correlationIds.append(correlationId) - } - default: - break + guard let strongSelf = self else { + return + } + + var correlationIds: [Int64] = [] + for message in messages { + switch message { + case let .message(_, _, _, _, _, _, _, _, correlationId, _): + if let correlationId = correlationId { + correlationIds.append(correlationId) } + default: + break } - strongSelf.commitPurposefulAction() - + } + strongSelf.commitPurposefulAction() + + if let peerId = strongSelf.chatLocation.peerId { var hasDisabledContent = false if "".isEmpty { hasDisabledContent = false @@ -8304,9 +8372,12 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G }) donateSendMessageIntent(account: strongSelf.context.account, sharedContext: strongSelf.context.sharedContext, intentContext: .chat, peerIds: [peerId]) - - strongSelf.updateChatPresentationInterfaceState(interactive: true, { $0.updatedShowCommands(false) }) + } else if case let .customChatContents(customChatContents) = strongSelf.subject { + customChatContents.enqueueMessages(messages: messages) + strongSelf.chatDisplayNode.historyNode.scrollToEndOfHistory() } + + strongSelf.updateChatPresentationInterfaceState(interactive: true, { $0.updatedShowCommands(false) }) } self.chatDisplayNode.requestUpdateChatInterfaceState = { [weak self] transition, saveInterfaceState, f in @@ -9183,7 +9254,10 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G return } - let _ = (strongSelf.context.engine.data.get(TelegramEngine.EngineData.Item.Messages.Message(id: editMessage.messageId)) + let sourceMessage: Signal + sourceMessage = strongSelf.context.engine.data.get(TelegramEngine.EngineData.Item.Messages.Message(id: editMessage.messageId)) + + let _ = (sourceMessage |> deliverOnMainQueue).start(next: { [weak strongSelf] message in guard let strongSelf, let message else { return @@ -9279,8 +9353,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } let _ = (strongSelf.context.account.postbox.messageAtId(editMessage.messageId) - |> deliverOnMainQueue) - .startStandalone(next: { [weak self] currentMessage in + |> deliverOnMainQueue).startStandalone(next: { [weak self] currentMessage in if let strongSelf = self { if let currentMessage = currentMessage { let currentEntities = currentMessage.textEntitiesAttribute?.entities ?? [] @@ -9430,7 +9503,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G strongSelf.updateItemNodesSearchTextHighlightStates() if let navigateIndex = navigateIndex { switch strongSelf.chatLocation { - case .peer, .replyThread, .feed: + case .peer, .replyThread, .customChatContents: strongSelf.navigateToMessage(from: nil, to: .index(navigateIndex), forceInCurrentChat: true) } } @@ -9537,6 +9610,74 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } } } + }, sendShortcut: { [weak self] shortcutId in + guard let self else { + return + } + guard let peerId = self.chatLocation.peerId else { + return + } + + self.updateChatPresentationInterfaceState(animated: true, interactive: false, { + $0.updatedInterfaceState { $0.withUpdatedReplyMessageSubject(nil).withUpdatedComposeInputState(ChatTextInputState(inputText: NSAttributedString(string: ""))).withUpdatedComposeDisableUrlPreviews([]) } + }) + + if !self.presentationInterfaceState.isPremium { + let controller = PremiumIntroScreen(context: self.context, source: .settings) + self.push(controller) + return + } + + self.context.engine.accountData.sendMessageShortcut(peerId: peerId, id: shortcutId) + + /*self.chatDisplayNode.setupSendActionOnViewUpdate({ [weak self] in + guard let self else { + return + } + self.updateChatPresentationInterfaceState(animated: true, interactive: false, { + $0.updatedInterfaceState { $0.withUpdatedReplyMessageSubject(nil).withUpdatedComposeInputState(ChatTextInputState(inputText: NSAttributedString(string: ""))).withUpdatedComposeDisableUrlPreviews([]) } + }) + }, nil) + + var messages: [EnqueueMessage] = [] + do { + let message = shortcut.topMessage + var attributes: [MessageAttribute] = [] + let entities = generateTextEntities(message.text, enabledTypes: .all) + if !entities.isEmpty { + attributes.append(TextEntitiesMessageAttribute(entities: entities)) + } + + messages.append(.message( + text: message.text, + attributes: attributes, + inlineStickers: [:], + mediaReference: message.media.first.flatMap { AnyMediaReference.standalone(media: $0) }, + threadId: self.chatLocation.threadId, + replyToMessageId: nil, + replyToStoryId: nil, + localGroupingKey: nil, + correlationId: nil, + bubbleUpEmojiOrStickersets: [] + )) + } + + self.sendMessages(messages)*/ + }, openEditShortcuts: { [weak self] in + guard let self else { + return + } + let _ = (self.context.sharedContext.makeQuickReplySetupScreenInitialData(context: self.context) + |> take(1) + |> deliverOnMainQueue).start(next: { [weak self] initialData in + guard let self else { + return + } + + let controller = self.context.sharedContext.makeQuickReplySetupScreen(context: self.context, initialData: initialData) + controller.navigationPresentation = .modal + self.push(controller) + }) }, sendBotStart: { [weak self] payload in if let strongSelf = self, canSendMessagesToChat(strongSelf.presentationInterfaceState) { strongSelf.startBot(payload) @@ -9558,9 +9699,6 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G guard let strongSelf = self else { return } - guard let peer = strongSelf.presentationInterfaceState.renderedPeer?.peer else { - return - } strongSelf.dismissAllTooltips() @@ -9570,32 +9708,34 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } var bannedMediaInput = false - if let channel = peer as? TelegramChannel { - if channel.hasBannedPermission(.banSendVoice) != nil && channel.hasBannedPermission(.banSendInstantVideos) != nil { - bannedMediaInput = true - } else if channel.hasBannedPermission(.banSendVoice) != nil { - if !isVideo { - strongSelf.controllerInteraction?.displayUndo(.info(title: nil, text: strongSelf.restrictedSendingContentsText(), timeout: nil, customUndoText: nil)) - return - } - } else if channel.hasBannedPermission(.banSendInstantVideos) != nil { - if isVideo { - strongSelf.controllerInteraction?.displayUndo(.info(title: nil, text: strongSelf.restrictedSendingContentsText(), timeout: nil, customUndoText: nil)) - return - } - } - } else if let group = peer as? TelegramGroup { - if group.hasBannedPermission(.banSendVoice) && group.hasBannedPermission(.banSendInstantVideos) { - bannedMediaInput = true - } else if group.hasBannedPermission(.banSendVoice) { - if !isVideo { - strongSelf.controllerInteraction?.displayUndo(.info(title: nil, text: strongSelf.restrictedSendingContentsText(), timeout: nil, customUndoText: nil)) - return + if let peer = strongSelf.presentationInterfaceState.renderedPeer?.peer { + if let channel = peer as? TelegramChannel { + if channel.hasBannedPermission(.banSendVoice) != nil && channel.hasBannedPermission(.banSendInstantVideos) != nil { + bannedMediaInput = true + } else if channel.hasBannedPermission(.banSendVoice) != nil { + if !isVideo { + strongSelf.controllerInteraction?.displayUndo(.info(title: nil, text: strongSelf.restrictedSendingContentsText(), timeout: nil, customUndoText: nil)) + return + } + } else if channel.hasBannedPermission(.banSendInstantVideos) != nil { + if isVideo { + strongSelf.controllerInteraction?.displayUndo(.info(title: nil, text: strongSelf.restrictedSendingContentsText(), timeout: nil, customUndoText: nil)) + return + } } - } else if group.hasBannedPermission(.banSendInstantVideos) { - if isVideo { - strongSelf.controllerInteraction?.displayUndo(.info(title: nil, text: strongSelf.restrictedSendingContentsText(), timeout: nil, customUndoText: nil)) - return + } else if let group = peer as? TelegramGroup { + if group.hasBannedPermission(.banSendVoice) && group.hasBannedPermission(.banSendInstantVideos) { + bannedMediaInput = true + } else if group.hasBannedPermission(.banSendVoice) { + if !isVideo { + strongSelf.controllerInteraction?.displayUndo(.info(title: nil, text: strongSelf.restrictedSendingContentsText(), timeout: nil, customUndoText: nil)) + return + } + } else if group.hasBannedPermission(.banSendInstantVideos) { + if isVideo { + strongSelf.controllerInteraction?.displayUndo(.info(title: nil, text: strongSelf.restrictedSendingContentsText(), timeout: nil, customUndoText: nil)) + return + } } } } @@ -9891,37 +10031,36 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G guard let strongSelf = self else { return } - guard let peer = strongSelf.presentationInterfaceState.renderedPeer?.peer else { - return - } var bannedMediaInput = false - if let channel = peer as? TelegramChannel { - if channel.hasBannedPermission(.banSendVoice) != nil && channel.hasBannedPermission(.banSendInstantVideos) != nil { - bannedMediaInput = true - } else if channel.hasBannedPermission(.banSendVoice) != nil { - if channel.hasBannedPermission(.banSendInstantVideos) == nil { - strongSelf.displayMediaRecordingTooltip() - return - } - } else if channel.hasBannedPermission(.banSendInstantVideos) != nil { - if channel.hasBannedPermission(.banSendVoice) == nil { - strongSelf.displayMediaRecordingTooltip() - return - } - } - } else if let group = peer as? TelegramGroup { - if group.hasBannedPermission(.banSendVoice) && group.hasBannedPermission(.banSendInstantVideos) { - bannedMediaInput = true - } else if group.hasBannedPermission(.banSendVoice) { - if !group.hasBannedPermission(.banSendInstantVideos) { - strongSelf.displayMediaRecordingTooltip() - return + if let peer = strongSelf.presentationInterfaceState.renderedPeer?.peer { + if let channel = peer as? TelegramChannel { + if channel.hasBannedPermission(.banSendVoice) != nil && channel.hasBannedPermission(.banSendInstantVideos) != nil { + bannedMediaInput = true + } else if channel.hasBannedPermission(.banSendVoice) != nil { + if channel.hasBannedPermission(.banSendInstantVideos) == nil { + strongSelf.displayMediaRecordingTooltip() + return + } + } else if channel.hasBannedPermission(.banSendInstantVideos) != nil { + if channel.hasBannedPermission(.banSendVoice) == nil { + strongSelf.displayMediaRecordingTooltip() + return + } } - } else if group.hasBannedPermission(.banSendInstantVideos) { - if !group.hasBannedPermission(.banSendVoice) { - strongSelf.displayMediaRecordingTooltip() - return + } else if let group = peer as? TelegramGroup { + if group.hasBannedPermission(.banSendVoice) && group.hasBannedPermission(.banSendInstantVideos) { + bannedMediaInput = true + } else if group.hasBannedPermission(.banSendVoice) { + if !group.hasBannedPermission(.banSendInstantVideos) { + strongSelf.displayMediaRecordingTooltip() + return + } + } else if group.hasBannedPermission(.banSendInstantVideos) { + if !group.hasBannedPermission(.banSendVoice) { + strongSelf.displayMediaRecordingTooltip() + return + } } } } @@ -11376,7 +11515,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G activitySpace = PeerActivitySpace(peerId: peerId, category: .global) case let .replyThread(replyThreadMessage): activitySpace = PeerActivitySpace(peerId: replyThreadMessage.peerId, category: .thread(replyThreadMessage.threadId)) - case .feed: + case .customChatContents: activitySpace = nil } @@ -11789,9 +11928,11 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G // // MARK: Nicegram NGStats - if !self.didAppear { - if let peerId = self.chatLocation.peerId { - shareChannelInfo(peerId: peerId, context: self.context) + if #available(iOS 13.0, *) { + if !self.didAppear { + if let peerId = self.chatLocation.peerId { + sharePeerData(peerId: peerId, context: self.context) + } } } // @@ -12336,7 +12477,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G peerId = replyThreadMessage.peerId threadId = replyThreadMessage.threadId } - case .feed: + case .customChatContents: return } @@ -12903,14 +13044,16 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G self.effectiveNavigationController?.pushViewController(infoController) } } - case .feed: + case .customChatContents: break } }) case .search: self.interfaceInteraction?.beginMessageSearch(.everything, "") case .dismiss: - self.dismiss() + if self.attemptNavigation({}) { + self.dismiss() + } case .clearCache: let controller = OverlayStatusController(theme: self.presentationData.theme, type: .loading(cancelled: nil)) self.present(controller, in: .window(.root)) @@ -13115,9 +13258,11 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G })) case .replyThread: break - case .feed: + case .customChatContents: break } + case .edit: + self.editChat() } } @@ -13847,7 +13992,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G if let effectiveMessageId = replyThreadMessage.effectiveMessageId { defaultReplyMessageSubject = EngineMessageReplySubject(messageId: effectiveMessageId, quote: nil) } - case .feed: + case .customChatContents: break } @@ -13902,6 +14047,11 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } func sendMessages(_ messages: [EnqueueMessage], media: Bool = false, commit: Bool = false) { + if case let .customChatContents(customChatContents) = self.subject { + customChatContents.enqueueMessages(messages: messages) + return + } + guard let peerId = self.chatLocation.peerId else { return } @@ -14025,6 +14175,15 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G }) } } + + if case let .customChatContents(customChatContents) = strongSelf.presentationInterfaceState.subject, let messageLimit = customChatContents.messageLimit { + if let originalHistoryView = strongSelf.chatDisplayNode.historyNode.originalHistoryView, originalHistoryView.entries.count + mappedMessages.count > messageLimit { + strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: nil, text: strongSelf.presentationData.strings.Chat_QuickReplyMediaMessageLimitReachedText(Int32(messageLimit)), actions: [ + TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_OK, action: {}) + ]), in: .window(.root)) + return + } + } let messages = strongSelf.transformEnqueueMessages(mappedMessages, silentPosting: silentPosting, scheduleTime: scheduleTime) let replyMessageSubject = strongSelf.presentationInterfaceState.interfaceState.replyMessageSubject @@ -14250,10 +14409,6 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } func requestVideoRecorder() { - guard let peerId = self.chatLocation.peerId else { - return - } - if self.videoRecorderValue == nil { if let currentInputPanelFrame = self.chatDisplayNode.currentInputPanelFrame() { if self.recorderFeedback == nil { @@ -14267,6 +14422,16 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } var isBot = false + + var allowLiveUpload = false + var viewOnceAvailable = false + if let peerId = self.chatLocation.peerId { + allowLiveUpload = peerId.namespace != Namespaces.Peer.SecretChat + viewOnceAvailable = !isScheduledMessages && peerId.namespace == Namespaces.Peer.CloudUser && peerId != self.context.account.peerId && !isBot + } else if case .customChatContents = self.chatLocation { + allowLiveUpload = true + } + if let user = self.presentationInterfaceState.renderedPeer?.peer as? TelegramUser, user.botInfo != nil { isBot = true } @@ -14274,8 +14439,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G let controller = VideoMessageCameraScreen( context: self.context, updatedPresentationData: self.updatedPresentationData, - allowLiveUpload: peerId.namespace != Namespaces.Peer.SecretChat, - viewOnceAvailable: !isScheduledMessages && peerId.namespace == Namespaces.Peer.CloudUser && peerId != self.context.account.peerId && !isBot, + allowLiveUpload: allowLiveUpload, + viewOnceAvailable: viewOnceAvailable, inputPanelFrame: (currentInputPanelFrame, self.chatDisplayNode.inputNode != nil), chatNode: self.chatDisplayNode.historyNode, completion: { [weak self] message, silentPosting, scheduleTime in @@ -15585,14 +15750,20 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G let _ = strongSelf.context.engine.messages.deleteMessagesInteractively(messageIds: Array(messageIds), type: .forEveryone).startStandalone() } if let giveaway { - Queue.mainQueue().after(0.2) { - let dateString = stringForDate(timestamp: giveaway.untilDate, timeZone: .current, strings: strongSelf.presentationData.strings) - strongSelf.present(textAlertController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, title: strongSelf.presentationData.strings.Chat_Giveaway_DeleteConfirmation_Title, text: strongSelf.presentationData.strings.Chat_Giveaway_DeleteConfirmation_Text(dateString).string, actions: [TextAlertAction(type: .destructiveAction, title: strongSelf.presentationData.strings.Common_Delete, action: { - commit() - }), TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_Cancel, action: { - })], parseMarkdown: true), in: .window(.root)) + let currentTime = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970) + if currentTime < giveaway.untilDate { + Queue.mainQueue().after(0.2) { + let dateString = stringForDate(timestamp: giveaway.untilDate, timeZone: .current, strings: strongSelf.presentationData.strings) + strongSelf.present(textAlertController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, title: strongSelf.presentationData.strings.Chat_Giveaway_DeleteConfirmation_Title, text: strongSelf.presentationData.strings.Chat_Giveaway_DeleteConfirmation_Text(dateString).string, actions: [TextAlertAction(type: .destructiveAction, title: strongSelf.presentationData.strings.Common_Delete, action: { + commit() + }), TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_Cancel, action: { + })], parseMarkdown: true), in: .window(.root)) + } + f(.default) + } else { + f(.dismissWithoutContent) + commit() } - f(.default) } else { if "".isEmpty { f(.dismissWithoutContent) @@ -16954,6 +17125,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G let controller = MediaEditorScreen( context: context, + mode: .storyEditor, subject: subject, transitionIn: nil, transitionOut: { _, _ in diff --git a/submodules/TelegramUI/Sources/ChatControllerEditChat.swift b/submodules/TelegramUI/Sources/ChatControllerEditChat.swift new file mode 100644 index 00000000000..57a7666b2ac --- /dev/null +++ b/submodules/TelegramUI/Sources/ChatControllerEditChat.swift @@ -0,0 +1,64 @@ +import Foundation +import TelegramPresentationData +import AccountContext +import Postbox +import TelegramCore +import SwiftSignalKit +import Display +import TelegramPresentationData +import PresentationDataUtils +import QuickReplyNameAlertController + +extension ChatControllerImpl { + func editChat() { + if case let .customChatContents(customChatContents) = self.subject, case let .quickReplyMessageInput(currentValue, shortcutType) = customChatContents.kind, case .generic = shortcutType { + var completion: ((String?) -> Void)? + let alertController = quickReplyNameAlertController( + context: self.context, + text: self.presentationData.strings.QuickReply_EditShortcutTitle, + subtext: self.presentationData.strings.QuickReply_EditShortcutText, + value: currentValue, + characterLimit: 32, + apply: { value in + completion?(value) + } + ) + completion = { [weak self, weak alertController] value in + guard let self else { + alertController?.dismissAnimated() + return + } + if let value, !value.isEmpty { + if value == currentValue { + alertController?.dismissAnimated() + return + } + + let _ = (self.context.engine.accountData.shortcutMessageList(onlyRemote: false) + |> take(1) + |> deliverOnMainQueue).start(next: { [weak self] shortcutMessageList in + guard let self else { + alertController?.dismissAnimated() + return + } + + if shortcutMessageList.items.contains(where: { $0.shortcut.lowercased() == value.lowercased() }) { + if let contentNode = alertController?.contentNode as? QuickReplyNameAlertContentNode { + contentNode.setErrorText(errorText: self.presentationData.strings.QuickReply_ShortcutExistsInlineError) + } + } else { + self.chatTitleView?.titleContent = .custom("\(value)", nil, false) + alertController?.view.endEditing(true) + alertController?.dismissAnimated() + + if case let .customChatContents(customChatContents) = self.subject { + customChatContents.quickReplyUpdateShortcut(value: value) + } + } + }) + } + } + self.present(alertController, in: .window(.root)) + } + } +} diff --git a/submodules/TelegramUI/Sources/ChatControllerNode.swift b/submodules/TelegramUI/Sources/ChatControllerNode.swift index bab49d8e450..f160c304112 100644 --- a/submodules/TelegramUI/Sources/ChatControllerNode.swift +++ b/submodules/TelegramUI/Sources/ChatControllerNode.swift @@ -338,6 +338,8 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { private var derivedLayoutState: ChatControllerNodeDerivedLayoutState? + private var loadMoreSearchResultsDisposable: Disposable? + private var isLoadingValue: Bool = false private var isLoadingEarlier: Bool = false private func updateIsLoading(isLoading: Bool, earlier: Bool, animated: Bool) { @@ -659,6 +661,12 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { } source = .custom(messages: messages, messageId: MessageId(peerId: PeerId(0), namespace: 0, id: 0), quote: nil, loadMore: nil) } + } else if case .customChatContents = chatLocation { + if case let .customChatContents(customChatContents) = subject { + source = .customView(historyView: customChatContents.historyView) + } else { + source = .custom(messages: .single(([], 0, false)), messageId: nil, quote: nil, loadMore: nil) + } } else { source = .default } @@ -973,6 +981,7 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { self.displayVideoUnmuteTipDisposable?.dispose() self.inputMediaNodeDataDisposable?.dispose() self.inlineSearchResultsReadyDisposable?.dispose() + self.loadMoreSearchResultsDisposable?.dispose() } override func didLoad() { @@ -1765,6 +1774,8 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { isSelectionEnabled = false } else if self.chatPresentationInterfaceState.inputTextPanelState.mediaRecordingState != nil { isSelectionEnabled = false + } else if case .customChatContents = self.chatLocation { + isSelectionEnabled = false } self.historyNode.isSelectionGestureEnabled = isSelectionEnabled @@ -2012,7 +2023,7 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { overlayNavigationBar.updateLayout(size: barFrame.size, transition: transition) } - var listInsets = UIEdgeInsets(top: containerInsets.bottom + contentBottomInset, left: containerInsets.right, bottom: containerInsets.top, right: containerInsets.left) + var listInsets = UIEdgeInsets(top: containerInsets.bottom + contentBottomInset, left: containerInsets.right, bottom: containerInsets.top + 6.0, right: containerInsets.left) let listScrollIndicatorInsets = UIEdgeInsets(top: containerInsets.bottom + inputPanelsHeight, left: containerInsets.right, bottom: containerInsets.top, right: containerInsets.left) var childContentInsets: UIEdgeInsets = containerInsets @@ -2852,6 +2863,51 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { } } return foundLocalPeers + }, + loadMoreSearchResults: { [weak self] in + guard let self, let controller = self.controller else { + return + } + guard let currentSearchState = controller.searchState, let currentResultsState = controller.presentationInterfaceState.search?.resultsState else { + return + } + + self.loadMoreSearchResultsDisposable?.dispose() + self.loadMoreSearchResultsDisposable = (self.context.engine.messages.searchMessages(location: currentSearchState.location, query: currentSearchState.query, state: currentResultsState.state) + |> deliverOnMainQueue).startStrict(next: { [weak self] results, updatedState in + guard let self, let controller = self.controller else { + return + } + + controller.searchResult.set(.single((results, updatedState, currentSearchState.location))) + + var navigateIndex: MessageIndex? + controller.updateChatPresentationInterfaceState(animated: true, interactive: true, { current in + if let data = current.search { + let messageIndices = results.messages.map({ $0.index }).sorted() + var currentIndex = messageIndices.last + if let previousResultId = data.resultsState?.currentId { + for index in messageIndices { + if index.id >= previousResultId { + currentIndex = index + break + } + } + } + navigateIndex = currentIndex + return current.updatedSearch(data.withUpdatedResultsState(ChatSearchResultsState(messageIndices: messageIndices, currentId: currentIndex?.id, state: updatedState, totalCount: results.totalCount, completed: results.completed))) + } else { + return current + } + }) + if let navigateIndex = navigateIndex { + switch controller.chatLocation { + case .peer, .replyThread, .customChatContents: + controller.navigateToMessage(from: nil, to: .index(navigateIndex), forceInCurrentChat: true) + } + } + controller.updateItemNodesSearchTextHighlightStates() + }) } )), environment: {}, diff --git a/submodules/TelegramUI/Sources/ChatControllerOpenAttachmentMenu.swift b/submodules/TelegramUI/Sources/ChatControllerOpenAttachmentMenu.swift index f519a120989..8772e663cc5 100644 --- a/submodules/TelegramUI/Sources/ChatControllerOpenAttachmentMenu.swift +++ b/submodules/TelegramUI/Sources/ChatControllerOpenAttachmentMenu.swift @@ -29,6 +29,8 @@ import ChatEntityKeyboardInputNode import PremiumUI import PremiumGiftAttachmentScreen import TelegramCallsUI +import AutomaticBusinessMessageSetupScreen +import MediaEditorScreen extension ChatControllerImpl { enum AttachMenuSubject { @@ -39,10 +41,6 @@ extension ChatControllerImpl { } func presentAttachmentMenu(subject: AttachMenuSubject) { - guard let peer = self.presentationInterfaceState.renderedPeer?.peer else { - return - } - let context = self.context let inputIsActive = self.presentationInterfaceState.inputMode == .text @@ -56,42 +54,46 @@ extension ChatControllerImpl { var bannedSendFiles: (Int32, Bool)? var canSendPolls = true - if let peer = peer as? TelegramUser, peer.botInfo == nil { - canSendPolls = false - } else if peer is TelegramSecretChat { - canSendPolls = false - } else if let channel = peer as? TelegramChannel { - if let value = channel.hasBannedPermission(.banSendPhotos, ignoreDefault: canByPassRestrictions) { - bannedSendPhotos = value - } - if let value = channel.hasBannedPermission(.banSendVideos, ignoreDefault: canByPassRestrictions) { - bannedSendVideos = value - } - if let value = channel.hasBannedPermission(.banSendFiles, ignoreDefault: canByPassRestrictions) { - bannedSendFiles = value - } - if let value = channel.hasBannedPermission(.banSendText, ignoreDefault: canByPassRestrictions) { - banSendText = value - } - if channel.hasBannedPermission(.banSendPolls, ignoreDefault: canByPassRestrictions) != nil { + if let peer = self.presentationInterfaceState.renderedPeer?.peer { + if let peer = peer as? TelegramUser, peer.botInfo == nil { canSendPolls = false - } - } else if let group = peer as? TelegramGroup { - if group.hasBannedPermission(.banSendPhotos) { - bannedSendPhotos = (Int32.max, false) - } - if group.hasBannedPermission(.banSendVideos) { - bannedSendVideos = (Int32.max, false) - } - if group.hasBannedPermission(.banSendFiles) { - bannedSendFiles = (Int32.max, false) - } - if group.hasBannedPermission(.banSendText) { - banSendText = (Int32.max, false) - } - if group.hasBannedPermission(.banSendPolls) { + } else if peer is TelegramSecretChat { canSendPolls = false + } else if let channel = peer as? TelegramChannel { + if let value = channel.hasBannedPermission(.banSendPhotos, ignoreDefault: canByPassRestrictions) { + bannedSendPhotos = value + } + if let value = channel.hasBannedPermission(.banSendVideos, ignoreDefault: canByPassRestrictions) { + bannedSendVideos = value + } + if let value = channel.hasBannedPermission(.banSendFiles, ignoreDefault: canByPassRestrictions) { + bannedSendFiles = value + } + if let value = channel.hasBannedPermission(.banSendText, ignoreDefault: canByPassRestrictions) { + banSendText = value + } + if channel.hasBannedPermission(.banSendPolls, ignoreDefault: canByPassRestrictions) != nil { + canSendPolls = false + } + } else if let group = peer as? TelegramGroup { + if group.hasBannedPermission(.banSendPhotos) { + bannedSendPhotos = (Int32.max, false) + } + if group.hasBannedPermission(.banSendVideos) { + bannedSendVideos = (Int32.max, false) + } + if group.hasBannedPermission(.banSendFiles) { + bannedSendFiles = (Int32.max, false) + } + if group.hasBannedPermission(.banSendText) { + banSendText = (Int32.max, false) + } + if group.hasBannedPermission(.banSendPolls) { + canSendPolls = false + } } + } else { + canSendPolls = false } var availableButtons: [AttachmentButtonType] = [.gallery, .file] @@ -111,26 +113,31 @@ extension ChatControllerImpl { } var peerType: AttachMenuBots.Bot.PeerFlags = [] - if let user = peer as? TelegramUser { - if let _ = user.botInfo { - peerType.insert(.bot) - } else { - peerType.insert(.user) - } - } else if let _ = peer as? TelegramGroup { - peerType = .group - } else if let channel = peer as? TelegramChannel { - if case .broadcast = channel.info { - peerType = .channel - } else { + if let peer = self.presentationInterfaceState.renderedPeer?.peer { + if let user = peer as? TelegramUser { + if let _ = user.botInfo { + peerType.insert(.bot) + } else { + peerType.insert(.user) + } + } else if let _ = peer as? TelegramGroup { peerType = .group + } else if let channel = peer as? TelegramChannel { + if case .broadcast = channel.info { + peerType = .channel + } else { + peerType = .group + } } } let buttons: Signal<([AttachmentButtonType], [AttachmentButtonType], AttachmentButtonType?), NoError> - if !isScheduledMessages && !peer.isDeleted { - buttons = self.context.engine.messages.attachMenuBots() - |> map { attachMenuBots in + if let peer = self.presentationInterfaceState.renderedPeer?.peer, !isScheduledMessages, !peer.isDeleted { + buttons = combineLatest( + self.context.engine.messages.attachMenuBots(), + self.context.engine.accountData.shortcutMessageList(onlyRemote: true) |> take(1) + ) + |> map { attachMenuBots, shortcutMessageList in var buttons = availableButtons var allButtons = availableButtons var initialButton: AttachmentButtonType? @@ -164,6 +171,19 @@ extension ChatControllerImpl { allButtons.insert(button, at: 1) } + if let user = peer as? TelegramUser, user.botInfo == nil { + if let index = buttons.firstIndex(where: { $0 == .location }) { + buttons.insert(.quickReply, at: index + 1) + } else { + buttons.append(.quickReply) + } + if let index = allButtons.firstIndex(where: { $0 == .location }) { + allButtons.insert(.quickReply, at: index + 1) + } else { + allButtons.append(.quickReply) + } + } + return (buttons, allButtons, initialButton) } } else { @@ -177,7 +197,7 @@ extension ChatControllerImpl { let premiumConfiguration = PremiumConfiguration.with(appConfiguration: self.context.currentAppConfiguration.with { $0 }) let premiumGiftOptions: [CachedPremiumGiftOption] - if !premiumConfiguration.isPremiumDisabled && premiumConfiguration.showPremiumGiftInAttachMenu, let user = peer as? TelegramUser, !user.isPremium && !user.isDeleted && user.botInfo == nil && !user.flags.contains(.isSupport) { + if let peer = self.presentationInterfaceState.renderedPeer?.peer, !premiumConfiguration.isPremiumDisabled, premiumConfiguration.showPremiumGiftInAttachMenu, let user = peer as? TelegramUser, !user.isPremium && !user.isDeleted && user.botInfo == nil && !user.flags.contains(.isSupport) { premiumGiftOptions = self.presentationInterfaceState.premiumGiftOptions } else { premiumGiftOptions = [] @@ -300,16 +320,12 @@ extension ChatControllerImpl { attachmentController?.dismiss(animated: true) self?.presentICloudFileGallery() }, send: { [weak self] mediaReference in - guard let strongSelf = self, let peerId = strongSelf.chatLocation.peerId else { + guard let strongSelf = self else { return } + let message: EnqueueMessage = .message(text: "", attributes: [], inlineStickers: [:], mediaReference: mediaReference, threadId: strongSelf.chatLocation.threadId, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: []) - let _ = (enqueueMessages(account: strongSelf.context.account, peerId: peerId, messages: strongSelf.transformEnqueueMessages([message])) - |> deliverOnMainQueue).startStandalone(next: { [weak self] _ in - if let strongSelf = self, strongSelf.presentationInterfaceState.subject != .scheduledMessages { - strongSelf.chatDisplayNode.historyNode.scrollToEndOfHistory() - } - }) + strongSelf.sendMessages([message], media: true) }) if let controller = controller as? AttachmentFileControllerImpl { let _ = currentFilesController.swap(controller) @@ -324,20 +340,30 @@ extension ChatControllerImpl { return } let selfPeerId: PeerId - if let peer = peer as? TelegramChannel, case .broadcast = peer.info { - selfPeerId = peer.id - } else if let peer = peer as? TelegramChannel, case .group = peer.info, peer.hasPermission(.canBeAnonymous) { - selfPeerId = peer.id + if let peer = strongSelf.presentationInterfaceState.renderedPeer?.peer { + if let peer = peer as? TelegramChannel, case .broadcast = peer.info { + selfPeerId = peer.id + } else if let peer = peer as? TelegramChannel, case .group = peer.info, peer.hasPermission(.canBeAnonymous) { + selfPeerId = peer.id + } else { + selfPeerId = strongSelf.context.account.peerId + } } else { selfPeerId = strongSelf.context.account.peerId } let _ = (strongSelf.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: selfPeerId)) - |> deliverOnMainQueue).startStandalone(next: { [weak self] selfPeer in + |> deliverOnMainQueue).startStandalone(next: { selfPeer in guard let strongSelf = self, let selfPeer = selfPeer else { return } - let hasLiveLocation = peer.id.namespace != Namespaces.Peer.SecretChat && peer.id != strongSelf.context.account.peerId && strongSelf.presentationInterfaceState.subject != .scheduledMessages - let controller = LocationPickerController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, mode: .share(peer: EnginePeer(peer), selfPeer: selfPeer, hasLiveLocation: hasLiveLocation), completion: { [weak self] location, _, _, _, _ in + let hasLiveLocation: Bool + if let peer = strongSelf.presentationInterfaceState.renderedPeer?.peer { + hasLiveLocation = peer.id.namespace != Namespaces.Peer.SecretChat && peer.id != strongSelf.context.account.peerId && strongSelf.presentationInterfaceState.subject != .scheduledMessages + } else { + hasLiveLocation = false + } + let sharePeer = (strongSelf.presentationInterfaceState.renderedPeer?.peer).flatMap(EnginePeer.init) + let controller = LocationPickerController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, mode: .share(peer: sharePeer, selfPeer: selfPeer, hasLiveLocation: hasLiveLocation), completion: { location, _, _, _, _ in guard let strongSelf = self else { return } @@ -523,69 +549,91 @@ extension ChatControllerImpl { completion(controller, controller?.mediaPickerContext) strongSelf.controllerNavigationDisposable.set(nil) case .gift: - let premiumGiftOptions = strongSelf.presentationInterfaceState.premiumGiftOptions - if !premiumGiftOptions.isEmpty { - let controller = PremiumGiftAttachmentScreen(context: context, peerIds: [peer.id], options: premiumGiftOptions, source: .attachMenu, pushController: { [weak self] c in - if let strongSelf = self { - strongSelf.push(c) - } - }, completion: { [weak self] in + if let peer = strongSelf.presentationInterfaceState.renderedPeer?.peer { + let premiumGiftOptions = strongSelf.presentationInterfaceState.premiumGiftOptions + if !premiumGiftOptions.isEmpty { + let controller = PremiumGiftAttachmentScreen(context: context, peerIds: [peer.id], options: premiumGiftOptions, source: .attachMenu, pushController: { [weak self] c in + if let strongSelf = self { + strongSelf.push(c) + } + }, completion: { [weak self] in + if let strongSelf = self { + strongSelf.hintPlayNextOutgoingGift() + strongSelf.attachmentController?.dismiss(animated: true) + } + }) + completion(controller, controller.mediaPickerContext) + strongSelf.controllerNavigationDisposable.set(nil) + + let _ = ApplicationSpecificNotice.incrementDismissedPremiumGiftSuggestion(accountManager: context.sharedContext.accountManager, peerId: peer.id).startStandalone() + } + } + case let .app(bot): + if let peer = strongSelf.presentationInterfaceState.renderedPeer?.peer { + var payload: String? + var fromAttachMenu = true + if case let .bot(_, botPayload, _) = subject { + payload = botPayload + fromAttachMenu = false + } + let params = WebAppParameters(source: fromAttachMenu ? .attachMenu : .generic, peerId: peer.id, botId: bot.peer.id, botName: bot.shortName, url: nil, queryId: nil, payload: payload, buttonText: nil, keepAliveSignal: nil, forceHasSettings: false) + let replyMessageSubject = strongSelf.presentationInterfaceState.interfaceState.replyMessageSubject + let controller = WebAppController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, params: params, replyToMessageId: replyMessageSubject?.messageId, threadId: strongSelf.chatLocation.threadId) + controller.openUrl = { [weak self] url, concealed, commit in + self?.openUrl(url, concealed: concealed, forceExternal: true, commit: commit) + } + controller.getNavigationController = { [weak self] in + return self?.effectiveNavigationController + } + controller.completion = { [weak self] in if let strongSelf = self { - strongSelf.hintPlayNextOutgoingGift() - strongSelf.attachmentController?.dismiss(animated: true) + strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { + $0.updatedInterfaceState { $0.withUpdatedReplyMessageSubject(nil) } + }) + strongSelf.chatDisplayNode.historyNode.scrollToEndOfHistory() } - }) + } completion(controller, controller.mediaPickerContext) strongSelf.controllerNavigationDisposable.set(nil) - let _ = ApplicationSpecificNotice.incrementDismissedPremiumGiftSuggestion(accountManager: context.sharedContext.accountManager, peerId: peer.id).startStandalone() - } - case let .app(bot): - var payload: String? - var fromAttachMenu = true - if case let .bot(_, botPayload, _) = subject { - payload = botPayload - fromAttachMenu = false - } - let params = WebAppParameters(source: fromAttachMenu ? .attachMenu : .generic, peerId: peer.id, botId: bot.peer.id, botName: bot.shortName, url: nil, queryId: nil, payload: payload, buttonText: nil, keepAliveSignal: nil, forceHasSettings: false) - let replyMessageSubject = strongSelf.presentationInterfaceState.interfaceState.replyMessageSubject - let controller = WebAppController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, params: params, replyToMessageId: replyMessageSubject?.messageId, threadId: strongSelf.chatLocation.threadId) - controller.openUrl = { [weak self] url, concealed, commit in - self?.openUrl(url, concealed: concealed, forceExternal: true, commit: commit) - } - controller.getNavigationController = { [weak self] in - return self?.effectiveNavigationController - } - controller.completion = { [weak self] in - if let strongSelf = self { - strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { - $0.updatedInterfaceState { $0.withUpdatedReplyMessageSubject(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.chatDisplayNode.historyNode.scrollToEndOfHistory() + strongSelf.present(alertController, in: .window(.root)) } } - completion(controller, controller.mediaPickerContext) - 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 { + case .quickReply: + let _ = (strongSelf.context.sharedContext.makeQuickReplySetupScreenInitialData(context: strongSelf.context) + |> take(1) + |> deliverOnMainQueue).start(next: { [weak strongSelf] initialData in + guard let strongSelf else { + return + } + + let controller = QuickReplySetupScreen(context: strongSelf.context, initialData: initialData as! QuickReplySetupScreen.InitialData, mode: .select(completion: { [weak strongSelf] shortcutId in + guard let strongSelf 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)) - } + strongSelf.interfaceInteraction?.sendShortcut(shortcutId) + })) + completion(controller, controller.mediaPickerContext) + strongSelf.controllerNavigationDisposable.set(nil) + }) default: break } @@ -632,26 +680,28 @@ extension ChatControllerImpl { return entry ?? GeneratedMediaStoreSettings.defaultSettings } |> deliverOnMainQueue).startStandalone(next: { [weak self] settings in - guard let strongSelf = self, let peer = strongSelf.presentationInterfaceState.renderedPeer?.peer else { + guard let strongSelf = self else { return } strongSelf.chatDisplayNode.dismissInput() var bannedSendMedia: (Int32, Bool)? var canSendPolls = true - if let channel = peer as? TelegramChannel { - if let value = channel.hasBannedPermission(.banSendMedia) { - bannedSendMedia = value - } - if channel.hasBannedPermission(.banSendPolls) != nil { - canSendPolls = false - } - } else if let group = peer as? TelegramGroup { - if group.hasBannedPermission(.banSendMedia) { - bannedSendMedia = (Int32.max, false) - } - if group.hasBannedPermission(.banSendPolls) { - canSendPolls = false + if let peer = strongSelf.presentationInterfaceState.renderedPeer?.peer { + if let channel = peer as? TelegramChannel { + if let value = channel.hasBannedPermission(.banSendMedia) { + bannedSendMedia = value + } + if channel.hasBannedPermission(.banSendPolls) != nil { + canSendPolls = false + } + } else if let group = peer as? TelegramGroup { + if group.hasBannedPermission(.banSendMedia) { + bannedSendMedia = (Int32.max, false) + } + if group.hasBannedPermission(.banSendPolls) { + canSendPolls = false + } } } @@ -719,11 +769,15 @@ extension ChatControllerImpl { } var slowModeEnabled = false - if let channel = peer as? TelegramChannel, channel.isRestrictedBySlowmode { - slowModeEnabled = true + var hasSchedule = false + if let peer = strongSelf.presentationInterfaceState.renderedPeer?.peer { + if let channel = peer as? TelegramChannel, channel.isRestrictedBySlowmode { + slowModeEnabled = true + } + hasSchedule = strongSelf.presentationInterfaceState.subject != .scheduledMessages && peer.id.namespace != Namespaces.Peer.SecretChat } - let controller = legacyAttachmentMenu(context: strongSelf.context, peer: peer, threadTitle: strongSelf.threadInfo?.title, chatLocation: strongSelf.chatLocation, editMediaOptions: menuEditMediaOptions, saveEditedPhotos: settings.storeEditedPhotos, allowGrouping: true, hasSchedule: strongSelf.presentationInterfaceState.subject != .scheduledMessages && peer.id.namespace != Namespaces.Peer.SecretChat, canSendPolls: canSendPolls, updatedPresentationData: strongSelf.updatedPresentationData, parentController: legacyController, recentlyUsedInlineBots: strongSelf.recentlyUsedInlineBotsValue, initialCaption: inputText, openGallery: { + let controller = legacyAttachmentMenu(context: strongSelf.context, peer: strongSelf.presentationInterfaceState.renderedPeer?.peer, threadTitle: strongSelf.threadInfo?.title, chatLocation: strongSelf.chatLocation, editMediaOptions: menuEditMediaOptions, saveEditedPhotos: settings.storeEditedPhotos, allowGrouping: true, hasSchedule: hasSchedule, canSendPolls: canSendPolls, updatedPresentationData: strongSelf.updatedPresentationData, parentController: legacyController, recentlyUsedInlineBots: strongSelf.recentlyUsedInlineBotsValue, initialCaption: inputText, openGallery: { self?.presentOldMediaPicker(fileMode: false, editingMedia: editMediaOptions != nil, completion: { signals, silentPosting, scheduleTime in if !inputText.string.isEmpty { strongSelf.clearInputText() @@ -735,7 +789,7 @@ extension ChatControllerImpl { } }) }, openCamera: { [weak self] cameraView, menuController in - if let strongSelf = self, let peer = strongSelf.presentationInterfaceState.renderedPeer?.peer { + if let strongSelf = self { var enablePhoto = true var enableVideo = true @@ -746,19 +800,21 @@ extension ChatControllerImpl { var bannedSendPhotos: (Int32, Bool)? var bannedSendVideos: (Int32, Bool)? - if let channel = peer as? TelegramChannel { - if let value = channel.hasBannedPermission(.banSendPhotos) { - bannedSendPhotos = value - } - if let value = channel.hasBannedPermission(.banSendVideos) { - bannedSendVideos = value - } - } else if let group = peer as? TelegramGroup { - if group.hasBannedPermission(.banSendPhotos) { - bannedSendPhotos = (Int32.max, false) - } - if group.hasBannedPermission(.banSendVideos) { - bannedSendVideos = (Int32.max, false) + if let peer = strongSelf.presentationInterfaceState.renderedPeer?.peer { + if let channel = peer as? TelegramChannel { + if let value = channel.hasBannedPermission(.banSendPhotos) { + bannedSendPhotos = value + } + if let value = channel.hasBannedPermission(.banSendVideos) { + bannedSendVideos = value + } + } else if let group = peer as? TelegramGroup { + if group.hasBannedPermission(.banSendPhotos) { + bannedSendPhotos = (Int32.max, false) + } + if group.hasBannedPermission(.banSendVideos) { + bannedSendVideos = (Int32.max, false) + } } } @@ -769,7 +825,15 @@ extension ChatControllerImpl { enableVideo = false } - presentedLegacyCamera(context: strongSelf.context, peer: peer, chatLocation: strongSelf.chatLocation, cameraView: cameraView, menuController: menuController, parentController: strongSelf, editingMedia: editMediaOptions != nil, saveCapturedPhotos: peer.id.namespace != Namespaces.Peer.SecretChat, mediaGrouping: true, initialCaption: inputText, hasSchedule: strongSelf.presentationInterfaceState.subject != .scheduledMessages && peer.id.namespace != Namespaces.Peer.SecretChat, enablePhoto: enablePhoto, enableVideo: enableVideo, sendMessagesWithSignals: { [weak self] signals, silentPosting, scheduleTime in + var storeCapturedPhotos = false + var hasSchedule = false + if let peer = strongSelf.presentationInterfaceState.renderedPeer?.peer { + storeCapturedPhotos = peer.id.namespace != Namespaces.Peer.SecretChat + + hasSchedule = strongSelf.presentationInterfaceState.subject != .scheduledMessages && peer.id.namespace != Namespaces.Peer.SecretChat + } + + presentedLegacyCamera(context: strongSelf.context, peer: strongSelf.presentationInterfaceState.renderedPeer?.peer, chatLocation: strongSelf.chatLocation, cameraView: cameraView, menuController: menuController, parentController: strongSelf, editingMedia: editMediaOptions != nil, saveCapturedPhotos: storeCapturedPhotos, mediaGrouping: true, initialCaption: inputText, hasSchedule: hasSchedule, enablePhoto: enablePhoto, enableVideo: enableVideo, sendMessagesWithSignals: { [weak self] signals, silentPosting, scheduleTime in if let strongSelf = self { if editMediaOptions != nil { strongSelf.editMessageMediaWithLegacySignals(signals!) @@ -927,7 +991,7 @@ extension ChatControllerImpl { func presentICloudFileGallery(editingMessage: Bool = false) { let _ = (self.context.engine.data.get( - TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId), + TelegramEngine.EngineData.Item.Peer.Peer(id: self.context.account.peerId), TelegramEngine.EngineData.Item.Configuration.UserLimits(isPremium: false), TelegramEngine.EngineData.Item.Configuration.UserLimits(isPremium: true) ) @@ -1063,9 +1127,6 @@ extension ChatControllerImpl { } func presentMediaPicker(subject: MediaPickerScreen.Subject = .assets(nil, .default), saveEditedPhotos: Bool, bannedSendPhotos: (Int32, Bool)?, bannedSendVideos: (Int32, Bool)?, present: @escaping (MediaPickerScreen, AttachmentMediaPickerContext?) -> Void, updateMediaPickerContext: @escaping (AttachmentMediaPickerContext?) -> Void, completion: @escaping ([Any], Bool, Int32?, @escaping (String) -> UIView?, @escaping () -> Void) -> Void) { - guard let peer = self.presentationInterfaceState.renderedPeer?.peer else { - return - } var isScheduledMessages = false if case .scheduledMessages = self.presentationInterfaceState.subject { isScheduledMessages = true @@ -1073,7 +1134,7 @@ extension ChatControllerImpl { let controller = MediaPickerScreen( context: self.context, updatedPresentationData: self.updatedPresentationData, - peer: EnginePeer(peer), + peer: (self.presentationInterfaceState.renderedPeer?.peer).flatMap(EnginePeer.init), threadTitle: self.threadInfo?.title, chatLocation: self.chatLocation, isScheduledMessages: isScheduledMessages, @@ -1561,17 +1622,12 @@ extension ChatControllerImpl { } func openCamera(cameraView: TGAttachmentCameraView? = nil) { - guard let peer = self.presentationInterfaceState.renderedPeer?.peer else { - return - } - let _ = peer - let _ = (self.context.sharedContext.accountManager.transaction { transaction -> GeneratedMediaStoreSettings in let entry = transaction.getSharedData(ApplicationSpecificSharedDataKeys.generatedMediaStoreSettings)?.get(GeneratedMediaStoreSettings.self) return entry ?? GeneratedMediaStoreSettings.defaultSettings } |> deliverOnMainQueue).startStandalone(next: { [weak self] settings in - guard let strongSelf = self, let peer = strongSelf.presentationInterfaceState.renderedPeer?.peer else { + guard let strongSelf = self else { return } @@ -1585,19 +1641,21 @@ extension ChatControllerImpl { var bannedSendPhotos: (Int32, Bool)? var bannedSendVideos: (Int32, Bool)? - if let channel = peer as? TelegramChannel { - if let value = channel.hasBannedPermission(.banSendPhotos) { - bannedSendPhotos = value - } - if let value = channel.hasBannedPermission(.banSendVideos) { - bannedSendVideos = value - } - } else if let group = peer as? TelegramGroup { - if group.hasBannedPermission(.banSendPhotos) { - bannedSendPhotos = (Int32.max, false) - } - if group.hasBannedPermission(.banSendVideos) { - bannedSendVideos = (Int32.max, false) + if let peer = strongSelf.presentationInterfaceState.renderedPeer?.peer { + if let channel = peer as? TelegramChannel { + if let value = channel.hasBannedPermission(.banSendPhotos) { + bannedSendPhotos = value + } + if let value = channel.hasBannedPermission(.banSendVideos) { + bannedSendVideos = value + } + } else if let group = peer as? TelegramGroup { + if group.hasBannedPermission(.banSendPhotos) { + bannedSendPhotos = (Int32.max, false) + } + if group.hasBannedPermission(.banSendVideos) { + bannedSendVideos = (Int32.max, false) + } } } @@ -1608,10 +1666,15 @@ extension ChatControllerImpl { enableVideo = false } - let storeCapturedMedia = peer.id.namespace != Namespaces.Peer.SecretChat + var storeCapturedMedia = false + var hasSchedule = false + if let peer = strongSelf.presentationInterfaceState.renderedPeer?.peer { + storeCapturedMedia = peer.id.namespace != Namespaces.Peer.SecretChat + hasSchedule = strongSelf.presentationInterfaceState.subject != .scheduledMessages && peer.id.namespace != Namespaces.Peer.SecretChat + } let inputText = strongSelf.presentationInterfaceState.interfaceState.effectiveInputState.inputText - presentedLegacyCamera(context: strongSelf.context, peer: peer, chatLocation: strongSelf.chatLocation, cameraView: cameraView, menuController: nil, parentController: strongSelf, attachmentController: self?.attachmentController, editingMedia: false, saveCapturedPhotos: storeCapturedMedia, mediaGrouping: true, initialCaption: inputText, hasSchedule: strongSelf.presentationInterfaceState.subject != .scheduledMessages && peer.id.namespace != Namespaces.Peer.SecretChat, enablePhoto: enablePhoto, enableVideo: enableVideo, sendMessagesWithSignals: { [weak self] signals, silentPosting, scheduleTime in + presentedLegacyCamera(context: strongSelf.context, peer: strongSelf.presentationInterfaceState.renderedPeer?.peer, chatLocation: strongSelf.chatLocation, cameraView: cameraView, menuController: nil, parentController: strongSelf, attachmentController: self?.attachmentController, editingMedia: false, saveCapturedPhotos: storeCapturedMedia, mediaGrouping: true, initialCaption: inputText, hasSchedule: hasSchedule, enablePhoto: enablePhoto, enableVideo: enableVideo, sendMessagesWithSignals: { [weak self] signals, silentPosting, scheduleTime in if let strongSelf = self { strongSelf.enqueueMediaMessages(signals: signals, silentPosting: silentPosting, scheduleTime: scheduleTime > 0 ? scheduleTime : nil) if !inputText.string.isEmpty { @@ -1650,4 +1713,71 @@ extension ChatControllerImpl { }) }) } + + func openStickerEditor() { + let mainController = AttachmentController(context: self.context, updatedPresentationData: self.updatedPresentationData, chatLocation: nil, buttons: [.standalone], initialButton: .standalone, fromMenu: false, hasTextInput: false, makeEntityInputView: { + return nil + }) +// controller.forceSourceRect = true +// controller.getSourceRect = getSourceRect + mainController.requestController = { [weak self, weak mainController] _, present in + guard let self else { + return + } + let mediaPickerController = MediaPickerScreen(context: self.context, updatedPresentationData: self.updatedPresentationData, peer: nil, threadTitle: nil, chatLocation: nil, subject: .assets(nil, .createSticker)) + mediaPickerController.customSelection = { [weak self, weak mainController] controller, result in + guard let self else { + return + } + if let result = result as? PHAsset { + controller.updateHiddenMediaId(result.localIdentifier) + if let transitionView = controller.transitionView(for: result.localIdentifier, snapshot: false) { + let editorController = MediaEditorScreen( + context: self.context, + mode: .stickerEditor, + subject: .single(.asset(result)), + transitionIn: .gallery( + MediaEditorScreen.TransitionIn.GalleryTransitionIn( + sourceView: transitionView, + sourceRect: transitionView.bounds, + sourceImage: controller.transitionImage(for: result.localIdentifier) + ) + ), + transitionOut: { finished, isNew in + if !finished { + return MediaEditorScreen.TransitionOut( + destinationView: transitionView, + destinationRect: transitionView.bounds, + destinationCornerRadius: 0.0 + ) + } + return nil + }, completion: { [weak self, weak mainController] result, commit in + mainController?.dismiss() + + Queue.mainQueue().after(0.1) { + commit({}) + if let mediaResult = result.media, case let .image(image, _) = mediaResult { + self?.enqueueStickerImage(image, isMemoji: false) + } + } + } as (MediaEditorScreen.Result, @escaping (@escaping () -> Void) -> Void) -> Void + ) + editorController.dismissed = { [weak controller] in + controller?.updateHiddenMediaId(nil) + } + self.push(editorController) + +// completion(result, transitionView, transitionView.bounds, controller.transitionImage(for: result.localIdentifier), transitionOut, { [weak controller] in +// controller?.updateHiddenMediaId(nil) +// }) + } + } + } + present(mediaPickerController, mediaPickerController.mediaPickerContext) + } + mainController.navigationPresentation = .flatModal + mainController.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait) + self.push(mainController) + } } diff --git a/submodules/TelegramUI/Sources/ChatControllerOpenCalendarSearch.swift b/submodules/TelegramUI/Sources/ChatControllerOpenCalendarSearch.swift index b29df6f9416..62ae3df62d2 100644 --- a/submodules/TelegramUI/Sources/ChatControllerOpenCalendarSearch.swift +++ b/submodules/TelegramUI/Sources/ChatControllerOpenCalendarSearch.swift @@ -59,7 +59,7 @@ extension ChatControllerImpl { case let .replyThread(replyThreadMessage): peerId = replyThreadMessage.peerId threadId = replyThreadMessage.threadId - case .feed: + case .customChatContents: return } @@ -96,7 +96,7 @@ extension ChatControllerImpl { case let .replyThread(replyThreadMessage): peerId = replyThreadMessage.peerId threadId = replyThreadMessage.threadId - case .feed: + case .customChatContents: return } diff --git a/submodules/TelegramUI/Sources/ChatControllerOpenMessageShareMenu.swift b/submodules/TelegramUI/Sources/ChatControllerOpenMessageShareMenu.swift index 1e6576268b2..d1445389c0e 100644 --- a/submodules/TelegramUI/Sources/ChatControllerOpenMessageShareMenu.swift +++ b/submodules/TelegramUI/Sources/ChatControllerOpenMessageShareMenu.swift @@ -31,7 +31,7 @@ func chatShareToSavedMessagesAdditionalView(_ chatController: ChatControllerImpl return } - let _ = (chatController.context.account.postbox.aroundMessageHistoryViewForLocation(.peer(peerId: chatController.context.account.peerId, threadId: nil), anchor: .upperBound, ignoreMessagesInTimestampRange: nil, count: 45, fixedCombinedReadStates: nil, topTaggedMessageIdNamespaces: Set(), tag: nil, appendMessagesFromTheSameGroup: false, namespaces: .not(Namespaces.Message.allScheduled), orderStatistics: []) + let _ = (chatController.context.account.postbox.aroundMessageHistoryViewForLocation(.peer(peerId: chatController.context.account.peerId, threadId: nil), anchor: .upperBound, ignoreMessagesInTimestampRange: nil, count: 45, fixedCombinedReadStates: nil, topTaggedMessageIdNamespaces: Set(), tag: nil, appendMessagesFromTheSameGroup: false, namespaces: .not(Namespaces.Message.allNonRegular), orderStatistics: []) |> map { view, _, _ -> [EngineMessage.Id] in let messageIds = correlationIds.compactMap { correlationId in return chatController.context.engine.messages.synchronouslyLookupCorrelationId(correlationId: correlationId) diff --git a/submodules/TelegramUI/Sources/ChatControllerUpdateSearch.swift b/submodules/TelegramUI/Sources/ChatControllerUpdateSearch.swift index 75d9bbc33f9..59f3755db38 100644 --- a/submodules/TelegramUI/Sources/ChatControllerUpdateSearch.swift +++ b/submodules/TelegramUI/Sources/ChatControllerUpdateSearch.swift @@ -33,7 +33,7 @@ extension ChatControllerImpl { break case let .replyThread(replyThreadMessage): threadId = replyThreadMessage.threadId - case .feed: + case .customChatContents: break } @@ -125,7 +125,7 @@ extension ChatControllerImpl { }) if let navigateIndex = navigateIndex { switch strongSelf.chatLocation { - case .peer, .replyThread, .feed: + case .peer, .replyThread, .customChatContents: strongSelf.navigateToMessage(from: nil, to: .index(navigateIndex), forceInCurrentChat: true) } } diff --git a/submodules/TelegramUI/Sources/ChatEmptyNode.swift b/submodules/TelegramUI/Sources/ChatEmptyNode.swift index 5983a2c37a9..b81dd01bc04 100644 --- a/submodules/TelegramUI/Sources/ChatEmptyNode.swift +++ b/submodules/TelegramUI/Sources/ChatEmptyNode.swift @@ -24,8 +24,8 @@ private protocol ChatEmptyNodeContent { func updateLayout(interfaceState: ChatPresentationInterfaceState, subject: ChatEmptyNode.Subject, size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize } -private let titleFont = Font.medium(15.0) -private let messageFont = Font.regular(14.0) +private let titleFont = Font.semibold(15.0) +private let messageFont = Font.regular(13.0) private final class ChatEmptyNodeRegularChatContent: ASDisplayNode, ChatEmptyNodeContent { private let textNode: ImmediateTextNode @@ -53,7 +53,7 @@ private final class ChatEmptyNodeRegularChatContent: ASDisplayNode, ChatEmptyNod text = interfaceState.strings.ChatList_StartMessaging } else { switch interfaceState.chatLocation { - case .peer, .replyThread, .feed: + case .peer, .replyThread, .customChatContents: if case .scheduledMessages = interfaceState.subject { text = interfaceState.strings.ScheduledMessages_EmptyPlaceholder } else { @@ -701,25 +701,87 @@ private final class ChatEmptyNodeCloudChatContent: ASDisplayNode, ChatEmptyNodeC } func updateLayout(interfaceState: ChatPresentationInterfaceState, subject: ChatEmptyNode.Subject, size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize { + var maxWidth: CGFloat = size.width + var centerText = false + + var insets = UIEdgeInsets(top: 15.0, left: 15.0, bottom: 15.0, right: 15.0) + var imageSpacing: CGFloat = 12.0 + var titleSpacing: CGFloat = 4.0 + + if case let .customChatContents(customChatContents) = interfaceState.subject { + maxWidth = min(240.0, maxWidth) + + switch customChatContents.kind { + case .quickReplyMessageInput: + insets.top = 10.0 + imageSpacing = 5.0 + titleSpacing = 5.0 + } + } + if self.currentTheme !== interfaceState.theme || self.currentStrings !== interfaceState.strings { self.currentTheme = interfaceState.theme self.currentStrings = interfaceState.strings let serviceColor = serviceMessageColorComponents(theme: interfaceState.theme, wallpaper: interfaceState.chatWallpaper) - self.iconNode.image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Empty Chat/Cloud"), color: serviceColor.primaryText) + var iconName = "Chat/Empty Chat/Cloud" - let titleString = interfaceState.strings.Conversation_CloudStorageInfo_Title - self.titleNode.attributedText = NSAttributedString(string: titleString, font: titleFont, textColor: serviceColor.primaryText) + let titleString: String + let strings: [String] - let strings: [String] = [ - interfaceState.strings.Conversation_ClousStorageInfo_Description1, - interfaceState.strings.Conversation_ClousStorageInfo_Description2, - interfaceState.strings.Conversation_ClousStorageInfo_Description3, - interfaceState.strings.Conversation_ClousStorageInfo_Description4 - ] + if case let .customChatContents(customChatContents) = interfaceState.subject { + switch customChatContents.kind { + case let .quickReplyMessageInput(shortcut, shortcutType): + switch shortcutType { + case .generic: + iconName = "Chat/Empty Chat/QuickReplies" + centerText = false + titleString = interfaceState.strings.Chat_EmptyState_QuickReply_Title + strings = [ + interfaceState.strings.Chat_EmptyState_QuickReply_Text1(shortcut).string, + interfaceState.strings.Chat_EmptyState_QuickReply_Text2 + ] + case .greeting: + iconName = "Chat/Empty Chat/GreetingShortcut" + centerText = true + titleString = interfaceState.strings.EmptyState_GreetingMessage_Title + strings = [ + interfaceState.strings.EmptyState_GreetingMessage_Text + ] + case .away: + iconName = "Chat/Empty Chat/AwayShortcut" + centerText = true + titleString = interfaceState.strings.EmptyState_AwayMessage_Title + strings = [ + interfaceState.strings.EmptyState_AwayMessage_Text + ] + } + } + } else { + titleString = interfaceState.strings.Conversation_CloudStorageInfo_Title + strings = [ + interfaceState.strings.Conversation_ClousStorageInfo_Description1, + interfaceState.strings.Conversation_ClousStorageInfo_Description2, + interfaceState.strings.Conversation_ClousStorageInfo_Description3, + interfaceState.strings.Conversation_ClousStorageInfo_Description4 + ] + } - let lines: [NSAttributedString] = strings.map { NSAttributedString(string: $0, font: messageFont, textColor: serviceColor.primaryText) } + self.iconNode.image = generateTintedImage(image: UIImage(bundleImageName: iconName), color: serviceColor.primaryText) + + self.titleNode.attributedText = NSAttributedString(string: titleString, font: titleFont, textColor: serviceColor.primaryText) + + let lines: [NSAttributedString] = strings.map { + return parseMarkdownIntoAttributedString($0, attributes: MarkdownAttributes( + body: MarkdownAttributeSet(font: Font.regular(14.0), textColor: serviceColor.primaryText), + bold: MarkdownAttributeSet(font: Font.semibold(14.0), textColor: serviceColor.primaryText), + link: MarkdownAttributeSet(font: Font.regular(14.0), textColor: serviceColor.primaryText), + linkAttribute: { url in + return ("URL", url) + } + ), textAlignment: centerText ? .center : .natural) + } for i in 0 ..< lines.count { if i >= self.lineNodes.count { @@ -727,6 +789,7 @@ private final class ChatEmptyNodeCloudChatContent: ASDisplayNode, ChatEmptyNodeC textNode.maximumNumberOfLines = 0 textNode.isUserInteractionEnabled = false textNode.displaysAsynchronously = false + textNode.textAlignment = centerText ? .center : .natural self.addSubnode(textNode) self.lineNodes.append(textNode) } @@ -735,11 +798,6 @@ private final class ChatEmptyNodeCloudChatContent: ASDisplayNode, ChatEmptyNodeC } } - let insets = UIEdgeInsets(top: 15.0, left: 15.0, bottom: 15.0, right: 15.0) - - let imageSpacing: CGFloat = 12.0 - let titleSpacing: CGFloat = 4.0 - var contentWidth: CGFloat = 100.0 var contentHeight: CGFloat = 0.0 @@ -751,7 +809,7 @@ private final class ChatEmptyNodeCloudChatContent: ASDisplayNode, ChatEmptyNodeC var lineNodes: [(CGSize, ImmediateTextNode)] = [] for textNode in self.lineNodes { - let textSize = textNode.updateLayout(CGSize(width: size.width - insets.left - insets.right - 10.0, height: CGFloat.greatestFiniteMagnitude)) + let textSize = textNode.updateLayout(CGSize(width: maxWidth - insets.left - insets.right - 10.0, height: CGFloat.greatestFiniteMagnitude)) contentWidth = max(contentWidth, textSize.width) contentHeight += textSize.height + titleSpacing lineNodes.append((textSize, textNode)) @@ -1166,7 +1224,9 @@ final class ChatEmptyNode: ASDisplayNode { case .detailsPlaceholder: contentType = .regular case let .emptyChat(emptyType): - if case .replyThread = interfaceState.chatLocation { + if case .customChatContents = interfaceState.subject { + contentType = .cloud + } else if case .replyThread = interfaceState.chatLocation { if case .topic = emptyType { contentType = .topic } else { diff --git a/submodules/TelegramUI/Sources/ChatHistoryEntriesForView.swift b/submodules/TelegramUI/Sources/ChatHistoryEntriesForView.swift index 3af4fe35df3..29924427880 100644 --- a/submodules/TelegramUI/Sources/ChatHistoryEntriesForView.swift +++ b/submodules/TelegramUI/Sources/ChatHistoryEntriesForView.swift @@ -516,6 +516,64 @@ func chatHistoryEntriesForView( } } + if let subject = associatedData.subject, case let .customChatContents(customChatContents) = subject, case let .quickReplyMessageInput(_, shortcutType) = customChatContents.kind, case .generic = shortcutType { + if !view.isLoading && view.laterId == nil && !view.entries.isEmpty { + for i in 0 ..< 2 { + let string = i == 1 ? presentationData.strings.Chat_QuickReply_ServiceHeader1 : presentationData.strings.Chat_QuickReply_ServiceHeader2 + let formattedString = parseMarkdownIntoAttributedString( + string, + attributes: MarkdownAttributes( + body: MarkdownAttributeSet(font: Font.regular(15.0), textColor: .black), + bold: MarkdownAttributeSet(font: Font.regular(15.0), textColor: .black), + link: MarkdownAttributeSet(font: Font.regular(15.0), textColor: .white), + linkAttribute: { url in + return ("URL", url) + } + ) + ) + var entities: [MessageTextEntity] = [] + formattedString.enumerateAttribute(.foregroundColor, in: NSRange(location: 0, length: formattedString.length), options: [], using: { value, range, _ in + if let value = value as? UIColor, value == .white { + entities.append(MessageTextEntity(range: range.lowerBound ..< range.upperBound, type: .Bold)) + } + }) + formattedString.enumerateAttribute(NSAttributedString.Key(rawValue: "URL"), in: NSRange(location: 0, length: formattedString.length), options: [], using: { value, range, _ in + if value != nil { + entities.append(MessageTextEntity(range: range.lowerBound ..< range.upperBound, type: .TextMention(peerId: context.account.peerId))) + } + }) + + let message = Message( + stableId: UInt32.max - 1001 - UInt32(i), + stableVersion: 0, + id: MessageId(peerId: context.account.peerId, namespace: Namespaces.Message.Local, id: Int32.max - 100 - Int32(i)), + globallyUniqueId: nil, + groupingKey: nil, + groupInfo: nil, + threadId: nil, + timestamp: -Int32(i), + flags: [.Incoming], + tags: [], + globalTags: [], + localTags: [], + customTags: [], + forwardInfo: nil, + author: nil, + text: "", + attributes: [], + media: [TelegramMediaAction(action: .customText(text: formattedString.string, entities: entities, additionalAttributes: nil))], + peers: SimpleDictionary(), + associatedMessages: SimpleDictionary(), + associatedMessageIds: [], + associatedMedia: [:], + associatedThreadInfo: nil, + associatedStories: [:] + ) + entries.insert(.MessageEntry(message, presentationData, false, nil, .none, ChatMessageEntryAttributes(rank: nil, isContact: false, contentTypeHint: .generic, updatingMedia: nil, isPlaying: false, isCentered: false, authorStoryStats: nil)), at: 0) + } + } + } + if reverse { return entries.reversed() } else { diff --git a/submodules/TelegramUI/Sources/ChatHistoryListNode.swift b/submodules/TelegramUI/Sources/ChatHistoryListNode.swift index 4d926a9e311..f042200c0b1 100644 --- a/submodules/TelegramUI/Sources/ChatHistoryListNode.swift +++ b/submodules/TelegramUI/Sources/ChatHistoryListNode.swift @@ -227,6 +227,11 @@ extension ListMessageItemInteraction { } private func mappedInsertEntries(context: AccountContext, chatLocation: ChatLocation, associatedData: ChatMessageItemAssociatedData, controllerInteraction: ChatControllerInteraction, mode: ChatHistoryListMode, lastHeaderId: Int64, entries: [ChatHistoryViewTransitionInsertEntry], wantTrButton: [(Bool, [String])]) -> [ListViewInsertItem] { + var disableFloatingDateHeaders = false + if case .customChatContents = chatLocation { + disableFloatingDateHeaders = true + } + return entries.map { entry -> ListViewInsertItem in switch entry.entry { case let .MessageEntry(message, presentationData, read, location, selection, attributes): @@ -234,7 +239,7 @@ private func mappedInsertEntries(context: AccountContext, chatLocation: ChatLoca switch mode { case .bubbles: // MARK: Nicegram, wantTrButton - item = ChatMessageItemImpl(presentationData: presentationData, context: context, chatLocation: chatLocation, associatedData: associatedData, controllerInteraction: controllerInteraction, content: .message(message: message, read: read, selection: selection, attributes: attributes, location: location), wantTrButton: wantTrButton) + item = ChatMessageItemImpl(presentationData: presentationData, context: context, chatLocation: chatLocation, associatedData: associatedData, controllerInteraction: controllerInteraction, content: .message(message: message, read: read, selection: selection, attributes: attributes, location: location), disableDate: disableFloatingDateHeaders, wantTrButton: wantTrButton) case let .list(_, _, _, displayHeaders, hintLinks, isGlobalSearch): let displayHeader: Bool switch displayHeaders { @@ -253,7 +258,7 @@ private func mappedInsertEntries(context: AccountContext, chatLocation: ChatLoca switch mode { case .bubbles: // MARK: Nicegram, wantTrButton - item = ChatMessageItemImpl(presentationData: presentationData, context: context, chatLocation: chatLocation, associatedData: associatedData, controllerInteraction: controllerInteraction, content: .group(messages: messages), wantTrButton: wantTrButton) + item = ChatMessageItemImpl(presentationData: presentationData, context: context, chatLocation: chatLocation, associatedData: associatedData, controllerInteraction: controllerInteraction, content: .group(messages: messages), disableDate: disableFloatingDateHeaders, wantTrButton: wantTrButton) case .list: assertionFailure() item = ListMessageItem(presentationData: presentationData, context: context, chatLocation: chatLocation, interaction: ListMessageItemInteraction(controllerInteraction: controllerInteraction), message: messages[0].0, selection: .none, displayHeader: false) @@ -274,6 +279,11 @@ private func mappedInsertEntries(context: AccountContext, chatLocation: ChatLoca } private func mappedUpdateEntries(context: AccountContext, chatLocation: ChatLocation, associatedData: ChatMessageItemAssociatedData, controllerInteraction: ChatControllerInteraction, mode: ChatHistoryListMode, lastHeaderId: Int64, entries: [ChatHistoryViewTransitionUpdateEntry], wantTrButton: [(Bool, [String])]) -> [ListViewUpdateItem] { + var disableFloatingDateHeaders = false + if case .customChatContents = chatLocation { + disableFloatingDateHeaders = true + } + return entries.map { entry -> ListViewUpdateItem in switch entry.entry { case let .MessageEntry(message, presentationData, read, location, selection, attributes): @@ -281,7 +291,7 @@ private func mappedUpdateEntries(context: AccountContext, chatLocation: ChatLoca switch mode { case .bubbles: // MARK: Nicegram, wantTrButton - item = ChatMessageItemImpl(presentationData: presentationData, context: context, chatLocation: chatLocation, associatedData: associatedData, controllerInteraction: controllerInteraction, content: .message(message: message, read: read, selection: selection, attributes: attributes, location: location), wantTrButton: wantTrButton) + item = ChatMessageItemImpl(presentationData: presentationData, context: context, chatLocation: chatLocation, associatedData: associatedData, controllerInteraction: controllerInteraction, content: .message(message: message, read: read, selection: selection, attributes: attributes, location: location), disableDate: disableFloatingDateHeaders, wantTrButton: wantTrButton) case let .list(_, _, _, displayHeaders, hintLinks, isGlobalSearch): let displayHeader: Bool switch displayHeaders { @@ -300,7 +310,7 @@ private func mappedUpdateEntries(context: AccountContext, chatLocation: ChatLoca switch mode { case .bubbles: // MARK: Nicegram, wantTrButton - item = ChatMessageItemImpl(presentationData: presentationData, context: context, chatLocation: chatLocation, associatedData: associatedData, controllerInteraction: controllerInteraction, content: .group(messages: messages), wantTrButton: wantTrButton) + item = ChatMessageItemImpl(presentationData: presentationData, context: context, chatLocation: chatLocation, associatedData: associatedData, controllerInteraction: controllerInteraction, content: .group(messages: messages), disableDate: disableFloatingDateHeaders, wantTrButton: wantTrButton) case .list: assertionFailure() item = ListMessageItem(presentationData: presentationData, context: context, chatLocation: chatLocation, interaction: ListMessageItemInteraction(controllerInteraction: controllerInteraction), message: messages[0].0, selection: .none, displayHeader: false) @@ -475,6 +485,9 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto private var enableUnreadAlignment: Bool = true private var historyView: ChatHistoryView? + public var originalHistoryView: MessageHistoryView? { + return self.historyView?.originalView + } private let historyDisposable = MetaDisposable() private let readHistoryDisposable = MetaDisposable() @@ -811,35 +824,35 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto //self.debugInfo = true self.messageProcessingManager.process = { [weak context] messageIds in - context?.account.viewTracker.updateViewCountForMessageIds(messageIds: messageIds, clientId: clientId.with { $0 }) + context?.account.viewTracker.updateViewCountForMessageIds(messageIds: Set(messageIds.map(\.messageId)), clientId: clientId.with { $0 }) } self.messageWithReactionsProcessingManager.process = { [weak context] messageIds in - context?.account.viewTracker.updateReactionsForMessageIds(messageIds: messageIds) + context?.account.viewTracker.updateReactionsForMessageIds(messageIds: Set(messageIds.map(\.messageId))) } self.seenLiveLocationProcessingManager.process = { [weak context] messageIds in - context?.account.viewTracker.updateSeenLiveLocationForMessageIds(messageIds: messageIds) + context?.account.viewTracker.updateSeenLiveLocationForMessageIds(messageIds: Set(messageIds.map(\.messageId))) } self.unsupportedMessageProcessingManager.process = { [weak context] messageIds in context?.account.viewTracker.updateUnsupportedMediaForMessageIds(messageIds: messageIds) } self.refreshMediaProcessingManager.process = { [weak context] messageIds in - context?.account.viewTracker.refreshSecretMediaMediaForMessageIds(messageIds: messageIds) + context?.account.viewTracker.refreshSecretMediaMediaForMessageIds(messageIds: Set(messageIds.map(\.messageId))) } self.refreshStoriesProcessingManager.process = { [weak context] messageIds in - context?.account.viewTracker.refreshStoriesForMessageIds(messageIds: messageIds) + context?.account.viewTracker.refreshStoriesForMessageIds(messageIds: Set(messageIds.map(\.messageId))) } self.translationProcessingManager.process = { [weak self, weak context] messageIds in if let context = context, let toLang = self?.toLang { - let _ = translateMessageIds(context: context, messageIds: Array(messageIds), toLang: toLang).startStandalone() + let _ = translateMessageIds(context: context, messageIds: Array(messageIds.map(\.messageId)), toLang: toLang).startStandalone() } } self.messageMentionProcessingManager.process = { [weak self, weak context] messageIds in if let strongSelf = self { if strongSelf.canReadHistoryValue { - context?.account.viewTracker.updateMarkMentionsSeenForMessageIds(messageIds: messageIds) + context?.account.viewTracker.updateMarkMentionsSeenForMessageIds(messageIds: Set(messageIds.map(\.messageId))) } else { - strongSelf.messageIdsScheduledForMarkAsSeen.formUnion(messageIds) + strongSelf.messageIdsScheduledForMarkAsSeen.formUnion(messageIds.map(\.messageId)) } } } @@ -849,9 +862,9 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto return } if strongSelf.canReadHistoryValue && !strongSelf.suspendReadingReactions && !strongSelf.context.sharedContext.immediateExperimentalUISettings.skipReadHistory { - strongSelf.context.account.viewTracker.updateMarkReactionsSeenForMessageIds(messageIds: messageIds) + strongSelf.context.account.viewTracker.updateMarkReactionsSeenForMessageIds(messageIds: Set(messageIds.map(\.messageId))) } else { - strongSelf.messageIdsWithReactionsScheduledForMarkAsSeen.formUnion(messageIds) + strongSelf.messageIdsWithReactionsScheduledForMarkAsSeen.formUnion(messageIds.map(\.messageId)) } } @@ -859,7 +872,7 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto guard let strongSelf = self else { return } - strongSelf.context.account.viewTracker.updatedExtendedMediaForMessageIds(messageIds: messageIds) + strongSelf.context.account.viewTracker.updatedExtendedMediaForMessageIds(messageIds: Set(messageIds.map(\.messageId))) } self.preloadPages = false @@ -1286,6 +1299,68 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto return (ChatHistoryViewUpdate.HistoryView(view: MessageHistoryView(tag: nil, namespaces: .all, entries: messages.reversed().map { MessageHistoryEntry(message: $0, isRead: false, location: nil, monthLocation: nil, attributes: MutableMessageHistoryEntryAttributes(authorIsContact: false)) }, holeEarlier: hasMore, holeLater: false, isLoading: false), type: .Generic(type: version > 0 ? ViewUpdateType.Generic : ViewUpdateType.Initial), scrollPosition: scrollPosition, flashIndicators: false, originalScrollPosition: nil, initialData: ChatHistoryCombinedInitialData(initialData: nil, buttonKeyboardMessage: nil, cachedData: nil, cachedDataMessages: nil, readStateData: nil), id: 0), version, nil, nil) } + } else if case let .customView(historyView) = self.source { + historyViewUpdate = combineLatest(queue: .mainQueue(), + self.chatHistoryLocationPromise.get(), + self.ignoreMessagesInTimestampRangePromise.get() + ) + |> distinctUntilChanged(isEqual: { lhs, rhs in + if lhs.0 != rhs.0 { + return false + } + if lhs.1 != rhs.1 { + return false + } + return true + }) + |> mapToSignal { location, _ -> Signal<((MessageHistoryView, ViewUpdateType), ChatHistoryLocationInput?), NoError> in + return historyView + |> map { historyView in + return (historyView, location) + } + } + |> map { viewAndUpdate, location in + let (view, update) = viewAndUpdate + + let version = currentViewVersion.modify({ value in + if let value = value { + return value + 1 + } else { + return 0 + } + })! + + var scrollPositionValue: ChatHistoryViewScrollPosition? + if let location { + switch location.content { + case let .Scroll(subject, _, _, scrollPosition, animated, highlight): + scrollPositionValue = .index(subject: subject, position: scrollPosition, directionHint: .Up, animated: animated, highlight: highlight, displayLink: false) + default: + break + } + } + + return ( + ChatHistoryViewUpdate.HistoryView( + view: view, + type: .Generic(type: update), + scrollPosition: scrollPositionValue, + flashIndicators: false, + originalScrollPosition: nil, + initialData: ChatHistoryCombinedInitialData( + initialData: nil, + buttonKeyboardMessage: nil, + cachedData: nil, + cachedDataMessages: nil, + readStateData: nil + ), + id: location?.id ?? 0 + ), + version, + location, + nil + ) + } } else { historyViewUpdate = combineLatest(queue: .mainQueue(), self.chatHistoryLocationPromise.get(), @@ -2043,10 +2118,12 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto } if apply { switch strongSelf.chatLocation { - case .peer, .replyThread, .feed: + case .peer, .replyThread: if !strongSelf.context.sharedContext.immediateExperimentalUISettings.skipReadHistory { strongSelf.context.applyMaxReadIndex(for: strongSelf.chatLocation, contextHolder: strongSelf.chatLocationContextHolder, messageIndex: messageIndex) } + case .customChatContents: + break } } }).strict()) @@ -2432,7 +2509,7 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto var messageIdsWithViewCount: [MessageId] = [] var messageIdsWithLiveLocation: [MessageId] = [] - var messageIdsWithUnsupportedMedia: [MessageId] = [] + var messageIdsWithUnsupportedMedia: [MessageAndThreadId] = [] var messageIdsWithRefreshMedia: [MessageId] = [] var messageIdsWithRefreshStories: [MessageId] = [] var messageIdsWithUnseenPersonalMention: [MessageId] = [] @@ -2533,7 +2610,7 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto } } if contentRequiredValidation { - messageIdsWithUnsupportedMedia.append(message.id) + messageIdsWithUnsupportedMedia.append(MessageAndThreadId(messageId: message.id, threadId: message.threadId)) } if mediaRequiredValidation { messageIdsWithRefreshMedia.append(message.id) @@ -2729,41 +2806,41 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto } if !messageIdsWithViewCount.isEmpty { - self.messageProcessingManager.add(messageIdsWithViewCount) + self.messageProcessingManager.add(messageIdsWithViewCount.map { MessageAndThreadId(messageId: $0, threadId: nil) }) } if !messageIdsWithLiveLocation.isEmpty { - self.seenLiveLocationProcessingManager.add(messageIdsWithLiveLocation) + self.seenLiveLocationProcessingManager.add(messageIdsWithLiveLocation.map { MessageAndThreadId(messageId: $0, threadId: nil) }) } if !messageIdsWithUnsupportedMedia.isEmpty { self.unsupportedMessageProcessingManager.add(messageIdsWithUnsupportedMedia) } if !messageIdsWithRefreshMedia.isEmpty { - self.refreshMediaProcessingManager.add(messageIdsWithRefreshMedia) + self.refreshMediaProcessingManager.add(messageIdsWithRefreshMedia.map { MessageAndThreadId(messageId: $0, threadId: nil) }) } if !messageIdsWithRefreshStories.isEmpty { - self.refreshStoriesProcessingManager.add(messageIdsWithRefreshStories) + self.refreshStoriesProcessingManager.add(messageIdsWithRefreshStories.map { MessageAndThreadId(messageId: $0, threadId: nil) }) } if !messageIdsWithUnseenPersonalMention.isEmpty { - self.messageMentionProcessingManager.add(messageIdsWithUnseenPersonalMention) + self.messageMentionProcessingManager.add(messageIdsWithUnseenPersonalMention.map { MessageAndThreadId(messageId: $0, threadId: nil) }) } if !messageIdsWithUnseenReactions.isEmpty { - self.unseenReactionsProcessingManager.add(messageIdsWithUnseenReactions) + self.unseenReactionsProcessingManager.add(messageIdsWithUnseenReactions.map { MessageAndThreadId(messageId: $0, threadId: nil) }) if self.canReadHistoryValue && !self.context.sharedContext.immediateExperimentalUISettings.skipReadHistory { let _ = self.displayUnseenReactionAnimations(messageIds: messageIdsWithUnseenReactions) } } if !messageIdsWithPossibleReactions.isEmpty { - self.messageWithReactionsProcessingManager.add(messageIdsWithPossibleReactions) + self.messageWithReactionsProcessingManager.add(messageIdsWithPossibleReactions.map { MessageAndThreadId(messageId: $0, threadId: nil) }) } if !downloadableResourceIds.isEmpty { let _ = markRecentDownloadItemsAsSeen(postbox: self.context.account.postbox, items: downloadableResourceIds).startStandalone() } if !messageIdsWithInactiveExtendedMedia.isEmpty { - self.extendedMediaProcessingManager.update(messageIdsWithInactiveExtendedMedia) + self.extendedMediaProcessingManager.update(Set(messageIdsWithInactiveExtendedMedia.map { MessageAndThreadId(messageId: $0, threadId: nil) })) } if !messageIdsToTranslate.isEmpty { - self.translationProcessingManager.add(messageIdsToTranslate) + self.translationProcessingManager.add(messageIdsToTranslate.map { MessageAndThreadId(messageId: $0, threadId: nil) }) } if !visibleAdOpaqueIds.isEmpty { for opaqueId in visibleAdOpaqueIds { @@ -2789,7 +2866,7 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto switch self.chatLocation { case .peer: messageIndex = maxIncomingIndex - case .replyThread, .feed: + case .replyThread, .customChatContents: messageIndex = maxOverallIndex } @@ -3174,7 +3251,13 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto } let isEmpty = transition.historyView.originalView.entries.isEmpty || loadState == .empty(.botInfo) - let historyState: ChatHistoryNodeHistoryState = .loaded(isEmpty: isEmpty) + + var hasReachedLimits = false + if case let .customChatContents(customChatContents) = self.subject, let messageLimit = customChatContents.messageLimit { + hasReachedLimits = transition.historyView.originalView.entries.count >= messageLimit + } + + let historyState: ChatHistoryNodeHistoryState = .loaded(isEmpty: isEmpty, hasReachedLimits: hasReachedLimits) if self.currentHistoryState != historyState { self.currentHistoryState = historyState self.historyState.set(historyState) @@ -3435,7 +3518,14 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto strongSelf.historyView = transition.historyView let loadState: ChatHistoryNodeLoadState + var alwaysHasMessages = false if case .custom = strongSelf.source { + if case .customChatContents = strongSelf.chatLocation { + } else { + alwaysHasMessages = true + } + } + if alwaysHasMessages { loadState = .messages } else if let historyView = strongSelf.historyView { if historyView.filteredEntries.isEmpty { @@ -3545,7 +3635,7 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto switch strongSelf.chatLocation { case .peer: messageIndex = incomingIndex - case .replyThread, .feed: + case .replyThread, .customChatContents: messageIndex = overallIndex } @@ -3566,7 +3656,11 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto } strongSelf._cachedPeerDataAndMessages.set(.single((transition.cachedData, transition.cachedDataMessages))) let isEmpty = transition.historyView.originalView.entries.isEmpty || loadState == .empty(.botInfo) - let historyState: ChatHistoryNodeHistoryState = .loaded(isEmpty: isEmpty) + var hasReachedLimits = false + if case let .customChatContents(customChatContents) = strongSelf.subject, let messageLimit = customChatContents.messageLimit { + hasReachedLimits = transition.historyView.originalView.entries.count >= messageLimit + } + let historyState: ChatHistoryNodeHistoryState = .loaded(isEmpty: isEmpty, hasReachedLimits: hasReachedLimits) if strongSelf.currentHistoryState != historyState { strongSelf.currentHistoryState = historyState strongSelf.historyState.set(historyState) @@ -3650,7 +3744,7 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto let visibleNewIncomingReactionMessageIds = strongSelf.displayUnseenReactionAnimations(messageIds: messageIds) if !visibleNewIncomingReactionMessageIds.isEmpty { - strongSelf.unseenReactionsProcessingManager.add(visibleNewIncomingReactionMessageIds) + strongSelf.unseenReactionsProcessingManager.add(visibleNewIncomingReactionMessageIds.map { MessageAndThreadId(messageId: $0, threadId: nil) }) } } @@ -4074,6 +4168,9 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto if let messageItem = messageItem { let associatedData = messageItem.associatedData let wantTrButton = usetrButton() + + let disableFloatingDateHeaders = messageItem.disableDate + loop: for i in 0 ..< historyView.filteredEntries.count { switch historyView.filteredEntries[i] { case let .MessageEntry(message, presentationData, read, location, selection, attributes): @@ -4083,7 +4180,7 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto switch self.mode { case .bubbles: // MARK: Nicegram, wantTrButton - item = ChatMessageItemImpl(presentationData: presentationData, context: self.context, chatLocation: self.chatLocation, associatedData: associatedData, controllerInteraction: self.controllerInteraction, content: .message(message: message, read: read, selection: selection, attributes: attributes, location: location), wantTrButton: wantTrButton) + item = ChatMessageItemImpl(presentationData: presentationData, context: self.context, chatLocation: self.chatLocation, associatedData: associatedData, controllerInteraction: self.controllerInteraction, content: .message(message: message, read: read, selection: selection, attributes: attributes, location: location), disableDate: disableFloatingDateHeaders, wantTrButton: wantTrButton) case let .list(_, _, _, displayHeaders, hintLinks, isGlobalSearch): let displayHeader: Bool switch displayHeaders { @@ -4130,6 +4227,7 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto if let messageItem = messageItem { let associatedData = messageItem.associatedData + let disableFloatingDateHeaders = messageItem.disableDate loop: for i in 0 ..< historyView.filteredEntries.count { switch historyView.filteredEntries[i] { @@ -4139,7 +4237,7 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto let item: ListViewItem switch self.mode { case .bubbles: - item = ChatMessageItemImpl(presentationData: presentationData, context: self.context, chatLocation: self.chatLocation, associatedData: associatedData, controllerInteraction: self.controllerInteraction, content: .message(message: message, read: read, selection: selection, attributes: attributes, location: location)) + item = ChatMessageItemImpl(presentationData: presentationData, context: self.context, chatLocation: self.chatLocation, associatedData: associatedData, controllerInteraction: self.controllerInteraction, content: .message(message: message, read: read, selection: selection, attributes: attributes, location: location), disableDate: disableFloatingDateHeaders) case let .list(_, _, _, displayHeaders, hintLinks, isGlobalSearch): let displayHeader: Bool switch displayHeaders { diff --git a/submodules/TelegramUI/Sources/ChatHistoryViewForLocation.swift b/submodules/TelegramUI/Sources/ChatHistoryViewForLocation.swift index 47c47ed6c3d..47f937eb456 100644 --- a/submodules/TelegramUI/Sources/ChatHistoryViewForLocation.swift +++ b/submodules/TelegramUI/Sources/ChatHistoryViewForLocation.swift @@ -333,7 +333,7 @@ private func extractAdditionalData(view: MessageHistoryView, chatLocation: ChatL readStateData[peerId] = ChatHistoryCombinedInitialReadStateData(unreadCount: readState.count, totalState: totalUnreadState, notificationSettings: notificationSettings) } } - case .replyThread, .feed: + case .replyThread, .customChatContents: break } default: diff --git a/submodules/TelegramUI/Sources/ChatInterfaceInputContextPanels.swift b/submodules/TelegramUI/Sources/ChatInterfaceInputContextPanels.swift index f9fd936ea06..60ad36b3211 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceInputContextPanels.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceInputContextPanels.swift @@ -15,7 +15,7 @@ private func inputQueryResultPriority(_ result: ChatPresentationInputQueryResult case let .mentions(items): return (2, !items.isEmpty) case let .commands(items): - return (3, !items.isEmpty) + return (3, !items.commands.isEmpty || items.hasShortcuts) case let .contextRequestResult(_, result): var nonEmpty = false if let result = result, !result.results.isEmpty { @@ -32,10 +32,6 @@ private func inputQueryResultPriority(_ result: ChatPresentationInputQueryResult } func inputContextPanelForChatPresentationIntefaceState(_ chatPresentationInterfaceState: ChatPresentationInterfaceState, context: AccountContext, currentPanel: ChatInputContextPanelNode?, controllerInteraction: ChatControllerInteraction, interfaceInteraction: ChatPanelInterfaceInteraction?, chatPresentationContext: ChatPresentationContext) -> ChatInputContextPanelNode? { - guard let _ = chatPresentationInterfaceState.renderedPeer?.peer else { - return nil - } - if chatPresentationInterfaceState.showCommands, let renderedPeer = chatPresentationInterfaceState.renderedPeer { if let currentPanel = currentPanel as? CommandMenuChatInputContextPanelNode { return currentPanel @@ -159,14 +155,14 @@ func inputContextPanelForChatPresentationIntefaceState(_ chatPresentationInterfa return nil } case let .commands(commands): - if !commands.isEmpty { + if !commands.commands.isEmpty || commands.hasShortcuts { if let currentPanel = currentPanel as? CommandChatInputContextPanelNode { - currentPanel.updateResults(commands) + currentPanel.updateResults(commands.commands, accountPeer: commands.accountPeer, hasShortcuts: commands.hasShortcuts, query: commands.query) return currentPanel } else { let panel = CommandChatInputContextPanelNode(context: context, theme: chatPresentationInterfaceState.theme, strings: chatPresentationInterfaceState.strings, fontSize: chatPresentationInterfaceState.fontSize, chatPresentationContext: controllerInteraction.presentationContext) panel.interfaceInteraction = interfaceInteraction - panel.updateResults(commands) + panel.updateResults(commands.commands, accountPeer: commands.accountPeer, hasShortcuts: commands.hasShortcuts, query: commands.query) return panel } } else { diff --git a/submodules/TelegramUI/Sources/ChatInterfaceStateAccessoryPanels.swift b/submodules/TelegramUI/Sources/ChatInterfaceStateAccessoryPanels.swift index fe7d3204546..ac201532fef 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceStateAccessoryPanels.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceStateAccessoryPanels.swift @@ -76,12 +76,21 @@ func accessoryPanelForChatPresentationIntefaceState(_ chatPresentationInterfaceS replyPanelNode.interfaceInteraction = interfaceInteraction replyPanelNode.updateThemeAndStrings(theme: chatPresentationInterfaceState.theme, strings: chatPresentationInterfaceState.strings) return replyPanelNode - } else if let peerId = chatPresentationInterfaceState.chatLocation.peerId { - let panelNode = ReplyAccessoryPanelNode(context: context, chatPeerId: peerId, messageId: replyMessageSubject.messageId, quote: replyMessageSubject.quote, theme: chatPresentationInterfaceState.theme, strings: chatPresentationInterfaceState.strings, nameDisplayOrder: chatPresentationInterfaceState.nameDisplayOrder, dateTimeFormat: chatPresentationInterfaceState.dateTimeFormat, animationCache: chatControllerInteraction?.presentationContext.animationCache, animationRenderer: chatControllerInteraction?.presentationContext.animationRenderer) - panelNode.interfaceInteraction = interfaceInteraction - return panelNode } else { - return nil + var chatPeerId: EnginePeer.Id? + if let peerId = chatPresentationInterfaceState.chatLocation.peerId { + chatPeerId = peerId + } else if case .customChatContents = chatPresentationInterfaceState.chatLocation { + chatPeerId = context.account.peerId + } + + if let chatPeerId { + let panelNode = ReplyAccessoryPanelNode(context: context, chatPeerId: chatPeerId, messageId: replyMessageSubject.messageId, quote: replyMessageSubject.quote, theme: chatPresentationInterfaceState.theme, strings: chatPresentationInterfaceState.strings, nameDisplayOrder: chatPresentationInterfaceState.nameDisplayOrder, dateTimeFormat: chatPresentationInterfaceState.dateTimeFormat, animationCache: chatControllerInteraction?.presentationContext.animationCache, animationRenderer: chatControllerInteraction?.presentationContext.animationRenderer) + panelNode.interfaceInteraction = interfaceInteraction + return panelNode + } else { + return nil + } } } else { return nil diff --git a/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift b/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift index 52e43d8871a..f2935d90359 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift @@ -90,6 +90,8 @@ private func canEditMessage(accountPeerId: PeerId, limitsConfiguration: EngineCo } else { hasEditRights = true } + } else if message.id.namespace == Namespaces.Message.QuickReplyCloud { + hasEditRights = true } else if message.id.peerId.namespace == Namespaces.Peer.SecretChat || message.id.namespace != Namespaces.Message.Cloud { hasEditRights = false } else if let author = message.author, author.id == accountPeerId, let peer = message.peers[message.id.peerId] { @@ -293,6 +295,10 @@ private func canViewReadStats(message: Message, participantCount: Int?, isMessag } func canReplyInChat(_ chatPresentationInterfaceState: ChatPresentationInterfaceState, accountPeerId: PeerId) -> Bool { + if case .customChatContents = chatPresentationInterfaceState.chatLocation { + return true + } + guard let peer = chatPresentationInterfaceState.renderedPeer?.peer else { return false } @@ -359,8 +365,8 @@ func canReplyInChat(_ chatPresentationInterfaceState: ChatPresentationInterfaceS } case .replyThread: canReply = true - case .feed: - canReply = false + case .customChatContents: + canReply = true } return canReply } @@ -671,12 +677,12 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState } } - if Namespaces.Message.allScheduled.contains(message.id.namespace) || message.id.peerId.isReplies { + if Namespaces.Message.allNonRegular.contains(message.id.namespace) || message.id.peerId.isReplies { canReply = false canPin = false } else if messages[0].flags.intersection([.Failed, .Unsent]).isEmpty { switch chatPresentationInterfaceState.chatLocation { - case .peer, .replyThread, .feed: + case .peer, .replyThread, .customChatContents: if let channel = messages[0].peers[messages[0].id.peerId] as? TelegramChannel { if !isAction { canPin = channel.hasPermission(.pinMessages) @@ -1134,8 +1140,6 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState }))) } - - var messageText: String = "" for message in messages { if !message.text.isEmpty { @@ -1158,6 +1162,16 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState } } + for attribute in message.attributes { + if hasExpandedAudioTranscription, let attribute = attribute as? AudioTranscriptionMessageAttribute { + if !messageText.isEmpty { + messageText.append("\n") + } + messageText.append(attribute.text) + break + } + } + var isPoll = false if messageText.isEmpty { for media in message.media { @@ -2229,6 +2243,76 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState actions.removeFirst() } + if let message = messages.first, case let .customChatContents(customChatContents) = chatPresentationInterfaceState.subject { + actions.removeAll() + + switch customChatContents.kind { + case .quickReplyMessageInput: + if !messageText.isEmpty || (resourceAvailable && isImage) || diceEmoji != nil { + actions.append(.action(ContextMenuActionItem(text: chatPresentationInterfaceState.strings.Conversation_ContextMenuCopy, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Copy"), color: theme.actionSheet.primaryTextColor) + }, action: { _, f in + var messageEntities: [MessageTextEntity]? + var restrictedText: String? + for attribute in message.attributes { + if let attribute = attribute as? TextEntitiesMessageAttribute { + messageEntities = attribute.entities + } + if let attribute = attribute as? RestrictedContentMessageAttribute { + restrictedText = attribute.platformText(platform: "ios", contentSettings: context.currentContentSettings.with { $0 }) ?? "" + } + } + + if let restrictedText = restrictedText { + storeMessageTextInPasteboard(restrictedText, entities: nil) + } else { + if let translationState = chatPresentationInterfaceState.translationState, translationState.isEnabled, + let translation = message.attributes.first(where: { ($0 as? TranslationMessageAttribute)?.toLang == translationState.toLang }) as? TranslationMessageAttribute, !translation.text.isEmpty { + storeMessageTextInPasteboard(translation.text, entities: translation.entities) + } else { + storeMessageTextInPasteboard(message.text, entities: messageEntities) + } + } + + Queue.mainQueue().after(0.2, { + let content: UndoOverlayContent = .copy(text: chatPresentationInterfaceState.strings.Conversation_MessageCopied) + controllerInteraction.displayUndo(content) + }) + + f(.default) + }))) + } + + if message.id.namespace == Namespaces.Message.QuickReplyCloud { + if data.canEdit { + actions.append(.action(ContextMenuActionItem(text: chatPresentationInterfaceState.strings.Conversation_MessageDialogEdit, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Edit"), color: theme.actionSheet.primaryTextColor) + }, action: { c, f in + interfaceInteraction.setupEditMessage(messages[0].id, { transition in + f(.custom(transition)) + }) + }))) + } + } + + if message.id.id < Int32.max - 1000 { + if !actions.isEmpty { + actions.append(.separator) + } + actions.append(.action(ContextMenuActionItem(text: chatPresentationInterfaceState.strings.Conversation_ContextMenuDelete, textColor: .destructive, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.actionSheet.destructiveActionTextColor) + }, action: { [weak customChatContents] _, f in + f(.dismissWithoutContent) + + guard let customChatContents else { + return + } + customChatContents.deleteMessages(ids: messages.map(\.id)) + }))) + } + } + } + return ContextController.Items(content: .list(actions), tip: nil) } } diff --git a/submodules/TelegramUI/Sources/ChatInterfaceStateContextQueries.swift b/submodules/TelegramUI/Sources/ChatInterfaceStateContextQueries.swift index d55673ba542..521035310f6 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceStateContextQueries.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceStateContextQueries.swift @@ -18,9 +18,6 @@ import ChatPresentationInterfaceState import ChatContextQuery func contextQueryResultStateForChatInterfacePresentationState(_ chatPresentationInterfaceState: ChatPresentationInterfaceState, context: AccountContext, currentQueryStates: inout [ChatPresentationInputQueryKind: (ChatPresentationInputQuery, Disposable)], requestBotLocationStatus: @escaping (PeerId) -> Void) -> [ChatPresentationInputQueryKind: ChatContextQueryUpdate] { - guard let peer = chatPresentationInterfaceState.renderedPeer?.peer else { - return [:] - } let inputQueries = inputContextQueriesForChatPresentationIntefaceState(chatPresentationInterfaceState).filter({ query in if chatPresentationInterfaceState.editMessageState != nil { switch query { @@ -39,7 +36,7 @@ func contextQueryResultStateForChatInterfacePresentationState(_ chatPresentation for query in inputQueries { let previousQuery = currentQueryStates[query.kind]?.0 if previousQuery != query { - let signal = updatedContextQueryResultStateForQuery(context: context, peer: peer, chatLocation: chatPresentationInterfaceState.chatLocation, inputQuery: query, previousQuery: previousQuery, requestBotLocationStatus: requestBotLocationStatus) + let signal = updatedContextQueryResultStateForQuery(context: context, peer: chatPresentationInterfaceState.renderedPeer?.peer, chatLocation: chatPresentationInterfaceState.chatLocation, inputQuery: query, previousQuery: previousQuery, requestBotLocationStatus: requestBotLocationStatus) updates[query.kind] = .update(query, signal) } } @@ -60,7 +57,7 @@ func contextQueryResultStateForChatInterfacePresentationState(_ chatPresentation return updates } -private func updatedContextQueryResultStateForQuery(context: AccountContext, peer: Peer, chatLocation: ChatLocation, inputQuery: ChatPresentationInputQuery, previousQuery: ChatPresentationInputQuery?, requestBotLocationStatus: @escaping (PeerId) -> Void) -> Signal<(ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?, ChatContextQueryError> { +private func updatedContextQueryResultStateForQuery(context: AccountContext, peer: Peer?, chatLocation: ChatLocation, inputQuery: ChatPresentationInputQuery, previousQuery: ChatPresentationInputQuery?, requestBotLocationStatus: @escaping (PeerId) -> Void) -> Signal<(ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?, ChatContextQueryError> { switch inputQuery { case let .emoji(query): var signal: Signal<(ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?, ChatContextQueryError> = .complete() @@ -182,74 +179,133 @@ private func updatedContextQueryResultStateForQuery(context: AccountContext, pee let inlineBots: Signal<[(EnginePeer, Double)], NoError> = types.contains(.contextBots) ? context.engine.peers.recentlyUsedInlineBots() : .single([]) let strings = context.sharedContext.currentPresentationData.with({ $0 }).strings - let participants = combineLatest(inlineBots, searchPeerMembers(context: context, peerId: peer.id, chatLocation: chatLocation, query: query, scope: .mention)) - |> map { inlineBots, peers -> (ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult? in - let filteredInlineBots = inlineBots.sorted(by: { $0.1 > $1.1 }).filter { peer, rating in - if rating < 0.14 { + + let participants: Signal<(ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?, ChatContextQueryError> + if let peer { + participants = combineLatest( + inlineBots, + searchPeerMembers(context: context, peerId: peer.id, chatLocation: chatLocation, query: query, scope: .mention) + ) + |> map { inlineBots, peers -> (ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult? in + let filteredInlineBots = inlineBots.sorted(by: { $0.1 > $1.1 }).filter { peer, rating in + if rating < 0.14 { + return false + } + if peer.indexName.matchesByTokens(normalizedQuery) { + return true + } + if let addressName = peer.addressName, addressName.lowercased().hasPrefix(normalizedQuery) { + return true + } return false - } - if peer.indexName.matchesByTokens(normalizedQuery) { - return true - } - if let addressName = peer.addressName, addressName.lowercased().hasPrefix(normalizedQuery) { + }.map { $0.0 } + + let inlineBotPeerIds = Set(filteredInlineBots.map { $0.id }) + + let filteredPeers = peers.filter { peer in + if inlineBotPeerIds.contains(peer.id) { + return false + } + if !types.contains(.accountPeer) && peer.id == context.account.peerId { + return false + } return true } - return false - }.map { $0.0 } - - let inlineBotPeerIds = Set(filteredInlineBots.map { $0.id }) - - let filteredPeers = peers.filter { peer in - if inlineBotPeerIds.contains(peer.id) { - return false - } - if !types.contains(.accountPeer) && peer.id == context.account.peerId { - return false + var sortedPeers = filteredInlineBots + sortedPeers.append(contentsOf: filteredPeers.sorted(by: { lhs, rhs in + let result = lhs.indexName.stringRepresentation(lastNameFirst: true).compare(rhs.indexName.stringRepresentation(lastNameFirst: true)) + return result == .orderedAscending + })) + sortedPeers = sortedPeers.filter { peer in + return !peer.displayTitle(strings: strings, displayOrder: .firstLast).isEmpty } - return true - } - var sortedPeers = filteredInlineBots - sortedPeers.append(contentsOf: filteredPeers.sorted(by: { lhs, rhs in - let result = lhs.indexName.stringRepresentation(lastNameFirst: true).compare(rhs.indexName.stringRepresentation(lastNameFirst: true)) - return result == .orderedAscending - })) - sortedPeers = sortedPeers.filter { peer in - return !peer.displayTitle(strings: strings, displayOrder: .firstLast).isEmpty + return { _ in return .mentions(sortedPeers) } } - return { _ in return .mentions(sortedPeers) } + |> castError(ChatContextQueryError.self) + } else { + participants = .single({ _ in return nil }) } - |> castError(ChatContextQueryError.self) return signal |> then(participants) case let .command(query): + guard let peer else { + return .single({ _ in return .commands(ChatInputQueryCommandsResult( + commands: [], + accountPeer: nil, + hasShortcuts: false, + query: "" + )) }) + } + let normalizedQuery = query.lowercased() var signal: Signal<(ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?, ChatContextQueryError> = .complete() if let previousQuery = previousQuery { switch previousQuery { - case .command: - break - default: - signal = .single({ _ in return .commands([]) }) + case .command: + break + default: + signal = .single({ _ in return .commands(ChatInputQueryCommandsResult( + commands: [], + accountPeer: nil, + hasShortcuts: false, + query: "" + )) }) } } else { - signal = .single({ _ in return .commands([]) }) + signal = .single({ _ in return .commands(ChatInputQueryCommandsResult( + commands: [], + accountPeer: nil, + hasShortcuts: false, + query: "" + )) }) + } + + var shortcuts: Signal<[ShortcutMessageList.Item], NoError> = .single([]) + if let user = peer as? TelegramUser, user.botInfo == nil { + context.account.viewTracker.keepQuickRepliesApproximatelyUpdated() + + shortcuts = context.engine.accountData.shortcutMessageList(onlyRemote: true) + |> map { shortcutMessageList -> [ShortcutMessageList.Item] in + return shortcutMessageList.items.filter { item in + return item.shortcut.hasPrefix(normalizedQuery) + } + } } - let commands = context.engine.peers.peerCommands(id: peer.id) - |> map { commands -> (ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult? in + let commands = combineLatest( + context.engine.peers.peerCommands(id: peer.id), + shortcuts, + context.engine.data.subscribe( + TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId) + ) + ) + |> map { commands, shortcuts, accountPeer -> (ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult? in let filteredCommands = commands.commands.filter { command in if command.command.text.hasPrefix(normalizedQuery) { return true } return false } - let sortedCommands = filteredCommands - return { _ in return .commands(sortedCommands) } + var sortedCommands = filteredCommands.map(ChatInputTextCommand.command) + if !shortcuts.isEmpty && sortedCommands.isEmpty { + for shortcut in shortcuts { + sortedCommands.append(.shortcut(shortcut)) + } + } + return { _ in return .commands(ChatInputQueryCommandsResult( + commands: sortedCommands, + accountPeer: accountPeer, + hasShortcuts: !shortcuts.isEmpty, + query: normalizedQuery + )) } } |> castError(ChatContextQueryError.self) return signal |> then(commands) case let .contextRequest(addressName, query): + guard let peer else { + return .single({ _ in return .contextRequestResult(nil, nil) }) + } var delayRequest = true var signal: Signal<(ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?, ChatContextQueryError> = .complete() if let previousQuery = previousQuery { diff --git a/submodules/TelegramUI/Sources/ChatInterfaceStateInputPanels.swift b/submodules/TelegramUI/Sources/ChatInterfaceStateInputPanels.swift index 893cc5ecca6..d5f3f2d97ef 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceStateInputPanels.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceStateInputPanels.swift @@ -377,7 +377,7 @@ func inputPanelForChatPresentationIntefaceState(_ chatPresentationInterfaceState if let user = chatPresentationInterfaceState.renderedPeer?.peer as? TelegramUser, user.botInfo != nil { displayBotStartPanel = true } - } else if let chatHistoryState = chatPresentationInterfaceState.chatHistoryState, case .loaded(true) = chatHistoryState { + } else if let chatHistoryState = chatPresentationInterfaceState.chatHistoryState, case .loaded(true, _) = chatHistoryState { if let user = chatPresentationInterfaceState.renderedPeer?.peer as? TelegramUser, user.botInfo != nil { displayBotStartPanel = true } @@ -410,6 +410,24 @@ func inputPanelForChatPresentationIntefaceState(_ chatPresentationInterfaceState } } + if case let .customChatContents(customChatContents) = chatPresentationInterfaceState.subject { + switch customChatContents.kind { + case .quickReplyMessageInput: + displayInputTextPanel = true + } + + if let chatHistoryState = chatPresentationInterfaceState.chatHistoryState, case .loaded(_, true) = chatHistoryState { + if let currentPanel = (currentPanel as? ChatRestrictedInputPanelNode) ?? (currentSecondaryPanel as? ChatRestrictedInputPanelNode) { + return (currentPanel, nil) + } else { + let panel = ChatRestrictedInputPanelNode() + panel.context = context + panel.interfaceInteraction = interfaceInteraction + return (panel, nil) + } + } + } + if case .inline = chatPresentationInterfaceState.mode { displayInputTextPanel = false } diff --git a/submodules/TelegramUI/Sources/ChatInterfaceStateNavigationButtons.swift b/submodules/TelegramUI/Sources/ChatInterfaceStateNavigationButtons.swift index a003c03e133..689cc49425a 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceStateNavigationButtons.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceStateNavigationButtons.swift @@ -54,6 +54,20 @@ func leftNavigationButtonForChatInterfaceState(_ presentationInterfaceState: Cha } } } + + if case let .customChatContents(customChatContents) = presentationInterfaceState.subject { + switch customChatContents.kind { + case .quickReplyMessageInput: + if let currentButton = currentButton, currentButton.action == .dismiss { + return currentButton + } else { + let buttonItem = UIBarButtonItem(title: strings.Common_Close, style: .plain, target: target, action: selector) + buttonItem.accessibilityLabel = strings.Common_Close + return ChatNavigationButton(action: .dismiss, buttonItem: buttonItem) + } + } + } + return nil } @@ -95,7 +109,7 @@ func rightNavigationButtonForChatInterfaceState(context: AccountContext, present var hasMessages = false if let chatHistoryState = presentationInterfaceState.chatHistoryState { - if case .loaded(false) = chatHistoryState { + if case .loaded(false, _) = chatHistoryState { hasMessages = true } } @@ -108,6 +122,24 @@ func rightNavigationButtonForChatInterfaceState(context: AccountContext, present return nil } + if case let .customChatContents(customChatContents) = presentationInterfaceState.subject { + switch customChatContents.kind { + case let .quickReplyMessageInput(_, shortcutType): + switch shortcutType { + case .generic: + if let currentButton = currentButton, currentButton.action == .edit { + return currentButton + } else { + let buttonItem = UIBarButtonItem(title: strings.Common_Edit, style: .plain, target: target, action: selector) + buttonItem.accessibilityLabel = strings.Common_Done + return ChatNavigationButton(action: .edit, buttonItem: buttonItem) + } + case .greeting, .away: + return nil + } + } + } + if case .replyThread = presentationInterfaceState.chatLocation { if let channel = presentationInterfaceState.renderedPeer?.peer as? TelegramChannel, channel.flags.contains(.isForum) { } else if hasMessages { diff --git a/submodules/TelegramUI/Sources/ChatMessageContextControllerContentSource.swift b/submodules/TelegramUI/Sources/ChatMessageContextControllerContentSource.swift index c14268f35a1..0bce425e9be 100644 --- a/submodules/TelegramUI/Sources/ChatMessageContextControllerContentSource.swift +++ b/submodules/TelegramUI/Sources/ChatMessageContextControllerContentSource.swift @@ -34,6 +34,7 @@ final class ChatMessageContextExtractedContentSource: ContextExtractedContentSou let blurBackground: Bool = true let centerVertically: Bool + private weak var chatController: ChatControllerImpl? private weak var chatNode: ChatControllerNode? private let engine: TelegramEngine private let message: Message @@ -43,6 +44,9 @@ final class ChatMessageContextExtractedContentSource: ContextExtractedContentSou if self.message.adAttribute != nil { return .single(false) } + if let chatController = self.chatController, case .customChatContents = chatController.subject { + return .single(false) + } return self.engine.data.subscribe(TelegramEngine.EngineData.Item.Messages.Message(id: self.message.id)) |> map { message -> Bool in @@ -55,7 +59,8 @@ final class ChatMessageContextExtractedContentSource: ContextExtractedContentSou |> distinctUntilChanged } - init(chatNode: ChatControllerNode, engine: TelegramEngine, message: Message, selectAll: Bool, centerVertically: Bool = false) { + init(chatController: ChatControllerImpl, chatNode: ChatControllerNode, engine: TelegramEngine, message: Message, selectAll: Bool, centerVertically: Bool = false) { + self.chatController = chatController self.chatNode = chatNode self.engine = engine self.message = message diff --git a/submodules/TelegramUI/Sources/ChatMessageThrottledProcessingManager.swift b/submodules/TelegramUI/Sources/ChatMessageThrottledProcessingManager.swift index a7eaa0f5b26..a29c5cb6f1a 100644 --- a/submodules/TelegramUI/Sources/ChatMessageThrottledProcessingManager.swift +++ b/submodules/TelegramUI/Sources/ChatMessageThrottledProcessingManager.swift @@ -4,30 +4,30 @@ import Postbox import SwiftSignalKit final class ChatMessageThrottledProcessingManager { - private let queue = Queue() + private let queue = Queue.mainQueue() private let delay: Double private let submitInterval: Double? - var process: ((Set) -> Void)? + var process: ((Set) -> Void)? private var timer: SwiftSignalKit.Timer? - private var processedList: [MessageId] = [] - private var processed: [MessageId: Double] = [:] - private var buffer = Set() + private var processedList: [MessageAndThreadId] = [] + private var processed: [MessageAndThreadId: Double] = [:] + private var buffer = Set() init(delay: Double = 1.0, submitInterval: Double? = nil) { self.delay = delay self.submitInterval = submitInterval } - func setProcess(process: @escaping (Set) -> Void) { + func setProcess(process: @escaping (Set) -> Void) { self.queue.async { self.process = process } } - func add(_ messageIds: [MessageId]) { + func add(_ messageIds: [MessageAndThreadId]) { self.queue.async { let timestamp = CFAbsoluteTimeGetCurrent() @@ -76,13 +76,13 @@ final class ChatMessageThrottledProcessingManager { final class ChatMessageVisibleThrottledProcessingManager { - private let queue = Queue() + private let queue = Queue.mainQueue() private let interval: Double - private var currentIds = Set() + private var currentIds = Set() - var process: ((Set) -> Void)? + var process: ((Set) -> Void)? private let timer: SwiftSignalKit.Timer @@ -107,13 +107,13 @@ final class ChatMessageVisibleThrottledProcessingManager { self.timer.invalidate() } - func setProcess(process: @escaping (Set) -> Void) { + func setProcess(process: @escaping (Set) -> Void) { self.queue.async { self.process = process } } - func update(_ ids: Set) { + func update(_ ids: Set) { self.queue.async { if self.currentIds != ids { self.currentIds = ids diff --git a/submodules/TelegramUI/Sources/ChatRestrictedInputPanelNode.swift b/submodules/TelegramUI/Sources/ChatRestrictedInputPanelNode.swift index 2325486c84d..265336934cf 100644 --- a/submodules/TelegramUI/Sources/ChatRestrictedInputPanelNode.swift +++ b/submodules/TelegramUI/Sources/ChatRestrictedInputPanelNode.swift @@ -91,16 +91,22 @@ final class ChatRestrictedInputPanelNode: ChatInputPanelNode { } else if personal { self.textNode.attributedText = NSAttributedString(string: interfaceState.strings.Conversation_RestrictedText, font: Font.regular(13.0), textColor: interfaceState.theme.chat.inputPanel.secondaryTextColor) } else { - if "".isEmpty { - //TODO:localize + if (self.presentationInterfaceState?.boostsToUnrestrict ?? 0) > 0 { iconSpacing = 0.0 iconImage = PresentationResourcesChat.chatPanelBoostIcon(interfaceState.theme) - self.textNode.attributedText = NSAttributedString(string: "Boost this group to send messages", font: Font.regular(15.0), textColor: interfaceState.theme.chat.inputPanel.panelControlAccentColor) + self.textNode.attributedText = NSAttributedString(string: interfaceState.strings.Conversation_BoostToUnrestrictText, font: Font.regular(15.0), textColor: interfaceState.theme.chat.inputPanel.panelControlAccentColor) isUserInteractionEnabled = true } else { self.textNode.attributedText = NSAttributedString(string: interfaceState.strings.Conversation_DefaultRestrictedText, font: Font.regular(13.0), textColor: interfaceState.theme.chat.inputPanel.secondaryTextColor) } } + } else if case let .customChatContents(customChatContents) = interfaceState.subject { + let displayCount: Int + switch customChatContents.kind { + case .quickReplyMessageInput: + displayCount = 20 + } + self.textNode.attributedText = NSAttributedString(string: interfaceState.strings.Chat_QuickReplyMessageLimitReachedText(Int32(displayCount)), font: Font.regular(13.0), textColor: interfaceState.theme.chat.inputPanel.secondaryTextColor) } self.buttonNode.isUserInteractionEnabled = isUserInteractionEnabled diff --git a/submodules/TelegramUI/Sources/ChatSearchNavigationContentNode.swift b/submodules/TelegramUI/Sources/ChatSearchNavigationContentNode.swift index 8ad90ba599f..73423965214 100644 --- a/submodules/TelegramUI/Sources/ChatSearchNavigationContentNode.swift +++ b/submodules/TelegramUI/Sources/ChatSearchNavigationContentNode.swift @@ -34,7 +34,7 @@ final class ChatSearchNavigationContentNode: NavigationBarContentNode { self.searchBar = SearchBarNode(theme: SearchBarNodeTheme(theme: theme, hasBackground: false, hasSeparator: false), strings: strings, fieldStyle: .modern) let placeholderText: String switch chatLocation { - case .peer, .replyThread, .feed: + case .peer, .replyThread, .customChatContents: if chatLocation.peerId == context.account.peerId, presentationInterfaceState.hasSearchTags { if case .standard(.embedded(false)) = presentationInterfaceState.mode { placeholderText = strings.Common_Search @@ -114,7 +114,7 @@ final class ChatSearchNavigationContentNode: NavigationBarContentNode { self.searchBar.prefixString = nil let placeholderText: String switch self.chatLocation { - case .peer, .replyThread, .feed: + case .peer, .replyThread, .customChatContents: if presentationInterfaceState.historyFilter != nil { placeholderText = self.strings.Common_Search } else if self.chatLocation.peerId == self.context.account.peerId, presentationInterfaceState.hasSearchTags { diff --git a/submodules/TelegramUI/Sources/ChatSearchResultsContollerNode.swift b/submodules/TelegramUI/Sources/ChatSearchResultsContollerNode.swift index 757c4e17b43..d55b4b900ba 100644 --- a/submodules/TelegramUI/Sources/ChatSearchResultsContollerNode.swift +++ b/submodules/TelegramUI/Sources/ChatSearchResultsContollerNode.swift @@ -289,6 +289,7 @@ class ChatSearchResultsControllerNode: ViewControllerTracingNode, UIScrollViewDe }, hideChatFolderUpdates: { }, openStories: { _, _ in }, dismissNotice: { _ in + }, editPeer: { _ in }) interaction.searchTextHighightState = searchQuery self.interaction = interaction diff --git a/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift b/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift index b8cc9b8c209..02d4a717607 100644 --- a/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift +++ b/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift @@ -1560,7 +1560,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch } else { if let user = interfaceState.renderedPeer?.peer as? TelegramUser, user.botInfo != nil { - if let chatHistoryState = interfaceState.chatHistoryState, case .loaded(true) = chatHistoryState { + if let chatHistoryState = interfaceState.chatHistoryState, case .loaded(true, _) = chatHistoryState { displayBotStartButton = true } else if interfaceState.peerIsBlocked { displayBotStartButton = true @@ -1844,37 +1844,57 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch let dismissedButtonMessageUpdated = interfaceState.interfaceState.messageActionsState.dismissedButtonKeyboardMessageId != previousState?.interfaceState.messageActionsState.dismissedButtonKeyboardMessageId let replyMessageUpdated = interfaceState.interfaceState.replyMessageSubject != previousState?.interfaceState.replyMessageSubject - if let peer = interfaceState.renderedPeer?.peer, previousState?.renderedPeer?.peer == nil || !peer.isEqual(previousState!.renderedPeer!.peer!) || previousState?.interfaceState.silentPosting != interfaceState.interfaceState.silentPosting || themeUpdated || !self.initializedPlaceholder || previousState?.keyboardButtonsMessage?.id != interfaceState.keyboardButtonsMessage?.id || previousState?.keyboardButtonsMessage?.visibleReplyMarkupPlaceholder != interfaceState.keyboardButtonsMessage?.visibleReplyMarkupPlaceholder || dismissedButtonMessageUpdated || replyMessageUpdated || (previousState?.interfaceState.editMessage == nil) != (interfaceState.interfaceState.editMessage == nil) || previousState?.forumTopicData != interfaceState.forumTopicData || previousState?.replyMessage?.id != interfaceState.replyMessage?.id { + var peerUpdated = false + if let peer = interfaceState.renderedPeer?.peer, previousState?.renderedPeer?.peer == nil || !peer.isEqual(previousState!.renderedPeer!.peer!) { + peerUpdated = true + } + + if peerUpdated || previousState?.interfaceState.silentPosting != interfaceState.interfaceState.silentPosting || themeUpdated || !self.initializedPlaceholder || previousState?.keyboardButtonsMessage?.id != interfaceState.keyboardButtonsMessage?.id || previousState?.keyboardButtonsMessage?.visibleReplyMarkupPlaceholder != interfaceState.keyboardButtonsMessage?.visibleReplyMarkupPlaceholder || dismissedButtonMessageUpdated || replyMessageUpdated || (previousState?.interfaceState.editMessage == nil) != (interfaceState.interfaceState.editMessage == nil) || previousState?.forumTopicData != interfaceState.forumTopicData || previousState?.replyMessage?.id != interfaceState.replyMessage?.id { self.initializedPlaceholder = true - var placeholder: String + var placeholder: String = "" - if let channel = peer as? TelegramChannel, case .broadcast = channel.info { - if interfaceState.interfaceState.silentPosting { - placeholder = interfaceState.strings.Conversation_InputTextSilentBroadcastPlaceholder - } else { - placeholder = interfaceState.strings.Conversation_InputTextBroadcastPlaceholder - } - } else { - if sendingTextDisabled { - placeholder = interfaceState.strings.Chat_PlaceholderTextNotAllowed + if let peer = interfaceState.renderedPeer?.peer { + if let channel = peer as? TelegramChannel, case .broadcast = channel.info { + if interfaceState.interfaceState.silentPosting { + placeholder = interfaceState.strings.Conversation_InputTextSilentBroadcastPlaceholder + } else { + placeholder = interfaceState.strings.Conversation_InputTextBroadcastPlaceholder + } } else { - if let channel = peer as? TelegramChannel, case .group = channel.info, channel.hasPermission(.canBeAnonymous) { - placeholder = interfaceState.strings.Conversation_InputTextAnonymousPlaceholder - } else if case let .replyThread(replyThreadMessage) = interfaceState.chatLocation, !replyThreadMessage.isForumPost, replyThreadMessage.peerId != self.context?.account.peerId { - if replyThreadMessage.isChannelPost { - placeholder = interfaceState.strings.Conversation_InputTextPlaceholderComment - } else { - placeholder = interfaceState.strings.Conversation_InputTextPlaceholderReply - } - } else if let channel = peer as? TelegramChannel, channel.isForum, let forumTopicData = interfaceState.forumTopicData { - if let replyMessage = interfaceState.replyMessage, let threadInfo = replyMessage.associatedThreadInfo { - placeholder = interfaceState.strings.Chat_InputPlaceholderReplyInTopic(threadInfo.title).string + if sendingTextDisabled { + placeholder = interfaceState.strings.Chat_PlaceholderTextNotAllowed + } else { + if let channel = peer as? TelegramChannel, case .group = channel.info, channel.hasPermission(.canBeAnonymous) { + placeholder = interfaceState.strings.Conversation_InputTextAnonymousPlaceholder + } else if case let .replyThread(replyThreadMessage) = interfaceState.chatLocation, !replyThreadMessage.isForumPost, replyThreadMessage.peerId != self.context?.account.peerId { + if replyThreadMessage.isChannelPost { + placeholder = interfaceState.strings.Conversation_InputTextPlaceholderComment + } else { + placeholder = interfaceState.strings.Conversation_InputTextPlaceholderReply + } + } else if let channel = peer as? TelegramChannel, channel.isForum, let forumTopicData = interfaceState.forumTopicData { + if let replyMessage = interfaceState.replyMessage, let threadInfo = replyMessage.associatedThreadInfo { + placeholder = interfaceState.strings.Chat_InputPlaceholderReplyInTopic(threadInfo.title).string + } else { + placeholder = interfaceState.strings.Chat_InputPlaceholderMessageInTopic(forumTopicData.title).string + } } else { - placeholder = interfaceState.strings.Chat_InputPlaceholderMessageInTopic(forumTopicData.title).string + placeholder = interfaceState.strings.Conversation_InputTextPlaceholder } - } else { - placeholder = interfaceState.strings.Conversation_InputTextPlaceholder + } + } + } + if case let .customChatContents(customChatContents) = interfaceState.subject { + switch customChatContents.kind { + case let .quickReplyMessageInput(_, shortcutType): + switch shortcutType { + case .generic: + placeholder = interfaceState.strings.Chat_Placeholder_QuickReply + case .greeting: + placeholder = interfaceState.strings.Chat_Placeholder_GreetingMessage + case .away: + placeholder = interfaceState.strings.Chat_Placeholder_AwayMessage } } } diff --git a/submodules/TelegramUI/Sources/CommandChatInputContextPanelNode.swift b/submodules/TelegramUI/Sources/CommandChatInputContextPanelNode.swift index 1e5fd7f7f6b..18b7c7bed8d 100644 --- a/submodules/TelegramUI/Sources/CommandChatInputContextPanelNode.swift +++ b/submodules/TelegramUI/Sources/CommandChatInputContextPanelNode.swift @@ -13,34 +13,230 @@ import ChatControllerInteraction import ItemListUI import ChatContextQuery import ChatInputContextPanelNode +import ChatListUI +import ComponentFlow +import ComponentDisplayAdapters -private struct CommandChatInputContextPanelEntryStableId: Hashable { - let command: PeerCommand +private enum CommandChatInputContextPanelEntryStableId: Hashable { + case editShortcuts + case command(PeerCommand) + case shortcut(Int32) } private struct CommandChatInputContextPanelEntry: Comparable, Identifiable { - let index: Int - let command: PeerCommand - let theme: PresentationTheme + struct Command: Equatable { + let command: ChatInputTextCommand + let accountPeer: EnginePeer? + let searchQuery: String? + + static func ==(lhs: Command, rhs: Command) -> Bool { + return lhs.command == rhs.command && lhs.accountPeer == rhs.accountPeer && lhs.searchQuery == rhs.searchQuery + } + } - var stableId: CommandChatInputContextPanelEntryStableId { - return CommandChatInputContextPanelEntryStableId(command: self.command) + enum Content: Equatable { + case editShortcuts + case command(Command) } - func withUpdatedTheme(_ theme: PresentationTheme) -> CommandChatInputContextPanelEntry { - return CommandChatInputContextPanelEntry(index: self.index, command: self.command, theme: theme) + let content: Content + let index: Int + let theme: PresentationTheme + + init(index: Int, content: Content, theme: PresentationTheme) { + self.content = content + self.index = index + self.theme = theme } static func ==(lhs: CommandChatInputContextPanelEntry, rhs: CommandChatInputContextPanelEntry) -> Bool { - return lhs.index == rhs.index && lhs.command == rhs.command && lhs.theme === rhs.theme + return lhs.index == rhs.index && lhs.content == rhs.content && lhs.theme === rhs.theme } static func <(lhs: CommandChatInputContextPanelEntry, rhs: CommandChatInputContextPanelEntry) -> Bool { return lhs.index < rhs.index } - func item(context: AccountContext, presentationData: PresentationData, commandSelected: @escaping (PeerCommand, Bool) -> Void) -> ListViewItem { - return CommandChatInputPanelItem(context: context, presentationData: ItemListPresentationData(presentationData), command: self.command, commandSelected: commandSelected) + var stableId: CommandChatInputContextPanelEntryStableId { + switch self.content { + case .editShortcuts: + return .editShortcuts + case let .command(command): + switch command.command { + case let .command(command): + return .command(command) + case let .shortcut(shortcut): + if let shortcutId = shortcut.id { + return .shortcut(shortcutId) + } else { + return .shortcut(0) + } + } + } + } + + func withUpdatedTheme(_ theme: PresentationTheme) -> CommandChatInputContextPanelEntry { + return CommandChatInputContextPanelEntry(index: self.index, content: self.content, theme: theme) + } + + func item(context: AccountContext, presentationData: PresentationData, commandSelected: @escaping (ChatInputTextCommand, Bool) -> Void, openEditShortcuts: @escaping () -> Void) -> ListViewItem { + switch self.content { + case .editShortcuts: + return VerticalListContextResultsChatInputPanelButtonItem(theme: presentationData.theme, style: .round, title: presentationData.strings.Chat_CommandList_EditQuickReplies, pressed: { + openEditShortcuts() + }) + case let .command(command): + switch command.command { + case let .command(command): + return CommandChatInputPanelItem(context: context, presentationData: ItemListPresentationData(presentationData), command: command, commandSelected: { value, sendImmediately in + commandSelected(.command(value), sendImmediately) + }) + case let .shortcut(shortcut): + let chatListNodeInteraction = ChatListNodeInteraction( + context: context, + animationCache: context.animationCache, + animationRenderer: context.animationRenderer, + activateSearch: { + }, + peerSelected: { _, _, _, _ in + commandSelected(.shortcut(shortcut), true) + }, + disabledPeerSelected: { _, _, _ in + }, + togglePeerSelected: { _, _ in + }, + togglePeersSelection: { _, _ in + }, + additionalCategorySelected: { _ in + }, + messageSelected: { _, _, _, _ in + commandSelected(.shortcut(shortcut), true) + }, + groupSelected: { _ in + }, + addContact: { _ in + }, + setPeerIdWithRevealedOptions: { _, _ in + }, + setItemPinned: { _, _ in + }, + setPeerMuted: { _, _ in + }, + setPeerThreadMuted: { _, _, _ in + }, + deletePeer: { _, _ in + }, + deletePeerThread: { _, _ in + }, + setPeerThreadStopped: { _, _, _ in + }, + setPeerThreadPinned: { _, _, _ in + }, + setPeerThreadHidden: { _, _, _ in + }, + updatePeerGrouping: { _, _ in + }, + togglePeerMarkedUnread: { _, _ in + }, + toggleArchivedFolderHiddenByDefault: { + }, + toggleThreadsSelection: { _, _ in + }, + hidePsa: { _ in + }, + activateChatPreview: { _, _, _, _, _ in + }, + present: { _ in + }, + openForumThread: { _, _ in + }, + openStorageManagement: { + }, + openPasswordSetup: { + }, + openPremiumIntro: { + }, + openPremiumGift: { + }, + openActiveSessions: { + }, + performActiveSessionAction: { _, _ in + }, + openChatFolderUpdates: { + }, + hideChatFolderUpdates: { + }, + openStories: { _, _ in + }, + dismissNotice: { _ in + }, + editPeer: { _ in + } + ) + + let chatListPresentationData = ChatListPresentationData( + theme: presentationData.theme, + fontSize: presentationData.listsFontSize, + strings: presentationData.strings, + dateTimeFormat: presentationData.dateTimeFormat, + nameSortOrder: presentationData.nameSortOrder, + nameDisplayOrder: presentationData.nameDisplayOrder, + disableAnimations: false + ) + + let renderedPeer: EngineRenderedPeer + if let accountPeer = command.accountPeer { + renderedPeer = EngineRenderedPeer(peer: accountPeer) + } else { + renderedPeer = EngineRenderedPeer(peerId: context.account.peerId, peers: [:], associatedMedia: [:]) + } + + return ChatListItem( + presentationData: chatListPresentationData, + context: context, + chatListLocation: .chatList(groupId: .root), + filterData: nil, + index: EngineChatList.Item.Index.chatList(ChatListIndex(pinningIndex: nil, messageIndex: MessageIndex(id: MessageId(peerId: context.account.peerId, namespace: 0, id: 0), timestamp: 0))), + content: .peer(ChatListItemContent.PeerData( + messages: [shortcut.topMessage], + peer: renderedPeer, + threadInfo: nil, + combinedReadState: nil, + isRemovedFromTotalUnreadCount: false, + presence: nil, + hasUnseenMentions: false, + hasUnseenReactions: false, + draftState: nil, + mediaDraftContentType: nil, + inputActivities: nil, + promoInfo: nil, + ignoreUnreadBadge: false, + displayAsMessage: false, + hasFailedMessages: false, + forumTopicData: nil, + topForumTopicItems: [], + autoremoveTimeout: nil, + storyState: nil, + requiresPremiumForMessaging: false, + displayAsTopicList: false, + tags: [], + customMessageListData: ChatListItemContent.CustomMessageListData( + commandPrefix: "/\(shortcut.shortcut)", + searchQuery: command.searchQuery.flatMap { "/\($0)"}, + messageCount: shortcut.totalCount, + hideSeparator: false + ) + )), + editing: false, + hasActiveRevealControls: false, + selected: false, + header: nil, + enableContextActions: false, + hiddenOffset: false, + interaction: chatListNodeInteraction + ) + } + } } } @@ -48,21 +244,33 @@ private struct CommandChatInputContextPanelTransition { let deletions: [ListViewDeleteItem] let insertions: [ListViewInsertItem] let updates: [ListViewUpdateItem] + let hasShortcuts: Bool + let itemCountChanged: Bool } -private func preparedTransition(from fromEntries: [CommandChatInputContextPanelEntry], to toEntries: [CommandChatInputContextPanelEntry], context: AccountContext, presentationData: PresentationData, commandSelected: @escaping (PeerCommand, Bool) -> Void) -> CommandChatInputContextPanelTransition { +private func preparedTransition(from fromEntries: [CommandChatInputContextPanelEntry], to toEntries: [CommandChatInputContextPanelEntry], context: AccountContext, presentationData: PresentationData, commandSelected: @escaping (ChatInputTextCommand, Bool) -> Void, openEditShortcuts: @escaping () -> Void) -> CommandChatInputContextPanelTransition { let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries) let deletions = deleteIndices.map { ListViewDeleteItem(index: $0, directionHint: nil) } - let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, presentationData: presentationData, commandSelected: commandSelected), directionHint: nil) } - let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, presentationData: presentationData, commandSelected: commandSelected), directionHint: nil) } + let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, presentationData: presentationData, commandSelected: commandSelected, openEditShortcuts: openEditShortcuts), directionHint: nil) } + let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, presentationData: presentationData, commandSelected: commandSelected, openEditShortcuts: openEditShortcuts), directionHint: nil) } - return CommandChatInputContextPanelTransition(deletions: deletions, insertions: insertions, updates: updates) + let itemCountChanged = fromEntries.count != toEntries.count + + return CommandChatInputContextPanelTransition(deletions: deletions, insertions: insertions, updates: updates, hasShortcuts: toEntries.contains(where: { entry in + if case .editShortcuts = entry.content { + return true + } + return false + }), itemCountChanged: itemCountChanged) } final class CommandChatInputContextPanelNode: ChatInputContextPanelNode { private let listView: ListView + private let listBackgroundView: UIView private var currentEntries: [CommandChatInputContextPanelEntry]? + private var contentOffsetChangeTransition: Transition? + private var isAnimatingOut: Bool = false private var enqueuedTransitions: [(CommandChatInputContextPanelTransition, Bool)] = [] private var validLayout: (CGSize, CGFloat, CGFloat, CGFloat)? @@ -78,20 +286,54 @@ final class CommandChatInputContextPanelNode: ChatInputContextPanelNode { return strings.VoiceOver_ScrollStatus(row, count).string } + self.listBackgroundView = UIView() + self.listBackgroundView.backgroundColor = theme.list.plainBackgroundColor + self.listBackgroundView.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner] + self.listBackgroundView.layer.cornerRadius = 10.0 + super.init(context: context, theme: theme, strings: strings, fontSize: fontSize, chatPresentationContext: chatPresentationContext) self.isOpaque = false self.clipsToBounds = true + self.view.addSubview(self.listBackgroundView) self.addSubnode(self.listView) + + self.listView.visibleContentOffsetChanged = { [weak self] offset in + guard let self else { + return + } + + if self.isAnimatingOut { + return + } + + var topItemOffset: CGFloat = self.listView.bounds.height + var isFirst = true + self.listView.forEachItemNode { itemNode in + if isFirst { + isFirst = false + topItemOffset = itemNode.frame.minY + } + } + + let transition: Transition = self.contentOffsetChangeTransition ?? .immediate + transition.setFrame(view: self.listBackgroundView, frame: CGRect(origin: CGPoint(x: 0.0, y: topItemOffset), size: CGSize(width: self.listView.bounds.width, height: self.listView.bounds.height + 1000.0))) + } } - func updateResults(_ results: [PeerCommand]) { + func updateResults(_ results: [ChatInputTextCommand], accountPeer: EnginePeer?, hasShortcuts: Bool, query: String?) { var entries: [CommandChatInputContextPanelEntry] = [] var index = 0 var stableIds = Set() + if hasShortcuts { + let entry = CommandChatInputContextPanelEntry(index: index, content: .editShortcuts, theme: self.theme) + stableIds.insert(entry.stableId) + entries.append(entry) + index += 1 + } for command in results { - let entry = CommandChatInputContextPanelEntry(index: index, command: command, theme: self.theme) + let entry = CommandChatInputContextPanelEntry(index: index, content: .command(CommandChatInputContextPanelEntry.Command(command: command, accountPeer: accountPeer, searchQuery: query)), theme: self.theme) if stableIds.contains(entry.stableId) { continue } @@ -106,7 +348,11 @@ final class CommandChatInputContextPanelNode: ChatInputContextPanelNode { let firstTime = self.currentEntries == nil let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } let transition = preparedTransition(from: from ?? [], to: to, context: self.context, presentationData: presentationData, commandSelected: { [weak self] command, sendImmediately in - if let strongSelf = self, let interfaceInteraction = strongSelf.interfaceInteraction { + guard let strongSelf = self, let interfaceInteraction = strongSelf.interfaceInteraction else { + return + } + switch command { + case let .command(command): if sendImmediately { interfaceInteraction.sendBotCommand(command.peer, "/" + command.command.text) } else { @@ -132,7 +378,16 @@ final class CommandChatInputContextPanelNode: ChatInputContextPanelNode { return (textInputState, inputMode) } } + case let .shortcut(shortcut): + if let shortcutId = shortcut.id { + interfaceInteraction.sendShortcut(shortcutId) + } + } + }, openEditShortcuts: { [weak self] in + guard let self, let interfaceInteraction = self.interfaceInteraction else { + return } + interfaceInteraction.openEditShortcuts() }) self.currentEntries = to self.enqueueTransition(transition, firstTime: firstTime) @@ -153,12 +408,20 @@ final class CommandChatInputContextPanelNode: ChatInputContextPanelNode { self.enqueuedTransitions.remove(at: 0) var options = ListViewDeleteAndInsertOptions() + options.insert(.Synchronous) + options.insert(.LowLatency) + options.insert(.PreferSynchronousResourceLoading) if firstTime { - //options.insert(.Synchronous) - //options.insert(.LowLatency) + self.contentOffsetChangeTransition = .immediate + + self.listBackgroundView.frame = CGRect(origin: CGPoint(x: 0.0, y: self.listView.bounds.height), size: CGSize(width: self.listView.bounds.width, height: self.listView.bounds.height + 1000.0)) } else { - options.insert(.AnimateTopItemPosition) - options.insert(.AnimateCrossfade) + if transition.itemCountChanged { + options.insert(.AnimateTopItemPosition) + options.insert(.AnimateCrossfade) + } + + self.contentOffsetChangeTransition = .spring(duration: 0.4) } var insets = UIEdgeInsets() @@ -178,19 +441,45 @@ final class CommandChatInputContextPanelNode: ChatInputContextPanelNode { } if let topItemOffset = topItemOffset { - let position = strongSelf.listView.layer.position - strongSelf.listView.position = CGPoint(x: position.x, y: position.y + (strongSelf.listView.bounds.size.height - topItemOffset)) - ContainedViewLayoutTransition.animated(duration: 0.3, curve: .spring).animateView { - strongSelf.listView.position = position - } + let transition: ContainedViewLayoutTransition = .animated(duration: 0.4, curve: .spring) + + transition.animatePositionAdditive(layer: strongSelf.listView.layer, offset: CGPoint(x: 0.0, y: strongSelf.listView.bounds.size.height - topItemOffset)) + transition.animatePositionAdditive(layer: strongSelf.listBackgroundView.layer, offset: CGPoint(x: 0.0, y: strongSelf.listView.bounds.size.height - topItemOffset)) } } }) + + self.contentOffsetChangeTransition = nil } } private func topInsetForLayout(size: CGSize) -> CGFloat { - let minimumItemHeights: CGFloat = floor(MentionChatInputPanelItemNode.itemHeight * 3.5) + var minimumItemHeights: CGFloat = 0.0 + if let currentEntries = self.currentEntries, !currentEntries.isEmpty { + let indexLimit = min(4, currentEntries.count - 1) + for i in 0 ... indexLimit { + var itemHeight: CGFloat + switch currentEntries[i].content { + case .editShortcuts: + itemHeight = VerticalListContextResultsChatInputPanelButtonItemNode.itemHeight(style: .round) + case let .command(command): + switch command.command { + case .command: + itemHeight = MentionChatInputPanelItemNode.itemHeight + case .shortcut: + itemHeight = 58.0 + } + } + if indexLimit >= 4 && i == indexLimit { + minimumItemHeights += floor(itemHeight * 0.5) + } else { + minimumItemHeights += itemHeight + } + } + } else { + minimumItemHeights = floor(MentionChatInputPanelItemNode.itemHeight * 3.5) + } + return max(size.height - minimumItemHeights, 0.0) } @@ -208,8 +497,12 @@ final class CommandChatInputContextPanelNode: ChatInputContextPanelNode { let (duration, curve) = listViewAnimationDurationAndCurve(transition: transition) let updateSizeAndInsets = ListViewUpdateSizeAndInsets(size: size, insets: insets, duration: duration, curve: curve) + self.contentOffsetChangeTransition = Transition(transition) + self.listView.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: nil, updateSizeAndInsets: updateSizeAndInsets, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) + self.contentOffsetChangeTransition = nil + if !hadValidLayout { while !self.enqueuedTransitions.isEmpty { self.dequeueTransition() @@ -226,6 +519,8 @@ final class CommandChatInputContextPanelNode: ChatInputContextPanelNode { } override func animateOut(completion: @escaping () -> Void) { + self.isAnimatingOut = true + var topItemOffset: CGFloat? self.listView.forEachItemNode { itemNode in if topItemOffset == nil { @@ -235,9 +530,12 @@ final class CommandChatInputContextPanelNode: ChatInputContextPanelNode { if let topItemOffset = topItemOffset { let position = self.listView.layer.position - self.listView.layer.animatePosition(from: position, to: CGPoint(x: position.x, y: position.y + (self.listView.bounds.size.height - topItemOffset)), duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, completion: { _ in + let offset = (self.listView.bounds.size.height - topItemOffset) + self.listView.layer.animatePosition(from: position, to: CGPoint(x: position.x, y: position.y + offset), duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, completion: { _ in completion() }) + self.listBackgroundView.layer.animatePosition(from: self.listBackgroundView.layer.position, to: CGPoint(x: self.listBackgroundView.layer.position.x, y: self.listBackgroundView.layer.position.y + offset), duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, completion: { _ in + }) } else { completion() } diff --git a/submodules/TelegramUI/Sources/ContactMultiselectionControllerNode.swift b/submodules/TelegramUI/Sources/ContactMultiselectionControllerNode.swift index a26be36efb7..177d376c497 100644 --- a/submodules/TelegramUI/Sources/ContactMultiselectionControllerNode.swift +++ b/submodules/TelegramUI/Sources/ContactMultiselectionControllerNode.swift @@ -127,8 +127,23 @@ final class ContactMultiselectionControllerNode: ASDisplayNode { let additionalCategories = chatSelection.additionalCategories let chatListFilters = chatSelection.chatListFilters + var chatListFilter: ChatListFilter? + if chatSelection.onlyUsers { + chatListFilter = .filter(id: Int32.max, title: "", emoticon: nil, data: ChatListFilterData( + isShared: false, + hasSharedLinks: false, + categories: [.contacts, .nonContacts], + excludeMuted: false, + excludeRead: false, + excludeArchived: false, + includePeers: ChatListFilterIncludePeers(), + excludePeers: [], + color: nil + )) + } + placeholder = placeholderValue - let chatListNode = ChatListNode(context: context, location: .chatList(groupId: .root), previewing: false, fillPreloadItems: false, mode: .peers(filter: [.excludeSecretChats], isSelecting: true, additionalCategories: additionalCategories?.categories ?? [], chatListFilters: chatListFilters, displayAutoremoveTimeout: chatSelection.displayAutoremoveTimeout, displayPresence: chatSelection.displayPresence), isPeerEnabled: isPeerEnabled, theme: self.presentationData.theme, fontSize: self.presentationData.listsFontSize, strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat, nameSortOrder: self.presentationData.nameSortOrder, nameDisplayOrder: self.presentationData.nameDisplayOrder, animationCache: self.animationCache, animationRenderer: self.animationRenderer, disableAnimations: true, isInlineMode: false, autoSetReady: true, isMainTab: false) + let chatListNode = ChatListNode(context: context, location: .chatList(groupId: .root), chatListFilter: chatListFilter, previewing: false, fillPreloadItems: false, mode: .peers(filter: [.excludeSecretChats], isSelecting: true, additionalCategories: additionalCategories?.categories ?? [], chatListFilters: chatListFilters, displayAutoremoveTimeout: chatSelection.displayAutoremoveTimeout, displayPresence: chatSelection.displayPresence), isPeerEnabled: isPeerEnabled, theme: self.presentationData.theme, fontSize: self.presentationData.listsFontSize, strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat, nameSortOrder: self.presentationData.nameSortOrder, nameDisplayOrder: self.presentationData.nameDisplayOrder, animationCache: self.animationCache, animationRenderer: self.animationRenderer, disableAnimations: true, isInlineMode: false, autoSetReady: true, isMainTab: false) chatListNode.passthroughPeerSelection = true chatListNode.disabledPeerSelected = { peer, _, reason in attemptDisabledItemSelection?(peer, reason) @@ -237,6 +252,8 @@ final class ContactMultiselectionControllerNode: ASDisplayNode { var searchGroups = false var searchChannels = false var globalSearch = false + var displaySavedMessages = true + var filters = filters switch mode { case .groupCreation, .channelCreation: globalSearch = true @@ -245,15 +262,31 @@ final class ContactMultiselectionControllerNode: ASDisplayNode { searchGroups = searchGroupsValue searchChannels = searchChannelsValue globalSearch = true - case .chatSelection: - searchChatList = true - searchGroups = true - searchChannels = true + case let .chatSelection(chatSelection): + if chatSelection.onlyUsers { + searchChatList = true + searchGroups = false + searchChannels = false + displaySavedMessages = false + filters.append(.excludeSelf) + } else { + searchChatList = true + searchGroups = true + searchChannels = true + } globalSearch = false case .premiumGifting, .requestedUsersSelection: searchChatList = true } - let searchResultsNode = ContactListNode(context: context, presentation: .single(.search(signal: searchText.get(), searchChatList: searchChatList, searchDeviceContacts: false, searchGroups: searchGroups, searchChannels: searchChannels, globalSearch: globalSearch)), filters: filters, onlyWriteable: strongSelf.onlyWriteable, isPeerEnabled: strongSelf.isPeerEnabled, selectionState: selectionState, isSearch: true) + let searchResultsNode = ContactListNode(context: context, presentation: .single(.search(ContactListPresentation.Search( + signal: searchText.get(), + searchChatList: searchChatList, + searchDeviceContacts: false, + searchGroups: searchGroups, + searchChannels: searchChannels, + globalSearch: globalSearch, + displaySavedMessages: displaySavedMessages + ))), filters: filters, onlyWriteable: strongSelf.onlyWriteable, isPeerEnabled: strongSelf.isPeerEnabled, selectionState: selectionState, isSearch: true) searchResultsNode.openPeer = { peer, _ in self?.tokenListNode.setText("") self?.openPeer?(peer) diff --git a/submodules/TelegramUI/Sources/EditAccessoryPanelNode.swift b/submodules/TelegramUI/Sources/EditAccessoryPanelNode.swift index c6e029ff09b..b844464a372 100644 --- a/submodules/TelegramUI/Sources/EditAccessoryPanelNode.swift +++ b/submodules/TelegramUI/Sources/EditAccessoryPanelNode.swift @@ -284,7 +284,9 @@ final class EditAccessoryPanelNode: AccessoryPanelNode { } let titleString: String - if canEditMedia { + if let message, message.id.namespace == Namespaces.Message.QuickReplyCloud { + titleString = self.strings.Conversation_EditingQuickReplyPanelTitle + } else if canEditMedia { titleString = isPhoto ? self.strings.Conversation_EditingPhotoPanelTitle : self.strings.Conversation_EditingCaptionPanelTitle } else { titleString = self.strings.Conversation_EditingMessagePanelTitle diff --git a/submodules/TelegramUI/Sources/Nicegram/NGDeeplinkHandler.swift b/submodules/TelegramUI/Sources/Nicegram/NGDeeplinkHandler.swift index 81c909ed355..5381d7975cf 100644 --- a/submodules/TelegramUI/Sources/Nicegram/NGDeeplinkHandler.swift +++ b/submodules/TelegramUI/Sources/Nicegram/NGDeeplinkHandler.swift @@ -1,6 +1,7 @@ import Foundation import AccountContext import Display +import FeatAvatarGeneratorUI import FeatCardUI import FeatImagesHubUI import FeatPremiumUI @@ -63,6 +64,25 @@ class NGDeeplinkHandler { } else { return false } + case "avatarGenerator": + if #available(iOS 15.0, *) { + Task { @MainActor in + guard let topController = UIApplication.topViewController else { + return + } + AvatarGeneratorUIHelper().navigateToGenerationFlow( + from: topController + ) + } + } + return true + case "avatarMyGenerations": + if #available(iOS 15.0, *) { + Task { @MainActor in + AvatarGeneratorUIHelper().navigateToGenerator() + } + } + return true case "generateImage": return handleGenerateImage(url: url) case "nicegramPremium": @@ -100,7 +120,8 @@ class NGDeeplinkHandler { } return true default: - return false + showUpdateAppAlert() + return true } } } @@ -222,6 +243,34 @@ private extension NGDeeplinkHandler { } return false } + + func showUpdateAppAlert() { + let alert = UIAlertController( + title: "Update the app", + message: "Please update the app to use the newest features!", + preferredStyle: .alert + ) + + alert.addAction( + UIAlertAction( + title: "Close", + style: .cancel + ) + ) + + alert.addAction( + UIAlertAction( + title: "Update", + style: .default, + handler: { _ in + let urlOpener = CoreContainer.shared.urlOpener() + urlOpener.open(.appStoreAppUrl) + } + ) + ) + + UIApplication.topViewController?.present(alert, animated: true) + } } // MARK: - Helpers diff --git a/submodules/TelegramUI/Sources/OpenResolvedUrl.swift b/submodules/TelegramUI/Sources/OpenResolvedUrl.swift index 2a5d09ecf77..c820caf77ab 100644 --- a/submodules/TelegramUI/Sources/OpenResolvedUrl.swift +++ b/submodules/TelegramUI/Sources/OpenResolvedUrl.swift @@ -302,7 +302,7 @@ func openResolvedUrlImpl( }) dismissInput() case let .share(url, text, to): - let continueWithPeer: (PeerId) -> Void = { peerId in + let continueWithPeer: (PeerId, Int64?) -> Void = { peerId, threadId in let textInputState: ChatTextInputState? if let text = text, !text.isEmpty { if let url = url, !url.isEmpty { @@ -320,15 +320,42 @@ func openResolvedUrlImpl( textInputState = nil } + let updateControllers = { [weak navigationController] in + guard let navigationController else { + return + } + let chatController: Signal + if let threadId { + chatController = chatControllerForForumThreadImpl(context: context, peerId: peerId, threadId: threadId) + } else { + chatController = .single(ChatControllerImpl(context: context, chatLocation: .peer(id: peerId))) + } + + let _ = (chatController + |> deliverOnMainQueue).start(next: { [weak navigationController] chatController in + guard let navigationController else { + return + } + var controllers = navigationController.viewControllers.filter { controller in + if controller is PeerSelectionController { + return false + } + return true + } + controllers.append(chatController) + navigationController.setViewControllers(controllers, animated: true) + }) + } + if let textInputState = textInputState { - let _ = (ChatInterfaceState.update(engine: context.engine, peerId: peerId, threadId: nil, { currentState in + let _ = (ChatInterfaceState.update(engine: context.engine, peerId: peerId, threadId: threadId, { currentState in return currentState.withUpdatedComposeInputState(textInputState) }) |> deliverOnMainQueue).startStandalone(completed: { - navigationController?.pushViewController(ChatControllerImpl(context: context, chatLocation: .peer(id: peerId))) + updateControllers() }) } else { - navigationController?.pushViewController(ChatControllerImpl(context: context, chatLocation: .peer(id: peerId))) + updateControllers() } } @@ -344,7 +371,7 @@ func openResolvedUrlImpl( |> deliverOnMainQueue).startStandalone(next: { peer in if let peer = peer { context.sharedContext.applicationBindings.dismissNativeController() - continueWithPeer(peer.id) + continueWithPeer(peer.id, nil) } }) } else { @@ -352,7 +379,7 @@ func openResolvedUrlImpl( |> deliverOnMainQueue).startStandalone(next: { peer in if let peer = peer { context.sharedContext.applicationBindings.dismissNativeController() - continueWithPeer(peer.id) + continueWithPeer(peer.id, nil) } }) /*let query = to.trimmingCharacters(in: CharacterSet(charactersIn: "0123456789").inverted) @@ -377,13 +404,8 @@ func openResolvedUrlImpl( context.sharedContext.applicationBindings.dismissNativeController() } else { let controller = context.sharedContext.makePeerSelectionController(PeerSelectionControllerParams(context: context, filter: [.onlyWriteable, .excludeDisabled], selectForumThreads: true)) - controller.peerSelected = { [weak controller] peer, _ in - let peerId = peer.id - - if let strongController = controller { - strongController.dismiss() - continueWithPeer(peerId) - } + controller.peerSelected = { peer, threadId in + continueWithPeer(peer.id, threadId) } context.sharedContext.applicationBindings.dismissNativeController() navigationController?.pushViewController(controller) diff --git a/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift b/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift index ba27451adaf..e7be8e9ccdb 100644 --- a/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift +++ b/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift @@ -173,6 +173,7 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, UIGestu }, openPremiumStatusInfo: { _, _, _, _ in }, openRecommendedChannelContextMenu: { _, _, _ in }, openGroupBoostInfo: { _, _ in + }, openStickerEditor: { }, requestMessageUpdate: { _, _ in }, cancelInteractiveKeyboardGestures: { }, dismissTextInput: { diff --git a/submodules/TelegramUI/Sources/PeerMessagesMediaPlaylist.swift b/submodules/TelegramUI/Sources/PeerMessagesMediaPlaylist.swift index f0b44c24c07..6830e72ed31 100644 --- a/submodules/TelegramUI/Sources/PeerMessagesMediaPlaylist.swift +++ b/submodules/TelegramUI/Sources/PeerMessagesMediaPlaylist.swift @@ -549,8 +549,10 @@ final class PeerMessagesMediaPlaylist: SharedMediaPlaylist { let namespaces: MessageIdNamespaces if Namespaces.Message.allScheduled.contains(anchor.id.namespace) { namespaces = .just(Namespaces.Message.allScheduled) + } else if Namespaces.Message.allQuickReply.contains(anchor.id.namespace) { + namespaces = .just(Namespaces.Message.allQuickReply) } else { - namespaces = .not(Namespaces.Message.allScheduled) + namespaces = .not(Namespaces.Message.allNonRegular) } switch anchor { diff --git a/submodules/TelegramUI/Sources/PrefetchManager.swift b/submodules/TelegramUI/Sources/PrefetchManager.swift index 0fd47eb4482..43717a36b4e 100644 --- a/submodules/TelegramUI/Sources/PrefetchManager.swift +++ b/submodules/TelegramUI/Sources/PrefetchManager.swift @@ -174,17 +174,17 @@ private final class PrefetchManagerInnerImpl { if case .full = automaticDownload { if let image = media as? TelegramMediaImage { - context.fetchDisposable.set(messageMediaImageInteractiveFetched(fetchManager: self.fetchManager, messageId: mediaItem.media.index.id, messageReference: MessageReference(peer: mediaItem.media.peer, author: nil, id: mediaItem.media.index.id, timestamp: mediaItem.media.index.timestamp, incoming: true, secret: false), image: image, resource: resource._asResource(), userInitiated: false, priority: priority, storeToDownloadsPeerId: nil).startStrict()) + context.fetchDisposable.set(messageMediaImageInteractiveFetched(fetchManager: self.fetchManager, messageId: mediaItem.media.index.id, messageReference: MessageReference(peer: mediaItem.media.peer, author: nil, id: mediaItem.media.index.id, timestamp: mediaItem.media.index.timestamp, incoming: true, secret: false, threadId: nil), image: image, resource: resource._asResource(), userInitiated: false, priority: priority, storeToDownloadsPeerId: nil).startStrict()) } else if let _ = media as? TelegramMediaWebFile { //strongSelf.fetchDisposable.set(chatMessageWebFileInteractiveFetched(account: context.account, image: image).startStrict()) } else if let file = media as? TelegramMediaFile { // MARK: Nicegram downloading feature, accountContext added - let fetchSignal = messageMediaFileInteractiveFetched(fetchManager: self.fetchManager, messageId: mediaItem.media.index.id, messageReference: MessageReference(peer: mediaItem.media.peer, author: nil, id: mediaItem.media.index.id, timestamp: mediaItem.media.index.timestamp, incoming: true, secret: false), file: file, userInitiated: false, priority: priority, accountContext: nil) + let fetchSignal = messageMediaFileInteractiveFetched(fetchManager: self.fetchManager, messageId: mediaItem.media.index.id, messageReference: MessageReference(peer: mediaItem.media.peer, author: nil, id: mediaItem.media.index.id, timestamp: mediaItem.media.index.timestamp, incoming: true, secret: false, threadId: nil), file: file, userInitiated: false, priority: priority, accountContext: nil) context.fetchDisposable.set(fetchSignal.startStrict()) } } else if case .prefetch = automaticDownload, mediaItem.media.peer.id.namespace != Namespaces.Peer.SecretChat { if let file = media as? TelegramMediaFile, let _ = file.size { - context.fetchDisposable.set(preloadVideoResource(postbox: self.account.postbox, userLocation: .peer(mediaItem.media.index.id.peerId), userContentType: MediaResourceUserContentType(file: file), resourceReference: FileMediaReference.message(message: MessageReference(peer: mediaItem.media.peer, author: nil, id: mediaItem.media.index.id, timestamp: mediaItem.media.index.timestamp, incoming: true, secret: false), media: file).resourceReference(file.resource), duration: 4.0).startStrict()) + context.fetchDisposable.set(preloadVideoResource(postbox: self.account.postbox, userLocation: .peer(mediaItem.media.index.id.peerId), userContentType: MediaResourceUserContentType(file: file), resourceReference: FileMediaReference.message(message: MessageReference(peer: mediaItem.media.peer, author: nil, id: mediaItem.media.index.id, timestamp: mediaItem.media.index.timestamp, incoming: true, secret: false, threadId: nil), media: file).resourceReference(file.resource), duration: 4.0).startStrict()) } } } diff --git a/submodules/TelegramUI/Sources/SharedAccountContext.swift b/submodules/TelegramUI/Sources/SharedAccountContext.swift index f84bbe96159..b761456f892 100644 --- a/submodules/TelegramUI/Sources/SharedAccountContext.swift +++ b/submodules/TelegramUI/Sources/SharedAccountContext.swift @@ -52,8 +52,10 @@ import PeerInfoScreen import ChatQrCodeScreen import UndoUI import ChatMessageNotificationItem -import BusinessSetupScreen import ChatbotSetupScreen +import BusinessLocationSetupScreen +import BusinessHoursSetupScreen +import AutomaticBusinessMessageSetupScreen import NGCore import NGData @@ -1885,6 +1887,7 @@ public final class SharedAccountContextImpl: SharedAccountContext { }, openPremiumStatusInfo: { _, _, _, _ in }, openRecommendedChannelContextMenu: { _, _, _ in }, openGroupBoostInfo: { _, _ in + }, openStickerEditor: { }, requestMessageUpdate: { _, _ in }, cancelInteractiveKeyboardGestures: { }, dismissTextInput: { @@ -2013,12 +2016,44 @@ public final class SharedAccountContextImpl: SharedAccountContext { return archiveSettingsController(context: context) } + public func makeFilterSettingsController(context: AccountContext, modal: Bool, scrollToTags: Bool, dismissed: (() -> Void)?) -> ViewController { + return chatListFilterPresetListController(context: context, mode: modal ? .modal : .default, scrollToTags: scrollToTags, dismissed: dismissed) + } + public func makeBusinessSetupScreen(context: AccountContext) -> ViewController { - return BusinessSetupScreen(context: context) + return PremiumIntroScreen(context: context, mode: .business, source: .settings, modal: false, forceDark: false) + } + + public func makeChatbotSetupScreen(context: AccountContext, initialData: ChatbotSetupScreenInitialData) -> ViewController { + return ChatbotSetupScreen(context: context, initialData: initialData as! ChatbotSetupScreen.InitialData) + } + + public func makeChatbotSetupScreenInitialData(context: AccountContext) -> Signal { + return ChatbotSetupScreen.initialData(context: context) + } + + public func makeBusinessLocationSetupScreen(context: AccountContext, initialValue: TelegramBusinessLocation?, completion: @escaping (TelegramBusinessLocation?) -> Void) -> ViewController { + return BusinessLocationSetupScreen(context: context, initialValue: initialValue, completion: completion) + } + + public func makeBusinessHoursSetupScreen(context: AccountContext, initialValue: TelegramBusinessHours?, completion: @escaping (TelegramBusinessHours?) -> Void) -> ViewController { + return BusinessHoursSetupScreen(context: context, initialValue: initialValue, completion: completion) + } + + public func makeAutomaticBusinessMessageSetupScreen(context: AccountContext, initialData: AutomaticBusinessMessageSetupScreenInitialData, isAwayMode: Bool) -> ViewController { + return AutomaticBusinessMessageSetupScreen(context: context, initialData: initialData as! AutomaticBusinessMessageSetupScreen.InitialData, mode: isAwayMode ? .away : .greeting) + } + + public func makeAutomaticBusinessMessageSetupScreenInitialData(context: AccountContext) -> Signal { + return AutomaticBusinessMessageSetupScreen.initialData(context: context) } - public func makeChatbotSetupScreen(context: AccountContext) -> ViewController { - return ChatbotSetupScreen(context: context) + public func makeQuickReplySetupScreen(context: AccountContext, initialData: QuickReplySetupScreenInitialData) -> ViewController { + return QuickReplySetupScreen(context: context, initialData: initialData as! QuickReplySetupScreen.InitialData, mode: .manage) + } + + public func makeQuickReplySetupScreenInitialData(context: AccountContext) -> Signal { + return QuickReplySetupScreen.initialData(context: context) } public func makePremiumIntroController(context: AccountContext, source: PremiumIntroSource, forceDark: Bool, dismissed: (() -> Void)?) -> ViewController { @@ -2098,8 +2133,10 @@ public final class SharedAccountContextImpl: SharedAccountContext { mappedSource = .readTime case .messageTags: mappedSource = .messageTags + case .folderTags: + mappedSource = .folderTags } - let controller = PremiumIntroScreen(context: context, modal: modal, source: mappedSource, forceDark: forceDark) + let controller = PremiumIntroScreen(context: context, source: mappedSource, modal: modal, forceDark: forceDark) controller.wasDismissed = dismissed return controller } @@ -2147,6 +2184,10 @@ public final class SharedAccountContextImpl: SharedAccountContext { mappedSubject = .lastSeen case .messagePrivacy: mappedSubject = .messagePrivacy + case .folderTags: + mappedSubject = .folderTags + default: + mappedSubject = .doubleLimits } return PremiumDemoScreen(context: context, subject: mappedSubject, action: action) } diff --git a/submodules/TelegramUI/Sources/TelegramRootController.swift b/submodules/TelegramUI/Sources/TelegramRootController.swift index d0f14394d02..faf9ede9b22 100644 --- a/submodules/TelegramUI/Sources/TelegramRootController.swift +++ b/submodules/TelegramUI/Sources/TelegramRootController.swift @@ -42,6 +42,8 @@ import ImageCompression import TextFormat import MediaEditor import PeerInfoScreen +import PeerInfoStoryGridScreen +import ShareWithPeersScreen private class DetailsChatPlaceholderNode: ASDisplayNode, NavigationDetailsPlaceholderNode { private var presentationData: PresentationData @@ -508,6 +510,7 @@ public final class TelegramRootController: NavigationController, TelegramRootCon let controller = MediaEditorScreen( context: context, + mode: .storyEditor, subject: subject, customTarget: customTarget, transitionIn: transitionIn, @@ -663,6 +666,19 @@ public final class TelegramRootController: NavigationController, TelegramRootCon viewControllers.removeSubrange(range) self.setViewControllers(viewControllers, animated: false) } + } else if self.viewControllers.contains(where: { $0 is PeerInfoStoryGridScreen }) { + var viewControllers: [UIViewController] = [] + for i in (0 ..< self.viewControllers.count) { + let controller = self.viewControllers[i] + if i == 0 { + viewControllers.append(controller) + } else if controller is MediaEditorScreen { + viewControllers.append(controller) + } else if controller is ShareWithPeersScreen { + viewControllers.append(controller) + } + } + self.setViewControllers(viewControllers, animated: false) } } diff --git a/submodules/TelegramUI/Sources/VerticalListContextResultsChatInputContextPanelNode.swift b/submodules/TelegramUI/Sources/VerticalListContextResultsChatInputContextPanelNode.swift index d26978cc54c..e8dba3fc2f9 100644 --- a/submodules/TelegramUI/Sources/VerticalListContextResultsChatInputContextPanelNode.swift +++ b/submodules/TelegramUI/Sources/VerticalListContextResultsChatInputContextPanelNode.swift @@ -287,7 +287,7 @@ final class VerticalListContextResultsChatInputContextPanelNode: ChatInputContex private func topInsetForLayout(size: CGSize, hasSwitchPeer: Bool) -> CGFloat { var minimumItemHeights: CGFloat = floor(VerticalListContextResultsChatInputPanelItemNode.itemHeight * 3.5) if hasSwitchPeer { - minimumItemHeights += VerticalListContextResultsChatInputPanelButtonItemNode.itemHeight + minimumItemHeights += VerticalListContextResultsChatInputPanelButtonItemNode.itemHeight(style: .regular) } return max(size.height - minimumItemHeights, 0.0) diff --git a/submodules/TelegramUI/Sources/VerticalListContextResultsChatInputPanelButtonItem.swift b/submodules/TelegramUI/Sources/VerticalListContextResultsChatInputPanelButtonItem.swift index 89b9d2cb20a..51c63e48bb3 100644 --- a/submodules/TelegramUI/Sources/VerticalListContextResultsChatInputPanelButtonItem.swift +++ b/submodules/TelegramUI/Sources/VerticalListContextResultsChatInputPanelButtonItem.swift @@ -7,12 +7,19 @@ import SwiftSignalKit import TelegramPresentationData final class VerticalListContextResultsChatInputPanelButtonItem: ListViewItem { + enum Style { + case regular + case round + } + fileprivate let theme: PresentationTheme + fileprivate let style: Style fileprivate let title: String fileprivate let pressed: () -> Void - public init(theme: PresentationTheme, title: String, pressed: @escaping () -> Void) { + public init(theme: PresentationTheme, style: Style = .regular, title: String, pressed: @escaping () -> Void) { self.theme = theme + self.style = style self.title = title self.pressed = pressed } @@ -65,10 +72,15 @@ final class VerticalListContextResultsChatInputPanelButtonItem: ListViewItem { } } -private let titleFont = Font.regular(15.0) - final class VerticalListContextResultsChatInputPanelButtonItemNode: ListViewItemNode { - static let itemHeight: CGFloat = 32.0 + static func itemHeight(style: VerticalListContextResultsChatInputPanelButtonItem.Style) -> CGFloat { + switch style { + case .regular: + return 32.0 + case .round: + return 42.0 + } + } private let buttonNode: HighlightTrackingButtonNode private let titleNode: TextNode @@ -125,11 +137,19 @@ final class VerticalListContextResultsChatInputPanelButtonItemNode: ListViewItem let makeTitleLayout = TextNode.asyncLayout(self.titleNode) return { [weak self] item, params, mergedTop, mergedBottom in + let titleFont: UIFont + switch item.style { + case .regular: + titleFont = Font.regular(15.0) + case .round: + titleFont = Font.regular(17.0) + } + let titleString = NSAttributedString(string: item.title, font: titleFont, textColor: item.theme.list.itemAccentColor) let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: titleString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - params.leftInset - params.rightInset - 16.0, height: 100.0), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) - let nodeLayout = ListViewItemNodeLayout(contentSize: CGSize(width: params.width, height: VerticalListContextResultsChatInputPanelButtonItemNode.itemHeight), insets: UIEdgeInsets()) + let nodeLayout = ListViewItemNodeLayout(contentSize: CGSize(width: params.width, height: VerticalListContextResultsChatInputPanelButtonItemNode.itemHeight(style: item.style)), insets: UIEdgeInsets()) return (nodeLayout, { _ in if let strongSelf = self { @@ -137,14 +157,24 @@ final class VerticalListContextResultsChatInputPanelButtonItemNode: ListViewItem strongSelf.separatorNode.backgroundColor = item.theme.list.itemPlainSeparatorColor strongSelf.topSeparatorNode.backgroundColor = item.theme.list.itemPlainSeparatorColor - strongSelf.backgroundColor = item.theme.list.plainBackgroundColor - let _ = titleApply() + let titleOffsetY: CGFloat + switch item.style { + case .regular: + strongSelf.backgroundColor = item.theme.list.plainBackgroundColor + strongSelf.topSeparatorNode.isHidden = mergedTop + strongSelf.separatorNode.isHidden = !mergedBottom + titleOffsetY = 2.0 + case .round: + strongSelf.backgroundColor = nil + strongSelf.topSeparatorNode.isHidden = true + strongSelf.separatorNode.isHidden = !mergedBottom + titleOffsetY = 1.0 + } - strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: floor((params.width - titleLayout.size.width) / 2.0), y: floor((nodeLayout.contentSize.height - titleLayout.size.height) / 2.0) + 2.0), size: titleLayout.size) + let _ = titleApply() - strongSelf.topSeparatorNode.isHidden = mergedTop - strongSelf.separatorNode.isHidden = !mergedBottom + strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: floor((params.width - titleLayout.size.width) / 2.0), y: floor((nodeLayout.contentSize.height - titleLayout.size.height) / 2.0) + titleOffsetY), size: titleLayout.size) strongSelf.topSeparatorNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: params.width, height: UIScreenPixel)) strongSelf.separatorNode.frame = CGRect(origin: CGPoint(x: 0.0, y: nodeLayout.contentSize.height - UIScreenPixel), size: CGSize(width: params.width, height: UIScreenPixel)) diff --git a/submodules/TelegramVoip/Sources/GroupCallContext.swift b/submodules/TelegramVoip/Sources/GroupCallContext.swift index eaeda817df1..daefa3fa480 100644 --- a/submodules/TelegramVoip/Sources/GroupCallContext.swift +++ b/submodules/TelegramVoip/Sources/GroupCallContext.swift @@ -392,7 +392,7 @@ public final class OngoingGroupCallContext { public let mirrorHorizontally: Bool public let mirrorVertically: Bool - init(frameData: CallVideoFrameData) { + public init(frameData: CallVideoFrameData) { if let nativeBuffer = frameData.buffer as? CallVideoFrameNativePixelBuffer { if CVPixelBufferGetPixelFormatType(nativeBuffer.pixelBuffer) == kCVPixelFormatType_32ARGB { self.buffer = .argb(NativeBuffer(pixelBuffer: nativeBuffer.pixelBuffer)) diff --git a/submodules/TelegramVoip/Sources/macOS/OngoingCallVideoCapturer.swift b/submodules/TelegramVoip/Sources/macOS/OngoingCallVideoCapturer.swift index 2bf532a9db2..a16c539778e 100644 --- a/submodules/TelegramVoip/Sources/macOS/OngoingCallVideoCapturer.swift +++ b/submodules/TelegramVoip/Sources/macOS/OngoingCallVideoCapturer.swift @@ -8,7 +8,7 @@ import Foundation import Cocoa import TgVoipWebrtc - +import SwiftSignalKit public enum OngoingCallVideoOrientation { @@ -79,6 +79,25 @@ public final class OngoingCallVideoCapturer { self.impl = OngoingCallThreadLocalContextVideoCapturer(deviceId: deviceId, keepLandscape: keepLandscape) } + public func video() -> Signal { + return Signal { [weak self] subscriber in + let disposable = MetaDisposable() + + guard let strongSelf = self else { + return disposable + } + let innerDisposable = strongSelf.impl.addVideoOutput({ videoFrameData in + subscriber.putNext(OngoingGroupCallContext.VideoFrameData(frameData: videoFrameData)) + }) + disposable.set(ActionDisposable { + innerDisposable.dispose() + }) + + return disposable + } + } + + public func makeOutgoingVideoView(completion: @escaping (OngoingCallContextPresentationCallVideoView?) -> Void) { self.impl.makeOutgoingVideoView(false, completion: { view, _ in if let view = view { diff --git a/submodules/TgVoipWebrtc/tgcalls b/submodules/TgVoipWebrtc/tgcalls index 6b73742cdc1..564c632f936 160000 --- a/submodules/TgVoipWebrtc/tgcalls +++ b/submodules/TgVoipWebrtc/tgcalls @@ -1 +1 @@ -Subproject commit 6b73742cdc140c46a1ab1b8e3390354a9738e429 +Subproject commit 564c632f9368409870631d3cef75a7fc4070d45b diff --git a/submodules/TranslateUI/Sources/TranslateScreen.swift b/submodules/TranslateUI/Sources/TranslateScreen.swift index ba0f4d1ba7c..94f61b42ae3 100644 --- a/submodules/TranslateUI/Sources/TranslateScreen.swift +++ b/submodules/TranslateUI/Sources/TranslateScreen.swift @@ -720,6 +720,7 @@ public class TranslateScreen: ViewController { statusBarHeight: 0.0, navigationHeight: navigationHeight, safeInsets: UIEdgeInsets(top: layout.intrinsicInsets.top + layout.safeInsets.top, left: layout.safeInsets.left, bottom: layout.intrinsicInsets.bottom + layout.safeInsets.bottom, right: layout.safeInsets.right), + additionalInsets: layout.additionalInsets, inputHeight: layout.inputHeight ?? 0.0, metrics: layout.metrics, deviceMetrics: layout.deviceMetrics, diff --git a/submodules/WebUI/Sources/WebAppWebView.swift b/submodules/WebUI/Sources/WebAppWebView.swift index 169383d6f7d..28c66b03616 100644 --- a/submodules/WebUI/Sources/WebAppWebView.swift +++ b/submodules/WebUI/Sources/WebAppWebView.swift @@ -159,7 +159,7 @@ final class WebAppWebView: WKWebView { self.interactiveTransitionGestureRecognizerTest = { point -> Bool in return point.x > 30.0 } - self.allowsBackForwardNavigationGestures = false + self.allowsBackForwardNavigationGestures = true if #available(iOS 16.4, *) { self.isInspectable = true } diff --git a/swift_deps.bzl b/swift_deps.bzl index 3fdfdb43d4a..3947adc4e8c 100644 --- a/swift_deps.bzl +++ b/swift_deps.bzl @@ -12,7 +12,7 @@ def swift_dependencies(): # version: 2.6.1 swift_package( name = "swiftpkg_floatingpanel", - commit = "5b33d3d5ff1f50f4a2d64158ccfe8c07b5a3e649", + commit = "8f2be39bf49b4d5e22bbf7bdde69d5b76d0ecd2a", dependencies_index = "@//:swift_deps_index.json", remote = "https://github.com/scenee/FloatingPanel", ) @@ -33,10 +33,18 @@ def swift_dependencies(): remote = "https://github.com/LeoNatan/LNExtensionExecutor", ) - # branch: develop + # branch: main + swift_package( + name = "swiftpkg_navigation_stack_backport", + commit = "66716ce9c31198931c2275a0b69de2fdaa687e74", + dependencies_index = "@//:swift_deps_index.json", + remote = "https://github.com/denis15yo/navigation-stack-backport.git", + ) + + # branch: avatar-generator swift_package( name = "swiftpkg_nicegram_assistant_ios", - commit = "123eb001dcc9477f9087e40ff1b0e7f012182d45", + commit = "9d2bc90674d3fa38fdd2e6f8f069c67d296d2bde", dependencies_index = "@//:swift_deps_index.json", remote = "git@bitbucket.org:mobyrix/nicegram-assistant-ios.git", ) @@ -52,7 +60,7 @@ def swift_dependencies(): # version: 5.15.5 swift_package( name = "swiftpkg_sdwebimage", - commit = "b11493f76481dff17ac8f45274a6b698ba0d3af5", + commit = "73b9397cfbd902f606572964055464903b1d84c6", dependencies_index = "@//:swift_deps_index.json", remote = "https://github.com/SDWebImage/SDWebImage.git", ) diff --git a/swift_deps_index.json b/swift_deps_index.json index 910153ab308..991eed44722 100644 --- a/swift_deps_index.json +++ b/swift_deps_index.json @@ -47,6 +47,16 @@ "LNExtensionExecutor-Static" ] }, + { + "name": "NavigationStackBackport", + "c99name": "NavigationStackBackport", + "src_type": "swift", + "label": "@swiftpkg_navigation_stack_backport//:NavigationStackBackport.rspm", + "package_identity": "navigation-stack-backport", + "product_memberships": [ + "NavigationStackBackport" + ] + }, { "name": "CoreSwiftUI", "c99name": "CoreSwiftUI", @@ -54,8 +64,10 @@ "label": "@swiftpkg_nicegram_assistant_ios//:CoreSwiftUI.rspm", "package_identity": "nicegram-assistant-ios", "product_memberships": [ + "FeatAvatarGeneratorUI", "FeatChatBanner", "FeatImagesHubUI", + "FeatNicegramHub", "FeatPhoneEntryBanner", "FeatPremiumUI", "NGAssistantUI", @@ -69,9 +81,30 @@ "label": "@swiftpkg_nicegram_assistant_ios//:FeatAmbassadors.rspm", "package_identity": "nicegram-assistant-ios", "product_memberships": [ + "FeatAvatarGeneratorUI", "NGAssistantUI" ] }, + { + "name": "FeatAvatarGenerator", + "c99name": "FeatAvatarGenerator", + "src_type": "swift", + "label": "@swiftpkg_nicegram_assistant_ios//:FeatAvatarGenerator.rspm", + "package_identity": "nicegram-assistant-ios", + "product_memberships": [ + "FeatAvatarGeneratorUI" + ] + }, + { + "name": "FeatAvatarGeneratorUI", + "c99name": "FeatAvatarGeneratorUI", + "src_type": "swift", + "label": "@swiftpkg_nicegram_assistant_ios//:FeatAvatarGeneratorUI.rspm", + "package_identity": "nicegram-assistant-ios", + "product_memberships": [ + "FeatAvatarGeneratorUI" + ] + }, { "name": "FeatBilling", "c99name": "FeatBilling", @@ -79,8 +112,10 @@ "label": "@swiftpkg_nicegram_assistant_ios//:FeatBilling.rspm", "package_identity": "nicegram-assistant-ios", "product_memberships": [ + "FeatAvatarGeneratorUI", "FeatCardUI", "FeatImagesHubUI", + "FeatNicegramHub", "FeatPremiumUI", "FeatSpeechToText", "NGAiChatUI", @@ -96,8 +131,10 @@ "label": "@swiftpkg_nicegram_assistant_ios//:FeatCard.rspm", "package_identity": "nicegram-assistant-ios", "product_memberships": [ + "FeatAvatarGeneratorUI", "FeatCardUI", "FeatImagesHubUI", + "FeatNicegramHub", "FeatPremiumUI", "NGAiChatUI", "NGAssistantUI", @@ -112,6 +149,7 @@ "label": "@swiftpkg_nicegram_assistant_ios//:FeatCardUI.rspm", "package_identity": "nicegram-assistant-ios", "product_memberships": [ + "FeatAvatarGeneratorUI", "FeatCardUI", "NGAssistantUI" ] @@ -133,6 +171,7 @@ "label": "@swiftpkg_nicegram_assistant_ios//:FeatChatListBanner.rspm", "package_identity": "nicegram-assistant-ios", "product_memberships": [ + "FeatAvatarGeneratorUI", "NGAssistantUI" ] }, @@ -153,6 +192,7 @@ "label": "@swiftpkg_nicegram_assistant_ios//:FeatHiddenChats.rspm", "package_identity": "nicegram-assistant-ios", "product_memberships": [ + "FeatAvatarGeneratorUI", "FeatHiddenChats", "NGAssistantUI" ] @@ -164,6 +204,7 @@ "label": "@swiftpkg_nicegram_assistant_ios//:FeatImagesHub.rspm", "package_identity": "nicegram-assistant-ios", "product_memberships": [ + "FeatAvatarGeneratorUI", "FeatImagesHubUI", "NGAssistantUI" ] @@ -175,10 +216,24 @@ "label": "@swiftpkg_nicegram_assistant_ios//:FeatImagesHubUI.rspm", "package_identity": "nicegram-assistant-ios", "product_memberships": [ + "FeatAvatarGeneratorUI", "FeatImagesHubUI", "NGAssistantUI" ] }, + { + "name": "FeatNicegramHub", + "c99name": "FeatNicegramHub", + "src_type": "swift", + "label": "@swiftpkg_nicegram_assistant_ios//:FeatNicegramHub.rspm", + "package_identity": "nicegram-assistant-ios", + "product_memberships": [ + "FeatAvatarGeneratorUI", + "FeatNicegramHub", + "NGAssistantUI", + "NGEntryPoint" + ] + }, { "name": "FeatPersistentStorage", "c99name": "FeatPersistentStorage", @@ -186,8 +241,10 @@ "label": "@swiftpkg_nicegram_assistant_ios//:FeatPersistentStorage.rspm", "package_identity": "nicegram-assistant-ios", "product_memberships": [ + "FeatAvatarGeneratorUI", "FeatCardUI", "FeatImagesHubUI", + "FeatNicegramHub", "FeatPremiumUI", "NGAiChat", "NGAiChatUI", @@ -223,9 +280,11 @@ "label": "@swiftpkg_nicegram_assistant_ios//:FeatPremium.rspm", "package_identity": "nicegram-assistant-ios", "product_memberships": [ + "FeatAvatarGeneratorUI", "FeatCardUI", "FeatChatBanner", "FeatImagesHubUI", + "FeatNicegramHub", "FeatPinnedChats", "FeatPremium", "FeatPremiumUI", @@ -248,6 +307,7 @@ "label": "@swiftpkg_nicegram_assistant_ios//:FeatPremiumUI.rspm", "package_identity": "nicegram-assistant-ios", "product_memberships": [ + "FeatAvatarGeneratorUI", "FeatImagesHubUI", "FeatPremiumUI", "NGAssistantUI", @@ -261,8 +321,10 @@ "label": "@swiftpkg_nicegram_assistant_ios//:FeatRewards.rspm", "package_identity": "nicegram-assistant-ios", "product_memberships": [ + "FeatAvatarGeneratorUI", "FeatCardUI", "FeatImagesHubUI", + "FeatNicegramHub", "FeatPremiumUI", "FeatTasks", "NGAiChatUI", @@ -278,6 +340,7 @@ "label": "@swiftpkg_nicegram_assistant_ios//:FeatRewardsUI.rspm", "package_identity": "nicegram-assistant-ios", "product_memberships": [ + "FeatAvatarGeneratorUI", "NGAssistantUI" ] }, @@ -319,8 +382,10 @@ "label": "@swiftpkg_nicegram_assistant_ios//:NGAiChat.rspm", "package_identity": "nicegram-assistant-ios", "product_memberships": [ + "FeatAvatarGeneratorUI", "FeatCardUI", "FeatImagesHubUI", + "FeatNicegramHub", "FeatPremiumUI", "NGAiChat", "NGAiChatUI", @@ -336,6 +401,7 @@ "label": "@swiftpkg_nicegram_assistant_ios//:NGAiChatUI.rspm", "package_identity": "nicegram-assistant-ios", "product_memberships": [ + "FeatAvatarGeneratorUI", "FeatImagesHubUI", "NGAiChatUI", "NGAssistantUI" @@ -348,9 +414,11 @@ "label": "@swiftpkg_nicegram_assistant_ios//:NGAnalytics.rspm", "package_identity": "nicegram-assistant-ios", "product_memberships": [ + "FeatAvatarGeneratorUI", "FeatCardUI", "FeatChatBanner", "FeatImagesHubUI", + "FeatNicegramHub", "FeatPinnedChats", "FeatPremiumUI", "FeatTasks", @@ -370,9 +438,11 @@ "label": "@swiftpkg_nicegram_assistant_ios//:NGApi.rspm", "package_identity": "nicegram-assistant-ios", "product_memberships": [ + "FeatAvatarGeneratorUI", "FeatCardUI", "FeatChatBanner", "FeatImagesHubUI", + "FeatNicegramHub", "FeatPinnedChats", "FeatPremiumUI", "FeatSpeechToText", @@ -395,6 +465,7 @@ "label": "@swiftpkg_nicegram_assistant_ios//:NGAssistant.rspm", "package_identity": "nicegram-assistant-ios", "product_memberships": [ + "FeatAvatarGeneratorUI", "FeatImagesHubUI", "NGAiChatUI", "NGAssistantUI", @@ -408,6 +479,7 @@ "label": "@swiftpkg_nicegram_assistant_ios//:NGAssistantUI.rspm", "package_identity": "nicegram-assistant-ios", "product_memberships": [ + "FeatAvatarGeneratorUI", "NGAssistantUI" ] }, @@ -418,8 +490,10 @@ "label": "@swiftpkg_nicegram_assistant_ios//:NGAuth.rspm", "package_identity": "nicegram-assistant-ios", "product_memberships": [ + "FeatAvatarGeneratorUI", "FeatCardUI", "FeatImagesHubUI", + "FeatNicegramHub", "FeatPremiumUI", "NGAiChatUI", "NGAssistantUI", @@ -434,10 +508,12 @@ "label": "@swiftpkg_nicegram_assistant_ios//:NGCore.rspm", "package_identity": "nicegram-assistant-ios", "product_memberships": [ + "FeatAvatarGeneratorUI", "FeatCardUI", "FeatChatBanner", "FeatHiddenChats", "FeatImagesHubUI", + "FeatNicegramHub", "FeatPhoneEntryBanner", "FeatPinnedChats", "FeatPremium", @@ -467,9 +543,11 @@ "modulemap_label": "@swiftpkg_nicegram_assistant_ios//:NGCoreUI.rspm_modulemap", "package_identity": "nicegram-assistant-ios", "product_memberships": [ + "FeatAvatarGeneratorUI", "FeatCardUI", "FeatChatBanner", "FeatImagesHubUI", + "FeatNicegramHub", "FeatPhoneEntryBanner", "FeatPinnedChats", "FeatPremiumUI", @@ -497,6 +575,7 @@ "label": "@swiftpkg_nicegram_assistant_ios//:NGGrumUI.rspm", "package_identity": "nicegram-assistant-ios", "product_memberships": [ + "FeatAvatarGeneratorUI", "NGAssistantUI" ] }, @@ -507,10 +586,12 @@ "label": "@swiftpkg_nicegram_assistant_ios//:NGLocalization.rspm", "package_identity": "nicegram-assistant-ios", "product_memberships": [ + "FeatAvatarGeneratorUI", "FeatCardUI", "FeatChatBanner", "FeatHiddenChats", "FeatImagesHubUI", + "FeatNicegramHub", "FeatPhoneEntryBanner", "FeatPinnedChats", "FeatPremium", @@ -540,8 +621,10 @@ "label": "@swiftpkg_nicegram_assistant_ios//:NGRepoTg.rspm", "package_identity": "nicegram-assistant-ios", "product_memberships": [ + "FeatAvatarGeneratorUI", "FeatCardUI", "FeatImagesHubUI", + "FeatNicegramHub", "FeatPremiumUI", "NGAiChatUI", "NGAssistantUI", @@ -557,9 +640,11 @@ "label": "@swiftpkg_nicegram_assistant_ios//:NGRepoUser.rspm", "package_identity": "nicegram-assistant-ios", "product_memberships": [ + "FeatAvatarGeneratorUI", "FeatCardUI", "FeatChatBanner", "FeatImagesHubUI", + "FeatNicegramHub", "FeatPinnedChats", "FeatPremiumUI", "FeatSpeechToText", @@ -581,6 +666,7 @@ "label": "@swiftpkg_nicegram_assistant_ios//:NGSpecialOffer.rspm", "package_identity": "nicegram-assistant-ios", "product_memberships": [ + "FeatAvatarGeneratorUI", "NGAssistantUI", "NGSpecialOffer" ] @@ -592,8 +678,10 @@ "label": "@swiftpkg_nicegram_assistant_ios//:Tapjoy.rspm", "package_identity": "nicegram-assistant-ios", "product_memberships": [ + "FeatAvatarGeneratorUI", "FeatCardUI", "FeatImagesHubUI", + "FeatNicegramHub", "FeatPremiumUI", "FeatTasks", "NGAiChatUI", @@ -609,9 +697,11 @@ "label": "@swiftpkg_nicegram_assistant_ios//:_NGRemoteConfig.rspm", "package_identity": "nicegram-assistant-ios", "product_memberships": [ + "FeatAvatarGeneratorUI", "FeatCardUI", "FeatChatBanner", "FeatImagesHubUI", + "FeatNicegramHub", "FeatPinnedChats", "FeatPremiumUI", "FeatSpeechToText", @@ -865,6 +955,18 @@ "type": "library", "label": "@swiftpkg_lnextensionexecutor//:LNExtensionExecutor-Static" }, + { + "identity": "navigation-stack-backport", + "name": "NavigationStackBackport", + "type": "library", + "label": "@swiftpkg_navigation_stack_backport//:NavigationStackBackport" + }, + { + "identity": "nicegram-assistant-ios", + "name": "FeatAvatarGeneratorUI", + "type": "library", + "label": "@swiftpkg_nicegram_assistant_ios//:FeatAvatarGeneratorUI" + }, { "identity": "nicegram-assistant-ios", "name": "FeatCardUI", @@ -889,6 +991,12 @@ "type": "library", "label": "@swiftpkg_nicegram_assistant_ios//:FeatImagesHubUI" }, + { + "identity": "nicegram-assistant-ios", + "name": "FeatNicegramHub", + "type": "library", + "label": "@swiftpkg_nicegram_assistant_ios//:FeatNicegramHub" + }, { "identity": "nicegram-assistant-ios", "name": "FeatPhoneEntryBanner", @@ -1114,9 +1222,9 @@ "name": "swiftpkg_floatingpanel", "identity": "floatingpanel", "remote": { - "commit": "5b33d3d5ff1f50f4a2d64158ccfe8c07b5a3e649", + "commit": "8f2be39bf49b4d5e22bbf7bdde69d5b76d0ecd2a", "remote": "https://github.com/scenee/FloatingPanel", - "version": "2.8.1" + "version": "2.8.2" } }, { @@ -1137,11 +1245,20 @@ "version": "1.2.0" } }, + { + "name": "swiftpkg_navigation_stack_backport", + "identity": "navigation-stack-backport", + "remote": { + "commit": "66716ce9c31198931c2275a0b69de2fdaa687e74", + "remote": "https://github.com/denis15yo/navigation-stack-backport.git", + "branch": "main" + } + }, { "name": "swiftpkg_nicegram_assistant_ios", "identity": "nicegram-assistant-ios", "remote": { - "commit": "123eb001dcc9477f9087e40ff1b0e7f012182d45", + "commit": "9d2bc90674d3fa38fdd2e6f8f069c67d296d2bde", "remote": "git@bitbucket.org:mobyrix/nicegram-assistant-ios.git", "branch": "develop" } @@ -1159,9 +1276,9 @@ "name": "swiftpkg_sdwebimage", "identity": "sdwebimage", "remote": { - "commit": "b11493f76481dff17ac8f45274a6b698ba0d3af5", + "commit": "73b9397cfbd902f606572964055464903b1d84c6", "remote": "https://github.com/SDWebImage/SDWebImage.git", - "version": "5.18.11" + "version": "5.19.0" } }, { diff --git a/versions.json b/versions.json index 3f573437c30..398ac3d2d61 100644 --- a/versions.json +++ b/versions.json @@ -1,5 +1,5 @@ { - "app": "1.5.6", + "app": "1.5.7", "bazel": "7.0.2", "xcode": "15.2", "macos": "13.0"