Various fixes

This commit is contained in:
Ilya Laktyushin 2026-04-17 11:06:56 +02:00
parent 532a3ae3e1
commit 1fa0c7991c
22 changed files with 401 additions and 195 deletions

View file

@ -225,6 +225,7 @@ public struct ResolvedBotAdminRights: OptionSet {
public static let manageVideoChats = ResolvedBotAdminRights(rawValue: 512)
public static let canBeAnonymous = ResolvedBotAdminRights(rawValue: 1024)
public static let manageChat = ResolvedBotAdminRights(rawValue: 2048)
public static let manageTopics = ResolvedBotAdminRights(rawValue: 4096)
public var chatAdminRights: TelegramChatAdminRightsFlags? {
var flags = TelegramChatAdminRightsFlags()
@ -259,6 +260,9 @@ public struct ResolvedBotAdminRights: OptionSet {
if self.contains(ResolvedBotAdminRights.canBeAnonymous) {
flags.insert(.canBeAnonymous)
}
if self.contains(ResolvedBotAdminRights.manageTopics) {
flags.insert(.canManageTopics)
}
if flags.isEmpty && !self.contains(ResolvedBotAdminRights.manageChat) {
return nil

View file

@ -1103,6 +1103,27 @@ public final class PagerComponent<ChildEnvironmentType: Equatable, TopPanelEnvir
self.isTopPanelExpandedUpdated(isExpanded: false, transition: ComponentTransition(animation: .curve(duration: 0.4, curve: .spring)))
}
public func revealHiddenPanels() {
guard let component = self.component else {
return
}
guard case .hideOnScroll = component.panelHideBehavior else {
return
}
guard let centralId = self.centralId, let contentView = self.contentViews[centralId] else {
return
}
guard !contentView.wantsExclusiveMode else {
return
}
guard contentView.scrollingPanelOffsetFraction != 0.0 else {
return
}
contentView.scrollingPanelOffsetFraction = 0.0
self.state?.updated(transition: ComponentTransition(animation: .curve(duration: 0.25, curve: .easeInOut)))
}
}
public func makeView() -> View {

View file

@ -437,7 +437,7 @@ public class ContactsController: ViewController {
strongSelf.contactsNode.contactListNode.listNode.clearHighlightAnimated(true)
default:
let presentationData = strongSelf.presentationData
strongSelf.present(textAlertController(context: strongSelf.context, title: presentationData.strings.AccessDenied_Title, text: presentationData.strings.Contacts_AccessDeniedError, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_NotNow, action: {}), TextAlertAction(type: .genericAction, title: presentationData.strings.AccessDenied_Settings, action: {
strongSelf.present(textAlertController(context: strongSelf.context, title: presentationData.strings.AccessDenied_Title, text: presentationData.strings.Contacts_AccessDeniedError, actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_NotNow, action: {}), TextAlertAction(type: .defaultAction, title: presentationData.strings.AccessDenied_Settings, action: {
self?.context.sharedContext.applicationBindings.openSettings()
})]), in: .window(.root))
strongSelf.contactsNode.contactListNode.listNode.clearHighlightAnimated(true)
@ -787,7 +787,7 @@ public class ContactsController: ViewController {
default:
let presentationData = strongSelf.presentationData
if let navigationController = strongSelf.context.sharedContext.mainWindow?.viewController as? NavigationController, let topController = navigationController.topViewController as? ViewController {
topController.present(textAlertController(context: strongSelf.context, title: presentationData.strings.AccessDenied_Title, text: presentationData.strings.Contacts_AccessDeniedError, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_NotNow, action: {}), TextAlertAction(type: .genericAction, title: presentationData.strings.AccessDenied_Settings, action: {
topController.present(textAlertController(context: strongSelf.context, title: presentationData.strings.AccessDenied_Title, text: presentationData.strings.Contacts_AccessDeniedError, actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_NotNow, action: {}), TextAlertAction(type: .defaultAction, title: presentationData.strings.AccessDenied_Settings, action: {
self?.context.sharedContext.applicationBindings.openSettings()
})]), in: .window(.root))
}

View file

@ -336,7 +336,7 @@ public final class DeviceAccess {
case .ageVerification:
text = presentationData.strings.AccessDenied_AgeVerificationCamera
}
present(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: presentationData.strings.AccessDenied_Title, text: text, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_NotNow, action: {}), TextAlertAction(type: .genericAction, title: presentationData.strings.AccessDenied_Settings, action: {
present(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: presentationData.strings.AccessDenied_Title, text: text, actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_NotNow, action: {}), TextAlertAction(type: .defaultAction, title: presentationData.strings.AccessDenied_Settings, action: {
openSettings()
})]), nil)
}
@ -363,7 +363,7 @@ public final class DeviceAccess {
text = presentationData.strings.AccessDenied_AgeVerificationCamera
}
}
present(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: presentationData.strings.AccessDenied_Title, text: text, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_NotNow, action: {}), TextAlertAction(type: .genericAction, title: presentationData.strings.AccessDenied_Settings, action: {
present(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: presentationData.strings.AccessDenied_Title, text: text, actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_NotNow, action: {}), TextAlertAction(type: .defaultAction, title: presentationData.strings.AccessDenied_Settings, action: {
openSettings()
})]), nil)
}
@ -392,7 +392,7 @@ public final class DeviceAccess {
case .voiceCall:
text = presentationData.strings.AccessDenied_CallMicrophone
}
present(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: presentationData.strings.AccessDenied_Title, text: text, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_NotNow, action: {}), TextAlertAction(type: .genericAction, title: presentationData.strings.AccessDenied_Settings, action: {
present(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: presentationData.strings.AccessDenied_Title, text: text, actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_NotNow, action: {}), TextAlertAction(type: .defaultAction, title: presentationData.strings.AccessDenied_Settings, action: {
openSettings()
})]), nil)
if case .voiceCall = microphoneSubject {
@ -421,7 +421,7 @@ public final class DeviceAccess {
case .qrCode:
text = presentationData.strings.AccessDenied_QrCode
}
present(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: presentationData.strings.AccessDenied_Title, text: text, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_NotNow, action: {}), TextAlertAction(type: .genericAction, title: presentationData.strings.AccessDenied_Settings, action: {
present(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: presentationData.strings.AccessDenied_Title, text: text, actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_NotNow, action: {}), TextAlertAction(type: .defaultAction, title: presentationData.strings.AccessDenied_Settings, action: {
openSettings()
})]), nil)
}
@ -462,7 +462,7 @@ public final class DeviceAccess {
completion(false)
if let presentationData = presentationData {
let text = presentationData.strings.AccessDenied_LocationPreciseDenied
present(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: presentationData.strings.AccessDenied_Title, text: text, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_NotNow, action: {}), TextAlertAction(type: .genericAction, title: presentationData.strings.AccessDenied_Settings, action: {
present(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: presentationData.strings.AccessDenied_Title, text: text, actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_NotNow, action: {}), TextAlertAction(type: .defaultAction, title: presentationData.strings.AccessDenied_Settings, action: {
openSettings()
})]), nil)
}
@ -477,7 +477,7 @@ public final class DeviceAccess {
completion(false)
if let presentationData = presentationData {
let text = presentationData.strings.AccessDenied_LocationAlwaysDenied
present(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: presentationData.strings.AccessDenied_Title, text: text, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_NotNow, action: {}), TextAlertAction(type: .genericAction, title: presentationData.strings.AccessDenied_Settings, action: {
present(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: presentationData.strings.AccessDenied_Title, text: text, actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_NotNow, action: {}), TextAlertAction(type: .defaultAction, title: presentationData.strings.AccessDenied_Settings, action: {
openSettings()
})]), nil)
}
@ -498,7 +498,7 @@ public final class DeviceAccess {
} else {
text = presentationData.strings.AccessDenied_LocationDisabled
}
present(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: presentationData.strings.AccessDenied_Title, text: text, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_NotNow, action: {}), TextAlertAction(type: .genericAction, title: presentationData.strings.AccessDenied_Settings, action: {
present(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: presentationData.strings.AccessDenied_Title, text: text, actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_NotNow, action: {}), TextAlertAction(type: .defaultAction, title: presentationData.strings.AccessDenied_Settings, action: {
openSettings()
})]), nil)
}
@ -558,7 +558,7 @@ public final class DeviceAccess {
}
case .cellularData:
if let presentationData = presentationData {
present(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: presentationData.strings.Permissions_CellularDataTitle_v0, text: presentationData.strings.Permissions_CellularDataText_v0, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_NotNow, action: {}), TextAlertAction(type: .genericAction, title: presentationData.strings.AccessDenied_Settings, action: {
present(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: presentationData.strings.Permissions_CellularDataTitle_v0, text: presentationData.strings.Permissions_CellularDataText_v0, actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_NotNow, action: {}), TextAlertAction(type: .defaultAction, title: presentationData.strings.AccessDenied_Settings, action: {
openSettings()
})]), nil)
}

View file

@ -3923,21 +3923,35 @@ public func avatarMediaPickerController(
final class PickerDelegate: NSObject, PHPickerViewControllerDelegate {
var completion: ((Any?, UIView?, CGRect, UIImage?, Bool, @escaping (Bool?) -> (UIView, CGRect)?, @escaping () -> Void) -> Void)?
func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
picker.dismiss(animated: true)
for item in results {
if item.itemProvider.canLoadObject(ofClass: UIImage.self) {
item.itemProvider.loadObject(ofClass: UIImage.self) { image, error in
if let uiImage = image as? UIImage {
Queue.mainQueue().async {
self.completion?(uiImage, nil, CGRect(), nil, false, { _ in return nil }, {})
}
private func resolveResult(_ result: PHPickerResult) {
if let assetIdentifier = result.assetIdentifier {
let fetchResult = PHAsset.fetchAssets(withLocalIdentifiers: [assetIdentifier], options: nil)
if let asset = fetchResult.firstObject {
Queue.mainQueue().async {
self.completion?(asset, nil, CGRect(), nil, false, { _ in return nil }, {})
}
return
}
}
if result.itemProvider.canLoadObject(ofClass: UIImage.self) {
result.itemProvider.loadObject(ofClass: UIImage.self) { [weak self] image, _ in
if let uiImage = image as? UIImage {
Queue.mainQueue().async {
self?.completion?(uiImage, nil, CGRect(), nil, false, { _ in return nil }, {})
}
}
}
}
}
func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
picker.dismiss(animated: true)
if let result = results.first {
self.resolveResult(result)
}
}
}
let holder = PickerDelegate()
@ -3945,7 +3959,7 @@ public func avatarMediaPickerController(
let openMediaPicker = {
var configuration = PHPickerConfiguration(photoLibrary: .shared())
configuration.filter = .images
configuration.filter = .any(of: [.images, .videos])
configuration.selectionLimit = 1
let picker = PHPickerViewController(configuration: configuration)

View file

@ -673,6 +673,7 @@ private func channelAdminControllerEntries(presentationData: PresentationData, s
.direct(.canManageRanks),
.direct(.canPinMessages),
.direct(.canManageTopics),
.sub(.stories, storiesRelatedFlags),
.direct(.canManageCalls),
.direct(.canBeAnonymous),
.direct(.canAddAdmins)
@ -1553,6 +1554,7 @@ public func channelAdminController(context: AccountContext, updatedPresentationD
dismissImpl?()
}, completed: {
updated(TelegramChatAdminRights(rights: updateFlags))
dismissImpl?()
}))
} else if updateFlags != defaultFlags || updateRank != nil {

View file

@ -478,6 +478,7 @@ public func sentShareItems(accountPeerId: PeerId, postbox: Postbox, network: Net
var mediaMessageCount = 0
var consumedText = false
var captionAssigned = false
for item in items {
switch item {
case let .text(text):
@ -493,11 +494,22 @@ public func sentShareItems(accountPeerId: PeerId, postbox: Postbox, network: Net
case let .media(media):
switch media {
case let .media(reference):
let captionText: String
if !captionAssigned {
if let file = reference.media as? TelegramMediaFile, file.isInstantVideo {
captionText = ""
} else {
captionText = additionalText
captionAssigned = true
}
} else {
captionText = ""
}
var message = StandaloneSendEnqueueMessage(
content: .arbitraryMedia(
media: reference,
text: StandaloneSendEnqueueMessage.Text(
string: additionalText,
string: captionText,
entities: []
)
),

View file

@ -864,7 +864,7 @@ func enqueueMessages(transaction: Transaction, account: Account, peerId: PeerId,
}
}
if threadId == nil, let peer = transaction.getPeer(peerId), (peer is TelegramChannel), peer.isForum {
if threadId == nil, let peer = transaction.getPeer(peerId), (peer is TelegramChannel), peer.isForum {
threadId = 1
}

View file

@ -217,6 +217,53 @@ public func standaloneSendEnqueueMessages(
}
}
if allDone {
if peerId.namespace == Namespaces.Peer.SecretChat {
return postbox.transaction { transaction -> Signal<Never, StandaloneSendMessagesError> in
var state = transaction.getPeerChatState(peerId) as? SecretChatState
for (content, media, attributes) in allResults {
var text: String = ""
switch content.content {
case let .text(textValue):
text = textValue
case let .media(_, textValue):
text = textValue
default:
break
}
if let currentState = state, let updatedState = enqueueSecretChatUploadedMessageContent(
transaction: transaction,
peerId: peerId,
state: currentState,
content: content,
text: text,
attributes: attributes,
media: media
) {
state = updatedState
} else {
return .fail(StandaloneSendMessagesError(peerId: peerId, reason: .none))
}
}
return managedSecretChatOutgoingOperations(
auxiliaryMethods: auxiliaryMethods,
postbox: postbox,
network: network,
accountPeerId: accountPeerId,
mode: .standaloneComplete(peerId: peerId)
)
|> castError(StandaloneSendMessagesError.self)
|> ignoreValues
}
|> castError(StandaloneSendMessagesError.self)
|> switchToLatest
|> map { _ -> StandaloneSendMessageStatus in
}
|> then(.single(.done))
}
var sendSignals: [Signal<Never, StandaloneSendMessagesError>] = []
for (content, media, attributes) in allResults {
@ -231,11 +278,10 @@ public func standaloneSendEnqueueMessages(
}
sendSignals.append(sendUploadedMessageContent(
auxiliaryMethods: auxiliaryMethods,
postbox: postbox,
network: network,
stateManager: stateManager,
accountPeerId: stateManager.accountPeerId,
accountPeerId: accountPeerId,
peerId: peerId,
content: content,
text: text,
@ -256,8 +302,52 @@ public func standaloneSendEnqueueMessages(
}
}
private func enqueueSecretChatUploadedMessageContent(
transaction: Transaction,
peerId: PeerId,
state: SecretChatState,
content: PendingMessageUploadedContentAndReuploadInfo,
text: String,
attributes: [MessageAttribute],
media: [Media]
) -> SecretChatState? {
var secretFile: SecretChatOutgoingFile?
switch content.content {
case let .secretMedia(file, size, key):
if let fileReference = SecretChatOutgoingFileReference(file) {
secretFile = SecretChatOutgoingFile(reference: fileReference, size: size, key: key)
}
default:
break
}
let layer: SecretChatLayer
switch state.embeddedState {
case .terminated, .handshake:
return nil
case .basicLayer:
layer = .layer8
case let .sequenceBasedLayer(sequenceState):
layer = sequenceState.layerNegotiationState.activeLayer.secretChatLayer
}
let messageContents = StandaloneSecretMessageContents(
id: Int64.random(in: Int64.min ... Int64.max),
text: text,
attributes: attributes,
media: media.first,
file: secretFile
)
let updatedState = addSecretChatOutgoingOperation(transaction: transaction, peerId: peerId, operation: .sendStandaloneMessage(layer: layer, contents: messageContents), state: state)
if updatedState != state {
transaction.setPeerChatState(peerId, state: updatedState)
}
return updatedState
}
private func sendUploadedMessageContent(
auxiliaryMethods: AccountAuxiliaryMethods,
postbox: Postbox,
network: Network,
stateManager: AccountStateManager,
@ -271,55 +361,7 @@ private func sendUploadedMessageContent(
) -> Signal<Never, StandaloneSendMessagesError> {
return postbox.transaction { transaction -> Signal<Never, StandaloneSendMessagesError> in
if peerId.namespace == Namespaces.Peer.SecretChat {
var secretFile: SecretChatOutgoingFile?
switch content.content {
case let .secretMedia(file, size, key):
if let fileReference = SecretChatOutgoingFileReference(file) {
secretFile = SecretChatOutgoingFile(reference: fileReference, size: size, key: key)
}
default:
break
}
var layer: SecretChatLayer?
let state = transaction.getPeerChatState(peerId) as? SecretChatState
if let state = state {
switch state.embeddedState {
case .terminated, .handshake:
break
case .basicLayer:
layer = .layer8
case let .sequenceBasedLayer(sequenceState):
layer = sequenceState.layerNegotiationState.activeLayer.secretChatLayer
}
}
if let state = state, let layer = layer {
let messageContents = StandaloneSecretMessageContents(
id: Int64.random(in: Int64.min ... Int64.max),
text: text,
attributes: attributes,
media: media.first,
file: secretFile
)
let updatedState = addSecretChatOutgoingOperation(transaction: transaction, peerId: peerId, operation: .sendStandaloneMessage(layer: layer, contents: messageContents), state: state)
if updatedState != state {
transaction.setPeerChatState(peerId, state: updatedState)
}
return managedSecretChatOutgoingOperations(
auxiliaryMethods: auxiliaryMethods,
postbox: postbox,
network: network,
accountPeerId: accountPeerId,
mode: .standaloneComplete(peerId: peerId)
)
|> castError(StandaloneSendMessagesError.self)
|> ignoreValues
} else {
return .fail(StandaloneSendMessagesError(peerId: peerId, reason: .none))
}
return .fail(StandaloneSendMessagesError(peerId: peerId, reason: .none))
} else if let peer = transaction.getPeer(peerId), let inputPeer = apiInputPeer(peer) {
var uniqueId: Int64 = 0
var forwardSourceInfoAttribute: ForwardSourceInfoAttribute?

View file

@ -1038,10 +1038,22 @@ public final class PendingMessageManager {
var flags: Int32 = 0
var topMsgId: Int32?
var monoforumPeerId: Api.InputPeer?
if let threadId = messages[0].0.threadId {
if let channel = peer as? TelegramChannel, channel.flags.contains(.isMonoforum) {
if let linkedMonoforumId = channel.linkedMonoforumId, let mainChannel = transaction.getPeer(linkedMonoforumId) as? TelegramChannel, mainChannel.hasPermission(.manageDirect) {
monoforumPeerId = transaction.getPeer(PeerId(threadId)).flatMap(apiInputPeer)
}
} else {
topMsgId = Int32(clamping: threadId)
}
}
for attribute in messages[0].0.attributes {
if let replyAttribute = attribute as? ReplyMessageAttribute {
replyMessageId = replyAttribute.messageId.id
if peerId != replyAttribute.messageId.peerId {
if peerId != replyAttribute.messageId.peerId || (replyAttribute.threadMessageId != nil && replyAttribute.threadMessageId?.id != topMsgId) {
replyPeerId = replyAttribute.messageId.peerId
}
if replyAttribute.isQuote {
@ -1134,19 +1146,10 @@ public final class PendingMessageManager {
}
}
var topMsgId: Int32?
var monoforumPeerId: Api.InputPeer?
if let threadId = messages[0].0.threadId {
if let channel = peer as? TelegramChannel, channel.flags.contains(.isMonoforum) {
if let linkedMonoforumId = channel.linkedMonoforumId, let mainChannel = transaction.getPeer(linkedMonoforumId) as? TelegramChannel, mainChannel.hasPermission(.manageDirect) {
monoforumPeerId = transaction.getPeer(PeerId(threadId)).flatMap(apiInputPeer)
}
} else {
flags |= Int32(1 << 9)
topMsgId = Int32(clamping: threadId)
}
if topMsgId != nil {
flags |= Int32(1 << 9)
}
var quickReplyShortcut: Api.InputQuickReplyShortcut?
if let quickReply {
if let threadId = messages[0].0.threadId {
@ -1233,20 +1236,7 @@ public final class PendingMessageManager {
if bubbleUpEmojiOrStickersets {
flags |= Int32(1 << 15)
}
var topMsgId: Int32?
var monoforumPeerId: Api.InputPeer?
if let threadId = messages[0].0.threadId {
if let channel = peer as? TelegramChannel, channel.flags.contains(.isMonoforum) {
if let linkedMonoforumId = channel.linkedMonoforumId, let mainChannel = transaction.getPeer(linkedMonoforumId) as? TelegramChannel, mainChannel.hasPermission(.manageDirect) {
monoforumPeerId = transaction.getPeer(PeerId(threadId)).flatMap(apiInputPeer)
}
} else {
flags |= Int32(1 << 9)
topMsgId = Int32(clamping: threadId)
}
}
var replyTo: Api.InputReplyTo?
if let replyMessageId = replyMessageId {
flags |= 1 << 0
@ -1571,7 +1561,7 @@ public final class PendingMessageManager {
for attribute in message.attributes {
if let replyAttribute = attribute as? ReplyMessageAttribute {
replyMessageId = replyAttribute.messageId.id
if peer.id != replyAttribute.messageId.peerId {
if peer.id != replyAttribute.messageId.peerId || (replyAttribute.threadMessageId != nil && replyAttribute.threadMessageId?.id != topMsgId) {
replyPeerId = replyAttribute.messageId.peerId
}
if replyAttribute.isQuote {
@ -1851,19 +1841,9 @@ public final class PendingMessageManager {
sendMessageRequest = network.request(Api.functions.messages.sendMedia(flags: flags, peer: inputPeer, replyTo: replyTo, media: inputMedia, message: text, randomId: uniqueId, replyMarkup: nil, entities: messageEntities, scheduleDate: scheduleTime, scheduleRepeatPeriod: scheduleRepeatPeriod, sendAs: sendAsInputPeer, quickReplyShortcut: quickReplyShortcut, effect: messageEffectId, allowPaidStars: allowPaidStars, suggestedPost: suggestedPost), tag: dependencyTag)
|> map(NetworkRequestResult.result)
case let .forward(sourceInfo):
var topMsgId: Int32?
var monoforumPeerId: Api.InputPeer?
if let threadId = message.threadId {
if let channel = peer as? TelegramChannel, channel.flags.contains(.isMonoforum) {
if let linkedMonoforumId = channel.linkedMonoforumId, let mainChannel = transaction.getPeer(linkedMonoforumId) as? TelegramChannel, mainChannel.hasPermission(.manageDirect) {
monoforumPeerId = transaction.getPeer(PeerId(threadId)).flatMap(apiInputPeer)
}
} else {
flags |= Int32(1 << 9)
topMsgId = Int32(clamping: threadId)
}
if topMsgId != nil {
flags |= Int32(1 << 9)
}
var quickReplyShortcut: Api.InputQuickReplyShortcut?
if let quickReply {
if let threadId = message.threadId {

View file

@ -1803,7 +1803,7 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
guard let textSelectionNode = self.textSelectionNode else {
return nil
}
guard let range = customRange ?? textSelectionNode.getSelection() else {
guard let rawRange = customRange ?? textSelectionNode.getSelection() else {
return nil
}
guard let item = self.item else {
@ -1813,6 +1813,17 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
return nil
}
func normalizedSelectionRange(_ range: NSRange, length: Int) -> NSRange {
let location = min(max(range.location, 0), length)
let upperBound = min(max(location, range.location + range.length), length)
return NSRange(location: location, length: upperBound - location)
}
let range = normalizedSelectionRange(rawRange, length: string.length)
guard range.length > 0 else {
return nil
}
let nsString = string.string as NSString
let substring = nsString.substring(with: range)
let offset = range.location

View file

@ -794,10 +794,12 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode {
if let emojiAttribute = emojiAttribute {
AudioServicesPlaySystemSound(0x450)
interaction.insertText(NSAttributedString(string: text, attributes: [ChatTextInputAttributes.customEmoji: emojiAttribute]))
(strongSelf.entityKeyboardView.view as? EntityKeyboardComponent.View)?.revealHiddenPanels()
}
} else if case let .staticEmoji(staticEmoji) = item.content {
AudioServicesPlaySystemSound(0x450)
interaction.insertText(NSAttributedString(string: staticEmoji, attributes: [:]))
(strongSelf.entityKeyboardView.view as? EntityKeyboardComponent.View)?.revealHiddenPanels()
}
})
},

View file

@ -1005,6 +1005,14 @@ public final class EntityKeyboardComponent: Component {
pagerView.collapseTopPanel()
}
public func revealHiddenPanels() {
guard let pagerView = self.pagerView.findTaggedView(tag: PagerComponentViewTag()) as? PagerComponent<EntityKeyboardChildEnvironment, EntityKeyboardTopContainerPanelEnvironment>.View else {
return
}
pagerView.revealHiddenPanels()
}
private func reorderPacks(category: ReorderCategory, items: [EntityKeyboardTopPanelComponent.Item]) {
self.component?.reorderItems(category, items)
}

View file

@ -666,7 +666,11 @@ extension PeerInfoScreenImpl {
}
if mainController is ActionSheetController {
self.present(mainController, in: .window(.root))
if let navigationController = self.navigationController, let topController = navigationController.topViewController as? ViewController {
topController.present(mainController, in: .window(.root))
} else {
self.present(mainController, in: .window(.root))
}
} else {
mainController.navigationPresentation = .flatModal
mainController.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait)

View file

@ -443,35 +443,60 @@ final class AuthorizedApplicationContext {
return false
}
if let minimizedContainer = strongSelf.rootController.minimizedContainer, minimizedContainer.isExpanded {
minimizedContainer.collapse()
} else if let topContoller = strongSelf.rootController.topViewController as? AttachmentController {
topContoller.minimizeIfNeeded()
} else if let topContoller = strongSelf.rootController.topViewController as? BrowserScreen {
topContoller.requestMinimize(topEdgeOffset: nil, initialVelocity: nil)
let proceedAction: (Bool) -> Bool = { allowExpansion in
if let minimizedContainer = strongSelf.rootController.minimizedContainer, minimizedContainer.isExpanded {
minimizedContainer.collapse()
} else if let topContoller = strongSelf.rootController.topViewController as? AttachmentController {
topContoller.minimizeIfNeeded()
} else if let topContoller = strongSelf.rootController.topViewController as? BrowserScreen {
topContoller.requestMinimize(topEdgeOffset: nil, initialVelocity: nil)
}
for controller in strongSelf.rootController.viewControllers {
if let controller = controller as? ChatControllerImpl, controller.chatLocation.peerId == chatLocation.peerId, (controller.chatLocation.threadId == nil || controller.chatLocation.threadId == chatLocation.threadId) {
if allowExpansion {
return true
} else {
strongSelf.notificationController.removeItemsWithGroupingKey(firstMessage.id.peerId)
let chatController = ChatControllerImpl(context: strongSelf.context, chatLocation: chatLocation.asChatLocation, mode: .overlay(strongSelf.rootController))
let presentationArguments = ChatControllerOverlayPresentationData(expandData: (nil, {}))
chatController.presentationArguments = presentationArguments
(strongSelf.rootController.viewControllers.last as? ViewController)?.present(chatController, in: .window(.root), with: presentationArguments)
return false
}
}
}
strongSelf.notificationController.removeItemsWithGroupingKey(firstMessage.id.peerId)
var processed = false
for media in firstMessage.media {
if let action = media as? TelegramMediaAction, case .geoProximityReached = action.action {
strongSelf.context.sharedContext.openLocationScreen(context: strongSelf.context, messageId: firstMessage.id, navigationController: strongSelf.rootController)
processed = true
break
}
}
if !processed {
strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: strongSelf.rootController, context: strongSelf.context, chatLocation: chatLocation))
}
return false
}
for controller in strongSelf.rootController.viewControllers {
if let controller = controller as? ChatControllerImpl, controller.chatLocation.peerId == chatLocation.peerId, (controller.chatLocation.threadId == nil || controller.chatLocation.threadId == chatLocation.threadId) {
return true
if let topController = strongSelf.rootController.topViewController as? ChatControllerImpl {
let didPresentAlert = topController.presentVoiceMessageDiscardAlert(action: {
let _ = proceedAction(false)
}, discardIfVideo: true, performAction: false)
if didPresentAlert {
return false
}
}
strongSelf.notificationController.removeItemsWithGroupingKey(firstMessage.id.peerId)
var processed = false
for media in firstMessage.media {
if let action = media as? TelegramMediaAction, case .geoProximityReached = action.action {
strongSelf.context.sharedContext.openLocationScreen(context: strongSelf.context, messageId: firstMessage.id, navigationController: strongSelf.rootController)
processed = true
break
}
}
if !processed {
strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: strongSelf.rootController, context: strongSelf.context, chatLocation: chatLocation))
}
return proceedAction(true)
}
return false
}, expandAction: { expandData in

View file

@ -27,6 +27,17 @@ private enum OptionsId: Hashable {
case link
}
private func chatLocationMatchesDestination(_ chatLocation: ChatLocation, peerId: EnginePeer.Id, threadId: Int64?) -> Bool {
switch chatLocation {
case let .peer(id):
return id == peerId && threadId == nil
case let .replyThread(replyThreadMessage):
return replyThreadMessage.peerId == peerId && replyThreadMessage.threadId == threadId
case .customChatContents:
return false
}
}
private func presentChatInputOptions(selfController: ChatControllerImpl, sourceView: UIView, initialId: OptionsId) {
var getContextController: (() -> ContextController?)?
@ -623,14 +634,13 @@ func moveReplyMessageToAnotherChat(selfController: ChatControllerImpl, replySubj
return
}
let peerId = peer.id
//let accountPeerId = selfController.context.account.peerId
var isPinnedMessages = false
if case .pinnedMessages = selfController.presentationInterfaceState.subject {
isPinnedMessages = true
}
if case .peer(peerId) = selfController.chatLocation, selfController.parentController == nil, !isPinnedMessages {
if chatLocationMatchesDestination(selfController.chatLocation, peerId: peerId, threadId: threadId), selfController.parentController == nil, !isPinnedMessages {
selfController.updateChatPresentationInterfaceState(animated: false, interactive: true, { $0.updatedInterfaceState({ $0.withUpdatedReplyMessageSubject(replySubject).withoutSelectionState() }).updatedSearch(nil) })
selfController.updateItemNodesSearchTextHighlightStates()
selfController.searchResultsController = nil
@ -651,7 +661,7 @@ func moveReplyToChat(selfController: ChatControllerImpl, peerId: EnginePeer.Id,
if let navigationController = selfController.effectiveNavigationController {
for controller in navigationController.viewControllers {
if let maybeChat = controller as? ChatControllerImpl {
if case .peer(peerId) = maybeChat.chatLocation {
if chatLocationMatchesDestination(maybeChat.chatLocation, peerId: peerId, threadId: threadId) {
var isChatPinnedMessages = false
if case .pinnedMessages = maybeChat.presentationInterfaceState.subject {
isChatPinnedMessages = true

View file

@ -10304,21 +10304,35 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
self.present(controller, in: .window(.root))
}
func presentVoiceMessageDiscardAlert(action: @escaping () -> Void = {}, alertAction: (() -> Void)? = nil, delay: Bool = false, performAction: Bool = true) -> Bool {
if let _ = self.presentationInterfaceState.inputTextPanelState.mediaRecordingState {
func presentVoiceMessageDiscardAlert(
action: @escaping () -> Void = {},
alertAction: (() -> Void)? = nil,
discardIfVideo: Bool = false,
delay: Bool = false,
performAction: Bool = true
) -> Bool {
if let mediaRecordingState = self.presentationInterfaceState.inputTextPanelState.mediaRecordingState {
var discard = false
if discardIfVideo, case .video = mediaRecordingState {
discard = true
}
alertAction?()
Queue.mainQueue().after(delay ? 0.2 : 0.0) {
let alertController = textAlertController(
context: self.context,
updatedPresentationData: self.updatedPresentationData,
title: nil,
text: self.presentationData.strings.Conversation_StopVoiceMessageDescription,
text: discard ? self.presentationData.strings.Conversation_DiscardRecordedVoiceMessageDescription : self.presentationData.strings.Conversation_StopVoiceMessageDescription,
actions: [
TextAlertAction(
type: .defaultAction,
title: self.presentationData.strings.Conversation_StopVoiceMessagePauseAction,
title: discard ? self.presentationData.strings.Conversation_DiscardRecordedVoiceMessageAction : self.presentationData.strings.Conversation_StopVoiceMessagePauseAction,
action: { [weak self] in
self?.stopMediaRecorder(pause: true)
if discard {
self?.dismissMediaRecorder(.dismiss)
} else {
self?.stopMediaRecorder(pause: true)
}
Queue.mainQueue().after(0.1) {
action()
}
@ -10343,26 +10357,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
}
return false
}
func presentRecordedVoiceMessageDiscardAlert(action: @escaping () -> Void = {}, alertAction: (() -> Void)? = nil, delay: Bool = false, performAction: Bool = true) -> Bool {
if let _ = self.presentationInterfaceState.interfaceState.mediaDraftState {
alertAction?()
Queue.mainQueue().after(delay ? 0.2 : 0.0) {
self.present(textAlertController(context: self.context, updatedPresentationData: self.updatedPresentationData, title: nil, text: self.presentationData.strings.Conversation_DiscardRecordedVoiceMessageDescription, actions: [TextAlertAction(type: .genericAction, title: self.presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .defaultAction, title: self.presentationData.strings.Conversation_DiscardRecordedVoiceMessageAction, action: { [weak self] in
self?.stopMediaRecorder()
Queue.mainQueue().after(0.1) {
action()
}
})]), in: .window(.root))
}
return true
} else if performAction {
action()
}
return false
}
func presentAutoremoveSetup() {
guard let peer = self.presentationInterfaceState.renderedPeer?.peer else {
return

View file

@ -226,7 +226,7 @@ public class ComposeControllerImpl: ViewController, ComposeController {
DeviceAccess.authorizeAccess(to: .contacts)
default:
let presentationData = strongSelf.presentationData
strongSelf.present(textAlertController(context: strongSelf.context, title: presentationData.strings.AccessDenied_Title, text: presentationData.strings.Contacts_AccessDeniedError, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_NotNow, action: {}), TextAlertAction(type: .genericAction, title: presentationData.strings.AccessDenied_Settings, action: {
strongSelf.present(textAlertController(context: strongSelf.context, title: presentationData.strings.AccessDenied_Title, text: presentationData.strings.Contacts_AccessDeniedError, actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_NotNow, action: {}), TextAlertAction(type: .defaultAction, title: presentationData.strings.AccessDenied_Settings, action: {
self?.context.sharedContext.applicationBindings.openSettings()
})]), in: .window(.root))
}

View file

@ -39,7 +39,7 @@ func openAddContactImpl(context: AccountContext, peer: EnginePeer?, firstName: S
DeviceAccess.authorizeAccess(to: .contacts)
default:
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
present(textAlertController(context: context, title: presentationData.strings.AccessDenied_Title, text: presentationData.strings.Contacts_AccessDeniedError, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_NotNow, action: {}), TextAlertAction(type: .genericAction, title: presentationData.strings.AccessDenied_Settings, action: {
present(textAlertController(context: context, title: presentationData.strings.AccessDenied_Title, text: presentationData.strings.Contacts_AccessDeniedError, actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_NotNow, action: {}), TextAlertAction(type: .defaultAction, title: presentationData.strings.AccessDenied_Settings, action: {
context.sharedContext.applicationBindings.openSettings()
})]), nil)
}

View file

@ -120,6 +120,15 @@ func openResolvedUrlImpl(
}
)
}
let isStartGroup: Bool
switch peerType {
case .group?:
isStartGroup = true
default:
isStartGroup = false
}
let shouldForceStartGroupStartFlow = isStartGroup && !payload.isEmpty && adminRights == nil
let shouldCheckExistingGroupAdmin = isStartGroup && !payload.isEmpty && adminRights != nil
var filter: ChatListNodePeersFilter = [.onlyGroupsAndChannels, .onlyManageable, .excludeDisabled, .excludeRecent, .doNotSearchMessages]
var title: String = presentationData.strings.Bot_AddToChat_Title
@ -198,42 +207,100 @@ func openResolvedUrlImpl(
}
}),
TextAlertAction(type: .genericAction, title: strings.Common_Cancel, action: {})
]
],
actionLayout: .vertical
)
present(alertController, nil)
}
let openAdminControllerImpl: (TelegramChatAdminRightsFlags?) -> Void = { initialAdminRights in
let adminController = channelAdminController(context: context, peerId: peerId, adminId: botPeerId, initialParticipant: nil, invite: true, initialAdminRights: initialAdminRights, updated: { _ in
if shouldCheckExistingGroupAdmin {
Queue.mainQueue().after(0.1) {
addMemberImpl()
}
} else {
controller?.dismiss()
}
}, upgradedToSupergroup: { _, _ in }, transferedOwnership: { _ in })
navigationController?.pushViewController(adminController)
}
let openAdminControllerWithResolvedRightsImpl: (Bool) -> Void = { isGroup in
if adminRights == nil {
let _ = (defaultAdminRights.get()
|> take(1)
|> deliverOnMainQueue).start(next: { defaultAdminRights in
let initialAdminRights = isGroup ? defaultAdminRights?.group?.rights : defaultAdminRights?.channel?.rights
openAdminControllerImpl(initialAdminRights)
})
} else {
openAdminControllerImpl(adminRights?.chatAdminRights)
}
}
if case let .channel(peer) = peer {
var isGroup = false
if case .group = peer.info {
isGroup = true
}
if peer.flags.contains(.isCreator) || peer.adminRights?.rights.contains(.canAddAdmins) == true {
let _ = (defaultAdminRights.get()
|> take(1)
|> deliverOnMainQueue).start(next: { defaultAdminRights in
let initialAdminRights = adminRights?.chatAdminRights ?? (isGroup ? defaultAdminRights?.group?.rights : defaultAdminRights?.channel?.rights)
let controller = channelAdminController(context: context, peerId: peerId, adminId: botPeerId, initialParticipant: nil, invite: true, initialAdminRights: initialAdminRights, updated: { _ in
controller?.dismiss()
}, upgradedToSupergroup: { _, _ in }, transferedOwnership: { _ in })
navigationController?.pushViewController(controller)
})
if shouldForceStartGroupStartFlow {
addMemberImpl()
} else if peer.flags.contains(.isCreator) || peer.adminRights?.rights.contains(.canAddAdmins) == true {
if shouldCheckExistingGroupAdmin && isGroup {
let _ = (context.engine.peers.fetchChannelParticipant(peerId: peerId, participantId: botPeerId)
|> deliverOnMainQueue).start(next: { participant in
let isBotAlreadyAdmin: Bool
if let participant = participant {
switch participant {
case .creator:
isBotAlreadyAdmin = true
case let .member(_, _, adminInfo, _, _, _):
isBotAlreadyAdmin = adminInfo != nil
}
} else {
isBotAlreadyAdmin = false
}
if isBotAlreadyAdmin {
addMemberImpl()
} else {
openAdminControllerWithResolvedRightsImpl(isGroup)
}
})
} else {
openAdminControllerWithResolvedRightsImpl(isGroup)
}
} else {
addMemberImpl()
}
} else if case let .legacyGroup(peer) = peer {
if case .member = peer.role {
if shouldForceStartGroupStartFlow {
addMemberImpl()
} else {
let _ = (defaultAdminRights.get()
|> take(1)
|> deliverOnMainQueue).start(next: { defaultAdminRights in
let initialAdminRights = adminRights?.chatAdminRights ?? defaultAdminRights?.group?.rights
let controller = channelAdminController(context: context, peerId: peerId, adminId: botPeerId, initialParticipant: nil, invite: true, initialAdminRights: initialAdminRights, updated: { _ in
controller?.dismiss()
}, upgradedToSupergroup: { _, _ in }, transferedOwnership: { _ in })
navigationController?.pushViewController(controller)
} else if case .member = peer.role {
addMemberImpl()
} else if shouldCheckExistingGroupAdmin {
let _ = (context.engine.peers.fetchAndUpdateCachedPeerData(peerId: peerId)
|> mapToSignal { _ in
context.engine.data.get(TelegramEngine.EngineData.Item.Peer.LegacyGroupParticipants(id: peerId))
}
|> deliverOnMainQueue).start(next: { participants in
let isBotAlreadyAdmin: Bool
if let participant = participants.knownValue?.first(where: { $0.peerId == botPeerId }) {
switch participant {
case .creator, .admin:
isBotAlreadyAdmin = true
case .member:
isBotAlreadyAdmin = false
}
} else {
isBotAlreadyAdmin = false
}
if isBotAlreadyAdmin {
addMemberImpl()
} else {
openAdminControllerWithResolvedRightsImpl(true)
}
})
} else {
openAdminControllerWithResolvedRightsImpl(true)
}
}
}
@ -788,7 +855,7 @@ func openResolvedUrlImpl(
navigationController?.pushViewController(controller)
default:
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
present(textAlertController(context: context, updatedPresentationData: updatedPresentationData, title: presentationData.strings.AccessDenied_Title, text: presentationData.strings.Contacts_AccessDeniedError, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_NotNow, action: {}), TextAlertAction(type: .genericAction, title: presentationData.strings.AccessDenied_Settings, action: {
present(textAlertController(context: context, updatedPresentationData: updatedPresentationData, title: presentationData.strings.AccessDenied_Title, text: presentationData.strings.Contacts_AccessDeniedError, actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_NotNow, action: {}), TextAlertAction(type: .defaultAction, title: presentationData.strings.AccessDenied_Settings, action: {
context.sharedContext.applicationBindings.openSettings()
})]), nil)
}

View file

@ -531,7 +531,13 @@ public final class TextSelectionNode: ASDisplayNode {
})
}
return adjustedRange
func normalizedSelectionRange(_ range: NSRange, length: Int) -> NSRange {
let location = min(max(range.location, 0), length)
let upperBound = min(max(location, range.location + range.length), length)
return NSRange(location: location, length: upperBound - location)
}
return normalizedSelectionRange(adjustedRange, length: attributedString.length)
}
private func convertSelectionFromOriginalText(attributedString: NSAttributedString, range: NSRange) -> NSRange {

View file

@ -54,6 +54,9 @@ extension ResolvedBotAdminRights {
if components.contains("manage_chat") {
rawValue |= ResolvedBotAdminRights.manageChat.rawValue
}
if components.contains("manage_topics") {
rawValue |= ResolvedBotAdminRights.manageTopics.rawValue
}
if components.contains("anonymous") {
rawValue |= ResolvedBotAdminRights.canBeAnonymous.rawValue
}