diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index c3292dd630..413ff1ac2d 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -15818,3 +15818,10 @@ Error: %8$@"; "Premium.CopyProtection" = "Disable Sharing"; "Premium.CopyProtectionInfo" = "Prevent forwarding, saving and copying content in private chats."; + +"Channel.EditAdmin.PermissionManageRanks" = "Edit Member Tags"; + +"Group.EditAdmin.MemberTagTitle" = "Member Tag"; +"Group.EditAdmin.MemberTagInfo" = "Add short tag next to %@'s name."; + +"Channel.Owner.Title" = "Owner"; diff --git a/submodules/AccountContext/Sources/AccountContext.swift b/submodules/AccountContext/Sources/AccountContext.swift index e84cc798fa..cd086902b3 100644 --- a/submodules/AccountContext/Sources/AccountContext.swift +++ b/submodules/AccountContext/Sources/AccountContext.swift @@ -132,6 +132,7 @@ public final class AccountWithInfo: Equatable { public enum OpenURLContext { case generic case chat(peerId: PeerId, message: Message?, updatedPresentationData: (initial: PresentationData, signal: Signal)?) + case external } public struct ChatAvailableMessageActionOptions: OptionSet { @@ -1534,6 +1535,8 @@ public protocol SharedAccountContext: AnyObject { func makeChatRankInfoScreen(context: AccountContext, chatPeer: EnginePeer, userPeer: EnginePeer, role: ChatRankInfoScreenRole, rank: String, canChange: Bool, completion: @escaping () -> Void) -> ViewController + func makeChatRankPreviewItem(context: AccountContext, peer: EnginePeer, rank: String, rankRole: ChatRankInfoScreenRole, theme: PresentationTheme, strings: PresentationStrings, wallpaper: TelegramWallpaper, fontSize: PresentationFontSize, chatBubbleCorners: PresentationChatBubbleCorners, dateTimeFormat: PresentationDateTimeFormat, nameOrder: PresentationPersonNameOrder, sectionId: Int32) -> ListViewItem + func navigateToCurrentCall() var hasOngoingCall: ValuePromise { get } var immediateHasOngoingCall: Bool { get } diff --git a/submodules/BrowserUI/Sources/BrowserWebContent.swift b/submodules/BrowserUI/Sources/BrowserWebContent.swift index 9bb574d5f7..73970b5fdc 100644 --- a/submodules/BrowserUI/Sources/BrowserWebContent.swift +++ b/submodules/BrowserUI/Sources/BrowserWebContent.swift @@ -151,6 +151,12 @@ final class WebView: WKWebView { } return result } + + func sendEvent(name: String, data: String?) { + let script = "window.TelegramGameProxy && window.TelegramGameProxy.receiveEvent && window.TelegramGameProxy.receiveEvent(\"\(name)\", \(data ?? "null"))" + self.evaluateJavaScript(script, completionHandler: { _, _ in + }) + } } private class WeakScriptMessageHandler: NSObject, WKScriptMessageHandler { @@ -399,6 +405,8 @@ final class BrowserWebContent: UIView, BrowserContent, WKNavigationDelegate, WKU switch eventName { case "cancellingTouch": self.cancelInteractiveTransitionGestures() + case "oauth_request": + self.webView.sendEvent(name: "oauth_supported", data: nil) default: break } diff --git a/submodules/PeerInfoUI/Sources/ChannelAdminController.swift b/submodules/PeerInfoUI/Sources/ChannelAdminController.swift index 922074b49d..bc243dfb20 100644 --- a/submodules/PeerInfoUI/Sources/ChannelAdminController.swift +++ b/submodules/PeerInfoUI/Sources/ChannelAdminController.swift @@ -71,6 +71,7 @@ private enum ChannelAdminEntryTag: ItemListItemTag { private enum ChannelAdminEntryStableId: Hashable { case info case rankTitle + case rankPreview case rank case rankInfo case adminRights @@ -113,6 +114,7 @@ private let storiesRelatedFlags: [TelegramChatAdminRightsFlags] = [ private enum ChannelAdminEntry: ItemListNodeEntry { case info(PresentationTheme, PresentationStrings, PresentationDateTimeFormat, EnginePeer, EnginePeer.Presence?) case rankTitle(PresentationTheme, String, Int32?, Int32) + case rankPreview(PresentationTheme, PresentationStrings, EnginePeer, String, Bool) case rank(PresentationTheme, PresentationStrings, String, String, Bool) case rankInfo(PresentationTheme, String, Bool) case adminRights(PresentationTheme, String, Bool) @@ -126,7 +128,7 @@ private enum ChannelAdminEntry: ItemListNodeEntry { switch self { case .info: return ChannelAdminSection.info.rawValue - case .rankTitle, .rank, .rankInfo: + case .rankTitle, .rankPreview, .rank, .rankInfo: return ChannelAdminSection.rank.rawValue case .adminRights: return ChannelAdminSection.adminRights.rawValue @@ -145,6 +147,8 @@ private enum ChannelAdminEntry: ItemListNodeEntry { return .info case .rankTitle: return .rankTitle + case .rankPreview: + return .rankPreview case .rank: return .rank case .rankInfo: @@ -194,6 +198,12 @@ private enum ChannelAdminEntry: ItemListNodeEntry { } else { return false } + case let .rankPreview(lhsTheme, lhsStrings, lhsPeer, lhsRank, lhsIsOwner): + if case let .rankPreview(rhsTheme, rhsStrings, rhsPeer, rhsRank, rhsIsOwner) = rhs, lhsTheme === rhsTheme, lhsStrings === rhsStrings, lhsPeer == rhsPeer, lhsRank == rhsRank, lhsIsOwner == rhsIsOwner { + return true + } else { + return false + } case let .rank(lhsTheme, lhsStrings, lhsPlaceholder, lhsValue, lhsEnabled): if case let .rank(rhsTheme, rhsStrings, rhsPlaceholder, rhsValue, rhsEnabled) = rhs, lhsTheme === rhsTheme, lhsStrings === rhsStrings, lhsPlaceholder == rhsPlaceholder, lhsValue == rhsValue, lhsEnabled == rhsEnabled { return true @@ -281,44 +291,23 @@ private enum ChannelAdminEntry: ItemListNodeEntry { default: return true } - case .rankTitle: - switch rhs { - case .info, .rankTitle: - return false - default: - return true - } - case .rank: - switch rhs { - case .info, .rankTitle, .rank: - return false - default: - return true - } - case .rankInfo: - switch rhs { - case .info, .rankTitle, .rank, .rankInfo: - return false - default: - return true - } case .adminRights: switch rhs { - case .info, .rankTitle, .rank, .rankInfo, .adminRights: + case .info, .adminRights: return false default: return true } case .rightsTitle: switch rhs { - case .info, .rankTitle, .rank, .rankInfo, .adminRights, .rightsTitle: + case .info, .adminRights, .rightsTitle: return false default: return true } case let .rightItem(_, lhsIndex, _, _, _, _, _, _, _): switch rhs { - case .info, .rankTitle, .rank, .rankInfo, .adminRights, .rightsTitle: + case .info, .adminRights, .rightsTitle: return false case let .rightItem(_, rhsIndex, _, _, _, _, _, _, _): return lhsIndex < rhsIndex @@ -327,14 +316,42 @@ private enum ChannelAdminEntry: ItemListNodeEntry { } case .addAdminsInfo: switch rhs { - case .info, .rankTitle, .rank, .rankInfo, .adminRights, .rightsTitle, .rightItem, .addAdminsInfo: + case .info, .adminRights, .rightsTitle, .rightItem, .addAdminsInfo: return false default: return true } case .transfer: switch rhs { - case .info, .rankTitle, .rank, .rankInfo, .adminRights, .rightsTitle, .rightItem, .addAdminsInfo, .transfer: + case .info, .adminRights, .rightsTitle, .rightItem, .addAdminsInfo, .transfer: + return false + default: + return true + } + case .rankTitle: + switch rhs { + case .info, .adminRights, .rightsTitle, .rightItem, .addAdminsInfo, .transfer, .rankTitle: + return false + default: + return true + } + case .rankPreview: + switch rhs { + case .info, .adminRights, .rightsTitle, .rightItem, .addAdminsInfo, .transfer, .rankTitle, .rankPreview: + return false + default: + return true + } + case .rank: + switch rhs { + case .info, .adminRights, .rightsTitle, .rightItem, .addAdminsInfo, .transfer, .rankTitle, .rankPreview, .rank: + return false + default: + return true + } + case .rankInfo: + switch rhs { + case .info, .adminRights, .rightsTitle, .rightItem, .addAdminsInfo, .transfer, .rankTitle, .rankPreview, .rank, .rankInfo: return false default: return true @@ -357,6 +374,9 @@ private enum ChannelAdminEntry: ItemListNodeEntry { accessoryText = ItemListSectionHeaderAccessoryText(value: "\(limit - count)", color: count > limit ? .destructive : .generic) } return ItemListSectionHeaderItem(presentationData: presentationData, text: text, accessoryText: accessoryText, sectionId: self.section) + case let .rankPreview(_, _, peer, rank, isOwner): + let globalPresentationData = arguments.context.sharedContext.currentPresentationData.with { $0 } + return arguments.context.sharedContext.makeChatRankPreviewItem(context: arguments.context, peer: peer, rank: rank, rankRole: isOwner ? .creator : .admin, theme: presentationData.theme, strings: presentationData.strings, wallpaper: globalPresentationData.chatWallpaper, fontSize: globalPresentationData.chatFontSize, chatBubbleCorners: globalPresentationData.chatBubbleCorners, dateTimeFormat: presentationData.dateTimeFormat, nameOrder: presentationData.nameDisplayOrder, sectionId: self.section) case let .rank(_, _, placeholder, text, enabled): return ItemListSingleLineInputItem(presentationData: presentationData, systemStyle: .glass, title: NSAttributedString(string: "", textColor: .black), text: text, placeholder: placeholder, type: .regular(capitalization: false, autocorrection: true), spacing: 0.0, clearType: enabled ? .always : .none, enabled: enabled, tag: ChannelAdminEntryTag.rank, sectionId: self.section, textUpdated: { updatedText in arguments.updateRank(text, updatedText) @@ -523,6 +543,8 @@ private func stringForRight(strings: PresentationStrings, right: TelegramChatAdm } } else if right.contains(.canPinMessages) { return strings.Channel_EditAdmin_PermissionPinMessages + } else if right.contains(.canManageRanks) { + return strings.Channel_EditAdmin_PermissionManageRanks } else if right.contains(.canManageTopics) { return strings.Channel_EditAdmin_PermissionManageTopics } else if right.contains(.canAddAdmins) { @@ -644,6 +666,7 @@ private func channelAdminControllerEntries(presentationData: PresentationData, s .direct(.canDeleteMessages), .direct(.canBanUsers), .direct(.canInviteUsers), + .direct(.canManageRanks), .direct(.canPinMessages), .direct(.canManageTopics), .direct(.canManageCalls), @@ -656,6 +679,7 @@ private func channelAdminControllerEntries(presentationData: PresentationData, s .direct(.canDeleteMessages), .direct(.canBanUsers), .direct(.canInviteUsers), + .direct(.canManageRanks), .direct(.canPinMessages), .sub(.stories, storiesRelatedFlags), .direct(.canManageCalls), @@ -665,25 +689,6 @@ private func channelAdminControllerEntries(presentationData: PresentationData, s } } - if isGroup, !invite { - let placeholder = isCreator ? presentationData.strings.Group_EditAdmin_RankOwnerPlaceholder : presentationData.strings.Group_EditAdmin_RankAdminPlaceholder - - let currentRank: String? - if let updatedRank = state.updatedRank { - currentRank = updatedRank - } else if let initialParticipant = initialParticipant { - currentRank = initialParticipant.rank - } else { - currentRank = nil - } - - let rankEnabled = !state.updating && canEdit - //TODO:localize - entries.append(.rankTitle(presentationData.theme, "Member Tag".uppercased(), rankEnabled && state.focusedOnRank ? Int32(currentRank?.count ?? 0) : nil, rankMaxLength)) - entries.append(.rank(presentationData.theme, presentationData.strings, placeholder, currentRank ?? "", rankEnabled)) - entries.append(.rankInfo(presentationData.theme, "Add short tag next to \(admin.compactDisplayTitle)'s name.", invite)) - } - if isCreator { if isGroup { entries.append(.rightsTitle(presentationData.theme, presentationData.strings.Channel_EditAdmin_PermissionsHeader)) @@ -907,6 +912,27 @@ private func channelAdminControllerEntries(presentationData: PresentationData, s if canTransfer { entries.append(.transfer(presentationData.theme, isGroup ? presentationData.strings.Group_EditAdmin_TransferOwnership : presentationData.strings.Channel_EditAdmin_TransferOwnership)) } + + if case .group = channel.info { + let placeholder = isCreator ? presentationData.strings.Group_EditAdmin_RankOwnerPlaceholder : presentationData.strings.Group_EditAdmin_RankAdminPlaceholder + + let currentRank: String? + if let updatedRank = state.updatedRank { + currentRank = updatedRank + } else if let initialParticipant = initialParticipant { + currentRank = initialParticipant.rank + } else { + currentRank = nil + } + + let rankEnabled = !state.updating && canEdit + entries.append(.rankTitle(presentationData.theme, presentationData.strings.Group_EditAdmin_MemberTagTitle.uppercased(), rankEnabled && state.focusedOnRank ? Int32(currentRank?.count ?? 0) : nil, rankMaxLength)) + if let adminPeer { + entries.append(.rankPreview(presentationData.theme, presentationData.strings, adminPeer, currentRank ?? placeholder, isCreator)) + entries.append(.rank(presentationData.theme, presentationData.strings, isCreator ? presentationData.strings.Group_EditAdmin_RankOwnerPlaceholder : presentationData.strings.Group_EditAdmin_RankAdminPlaceholder, currentRank ?? "", rankEnabled)) + entries.append(.rankInfo(presentationData.theme, presentationData.strings.Group_EditAdmin_MemberTagInfo(adminPeer.compactDisplayTitle).string, invite)) + } + } } if canDismiss { @@ -929,16 +955,10 @@ private func channelAdminControllerEntries(presentationData: PresentationData, s let rankEnabled = !state.updating && canEdit - if !invite { - //TODO:localize - let placeholder = isCreator ? presentationData.strings.Group_EditAdmin_RankOwnerPlaceholder : presentationData.strings.Group_EditAdmin_RankAdminPlaceholder - entries.append(.rankTitle(presentationData.theme, "Member Tag".uppercased(), rankEnabled && state.focusedOnRank ? Int32(currentRank?.count ?? 0) : nil, rankMaxLength)) - entries.append(.rank(presentationData.theme, presentationData.strings, placeholder, currentRank ?? "", rankEnabled)) - entries.append(.rankInfo(presentationData.theme, "Add short tag next to \(admin.compactDisplayTitle)'s name.", invite)) - } - if isCreator { - + entries.append(.rankTitle(presentationData.theme, presentationData.strings.Group_EditAdmin_MemberTagTitle.uppercased(), rankEnabled && state.focusedOnRank ? Int32(currentRank?.count ?? 0) : nil, rankMaxLength)) + entries.append(.rankPreview(presentationData.theme, presentationData.strings, admin, currentRank ?? presentationData.strings.Group_EditAdmin_RankOwnerPlaceholder, true)) + entries.append(.rank(presentationData.theme, presentationData.strings, presentationData.strings.Group_EditAdmin_RankOwnerPlaceholder, currentRank ?? "", rankEnabled)) } else { if case let .user(adminPeer) = adminPeer, adminPeer.botInfo != nil, invite { if let initialParticipant = initialParticipant, case let .member(_, _, adminRights, _, _, _) = initialParticipant, adminRights != nil { @@ -995,6 +1015,12 @@ private func channelAdminControllerEntries(presentationData: PresentationData, s if case let .user(admin) = admin, case .creator = group.role, admin.botInfo == nil && !admin.isDeleted && areAllAdminRightsEnabled(currentRightsFlags, peer: .legacyGroup(group), except: .canBeAnonymous) { entries.append(.transfer(presentationData.theme, presentationData.strings.Group_EditAdmin_TransferOwnership)) } + + let placeholder = isCreator ? presentationData.strings.Group_EditAdmin_RankOwnerPlaceholder : presentationData.strings.Group_EditAdmin_RankAdminPlaceholder + entries.append(.rankTitle(presentationData.theme, presentationData.strings.Group_EditAdmin_MemberTagTitle.uppercased(), rankEnabled && state.focusedOnRank ? Int32(currentRank?.count ?? 0) : nil, rankMaxLength)) + entries.append(.rankPreview(presentationData.theme, presentationData.strings, admin, currentRank ?? placeholder, isCreator)) + entries.append(.rank(presentationData.theme, presentationData.strings, placeholder, currentRank ?? "", rankEnabled)) + entries.append(.rankInfo(presentationData.theme, presentationData.strings.Group_EditAdmin_MemberTagInfo(admin.compactDisplayTitle).string, invite)) } if let initialParticipant = initialParticipant, case let .member(_, _, adminInfo, _, _, _) = initialParticipant, admin.id != accountPeerId, let adminInfo { @@ -1613,8 +1639,15 @@ public func channelAdminController(context: AccountContext, updatedPresentationD var footerItem: ItemListControllerFooterItem? + var isCreator = false + if case let .channel(channel) = channelPeer, channel.flags.contains(.isCreator) { + isCreator = true + } else if case let .legacyGroup(group) = channelPeer, case .creator = group.role { + isCreator = true + } + let title: String - if initialParticipant?.adminInfo == nil { + if initialParticipant?.adminInfo == nil && !isCreator { var isGroup: Bool = false var peerTitle: String = "" if case let .legacyGroup(peer) = channelPeer { @@ -1655,7 +1688,7 @@ public func channelAdminController(context: AccountContext, updatedPresentationD title = presentationData.strings.Channel_Management_AddModerator } } else { - title = presentationData.strings.Channel_Moderator_Title + title = isCreator ? presentationData.strings.Channel_Owner_Title : presentationData.strings.Channel_Moderator_Title } let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(title), leftNavigationButton: leftNavigationButton, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: false) diff --git a/submodules/TelegramCore/Sources/State/Serialization.swift b/submodules/TelegramCore/Sources/State/Serialization.swift index dc7b329822..400f7e2881 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 224 + return 223 } public func parseMessage(_ data: Data!) -> Any! { diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/RequestMessageActionCallback.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/RequestMessageActionCallback.swift index 4cf4dd8714..ca0c925309 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/RequestMessageActionCallback.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/RequestMessageActionCallback.swift @@ -206,30 +206,33 @@ public enum MessageActionUrlAuthError { public enum MessageActionUrlSubject { case message(id: MessageId, buttonId: Int32) - case url(String) + case url(url: String, inAppOrigin: String?) } func _internal_requestMessageActionUrlAuth(account: Account, subject: MessageActionUrlSubject) -> Signal { let request: Signal var flags: Int32 = 0 switch subject { - case let .message(messageId, buttonId): - flags |= (1 << 1) - request = account.postbox.loadedPeerWithId(messageId.peerId) - |> take(1) - |> castError(MTRpcError.self) - |> mapToSignal { peer -> Signal in - if let inputPeer = apiInputPeer(peer) { - return account.network.request(Api.functions.messages.requestUrlAuth(flags: flags, peer: inputPeer, msgId: messageId.id, buttonId: buttonId, url: nil, inAppOrigin: nil)) - |> map(Optional.init) - } else { - return .single(nil) - } + case let .message(messageId, buttonId): + flags |= (1 << 1) + request = account.postbox.loadedPeerWithId(messageId.peerId) + |> take(1) + |> castError(MTRpcError.self) + |> mapToSignal { peer -> Signal in + if let inputPeer = apiInputPeer(peer) { + return account.network.request(Api.functions.messages.requestUrlAuth(flags: flags, peer: inputPeer, msgId: messageId.id, buttonId: buttonId, url: nil, inAppOrigin: nil)) + |> map(Optional.init) + } else { + return .single(nil) } - case let .url(url): - flags |= (1 << 2) - request = account.network.request(Api.functions.messages.requestUrlAuth(flags: flags, peer: nil, msgId: nil, buttonId: nil, url: url, inAppOrigin: nil)) - |> map(Optional.init) + } + case let .url(url, inAppOrigin): + flags |= (1 << 2) + if let _ = inAppOrigin { + flags |= (1 << 3) + } + request = account.network.request(Api.functions.messages.requestUrlAuth(flags: flags, peer: nil, msgId: nil, buttonId: nil, url: url, inAppOrigin: inAppOrigin)) + |> map(Optional.init) } return request @@ -303,7 +306,7 @@ func _internal_acceptMessageActionUrlAuth(account: Account, subject: MessageActi return .single(nil) } } - case let .url(url): + case let .url(url, _): flags |= (1 << 2) request = account.network.request(Api.functions.messages.acceptUrlAuth(flags: flags, peer: nil, msgId: nil, buttonId: nil, url: url, matchCode: matchCode)) |> map(Optional.init) diff --git a/submodules/TelegramUI/BUILD b/submodules/TelegramUI/BUILD index 72b8d880c7..52e976f39d 100644 --- a/submodules/TelegramUI/BUILD +++ b/submodules/TelegramUI/BUILD @@ -520,6 +520,7 @@ swift_library( "//submodules/TelegramUI/Components/ChatParticipantRightsScreen", "//submodules/TelegramUI/Components/PeerInfo/PeerCopyProtectionInfoScreen", "//submodules/TelegramUI/Components/Chat/ChatRankInfoScreen", + "//submodules/TelegramUI/Components/RankChatPreviewItem", ] + select({ "@build_bazel_rules_apple//apple:ios_arm64": appcenter_targets, "//build-system:ios_sim_arm64": [], diff --git a/submodules/TelegramUI/Components/AlertComponent/Sources/AlertComponent.swift b/submodules/TelegramUI/Components/AlertComponent/Sources/AlertComponent.swift index 2232355fc1..900b1c44c4 100644 --- a/submodules/TelegramUI/Components/AlertComponent/Sources/AlertComponent.swift +++ b/submodules/TelegramUI/Components/AlertComponent/Sources/AlertComponent.swift @@ -809,6 +809,8 @@ open class AlertScreen: ViewControllerComponentContainer, KeyShortcutResponder { if !self.processedDidDisappear { self.processedDidDisappear = true + self.view.window?.endEditing(true) + if let componentView = self.node.hostView.componentView as? AlertScreenComponent.View { let dismissedByTapOutside = componentView.dismissedByTapOutside componentView.animateOut(completion: { [weak self] in diff --git a/submodules/TelegramUI/Components/RankChatPreviewItem/Sources/RankChatPreviewItem.swift b/submodules/TelegramUI/Components/RankChatPreviewItem/Sources/RankChatPreviewItem.swift index bf0a7d479c..ee66c5d33b 100644 --- a/submodules/TelegramUI/Components/RankChatPreviewItem/Sources/RankChatPreviewItem.swift +++ b/submodules/TelegramUI/Components/RankChatPreviewItem/Sources/RankChatPreviewItem.swift @@ -47,6 +47,7 @@ public final class RankChatPreviewItem: ListViewItem, ItemListItem, ListItemComp } let context: AccountContext + let systemStyle: ItemListSystemStyle let theme: PresentationTheme let componentTheme: PresentationTheme let strings: PresentationStrings @@ -64,6 +65,7 @@ public final class RankChatPreviewItem: ListViewItem, ItemListItem, ListItemComp public init( context: AccountContext, + systemStyle: ItemListSystemStyle = .legacy, theme: PresentationTheme, componentTheme: PresentationTheme, strings: PresentationStrings, @@ -80,6 +82,7 @@ public final class RankChatPreviewItem: ListViewItem, ItemListItem, ListItemComp maskSide: Bool = false ) { self.context = context + self.systemStyle = systemStyle self.theme = theme self.componentTheme = componentTheme self.strings = strings @@ -430,7 +433,7 @@ final class RankChatPreviewItemNode: ListViewItemNode { strongSelf.bottomStripeNode.isHidden = hasCorners } - strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.componentTheme, top: hasTopCorners, bottom: hasBottomCorners) : nil + strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.componentTheme, top: hasTopCorners, bottom: hasBottomCorners, glass: item.systemStyle == .glass) : nil strongSelf.topStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: layoutSize.width, height: separatorHeight)) strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height + bottomStripeOffset), size: CGSize(width: layoutSize.width - bottomStripeInset, height: separatorHeight)) diff --git a/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/QuickReplyEmptyStateComponent.swift b/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/QuickReplyEmptyStateComponent.swift index cfd8eb14a2..e1a0402ec4 100644 --- a/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/QuickReplyEmptyStateComponent.swift +++ b/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/QuickReplyEmptyStateComponent.swift @@ -68,6 +68,7 @@ final class QuickReplyEmptyStateComponent: Component { transition: transition, component: AnyComponent(ButtonComponent( background: ButtonComponent.Background( + style: .glass, color: component.theme.list.itemCheckColors.fillColor, foreground: component.theme.list.itemCheckColors.foregroundColor, pressedColor: component.theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.9) @@ -92,7 +93,7 @@ final class QuickReplyEmptyStateComponent: Component { } )), environment: {}, - containerSize: CGSize(width: min(availableSize.width - 16.0 * 2.0, 280.0), height: 50.0) + containerSize: CGSize(width: min(availableSize.width - 16.0 * 2.0, 280.0), height: 52.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 { diff --git a/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/QuickReplySetupScreen.swift b/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/QuickReplySetupScreen.swift index 53ac0957a2..ea2f1620ee 100644 --- a/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/QuickReplySetupScreen.swift +++ b/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/QuickReplySetupScreen.swift @@ -95,6 +95,7 @@ final class QuickReplySetupScreenComponent: Component { case .add: return ItemListPeerActionItem( presentationData: ItemListPresentationData(listNode.presentationData), + systemStyle: .glass, icon: PresentationResourcesItemList.plusIconImage(listNode.presentationData.theme), iconSignal: nil, title: listNode.presentationData.strings.QuickReply_InlineCreateAction, @@ -600,7 +601,7 @@ final class QuickReplySetupScreenComponent: Component { } } else { var completion: ((String?) -> Void)? - let alertController = quickReplyNameAlertController( + let (alertController, displayError) = quickReplyNameAlertController( context: component.context, text: environment.strings.QuickReply_CreateShortcutTitle, subtext: environment.strings.QuickReply_CreateShortcutText, @@ -612,24 +613,21 @@ final class QuickReplySetupScreenComponent: Component { ) completion = { [weak self, weak alertController] value in guard let self, let environment = self.environment else { - alertController?.dismissAnimated() + alertController?.dismiss(animated: true, completion: nil) return } if let value, !value.isEmpty { guard let shortcutMessageList = self.shortcutMessageList else { - alertController?.dismissAnimated() + alertController?.dismiss(animated: true, completion: nil) 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) - } + displayError(environment.strings.QuickReply_ShortcutExistsInlineError) return } - alertController?.view.endEditing(true) - alertController?.dismissAnimated() + alertController?.dismiss(animated: true, completion: nil) self.openQuickReplyChat(shortcut: value, shortcutId: nil) } } @@ -643,7 +641,7 @@ final class QuickReplySetupScreenComponent: Component { } var completion: ((String?) -> Void)? - let alertController = quickReplyNameAlertController( + let (alertController, displayError) = quickReplyNameAlertController( context: component.context, text: environment.strings.QuickReply_EditShortcutTitle, subtext: environment.strings.QuickReply_EditShortcutText, @@ -655,28 +653,25 @@ final class QuickReplySetupScreenComponent: Component { ) completion = { [weak self, weak alertController] value in guard let self, let component = self.component, let environment = self.environment else { - alertController?.dismissAnimated() + alertController?.dismiss(animated: true, completion: nil) return } if let value, !value.isEmpty { if value == currentValue { - alertController?.dismissAnimated() + alertController?.dismiss(animated: true, completion: nil) return } guard let shortcutMessageList = self.shortcutMessageList else { - alertController?.dismissAnimated() + alertController?.dismiss(animated: true, completion: nil) 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) - } + displayError(environment.strings.QuickReply_ShortcutExistsInlineError) } else { component.context.engine.accountData.editMessageShortcut(id: id, shortcut: value) - alertController?.view.endEditing(true) - alertController?.dismissAnimated() + alertController?.dismiss(animated: true, completion: nil) } } } diff --git a/submodules/TelegramUI/Components/Settings/QuickReplyNameAlertController/Sources/QuickReplyNameAlertController.swift b/submodules/TelegramUI/Components/Settings/QuickReplyNameAlertController/Sources/QuickReplyNameAlertController.swift index 7966d579a2..b78cc739f1 100644 --- a/submodules/TelegramUI/Components/Settings/QuickReplyNameAlertController/Sources/QuickReplyNameAlertController.swift +++ b/submodules/TelegramUI/Components/Settings/QuickReplyNameAlertController/Sources/QuickReplyNameAlertController.swift @@ -8,632 +8,96 @@ import TelegramCore import TelegramPresentationData import AccountContext import ComponentFlow -import MultilineTextComponent -import BalancedTextComponent -import EmojiStatusComponent import AlertComponent import AlertInputFieldComponent -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() +public func quickReplyNameAlertController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, text: String, subtext: String, value: String?, characterLimit: Int, apply: @escaping (String?) -> Void) -> (controller: AlertScreen, displayError: (String) -> Void) { + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + let strings = presentationData.strings - private let characterLimit: Int + let inputState = AlertInputFieldComponent.ExternalState() - 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 = context.sharedContext.currentPresentationData.with { $0 } -// let strings = presentationData.strings -// -// let inputState = AlertInputFieldComponent.ExternalState() -// -// let doneIsEnabled: Signal = inputState.valueSignal -// |> map { value in -// return !value.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty -// } -// -// let doneInProgressValuePromise = ValuePromise(false) -// let doneInProgress = doneInProgressValuePromise.get() -// -// var content: [AnyComponentWithIdentity] = [] -// content.append(AnyComponentWithIdentity( -// id: "title", -// component: AnyComponent( -// AlertTitleComponent(title: strings.WebBrowser_Exceptions_Create_Title) -// ) -// )) -// content.append(AnyComponentWithIdentity( -// id: "text", -// component: AnyComponent( -// AlertTextComponent(content: .plain(strings.WebBrowser_Exceptions_Create_Text)) -// ) -// )) -// -// let domainRegex = try? NSRegularExpression(pattern: "^(https?://)?([a-zA-Z0-9-]+\\.?)*([a-zA-Z]*)?(:)?(/)?$", options: []) -// let pathRegex = try? NSRegularExpression(pattern: "^(https?://)?([a-zA-Z0-9-]+\\.)+[a-zA-Z]{2,6}/", options: []) -// var applyImpl: (() -> Void)? -// content.append(AnyComponentWithIdentity( -// id: "input", -// component: AnyComponent( -// AlertInputFieldComponent( -// context: context, -// initialValue: nil, -// placeholder: strings.QuickReply_ShortcutPlaceholder, -// characterLimit: characterLimit, -// hasClearButton: false, -// keyboardType: .URL, -// autocapitalizationType: .none, -// autocorrectionType: .no, -// isInitiallyFocused: true, -// externalState: inputState, -// shouldChangeText: { updatedText in -// guard let domainRegex, let pathRegex else { -// return true -// } -// let domainMatches = domainRegex.matches(in: updatedText, options: [], range: NSRange(location: 0, length: updatedText.utf16.count)) -// let pathMatches = pathRegex.matches(in: updatedText, options: [], range: NSRange(location: 0, length: updatedText.utf16.count)) -// if domainMatches.count > 0, pathMatches.count == 0 { -// return true -// } else { -// return false -// } -// }, -// returnKeyAction: { -// applyImpl?() -// } -// ) -// ) -// )) -// -// var effectiveUpdatedPresentationData: (PresentationData, Signal) -// if let updatedPresentationData { -// effectiveUpdatedPresentationData = updatedPresentationData -// } else { -// effectiveUpdatedPresentationData = (presentationData, context.sharedContext.presentationData) -// } -// -// let alertController = AlertScreen( -// configuration: AlertScreen.Configuration(allowInputInset: true), -// content: content, -// actions: [ -// .init(title: strings.Common_Cancel, action: { -// apply(nil) -// }), -// .init(title: strings.Common_Done, type: .default, action: { -// applyImpl?() -// }, autoDismiss: false, isEnabled: doneIsEnabled, progress: doneInProgress) -// ], -// updatedPresentationData: effectiveUpdatedPresentationData -// ) -// applyImpl = { -// let updatedLink = explicitUrl(inputState.value) -// if !updatedLink.isEmpty && isValidUrl(updatedLink, validSchemes: ["http": true, "https": true]) { -// doneInProgressValuePromise.set(true) -// apply(updatedLink) -// } else { -// inputState.animateError() -// } -// } -// return 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() + let errorPromise = ValuePromise(nil) + let contentSignal = errorPromise.get() + |> map { error in + var content: [AnyComponentWithIdentity] = [] + content.append(AnyComponentWithIdentity( + id: "title", + component: AnyComponent( + AlertTitleComponent(title: text) + ) + )) + if let error { + content.append(AnyComponentWithIdentity( + id: "text", + component: AnyComponent( + AlertTextComponent(content: .plain(error), color: .destructive) + ) + )) } else { - controller?.dismiss() + content.append(AnyComponentWithIdentity( + id: "text", + component: AnyComponent( + AlertTextComponent(content: .plain(subtext)) + ) + )) } + + content.append(AnyComponentWithIdentity( + id: "input", + component: AnyComponent( + AlertInputFieldComponent( + context: context, + initialValue: nil, + placeholder: strings.QuickReply_ShortcutPlaceholder, + characterLimit: characterLimit, + hasClearButton: false, + keyboardType: .URL, + autocapitalizationType: .none, + autocorrectionType: .no, + isInitiallyFocused: true, + externalState: inputState, + returnKeyAction: { + applyImpl?() + } + ) + ) + )) + + return content } - return controller + + var effectiveUpdatedPresentationData: (PresentationData, Signal) + if let updatedPresentationData { + effectiveUpdatedPresentationData = updatedPresentationData + } else { + effectiveUpdatedPresentationData = (presentationData, context.sharedContext.presentationData) + } + + let alertController = AlertScreen( + configuration: AlertScreen.Configuration(allowInputInset: true), + contentSignal: contentSignal, + actionsSignal: .single([ + .init(title: strings.Common_Cancel, action: { + apply(nil) + }), + .init(title: strings.Common_Done, type: .default, action: { + applyImpl?() + }, autoDismiss: false) + ]), + updatedPresentationData: effectiveUpdatedPresentationData + ) + applyImpl = { + apply(inputState.value) + } + + let displayError = { [weak inputState] error in + errorPromise.set(error) + inputState?.animateError() + HapticFeedback().error() + } + + return (alertController, displayError) } diff --git a/submodules/TelegramUI/Sources/AppDelegate.swift b/submodules/TelegramUI/Sources/AppDelegate.swift index 0f72902014..99c147a9e5 100644 --- a/submodules/TelegramUI/Sources/AppDelegate.swift +++ b/submodules/TelegramUI/Sources/AppDelegate.swift @@ -2522,7 +2522,7 @@ private func extractAccountManagerState(records: AccountRecordsView Void)? - let alertController = quickReplyNameAlertController( + let (alertController, displayError) = quickReplyNameAlertController( context: self.context, text: self.presentationData.strings.QuickReply_EditShortcutTitle, subtext: self.presentationData.strings.QuickReply_EditShortcutText, @@ -27,12 +27,12 @@ extension ChatControllerImpl { ) completion = { [weak self, weak alertController] value in guard let self else { - alertController?.dismissAnimated() + alertController?.dismiss(animated: true, completion: nil) return } if let value, !value.isEmpty { if value == currentValue { - alertController?.dismissAnimated() + alertController?.dismiss(animated: true, completion: nil) return } @@ -40,14 +40,12 @@ extension ChatControllerImpl { |> take(1) |> deliverOnMainQueue).start(next: { [weak self] shortcutMessageList in guard let self else { - alertController?.dismissAnimated() + alertController?.dismiss(animated: true, completion: nil) 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) - } + displayError(self.presentationData.strings.QuickReply_ShortcutExistsInlineError) } else { let _ = self.chatTitleView?.update( context: self.context, @@ -61,8 +59,7 @@ extension ChatControllerImpl { transition: .immediate ) - alertController?.view.endEditing(true) - alertController?.dismissAnimated() + alertController?.dismiss(animated: true, completion: nil) if case let .customChatContents(customChatContents) = self.subject { customChatContents.quickReplyUpdateShortcut(value: value) diff --git a/submodules/TelegramUI/Sources/ChatControllerOpenAttachmentMenu.swift b/submodules/TelegramUI/Sources/ChatControllerOpenAttachmentMenu.swift index 1b343f3c81..8127513a4b 100644 --- a/submodules/TelegramUI/Sources/ChatControllerOpenAttachmentMenu.swift +++ b/submodules/TelegramUI/Sources/ChatControllerOpenAttachmentMenu.swift @@ -204,7 +204,7 @@ extension ChatControllerImpl { allButtons.insert(button, at: 1) } - if !"".isEmpty, let user = peer as? TelegramUser, user.botInfo == nil { + if let user = peer as? TelegramUser, user.botInfo == nil { if let index = buttons.firstIndex(where: { $0 == .location }) { buttons.insert(.quickReply, at: index + 1) } else { diff --git a/submodules/TelegramUI/Sources/OpenResolvedUrl.swift b/submodules/TelegramUI/Sources/OpenResolvedUrl.swift index 3d21604230..9dfbb61575 100644 --- a/submodules/TelegramUI/Sources/OpenResolvedUrl.swift +++ b/submodules/TelegramUI/Sources/OpenResolvedUrl.swift @@ -91,7 +91,7 @@ func openResolvedUrlImpl( case let .externalUrl(url): context.sharedContext.openExternalUrl(context: context, urlContext: urlContext, url: url, forceExternal: forceExternal, presentationData: context.sharedContext.currentPresentationData.with { $0 }, navigationController: navigationController, dismissInput: dismissInput) case let .urlAuth(url): - requestMessageActionUrlAuth?(.url(url)) + requestMessageActionUrlAuth?(.url(url: url, inAppOrigin: nil)) dismissInput() break case let .peer(peer, navigation): @@ -1816,7 +1816,8 @@ func openResolvedUrlImpl( present(alertController, nil) }) case let .oauth(url): - let _ = (context.engine.messages.requestMessageActionUrlAuth(subject: .url(url)) + let subject: MessageActionUrlSubject = .url(url: url, inAppOrigin: nil) + let _ = (context.engine.messages.requestMessageActionUrlAuth(subject: subject) |> deliverOnMainQueue).start(next: { result in if case .request = result { var dismissImpl: (() -> Void)? @@ -1825,13 +1826,13 @@ func openResolvedUrlImpl( case let .accept(allowWriteAccess, sharePhoneNumber): let signal: Signal if accountContext === context { - signal = accountContext.engine.messages.acceptMessageActionUrlAuth(subject: .url(url), allowWriteAccess: allowWriteAccess, sharePhoneNumber: sharePhoneNumber) + signal = accountContext.engine.messages.acceptMessageActionUrlAuth(subject: subject, allowWriteAccess: allowWriteAccess, sharePhoneNumber: sharePhoneNumber) } else { accountContext.account.shouldBeServiceTaskMaster.set(.single(.now)) - signal = accountContext.engine.messages.requestMessageActionUrlAuth(subject: .url(url)) + signal = accountContext.engine.messages.requestMessageActionUrlAuth(subject: subject) |> castError(MessageActionUrlAuthError.self) |> mapToSignal { result in - return accountContext.engine.messages.acceptMessageActionUrlAuth(subject: .url(url), allowWriteAccess: allowWriteAccess, sharePhoneNumber: sharePhoneNumber) + return accountContext.engine.messages.acceptMessageActionUrlAuth(subject: subject, allowWriteAccess: allowWriteAccess, sharePhoneNumber: sharePhoneNumber) } |> afterDisposed { accountContext.account.shouldBeServiceTaskMaster.set(.single(.never)) } diff --git a/submodules/TelegramUI/Sources/OpenUrl.swift b/submodules/TelegramUI/Sources/OpenUrl.swift index e2c63e3479..6b42e1a50a 100644 --- a/submodules/TelegramUI/Sources/OpenUrl.swift +++ b/submodules/TelegramUI/Sources/OpenUrl.swift @@ -504,7 +504,12 @@ func openExternalUrlImpl(context: AccountContext, urlContext: OpenURLContext, ur } case "passport", "oauth", "resolve": if isOAuthUrl(parsedUrl) { - handleResolvedUrl(.oauth(url: url)) + switch urlContext { + case .external: + handleResolvedUrl(.oauth(url: url)) + default: + break + } return } else if let secureId = parseSecureIdUrl(parsedUrl) { if case .chat = urlContext { diff --git a/submodules/TelegramUI/Sources/SharedAccountContext.swift b/submodules/TelegramUI/Sources/SharedAccountContext.swift index 171a481373..9d051c4b47 100644 --- a/submodules/TelegramUI/Sources/SharedAccountContext.swift +++ b/submodules/TelegramUI/Sources/SharedAccountContext.swift @@ -99,6 +99,7 @@ import GiftCraftScreen import ChatParticipantRightsScreen import PeerCopyProtectionInfoScreen import ChatRankInfoScreen +import RankChatPreviewItem private final class AccountUserInterfaceInUseContext { let subscribers = Bag<(Bool) -> Void>() @@ -4331,6 +4332,18 @@ public final class SharedAccountContextImpl: SharedAccountContext { public func makeChatRankInfoScreen(context: AccountContext, chatPeer: EnginePeer, userPeer: EnginePeer, role: ChatRankInfoScreenRole, rank: String, canChange: Bool, completion: @escaping () -> Void) -> ViewController { return ChatRankInfoScreen(context: context, chatPeer: chatPeer, userPeer: userPeer, role: role, rank: rank, canChange: canChange, completion: completion) } + + public func makeChatRankPreviewItem(context: AccountContext, peer: EnginePeer, rank: String, rankRole: ChatRankInfoScreenRole, theme: PresentationTheme, strings: PresentationStrings, wallpaper: TelegramWallpaper, fontSize: PresentationFontSize, chatBubbleCorners: PresentationChatBubbleCorners, dateTimeFormat: PresentationDateTimeFormat, nameOrder: PresentationPersonNameOrder, sectionId: Int32) -> ListViewItem { + let messageItem = RankChatPreviewItem.MessageItem( + peer: peer, + text: "Reinhardt, we need to find you some new tunes.", + entities: nil, + media: [], + rank: rank, + rankRole: rankRole + ) + return RankChatPreviewItem(context: context, systemStyle: .glass, theme: theme, componentTheme: theme, strings: strings, sectionId: sectionId, fontSize: fontSize, chatBubbleCorners: chatBubbleCorners, wallpaper: wallpaper, dateTimeFormat: dateTimeFormat, nameDisplayOrder: nameOrder, messageItems: [messageItem]) + } } private func peerInfoControllerImpl(context: AccountContext, updatedPresentationData: (PresentationData, Signal)?, peer: Peer, mode: PeerInfoControllerMode, avatarInitiallyExpanded: Bool, isOpenedFromChat: Bool, requestsContext: PeerInvitationImportersContext? = nil) -> ViewController? {