mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
Various improvements
This commit is contained in:
parent
13026a5cc4
commit
6807abf42c
@ -1 +1 @@
|
||||
Subproject commit 77e00ae3fd2d7f6fae3790420bc95c8f7abb6f7b
|
||||
Subproject commit db0ce201aa4f2099559d6e4b4373f7de83b81eff
|
@ -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
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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) {
|
||||
|
@ -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
|
||||
|
@ -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)),
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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() {
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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?
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -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 {
|
||||
|
@ -20,7 +20,8 @@ import TelegramNotices
|
||||
import ComponentFlow
|
||||
import MediaScrubberComponent
|
||||
|
||||
#if swift(>=6.0)
|
||||
//Xcode 16
|
||||
#if canImport(ContactProvider)
|
||||
extension AudioWaveformNode: @retroactive CustomMediaPlayerScrubbingForegroundNode {
|
||||
}
|
||||
#else
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
10
submodules/TelegramVoip/Sources/LiveStreamController.swift
Normal file
10
submodules/TelegramVoip/Sources/LiveStreamController.swift
Normal file
@ -0,0 +1,10 @@
|
||||
import Foundation
|
||||
import SwiftSignalKit
|
||||
import TgVoipWebrtc
|
||||
import TelegramCore
|
||||
|
||||
public final class LiveStreamController {
|
||||
public init(network: Network) {
|
||||
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user