Various fixes

This commit is contained in:
Ilya Laktyushin 2026-04-16 21:45:26 +02:00
parent 31e3a7dc79
commit 532a3ae3e1
27 changed files with 548 additions and 435 deletions

View file

@ -286,7 +286,7 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS
public let inputModeView: ComponentHostView<Empty>
private var validLayout: (width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, keyboardHeight: CGFloat, additionalSideInsets: UIEdgeInsets, maxHeight: CGFloat, metrics: LayoutMetrics, isSecondary: Bool)?
private var validLayout: (width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, keyboardHeight: CGFloat, additionalSideInsets: UIEdgeInsets, textFieldMaxHeight: CGFloat, availableHeight: CGFloat, metrics: LayoutMetrics, isSecondary: Bool)?
private var currentInputMode: AttachmentTextInputMode = .text
private var currentAdditionalInputHeight: CGFloat = 0.0
private var currentSafeAreaInset: UIEdgeInsets = .zero
@ -626,7 +626,7 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS
guard let presentationInterfaceState = self.presentationInterfaceState else {
return 0.0
}
return self.updateLayout(width: size.width, leftInset: sideInset, rightInset: sideInset, bottomInset: 0.0, keyboardHeight: keyboardHeight, additionalSideInsets: UIEdgeInsets(), maxHeight: size.height, isSecondary: false, transition: animated ? .animated(duration: 0.2, curve: .easeInOut) : .immediate, interfaceState: presentationInterfaceState, metrics: LayoutMetrics(widthClass: .compact, heightClass: .compact, orientation: nil), isMediaInputExpanded: false)
return self.updateLayout(width: size.width, leftInset: sideInset, rightInset: sideInset, bottomInset: 0.0, keyboardHeight: keyboardHeight, additionalSideInsets: UIEdgeInsets(), textFieldMaxHeight: size.height, availableHeight: size.height, isSecondary: false, transition: animated ? .animated(duration: 0.2, curve: .easeInOut) : .immediate, interfaceState: presentationInterfaceState, metrics: LayoutMetrics(widthClass: .compact, heightClass: .compact, orientation: nil), isMediaInputExpanded: false)
}
@objc(updateContainerLayoutSize:safeAreaInset:bottomInset:keyboardHeight:animated:)
@ -637,7 +637,7 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS
guard let presentationInterfaceState = self.presentationInterfaceState else {
return 0.0
}
return self.updateLayout(width: size.width, leftInset: 0.0, rightInset: 0.0, bottomInset: 0.0, keyboardHeight: keyboardHeight, additionalSideInsets: UIEdgeInsets(), maxHeight: size.height, isSecondary: false, transition: animated ? .animated(duration: 0.4, curve: .spring) : .immediate, interfaceState: presentationInterfaceState, metrics: LayoutMetrics(widthClass: .compact, heightClass: .compact, orientation: nil), isMediaInputExpanded: false)
return self.updateLayout(width: size.width, leftInset: 0.0, rightInset: 0.0, bottomInset: safeAreaInset.bottom, keyboardHeight: keyboardHeight, additionalSideInsets: UIEdgeInsets(), textFieldMaxHeight: size.height, availableHeight: size.height, isSecondary: false, transition: animated ? .animated(duration: 0.4, curve: .spring) : .immediate, interfaceState: presentationInterfaceState, metrics: LayoutMetrics(widthClass: .compact, heightClass: .compact, orientation: nil), isMediaInputExpanded: false)
}
public func setCaption(_ caption: NSAttributedString?) {
@ -873,13 +873,17 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS
}
public func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, additionalSideInsets: UIEdgeInsets, maxHeight: CGFloat, isSecondary: Bool, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState, metrics: LayoutMetrics, isMediaInputExpanded: Bool) -> CGFloat {
self.updateLayout(width: width, leftInset: leftInset, rightInset: rightInset, bottomInset: bottomInset, keyboardHeight: 0.0, additionalSideInsets: additionalSideInsets, maxHeight: maxHeight, isSecondary: isSecondary, transition: transition, interfaceState: interfaceState, metrics: metrics, isMediaInputExpanded: isMediaInputExpanded)
self.updateLayout(width: width, leftInset: leftInset, rightInset: rightInset, bottomInset: bottomInset, keyboardHeight: 0.0, additionalSideInsets: additionalSideInsets, textFieldMaxHeight: maxHeight, availableHeight: maxHeight, isSecondary: isSecondary, transition: transition, interfaceState: interfaceState, metrics: metrics, isMediaInputExpanded: isMediaInputExpanded)
}
public func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, keyboardHeight: CGFloat, additionalSideInsets: UIEdgeInsets, maxHeight: CGFloat, isSecondary: Bool, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState, metrics: LayoutMetrics, isMediaInputExpanded: Bool) -> CGFloat {
self.updateLayout(width: width, leftInset: leftInset, rightInset: rightInset, bottomInset: bottomInset, keyboardHeight: keyboardHeight, additionalSideInsets: additionalSideInsets, textFieldMaxHeight: maxHeight, availableHeight: maxHeight, isSecondary: isSecondary, transition: transition, interfaceState: interfaceState, metrics: metrics, isMediaInputExpanded: isMediaInputExpanded)
}
public func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, keyboardHeight: CGFloat, additionalSideInsets: UIEdgeInsets, textFieldMaxHeight: CGFloat, availableHeight: CGFloat, isSecondary: Bool, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState, metrics: LayoutMetrics, isMediaInputExpanded: Bool) -> CGFloat {
let hadLayout = self.validLayout != nil
let previousLayout = self.validLayout
self.validLayout = (width: width, leftInset: leftInset, rightInset: rightInset, bottomInset: bottomInset, keyboardHeight: keyboardHeight, additionalSideInsets: additionalSideInsets, maxHeight: maxHeight, metrics: metrics, isSecondary: isSecondary)
self.validLayout = (width: width, leftInset: leftInset, rightInset: rightInset, bottomInset: bottomInset, keyboardHeight: keyboardHeight, additionalSideInsets: additionalSideInsets, textFieldMaxHeight: textFieldMaxHeight, availableHeight: availableHeight, metrics: metrics, isSecondary: isSecondary)
let previousAdditionalSideInsets = previousLayout?.additionalSideInsets
let leftInset = leftInset + 8.0
@ -1006,8 +1010,8 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS
}
}
let isLandscape = width > maxHeight
let deviceMetrics = DeviceMetrics(screenSize: CGSize(width: width, height: maxHeight), scale: UIScreen.main.scale, statusBarHeight: 0.0, onScreenNavigationHeight: nil)
let isLandscape = width > availableHeight
let deviceMetrics = DeviceMetrics(screenSize: CGSize(width: width, height: availableHeight), scale: UIScreen.main.scale, statusBarHeight: 0.0, onScreenNavigationHeight: nil)
let standardInputHeight = deviceMetrics.standardInputHeight(inLandscape: isLandscape)
if keyboardHeight > 0.0 || !self.isFocused {
self.isTransitioningToTextKeyboard = false
@ -1020,7 +1024,7 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS
let minimalHeight: CGFloat = 14.0 + textFieldMinHeight
let baseWidth = width - leftInset - rightInset
let (_, textFieldHeight) = self.calculateTextFieldMetrics(width: baseWidth, maxHeight: maxHeight, metrics: metrics)
let (_, textFieldHeight) = self.calculateTextFieldMetrics(width: baseWidth, maxHeight: textFieldMaxHeight, metrics: metrics)
var panelContentHeight = self.panelHeight(textFieldHeight: textFieldHeight, metrics: metrics)
var inputHasText = false
@ -1103,10 +1107,10 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS
width: width,
leftInset: 0.0,
rightInset: 0.0,
bottomInset: 0.0,
bottomInset: bottomInset,
standardInputHeight: standardInputHeight,
inputHeight: 0.0,
maximumHeight: maxHeight,
maximumHeight: availableHeight,
inputPanelHeight: 0.0,
transition: .immediate,
interfaceState: interfaceState,
@ -1129,9 +1133,9 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS
let targetOriginY: CGFloat
if self.usesContainerLayout {
if dismissingInputHeight > 0.0 {
targetOriginY = maxHeight - dismissingInputHeight
targetOriginY = availableHeight - dismissingInputHeight
} else {
targetOriginY = maxHeight
targetOriginY = availableHeight
}
} else if dismissingInputHeight > 0.0 {
targetOriginY = inputPanelHeight
@ -1164,7 +1168,7 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS
totalHeight += retainedInputHeight
}
let isLandscapePhone = width > maxHeight && UIDevice.current.userInterfaceIdiom != .pad
let isLandscapePhone = width > availableHeight && UIDevice.current.userInterfaceIdiom != .pad
let collapsedCaptionTopInset = self.currentSafeAreaInset.top + 48.0
let expandedCaptionTopInset = self.currentSafeAreaInset.top + 8.0
@ -1172,10 +1176,10 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS
var inputMediaFrame = CGRect(origin: CGPoint(x: 0.0, y: inputPanelHeight), size: CGSize(width: width, height: inputMediaHeight))
if self.usesContainerLayout {
if isLandscapePhone {
panelOriginY = maxHeight + 16.0
inputMediaFrame.origin.y = maxHeight + 16.0
panelOriginY = availableHeight + 16.0
inputMediaFrame.origin.y = availableHeight + 16.0
} else if case .emoji = self.currentInputMode {
inputMediaFrame.origin.y = maxHeight - inputMediaHeight
inputMediaFrame.origin.y = availableHeight - inputMediaHeight
if self.currentIsCaptionAbove {
panelOriginY = expandedCaptionTopInset
} else {
@ -1186,9 +1190,9 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS
panelOriginY = (retainedInputHeight > 0.0 ? expandedCaptionTopInset : collapsedCaptionTopInset)
} else {
let bottomOffset = max(self.currentContainerBottomInset, retainedInputHeight)
panelOriginY = maxHeight - inputPanelHeight - bottomOffset
panelOriginY = availableHeight - inputPanelHeight - bottomOffset
}
inputMediaFrame.origin.y = maxHeight
inputMediaFrame.origin.y = availableHeight
}
}
@ -1897,14 +1901,14 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS
let leftInset = layout.leftInset + 8.0
let rightInset = layout.rightInset + 8.0
let (_, textFieldHeight) = self.calculateTextFieldMetrics(width: layout.width - leftInset - rightInset - layout.additionalSideInsets.right, maxHeight: layout.maxHeight, metrics: layout.metrics)
let panelHeight = self.panelHeight(textFieldHeight: textFieldHeight, metrics: layout.metrics) + (self.glass ? 11.0 : 0.0)
let totalHeight = panelHeight + self.currentAdditionalInputHeight
let (_, textFieldHeight) = self.calculateTextFieldMetrics(width: layout.width - leftInset - rightInset - layout.additionalSideInsets.right, maxHeight: layout.textFieldMaxHeight, metrics: layout.metrics)
let panelContentHeight = self.panelHeight(textFieldHeight: textFieldHeight, metrics: layout.metrics)
let totalHeight = panelContentHeight + (self.glass ? 11.0 : 0.0) + self.currentAdditionalInputHeight
if self.currentHeight != totalHeight {
self.updateHeight(animated)
self.heightUpdated?(animated)
}
return panelHeight
return panelContentHeight
} else {
return nil
}

View file

@ -2475,6 +2475,7 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate, ASGestureRecog
let defaultPanelSideInset: CGFloat = glassPanelSideInset
let panelSideInset: CGFloat = (isSelecting ? textPanelSideInset : defaultPanelSideInset) + layout.safeInsets.left
var textPanelHeight: CGFloat = 0.0
var visualTextPanelHeight: CGFloat = 0.0
var textPanelWidth: CGFloat = 0.0
if let textInputPanelNode = self.textInputPanelNode {
textInputPanelNode.isUserInteractionEnabled = isSelecting
@ -2483,7 +2484,7 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate, ASGestureRecog
if textInputPanelNode.frame.width.isZero {
panelTransition = .immediate
}
let panelHeight = textInputPanelNode.updateLayout(width: layout.size.width, leftInset: insets.left + layout.safeInsets.left, rightInset: insets.right + layout.safeInsets.right, bottomInset: 0.0, keyboardHeight: layout.inputHeight ?? 0.0, additionalSideInsets: UIEdgeInsets(), maxHeight: layout.size.height / 2.0, isSecondary: false, transition: panelTransition, interfaceState: self.presentationInterfaceState, metrics: layout.metrics, isMediaInputExpanded: false)
let panelHeight = textInputPanelNode.updateLayout(width: layout.size.width, leftInset: insets.left + layout.safeInsets.left, rightInset: insets.right + layout.safeInsets.right, bottomInset: layout.safeInsets.bottom, keyboardHeight: layout.inputHeight ?? 0.0, additionalSideInsets: UIEdgeInsets(), textFieldMaxHeight: layout.size.height / 2.0, availableHeight: layout.size.height, isSecondary: false, transition: panelTransition, interfaceState: self.presentationInterfaceState, metrics: layout.metrics, isMediaInputExpanded: false)
let panelFrame = CGRect(x: 0.0, y: topAccessoryHeight, width: layout.size.width, height: panelHeight)
if textInputPanelNode.frame.width.isZero {
textInputPanelNode.frame = panelFrame
@ -2491,8 +2492,10 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate, ASGestureRecog
transition.updateFrame(node: textInputPanelNode, frame: panelFrame)
if panelFrame.height > 0.0 {
textPanelHeight = panelFrame.height
visualTextPanelHeight = max(0.0, textPanelHeight - textInputPanelNode.additionalInputHeight)
} else {
textPanelHeight = self.panelStyle == .glass ? 40.0 : 45.0
visualTextPanelHeight = textPanelHeight
}
textPanelWidth = layout.size.width - panelSideInset * 2.0
}
@ -2545,7 +2548,7 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate, ASGestureRecog
buttonsPanelWidth = layout.size.width - layout.safeInsets.left - layout.safeInsets.right - panelSideInset * 2.0
}
let basePanelHeight = isSelecting ? max(0.0, textPanelHeight - 11.0) : glassPanelHeight
let basePanelHeight = isSelecting ? max(0.0, visualTextPanelHeight - 11.0) : glassPanelHeight
var panelSize = CGSize(width: isSelecting ? textPanelWidth : buttonsPanelWidth, height: basePanelHeight + topAccessoryHeight)
if !isSelecting && (buttons.count == 1 || hideButtons) && topAccessoryHeight > 0.0 {
panelSize.height = topAccessoryHeight
@ -2605,6 +2608,13 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate, ASGestureRecog
self.backgroundView?.isHidden = true
}
let effectiveBottomInset: CGFloat
if let textInputPanelNode = self.textInputPanelNode, textInputPanelNode.additionalInputHeight > 0.0 {
effectiveBottomInset = 0.0
} else {
effectiveBottomInset = insets.bottom
}
if isAnyButtonVisible {
var height: CGFloat
if layout.intrinsicInsets.bottom > 0.0 && (layout.inputHeight ?? 0.0).isZero {
@ -2632,7 +2642,7 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate, ASGestureRecog
}
containerFrame = CGRect(origin: CGPoint(), size: CGSize(width: bounds.width, height: height))
} else if isSelecting {
containerFrame = CGRect(origin: CGPoint(), size: CGSize(width: bounds.width, height: textPanelHeight + insets.bottom + topAccessoryHeight))
containerFrame = CGRect(origin: CGPoint(), size: CGSize(width: bounds.width, height: textPanelHeight + effectiveBottomInset + topAccessoryHeight))
} else {
containerFrame = bounds
}
@ -2661,8 +2671,8 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate, ASGestureRecog
if let textInputPanelNode = self.textInputPanelNode {
textInputPanelNode.isUserInteractionEnabled = true
if case .glass = self.panelStyle {
var textInputPanelHeight = textInputPanelNode.frame.height
if textInputPanelHeight < 1.0 {
var textInputPanelHeight = visualTextPanelHeight
if textInputPanelNode.frame.height < 1.0 {
textInputPanelHeight = 51.0
}
let heightDelta = glassPanelHeight - textInputPanelHeight
@ -2699,8 +2709,8 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate, ASGestureRecog
if let textInputPanelNode = self.textInputPanelNode {
textInputPanelNode.isUserInteractionEnabled = false
if case .glass = self.panelStyle {
var textInputPanelHeight = textInputPanelNode.frame.height
if textInputPanelHeight < 1.0 {
var textInputPanelHeight = visualTextPanelHeight
if textInputPanelNode.frame.height < 1.0 {
textInputPanelHeight = 51.0
}
let heightDelta = glassPanelHeight - textInputPanelHeight

View file

@ -2070,7 +2070,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
var defaultFoundRemoteMessagesSignal: Signal<([FoundRemoteMessages], Bool), NoError> = .single(([FoundRemoteMessages(messages: [], readCounters: [:], threadsData: [:], totalCount: 0)], false))
if key == .globalPosts, let data = context.currentAppConfiguration.with({ $0 }).data, let value = data["ios_load_empty_global_posts"] as? Double, value != 0.0 {
let searchSignal = context.engine.messages.searchMessages(location: .general(scope: .globalPosts(allowPaidStars: nil), tags: nil, minDate: nil, maxDate: nil, folderId: nil), query: "", state: nil, limit: 50)
let searchSignal = context.engine.messages.searchMessages(location: .general(scope: .globalPosts(allowPaidStars: nil), groupId: nil, tags: nil, minDate: nil, maxDate: nil, folderId: nil), query: "", state: nil, limit: 50)
|> map { resultData -> ChatListSearchMessagesResult in
let (result, updatedState) = resultData
@ -2606,28 +2606,28 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
}
let searchLocations: [SearchMessagesLocation]
if key == .globalPosts {
searchLocations = [SearchMessagesLocation.general(scope: .globalPosts(allowPaidStars: approvedGlobalPostQueryState?.price), tags: nil, minDate: nil, maxDate: nil, folderId: nil)]
searchLocations = [SearchMessagesLocation.general(scope: .globalPosts(allowPaidStars: approvedGlobalPostQueryState?.price), groupId: nil, tags: nil, minDate: nil, maxDate: nil, folderId: nil)]
} else if let options {
if case let .forum(peerId) = location {
searchLocations = [.peer(peerId: peerId, fromId: nil, tags: tagMask, reactions: nil, threadId: nil, minDate: options.date?.0, maxDate: options.date?.1), .general(scope: .everywhere, tags: tagMask, minDate: options.date?.0, maxDate: options.date?.1, folderId: nil)]
searchLocations = [.peer(peerId: peerId, fromId: nil, tags: tagMask, reactions: nil, threadId: nil, minDate: options.date?.0, maxDate: options.date?.1), .general(scope: .everywhere, groupId: nil, tags: tagMask, minDate: options.date?.0, maxDate: options.date?.1, folderId: nil)]
} else if let (peerId, _, _) = options.peer {
searchLocations = [.peer(peerId: peerId, fromId: nil, tags: tagMask, reactions: nil, threadId: nil, minDate: options.date?.0, maxDate: options.date?.1)]
} else {
if case let .chatList(groupId) = location, case .archive = groupId {
searchLocations = [.group(groupId: groupId._asGroup(), tags: tagMask, minDate: options.date?.0, maxDate: options.date?.1)]
if case let .chatList(groupId) = location {
searchLocations = [.general(scope: searchScope, groupId: groupId._asGroup(), tags: tagMask, minDate: options.date?.0, maxDate: options.date?.1, folderId: options.folder?.0)]
} else {
searchLocations = [.general(scope: searchScope, tags: tagMask, minDate: options.date?.0, maxDate: options.date?.1, folderId: options.folder?.0)]
searchLocations = [.general(scope: searchScope, groupId: nil, tags: tagMask, minDate: options.date?.0, maxDate: options.date?.1, folderId: options.folder?.0)]
}
}
} else {
if case .channels = key {
searchLocations = [.general(scope: .channels, tags: tagMask, minDate: nil, maxDate: nil, folderId: nil)]
searchLocations = [.general(scope: .channels, groupId: nil, tags: tagMask, minDate: nil, maxDate: nil, folderId: nil)]
} else if case let .forum(peerId) = location {
searchLocations = [.peer(peerId: peerId, fromId: nil, tags: tagMask, reactions: nil, threadId: nil, minDate: nil, maxDate: nil), .general(scope: .everywhere, tags: tagMask, minDate: nil, maxDate: nil, folderId: nil)]
} else if case let .chatList(groupId) = location, case .archive = groupId {
searchLocations = [.group(groupId: groupId._asGroup(), tags: tagMask, minDate: nil, maxDate: nil)]
searchLocations = [.peer(peerId: peerId, fromId: nil, tags: tagMask, reactions: nil, threadId: nil, minDate: nil, maxDate: nil), .general(scope: .everywhere, groupId: nil, tags: tagMask, minDate: nil, maxDate: nil, folderId: nil)]
} else if case let .chatList(groupId) = location {
searchLocations = [.general(scope: searchScope, groupId: groupId._asGroup(), tags: tagMask, minDate: nil, maxDate: nil, folderId: nil)]
} else {
searchLocations = [.general(scope: searchScope, tags: tagMask, minDate: nil, maxDate: nil, folderId: nil)]
searchLocations = [.general(scope: searchScope, groupId: nil, tags: tagMask, minDate: nil, maxDate: nil, folderId: nil)]
}
}
@ -2726,7 +2726,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
let searchSignals: [Signal<(SearchMessagesResult, SearchMessagesState), NoError>]
if key == .globalPosts {
searchSignals = [context.engine.messages.searchMessages(location: .general(scope: .globalPosts(allowPaidStars: approvedGlobalPostQueryState?.price), tags: nil, minDate: nil, maxDate: nil, folderId: nil), query: finalQuery, state: nil, limit: 50)]
searchSignals = [context.engine.messages.searchMessages(location: .general(scope: .globalPosts(allowPaidStars: approvedGlobalPostQueryState?.price), groupId: nil, tags: nil, minDate: nil, maxDate: nil, folderId: nil), query: finalQuery, state: nil, limit: 50)]
} else {
searchSignals = searchLocations.map { searchLocation in
return context.engine.messages.searchMessages(location: searchLocation, query: finalQuery, state: nil, limit: 50)

View file

@ -54,7 +54,7 @@ final class HashtagSearchGlobalChatContents: ChatCustomContentsProtocol {
if self.publicPosts {
search = self.context.engine.messages.searchHashtagPosts(hashtag: self.query, state: nil)
} else {
search = self.context.engine.messages.searchMessages(location: .general(scope: .everywhere, tags: nil, minDate: nil, maxDate: nil, folderId: nil), query: self.query, state: nil)
search = self.context.engine.messages.searchMessages(location: .general(scope: .everywhere, groupId: nil, tags: nil, minDate: nil, maxDate: nil, folderId: nil), query: self.query, state: nil)
}
self.isSearchingPromise.set(true)
@ -102,7 +102,7 @@ final class HashtagSearchGlobalChatContents: ChatCustomContentsProtocol {
if self.publicPosts {
search = self.context.engine.messages.searchHashtagPosts(hashtag: self.query, state: self.currentSearchState)
} else {
search = self.context.engine.messages.searchMessages(location: .general(scope: .everywhere, tags: nil, minDate: nil, maxDate: nil, folderId: nil), query: self.query, state: currentSearchState)
search = self.context.engine.messages.searchMessages(location: .general(scope: .everywhere, groupId: nil, tags: nil, minDate: nil, maxDate: nil, folderId: nil), query: self.query, state: currentSearchState)
}
self.historyViewDisposable?.dispose()

View file

@ -498,42 +498,44 @@ public func inviteLinkListController(context: AccountContext, updatedPresentatio
guard let node = node as? ContextReferenceContentNode, let controller = getControllerImpl?(), let invite = invite else {
return
}
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
var items: [ContextMenuItem] = []
items.append(.action(ContextMenuActionItem(text: presentationData.strings.InviteLink_ContextCopy, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Copy"), color: theme.contextMenu.primaryColor)
}, action: { _, f in
f(.dismissWithoutContent)
dismissTooltipsImpl?()
UIPasteboard.general.string = invite.link
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
presentControllerImpl?(UndoOverlayController(presentationData: presentationData, content: .linkCopied(title: nil, text: presentationData.strings.InviteLink_InviteLinkCopiedText), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), nil)
})))
items.append(.action(ContextMenuActionItem(text: presentationData.strings.InviteLink_ContextGetQRCode, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Settings/QrIcon"), color: theme.contextMenu.primaryColor)
}, action: { _, f in
f(.dismissWithoutContent)
let _ = (context.account.postbox.loadedPeerWithId(peerId)
|> deliverOnMainQueue).start(next: { peer in
let isGroup: Bool
if let peer = peer as? TelegramChannel, case .broadcast = peer.info {
isGroup = false
let creatorIsBot: Signal<Bool, NoError>
if let adminPeer = admin?.peer.peer as? TelegramUser {
creatorIsBot = .single(adminPeer.botInfo != nil)
} else if case let .link(_, _, _, _, _, adminId, _, _, _, _, _, _, _) = invite, adminId.toInt64() != 0 {
creatorIsBot = context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: adminId))
|> map { peer -> Bool in
if let peer, case let .user(user) = peer, user.botInfo != nil {
return true
} else {
isGroup = true
return false
}
presentControllerImpl?(QrCodeScreen(context: context, updatedPresentationData: updatedPresentationData, subject: .invite(invite: invite, type: isGroup ? .group : .channel)), nil)
})
})))
}
} else {
creatorIsBot = .single(false)
}
if case let .link(_, _, _, _, _, adminId, _, _, _, _, _, _, _) = invite, adminId.toInt64() != 0 {
items.append(.action(ContextMenuActionItem(text: presentationData.strings.InviteLink_ContextRevoke, textColor: .destructive, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.actionSheet.destructiveActionTextColor)
let _ = (creatorIsBot
|> take(1)
|> deliverOnMainQueue).start(next: { creatorIsBot in
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
var items: [ContextMenuItem] = []
items.append(.action(ContextMenuActionItem(text: presentationData.strings.InviteLink_ContextCopy, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Copy"), color: theme.contextMenu.primaryColor)
}, action: { _, f in
f(.dismissWithoutContent)
dismissTooltipsImpl?()
UIPasteboard.general.string = invite.link
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
presentControllerImpl?(UndoOverlayController(presentationData: presentationData, content: .linkCopied(title: nil, text: presentationData.strings.InviteLink_InviteLinkCopiedText), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), nil)
})))
items.append(.action(ContextMenuActionItem(text: presentationData.strings.InviteLink_ContextGetQRCode, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Settings/QrIcon"), color: theme.contextMenu.primaryColor)
}, action: { _, f in
f(.dismissWithoutContent)
@ -545,62 +547,81 @@ public func inviteLinkListController(context: AccountContext, updatedPresentatio
} else {
isGroup = true
}
let controller = ActionSheetController(presentationData: presentationData)
let dismissAction: () -> Void = { [weak controller] in
controller?.dismissAnimated()
}
controller.setItemGroups([
ActionSheetItemGroup(items: [
ActionSheetTextItem(title: isGroup ? presentationData.strings.GroupInfo_InviteLink_RevokeAlert_Text : presentationData.strings.ChannelInfo_InviteLink_RevokeAlert_Text),
ActionSheetButtonItem(title: presentationData.strings.GroupInfo_InviteLink_RevokeLink, color: .destructive, action: {
dismissAction()
var revoke = false
updateState { state in
if !state.revokingPrivateLink {
revoke = true
var updatedState = state
updatedState.revokingPrivateLink = true
return updatedState
} else {
return state
}
}
if revoke, let inviteLink = invite.link {
revokeLinkDisposable.set((context.engine.peers.revokePeerExportedInvitation(peerId: peerId, link: inviteLink) |> deliverOnMainQueue).start(next: { result in
updateState { state in
var updatedState = state
updatedState.revokingPrivateLink = false
return updatedState
}
if let result = result {
switch result {
case let .update(newInvite):
invitesContext.remove(newInvite)
revokedInvitesContext.add(newInvite)
case let .replace(previousInvite, newInvite):
revokedInvitesContext.add(previousInvite)
invitesContext.remove(previousInvite)
invitesContext.add(newInvite)
}
}
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
presentControllerImpl?(UndoOverlayController(presentationData: presentationData, content: .linkRevoked(text: presentationData.strings.InviteLink_InviteLinkRevoked), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), nil)
}))
}
})
]),
ActionSheetItemGroup(items: [ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, action: { dismissAction() })])
])
presentControllerImpl?(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet))
presentControllerImpl?(QrCodeScreen(context: context, updatedPresentationData: updatedPresentationData, subject: .invite(invite: invite, type: isGroup ? .group : .channel)), nil)
})
})))
}
let contextController = makeContextController(presentationData: presentationData, source: .reference(InviteLinkContextReferenceContentSource(controller: controller, sourceNode: node)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture)
presentInGlobalOverlayImpl?(contextController)
if case let .link(_, _, _, _, _, adminId, _, _, _, _, _, _, _) = invite, adminId.toInt64() != 0, !creatorIsBot {
items.append(.action(ContextMenuActionItem(text: presentationData.strings.InviteLink_ContextRevoke, textColor: .destructive, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.actionSheet.destructiveActionTextColor)
}, action: { _, f in
f(.dismissWithoutContent)
let _ = (context.account.postbox.loadedPeerWithId(peerId)
|> deliverOnMainQueue).start(next: { peer in
let isGroup: Bool
if let peer = peer as? TelegramChannel, case .broadcast = peer.info {
isGroup = false
} else {
isGroup = true
}
let controller = ActionSheetController(presentationData: presentationData)
let dismissAction: () -> Void = { [weak controller] in
controller?.dismissAnimated()
}
controller.setItemGroups([
ActionSheetItemGroup(items: [
ActionSheetTextItem(title: isGroup ? presentationData.strings.GroupInfo_InviteLink_RevokeAlert_Text : presentationData.strings.ChannelInfo_InviteLink_RevokeAlert_Text),
ActionSheetButtonItem(title: presentationData.strings.GroupInfo_InviteLink_RevokeLink, color: .destructive, action: {
dismissAction()
var revoke = false
updateState { state in
if !state.revokingPrivateLink {
revoke = true
var updatedState = state
updatedState.revokingPrivateLink = true
return updatedState
} else {
return state
}
}
if revoke, let inviteLink = invite.link {
revokeLinkDisposable.set((context.engine.peers.revokePeerExportedInvitation(peerId: peerId, link: inviteLink) |> deliverOnMainQueue).start(next: { result in
updateState { state in
var updatedState = state
updatedState.revokingPrivateLink = false
return updatedState
}
if let result = result {
switch result {
case let .update(newInvite):
invitesContext.remove(newInvite)
revokedInvitesContext.add(newInvite)
case let .replace(previousInvite, newInvite):
revokedInvitesContext.add(previousInvite)
invitesContext.remove(previousInvite)
invitesContext.add(newInvite)
}
}
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
presentControllerImpl?(UndoOverlayController(presentationData: presentationData, content: .linkRevoked(text: presentationData.strings.InviteLink_InviteLinkRevoked), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), nil)
}))
}
})
]),
ActionSheetItemGroup(items: [ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, action: { dismissAction() })])
])
presentControllerImpl?(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet))
})
})))
}
let contextController = makeContextController(presentationData: presentationData, source: .reference(InviteLinkContextReferenceContentSource(controller: controller, sourceNode: node)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture)
presentInGlobalOverlayImpl?(contextController)
})
}, createLink: {
let controller = inviteLinkEditController(context: context, updatedPresentationData: updatedPresentationData, peerId: peerId, invite: nil, completion: { invite in
if let invite = invite {

View file

@ -265,7 +265,7 @@ static const CGFloat outerCircleMinScale = innerCircleRadius / outerCircleRadius
_innerCircleView.center = centerPoint;
_outerCircleView.center = centerPoint;
_decoration.center = centerPoint;
_innerIconWrapperView.center = CGPointMake(_decoration.frame.size.width / 2.0f, _decoration.frame.size.height / 2.0f);
_innerIconWrapperView.center = CGPointMake(CGRectGetMidX(_decoration.bounds), CGRectGetMidY(_decoration.bounds));
_lockPanelWrapperView.frame = CGRectMake(floor(centerPoint.x - _lockPanelWrapperView.frame.size.width / 2.0f), floor(centerPoint.y - 122.0f - _lockPanelWrapperView.frame.size.height / 2.0f), _lockPanelWrapperView.frame.size.width, _lockPanelWrapperView.frame.size.height);
@ -918,6 +918,8 @@ static const CGFloat outerCircleMinScale = innerCircleRadius / outerCircleRadius
_currentTranslation = MIN(0.0, _currentTranslation * 0.7f + _targetTranslation * 0.3f);
_cancelTranslation = MIN(0.0, _cancelTranslation * 0.7f + _cancelTargetTranslation * 0.3f);
[self updateOverlay];
if (t > _animationStartTime) {
CGFloat outerScale = outerCircleMinScale + _currentLevel * (1.0f - outerCircleMinScale);
CGAffineTransform translation = CGAffineTransformMakeTranslation(0, _currentTranslation);

View file

@ -265,10 +265,19 @@ private enum ChannelMembersEntry: ItemListNodeEntry {
func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem {
let arguments = arguments as! ChannelMembersControllerArguments
switch self {
case let .hideMembers(text, disabledReason, isInteractive, value):
return ItemListSwitchItem(presentationData: presentationData, systemStyle: .glass, title: text, value: value, enableInteractiveChanges: isInteractive, enabled: true, displayLocked: !value && disabledReason != nil, sectionId: self.section, style: .blocks, updated: { value in
case let .hideMembers(text, disabledReason, isInteractive, currentValue):
return ItemListSwitchItem(presentationData: presentationData, systemStyle: .glass, title: text, value: currentValue, enableInteractiveChanges: isInteractive, enabled: true, displayLocked: !currentValue && disabledReason != nil, sectionId: self.section, style: .blocks, updated: { value in
if let disabledReason {
arguments.displayHideMembersTip(disabledReason)
switch disabledReason {
case .notEnoughMembers:
if currentValue && !value {
arguments.updateHideMembers(value)
} else {
arguments.displayHideMembersTip(disabledReason)
}
case .notAllowed:
arguments.displayHideMembersTip(disabledReason)
}
} else {
arguments.updateHideMembers(value)
}

View file

@ -188,6 +188,9 @@ private func requestEditMessageInternal(accountPeerId: PeerId, postbox: Postbox,
}
}
if let webpagePreviewAttribute, webpagePreviewAttribute.leadingPreview {
flags |= Int32(1 << 16)
}
if let _ = invertMediaAttribute {
flags |= Int32(1 << 16)
}

View file

@ -33,7 +33,7 @@ public struct StandaloneSendMessagesError {
public var peerId: PeerId
public var reason: PendingMessageFailureReason?
init(
public init(
peerId: PeerId,
reason: PendingMessageFailureReason?
) {

View file

@ -12,6 +12,9 @@ internal func _internal_updateIsPremiumRequiredToContact(account: Account, peerI
var inputUsers: [Api.InputUser] = []
var ids: [PeerId] = []
for id in peerIds {
guard id != account.peerId else {
continue
}
if let peer = transaction.getPeer(id), let inputUser = apiInputUser(peer) {
if peer.isPremium {
if let cachedData = transaction.getPeerCachedData(peerId: id) as? CachedUserData {

View file

@ -1688,6 +1688,9 @@ public extension TelegramEngine.EngineData.Item {
}
func _extract(data: TelegramEngine.EngineData, views: [PostboxViewKey: PostboxView]) -> Any {
guard self.id != data.accountPeerId else {
return false
}
guard let basicPeerView = views[.basicPeer(data.accountPeerId)] as? BasicPeerView else {
assertionFailure()
return false

View file

@ -474,10 +474,13 @@ public extension EngineChatList.RelativePosition {
}
}
private func calculateIsPremiumRequiredToMessage(isPremium: Bool, targetPeer: Peer, cachedIsPremiumRequired: Bool) -> Bool {
private func calculateIsPremiumRequiredToMessage(accountPeerId: PeerId?, isPremium: Bool, targetPeer: Peer, cachedIsPremiumRequired: Bool) -> Bool {
if isPremium {
return false
}
if let accountPeerId, targetPeer.id == accountPeerId {
return false
}
guard let targetPeer = targetPeer as? TelegramUser else {
return false
}
@ -488,7 +491,7 @@ private func calculateIsPremiumRequiredToMessage(isPremium: Bool, targetPeer: Pe
}
extension EngineChatList.Item {
convenience init?(_ entry: ChatListEntry, isPremium: Bool, displayAsTopicList: Bool) {
convenience init?(_ entry: ChatListEntry, accountPeerId: PeerId?, isPremium: Bool, displayAsTopicList: Bool) {
switch entry {
case let .MessageEntry(entryData):
let index = entryData.index
@ -507,7 +510,7 @@ extension EngineChatList.Item {
var isPremiumRequiredToMessage = false
if let targetPeer = renderedPeer.chatMainPeer, let extractedData = entryData.extractedCachedData?.base as? ExtractedChatListItemCachedData {
isPremiumRequiredToMessage = calculateIsPremiumRequiredToMessage(isPremium: isPremium, targetPeer: targetPeer, cachedIsPremiumRequired: extractedData.isPremiumRequiredToMessage)
isPremiumRequiredToMessage = calculateIsPremiumRequiredToMessage(accountPeerId: accountPeerId, isPremium: isPremium, targetPeer: targetPeer, cachedIsPremiumRequired: extractedData.isPremiumRequiredToMessage)
}
var draft: EngineChatList.Draft?
@ -632,7 +635,7 @@ extension EngineChatList.AdditionalItem.PromoInfo {
extension EngineChatList.AdditionalItem {
convenience init?(_ entry: ChatListAdditionalItemEntry) {
guard let item = EngineChatList.Item(entry.entry, isPremium: false, displayAsTopicList: false) else {
guard let item = EngineChatList.Item(entry.entry, accountPeerId: nil, isPremium: false, displayAsTopicList: false) else {
return nil
}
guard let promoInfo = (entry.info as? PromoChatListItem).flatMap(EngineChatList.AdditionalItem.PromoInfo.init) else {
@ -657,7 +660,7 @@ public extension EngineChatList {
loop: for entry in view.entries {
switch entry {
case .MessageEntry:
if let item = EngineChatList.Item(entry, isPremium: isPremium, displayAsTopicList: entry.index.messageIndex.id.peerId == accountPeerId ? displaySavedMessagesAsTopicList : false) {
if let item = EngineChatList.Item(entry, accountPeerId: accountPeerId, isPremium: isPremium, displayAsTopicList: entry.index.messageIndex.id.peerId == accountPeerId ? displaySavedMessagesAsTopicList : false) {
items.append(item)
}
case .HoleEntry:

View file

@ -6,8 +6,7 @@ import MtProtoKit
public enum SearchMessagesLocation: Equatable {
case general(scope: TelegramSearchPeersScope, tags: MessageTags?, minDate: Int32?, maxDate: Int32?, folderId: Int32?)
case group(groupId: PeerGroupId, tags: MessageTags?, minDate: Int32?, maxDate: Int32?)
case general(scope: TelegramSearchPeersScope, groupId: PeerGroupId?, tags: MessageTags?, minDate: Int32?, maxDate: Int32?, folderId: Int32?)
case peer(peerId: PeerId, fromId: PeerId?, tags: MessageTags?, reactions: [MessageReaction.Reaction]?, threadId: Int64?, minDate: Int32?, maxDate: Int32?)
case sentMedia(tags: MessageTags?)
}
@ -471,17 +470,17 @@ func _internal_searchMessages(account: Account, location: SearchMessagesLocation
}
return combineLatest(peerMessages, additionalPeerMessages)
}
case let .general(_, tags, minDate, maxDate, _), let .group(_, tags, minDate, maxDate):
case let .general(_, groupId, tags, minDate, maxDate, _):
var flags: Int32 = 0
let folderId: Int32?
if case let .group(groupId, _, _, _) = location {
if let groupId {
folderId = groupId.rawValue
flags |= (1 << 0)
} else {
folderId = nil
}
if case let .general(scope, _, _, _, _) = location, case let .globalPosts(allowPaidStars) = scope {
if case let .general(scope, _, _, _, _, _) = location, case let .globalPosts(allowPaidStars) = scope {
remoteSearchResult = account.postbox.transaction { transaction -> (Int32, MessageIndex?, Api.InputPeer) in
var lowerBound: MessageIndex?
if let state = state, let message = state.main.messages.last {
@ -508,7 +507,7 @@ func _internal_searchMessages(account: Account, location: SearchMessagesLocation
}
}
} else {
if case let .general(scope, _, _, _, _) = location {
if case let .general(scope, _, _, _, _, _) = location {
switch scope {
case .everywhere:
break
@ -567,9 +566,9 @@ func _internal_searchMessages(account: Account, location: SearchMessagesLocation
if state?.additional == nil {
switch location {
case let .general(_, tags, minDate, maxDate, _), let .group(_, tags, minDate, maxDate):
case let .general(_, _, tags, minDate, maxDate, _):
let secretMessages: [Message]
if case let .general(scope, _, _, _, _) = location, case .channels = scope {
if case let .general(scope, _, _, _, _, _) = location, case .channels = scope {
secretMessages = []
} else {
secretMessages = transaction.searchMessages(peerId: nil, query: query, tags: tags)

View file

@ -1591,7 +1591,7 @@ public extension TelegramEngine {
}
public func subscribeIsPremiumRequiredForMessaging(id: EnginePeer.Id) -> Signal<Bool, NoError> {
if id.namespace != Namespaces.Peer.CloudUser {
if id == self.account.peerId || id.namespace != Namespaces.Peer.CloudUser {
return .single(false)
}

View file

@ -302,7 +302,7 @@ func _internal_fetchAndUpdateCachedPeerData(accountPeerId: PeerId, peerId rawPee
let unofficialSecurityRisk = (userFullFlags2 & (1 << 26)) != 0
var flags: CachedUserFlags = previous.flags
if premiumRequired {
if premiumRequired && peerId != accountPeerId {
flags.insert(.premiumRequired)
} else {
flags.remove(.premiumRequired)

View file

@ -756,7 +756,7 @@ public func makeAttachmentFileControllerImpl(
case .audio:
recentDocuments = .single(nil)
|> then(
context.engine.messages.searchMessages(location: .general(scope: .everywhere, tags: [.music], minDate: nil, maxDate: nil, folderId: nil), query: "", state: nil)
context.engine.messages.searchMessages(location: .general(scope: .everywhere, groupId: nil, tags: [.music], minDate: nil, maxDate: nil, folderId: nil), query: "", state: nil)
|> map { result -> [Message]? in
return result.0.messages
}

View file

@ -530,7 +530,7 @@ public final class AttachmentFileSearchContainerNode: SearchDisplayControllerCon
case .audio:
shared = .single(nil)
|> then(
context.engine.messages.searchMessages(location: .general(scope: .everywhere, tags: [.music], minDate: nil, maxDate: nil, folderId: nil), query: query, state: nil)
context.engine.messages.searchMessages(location: .general(scope: .everywhere, groupId: nil, tags: [.music], minDate: nil, maxDate: nil, folderId: nil), query: query, state: nil)
|> delay(0.6, queue: Queue.mainQueue())
|> map { result -> [Message]? in
return result.0.messages.filter { !$0.isRestricted(platform: "ios", contentSettings: context.currentContentSettings.with { $0 }) }

View file

@ -21,19 +21,22 @@ public final class MediaPlaybackHeaderPanelComponent: Component {
public let strings: PresentationStrings
public let data: GlobalControlPanelsContext.MediaPlayback
public let controller: () -> ViewController?
public let shouldPerformAction: ((@escaping () -> Void) -> Void)?
public init(
context: AccountContext,
theme: PresentationTheme,
strings: PresentationStrings,
data: GlobalControlPanelsContext.MediaPlayback,
controller: @escaping () -> ViewController?
controller: @escaping () -> ViewController?,
shouldPerformAction: ((@escaping () -> Void) -> Void)? = nil
) {
self.context = context
self.theme = theme
self.strings = strings
self.data = data
self.controller = controller
self.shouldPerformAction = shouldPerformAction
}
public static func ==(lhs: MediaPlaybackHeaderPanelComponent, rhs: MediaPlaybackHeaderPanelComponent) -> Bool {
@ -71,6 +74,17 @@ public final class MediaPlaybackHeaderPanelComponent: Component {
deinit {
self.playlistPreloadDisposable?.dispose()
}
private func performAction(_ action: @escaping () -> Void) {
guard let component = self.component else {
return
}
if let shouldPerformAction = component.shouldPerformAction {
shouldPerformAction(action)
} else {
action()
}
}
func update(component: MediaPlaybackHeaderPanelComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
let themeUpdated = self.component?.theme !== component.theme
@ -220,102 +234,110 @@ public final class MediaPlaybackHeaderPanelComponent: Component {
})
}
panel.togglePlayPause = { [weak self] in
guard let self, let component = self.component else {
return
}
component.context.sharedContext.mediaManager.playlistControl(.playback(.togglePlayPause), type: component.data.kind)
self?.performAction({ [weak self] in
guard let self, let component = self.component else {
return
}
component.context.sharedContext.mediaManager.playlistControl(.playback(.togglePlayPause), type: component.data.kind)
})
}
panel.playPrevious = { [weak self] in
guard let self, let component = self.component else {
return
}
component.context.sharedContext.mediaManager.playlistControl(.next, type: component.data.kind)
self?.performAction({ [weak self] in
guard let self, let component = self.component else {
return
}
component.context.sharedContext.mediaManager.playlistControl(.next, type: component.data.kind)
})
}
panel.playNext = { [weak self] in
guard let self, let component = self.component else {
return
}
component.context.sharedContext.mediaManager.playlistControl(.previous, type: component.data.kind)
self?.performAction({ [weak self] in
guard let self, let component = self.component else {
return
}
component.context.sharedContext.mediaManager.playlistControl(.previous, type: component.data.kind)
})
}
panel.tapAction = { [weak self] in
guard let self, let component = self.component, let controller = component.controller(), let navigationController = controller.navigationController as? NavigationController else {
return
}
if let id = component.data.item.id as? PeerMessagesMediaPlaylistItemId, let playlistLocation = component.data.playlistLocation as? PeerMessagesPlaylistLocation {
if case .music = component.data.kind {
switch playlistLocation {
case .custom, .savedMusic:
let controllerContext: AccountContext
if component.data.account.id == component.context.account.id {
controllerContext = component.context
} else {
controllerContext = component.context.sharedContext.makeTempAccountContext(account: component.data.account)
}
let playerController = component.context.sharedContext.makeOverlayAudioPlayerController(context: controllerContext, chatLocation: .peer(id: id.messageId.peerId), type: component.data.kind, initialMessageId: id.messageId, initialOrder: component.data.playbackOrder, playlistLocation: playlistLocation, parentNavigationController: navigationController)
self.window?.endEditing(true)
playerController.navigationPresentation = .flatModal
controller.push(playerController)
case let .messages(chatLocation, _, _):
let signal = component.context.sharedContext.messageFromPreloadedChatHistoryViewForLocation(id: id.messageId, location: ChatHistoryLocationInput(content: .InitialSearch(subject: MessageHistoryInitialSearchSubject(location: .id(id.messageId)), count: 60, highlight: true, setupReply: false), id: 0), context: component.context, chatLocation: chatLocation, subject: nil, chatLocationContextHolder: Atomic<ChatLocationContextHolder?>(value: nil), tag: .tag(MessageTags.music))
var cancelImpl: (() -> Void)?
let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }
let progressSignal = Signal<Never, NoError> { [weak self] subscriber in
let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: {
cancelImpl?()
}))
self?.component?.controller()?.present(controller, in: .window(.root))
return ActionDisposable { [weak controller] in
Queue.mainQueue().async() {
controller?.dismiss()
}
}
}
|> runOn(Queue.mainQueue())
|> delay(0.15, queue: Queue.mainQueue())
let progressDisposable = MetaDisposable()
var progressStarted = false
self.playlistPreloadDisposable?.dispose()
self.playlistPreloadDisposable = (signal
|> afterDisposed {
Queue.mainQueue().async {
progressDisposable.dispose()
}
}
|> deliverOnMainQueue).start(next: { [weak self] index in
guard let self, let component = self.component else {
return
}
if let _ = index.0 {
let controllerContext: AccountContext
if component.data.account.id == component.context.account.id {
controllerContext = component.context
} else {
controllerContext = component.context.sharedContext.makeTempAccountContext(account: component.data.account)
}
let playerController = component.context.sharedContext.makeOverlayAudioPlayerController(context: controllerContext, chatLocation: chatLocation, type: component.data.kind, initialMessageId: id.messageId, initialOrder: component.data.playbackOrder, playlistLocation: nil, parentNavigationController: navigationController)
self.window?.endEditing(true)
playerController.navigationPresentation = .flatModal
controller.push(playerController)
} else if index.1 {
if !progressStarted {
progressStarted = true
progressDisposable.set(progressSignal.start())
}
}
}, completed: {
})
cancelImpl = { [weak self] in
self?.playlistPreloadDisposable?.dispose()
}
default:
break
}
} else {
component.context.sharedContext.navigateToChat(accountId: component.context.account.id, peerId: id.messageId.peerId, messageId: id.messageId)
self?.performAction({ [weak self] in
guard let self, let component = self.component, let controller = component.controller(), let navigationController = controller.navigationController as? NavigationController else {
return
}
}
if let id = component.data.item.id as? PeerMessagesMediaPlaylistItemId, let playlistLocation = component.data.playlistLocation as? PeerMessagesPlaylistLocation {
if case .music = component.data.kind {
switch playlistLocation {
case .custom, .savedMusic:
let controllerContext: AccountContext
if component.data.account.id == component.context.account.id {
controllerContext = component.context
} else {
controllerContext = component.context.sharedContext.makeTempAccountContext(account: component.data.account)
}
let playerController = component.context.sharedContext.makeOverlayAudioPlayerController(context: controllerContext, chatLocation: .peer(id: id.messageId.peerId), type: component.data.kind, initialMessageId: id.messageId, initialOrder: component.data.playbackOrder, playlistLocation: playlistLocation, parentNavigationController: navigationController)
self.window?.endEditing(true)
playerController.navigationPresentation = .flatModal
controller.push(playerController)
case let .messages(chatLocation, _, _):
let signal = component.context.sharedContext.messageFromPreloadedChatHistoryViewForLocation(id: id.messageId, location: ChatHistoryLocationInput(content: .InitialSearch(subject: MessageHistoryInitialSearchSubject(location: .id(id.messageId)), count: 60, highlight: true, setupReply: false), id: 0), context: component.context, chatLocation: chatLocation, subject: nil, chatLocationContextHolder: Atomic<ChatLocationContextHolder?>(value: nil), tag: .tag(MessageTags.music))
var cancelImpl: (() -> Void)?
let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }
let progressSignal = Signal<Never, NoError> { [weak self] subscriber in
let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: {
cancelImpl?()
}))
self?.component?.controller()?.present(controller, in: .window(.root))
return ActionDisposable { [weak controller] in
Queue.mainQueue().async() {
controller?.dismiss()
}
}
}
|> runOn(Queue.mainQueue())
|> delay(0.15, queue: Queue.mainQueue())
let progressDisposable = MetaDisposable()
var progressStarted = false
self.playlistPreloadDisposable?.dispose()
self.playlistPreloadDisposable = (signal
|> afterDisposed {
Queue.mainQueue().async {
progressDisposable.dispose()
}
}
|> deliverOnMainQueue).start(next: { [weak self] index in
guard let self, let component = self.component else {
return
}
if let _ = index.0 {
let controllerContext: AccountContext
if component.data.account.id == component.context.account.id {
controllerContext = component.context
} else {
controllerContext = component.context.sharedContext.makeTempAccountContext(account: component.data.account)
}
let playerController = component.context.sharedContext.makeOverlayAudioPlayerController(context: controllerContext, chatLocation: chatLocation, type: component.data.kind, initialMessageId: id.messageId, initialOrder: component.data.playbackOrder, playlistLocation: nil, parentNavigationController: navigationController)
self.window?.endEditing(true)
playerController.navigationPresentation = .flatModal
controller.push(playerController)
} else if index.1 {
if !progressStarted {
progressStarted = true
progressDisposable.set(progressSignal.start())
}
}
}, completed: {
})
cancelImpl = { [weak self] in
self?.playlistPreloadDisposable?.dispose()
}
default:
break
}
} else {
component.context.sharedContext.navigateToChat(accountId: component.context.account.id, peerId: id.messageId.peerId, messageId: id.messageId)
}
}
})
}
}

View file

@ -2333,11 +2333,13 @@ func availableActionsForMemberOfPeer(accountPeerId: PeerId, peer: Peer?, member:
result.insert(.editRank)
case .admin:
switch member {
case let .legacyGroupMember(_, _, invitedBy, _, _, _):
result.insert(.restrict)
if invitedBy == accountPeerId {
result.insert(.promote)
result.insert(.editRank)
case let .legacyGroupMember(_, role, invitedBy, _, _, _):
if case .member = role {
result.insert(.restrict)
if invitedBy == accountPeerId {
result.insert(.promote)
result.insert(.editRank)
}
}
case .channelMember:
break
@ -2346,8 +2348,8 @@ func availableActionsForMemberOfPeer(accountPeerId: PeerId, peer: Peer?, member:
}
case .member:
switch member {
case let .legacyGroupMember(_, _, invitedBy, _, _, _):
if invitedBy == accountPeerId {
case let .legacyGroupMember(_, role, invitedBy, _, _, _):
if case .member = role, invitedBy == accountPeerId {
result.insert(.restrict)
result.insert(.editRank)
}

View file

@ -1267,8 +1267,7 @@ final class PeerInfoHeaderNode: ASDisplayNode {
panelSubtitleString = (panelStatusData.text, MultiScaleTextState.Attributes(font: Font.regular(17.0), color: subtitleColor))
}
} else if let _ = threadData {
let subtitleColor: UIColor
subtitleColor = UIColor.white
let subtitleColor: UIColor = .white
let statusText: String
if let user = peer as? TelegramUser, user.isForum {
@ -1364,6 +1363,7 @@ final class PeerInfoHeaderNode: ASDisplayNode {
], mainState: TitleNodeStateRegular)
self.subtitleNode.accessibilityLabel = subtitleStringText
var subtitleButtonHorizontalOffset: CGFloat = 0.0
if subtitleIsButton {
let subtitleBackgroundNode: ASDisplayNode
if let current = self.subtitleBackgroundNode {
@ -1400,20 +1400,21 @@ final class PeerInfoHeaderNode: ASDisplayNode {
let subtitleArrowNode: ASImageNode
if let current = self.subtitleArrowNode {
subtitleArrowNode = current
if themeUpdated {
subtitleArrowNode.image = generateTintedImage(image: UIImage(bundleImageName: "Item List/DisclosureArrow"), color: .white)?.withRenderingMode(.alwaysTemplate)
}
} else {
subtitleArrowNode = ASImageNode()
self.subtitleArrowNode = subtitleArrowNode
self.subtitleNode.insertSubnode(subtitleArrowNode, at: 1)
subtitleArrowNode.image = generateTintedImage(image: UIImage(bundleImageName: "Item List/DisclosureArrow"), color: .white)?.withRenderingMode(.alwaysTemplate)
}
subtitleBackgroundNode.backgroundColor = .white.withMultipliedAlpha(0.1)
if subtitleArrowNode.image == nil || themeUpdated {
subtitleArrowNode.image = generateTintedImage(image: UIImage(bundleImageName: "Item List/DisclosureArrow"), color: presentationData.theme.list.itemSecondaryTextColor)
}
self.subtitleNode.updateTintColor(color: presentationData.theme.list.itemSecondaryTextColor, transition: navigationTransition)
transition.updateBackgroundColor(node: subtitleBackgroundNode, color: contentButtonBackgroundColor)
let subtitleSize = subtitleNodeLayout[TitleNodeStateRegular]!.size
var subtitleBackgroundFrame = CGRect(origin: CGPoint(), size: subtitleSize).offsetBy(dx: -subtitleSize.width * 0.5, dy: -subtitleSize.height * 0.5).insetBy(dx: -6.0, dy: -4.0)
var subtitleBackgroundFrame = CGRect(origin: CGPoint(), size: subtitleSize).offsetBy(dx: -subtitleSize.width * 0.5, dy: -subtitleSize.height * 0.5).insetBy(dx: -8.0, dy: -4.0)
subtitleBackgroundFrame.size.width += 12.0
subtitleButtonHorizontalOffset = subtitleBackgroundFrame.midX
transition.updateFrame(node: subtitleBackgroundNode, frame: subtitleBackgroundFrame)
transition.updateCornerRadius(node: subtitleBackgroundNode, cornerRadius: subtitleBackgroundFrame.height * 0.5)
@ -1620,7 +1621,7 @@ final class PeerInfoHeaderNode: ASDisplayNode {
titleOffset = -min(titleCollapseOffset, contentOffset)
titleCollapseFraction = max(0.0, min(1.0, contentOffset / titleCollapseOffset))
subtitleFrame = CGRect(origin: CGPoint(x: 16.0, y: minTitleFrame.maxY + 2.0), size: subtitleSize)
subtitleFrame = CGRect(origin: CGPoint(x: 16.0 - subtitleButtonHorizontalOffset * (1.0 - titleCollapseFraction), y: minTitleFrame.maxY + 2.0), size: subtitleSize)
if self.subtitleRating != nil {
subtitleFrame.origin.x += 22.0
}
@ -1642,10 +1643,10 @@ final class PeerInfoHeaderNode: ASDisplayNode {
let totalSubtitleWidth = effectiveSubtitleWidth + usernameSpacing + usernameSize.width
if usernameSize.width == 0.0 {
subtitleFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((width - effectiveSubtitleWidth) / 2.0), y: titleFrame.maxY + 1.0), size: subtitleSize)
subtitleFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((width - effectiveSubtitleWidth) / 2.0) - subtitleButtonHorizontalOffset * (1.0 - titleCollapseFraction), y: titleFrame.maxY + 1.0), size: subtitleSize)
usernameFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((width - usernameSize.width) / 2.0), y: subtitleFrame.maxY + 1.0), size: usernameSize)
} else {
subtitleFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((width - totalSubtitleWidth) / 2.0), y: titleFrame.maxY + 1.0), size: subtitleSize)
subtitleFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((width - totalSubtitleWidth) / 2.0) - subtitleButtonHorizontalOffset * (1.0 - titleCollapseFraction), y: titleFrame.maxY + 1.0), size: subtitleSize)
usernameFrame = CGRect(origin: CGPoint(x: subtitleFrame.maxX + usernameSpacing, y: titleFrame.maxY + 1.0), size: usernameSize)
}
}
@ -2859,4 +2860,3 @@ final class PeerInfoHeaderNode: ASDisplayNode {
transition.updateAnchorPoint(layer: self.avatarListNode.maskNode.layer, anchorPoint: maskAnchorPoint)
}
}

View file

@ -1079,12 +1079,9 @@ final class PeerSelectionControllerNode: ASDisplayNode {
if textInputPanelNode.frame.width.isZero {
panelTransition = .immediate
}
var panelHeight = textInputPanelNode.updateLayout(width: layout.size.width, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, bottomInset: layout.intrinsicInsets.bottom, keyboardHeight: layout.inputHeight ?? 0.0, additionalSideInsets: UIEdgeInsets(), maxHeight: layout.size.height / 2.0, isSecondary: false, transition: panelTransition, interfaceState: self.presentationInterfaceState, metrics: layout.metrics, isMediaInputExpanded: false)
if self.searchDisplayController == nil {
panelHeight += insets.bottom
} else {
panelHeight += cleanInsets.bottom
}
var panelHeight = textInputPanelNode.updateLayout(width: layout.size.width, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, bottomInset: layout.safeInsets.bottom, keyboardHeight: layout.inputHeight ?? 0.0, additionalSideInsets: UIEdgeInsets(), textFieldMaxHeight: layout.size.height / 2.0, availableHeight: layout.size.height, isSecondary: false, transition: panelTransition, interfaceState: self.presentationInterfaceState, metrics: layout.metrics, isMediaInputExpanded: false)
let effectiveBottomInset = textInputPanelNode.additionalInputHeight > 0.0 ? 0.0 : (self.searchDisplayController == nil ? insets.bottom : cleanInsets.bottom)
panelHeight += effectiveBottomInset
textPanelHeight = panelHeight
let panelFrame = CGRect(x: 0.0, y: layout.size.height - panelHeight, width: layout.size.width, height: panelHeight)

View file

@ -378,22 +378,24 @@ public final class AccountContextImpl: AccountContext {
}).start()
}
})
let queue = Queue()
self.deviceSpecificContactImportContexts = QueueLocalObject(queue: queue, generate: {
return DeviceSpecificContactImportContexts(queue: queue)
})
let langCode = sharedContext.currentPresentationData.with { $0 }.strings.baseLanguageCode
self.currentCountriesConfiguration = Atomic(value: CountriesConfiguration(countries: loadCountryCodes()))
if !temp {
let currentCountriesConfiguration = self.currentCountriesConfiguration
self.countriesConfigurationDisposable = (self.engine.localization.getCountriesList(accountManager: sharedContext.accountManager, langCode: langCode)
|> deliverOnMainQueue).start(next: { value in
let _ = currentCountriesConfiguration.swap(CountriesConfiguration(countries: value))
|> deliverOnMainQueue).start(next: { [weak self] value in
let configuration = CountriesConfiguration(countries: value)
let _ = currentCountriesConfiguration.swap(configuration)
self?._countriesConfiguration.set(.single(configuration))
})
}
let queue = Queue()
self.deviceSpecificContactImportContexts = QueueLocalObject(queue: queue, generate: {
return DeviceSpecificContactImportContexts(queue: queue)
})
if let contactDataManager = sharedContext.contactDataManager {
let deviceSpecificContactImportContexts = self.deviceSpecificContactImportContexts
self.managedAppSpecificContactsDisposable = (contactDataManager.appSpecificReferences()

View file

@ -1171,7 +1171,7 @@ extension ChatControllerImpl {
}
return updatedState
})
self.searchResult.set(.single((results, state, .general(scope: .channels, tags: nil, minDate: nil, maxDate: nil, folderId: nil))))
self.searchResult.set(.single((results, state, .general(scope: .channels, groupId: nil, tags: nil, minDate: nil, maxDate: nil, folderId: nil))))
}
}
@ -2160,15 +2160,17 @@ extension ChatControllerImpl {
}
var invertedMediaAttribute: InvertMediaMessageAttribute?
if let attribute = message.attributes.first(where: { $0 is InvertMediaMessageAttribute }) {
invertedMediaAttribute = attribute as? InvertMediaMessageAttribute
}
if let mediaCaptionIsAbove = editMessage.mediaCaptionIsAbove {
if mediaCaptionIsAbove {
invertedMediaAttribute = InvertMediaMessageAttribute()
} else {
invertedMediaAttribute = nil
if webpagePreviewAttribute == nil {
if let attribute = message.attributes.first(where: { $0 is InvertMediaMessageAttribute }) {
invertedMediaAttribute = attribute as? InvertMediaMessageAttribute
}
if let mediaCaptionIsAbove = editMessage.mediaCaptionIsAbove {
if mediaCaptionIsAbove {
invertedMediaAttribute = InvertMediaMessageAttribute()
} else {
invertedMediaAttribute = nil
}
}
}

View file

@ -10285,7 +10285,11 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
}
)
strongSelf.chatDisplayNode.dismissInput()
strongSelf.push(controller)
if strongSelf.videoRecorderValue != nil {
strongSelf.present(controller, in: .window(.root))
} else {
strongSelf.push(controller)
}
})
}

View file

@ -1395,6 +1395,13 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate {
data: mediaPlayback,
controller: { [weak self] in
return self?.controller
},
shouldPerformAction: { [weak self] action in
guard let controller = self?.controller else {
action()
return
}
let _ = controller.presentVoiceMessageDiscardAlert(action: action)
}
)))
)

View file

@ -4,35 +4,37 @@ private enum ID3Tag: CaseIterable {
case v2
case v3
static var headerLength = 5
static let headerLength = 10
static let versionHeaderLength = 5
var header: Data {
switch self {
case .v2:
return Data([ 0x49, 0x44, 0x33, 0x02, 0x00 ])
return Data([0x49, 0x44, 0x33, 0x02, 0x00])
case .v3:
return Data([ 0x49, 0x44, 0x33, 0x03, 0x00 ])
return Data([0x49, 0x44, 0x33, 0x03, 0x00])
}
}
var artworkHeader: Data {
switch self {
case .v2:
return Data([ 0x50, 0x49, 0x43 ])
return Data([0x50, 0x49, 0x43])
case .v3:
return Data([ 0x41, 0x50, 0x49, 0x43 ])
return Data([0x41, 0x50, 0x49, 0x43])
}
}
var frameSizeOffset: Int {
var frameIdentifierLength: Int {
switch self {
case .v2:
return 2
return 3
case .v3:
return 4
}
}
var frameOffset: Int {
var frameHeaderLength: Int {
switch self {
case .v2:
return 6
@ -41,25 +43,30 @@ private enum ID3Tag: CaseIterable {
}
}
func frameSize(_ value: Int32) -> Int {
var unsupportedFlagsMask: UInt8 {
switch self {
case .v2, .v3:
return 0xc0
}
}
func frameDataSize(in data: Data, at frameOffset: Int) -> Int? {
switch self {
case .v2:
return Int(value & 0x00ffffff) + self.frameOffset
guard let range = makeRange(start: frameOffset + self.frameIdentifierLength, length: 3, upperBound: data.count) else {
return nil
}
let bytes = data[range]
return Int(UInt32(bytes[bytes.startIndex]) << 16 | UInt32(bytes[bytes.startIndex + 1]) << 8 | UInt32(bytes[bytes.startIndex + 2]))
case .v3:
return Int(value) + self.frameOffset
guard let value = readBigEndianUInt32(in: data, at: frameOffset + self.frameIdentifierLength) else {
return nil
}
return Int(value)
}
}
}
private let sizeOffset = 6
private let tagOffset = 10
private let artOffset = 10
private let v2FrameOffset: UInt32 = 6
private let v3FrameOffset: UInt32 = 10
private let tagEnding = Data([ 0x00, 0x00, 0x00 ])
private enum ID3ArtworkFormat: CaseIterable {
case jpg
case png
@ -67,77 +74,103 @@ private enum ID3ArtworkFormat: CaseIterable {
var magic: Data {
switch self {
case .jpg:
return Data([ 0xff, 0xd8, 0xff ])
return Data([0xff, 0xd8, 0xff])
case .png:
return Data([ 0x89, 0x50, 0x4e, 0x47 ])
return Data([0x89, 0x50, 0x4e, 0x47])
}
}
}
private class DataStream {
private let data: Data
private(set) var position = 0
var reachedEnd: Bool {
return self.position >= self.data.count
private enum ID3ArtworkReaderLimits {
static let maximumTagSize = 16 * 1024 * 1024
static let maximumFrameSize = 16 * 1024 * 1024
static let maximumArtworkSize = 16 * 1024 * 1024
}
private let id3Prefix = Data([0x49, 0x44, 0x33])
private let tagEnding = Data([0x00, 0x00, 0x00])
private let jpegEndMarker = Data([0xff, 0xd9])
private let pngEndMarker = Data([0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4e, 0x44, 0xae, 0x42, 0x60, 0x82])
private func makeRange(start: Int, length: Int, upperBound: Int) -> Range<Int>? {
guard start >= 0, length >= 0 else {
return nil
}
init(_ data: Data) {
self.data = data
let (end, overflow) = start.addingReportingOverflow(length)
guard !overflow, end <= upperBound else {
return nil
}
func next() -> UInt8? {
guard !self.reachedEnd else {
return nil
}
let byte = self.data[self.position]
self.position += 1
return byte
return start ..< end
}
private func checkedAdd(_ lhs: Int, _ rhs: Int) -> Int? {
let (result, overflow) = lhs.addingReportingOverflow(rhs)
return overflow ? nil : result
}
private func readBigEndianUInt32(in data: Data, at offset: Int) -> UInt32? {
guard let range = makeRange(start: offset, length: 4, upperBound: data.count) else {
return nil
}
func nextValue<T: BinaryInteger>() -> T? {
let count = MemoryLayout<T>.size
guard self.position + count <= self.data.count else {
return nil
}
let value = self.data.subdata(in: self.position ..< self.position + count).withUnsafeBytes { (pointer: UnsafeRawBufferPointer) -> T in
return pointer.baseAddress!.assumingMemoryBound(to: T.self).pointee
}
self.position += count
return value
let bytes = data[range]
return UInt32(bytes[bytes.startIndex]) << 24
| UInt32(bytes[bytes.startIndex + 1]) << 16
| UInt32(bytes[bytes.startIndex + 2]) << 8
| UInt32(bytes[bytes.startIndex + 3])
}
private func decodeSynchsafeUInt32(_ value: UInt32) -> Int? {
guard value & 0x80808080 == 0 else {
return nil
}
let b1 = Int((value >> 24) & 0x7f)
let b2 = Int((value >> 16) & 0x7f)
let b3 = Int((value >> 8) & 0x7f)
let b4 = Int(value & 0x7f)
return (b1 << 21) | (b2 << 14) | (b3 << 7) | b4
}
private func extractArtworkData(from data: Data, frameDataRange: Range<Int>) -> Data? {
let frameData = data.subdata(in: frameDataRange)
func next(_ count: Int, offset: Int = 0, peek: Bool = false) -> Data? {
guard self.position + offset + count <= self.data.count else {
return nil
var bestMatch: (format: ID3ArtworkFormat, range: Range<Data.Index>)?
for format in ID3ArtworkFormat.allCases {
guard let range = frameData.range(of: format.magic) else {
continue
}
let subdata = self.data.subdata(in: self.position + offset ..< self.position + offset + count)
if !peek {
self.position += count + offset
}
return subdata
}
func upTo(_ marker: UInt8) -> Data? {
if let end = (self.position ..< self.data.count).firstIndex( where: { self.data[$0] == marker } ) {
let upTo = self.next(end - self.position)
self.skip()
return upTo
if let current = bestMatch {
if range.lowerBound < current.range.lowerBound {
bestMatch = (format, range)
}
} else {
return nil
bestMatch = (format, range)
}
}
func skip(_ count: Int = 1) {
self.position += count
guard let match = bestMatch else {
return nil
}
func skipThrough(_ marker: UInt8) {
if let end = (self.position ..< self.data.count).firstIndex( where: { self.data[$0] == marker } ) {
self.position = end + 1
} else {
self.position = self.data.count
}
let payload: Data
switch match.format {
case .jpg:
if let endMarkerRange = frameData[match.range.lowerBound ..< frameData.endIndex].range(of: jpegEndMarker) {
payload = frameData.subdata(in: match.range.lowerBound ..< endMarkerRange.upperBound)
} else {
payload = frameData.subdata(in: match.range.lowerBound ..< frameData.endIndex)
}
case .png:
if let endMarkerRange = frameData[match.range.lowerBound ..< frameData.endIndex].range(of: pngEndMarker) {
payload = frameData.subdata(in: match.range.lowerBound ..< endMarkerRange.upperBound)
} else {
payload = frameData.subdata(in: match.range.lowerBound ..< frameData.endIndex)
}
}
guard payload.count <= ID3ArtworkReaderLimits.maximumArtworkSize else {
return nil
}
return payload
}
enum ID3ArtworkResult {
@ -147,12 +180,17 @@ enum ID3ArtworkResult {
}
func readAlbumArtworkData(_ data: Data) -> ID3ArtworkResult {
guard data.count >= 4 else {
return .notFound
if data.count < id3Prefix.count {
return id3Prefix.starts(with: data) ? .moreDataNeeded(ID3Tag.headerLength) : .notFound
}
if data.count < ID3Tag.headerLength {
return data.starts(with: id3Prefix) ? .moreDataNeeded(ID3Tag.headerLength) : .notFound
}
let stream = DataStream(data)
let versionHeader = stream.next(ID3Tag.headerLength)
guard let versionHeaderRange = makeRange(start: 0, length: ID3Tag.versionHeaderLength, upperBound: data.count) else {
return .notFound
}
let versionHeader = data.subdata(in: versionHeaderRange)
var version: ID3Tag?
for tag in ID3Tag.allCases {
@ -165,79 +203,57 @@ func readAlbumArtworkData(_ data: Data) -> ID3ArtworkResult {
return .notFound
}
stream.skip()
guard let value: UInt32 = stream.nextValue() else {
let flags = data[5]
guard flags & id3Tag.unsupportedFlagsMask == 0 else {
return .notFound
}
let size = CFSwapInt32HostToBig(value)
let b1 = (size & 0x7f000000) >> 3
let b2 = (size & 0x007f0000) >> 2
let b3 = (size & 0x00007f00) >> 1
let b4 = size & 0x0000007f
let tagSize = Int(b1 + b2 + b3 + b4)
while !stream.reachedEnd {
guard let frameHeader = stream.next(4, peek: true) else {
return .moreDataNeeded(tagSize)
}
stream.skip(id3Tag.frameSizeOffset)
guard let value: UInt32 = stream.nextValue() else {
return .moreDataNeeded(tagSize)
}
let val = CFSwapInt32HostToBig(value)
if val > Int32.max {
return .notFound
}
let frameSize = id3Tag.frameSize(Int32(val))
let bytesLeft = frameSize - id3Tag.frameSizeOffset - 4
if frameHeader == id3Tag.artworkHeader {
var image: (ID3ArtworkFormat, Int)?
outer: for i in 0 ..< frameSize - 4 {
if let head = stream.next(4, offset: i, peek: true) {
for format in ID3ArtworkFormat.allCases {
if head.prefix(format.magic.count) == format.magic {
image = (format, i)
break outer
}
}
}
}
if let (format, offset) = image {
stream.skip(offset)
switch format {
case .jpg:
var data = Data(capacity: frameSize + 1024)
var previousByte: UInt8 = 0xff
let limit = Int(Double(frameSize - offset) * 0.8)
for _ in 0 ..< frameSize - offset {
if let byte: UInt8 = stream.nextValue() {
data.append(byte)
if byte == 0xd9 && previousByte == 0xff && data.count > limit {
break
}
previousByte = byte
} else {
return .moreDataNeeded(tagSize)
}
}
return .artworkData(data)
case .png:
if let data = stream.next(frameSize - offset) {
return .artworkData(data)
} else {
return .moreDataNeeded(tagSize)
}
}
}
} else if frameHeader.prefix(3) == tagEnding {
return .notFound
}
stream.skip(bytesLeft)
guard let rawTagSize = readBigEndianUInt32(in: data, at: 6), let tagPayloadSize = decodeSynchsafeUInt32(rawTagSize) else {
return .notFound
}
guard let totalTagSize = checkedAdd(ID3Tag.headerLength, tagPayloadSize), totalTagSize <= ID3ArtworkReaderLimits.maximumTagSize else {
return .notFound
}
let availableTagEnd = min(totalTagSize, data.count)
var frameOffset = ID3Tag.headerLength
while frameOffset < availableTagEnd {
let remainingTagBytes = availableTagEnd - frameOffset
if remainingTagBytes < id3Tag.frameHeaderLength {
return totalTagSize > data.count ? .moreDataNeeded(totalTagSize) : .notFound
}
guard let frameIdentifierRange = makeRange(start: frameOffset, length: id3Tag.frameIdentifierLength, upperBound: data.count) else {
return totalTagSize > data.count ? .moreDataNeeded(totalTagSize) : .notFound
}
let frameIdentifier = data.subdata(in: frameIdentifierRange)
if frameIdentifier.prefix(3) == tagEnding {
return .notFound
}
guard let framePayloadSize = id3Tag.frameDataSize(in: data, at: frameOffset), framePayloadSize <= ID3ArtworkReaderLimits.maximumFrameSize else {
return .notFound
}
guard let frameSize = checkedAdd(id3Tag.frameHeaderLength, framePayloadSize), let frameEnd = checkedAdd(frameOffset, frameSize) else {
return .notFound
}
guard frameEnd <= totalTagSize else {
return .notFound
}
if frameEnd > data.count {
return .moreDataNeeded(totalTagSize)
}
if frameIdentifier == id3Tag.artworkHeader {
let frameDataStart = frameOffset + id3Tag.frameHeaderLength
if let artworkData = extractArtworkData(from: data, frameDataRange: frameDataStart ..< frameEnd) {
return .artworkData(artworkData)
}
}
frameOffset = frameEnd
}
return .notFound
}

View file

@ -168,8 +168,10 @@ private final class SheetContent: CombinedComponent {
case let .invoice(invoice, replyMarkupValue):
media = [invoice]
replyMarkup = replyMarkupValue
default:
break
case let .webpage(textValue, entitiesValue, _, _, replyMarkupValue):
text = textValue
entities = entitiesValue
replyMarkup = replyMarkupValue
}
case let .externalReference(reference):
switch reference.message {
@ -195,8 +197,10 @@ private final class SheetContent: CombinedComponent {
case let .invoice(invoice, replyMarkupValue):
media = [invoice]
replyMarkup = replyMarkupValue
default:
break
case let .webpage(textValue, entitiesValue, _, _, replyMarkupValue):
text = textValue
entities = entitiesValue
replyMarkup = replyMarkupValue
}
}