Various improvements

This commit is contained in:
Ilya Laktyushin
2026-03-22 16:41:22 +01:00
parent 6aa99e3553
commit 8098f24f4e
26 changed files with 251 additions and 75 deletions
@@ -15983,3 +15983,12 @@ Error: %8$@";
"Notification.PollAddedOptionYou" = "You added the option \"%1$@\" to the poll";
"Notification.ManagedBotCreated" = "%@ bot created";
"Chat.PolOptionAddedTimestamp.Date" = "Added by %1$@ %2$@";
"Chat.PolOptionAddedTimestamp.TodayAt" = "Added by %1$@ today at %2$@";
"Chat.PolOptionAddedTimestamp.YesterdayAt" = "Added by %1$@ yesterday at %2$@";
"Chat.PolOptionAddedTimestampYou.Date" = "Added by you %1$@";
"Chat.PolOptionAddedTimestampYou.TodayAt" = "Added by you today at %1$@";
"Chat.PolOptionAddedTimestampYou.YesterdayAt" = "Added by you yesterday at %1$@";
@@ -1390,7 +1390,7 @@ public protocol SharedAccountContext: AnyObject {
func makeBirthdayPrivacyController(context: AccountContext, settings: Promise<AccountPrivacySettings?>, openedFromBirthdayScreen: Bool, present: @escaping (ViewController) -> Void)
func makeSetupTwoFactorAuthController(context: AccountContext) -> ViewController
func makeStorageManagementController(context: AccountContext) -> ViewController
func makeAttachmentFileController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)?, audio: Bool, bannedSendMedia: (Int32, Bool)?, presentGallery: @escaping () -> Void, presentFiles: @escaping () -> Void, presentDocumentScanner: (() -> Void)?, send: @escaping ([AnyMediaReference]) -> Void) -> AttachmentFileController
func makeAttachmentFileController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)?, audio: Bool, bannedSendMedia: (Int32, Bool)?, presentGallery: @escaping () -> Void, presentFiles: @escaping () -> Void, presentDocumentScanner: (() -> Void)?, send: @escaping ([AnyMediaReference], Bool, Int32?, NSAttributedString?) -> Void) -> AttachmentFileController
func makeGalleryCaptionPanelView(context: AccountContext, chatLocation: ChatLocation, isScheduledMessages: Bool, isFile: Bool, hasTimer: Bool, customEmojiAvailable: Bool, present: @escaping (ViewController) -> Void, presentInGlobalOverlay: @escaping (ViewController) -> Void) -> NSObject?
func makeHashtagSearchController(context: AccountContext, peer: EnginePeer?, query: String, stories: Bool, forceDark: Bool) -> ViewController
func makeStorySearchController(context: AccountContext, scope: StorySearchControllerScope, listContext: SearchStoryListContext?) -> ViewController
@@ -263,7 +263,7 @@ public enum ChatControllerInteractionLongTapAction {
public enum ChatHistoryMessageSelection: Equatable {
case none
case selectable(selected: Bool)
case selectable(selected: Bool, num: Int?)
public static func ==(lhs: ChatHistoryMessageSelection, rhs: ChatHistoryMessageSelection) -> Bool {
switch lhs {
@@ -273,8 +273,8 @@ public enum ChatHistoryMessageSelection: Equatable {
} else {
return false
}
case let .selectable(selected):
if case .selectable(selected) = rhs {
case let .selectable(selected, num):
if case .selectable(selected, num) = rhs {
return true
} else {
return false
@@ -1148,7 +1148,7 @@ public enum ChatListSearchEntry: Comparable, Identifiable {
})
}
}
let selection: ChatHistoryMessageSelection = selected.flatMap { .selectable(selected: $0) } ?? .none
let selection: ChatHistoryMessageSelection = selected.flatMap { .selectable(selected: $0, num: nil) } ?? .none
var isMedia = false
if let tagMask, tagMask != .photoOrVideo {
isMedia = true
@@ -5886,7 +5886,7 @@ public final class ChatListSearchShimmerNode: ASDisplayNode {
associatedStories: [:]
)
return ListMessageItem(presentationData: ChatPresentationData(presentationData: presentationData), context: context, chatLocation: .peer(id: peer1.id), interaction: ListMessageItemInteraction.default, message: message._asMessage(), selection: hasSelection ? .selectable(selected: false) : .none, displayHeader: false, customHeader: nil, hintIsLink: true, isGlobalSearchResult: true)
return ListMessageItem(presentationData: ChatPresentationData(presentationData: presentationData), context: context, chatLocation: .peer(id: peer1.id), interaction: ListMessageItemInteraction.default, message: message._asMessage(), selection: hasSelection ? .selectable(selected: false, num: nil) : .none, displayHeader: false, customHeader: nil, hintIsLink: true, isGlobalSearchResult: true)
case .files:
var media: [EngineMedia] = []
media.append(.file(TelegramMediaFile(fileId: EngineMedia.Id(namespace: 0, id: 0), partialReference: nil, resource: LocalFileMediaResource(fileId: 0), previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "application/text", size: 0, attributes: [.FileName(fileName: "Text.txt")], alternativeRepresentations: [])))
@@ -5917,7 +5917,7 @@ public final class ChatListSearchShimmerNode: ASDisplayNode {
associatedStories: [:]
)
return ListMessageItem(presentationData: ChatPresentationData(presentationData: presentationData), context: context, chatLocation: .peer(id: peer1.id), interaction: ListMessageItemInteraction.default, message: message._asMessage(), selection: hasSelection ? .selectable(selected: false) : .none, displayHeader: false, customHeader: nil, hintIsLink: false, isGlobalSearchResult: true)
return ListMessageItem(presentationData: ChatPresentationData(presentationData: presentationData), context: context, chatLocation: .peer(id: peer1.id), interaction: ListMessageItemInteraction.default, message: message._asMessage(), selection: hasSelection ? .selectable(selected: false, num: nil) : .none, displayHeader: false, customHeader: nil, hintIsLink: false, isGlobalSearchResult: true)
case .music:
var media: [EngineMedia] = []
media.append(.file(TelegramMediaFile(fileId: EngineMedia.Id(namespace: 0, id: 0), partialReference: nil, resource: LocalFileMediaResource(fileId: 0), previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "audio/ogg", size: 0, attributes: [.Audio(isVoice: false, duration: 0, title: nil, performer: nil, waveform: Data())], alternativeRepresentations: [])))
@@ -5948,7 +5948,7 @@ public final class ChatListSearchShimmerNode: ASDisplayNode {
associatedStories: [:]
)
return ListMessageItem(presentationData: ChatPresentationData(presentationData: presentationData), context: context, chatLocation: .peer(id: peer1.id), interaction: ListMessageItemInteraction.default, message: message._asMessage(), selection: hasSelection ? .selectable(selected: false) : .none, displayHeader: false, customHeader: nil, hintIsLink: false, isGlobalSearchResult: true)
return ListMessageItem(presentationData: ChatPresentationData(presentationData: presentationData), context: context, chatLocation: .peer(id: peer1.id), interaction: ListMessageItemInteraction.default, message: message._asMessage(), selection: hasSelection ? .selectable(selected: false, num: nil) : .none, displayHeader: false, customHeader: nil, hintIsLink: false, isGlobalSearchResult: true)
case .voice, .instantVideo:
var media: [EngineMedia] = []
media.append(.file(TelegramMediaFile(fileId: EngineMedia.Id(namespace: 0, id: 0), partialReference: nil, resource: LocalFileMediaResource(fileId: 0), previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "audio/ogg", size: 0, attributes: [.Audio(isVoice: true, duration: 0, title: nil, performer: nil, waveform: Data())], alternativeRepresentations: [])))
@@ -5979,7 +5979,7 @@ public final class ChatListSearchShimmerNode: ASDisplayNode {
associatedStories: [:]
)
return ListMessageItem(presentationData: ChatPresentationData(presentationData: presentationData), context: context, chatLocation: .peer(id: peer1.id), interaction: ListMessageItemInteraction.default, message: message._asMessage(), selection: hasSelection ? .selectable(selected: false) : .none, displayHeader: false, customHeader: nil, hintIsLink: false, isGlobalSearchResult: true)
return ListMessageItem(presentationData: ChatPresentationData(presentationData: presentationData), context: context, chatLocation: .peer(id: peer1.id), interaction: ListMessageItemInteraction.default, message: message._asMessage(), selection: hasSelection ? .selectable(selected: false, num: nil) : .none, displayHeader: false, customHeader: nil, hintIsLink: false, isGlobalSearchResult: true)
}
}
@@ -2395,7 +2395,7 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
selectionControlStyle = .compact
}
let sizeAndApply = selectableControlLayout(item.presentationData.theme.list.itemCheckColors.strokeColor, item.presentationData.theme.list.itemCheckColors.fillColor, item.presentationData.theme.list.itemCheckColors.foregroundColor, item.selected, selectionControlStyle)
let sizeAndApply = selectableControlLayout(item.presentationData.theme.list.itemCheckColors.strokeColor, item.presentationData.theme.list.itemCheckColors.fillColor, item.presentationData.theme.list.itemCheckColors.foregroundColor, item.selected, selectionControlStyle, nil)
if promoInfo == nil && !isPeerGroup {
selectableControlSizeAndApply = sizeAndApply
}
@@ -184,7 +184,7 @@ public final class BalancedTextComponent: Component {
var bestSize: (availableWidth: CGFloat, info: TextNodeLayout)
let info = self.textView.updateLayoutFullInfo(availableSize)
let info = self.textView.updateLayoutFullInfo(availableSize)
bestSize = (availableSize.width, info)
if component.balanced && info.numberOfLines > 1 {
@@ -191,7 +191,7 @@ public class ItemListAddressItemNode: ListViewItemNode {
var leftOffset: CGFloat = 0.0
var selectionNodeWidthAndApply: (CGFloat, (CGSize, Bool) -> ItemListSelectableControlNode)?
if let selected = item.selected {
let (selectionWidth, selectionApply) = selectionNodeLayout(item.theme.list.itemCheckColors.strokeColor, item.theme.list.itemCheckColors.fillColor, item.theme.list.itemCheckColors.foregroundColor, selected, .regular)
let (selectionWidth, selectionApply) = selectionNodeLayout(item.theme.list.itemCheckColors.strokeColor, item.theme.list.itemCheckColors.fillColor, item.theme.list.itemCheckColors.foregroundColor, selected, .regular, nil)
selectionNodeWidthAndApply = (selectionWidth, selectionApply)
leftOffset += selectionWidth - 8.0
}
@@ -451,7 +451,7 @@ class ItemListStickerPackItemNode: ItemListRevealOptionsItemNode {
if case let .check(checked) = item.control {
selected = checked
}
let sizeAndApply = selectableControlLayout(item.presentationData.theme.list.itemCheckColors.strokeColor, item.presentationData.theme.list.itemCheckColors.fillColor, item.presentationData.theme.list.itemCheckColors.foregroundColor, selected, .compact)
let sizeAndApply = selectableControlLayout(item.presentationData.theme.list.itemCheckColors.strokeColor, item.presentationData.theme.list.itemCheckColors.fillColor, item.presentationData.theme.list.itemCheckColors.foregroundColor, selected, .compact, nil)
selectableControlSizeAndApply = sizeAndApply
editingOffset = sizeAndApply.0
} else {
@@ -22,8 +22,8 @@ public final class ItemListSelectableControlNode: ASDisplayNode {
self.addSubnode(self.checkNode)
}
public static func asyncLayout(_ node: ItemListSelectableControlNode?) -> (_ strokeColor: UIColor, _ fillColor: UIColor, _ foregroundColor: UIColor, _ selected: Bool, _ style: Style) -> (CGFloat, (CGSize, Bool) -> ItemListSelectableControlNode) {
return { strokeColor, fillColor, foregroundColor, selected, style in
public static func asyncLayout(_ node: ItemListSelectableControlNode?) -> (_ strokeColor: UIColor, _ fillColor: UIColor, _ foregroundColor: UIColor, _ selected: Bool, _ style: Style, _ num: Int?) -> (CGFloat, (CGSize, Bool) -> ItemListSelectableControlNode) {
return { strokeColor, fillColor, foregroundColor, selected, style, num in
let resultNode: ItemListSelectableControlNode
if let node = node {
resultNode = node
@@ -53,6 +53,9 @@ public final class ItemListSelectableControlNode: ASDisplayNode {
checkOffset = 16.0
}
resultNode.checkNode.frame = CGRect(origin: CGPoint(x: checkOffset, y: floorToScreenPixels((size.height - checkSize.height) / 2.0)), size: checkSize)
if let num {
resultNode.checkNode.content = .counter(num)
}
resultNode.checkNode.setSelected(selected, animated: animated)
return resultNode
})
@@ -189,7 +189,7 @@ public class ItemListTextWithLabelItemNode: ListViewItemNode {
var leftOffset: CGFloat = 0.0
var selectionNodeWidthAndApply: (CGFloat, (CGSize, Bool) -> ItemListSelectableControlNode)?
if let selected = item.selected {
let (selectionWidth, selectionApply) = selectionNodeLayout(item.presentationData.theme.list.itemCheckColors.strokeColor, item.presentationData.theme.list.itemCheckColors.fillColor, item.presentationData.theme.list.itemCheckColors.foregroundColor, selected, .regular)
let (selectionWidth, selectionApply) = selectionNodeLayout(item.presentationData.theme.list.itemCheckColors.strokeColor, item.presentationData.theme.list.itemCheckColors.fillColor, item.presentationData.theme.list.itemCheckColors.foregroundColor, selected, .regular, nil)
selectionNodeWidthAndApply = (selectionWidth, selectionApply)
leftOffset += selectionWidth - 8.0
}
@@ -638,8 +638,8 @@ public final class ListMessageFileItemNode: ListMessageNode {
var leftOffset: CGFloat = 0.0
var selectionNodeWidthAndApply: (CGFloat, (CGSize, Bool) -> ItemListSelectableControlNode)?
if case let .selectable(selected) = item.selection {
let (selectionWidth, selectionApply) = selectionNodeLayout(item.presentationData.theme.theme.list.itemCheckColors.strokeColor, item.presentationData.theme.theme.list.itemCheckColors.fillColor, item.presentationData.theme.theme.list.itemCheckColors.foregroundColor, selected, .regular)
if case let .selectable(selected, num) = item.selection {
let (selectionWidth, selectionApply) = selectionNodeLayout(item.presentationData.theme.theme.list.itemCheckColors.strokeColor, item.presentationData.theme.theme.list.itemCheckColors.fillColor, item.presentationData.theme.theme.list.itemCheckColors.foregroundColor, selected, .regular, num.flatMap { $0 + 1 })
selectionNodeWidthAndApply = (selectionWidth, selectionApply)
leftOffset += selectionWidth
}
@@ -245,7 +245,7 @@ public final class ListMessageItem: ListViewItem, ItemListItem {
return
}
if case let .selectable(selected) = self.selection {
if case let .selectable(selected, _) = self.selection {
self.interaction.toggleMessagesSelection([message.id], !selected)
} else {
if !self.displayFileInfo || self.isAttachMusic {
@@ -267,8 +267,8 @@ public final class ListMessageSnippetItemNode: ListMessageNode {
var leftOffset: CGFloat = 0.0
var selectionNodeWidthAndApply: (CGFloat, (CGSize, Bool) -> ItemListSelectableControlNode)?
if case let .selectable(selected) = item.selection {
let (selectionWidth, selectionApply) = selectionNodeLayout(item.presentationData.theme.theme.list.itemCheckColors.strokeColor, item.presentationData.theme.theme.list.itemCheckColors.fillColor, item.presentationData.theme.theme.list.itemCheckColors.foregroundColor, selected, .regular)
if case let .selectable(selected, num) = item.selection {
let (selectionWidth, selectionApply) = selectionNodeLayout(item.presentationData.theme.theme.list.itemCheckColors.strokeColor, item.presentationData.theme.theme.list.itemCheckColors.fillColor, item.presentationData.theme.theme.list.itemCheckColors.foregroundColor, selected, .regular, num.flatMap { $0 + 1 })
selectionNodeWidthAndApply = (selectionWidth, selectionApply)
leftOffset += selectionWidth
}
@@ -303,7 +303,7 @@ class GiftOptionItemNode: ItemListRevealOptionsItemNode {
var editingOffset: CGFloat = 0.0
if let isSelected = item.isSelected {
let sizeAndApply = selectableControlLayout(item.presentationData.theme.list.itemCheckColors.strokeColor, item.presentationData.theme.list.itemCheckColors.fillColor, item.presentationData.theme.list.itemCheckColors.foregroundColor, isSelected, .regular)
let sizeAndApply = selectableControlLayout(item.presentationData.theme.list.itemCheckColors.strokeColor, item.presentationData.theme.list.itemCheckColors.fillColor, item.presentationData.theme.list.itemCheckColors.foregroundColor, isSelected, .regular, nil)
selectableControlSizeAndApply = sizeAndApply
editingOffset = sizeAndApply.0
}
@@ -404,7 +404,7 @@ final class AttachmentFileContext: AttachmentMediaPickerContext {
}
func send(mode: AttachmentMediaPickerSendMode, attachmentMode: AttachmentMediaPickerAttachmentMode, parameters: ChatSendMessageActionSheetController.SendParameters?) {
self.controller?.mulitpleCompletion?(mode, .files, parameters)
self.controller?.mulitpleCompletion?(mode, .files, parameters, self.controller?.caption)
}
func schedule(parameters: ChatSendMessageActionSheetController.SendParameters?) {
@@ -436,7 +436,7 @@ public class AttachmentFileControllerImpl: ItemListController, AttachmentFileCon
fileprivate var bottomEdgeColor: UIColor = .clear
fileprivate var mulitpleCompletion: ((AttachmentMediaPickerSendMode, AttachmentMediaPickerAttachmentMode, ChatSendMessageActionSheetController.SendParameters?) -> Void)?
fileprivate var mulitpleCompletion: ((AttachmentMediaPickerSendMode, AttachmentMediaPickerAttachmentMode, ChatSendMessageActionSheetController.SendParameters?, NSAttributedString?) -> Void)?
var delayDisappear = false
@@ -508,15 +508,19 @@ private struct AttachmentFileControllerState: Equatable {
var searching: Bool
var savedMusicExpanded: Bool
var recentMusicExpanded: Bool
var selectedMessageIds: Set<MessageId>?
var selectedMessageIds: [MessageId]?
var messageMap: [MessageId: EngineMessage]
}
private func messageSelectionState(state: AttachmentFileControllerState, message: Message?) -> ChatHistoryMessageSelection {
guard let message = message, let selectedMessageIds = state.selectedMessageIds else {
guard let message, let selectedMessageIds = state.selectedMessageIds else {
return .none
}
return .selectable(selected: selectedMessageIds.contains(message.id))
if let index = selectedMessageIds.firstIndex(where: { $0 == message.id }) {
return .selectable(selected: true, num: index)
} else {
return .selectable(selected: false, num: nil)
}
}
public enum AttachmentFileControllerMode {
@@ -551,7 +555,7 @@ public func makeAttachmentFileControllerImpl(
presentGallery: @escaping () -> Void,
presentFiles: @escaping () -> Void,
presentDocumentScanner: (() -> Void)?,
send: @escaping ([AnyMediaReference]) -> Void
send: @escaping ([AnyMediaReference], Bool, Int32?, NSAttributedString?) -> Void
) -> AttachmentFileController {
let actionsDisposable = DisposableSet()
@@ -609,7 +613,7 @@ public func makeAttachmentFileControllerImpl(
send: { message in
if message.id.namespace == Namespaces.Message.Local {
if let file = message.media.first(where: { $0 is TelegramMediaFile }) as? TelegramMediaFile {
send([.standalone(media: file)])
send([.standalone(media: file)], false, nil, nil)
dismissImpl?()
}
} else {
@@ -625,7 +629,7 @@ public func makeAttachmentFileControllerImpl(
}
|> deliverOnMainQueue).startStandalone(next: { messages in
if let message = messages.first, let file = message.media.first(where: { $0 is TelegramMediaFile }) as? TelegramMediaFile {
send([.message(message: MessageReference(message), media: file)])
send([.message(message: MessageReference(message), media: file)], false, nil, nil)
}
dismissImpl?()
})
@@ -647,9 +651,9 @@ public func makeAttachmentFileControllerImpl(
}
let messageId = message.id
if selectedMessageIds.contains(messageId) {
selectedMessageIds.remove(messageId)
selectedMessageIds.removeAll(where: { $0 == messageId })
} else {
selectedMessageIds.insert(messageId)
selectedMessageIds.append(messageId)
}
var updatedState = state
updatedState.selectedMessageIds = selectedMessageIds
@@ -665,9 +669,9 @@ public func makeAttachmentFileControllerImpl(
}
for messageId in messageIds {
if value {
selectedMessageIds.insert(messageId)
selectedMessageIds.append(messageId)
} else {
selectedMessageIds.remove(messageId)
selectedMessageIds.removeAll(where: { $0 == messageId })
}
}
var updatedState = state
@@ -701,8 +705,8 @@ public func makeAttachmentFileControllerImpl(
c?.dismiss(completion: {})
updateState { state in
var updatedState = state
var selectedMessageIds = updatedState.selectedMessageIds ?? Set()
selectedMessageIds.insert(message.id)
var selectedMessageIds = updatedState.selectedMessageIds ?? []
selectedMessageIds.append(message.id)
updatedState.selectedMessageIds = selectedMessageIds
updatedState.messageMap[message.id] = message
updateSelectionCountImpl?(selectedMessageIds.count)
@@ -932,7 +936,7 @@ public func makeAttachmentFileControllerImpl(
}
let controller = AttachmentFileControllerImpl(context: context, state: signal, hideNavigationBarBackground: true)
controller.mulitpleCompletion = { _, _, _ in
controller.mulitpleCompletion = { sendMode, _, _, caption in
let _ = stateValue.with({ state in
if let selectedMessageIds = state.selectedMessageIds {
var mediaReferences: [AnyMediaReference] = []
@@ -941,7 +945,7 @@ public func makeAttachmentFileControllerImpl(
mediaReferences.append(.standalone(media: file))
}
}
send(mediaReferences)
send(mediaReferences, sendMode == .silently, nil, caption)
dismissImpl?()
}
})
@@ -1026,7 +1030,7 @@ public func storyAudioPickerController(
let filePickerController = makeAttachmentFileControllerImpl(context: context, updatedPresentationData: updatedPresentationData, mode: .audio(story: true), bannedSendMedia: nil, presentGallery: {}, presentFiles: {
selectFromFiles()
dismissImpl?()
}, presentDocumentScanner: nil, send: { files in
}, presentDocumentScanner: nil, send: { files, _, _, _ in
completion(files.first!)
dismissImpl?()
}) as! AttachmentFileControllerImpl
@@ -348,16 +348,30 @@ struct AttachmentFileSearchContainerTransition {
let isSearching: Bool
let isEmpty: Bool
let query: String
let crossfade: Bool
}
private func attachmentFileSearchContainerPreparedRecentTransition(from fromEntries: [AttachmentFileSearchEntry], to toEntries: [AttachmentFileSearchEntry], isSearching: Bool, isEmpty: Bool, query: String, context: AccountContext, presentationData: PresentationData, nameSortOrder: PresentationPersonNameOrder, nameDisplayOrder: PresentationPersonNameOrder, interaction: AttachmentFileSearchContainerInteraction, mode: AttachmentFileControllerMode) -> AttachmentFileSearchContainerTransition {
private func attachmentFileSearchContainerPreparedRecentTransition(
from fromEntries: [AttachmentFileSearchEntry],
to toEntries: [AttachmentFileSearchEntry],
isSearching: Bool,
isEmpty: Bool,
query: String,
context: AccountContext,
presentationData: PresentationData,
nameSortOrder: PresentationPersonNameOrder,
nameDisplayOrder: PresentationPersonNameOrder,
interaction: AttachmentFileSearchContainerInteraction,
mode: AttachmentFileControllerMode,
crossfade: Bool
) -> AttachmentFileSearchContainerTransition {
let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries)
let deletions = deleteIndices.map { ListViewDeleteItem(index: $0, directionHint: nil) }
let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, presentationData: presentationData, nameSortOrder: nameSortOrder, nameDisplayOrder: nameDisplayOrder, interaction: interaction, mode: mode), directionHint: nil) }
let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, presentationData: presentationData, nameSortOrder: nameSortOrder, nameDisplayOrder: nameDisplayOrder, interaction: interaction, mode: mode), directionHint: nil) }
return AttachmentFileSearchContainerTransition(deletions: deletions, insertions: insertions, updates: updates, isSearching: isSearching, isEmpty: isEmpty, query: query)
return AttachmentFileSearchContainerTransition(deletions: deletions, insertions: insertions, updates: updates, isSearching: isSearching, isEmpty: isEmpty, query: query, crossfade: crossfade)
}
@@ -553,7 +567,9 @@ public final class AttachmentFileSearchContainerNode: SearchDisplayControllerCon
}
)
if let data = context.currentAppConfiguration.with({ $0 }).data, let searchBot = data["music_search_username"] as? String, !searchBot.isEmpty {
let trimmedQuery = query.trimmingCharacters(in: .whitespacesAndNewlines)
if let data = context.currentAppConfiguration.with({ $0 }).data, let searchBot = data["music_search_username"] as? String, !searchBot.isEmpty, trimmedQuery.count >= 3 {
globalMusic = .single(nil)
|> then(
context.engine.peers.resolvePeerByName(name: searchBot, referrer: nil)
@@ -567,7 +583,7 @@ public final class AttachmentFileSearchContainerNode: SearchDisplayControllerCon
guard let peer = peer else {
return .single(nil)
}
return context.engine.messages.requestChatContextResults(botId: peer.id, peerId: context.account.peerId, query: query, offset: "")
return context.engine.messages.requestChatContextResults(botId: peer.id, peerId: context.account.peerId, query: trimmedQuery, offset: "")
|> map { results -> ChatContextResultCollection? in
return results?.results
}
@@ -673,13 +689,27 @@ public final class AttachmentFileSearchContainerNode: SearchDisplayControllerCon
}
let previousSearchItems = Atomic<[AttachmentFileSearchEntry]?>(value: nil)
let previousHadGlobalItems = Atomic<Bool>(value: false)
self.searchDisposable.set((combineLatest(searchQuery, foundItems, self.presentationDataPromise.get())
|> deliverOnMainQueue).startStrict(next: { [weak self] query, entries, presentationData in
if let strongSelf = self {
let previousEntries = previousSearchItems.swap(entries)
updateActivity(false)
let firstTime = previousEntries == nil
let transition = attachmentFileSearchContainerPreparedRecentTransition(from: previousEntries ?? [], to: entries ?? [], isSearching: entries != nil, isEmpty: entries?.isEmpty ?? false, query: query ?? "", context: context, presentationData: presentationData, nameSortOrder: presentationData.nameSortOrder, nameDisplayOrder: presentationData.nameDisplayOrder, interaction: interaction, mode: mode)
var hasGlobalItems = false
if let entries {
for entry in entries {
if case let .header(_, section) = entry, section == 2 {
hasGlobalItems = true
}
}
}
let hadGlobalItems = previousHadGlobalItems.swap(hasGlobalItems)
let crossfade = hadGlobalItems != hasGlobalItems
let transition = attachmentFileSearchContainerPreparedRecentTransition(from: previousEntries ?? [], to: entries ?? [], isSearching: entries != nil, isEmpty: entries?.isEmpty ?? false, query: query ?? "", context: context, presentationData: presentationData, nameSortOrder: presentationData.nameSortOrder, nameDisplayOrder: presentationData.nameDisplayOrder, interaction: interaction, mode: mode, crossfade: crossfade)
strongSelf.enqueueTransition(transition, firstTime: firstTime)
}
}))
@@ -739,6 +769,10 @@ public final class AttachmentFileSearchContainerNode: SearchDisplayControllerCon
//options.insert(.AnimateInsertion)
if transition.crossfade {
options.insert(.AnimateCrossfade)
}
let isSearching = transition.isSearching
self.listNode.transaction(deleteIndices: transition.deletions, insertIndicesAndItems: transition.insertions, updateIndicesAndItems: transition.updates, options: options, updateSizeAndInsets: nil, updateOpaqueState: nil, completion: { [weak self] _ in
guard let strongSelf = self else {
@@ -2183,7 +2183,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
switch selection {
case .none:
break
case let .selectable(selected):
case let .selectable(selected, _):
itemSelection = selected
}
break
@@ -100,7 +100,7 @@ public func presentPollAttachmentScreen(
//TODO
},
presentDocumentScanner: nil,
send: { mediaReferences in
send: { mediaReferences, _, _, _ in
completion(mediaReferences.first!)
}
) as! AttachmentFileControllerImpl
@@ -35,6 +35,7 @@ swift_library(
"//submodules/UIKitRuntimeUtils",
"//submodules/UndoUI",
"//submodules/TelegramUI/Components/LensTransition",
"//submodules/Components/BalancedTextComponent",
],
visibility = [
"//visibility:public",
@@ -20,6 +20,7 @@ import LottieComponent
import TextNodeWithEntities
import ContextUI
import LensTransition
import BalancedTextComponent
public protocol ContextControllerActionsListItemNode: ASDisplayNode {
func update(presentationData: PresentationData, constrainedSize: CGSize) -> (minSize: CGSize, apply: (_ size: CGSize, _ transition: ContainedViewLayoutTransition) -> Void)
@@ -39,6 +40,7 @@ public final class ContextControllerActionsListActionItemNode: HighlightTracking
private var item: ContextMenuActionItem
private let titleLabelNode: ImmediateTextNodeWithEntities
private let balancedTitleLabel = ComponentView<Empty>()
private let subtitleNode: ImmediateTextNode
private let iconNode: ASImageNode
private let additionalIconNode: ASImageNode
@@ -282,7 +284,13 @@ public final class ContextControllerActionsListActionItemNode: HighlightTracking
titleColor = presentationData.theme.contextMenu.primaryColor.withMultipliedAlpha(0.4)
}
var balanceTitleAttributedText: NSAttributedString?
if self.item.parseMarkdown || !self.item.entities.isEmpty {
var balancedTextLayout = false
if self.item.parseMarkdown, case .twoLinesMax = self.item.textLayout {
balancedTextLayout = true
}
let attributedText: NSAttributedString
if !self.item.entities.isEmpty {
let inputStateText = ChatTextInputStateText(text: self.item.text, attributes: self.item.entities.compactMap { entity -> ChatTextInputStateTextAttribute? in
@@ -324,18 +332,25 @@ public final class ContextControllerActionsListActionItemNode: HighlightTracking
)
)
}
self.titleLabelNode.attributedText = attributedText
self.titleLabelNode.linkHighlightColor = presentationData.theme.list.itemAccentColor.withMultipliedAlpha(0.5)
self.titleLabelNode.highlightAttributeAction = { attributes in
if let _ = attributes[NSAttributedString.Key(rawValue: "URL")] {
return NSAttributedString.Key(rawValue: "URL")
} else {
return nil
if self.item.entities.isEmpty && balancedTextLayout {
self.titleLabelNode.isHidden = true
balanceTitleAttributedText = attributedText
} else {
self.titleLabelNode.isHidden = false
self.titleLabelNode.attributedText = attributedText
self.titleLabelNode.linkHighlightColor = presentationData.theme.list.itemAccentColor.withMultipliedAlpha(0.5)
self.titleLabelNode.highlightAttributeAction = { attributes in
if let _ = attributes[NSAttributedString.Key(rawValue: "URL")] {
return NSAttributedString.Key(rawValue: "URL")
} else {
return nil
}
}
}
self.titleLabelNode.tapAttributeAction = { [weak item] attributes, _ in
if let _ = attributes[NSAttributedString.Key(rawValue: "URL")] {
item?.textLinkAction()
self.titleLabelNode.tapAttributeAction = { [weak item] attributes, _ in
if let _ = attributes[NSAttributedString.Key(rawValue: "URL")] {
item?.textLinkAction()
}
}
}
} else {
@@ -506,7 +521,22 @@ public final class ContextControllerActionsListActionItemNode: HighlightTracking
maxTextWidth = max(1.0, maxTextWidth)
let titleSize = self.titleLabelNode.updateLayout(CGSize(width: maxTextWidth, height: 1000.0))
let titleSize: CGSize
if let balanceTitleAttributedText {
titleSize = self.balancedTitleLabel.update(
transition: .immediate,
component: AnyComponent(
BalancedTextComponent(
text: .plain(balanceTitleAttributedText),
maximumNumberOfLines: 2
)
),
environment: {},
containerSize: CGSize(width: maxTextWidth, height: 1000.0)
)
} else {
titleSize = self.titleLabelNode.updateLayout(CGSize(width: maxTextWidth, height: 1000.0))
}
let subtitleSize = self.subtitleNode.updateLayout(CGSize(width: maxTextWidth, height: 1000.0))
var minSize = CGSize()
@@ -549,7 +579,16 @@ public final class ContextControllerActionsListActionItemNode: HighlightTracking
subtitleFrame.origin.x = titleFrame.minX
}
transition.updateFrameAdditive(node: self.titleLabelNode, frame: titleFrame)
if let _ = balanceTitleAttributedText {
if let balancedTitleLabelView = self.balancedTitleLabel.view {
if balancedTitleLabelView.superview == nil {
self.view.addSubview(balancedTitleLabelView)
}
transition.updateFrameAdditive(view: balancedTitleLabelView, frame: titleFrame)
}
} else {
transition.updateFrameAdditive(node: self.titleLabelNode, frame: titleFrame)
}
transition.updateFrameAdditive(node: self.subtitleNode, frame: subtitleFrame)
if let badgeIconNode = self.badgeIconNode, let iconSize = badgeIconNode.image?.size {
@@ -532,7 +532,7 @@ private final class ItemView: UIView, SparseItemGridView {
}
if let item = self.item, let messageItem = self.messageItem, let itemNode = itemNode as? ListMessageFileItemNode {
if case let .selectable(selected) = messageItem.selection {
if case let .selectable(selected, _) = messageItem.selection {
self.interaction?.toggleMessagesSelection([item.message.id], !selected)
} else {
itemNode.activateMedia()
@@ -560,7 +560,7 @@ private final class ItemView: UIView, SparseItemGridView {
interaction: interaction,
message: item.message,
selection: isSelected.flatMap { isSelected in
return .selectable(selected: isSelected)
return .selectable(selected: isSelected, num: nil)
} ?? .none,
displayHeader: false
)
@@ -1948,7 +1948,7 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) {
}
attachmentController?.dismiss(animated: true)
self.presentICloudFileGallery(view: view, peer: peer, replyMessageId: nil, replyToStoryId: focusedStoryId)
}, presentDocumentScanner: nil, send: { [weak view] mediaReferences in
}, presentDocumentScanner: nil, send: { [weak view] mediaReferences, _, _, _ in
guard let view, let component = view.component else {
return
}
@@ -14,6 +14,7 @@ import ChatControllerInteraction
import Pasteboard
import TelegramStringFormatting
import TelegramPresentationData
import AvatarNode
private enum OptionsId: Hashable {
case item
@@ -37,8 +38,16 @@ extension ChatControllerImpl {
}
}
let _ = (contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState: self.presentationInterfaceState, context: self.context, messages: [message], controllerInteraction: self.controllerInteraction, selectAll: false, interfaceInteraction: self.interfaceInteraction, messageNode: params.messageNode as? ChatMessageItemView)
|> deliverOnMainQueue).start(next: { [weak self] actions in
var addedByPeer: Signal<EnginePeer?, NoError> = .single(nil)
if let peerId = pollOption.addedBy {
addedByPeer = self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId))
}
let _ = combineLatest(
queue: Queue.mainQueue(),
contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState: self.presentationInterfaceState, context: self.context, messages: [message], controllerInteraction: self.controllerInteraction, selectAll: false, interfaceInteraction: self.interfaceInteraction, messageNode: params.messageNode as? ChatMessageItemView),
addedByPeer
).start(next: { [weak self] actions, addedByPeer in
guard let self else {
return
}
@@ -146,7 +155,7 @@ extension ChatControllerImpl {
})))
}
if pollOption.date != nil {
if let addedByPeer, let date = pollOption.date {
//TODO:localize
items.append(.action(ContextMenuActionItem(text: "Remove", textColor: .destructive, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.contextMenu.destructiveColor) }, action: { [weak self] _, f in
f(.default)
@@ -156,6 +165,49 @@ extension ChatControllerImpl {
}
let _ = self.context.engine.messages.deletePollOption(messageId: message.id, opaqueIdentifier: pollOption.opaqueIdentifier).start()
})))
items.append(.separator)
let peerName = "**\(addedByPeer.compactDisplayTitle)**"
let dateText = humanReadableStringForTimestamp(strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat, timestamp: date, alwaysShowTime: true, allowYesterday: true, format: HumanReadableStringFormat(
dateFormatString: { value in
if addedByPeer.id == self.context.account.peerId {
return PresentationStrings.FormattedString(string: self.presentationData.strings.Chat_PolOptionAddedTimestampYou_Date(value).string, ranges: [])
} else {
return PresentationStrings.FormattedString(string: self.presentationData.strings.Chat_PolOptionAddedTimestamp_Date(peerName, value).string, ranges: [])
}
},
tomorrowFormatString: { value in
if addedByPeer.id == self.context.account.peerId {
return PresentationStrings.FormattedString(string: self.presentationData.strings.Chat_PolOptionAddedTimestampYou_TodayAt(value).string, ranges: [])
} else {
return PresentationStrings.FormattedString(string: self.presentationData.strings.Chat_PolOptionAddedTimestamp_TodayAt(peerName, value).string, ranges: [])
}
},
todayFormatString: { value in
if addedByPeer.id == self.context.account.peerId {
return PresentationStrings.FormattedString(string: self.presentationData.strings.Chat_PolOptionAddedTimestampYou_TodayAt(value).string, ranges: [])
} else {
return PresentationStrings.FormattedString(string: self.presentationData.strings.Chat_PolOptionAddedTimestamp_TodayAt(peerName, value).string, ranges: [])
}
},
yesterdayFormatString: { value in
if addedByPeer.id == self.context.account.peerId {
return PresentationStrings.FormattedString(string: self.presentationData.strings.Chat_PolOptionAddedTimestampYou_YesterdayAt(value).string, ranges: [])
} else {
return PresentationStrings.FormattedString(string: self.presentationData.strings.Chat_PolOptionAddedTimestamp_YesterdayAt(peerName, value).string, ranges: [])
}
}
)).string
let avatarSize = CGSize(width: 24.0, height: 24.0)
items.append(.action(ContextMenuActionItem(text: dateText, textFont: .small, parseMarkdown: true, icon: { _ in return nil }, iconSource: ContextMenuActionItemIconSource(size: avatarSize, signal: peerAvatarCompleteImage(account: self.context.account, peer: addedByPeer, size: avatarSize)), action: { [weak self] _, f in
f(.default)
guard let self else {
return
}
self.openPeer(peer: addedByPeer, navigation: .chat(textInputState: nil, subject: nil, peekData: nil), fromMessage: nil)
})))
}
self.canReadHistory.set(false)
@@ -381,7 +381,7 @@ extension ChatControllerImpl {
self?.presentICloudFileGallery()
}, presentDocumentScanner: { [weak self] in
self?.presentDocumentScanner()
}, send: { [weak self] mediaReferences in
}, send: { [weak self] mediaReferences, silentPosting, scheduleTime, caption in
guard let self else {
return
}
@@ -390,9 +390,26 @@ extension ChatControllerImpl {
if mediaReferences.count > 1 {
groupingKey = Int64.random(in: .min ..< .max)
}
for mediaReference in mediaReferences {
messages.append(.message(text: "", attributes: [], inlineStickers: [:], mediaReference: mediaReference, threadId: strongSelf.chatLocation.threadId, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: groupingKey, correlationId: nil, bubbleUpEmojiOrStickersets: []))
var attributes: [MessageAttribute] = []
var text = ""
if let caption {
text = caption.string
let entities = generateTextEntities(text, enabledTypes: .all, currentEntities: generateChatInputTextEntities(caption))
if !entities.isEmpty {
attributes.append(TextEntitiesMessageAttribute(entities: entities))
}
}
for mediaReference in mediaReferences {
if messages.count == 10 {
groupingKey = Int64.random(in: .min ..< .max)
}
let isLast = mediaReference == mediaReferences.last
messages.append(.message(text: isLast ? text : "", attributes: isLast ? attributes : [], inlineStickers: [:], mediaReference: mediaReference, threadId: strongSelf.chatLocation.threadId, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: groupingKey, correlationId: nil, bubbleUpEmojiOrStickersets: []))
}
messages = self.transformEnqueueMessages(messages, silentPosting: silentPosting, scheduleTime: scheduleTime, repeatPeriod: nil, postpone: false)
self.presentPaidMessageAlertIfNeeded(completion: { [weak self] postpone in
self?.sendMessages(messages, media: true, postpone: postpone)
})
@@ -414,7 +431,7 @@ extension ChatControllerImpl {
}, presentFiles: { [weak self, weak attachmentController] in
attachmentController?.dismiss(animated: true)
self?.presentICloudFileGallery(documentTypes: ["public.mp3", "public.mpeg-4-audio", "public.aac-audio", "org.xiph.flac"])
}, presentDocumentScanner: nil, send: { [weak self] mediaReferences in
}, presentDocumentScanner: nil, send: { [weak self] mediaReferences, silentPosting, scheduleTime, caption in
guard let self else {
return
}
@@ -423,9 +440,26 @@ extension ChatControllerImpl {
if mediaReferences.count > 1 {
groupingKey = Int64.random(in: .min ..< .max)
}
for mediaReference in mediaReferences {
messages.append(.message(text: "", attributes: [], inlineStickers: [:], mediaReference: mediaReference, threadId: strongSelf.chatLocation.threadId, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: groupingKey, correlationId: nil, bubbleUpEmojiOrStickersets: []))
var attributes: [MessageAttribute] = []
var text = ""
if let caption {
text = caption.string
let entities = generateTextEntities(text, enabledTypes: .all, currentEntities: generateChatInputTextEntities(caption))
if !entities.isEmpty {
attributes.append(TextEntitiesMessageAttribute(entities: entities))
}
}
for mediaReference in mediaReferences {
if messages.count == 10 {
groupingKey = Int64.random(in: .min ..< .max)
}
let isLast = mediaReference == mediaReferences.last
messages.append(.message(text: isLast ? text : "", attributes: isLast ? attributes : [], inlineStickers: [:], mediaReference: mediaReference, threadId: strongSelf.chatLocation.threadId, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: groupingKey, correlationId: nil, bubbleUpEmojiOrStickersets: []))
}
messages = self.transformEnqueueMessages(messages, silentPosting: silentPosting, scheduleTime: scheduleTime, repeatPeriod: nil, postpone: false)
self.presentPaidMessageAlertIfNeeded(completion: { [weak self] postpone in
self?.sendMessages(messages, media: true, postpone: postpone)
})
@@ -247,7 +247,7 @@ func chatHistoryEntriesForView(
if let messageGroupingKey = message.groupingKey {
let selection: ChatHistoryMessageSelection
if let selectedMessages = selectedMessages {
selection = .selectable(selected: selectedMessages.contains(message.id))
selection = .selectable(selected: selectedMessages.contains(message.id), num: nil)
} else {
selection = .none
}
@@ -293,7 +293,7 @@ func chatHistoryEntriesForView(
} else {
let selection: ChatHistoryMessageSelection
if let selectedMessages = selectedMessages {
selection = .selectable(selected: selectedMessages.contains(message.id))
selection = .selectable(selected: selectedMessages.contains(message.id), num: nil)
} else {
selection = .none
}
@@ -308,7 +308,7 @@ func chatHistoryEntriesForView(
} else {
let selection: ChatHistoryMessageSelection
if let selectedMessages = selectedMessages {
selection = .selectable(selected: selectedMessages.contains(message.id))
selection = .selectable(selected: selectedMessages.contains(message.id), num: nil)
} else {
selection = .none
}
@@ -2750,7 +2750,7 @@ public final class SharedAccountContextImpl: SharedAccountContext {
})
}
public func makeAttachmentFileController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)?, audio: Bool, bannedSendMedia: (Int32, Bool)?, presentGallery: @escaping () -> Void, presentFiles: @escaping () -> Void, presentDocumentScanner: (() -> Void)?, send: @escaping ([AnyMediaReference]) -> Void) -> AttachmentFileController {
public func makeAttachmentFileController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)?, audio: Bool, bannedSendMedia: (Int32, Bool)?, presentGallery: @escaping () -> Void, presentFiles: @escaping () -> Void, presentDocumentScanner: (() -> Void)?, send: @escaping ([AnyMediaReference], Bool, Int32?, NSAttributedString?) -> Void) -> AttachmentFileController {
return makeAttachmentFileControllerImpl(context: context, updatedPresentationData: updatedPresentationData, mode: audio ? .audio(story: false) : .recent, bannedSendMedia: bannedSendMedia, presentGallery: presentGallery, presentFiles: presentFiles, presentDocumentScanner: presentDocumentScanner, send: send)
}