Various improvements

This commit is contained in:
Isaac 2024-06-24 18:19:53 +01:00
parent 13026a5cc4
commit 6807abf42c
21 changed files with 915 additions and 211 deletions

@ -1 +1 @@
Subproject commit 77e00ae3fd2d7f6fae3790420bc95c8f7abb6f7b
Subproject commit db0ce201aa4f2099559d6e4b4373f7de83b81eff

View File

@ -34,6 +34,7 @@ public final class ChatMessageItemAssociatedData: Equatable {
public let automaticDownloadPeerType: MediaAutoDownloadPeerType
public let automaticDownloadPeerId: EnginePeer.Id?
public let automaticDownloadNetworkType: MediaAutoDownloadNetworkType
public let preferredStoryHighQuality: Bool
public let isRecentActions: Bool
public let subject: ChatControllerSubject?
public let contactsPeerIds: Set<EnginePeer.Id>
@ -66,6 +67,7 @@ public final class ChatMessageItemAssociatedData: Equatable {
automaticDownloadPeerType: MediaAutoDownloadPeerType,
automaticDownloadPeerId: EnginePeer.Id?,
automaticDownloadNetworkType: MediaAutoDownloadNetworkType,
preferredStoryHighQuality: Bool = false,
isRecentActions: Bool = false,
subject: ChatControllerSubject? = nil,
contactsPeerIds: Set<EnginePeer.Id> = Set(),
@ -97,6 +99,7 @@ public final class ChatMessageItemAssociatedData: Equatable {
self.automaticDownloadPeerType = automaticDownloadPeerType
self.automaticDownloadPeerId = automaticDownloadPeerId
self.automaticDownloadNetworkType = automaticDownloadNetworkType
self.preferredStoryHighQuality = preferredStoryHighQuality
self.isRecentActions = isRecentActions
self.subject = subject
self.contactsPeerIds = contactsPeerIds
@ -136,6 +139,9 @@ public final class ChatMessageItemAssociatedData: Equatable {
if lhs.automaticDownloadNetworkType != rhs.automaticDownloadNetworkType {
return false
}
if lhs.preferredStoryHighQuality != rhs.preferredStoryHighQuality {
return false
}
if lhs.isRecentActions != rhs.isRecentActions {
return false
}

View File

@ -4741,89 +4741,186 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
])
self.present(actionSheet, in: .window(.root))
} else if !peerIds.isEmpty {
let actionSheet = ActionSheetController(presentationData: self.presentationData)
var items: [ActionSheetItem] = []
items.append(ActionSheetButtonItem(title: self.presentationData.strings.ChatList_DeleteConfirmation(Int32(peerIds.count)), color: .destructive, action: { [weak self, weak actionSheet] in
actionSheet?.dismissAnimated()
guard let strongSelf = self else {
let _ = (self.context.engine.data.get(
EngineDataList(peerIds.map(TelegramEngine.EngineData.Item.Peer.Peer.init(id:)))
)
|> deliverOnMainQueue).start(next: { [weak self] peers in
guard let self else {
return
}
strongSelf.chatListDisplayNode.effectiveContainerNode.updateState(onlyCurrent: false, { state in
var state = state
for peerId in peerIds {
state.pendingRemovalItemIds.insert(ChatListNodeState.ItemId(peerId: peerId, threadId: nil))
var havePrivateChats = false
var haveNonPrivateChats = false
for peer in peers {
if let peer {
switch peer {
case .user, .secretChat:
havePrivateChats = true
default:
haveNonPrivateChats = true
}
}
return state
})
}
let text = strongSelf.presentationData.strings.ChatList_DeletedChats(Int32(peerIds.count))
strongSelf.present(UndoOverlayController(presentationData: strongSelf.context.sharedContext.currentPresentationData.with { $0 }, content: .removedChat(title: text, text: nil), elevatedLayout: false, animateInAsReplacement: true, action: { value in
let actionSheet = ActionSheetController(presentationData: self.presentationData)
var items: [ActionSheetItem] = []
if havePrivateChats {
//TODO:localize
items.append(ActionSheetButtonItem(title: haveNonPrivateChats ? "Delete from both sides where possible" : "Delete from both sides", color: .destructive, action: { [weak self, weak actionSheet] in
actionSheet?.dismissAnimated()
guard let strongSelf = self else {
return
}
strongSelf.chatListDisplayNode.effectiveContainerNode.updateState(onlyCurrent: false, { state in
var state = state
for peerId in peerIds {
state.pendingRemovalItemIds.insert(ChatListNodeState.ItemId(peerId: peerId, threadId: nil))
}
return state
})
let text = strongSelf.presentationData.strings.ChatList_DeletedChats(Int32(peerIds.count))
strongSelf.present(UndoOverlayController(presentationData: strongSelf.context.sharedContext.currentPresentationData.with { $0 }, content: .removedChat(title: text, text: nil), elevatedLayout: false, animateInAsReplacement: true, action: { value in
guard let strongSelf = self else {
return false
}
if value == .commit {
let presentationData = strongSelf.presentationData
let progressSignal = Signal<Never, NoError> { subscriber in
let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: nil))
self?.present(controller, in: .window(.root))
return ActionDisposable { [weak controller] in
Queue.mainQueue().async() {
controller?.dismiss()
}
}
}
|> runOn(Queue.mainQueue())
|> delay(0.8, queue: Queue.mainQueue())
let progressDisposable = progressSignal.start()
let signal: Signal<Never, NoError> = strongSelf.context.engine.peers.removePeerChats(peerIds: Array(peerIds), deleteGloballyIfPossible: true)
|> afterDisposed {
Queue.mainQueue().async {
progressDisposable.dispose()
}
}
let _ = (signal
|> deliverOnMainQueue).start()
strongSelf.chatListDisplayNode.effectiveContainerNode.updateState(onlyCurrent: false, { state in
var state = state
for peerId in peerIds {
state.selectedPeerIds.remove(peerId)
}
return state
})
return true
} else if value == .undo {
strongSelf.chatListDisplayNode.effectiveContainerNode.currentItemNode.setCurrentRemovingItemId(ChatListNodeState.ItemId(peerId: peerIds.first!, threadId: nil))
strongSelf.chatListDisplayNode.effectiveContainerNode.updateState(onlyCurrent: false, { state in
var state = state
for peerId in peerIds {
state.pendingRemovalItemIds.remove(ChatListNodeState.ItemId(peerId: peerId, threadId: nil))
}
return state
})
self?.chatListDisplayNode.effectiveContainerNode.currentItemNode.setCurrentRemovingItemId(ChatListNodeState.ItemId(peerId: peerIds.first!, threadId: nil))
return true
}
return false
}), in: .current)
strongSelf.donePressed()
}))
}
//TODO:localize
items.append(ActionSheetButtonItem(title: havePrivateChats ? "Delete for me" : self.presentationData.strings.ChatList_DeleteConfirmation(Int32(peerIds.count)), color: .destructive, action: { [weak self, weak actionSheet] in
actionSheet?.dismissAnimated()
guard let strongSelf = self else {
return false
return
}
if value == .commit {
let presentationData = strongSelf.presentationData
let progressSignal = Signal<Never, NoError> { subscriber in
let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: nil))
self?.present(controller, in: .window(.root))
return ActionDisposable { [weak controller] in
Queue.mainQueue().async() {
controller?.dismiss()
strongSelf.chatListDisplayNode.effectiveContainerNode.updateState(onlyCurrent: false, { state in
var state = state
for peerId in peerIds {
state.pendingRemovalItemIds.insert(ChatListNodeState.ItemId(peerId: peerId, threadId: nil))
}
return state
})
let text = strongSelf.presentationData.strings.ChatList_DeletedChats(Int32(peerIds.count))
strongSelf.present(UndoOverlayController(presentationData: strongSelf.context.sharedContext.currentPresentationData.with { $0 }, content: .removedChat(title: text, text: nil), elevatedLayout: false, animateInAsReplacement: true, action: { value in
guard let strongSelf = self else {
return false
}
if value == .commit {
let presentationData = strongSelf.presentationData
let progressSignal = Signal<Never, NoError> { subscriber in
let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: nil))
self?.present(controller, in: .window(.root))
return ActionDisposable { [weak controller] in
Queue.mainQueue().async() {
controller?.dismiss()
}
}
}
|> runOn(Queue.mainQueue())
|> delay(0.8, queue: Queue.mainQueue())
let progressDisposable = progressSignal.start()
let signal: Signal<Never, NoError> = strongSelf.context.engine.peers.removePeerChats(peerIds: Array(peerIds))
|> afterDisposed {
Queue.mainQueue().async {
progressDisposable.dispose()
}
}
let _ = (signal
|> deliverOnMainQueue).start()
strongSelf.chatListDisplayNode.effectiveContainerNode.updateState(onlyCurrent: false, { state in
var state = state
for peerId in peerIds {
state.selectedPeerIds.remove(peerId)
}
return state
})
return true
} else if value == .undo {
strongSelf.chatListDisplayNode.effectiveContainerNode.currentItemNode.setCurrentRemovingItemId(ChatListNodeState.ItemId(peerId: peerIds.first!, threadId: nil))
strongSelf.chatListDisplayNode.effectiveContainerNode.updateState(onlyCurrent: false, { state in
var state = state
for peerId in peerIds {
state.pendingRemovalItemIds.remove(ChatListNodeState.ItemId(peerId: peerId, threadId: nil))
}
return state
})
self?.chatListDisplayNode.effectiveContainerNode.currentItemNode.setCurrentRemovingItemId(ChatListNodeState.ItemId(peerId: peerIds.first!, threadId: nil))
return true
}
|> runOn(Queue.mainQueue())
|> delay(0.8, queue: Queue.mainQueue())
let progressDisposable = progressSignal.start()
let signal: Signal<Never, NoError> = strongSelf.context.engine.peers.removePeerChats(peerIds: Array(peerIds))
|> afterDisposed {
Queue.mainQueue().async {
progressDisposable.dispose()
}
}
let _ = (signal
|> deliverOnMainQueue).start()
strongSelf.chatListDisplayNode.effectiveContainerNode.updateState(onlyCurrent: false, { state in
var state = state
for peerId in peerIds {
state.selectedPeerIds.remove(peerId)
}
return state
})
return true
} else if value == .undo {
strongSelf.chatListDisplayNode.effectiveContainerNode.currentItemNode.setCurrentRemovingItemId(ChatListNodeState.ItemId(peerId: peerIds.first!, threadId: nil))
strongSelf.chatListDisplayNode.effectiveContainerNode.updateState(onlyCurrent: false, { state in
var state = state
for peerId in peerIds {
state.pendingRemovalItemIds.remove(ChatListNodeState.ItemId(peerId: peerId, threadId: nil))
}
return state
})
self?.chatListDisplayNode.effectiveContainerNode.currentItemNode.setCurrentRemovingItemId(ChatListNodeState.ItemId(peerId: peerIds.first!, threadId: nil))
return true
}
return false
}), in: .current)
return false
}), in: .current)
strongSelf.donePressed()
}))
strongSelf.donePressed()
}))
actionSheet.setItemGroups([
ActionSheetItemGroup(items: items),
ActionSheetItemGroup(items: [
ActionSheetButtonItem(title: self.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
})
actionSheet.setItemGroups([
ActionSheetItemGroup(items: items),
ActionSheetItemGroup(items: [
ActionSheetButtonItem(title: self.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
})
])
])
])
self.present(actionSheet, in: .window(.root))
self.present(actionSheet, in: .window(.root))
})
}
} else if case .middle = action {
switch self.chatListDisplayNode.effectiveContainerNode.location {

View File

@ -423,6 +423,11 @@ final class MessageItemView: UIView {
}
let messageAttributedText = NSMutableAttributedString(attributedString: textString)
for entity in generateTextEntities(textString.string, enabledTypes: .all) {
messageAttributedText.addAttribute(.foregroundColor, value: presentationData.theme.chat.message.outgoing.linkTextColor, range: NSRange(location: entity.range.lowerBound, length: entity.range.upperBound - entity.range.lowerBound))
}
textNode.attributedText = messageAttributedText
}
@ -620,6 +625,11 @@ final class MessageItemView: UIView {
}
let messageAttributedText = NSMutableAttributedString(attributedString: textString)
for entity in generateTextEntities(textString.string, enabledTypes: .all) {
messageAttributedText.addAttribute(.foregroundColor, value: presentationData.theme.chat.message.outgoing.linkTextColor, range: NSRange(location: entity.range.lowerBound, length: entity.range.upperBound - entity.range.lowerBound))
}
textNode.attributedText = messageAttributedText
}

View File

@ -4,6 +4,7 @@ import Display
import AsyncDisplayKit
import SwiftSignalKit
import TelegramCore
import Postbox
import TelegramPresentationData
import TelegramUIPreferences
import DeviceAccess
@ -379,7 +380,7 @@ private enum ContactListNodeEntry: Comparable, Identifiable {
}
}
private func contactListNodeEntries(accountPeer: EnginePeer?, peers: [ContactListPeer], presences: [EnginePeer.Id: EnginePeer.Presence], presentation: ContactListPresentation, selectionState: ContactListNodeGroupSelectionState?, theme: PresentationTheme, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, sortOrder: PresentationPersonNameOrder, displayOrder: PresentationPersonNameOrder, disabledPeerIds: Set<EnginePeer.Id>, peerRequiresPremiumForMessaging: [EnginePeer.Id: Bool], authorizationStatus: AccessType, warningSuppressed: (Bool, Bool), displaySortOptions: Bool, displayCallIcons: Bool, storySubscriptions: EngineStorySubscriptions?, topPeers: [EnginePeer], topPeersPresentation: ContactListPresentation.TopPeers, interaction: ContactListNodeInteraction) -> [ContactListNodeEntry] {
private func contactListNodeEntries(accountPeer: EnginePeer?, peers: [ContactListPeer], presences: [EnginePeer.Id: EnginePeer.Presence], presentation: ContactListPresentation, selectionState: ContactListNodeGroupSelectionState?, theme: PresentationTheme, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, sortOrder: PresentationPersonNameOrder, displayOrder: PresentationPersonNameOrder, disabledPeerIds: Set<EnginePeer.Id>, peerRequiresPremiumForMessaging: [EnginePeer.Id: Bool], peersWithStories: [EnginePeer.Id: PeerStoryStats], authorizationStatus: AccessType, warningSuppressed: (Bool, Bool), displaySortOptions: Bool, displayCallIcons: Bool, storySubscriptions: EngineStorySubscriptions?, topPeers: [EnginePeer], topPeersPresentation: ContactListPresentation.TopPeers, interaction: ContactListNodeInteraction) -> [ContactListNodeEntry] {
var entries: [ContactListNodeEntry] = []
var commonHeader: ListViewItemHeader?
@ -665,7 +666,9 @@ private func contactListNodeEntries(accountPeer: EnginePeer?, peers: [ContactLis
}
let presence = presences[peer.id]
entries.append(.peer(index, .peer(peer: peer._asPeer(), isGlobal: false, participantCount: nil), presence, header, selection, theme, strings, dateTimeFormat, sortOrder, displayOrder, false, false, true, nil, false))
entries.append(.peer(index, .peer(peer: peer._asPeer(), isGlobal: false, participantCount: nil), presence, header, selection, theme, strings, dateTimeFormat, sortOrder, displayOrder, false, false, true, peersWithStories[peer.id].flatMap {
ContactListNodeEntry.StoryData(count: $0.totalCount, unseenCount: $0.unseenCount, hasUnseenCloseFriends: $0.hasUnseenCloseFriends)
}, false))
index += 1
}
@ -709,7 +712,14 @@ private func contactListNodeEntries(accountPeer: EnginePeer?, peers: [ContactLis
enabled = true
}
entries.append(.peer(index, peer, presence, nil, selection, theme, strings, dateTimeFormat, sortOrder, displayOrder, displayCallIcons, false, enabled, nil, false))
var storyData: ContactListNodeEntry.StoryData?
if case let .peer(id) = peer.id {
storyData = peersWithStories[id].flatMap {
ContactListNodeEntry.StoryData(count: $0.totalCount, unseenCount: $0.unseenCount, hasUnseenCloseFriends: $0.hasUnseenCloseFriends)
}
}
entries.append(.peer(index, peer, presence, nil, selection, theme, strings, dateTimeFormat, sortOrder, displayOrder, displayCallIcons, false, enabled, storyData, false))
index += 1
}
}
@ -755,8 +765,14 @@ private func contactListNodeEntries(accountPeer: EnginePeer?, peers: [ContactLis
enabled = true
}
var storyData: ContactListNodeEntry.StoryData?
if case let .peer(id) = peer.id {
storyData = peersWithStories[id].flatMap {
ContactListNodeEntry.StoryData(count: $0.totalCount, unseenCount: $0.unseenCount, hasUnseenCloseFriends: $0.hasUnseenCloseFriends)
}
}
entries.append(.peer(index, peer, presence, header, selection, theme, strings, dateTimeFormat, sortOrder, displayOrder, displayCallIcons, false, enabled, nil, requiresPremiumForMessaging))
entries.append(.peer(index, peer, presence, header, selection, theme, strings, dateTimeFormat, sortOrder, displayOrder, displayCallIcons, false, enabled, storyData, requiresPremiumForMessaging))
index += 1
}
return entries
@ -927,7 +943,7 @@ public final class ContactListNode: ASDisplayNode {
}
private var didSetReady = false
private let contactPeersViewPromise = Promise<(EngineContactList, EnginePeer?, [EnginePeer.Id: Bool])>()
private let contactPeersViewPromise = Promise<(EngineContactList, EnginePeer?, [EnginePeer.Id: Bool], [EnginePeer.Id: PeerStoryStats])>()
let storySubscriptions = Promise<EngineStorySubscriptions?>(nil)
private let selectionStatePromise = Promise<ContactListNodeGroupSelectionState?>(nil)
@ -1009,6 +1025,34 @@ public final class ContactListNode: ASDisplayNode {
} else {
contactsWithPremiumRequired = .single([:])
}
let contactsWithStories: Signal<[EnginePeer.Id: PeerStoryStats], NoError> = self.context.engine.data.subscribe(
TelegramEngine.EngineData.Item.Contacts.List(includePresences: false)
)
|> map { contacts -> Set<EnginePeer.Id> in
var result = Set<EnginePeer.Id>()
for peer in contacts.peers {
result.insert(peer.id)
}
return result
}
|> distinctUntilChanged
|> mapToSignal { peerIds -> Signal<[EnginePeer.Id: PeerStoryStats], NoError> in
return context.engine.data.subscribe(
EngineDataMap(
peerIds.map(TelegramEngine.EngineData.Item.Peer.StoryStats.init(id:))
)
)
|> map { result -> [EnginePeer.Id: PeerStoryStats] in
var filtered: [EnginePeer.Id: PeerStoryStats] = [:]
for (id, value) in result {
if let value {
filtered[id] = value
}
}
return filtered
}
}
if value {
self.contactPeersViewPromise.set(combineLatest(
@ -1016,10 +1060,11 @@ public final class ContactListNode: ASDisplayNode {
TelegramEngine.EngineData.Item.Contacts.List(includePresences: true),
TelegramEngine.EngineData.Item.Peer.Peer(id: self.context.engine.account.peerId)
),
contactsWithPremiumRequired
contactsWithPremiumRequired,
contactsWithStories
)
|> mapToThrottled { next, contactsWithPremiumRequired -> Signal<(EngineContactList, EnginePeer?, [EnginePeer.Id: Bool]), NoError> in
return .single((next.0, next.1, contactsWithPremiumRequired))
|> mapToThrottled { next, contactsWithPremiumRequired, contactsWithStories -> Signal<(EngineContactList, EnginePeer?, [EnginePeer.Id: Bool], [EnginePeer.Id: PeerStoryStats]), NoError> in
return .single((next.0, next.1, contactsWithPremiumRequired, contactsWithStories))
|> then(
.complete()
|> delay(5.0, queue: Queue.concurrentDefaultQueue())
@ -1030,9 +1075,9 @@ public final class ContactListNode: ASDisplayNode {
TelegramEngine.EngineData.Item.Contacts.List(includePresences: true),
TelegramEngine.EngineData.Item.Peer.Peer(id: self.context.engine.account.peerId)
),
contactsWithPremiumRequired)
|> map { next, contactsWithPremiumRequired -> (EngineContactList, EnginePeer?, [EnginePeer.Id: Bool]) in
return (next.0, next.1, contactsWithPremiumRequired)
contactsWithPremiumRequired, contactsWithStories)
|> map { next, contactsWithPremiumRequired, contactsWithStories -> (EngineContactList, EnginePeer?, [EnginePeer.Id: Bool], [EnginePeer.Id: PeerStoryStats]) in
return (next.0, next.1, contactsWithPremiumRequired, contactsWithStories)
}
|> take(1))
}
@ -1574,7 +1619,7 @@ public final class ContactListNode: ASDisplayNode {
peers.append(.deviceContact(stableId, contact.0))
}
let entries = contactListNodeEntries(accountPeer: nil, peers: peers, presences: localPeersAndStatuses.1, presentation: presentation, selectionState: selectionState, theme: presentationData.theme, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, sortOrder: presentationData.nameSortOrder, displayOrder: presentationData.nameDisplayOrder, disabledPeerIds: disabledPeerIds, peerRequiresPremiumForMessaging: peerRequiresPremiumForMessaging, authorizationStatus: .allowed, warningSuppressed: (true, true), displaySortOptions: false, displayCallIcons: displayCallIcons, storySubscriptions: nil, topPeers: [], topPeersPresentation: .none, interaction: interaction)
let entries = contactListNodeEntries(accountPeer: nil, peers: peers, presences: localPeersAndStatuses.1, presentation: presentation, selectionState: selectionState, theme: presentationData.theme, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, sortOrder: presentationData.nameSortOrder, displayOrder: presentationData.nameDisplayOrder, disabledPeerIds: disabledPeerIds, peerRequiresPremiumForMessaging: peerRequiresPremiumForMessaging, peersWithStories: [:], authorizationStatus: .allowed, warningSuppressed: (true, true), displaySortOptions: false, displayCallIcons: displayCallIcons, storySubscriptions: nil, topPeers: [], topPeersPresentation: .none, interaction: interaction)
let previous = previousEntries.swap(entries)
return .single(preparedContactListNodeTransition(context: context, presentationData: presentationData, from: previous ?? [], to: entries, interaction: interaction, firstTime: previous == nil, isEmpty: false, generateIndexSections: generateSections, animation: .none, isSearch: isSearch))
}
@ -1771,7 +1816,7 @@ public final class ContactListNode: ASDisplayNode {
isEmpty = true
}
let entries = contactListNodeEntries(accountPeer: view.1, peers: peers, presences: presences, presentation: presentation, selectionState: selectionState, theme: presentationData.theme, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, sortOrder: presentationData.nameSortOrder, displayOrder: presentationData.nameDisplayOrder, disabledPeerIds: disabledPeerIds, peerRequiresPremiumForMessaging: view.2, authorizationStatus: authorizationStatus, warningSuppressed: warningSuppressed, displaySortOptions: displaySortOptions, displayCallIcons: displayCallIcons, storySubscriptions: storySubscriptions, topPeers: topPeers.map { $0.peer }, topPeersPresentation: displayTopPeers, interaction: interaction)
let entries = contactListNodeEntries(accountPeer: view.1, peers: peers, presences: presences, presentation: presentation, selectionState: selectionState, theme: presentationData.theme, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, sortOrder: presentationData.nameSortOrder, displayOrder: presentationData.nameDisplayOrder, disabledPeerIds: disabledPeerIds, peerRequiresPremiumForMessaging: view.2, peersWithStories: view.3, authorizationStatus: authorizationStatus, warningSuppressed: warningSuppressed, displaySortOptions: displaySortOptions, displayCallIcons: displayCallIcons, storySubscriptions: storySubscriptions, topPeers: topPeers.map { $0.peer }, topPeersPresentation: displayTopPeers, interaction: interaction)
let previous = previousEntries.swap(entries)
let previousSelection = previousSelectionState.swap(selectionState)
let previousPendingRemovalPeerIds = previousPendingRemovalPeerIds.swap(pendingRemovalPeerIds)

View File

@ -610,7 +610,8 @@ public final class LegacyPaintStickersContext: NSObject, TGPhotoPaintStickersCon
}
}
#if swift(>=6.0)
//Xcode 16
#if canImport(ContactProvider)
extension SolidRoundedButtonView: @retroactive TGPhotoSolidRoundedButtonView {
public func updateWidth(_ width: CGFloat) {
let _ = self.updateLayout(width: width, transition: .immediate)

View File

@ -226,6 +226,12 @@ public func combineLatest<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13
}, initialValues: [:], queue: queue)
}
public func combineLatest<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18, T19, T20, T21, T22, T23, T24, E>(queue: Queue? = nil, _ s1: Signal<T1, E>, _ s2: Signal<T2, E>, _ s3: Signal<T3, E>, _ s4: Signal<T4, E>, _ s5: Signal<T5, E>, _ s6: Signal<T6, E>, _ s7: Signal<T7, E>, _ s8: Signal<T8, E>, _ s9: Signal<T9, E>, _ s10: Signal<T10, E>, _ s11: Signal<T11, E>, _ s12: Signal<T12, E>, _ s13: Signal<T13, E>, _ s14: Signal<T14, E>, _ s15: Signal<T15, E>, _ s16: Signal<T16, E>, _ s17: Signal<T17, E>, _ s18: Signal<T18, E>, _ s19: Signal<T19, E>, _ s20: Signal<T20, E>, _ s21: Signal<T21, E>, _ s22: Signal<T22, E>, _ s23: Signal<T23, E>, _ s24: Signal<T24, E>) -> Signal<(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18, T19, T20, T21, T22, T23, T24), E> {
return combineLatestAny([signalOfAny(s1), signalOfAny(s2), signalOfAny(s3), signalOfAny(s4), signalOfAny(s5), signalOfAny(s6), signalOfAny(s7), signalOfAny(s8), signalOfAny(s9), signalOfAny(s10), signalOfAny(s11), signalOfAny(s12), signalOfAny(s13), signalOfAny(s14), signalOfAny(s15), signalOfAny(s16), signalOfAny(s17), signalOfAny(s18), signalOfAny(s19), signalOfAny(s20), signalOfAny(s21), signalOfAny(s22), signalOfAny(s23), signalOfAny(s24)], combine: { values in
return (values[0] as! T1, values[1] as! T2, values[2] as! T3, values[3] as! T4, values[4] as! T5, values[5] as! T6, values[6] as! T7, values[7] as! T8, values[8] as! T9, values[9] as! T10, values[10] as! T11, values[11] as! T12, values[12] as! T13, values[13] as! T14, values[14] as! T15, values[15] as! T16, values[16] as! T17, values[17] as! T18, values[18] as! T19, values[19] as! T20, values[20] as! T21, values[21] as! T22, values[22] as! T23, values[23] as! T24)
}, initialValues: [:], queue: queue)
}
public func combineLatest<T, E>(queue: Queue? = nil, _ signals: [Signal<T, E>]) -> Signal<[T], E> {
if signals.count == 0 {
return single([T](), E.self)

View File

@ -2626,14 +2626,24 @@ public final class VoiceChatControllerImpl: ViewController, VoiceChatController
}
if !isScheduled && canSpeak {
items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.VoiceChat_NoiseSuppression, textColor: .primary, textLayout: .secondLineWithValue(strongSelf.isNoiseSuppressionEnabled ? strongSelf.presentationData.strings.VoiceChat_NoiseSuppressionEnabled : strongSelf.presentationData.strings.VoiceChat_NoiseSuppressionDisabled), icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Call/Context Menu/Noise"), color: theme.actionSheet.primaryTextColor)
}, action: { _, f in
f(.dismissWithoutContent)
if let strongSelf = self {
strongSelf.call.setIsNoiseSuppressionEnabled(!strongSelf.isNoiseSuppressionEnabled)
}
})))
if #available(iOS 15.0, *) {
//TODO:localize
items.append(.action(ContextMenuActionItem(text: "Microphone Modes", textColor: .primary, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Call/Context Menu/Noise"), color: theme.actionSheet.primaryTextColor)
}, action: { _, f in
f(.dismissWithoutContent)
AVCaptureDevice.showSystemUserInterface(.microphoneModes)
})))
} else {
items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.VoiceChat_NoiseSuppression, textColor: .primary, textLayout: .secondLineWithValue(strongSelf.isNoiseSuppressionEnabled ? strongSelf.presentationData.strings.VoiceChat_NoiseSuppressionEnabled : strongSelf.presentationData.strings.VoiceChat_NoiseSuppressionDisabled), icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Call/Context Menu/Noise"), color: theme.actionSheet.primaryTextColor)
}, action: { _, f in
f(.dismissWithoutContent)
if let strongSelf = self {
strongSelf.call.setIsNoiseSuppressionEnabled(!strongSelf.isNoiseSuppressionEnabled)
}
})))
}
}
if let callState = strongSelf.callState, callState.isVideoEnabled && (callState.muteState?.canUnmute ?? true) {

View File

@ -561,10 +561,10 @@ public extension TelegramEngine {
return _internal_removePeerChat(account: self.account, peerId: peerId, reportChatSpam: reportChatSpam, deleteGloballyIfPossible: deleteGloballyIfPossible)
}
public func removePeerChats(peerIds: [PeerId]) -> Signal<Never, NoError> {
public func removePeerChats(peerIds: [PeerId], deleteGloballyIfPossible: Bool = false) -> Signal<Never, NoError> {
return self.account.postbox.transaction { transaction -> Void in
for peerId in peerIds {
_internal_removePeerChat(account: self.account, transaction: transaction, mediaBox: self.account.postbox.mediaBox, peerId: peerId, reportChatSpam: false, deleteGloballyIfPossible: peerId.namespace == Namespaces.Peer.SecretChat)
_internal_removePeerChat(account: self.account, transaction: transaction, mediaBox: self.account.postbox.mediaBox, peerId: peerId, reportChatSpam: false, deleteGloballyIfPossible: peerId.namespace == Namespaces.Peer.SecretChat || deleteGloballyIfPossible)
}
}
|> ignoreValues

View File

@ -398,6 +398,8 @@ public final class ChatMessageAttachedContentNode: ASDisplayNode {
} else {
let contentMode: InteractiveMediaNodeContentMode = contentMediaAspectFilled ? .aspectFill : .aspectFit
let automaticDownload = shouldDownloadMediaAutomatically(settings: automaticDownloadSettings, peerType: associatedData.automaticDownloadPeerType, networkType: associatedData.automaticDownloadNetworkType, authorPeerId: message.author?.id, contactsPeerIds: associatedData.contactsPeerIds, media: contentMediaValue)
let (_, initialImageWidth, refineLayout) = makeContentMedia(
context,
presentationData,
@ -406,7 +408,7 @@ public final class ChatMessageAttachedContentNode: ASDisplayNode {
attributes,
contentMediaValue,
nil,
.full,
automaticDownload ? .full : .none,
associatedData.automaticDownloadPeerType,
associatedData.automaticDownloadPeerId,
.constrained(CGSize(width: constrainedSize.width - insets.left - insets.right, height: constrainedSize.height)),

View File

@ -412,6 +412,14 @@ private class ExtendedMediaOverlayNode: ASDisplayNode {
}
}
private func selectStoryMedia(item: Stories.Item, preferredHighQuality: Bool) -> Media? {
if !preferredHighQuality, let alternativeMedia = item.alternativeMedia {
return alternativeMedia
} else {
return item.media
}
}
public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitionNode {
private let pinchContainerNode: PinchSourceContainerNode
private let imageNode: TransformImageNode
@ -442,6 +450,7 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr
private var wideLayout: Bool?
private var automaticDownload: InteractiveMediaNodeAutodownloadMode?
public var automaticPlayback: Bool?
private var preferredStoryHighQuality: Bool = false
private let statusDisposable = MetaDisposable()
private let fetchControls = Atomic<FetchControls?>(value: nil)
@ -666,6 +675,13 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr
} else if let media = media as? TelegramMediaImage, let resource = largestImageRepresentation(media.representations)?.resource {
messageMediaImageCancelInteractiveFetch(context: context, messageId: message.id, image: media, resource: resource)
}
if let alternativeMedia = item.alternativeMedia {
if let media = alternativeMedia as? TelegramMediaFile {
messageMediaFileCancelInteractiveFetch(context: context, messageId: message.id, file: media)
} else if let media = alternativeMedia as? TelegramMediaImage, let resource = largestImageRepresentation(media.representations)?.resource {
messageMediaImageCancelInteractiveFetch(context: context, messageId: message.id, image: media, resource: resource)
}
}
}
}
}
@ -703,8 +719,8 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr
}
if let storyMedia = media as? TelegramMediaStory, let storyItem = self.message?.associatedStories[storyMedia.storyId]?.get(Stories.StoredItem.self) {
if case let .item(item) = storyItem, let mediaValue = item.media {
media = mediaValue
if case let .item(item) = storyItem, let _ = item.media {
media = selectStoryMedia(item: item, preferredHighQuality: self.preferredStoryHighQuality)
}
}
@ -720,8 +736,7 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr
if let invoice = self.media as? TelegramMediaInvoice, let _ = invoice.extendedMedia {
self.activateLocalContent(.default)
} else if let storyMedia = media as? TelegramMediaStory, let storyItem = self.message?.associatedStories[storyMedia.storyId]?.get(Stories.StoredItem.self) {
if case let .item(item) = storyItem, let mediaValue = item.media {
let _ = mediaValue
if case let .item(item) = storyItem, let _ = item.media {
self.activateLocalContent(.default)
}
} else {
@ -1118,7 +1133,7 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr
replaceAnimatedStickerNode = true
}
if let storyItem = message.associatedStories[story.storyId]?.get(Stories.StoredItem.self), case let .item(item) = storyItem, let media = item.media {
if let storyItem = message.associatedStories[story.storyId]?.get(Stories.StoredItem.self), case let .item(item) = storyItem, let media = selectStoryMedia(item: item, preferredHighQuality: associatedData.preferredStoryHighQuality) {
if let image = media as? TelegramMediaImage {
if hasCurrentVideoNode {
replaceVideoNode = true
@ -1431,7 +1446,7 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr
media = fullMedia
}
if let storyMedia = media as? TelegramMediaStory, let storyItem = message.associatedStories[storyMedia.storyId]?.get(Stories.StoredItem.self) {
if case let .item(item) = storyItem, let mediaValue = item.media {
if case let .item(item) = storyItem, let mediaValue = selectStoryMedia(item: item, preferredHighQuality: associatedData.preferredStoryHighQuality) {
media = mediaValue
}
}
@ -1494,6 +1509,7 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr
strongSelf.sizeCalculation = sizeCalculation
strongSelf.automaticPlayback = automaticPlayback
strongSelf.automaticDownload = automaticDownload
strongSelf.preferredStoryHighQuality = associatedData.preferredStoryHighQuality
if let previousArguments = strongSelf.currentImageArguments {
if previousArguments.imageSize == arguments.imageSize {
@ -1731,7 +1747,7 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr
media = fullMedia
}
if let storyMedia = media as? TelegramMediaStory, let storyItem = message.associatedStories[storyMedia.storyId]?.get(Stories.StoredItem.self) {
if case let .item(item) = storyItem, let mediaValue = item.media {
if case let .item(item) = storyItem, let mediaValue = selectStoryMedia(item: item, preferredHighQuality: associatedData.preferredStoryHighQuality) {
media = mediaValue
}
}
@ -2011,7 +2027,7 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr
media = fullMedia
}
if let storyMedia = media as? TelegramMediaStory, let storyItem = message.associatedStories[storyMedia.storyId]?.get(Stories.StoredItem.self) {
if case let .item(item) = storyItem, let mediaValue = item.media {
if case let .item(item) = storyItem, let mediaValue = selectStoryMedia(item: item, preferredHighQuality: self.preferredStoryHighQuality) {
media = mediaValue
}
}

View File

@ -1562,51 +1562,53 @@ open class InteractiveTextNode: ASDisplayNode, TextNodeProtocol, UIGestureRecogn
}
if remainingLines <= 0, let lastSegment = calculatedSegments.last, let lastLine = lastSegment.lines.last, !lastLine.isTruncated, let lineRange = lastLine.range, let lineFont = attributedString.attribute(.font, at: lineRange.lowerBound, effectiveRange: nil) as? UIFont {
let truncatedTokenString: NSAttributedString
if let customTruncationTokenValue = customTruncationToken?(lineFont, lastSegment.blockQuote != nil) {
if lineRange.length == 0 && customTruncationTokenValue.string.hasPrefix("\u{2026} ") {
truncatedTokenString = customTruncationTokenValue.attributedSubstring(from: NSRange(location: 2, length: customTruncationTokenValue.length - 2))
if let range = lastLine.range, range.upperBound != attributedString.length {
let truncatedTokenString: NSAttributedString
if let customTruncationTokenValue = customTruncationToken?(lineFont, lastSegment.blockQuote != nil) {
if lineRange.length == 0 && customTruncationTokenValue.string.hasPrefix("\u{2026} ") {
truncatedTokenString = customTruncationTokenValue.attributedSubstring(from: NSRange(location: 2, length: customTruncationTokenValue.length - 2))
} else {
truncatedTokenString = customTruncationTokenValue
}
} else {
truncatedTokenString = customTruncationTokenValue
var truncationTokenAttributes: [NSAttributedString.Key : AnyObject] = [:]
truncationTokenAttributes[NSAttributedString.Key.font] = lineFont
truncationTokenAttributes[NSAttributedString.Key(rawValue: kCTForegroundColorFromContextAttributeName as String)] = true as NSNumber
let tokenString = "\u{2026}"
truncatedTokenString = NSAttributedString(string: tokenString, attributes: truncationTokenAttributes)
}
} else {
var truncationTokenAttributes: [NSAttributedString.Key : AnyObject] = [:]
truncationTokenAttributes[NSAttributedString.Key.font] = lineFont
truncationTokenAttributes[NSAttributedString.Key(rawValue: kCTForegroundColorFromContextAttributeName as String)] = true as NSNumber
let tokenString = "\u{2026}"
truncatedTokenString = NSAttributedString(string: tokenString, attributes: truncationTokenAttributes)
}
let truncationToken = CTLineCreateWithAttributedString(truncatedTokenString)
var truncationTokenAscent: CGFloat = 0.0
var truncationTokenDescent: CGFloat = 0.0
let truncationTokenWidth = CTLineGetTypographicBounds(truncationToken, &truncationTokenAscent, &truncationTokenDescent, nil)
if let updatedLine = CTLineCreateTruncatedLine(lastLine.line, max(0.0, lastLine.constrainedWidth - truncationTokenWidth), .end, nil) {
var lineAscent: CGFloat = 0.0
var lineDescent: CGFloat = 0.0
var lineWidth = CTLineGetTypographicBounds(updatedLine, &lineAscent, &lineDescent, nil)
lineWidth = min(lineWidth, lastLine.constrainedWidth)
let truncationToken = CTLineCreateWithAttributedString(truncatedTokenString)
lastSegment.lines[lastSegment.lines.count - 1] = InteractiveTextNodeLine(
line: updatedLine,
constrainedWidth: lastLine.constrainedWidth,
frame: CGRect(origin: lastLine.frame.origin, size: CGSize(width: lineWidth, height: lineAscent + lineDescent)),
intrinsicWidth: lineWidth,
ascent: lineAscent,
descent: lineDescent,
range: lastLine.range,
isTruncated: true,
isRTL: lastLine.isRTL,
strikethroughs: [],
spoilers: [],
spoilerWords: [],
embeddedItems: [],
attachments: [],
additionalTrailingLine: (truncationToken, 0.0)
)
var truncationTokenAscent: CGFloat = 0.0
var truncationTokenDescent: CGFloat = 0.0
let truncationTokenWidth = CTLineGetTypographicBounds(truncationToken, &truncationTokenAscent, &truncationTokenDescent, nil)
if let updatedLine = CTLineCreateTruncatedLine(lastLine.line, max(0.0, lastLine.constrainedWidth - truncationTokenWidth), .end, nil) {
var lineAscent: CGFloat = 0.0
var lineDescent: CGFloat = 0.0
var lineWidth = CTLineGetTypographicBounds(updatedLine, &lineAscent, &lineDescent, nil)
lineWidth = min(lineWidth, lastLine.constrainedWidth)
lastSegment.lines[lastSegment.lines.count - 1] = InteractiveTextNodeLine(
line: updatedLine,
constrainedWidth: lastLine.constrainedWidth,
frame: CGRect(origin: lastLine.frame.origin, size: CGSize(width: lineWidth, height: lineAscent + lineDescent)),
intrinsicWidth: lineWidth,
ascent: lineAscent,
descent: lineDescent,
range: lastLine.range,
isTruncated: true,
isRTL: lastLine.isRTL,
strikethroughs: [],
spoilers: [],
spoilerWords: [],
embeddedItems: [],
attachments: [],
additionalTrailingLine: (truncationToken, 0.0)
)
}
}
}
@ -2317,25 +2319,6 @@ final class TextContentItemLayer: SimpleLayer {
}
}
/*if !line.strikethroughs.isEmpty {
for strikethrough in line.strikethroughs {
guard let lineRange = line.range else {
continue
}
var textColor: UIColor?
params.item.attributedString?.enumerateAttributes(in: NSMakeRange(lineRange.location, lineRange.length), options: []) { attributes, range, _ in
if range == strikethrough.range, let color = attributes[NSAttributedString.Key.foregroundColor] as? UIColor {
textColor = color
}
}
if let textColor = textColor {
context.setFillColor(textColor.cgColor)
}
let frame = strikethrough.frame.offsetBy(dx: lineFrame.minX, dy: lineFrame.minY)
context.fill(CGRect(x: frame.minX, y: frame.minY - 5.0, width: frame.width, height: 1.0))
}
}*/
if let (additionalTrailingLine, _) = line.additionalTrailingLine {
context.textPosition = CGPoint(x: lineFrame.minX + line.intrinsicWidth, y: lineFrame.maxY - line.descent)

View File

@ -14,7 +14,8 @@ import SearchUI
import ChatListSearchItemHeader
import ContactsPeerItem
#if swift(>=6.0)
//Xcode 16
#if canImport(ContactProvider)
extension NavigationBarSearchContentNode: @retroactive ItemListControllerSearchNavigationContentNode {
public func activate() {
}

View File

@ -5,6 +5,381 @@ import ComponentFlow
import HierarchyTrackingLayer
import TelegramPresentationData
private extension CGFloat {
func remap(fromLow: CGFloat, fromHigh: CGFloat, toLow: CGFloat, toHigh: CGFloat) -> CGFloat {
guard (fromHigh - fromLow) != 0 else {
// Would produce NAN
return 0
}
return toLow + (self - fromLow) * (toHigh - toLow) / (fromHigh - fromLow)
}
}
private extension CGPoint {
/// Returns the length between the receiver and *CGPoint.zero*
var vectorLength: CGFloat {
distanceTo(.zero)
}
var isZero: Bool {
x == 0 && y == 0
}
/// Operator convenience to divide points with /
static func / (lhs: CGPoint, rhs: CGFloat) -> CGPoint {
CGPoint(x: lhs.x / CGFloat(rhs), y: lhs.y / CGFloat(rhs))
}
/// Operator convenience to multiply points with *
static func * (lhs: CGPoint, rhs: CGFloat) -> CGPoint {
CGPoint(x: lhs.x * CGFloat(rhs), y: lhs.y * CGFloat(rhs))
}
/// Operator convenience to add points with +
static func +(left: CGPoint, right: CGPoint) -> CGPoint {
left.add(right)
}
/// Operator convenience to subtract points with -
static func -(left: CGPoint, right: CGPoint) -> CGPoint {
left.subtract(right)
}
/// Returns the distance between the receiver and the given point.
func distanceTo(_ a: CGPoint) -> CGFloat {
let xDist = a.x - x
let yDist = a.y - y
return CGFloat(sqrt((xDist * xDist) + (yDist * yDist)))
}
func rounded(decimal: CGFloat) -> CGPoint {
CGPoint(x: round(decimal * x) / decimal, y: round(decimal * y) / decimal)
}
func interpolate(to: CGPoint, amount: CGFloat) -> CGPoint {
return self + ((to - self) * amount)
}
func interpolate(
_ to: CGPoint,
outTangent: CGPoint,
inTangent: CGPoint,
amount: CGFloat,
maxIterations: Int = 3,
samples: Int = 20,
accuracy: CGFloat = 1)
-> CGPoint
{
if amount == 0 {
return self
}
if amount == 1 {
return to
}
if
colinear(outTangent, inTangent) == true,
outTangent.colinear(inTangent, to) == true
{
return interpolate(to: to, amount: amount)
}
let step = 1 / CGFloat(samples)
var points: [(point: CGPoint, distance: CGFloat)] = [(point: self, distance: 0)]
var totalLength: CGFloat = 0
var previousPoint = self
var previousAmount = CGFloat(0)
var closestPoint = 0
while previousAmount < 1 {
previousAmount = previousAmount + step
if previousAmount < amount {
closestPoint = closestPoint + 1
}
let newPoint = pointOnPath(to, outTangent: outTangent, inTangent: inTangent, amount: previousAmount)
let distance = previousPoint.distanceTo(newPoint)
totalLength = totalLength + distance
points.append((point: newPoint, distance: totalLength))
previousPoint = newPoint
}
let accurateDistance = amount * totalLength
var point = points[closestPoint]
var foundPoint = false
var pointAmount = CGFloat(closestPoint) * step
var nextPointAmount: CGFloat = pointAmount + step
var refineIterations = 0
while foundPoint == false {
refineIterations = refineIterations + 1
/// First see if the next point is still less than the projected length.
let nextPoint = points[min(closestPoint + 1, points.indices.last!)]
if nextPoint.distance < accurateDistance {
point = nextPoint
closestPoint = closestPoint + 1
pointAmount = CGFloat(closestPoint) * step
nextPointAmount = pointAmount + step
if closestPoint == points.count {
foundPoint = true
}
continue
}
if accurateDistance < point.distance {
closestPoint = closestPoint - 1
if closestPoint < 0 {
foundPoint = true
continue
}
point = points[closestPoint]
pointAmount = CGFloat(closestPoint) * step
nextPointAmount = pointAmount + step
continue
}
/// Now we are certain the point is the closest point under the distance
let pointDiff = nextPoint.distance - point.distance
let proposedPointAmount = ((accurateDistance - point.distance) / pointDiff)
.remap(fromLow: 0, fromHigh: 1, toLow: pointAmount, toHigh: nextPointAmount)
let newPoint = pointOnPath(to, outTangent: outTangent, inTangent: inTangent, amount: proposedPointAmount)
let newDistance = point.distance + point.point.distanceTo(newPoint)
pointAmount = proposedPointAmount
point = (point: newPoint, distance: newDistance)
if
accurateDistance - newDistance <= accuracy ||
newDistance - accurateDistance <= accuracy
{
foundPoint = true
}
if refineIterations == maxIterations {
foundPoint = true
}
}
return point.point
}
func pointOnPath(_ to: CGPoint, outTangent: CGPoint, inTangent: CGPoint, amount: CGFloat) -> CGPoint {
let a = interpolate(to: outTangent, amount: amount)
let b = outTangent.interpolate(to: inTangent, amount: amount)
let c = inTangent.interpolate(to: to, amount: amount)
let d = a.interpolate(to: b, amount: amount)
let e = b.interpolate(to: c, amount: amount)
let f = d.interpolate(to: e, amount: amount)
return f
}
func colinear(_ a: CGPoint, _ b: CGPoint) -> Bool {
let area = x * (a.y - b.y) + a.x * (b.y - y) + b.x * (y - a.y);
let accuracy: CGFloat = 0.05
if area < accuracy && area > -accuracy {
return true
}
return false
}
/// Subtracts the given point from the receiving point.
func subtract(_ point: CGPoint) -> CGPoint {
CGPoint(
x: x - point.x,
y: y - point.y)
}
/// Adds the given point from the receiving point.
func add(_ point: CGPoint) -> CGPoint {
CGPoint(
x: x + point.x,
y: y + point.y)
}
}
private extension CurveVertex {
func interpolate(to: CurveVertex, amount: CGFloat) -> CurveVertex {
CurveVertex(
point: point.interpolate(to: to.point, amount: amount),
inTangent: inTangent.interpolate(to: to.inTangent, amount: amount),
outTangent: outTangent.interpolate(to: to.outTangent, amount: amount))
}
}
private struct CurveVertex {
init(_ inTangent: CGPoint, _ point: CGPoint, _ outTangent: CGPoint) {
self.point = point
self.inTangent = inTangent
self.outTangent = outTangent
}
init(point: CGPoint, inTangentRelative: CGPoint, outTangentRelative: CGPoint) {
self.point = point
inTangent = CGPoint(x: point.x + inTangentRelative.x, y: point.y + inTangentRelative.y)
outTangent = CGPoint(x: point.x + outTangentRelative.x, y: point.y + outTangentRelative.y)
}
init(point: CGPoint, inTangent: CGPoint, outTangent: CGPoint) {
self.point = point
self.inTangent = inTangent
self.outTangent = outTangent
}
// MARK: Internal
let point: CGPoint
var inTangent: CGPoint
var outTangent: CGPoint
var inTangentRelative: CGPoint {
return CGPoint(x: inTangent.x - point.x, y: inTangent.y - point.y)
}
var outTangentRelative: CGPoint {
return CGPoint(x: outTangent.x - point.x, y: outTangent.y - point.y)
}
func reversed() -> CurveVertex {
return CurveVertex(point: point, inTangent: outTangent, outTangent: inTangent)
}
func translated(_ translation: CGPoint) -> CurveVertex {
return CurveVertex(point: CGPoint(x: point.x + translation.x, y: point.y + translation.y), inTangent: CGPoint(x: inTangent.x + translation.x, y: inTangent.y + translation.y), outTangent: CGPoint(x: outTangent.x + translation.x, y: outTangent.y + translation.y))
}
/// Trims a path defined by two Vertices at a specific position, from 0 to 1
///
/// The path can be visualized below.
///
/// F is fromVertex.
/// V is the vertex of the receiver.
/// P is the position from 0-1.
/// O is the outTangent of fromVertex.
/// F====O=========P=======I====V
///
/// After trimming the curve can be visualized below.
///
/// S is the returned Start vertex.
/// E is the returned End vertex.
/// T is the trim point.
/// TI and TO are the new tangents for the trimPoint
/// NO and NI are the new tangents for the startPoint and endPoints
/// S==NO=========TI==T==TO=======NI==E
func splitCurve(toVertex: CurveVertex, position: CGFloat) ->
(start: CurveVertex, trimPoint: CurveVertex, end: CurveVertex)
{
/// If position is less than or equal to 0, trim at start.
if position <= 0 {
return (
start: CurveVertex(point: point, inTangentRelative: inTangentRelative, outTangentRelative: .zero),
trimPoint: CurveVertex(point: point, inTangentRelative: .zero, outTangentRelative: outTangentRelative),
end: toVertex)
}
/// If position is greater than or equal to 1, trim at end.
if position >= 1 {
return (
start: self,
trimPoint: CurveVertex(
point: toVertex.point,
inTangentRelative: toVertex.inTangentRelative,
outTangentRelative: .zero),
end: CurveVertex(
point: toVertex.point,
inTangentRelative: .zero,
outTangentRelative: toVertex.outTangentRelative))
}
if outTangentRelative == CGPoint() && toVertex.inTangentRelative == CGPoint() {
/// If both tangents are zero, then span to be trimmed is a straight line.
let trimPoint = point.interpolate(to: toVertex.point, amount: position)
return (
start: self,
trimPoint: CurveVertex(point: trimPoint, inTangentRelative: .zero, outTangentRelative: .zero),
end: toVertex)
}
/// Cutting by amount gives incorrect length....
/// One option is to cut by a stride until it gets close then edge it down.
/// Measuring a percentage of the spans does not equal the same as measuring a percentage of length.
/// This is where the historical trim path bugs come from.
let a = point.interpolate(to: outTangent, amount: position)
let b = outTangent.interpolate(to: toVertex.inTangent, amount: position)
let c = toVertex.inTangent.interpolate(to: toVertex.point, amount: position)
let d = a.interpolate(to: b, amount: position)
let e = b.interpolate(to: c, amount: position)
let f = d.interpolate(to: e, amount: position)
return (
start: CurveVertex(point: point, inTangent: inTangent, outTangent: a),
trimPoint: CurveVertex(point: f, inTangent: d, outTangent: e),
end: CurveVertex(point: toVertex.point, inTangent: c, outTangent: toVertex.outTangent))
}
/// Trims a curve of a known length to a specific length and returns the points.
///
/// There is not a performant yet accurate way to cut a curve to a specific length.
/// This calls splitCurve(toVertex: position:) to split the curve and then measures
/// the length of the new curve. The function then iterates through the samples,
/// adjusting the position of the cut for a more precise cut.
/// Usually a single iteration is enough to get within 0.5 points of the desired
/// length.
///
/// This function should probably live in PathElement, since it deals with curve
/// lengths.
func trimCurve(toVertex: CurveVertex, atLength: CGFloat, curveLength: CGFloat, maxSamples: Int, accuracy: CGFloat = 1) ->
(start: CurveVertex, trimPoint: CurveVertex, end: CurveVertex)
{
var currentPosition = atLength / curveLength
var results = splitCurve(toVertex: toVertex, position: currentPosition)
if maxSamples == 0 {
return results
}
for _ in 1...maxSamples {
let length = results.start.distanceTo(results.trimPoint)
let lengthDiff = atLength - length
/// Check if length is correct.
if lengthDiff < accuracy {
return results
}
let diffPosition = max(min((currentPosition / length) * lengthDiff, currentPosition * 0.5), currentPosition * -0.5)
currentPosition = diffPosition + currentPosition
results = splitCurve(toVertex: toVertex, position: currentPosition)
}
return results
}
/// The distance from the receiver to the provided vertex.
///
/// For lines (zeroed tangents) the distance between the two points is measured.
/// For curves the curve is iterated over by sample count and the points are measured.
/// This is ~99% accurate at a sample count of 30
func distanceTo(_ toVertex: CurveVertex, sampleCount: Int = 25) -> CGFloat {
if outTangentRelative.isZero && toVertex.inTangentRelative.isZero {
/// Return a linear distance.
return point.distanceTo(toVertex.point)
}
var distance: CGFloat = 0
var previousPoint = point
for i in 0..<sampleCount {
let pointOnCurve = splitCurve(toVertex: toVertex, position: CGFloat(i) / CGFloat(sampleCount)).trimPoint
distance = distance + previousPoint.distanceTo(pointOnCurve.point)
previousPoint = pointOnCurve.point
}
distance = distance + previousPoint.distanceTo(toVertex.point)
return distance
}
}
public final class AvatarStoryIndicatorComponent: Component {
public struct Colors: Equatable {
public var unseenColors: [UIColor]
@ -295,60 +670,160 @@ public final class AvatarStoryIndicatorComponent: Component {
let radius = (diameter - component.activeLineWidth) * 0.5
self.indicatorView.image = generateImage(CGSize(width: imageDiameter, height: imageDiameter), rotatedContext: { size, context in
UIGraphicsPushContext(context)
defer {
UIGraphicsPopContext()
}
context.clear(CGRect(origin: CGPoint(), size: size))
context.setLineCap(.round)
var locations: [CGFloat] = [0.0, 1.0]
if let counters = component.counters, counters.totalCount > 1, !component.isRoundedRect {
let center = CGPoint(x: size.width * 0.5, y: size.height * 0.5)
let spacing: CGFloat = component.activeLineWidth * 2.0
let angularSpacing: CGFloat = spacing / radius
let circleLength = CGFloat.pi * 2.0 * radius
let segmentLength = (circleLength - spacing * CGFloat(counters.totalCount)) / CGFloat(counters.totalCount)
let segmentAngle = segmentLength / radius
for pass in 0 ..< 2 {
context.resetClip()
if let counters = component.counters, counters.totalCount > 1 {
if component.isRoundedRect {
let lineWidth: CGFloat = component.hasUnseen ? component.activeLineWidth : component.inactiveLineWidth
context.setLineWidth(lineWidth)
let path = UIBezierPath(roundedRect: CGRect(origin: CGPoint(x: size.width * 0.5 - diameter * 0.5, y: size.height * 0.5 - diameter * 0.5), size: size).insetBy(dx: lineWidth * 0.5, dy: lineWidth * 0.5), cornerRadius: floor(diameter * 0.27))
if pass == 0 {
context.setLineWidth(component.inactiveLineWidth)
} else {
context.setLineWidth(component.activeLineWidth)
}
let startIndex: Int
let endIndex: Int
if pass == 0 {
startIndex = 0
endIndex = counters.totalCount - counters.unseenCount
} else {
startIndex = counters.totalCount - counters.unseenCount
endIndex = counters.totalCount
}
if startIndex < endIndex {
for i in startIndex ..< endIndex {
let startAngle = CGFloat(i) * (angularSpacing + segmentAngle) - CGFloat.pi * 0.5 + angularSpacing * 0.5
context.move(to: CGPoint(x: center.x + cos(startAngle) * radius, y: center.y + sin(startAngle) * radius))
context.addArc(center: center, radius: radius, startAngle: startAngle, endAngle: startAngle + segmentAngle, clockwise: false)
var startPoint: CGPoint?
var vertices: [CurveVertex] = []
path.cgPath.applyWithBlock({ element in
switch element.pointee.type {
case .moveToPoint:
startPoint = element.pointee.points[0]
case .addLineToPoint:
if let _ = vertices.last {
vertices.append(CurveVertex(point: element.pointee.points[0], inTangentRelative: CGPoint(), outTangentRelative: CGPoint()))
} else if let startPoint {
vertices.append(CurveVertex(point: startPoint, inTangentRelative: CGPoint(), outTangentRelative: CGPoint()))
vertices.append(CurveVertex(point: element.pointee.points[0], inTangentRelative: CGPoint(), outTangentRelative: CGPoint()))
}
case .addQuadCurveToPoint:
break
case .addCurveToPoint:
if let _ = vertices.last {
vertices.append(CurveVertex(point: element.pointee.points[2], inTangentRelative: CGPoint(), outTangentRelative: CGPoint()))
} else if let startPoint {
vertices.append(CurveVertex(point: startPoint, inTangentRelative: CGPoint(), outTangentRelative: CGPoint()))
vertices.append(CurveVertex(point: element.pointee.points[2], inTangentRelative: CGPoint(), outTangentRelative: CGPoint()))
}
if vertices.count >= 2 {
vertices[vertices.count - 2].outTangent = element.pointee.points[0]
vertices[vertices.count - 1].inTangent = element.pointee.points[1]
}
case .closeSubpath:
if let startPointValue = startPoint {
vertices.append(CurveVertex(point: startPointValue, inTangentRelative: CGPoint(), outTangentRelative: CGPoint()))
startPoint = nil
}
@unknown default:
break
}
context.replacePathWithStrokedPath()
context.clip()
})
var length: CGFloat = 0.0
var firstOffset: CGFloat = 0.0
for i in 0 ..< vertices.count - 1 {
let value = vertices[i].distanceTo(vertices[i + 1])
if firstOffset == 0.0 {
firstOffset = value * 0.5
}
length += value
}
let spacing: CGFloat = component.activeLineWidth * 2.0
let useableLength = length - spacing * CGFloat(counters.totalCount)
let segmentLength = useableLength / CGFloat(counters.totalCount)
context.setLineWidth(lineWidth)
for index in 0 ..< counters.totalCount {
var dashWidths: [CGFloat] = []
dashWidths.append(segmentLength)
dashWidths.append(10000000.0)
let colors: [CGColor]
if pass == 1 {
if index >= counters.totalCount - counters.unseenCount {
colors = activeColors
} else {
colors = inactiveColors
}
let colorSpace = CGColorSpaceCreateDeviceRGB()
if let gradient = CGGradient(colorsSpace: colorSpace, colors: colors as CFArray, locations: &locations) {
let gradient = CGGradient(colorsSpace: colorSpace, colors: colors as CFArray, locations: &locations)!
context.resetClip()
context.setLineDash(phase: -firstOffset - spacing * 0.5 - CGFloat(index) * (spacing + segmentLength), lengths: dashWidths)
context.addPath(path.cgPath)
context.replacePathWithStrokedPath()
context.clip()
context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: 0.0, y: size.height), options: CGGradientDrawingOptions())
if index == counters.totalCount - 1 {
context.resetClip()
let addPath = CGMutablePath()
addPath.move(to: CGPoint(x: vertices[0].interpolate(to: vertices[1], amount: 0.5).point.x - spacing * 0.5, y: vertices[0].point.y))
addPath.addLine(to: CGPoint(x: vertices[0].point.x, y: vertices[0].point.y))
context.setLineDash(phase: 0.0, lengths: [])
context.addPath(addPath)
context.replacePathWithStrokedPath()
context.clip()
context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: 0.0, y: size.height), options: CGGradientDrawingOptions())
}
}
} else {
let center = CGPoint(x: size.width * 0.5, y: size.height * 0.5)
let spacing: CGFloat = component.activeLineWidth * 2.0
let angularSpacing: CGFloat = spacing / radius
let circleLength = CGFloat.pi * 2.0 * radius
let segmentLength = (circleLength - spacing * CGFloat(counters.totalCount)) / CGFloat(counters.totalCount)
let segmentAngle = segmentLength / radius
for pass in 0 ..< 2 {
context.resetClip()
if pass == 0 {
context.setLineWidth(component.inactiveLineWidth)
} else {
context.setLineWidth(component.activeLineWidth)
}
let startIndex: Int
let endIndex: Int
if pass == 0 {
startIndex = 0
endIndex = counters.totalCount - counters.unseenCount
} else {
startIndex = counters.totalCount - counters.unseenCount
endIndex = counters.totalCount
}
if startIndex < endIndex {
for i in startIndex ..< endIndex {
let startAngle = CGFloat(i) * (angularSpacing + segmentAngle) - CGFloat.pi * 0.5 + angularSpacing * 0.5
context.move(to: CGPoint(x: center.x + cos(startAngle) * radius, y: center.y + sin(startAngle) * radius))
context.addArc(center: center, radius: radius, startAngle: startAngle, endAngle: startAngle + segmentAngle, clockwise: false)
}
context.replacePathWithStrokedPath()
context.clip()
let colors: [CGColor]
if pass == 1 {
colors = activeColors
} else {
colors = inactiveColors
}
let colorSpace = CGColorSpaceCreateDeviceRGB()
if let gradient = CGGradient(colorsSpace: colorSpace, colors: colors as CFArray, locations: &locations) {
context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: 0.0, y: size.height), options: CGGradientDrawingOptions())
}
}
}
}
} else {
let lineWidth: CGFloat = component.hasUnseen ? component.activeLineWidth : component.inactiveLineWidth

View File

@ -71,6 +71,13 @@ extension ChatControllerImpl {
case .custom, .twoLists:
break
}
var allowedReactions = allowedReactions
if allowedReactions != nil, case let .customChatContents(customChatContents) = self.presentationInterfaceState.subject {
if case let .hashTagSearch(publicPosts) = customChatContents.kind, publicPosts {
allowedReactions = nil
}
}
var tip: ContextController.Tip?

View File

@ -1487,6 +1487,11 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
if case .default = reaction, strongSelf.chatLocation.peerId == strongSelf.context.account.peerId {
return
}
if case let .customChatContents(customChatContents) = strongSelf.presentationInterfaceState.subject {
if case let .hashTagSearch(publicPosts) = customChatContents.kind, publicPosts {
return
}
}
if !force && message.areReactionsTags(accountPeerId: strongSelf.context.account.peerId) {
if case .pinnedMessages = strongSelf.subject {

View File

@ -329,6 +329,7 @@ private func extractAssociatedData(
chatLocation: ChatLocation,
view: MessageHistoryView,
automaticDownloadNetworkType: MediaAutoDownloadNetworkType,
preferredStoryHighQuality: Bool,
animatedEmojiStickers: [String: [StickerPackItem]],
additionalAnimatedEmojiStickers: [String: [Int: StickerPackItem]],
subject: ChatControllerSubject?,
@ -404,7 +405,7 @@ private func extractAssociatedData(
automaticDownloadPeerId = message.peerId
}
return ChatMessageItemAssociatedData(automaticDownloadPeerType: automaticMediaDownloadPeerType, automaticDownloadPeerId: automaticDownloadPeerId, automaticDownloadNetworkType: automaticDownloadNetworkType, isRecentActions: false, subject: subject, contactsPeerIds: contactsPeerIds, channelDiscussionGroup: channelDiscussionGroup, animatedEmojiStickers: animatedEmojiStickers, additionalAnimatedEmojiStickers: additionalAnimatedEmojiStickers, currentlyPlayingMessageId: currentlyPlayingMessageId, isCopyProtectionEnabled: isCopyProtectionEnabled, availableReactions: availableReactions, availableMessageEffects: availableMessageEffects, savedMessageTags: savedMessageTags, defaultReaction: defaultReaction, isPremium: isPremium, accountPeer: accountPeer, alwaysDisplayTranscribeButton: alwaysDisplayTranscribeButton, topicAuthorId: topicAuthorId, hasBots: hasBots, translateToLanguage: translateToLanguage, maxReadStoryId: maxReadStoryId, recommendedChannels: recommendedChannels, audioTranscriptionTrial: audioTranscriptionTrial, chatThemes: chatThemes, deviceContactsNumbers: deviceContactsNumbers, isInline: isInline)
return ChatMessageItemAssociatedData(automaticDownloadPeerType: automaticMediaDownloadPeerType, automaticDownloadPeerId: automaticDownloadPeerId, automaticDownloadNetworkType: automaticDownloadNetworkType, preferredStoryHighQuality: preferredStoryHighQuality, isRecentActions: false, subject: subject, contactsPeerIds: contactsPeerIds, channelDiscussionGroup: channelDiscussionGroup, animatedEmojiStickers: animatedEmojiStickers, additionalAnimatedEmojiStickers: additionalAnimatedEmojiStickers, currentlyPlayingMessageId: currentlyPlayingMessageId, isCopyProtectionEnabled: isCopyProtectionEnabled, availableReactions: availableReactions, availableMessageEffects: availableMessageEffects, savedMessageTags: savedMessageTags, defaultReaction: defaultReaction, isPremium: isPremium, accountPeer: accountPeer, alwaysDisplayTranscribeButton: alwaysDisplayTranscribeButton, topicAuthorId: topicAuthorId, hasBots: hasBots, translateToLanguage: translateToLanguage, maxReadStoryId: maxReadStoryId, recommendedChannels: recommendedChannels, audioTranscriptionTrial: audioTranscriptionTrial, chatThemes: chatThemes, deviceContactsNumbers: deviceContactsNumbers, isInline: isInline)
}
private extension ChatHistoryLocationInput {
@ -1588,6 +1589,22 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto
let premiumConfiguration = PremiumConfiguration.with(appConfiguration: self.context.currentAppConfiguration.with { $0 })
let preferredStoryHighQuality: Signal<Bool, NoError> = combineLatest(
context.sharedContext.automaticMediaDownloadSettings
|> map { settings in
return settings.highQualityStories
}
|> distinctUntilChanged,
context.engine.data.subscribe(
TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId)
)
)
|> map { setting, peer -> Bool in
let isPremium = peer?.isPremium ?? false
return setting && isPremium
}
|> distinctUntilChanged
let messageViewQueue = Queue.mainQueue()
let historyViewTransitionDisposable = combineLatest(queue: messageViewQueue,
historyViewUpdate,
@ -1595,6 +1612,7 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto
selectedMessages,
updatingMedia,
automaticDownloadNetworkType,
preferredStoryHighQuality,
animatedEmojiStickers,
additionalAnimatedEmojiStickers,
customChannelDiscussionReadState,
@ -1613,7 +1631,7 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto
audioTranscriptionTrial,
chatThemes,
deviceContactsNumbers
).startStrict(next: { [weak self] update, chatPresentationData, selectedMessages, updatingMedia, networkType, animatedEmojiStickers, additionalAnimatedEmojiStickers, customChannelDiscussionReadState, customThreadOutgoingReadState, availableReactions, availableMessageEffects, savedMessageTags, defaultReaction, accountPeer, suggestAudioTranscription, promises, topicAuthorId, translationState, maxReadStoryId, recommendedChannels, audioTranscriptionTrial, chatThemes, deviceContactsNumbers in
).startStrict(next: { [weak self] update, chatPresentationData, selectedMessages, updatingMedia, networkType, preferredStoryHighQuality, animatedEmojiStickers, additionalAnimatedEmojiStickers, customChannelDiscussionReadState, customThreadOutgoingReadState, availableReactions, availableMessageEffects, savedMessageTags, defaultReaction, accountPeer, suggestAudioTranscription, promises, topicAuthorId, translationState, maxReadStoryId, recommendedChannels, audioTranscriptionTrial, chatThemes, deviceContactsNumbers in
let (historyAppearsCleared, pendingUnpinnedAllMessages, pendingRemovedMessages, currentlyPlayingMessageIdAndType, scrollToMessageId, chatHasBots, allAdMessages) = promises
func applyHole() {
@ -1832,7 +1850,7 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto
translateToLanguage = languageCode
}
let associatedData = extractAssociatedData(chatLocation: chatLocation, view: view, automaticDownloadNetworkType: networkType, animatedEmojiStickers: animatedEmojiStickers, additionalAnimatedEmojiStickers: additionalAnimatedEmojiStickers, subject: subject, currentlyPlayingMessageId: currentlyPlayingMessageIdAndType?.0, isCopyProtectionEnabled: isCopyProtectionEnabled, availableReactions: availableReactions, availableMessageEffects: availableMessageEffects, savedMessageTags: savedMessageTags, defaultReaction: defaultReaction, isPremium: isPremium, alwaysDisplayTranscribeButton: alwaysDisplayTranscribeButton, accountPeer: accountPeer, topicAuthorId: topicAuthorId, hasBots: chatHasBots, translateToLanguage: translateToLanguage, maxReadStoryId: maxReadStoryId, recommendedChannels: recommendedChannels, audioTranscriptionTrial: audioTranscriptionTrial, chatThemes: chatThemes, deviceContactsNumbers: deviceContactsNumbers, isInline: !rotated)
let associatedData = extractAssociatedData(chatLocation: chatLocation, view: view, automaticDownloadNetworkType: networkType, preferredStoryHighQuality: preferredStoryHighQuality, animatedEmojiStickers: animatedEmojiStickers, additionalAnimatedEmojiStickers: additionalAnimatedEmojiStickers, subject: subject, currentlyPlayingMessageId: currentlyPlayingMessageIdAndType?.0, isCopyProtectionEnabled: isCopyProtectionEnabled, availableReactions: availableReactions, availableMessageEffects: availableMessageEffects, savedMessageTags: savedMessageTags, defaultReaction: defaultReaction, isPremium: isPremium, alwaysDisplayTranscribeButton: alwaysDisplayTranscribeButton, accountPeer: accountPeer, topicAuthorId: topicAuthorId, hasBots: chatHasBots, translateToLanguage: translateToLanguage, maxReadStoryId: maxReadStoryId, recommendedChannels: recommendedChannels, audioTranscriptionTrial: audioTranscriptionTrial, chatThemes: chatThemes, deviceContactsNumbers: deviceContactsNumbers, isInline: !rotated)
var includeEmbeddedSavedChatInfo = false
if case let .replyThread(message) = chatLocation, message.peerId == context.account.peerId, !rotated {

View File

@ -20,7 +20,8 @@ import TelegramNotices
import ComponentFlow
import MediaScrubberComponent
#if swift(>=6.0)
//Xcode 16
#if canImport(ContactProvider)
extension AudioWaveformNode: @retroactive CustomMediaPlayerScrubbingForegroundNode {
}
#else

View File

@ -455,10 +455,20 @@ final class ChatTagSearchInputPanelNode: ChatInputPanelNode {
let resultsTextSize = resultsText.update(
transition: resultsTextTransition,
component: AnyComponent(AnimatedTextComponent(
font: Font.regular(15.0),
color: params.interfaceState.theme.rootController.navigationBar.secondaryTextColor,
items: resultsTextString
component: AnyComponent(PlainButtonComponent(
content: AnyComponent(AnimatedTextComponent(
font: Font.regular(15.0),
color: (params.interfaceState.displayHistoryFilterAsList && canChangeListMode) ? params.interfaceState.theme.rootController.navigationBar.accentTextColor : params.interfaceState.theme.rootController.navigationBar.secondaryTextColor,
items: resultsTextString
)),
effectAlignment: .center,
action: { [weak self] in
guard let self, let params = self.currentLayout?.params else {
return
}
self.interfaceInteraction?.updateDisplayHistoryFilterAsList(!params.interfaceState.displayHistoryFilterAsList)
},
isEnabled: params.interfaceState.displayHistoryFilterAsList && canChangeListMode
)),
environment: {},
containerSize: CGSize(width: 200.0, height: 100.0)

View File

@ -687,7 +687,8 @@ public final class TelegramRootController: NavigationController, TelegramRootCon
}
}
#if swift(>=6.0)
//Xcode 16
#if canImport(ContactProvider)
extension MediaEditorScreen.Result: @retroactive MediaEditorScreenResult {
}
#else

View File

@ -0,0 +1,10 @@
import Foundation
import SwiftSignalKit
import TgVoipWebrtc
import TelegramCore
public final class LiveStreamController {
public init(network: Network) {
}
}