Business fixes

This commit is contained in:
Isaac 2024-02-29 21:35:36 +04:00
parent c88e69ec29
commit c69ba2c089
7 changed files with 204 additions and 28 deletions

View File

@ -101,6 +101,7 @@ swift_library(
"//submodules/TelegramUI/Components/Settings/ArchiveInfoScreen",
"//submodules/TelegramUI/Components/Settings/NewSessionInfoScreen",
"//submodules/TelegramUI/Components/Settings/PeerNameColorItem",
"//submodules/Components/MultilineTextComponent",
],
visibility = [
"//visibility:public",

View File

@ -27,6 +27,7 @@ import ComponentFlow
import EmojiStatusComponent
import AvatarVideoNode
import AppBundle
import MultilineTextComponent
public enum ChatListItemContent {
public struct ThreadInfo: Equatable {
@ -269,7 +270,9 @@ private final class ChatListItemTagListComponent: Component {
func update(context: AccountContext, title: String, backgroundColor: UIColor, foregroundColor: UIColor) -> CGSize {
let titleSize = self.title.update(
transition: .immediate,
component: AnyComponent(Text(text: title.isEmpty ? " " : title, font: Font.semibold(11.0), color: foregroundColor)),
component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(string: title.isEmpty ? " " : title, font: Font.semibold(11.0), textColor: foregroundColor))
)),
environment: {},
containerSize: CGSize(width: 100.0, height: 100.0)
)
@ -800,8 +803,8 @@ private let playIconImage = UIImage(bundleImageName: "Chat List/MiniThumbnailPla
private final class ChatListMediaPreviewNode: ASDisplayNode {
private let context: AccountContext
private let message: EngineMessage
private let media: EngineMedia
let message: EngineMessage
let media: EngineMedia
private let imageNode: TransformImageNode
private let playIcon: ASImageNode
@ -862,10 +865,14 @@ private final class ChatListMediaPreviewNode: ASDisplayNode {
if file.isInstantVideo {
isRound = true
}
if file.isAnimated {
if file.isSticker || file.isAnimatedSticker {
self.playIcon.isHidden = true
} else {
self.playIcon.isHidden = false
if file.isAnimated {
self.playIcon.isHidden = true
} else {
self.playIcon.isHidden = false
}
}
if let mediaDimensions = file.dimensions {
dimensions = mediaDimensions.cgSize
@ -877,9 +884,18 @@ private final class ChatListMediaPreviewNode: ASDisplayNode {
}
}
let radius: CGFloat
if isRound {
radius = size.width / 2.0
} else if size.width >= 30.0 {
radius = 8.0
} else {
radius = 2.0
}
let makeLayout = self.imageNode.asyncLayout()
self.imageNode.frame = CGRect(origin: CGPoint(), size: size)
let apply = makeLayout(TransformImageArguments(corners: ImageCorners(radius: isRound ? size.width / 2.0 : 2.0), imageSize: dimensions.aspectFilled(size), boundingSize: size, intrinsicInsets: UIEdgeInsets()))
let apply = makeLayout(TransformImageArguments(corners: ImageCorners(radius: radius), imageSize: dimensions.aspectFilled(size), boundingSize: size, intrinsicInsets: UIEdgeInsets()))
apply()
}
}
@ -1142,6 +1158,18 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
}
}
private struct ContentImageSpec {
var message: EngineMessage
var media: EngineMedia
var size: CGSize
init(message: EngineMessage, media: EngineMedia, size: CGSize) {
self.message = message
self.media = media
self.size = size
}
}
var item: ChatListItem?
private let backgroundNode: ASDisplayNode
@ -1156,7 +1184,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
var avatarIconComponent: EmojiStatusComponent?
var avatarVideoNode: AvatarVideoNode?
var avatarTapRecognizer: UITapGestureRecognizer?
var avatarMediaNode: AvatarVideoNode?
private var avatarMediaNode: ChatListMediaPreviewNode?
private var inlineNavigationMarkLayer: SimpleLayer?
@ -1197,7 +1225,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
private var cachedDataDisposable = MetaDisposable()
private var currentTextLeftCutout: CGFloat = 0.0
private var currentMediaPreviewSpecs: [(message: EngineMessage, media: EngineMedia, size: CGSize)] = []
private var currentMediaPreviewSpecs: [ContentImageSpec] = []
private var mediaPreviewNodes: [EngineMedia.Id: ChatListMediaPreviewNode] = [:]
var selectableControlNode: ItemListSelectableControlNode?
@ -2153,6 +2181,21 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
if !itemTags.isEmpty {
forumTopicData = nil
topForumTopicItems = []
if case let .chat(itemPeer, _, _, _, _, _, _) = contentData {
if let messagePeer = itemPeer.chatMainPeer {
switch messagePeer {
case let .channel(channel):
if case .group = channel.info {
useInlineAuthorPrefix = true
}
case .legacyGroup:
useInlineAuthorPrefix = true
default:
break
}
}
}
}
if useInlineAuthorPrefix {
@ -2175,7 +2218,9 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
let contentImageSpacing: CGFloat = 2.0
let forwardedIconSpacing: CGFloat = 6.0
let contentImageTrailingSpace: CGFloat = 5.0
var contentImageSpecs: [(message: EngineMessage, media: EngineMedia, size: CGSize)] = []
var contentImageSpecs: [ContentImageSpec] = []
var avatarContentImageSpec: ContentImageSpec?
var forumThread: (id: Int64, title: String, iconId: Int64?, iconColor: Int32, isUnread: Bool)?
var displayForwardedIcon = false
@ -2494,21 +2539,31 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
if displayMediaPreviews {
let contentImageFillSize = CGSize(width: 8.0, height: contentImageSize.height)
_ = contentImageFillSize
var contentImageIsDisplayedAsAvatar = false
if case let .peer(peerData) = item.content, let customMessageListData = peerData.customMessageListData, customMessageListData.commandPrefix != nil {
contentImageIsDisplayedAsAvatar = true
}
for message in messages {
if contentImageSpecs.count >= 3 {
break
}
inner: for media in message.media {
if let image = media as? TelegramMediaImage {
if let _ = largestImageRepresentation(image.representations) {
let fitSize = contentImageSize
contentImageSpecs.append((message, .image(image), fitSize))
contentImageSpecs.append(ContentImageSpec(message: message, media: .image(image), size: fitSize))
}
break inner
} else if let file = media as? TelegramMediaFile {
if file.isVideo, !file.isVideoSticker, let _ = file.dimensions {
let fitSize = contentImageSize
contentImageSpecs.append((message, .file(file), fitSize))
contentImageSpecs.append(ContentImageSpec(message: message, media: .file(file), size: fitSize))
} else if contentImageIsDisplayedAsAvatar && (file.isSticker || file.isVideoSticker) {
let fitSize = contentImageSize
contentImageSpecs.append(ContentImageSpec(message: message, media: .file(file), size: fitSize))
}
break inner
} else if let webpage = media as? TelegramMediaWebpage, case let .Loaded(content) = webpage.content {
@ -2516,36 +2571,41 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
if let image = content.image, let type = content.type, imageTypes.contains(type) {
if let _ = largestImageRepresentation(image.representations) {
let fitSize = contentImageSize
contentImageSpecs.append((message, .image(image), fitSize))
contentImageSpecs.append(ContentImageSpec(message: message, media: .image(image), size: fitSize))
}
break inner
} else if let file = content.file {
if file.isVideo, !file.isInstantVideo, let _ = file.dimensions {
let fitSize = contentImageSize
contentImageSpecs.append((message, .file(file), fitSize))
contentImageSpecs.append(ContentImageSpec(message: message, media: .file(file), size: fitSize))
}
break inner
}
} else if let action = media as? TelegramMediaAction, case let .suggestedProfilePhoto(image) = action.action, let _ = image {
let fitSize = contentImageSize
contentImageSpecs.append((message, .action(action), fitSize))
contentImageSpecs.append(ContentImageSpec(message: message, media: .action(action), size: fitSize))
} else if let storyMedia = media as? TelegramMediaStory, let story = message.associatedStories[storyMedia.storyId], !story.data.isEmpty, case let .item(storyItem) = story.get(Stories.StoredItem.self) {
if let image = storyItem.media as? TelegramMediaImage {
if let _ = largestImageRepresentation(image.representations) {
let fitSize = contentImageSize
contentImageSpecs.append((message, .image(image), fitSize))
contentImageSpecs.append(ContentImageSpec(message: message, media: .image(image), size: fitSize))
}
break inner
} else if let file = storyItem.media as? TelegramMediaFile {
if file.isVideo, !file.isInstantVideo, let _ = file.dimensions {
let fitSize = contentImageSize
contentImageSpecs.append((message, .file(file), fitSize))
contentImageSpecs.append(ContentImageSpec(message: message, media: .file(file), size: fitSize))
}
break inner
}
}
}
}
if contentImageIsDisplayedAsAvatar {
avatarContentImageSpec = contentImageSpecs.first
contentImageSpecs.removeAll()
}
}
} else {
attributedText = NSAttributedString(string: messageText, font: textFont, textColor: theme.messageTextColor)
@ -4056,7 +4116,11 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
}
var validMediaIds: [EngineMedia.Id] = []
for (message, media, mediaSize) in contentImageSpecs {
for spec in contentImageSpecs {
let message = spec.message
let media = spec.media
let mediaSize = spec.size
var mediaId = media.id
if mediaId == nil, case let .action(action) = media, case let .suggestedProfilePhoto(image) = action.action {
mediaId = image?.id
@ -4095,6 +4159,36 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
strongSelf.currentMediaPreviewSpecs = contentImageSpecs
strongSelf.currentTextLeftCutout = textLeftCutout
if let avatarContentImageSpec {
strongSelf.avatarNode.isHidden = true
if let previous = strongSelf.avatarMediaNode, previous.media != avatarContentImageSpec.media {
strongSelf.avatarMediaNode = nil
previous.removeFromSupernode()
}
var avatarMediaNodeTransition = transition
let avatarMediaNode: ChatListMediaPreviewNode
if let current = strongSelf.avatarMediaNode {
avatarMediaNode = current
} else {
avatarMediaNodeTransition = .immediate
avatarMediaNode = ChatListMediaPreviewNode(context: item.context, message: avatarContentImageSpec.message, media: avatarContentImageSpec.media)
strongSelf.avatarMediaNode = avatarMediaNode
strongSelf.contextContainer.addSubnode(avatarMediaNode)
}
avatarMediaNodeTransition.updateFrame(node: avatarMediaNode, frame: avatarFrame)
avatarMediaNode.updateLayout(size: avatarFrame.size, synchronousLoads: synchronousLoads)
} else {
strongSelf.avatarNode.isHidden = false
if let avatarMediaNode = strongSelf.avatarMediaNode {
strongSelf.avatarMediaNode = nil
avatarMediaNode.removeFromSupernode()
}
}
if !contentDelta.x.isZero || !contentDelta.y.isZero {
let titlePosition = strongSelf.titleNode.position
transition.animatePosition(node: strongSelf.titleNode, from: CGPoint(x: titlePosition.x - contentDelta.x, y: titlePosition.y - contentDelta.y))

View File

@ -702,9 +702,29 @@ private struct ContactsListNodeTransition {
}
public enum ContactListPresentation {
public struct Search {
public var signal: Signal<String, NoError>
public var searchChatList: Bool
public var searchDeviceContacts: Bool
public var searchGroups: Bool
public var searchChannels: Bool
public var globalSearch: Bool
public var displaySavedMessages: Bool
public init(signal: Signal<String, NoError>, searchChatList: Bool, searchDeviceContacts: Bool, searchGroups: Bool, searchChannels: Bool, globalSearch: Bool, displaySavedMessages: Bool) {
self.signal = signal
self.searchChatList = searchChatList
self.searchDeviceContacts = searchDeviceContacts
self.searchGroups = searchGroups
self.searchChannels = searchChannels
self.globalSearch = globalSearch
self.displaySavedMessages = displaySavedMessages
}
}
case orderedByPresence(options: [ContactListAdditionalOption])
case natural(options: [ContactListAdditionalOption], includeChatList: Bool, topPeers: Bool)
case search(signal: Signal<String, NoError>, searchChatList: Bool, searchDeviceContacts: Bool, searchGroups: Bool, searchChannels: Bool, globalSearch: Bool)
case search(Search)
public var sortOrder: ContactsSortOrder? {
switch self {
@ -1097,7 +1117,15 @@ public final class ContactListNode: ASDisplayNode {
displayTopPeers = displayTopPeersValue
}
if case let .search(query, searchChatList, searchDeviceContacts, searchGroups, searchChannels, globalSearch) = presentation {
if case let .search(search) = presentation {
let query = search.signal
let searchChatList = search.searchChatList
let searchDeviceContacts = search.searchDeviceContacts
let searchGroups = search.searchGroups
let searchChannels = search.searchChannels
let globalSearch = search.globalSearch
let displaySavedMessages = search.displaySavedMessages
return query
|> mapToSignal { query in
let foundLocalContacts: Signal<([FoundPeer], [EnginePeer.Id: EnginePeer.Presence]), NoError>
@ -1108,6 +1136,12 @@ public final class ContactListNode: ASDisplayNode {
var resultPeers: [FoundPeer] = []
for peer in peers {
if !displaySavedMessages {
if peer.peerId == context.account.peerId {
continue
}
}
if searchGroups || searchChannels {
let mainPeer = peer.chatMainPeer
if let _ = mainPeer as? TelegramUser {

View File

@ -354,7 +354,7 @@ public final class TelegramBusinessHours: Equatable, Codable {
}
if minutes.isEmpty {
return .closed
} else if minutes == IndexSet(integersIn: 0 ..< 24 * 60) {
} else if minutes == IndexSet(integersIn: 0 ..< 24 * 60) || minutes == IndexSet(integersIn: 0 ..< (24 * 60 - 1)) {
return .open
} else {
return .intervals(day)

View File

@ -22,6 +22,10 @@ private func dayBusinessHoursText(presentationData: PresentationData, day: Teleg
businessHoursText += "closed"
case let .intervals(intervals):
func clipMinutes(_ value: Int) -> Int {
var value = value
if value < 0 {
value = 24 * 60 + value
}
return value % (24 * 60)
}
@ -478,7 +482,13 @@ private final class PeerInfoScreenBusinessHoursItemNode: PeerInfoScreenItemNode
var dayHeights: CGFloat = 0.0
for i in 0 ..< businessDays.count {
for rawI in 0 ..< businessDays.count {
if rawI == 0 {
//skip current day
continue
}
let i = (rawI + currentDayIndex) % businessDays.count
dayHeights += daySpacing
var dayTransition = transition

View File

@ -41,7 +41,7 @@ private func generateRingImage(color: UIColor, size: CGSize = CGSize(width: 40.0
})
}
public func generatePeerNameColorImage(nameColor: PeerNameColors.Colors?, isDark: Bool, isLocked: Bool = false, bounds: CGSize = CGSize(width: 40.0, height: 40.0), size: CGSize = CGSize(width: 40.0, height: 40.0)) -> UIImage? {
public func generatePeerNameColorImage(nameColor: PeerNameColors.Colors?, isDark: Bool, isLocked: Bool = false, isEmpty: Bool = false, bounds: CGSize = CGSize(width: 40.0, height: 40.0), size: CGSize = CGSize(width: 40.0, height: 40.0)) -> UIImage? {
return generateImage(bounds, rotatedContext: { contextSize, context in
let bounds = CGRect(origin: CGPoint(), size: contextSize)
context.clear(bounds)
@ -105,6 +105,25 @@ public func generatePeerNameColorImage(nameColor: PeerNameColors.Colors?, isDark
context.scaleBy(x: 1.0, y: -1.0)
context.translateBy(x: -imageFrame.midX, y: -imageFrame.midY)
if let cgImage = image.cgImage {
context.clip(to: imageFrame, mask: cgImage)
context.setFillColor(UIColor.clear.cgColor)
context.setBlendMode(.copy)
context.fill(imageFrame)
}
}
} else if isEmpty {
if let image = UIImage(bundleImageName: "Chat/Message/SideCloseIcon") {
let scaleFactor: CGFloat = 1.0
let imageSize = CGSize(width: floor(image.size.width * scaleFactor), height: floor(image.size.height * scaleFactor))
var imageFrame = CGRect(origin: CGPoint(x: circleBounds.minX + floor((circleBounds.width - imageSize.width) * 0.5), y: circleBounds.minY + floor((circleBounds.height - imageSize.height) * 0.5)), size: imageSize)
imageFrame.origin.y += 0.5
imageFrame.origin.x += 0.5
context.translateBy(x: imageFrame.midX, y: imageFrame.midY)
context.scaleBy(x: 1.0, y: -1.0)
context.translateBy(x: -imageFrame.midX, y: -imageFrame.midY)
if let cgImage = image.cgImage {
context.clip(to: imageFrame, mask: cgImage)
context.setFillColor(UIColor.clear.cgColor)
@ -212,7 +231,7 @@ private final class PeerNameColorIconItemNode : ASDisplayNode {
self.item = item
if updatedAccentColor {
self.fillNode.image = generatePeerNameColorImage(nameColor: item.colors, isDark: item.isDark, isLocked: item.selected && item.isLocked, bounds: size, size: size)
self.fillNode.image = generatePeerNameColorImage(nameColor: item.colors, isDark: item.isDark, isLocked: item.selected && item.isLocked, isEmpty: item.colors == nil, bounds: size, size: size)
self.ringNode.image = generateRingImage(color: item.colors?.main ?? UIColor(rgb: 0x798896), size: size)
}

View File

@ -252,6 +252,8 @@ final class ContactMultiselectionControllerNode: ASDisplayNode {
var searchGroups = false
var searchChannels = false
var globalSearch = false
var displaySavedMessages = true
var filters = filters
switch mode {
case .groupCreation, .channelCreation:
globalSearch = true
@ -260,15 +262,31 @@ final class ContactMultiselectionControllerNode: ASDisplayNode {
searchGroups = searchGroupsValue
searchChannels = searchChannelsValue
globalSearch = true
case .chatSelection:
searchChatList = true
searchGroups = true
searchChannels = true
case let .chatSelection(chatSelection):
if chatSelection.onlyUsers {
searchChatList = true
searchGroups = false
searchChannels = false
displaySavedMessages = false
filters.append(.excludeSelf)
} else {
searchChatList = true
searchGroups = true
searchChannels = true
}
globalSearch = false
case .premiumGifting, .requestedUsersSelection:
searchChatList = true
}
let searchResultsNode = ContactListNode(context: context, presentation: .single(.search(signal: searchText.get(), searchChatList: searchChatList, searchDeviceContacts: false, searchGroups: searchGroups, searchChannels: searchChannels, globalSearch: globalSearch)), filters: filters, onlyWriteable: strongSelf.onlyWriteable, isPeerEnabled: strongSelf.isPeerEnabled, selectionState: selectionState, isSearch: true)
let searchResultsNode = ContactListNode(context: context, presentation: .single(.search(ContactListPresentation.Search(
signal: searchText.get(),
searchChatList: searchChatList,
searchDeviceContacts: false,
searchGroups: searchGroups,
searchChannels: searchChannels,
globalSearch: globalSearch,
displaySavedMessages: displaySavedMessages
))), filters: filters, onlyWriteable: strongSelf.onlyWriteable, isPeerEnabled: strongSelf.isPeerEnabled, selectionState: selectionState, isSearch: true)
searchResultsNode.openPeer = { peer, _ in
self?.tokenListNode.setText("")
self?.openPeer?(peer)