Various improvements

This commit is contained in:
Ilya Laktyushin 2026-02-23 13:51:13 +04:00
parent 5018fd7c65
commit 1d80861e09
20 changed files with 269 additions and 733 deletions

View file

@ -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";

View file

@ -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<PresentationData, NoError>)?)
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<Bool> { get }
var immediateHasOngoingCall: Bool { get }

View file

@ -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
}

View file

@ -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)

View file

@ -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! {

View file

@ -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<MessageActionUrlAuthResult, NoError> {
let request: Signal<Api.UrlAuthResult?, MTRpcError>
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<Api.UrlAuthResult?, MTRpcError> 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<Api.UrlAuthResult?, MTRpcError> 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)

View file

@ -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": [],

View file

@ -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

View file

@ -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))

View file

@ -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 {

View file

@ -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)
}
}
}

View file

@ -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<Empty>()
public func quickReplyNameAlertController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = 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<Empty>()
private let subtextView = ComponentView<Empty>()
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<PresentationData, NoError>)? = 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<Bool, NoError> = inputState.valueSignal
// |> map { value in
// return !value.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
// }
//
// let doneInProgressValuePromise = ValuePromise<Bool>(false)
// let doneInProgress = doneInProgressValuePromise.get()
//
// var content: [AnyComponentWithIdentity<AlertComponentEnvironment>] = []
// 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<PresentationData, NoError>)
// 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<String?>(nil)
let contentSignal = errorPromise.get()
|> map { error in
var content: [AnyComponentWithIdentity<AlertComponentEnvironment>] = []
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<PresentationData, NoError>)
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)
}

View file

@ -2522,7 +2522,7 @@ private func extractAccountManagerState(records: AccountRecordsView<TelegramAcco
if let authContext = authContext, let confirmationCode = parseConfirmationCodeUrl(sharedContext: sharedContext, url: url) {
authContext.rootController.applyConfirmationCode(confirmationCode)
} else if let context = context {
context.openUrl(url)
context.openUrl(url, external: true)
} else if let authContext = authContext {
if let proxyData = parseProxyUrl(sharedContext: sharedContext, url: url) {
authContext.rootController.view.endEditing(true)

View file

@ -923,10 +923,10 @@ final class AuthorizedApplicationContext {
}
}
func openUrl(_ url: URL) {
func openUrl(_ url: URL, external: Bool = false) {
if self.rootController.rootTabController != nil {
let presentationData = self.context.sharedContext.currentPresentationData.with { $0 }
self.context.sharedContext.openExternalUrl(context: self.context, urlContext: .generic, url: url.absoluteString, forceExternal: false, presentationData: presentationData, navigationController: self.rootController, dismissInput: { [weak self] in
self.context.sharedContext.openExternalUrl(context: self.context, urlContext: external ? .external : .generic, url: url.absoluteString, forceExternal: false, presentationData: presentationData, navigationController: self.rootController, dismissInput: { [weak self] in
self?.rootController.view.endEditing(true)
})
} else {

View file

@ -9089,7 +9089,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
self.controllerInteraction?.sendEmoji(text, attribute, false)
},
requestMessageActionUrlAuth: { [weak self] subject in
if case let .url(url) = subject {
if case let .url(url, _) = subject {
self?.controllerInteraction?.requestMessageActionUrlAuth(url, subject)
}
}, joinVoiceChat: { [weak self] peerId, invite, call in

View file

@ -15,7 +15,7 @@ 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(
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)

View file

@ -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 {

View file

@ -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<MessageActionUrlAuthResult, MessageActionUrlAuthError>
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))
}

View file

@ -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 {

View file

@ -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<PresentationData, NoError>)?, peer: Peer, mode: PeerInfoControllerMode, avatarInitiallyExpanded: Bool, isOpenedFromChat: Bool, requestsContext: PeerInvitationImportersContext? = nil) -> ViewController? {