Merge commit '1c4500fcf89d52a7f1a00334065d5402ca829e37'

This commit is contained in:
Peter 2019-04-15 01:31:46 +01:00
commit 90b4d379d5
43 changed files with 954 additions and 281 deletions

View File

@ -143,6 +143,7 @@
09F21565225C83E100AEDF6D /* ChatListStatusNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09F21564225C83E100AEDF6D /* ChatListStatusNode.swift */; };
09F21567225C8EF500AEDF6D /* ChatListOnlineNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09F21566225C8EF500AEDF6D /* ChatListOnlineNode.swift */; };
09F2158D225CF5BC00AEDF6D /* Pasteboard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09F2158C225CF5BC00AEDF6D /* Pasteboard.swift */; };
09F21594225F20D300AEDF6D /* LargeEmojiResource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09F21593225F20D300AEDF6D /* LargeEmojiResource.swift */; };
09F664C021EAAFAF00AB7E26 /* ThemeColorsGridController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09F664BF21EAAFAF00AB7E26 /* ThemeColorsGridController.swift */; };
09F664C221EAAFCB00AB7E26 /* ThemeColorsGridControllerNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09F664C121EAAFCB00AB7E26 /* ThemeColorsGridControllerNode.swift */; };
09F664C421EAB98300AB7E26 /* ThemeColorsGridControllerItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09F664C321EAB98300AB7E26 /* ThemeColorsGridControllerItem.swift */; };
@ -1309,6 +1310,7 @@
09F21564225C83E100AEDF6D /* ChatListStatusNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatListStatusNode.swift; sourceTree = "<group>"; };
09F21566225C8EF500AEDF6D /* ChatListOnlineNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatListOnlineNode.swift; sourceTree = "<group>"; };
09F2158C225CF5BC00AEDF6D /* Pasteboard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Pasteboard.swift; sourceTree = "<group>"; };
09F21593225F20D300AEDF6D /* LargeEmojiResource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LargeEmojiResource.swift; sourceTree = "<group>"; };
09F664BF21EAAFAF00AB7E26 /* ThemeColorsGridController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThemeColorsGridController.swift; sourceTree = "<group>"; };
09F664C121EAAFCB00AB7E26 /* ThemeColorsGridControllerNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThemeColorsGridControllerNode.swift; sourceTree = "<group>"; };
09F664C321EAB98300AB7E26 /* ThemeColorsGridControllerItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThemeColorsGridControllerItem.swift; sourceTree = "<group>"; };
@ -4916,6 +4918,7 @@
099529AB21CDBBB200805E13 /* QRCode.swift */,
0910B0EE21FA532D00F8F87D /* WallpaperResources.swift */,
D0CCD61A222E8B4500EE1E08 /* TimeBasedVideoPreload.swift */,
09F21593225F20D300AEDF6D /* LargeEmojiResource.swift */,
);
name = Resources;
sourceTree = "<group>";
@ -6121,6 +6124,7 @@
D01776B31F1D69A80044446D /* RadialStatusNode.swift in Sources */,
D084023420E295F000065674 /* GroupStickerPackSetupController.swift in Sources */,
D01C06BE1FBCAF06001561AB /* ChatMessageBubbleMosaicLayout.swift in Sources */,
09F21594225F20D300AEDF6D /* LargeEmojiResource.swift in Sources */,
0900678D21ED5EA800530762 /* WallpaperColorPanelNode.swift in Sources */,
D0EC6E451EB9F58900EBF1C3 /* ItemListMultilineTextItem.swift in Sources */,
091417F421EF4F5F00C8325A /* WallpaperGalleryItem.swift in Sources */,

View File

@ -119,7 +119,7 @@ private enum BlockedPeersEntry: ItemListNodeEntry {
arguments.addPeer()
})
case let .peerItem(_, theme, strings, dateTimeFormat, nameDisplayOrder, peer, editing, enabled):
return ItemListPeerItem(theme: theme, strings: strings, dateTimeFormat: dateTimeFormat, nameDisplayOrder: nameDisplayOrder, account: arguments.account, peer: peer, presence: nil, text: .none, label: .none, editing: editing, switchValue: nil, enabled: enabled, sectionId: self.section, action: {
return ItemListPeerItem(theme: theme, strings: strings, dateTimeFormat: dateTimeFormat, nameDisplayOrder: nameDisplayOrder, account: arguments.account, peer: peer, presence: nil, text: .none, label: .none, editing: editing, switchValue: nil, enabled: enabled, selectable: true, sectionId: self.section, action: {
arguments.openPeer(peer)
}, setPeerIdWithRevealedOptions: { previousId, id in
arguments.setPeerIdWithRevealedOptions(previousId, id)

View File

@ -171,3 +171,17 @@ final class CachedAlbumArtworkRepresentation: CachedMediaResourceRepresentation
}
}
}
final class CachedLargeEmojiRepresentation: CachedMediaResourceRepresentation {
var uniqueId: String {
return "large-emoji"
}
func isEqual(to: CachedMediaResourceRepresentation) -> Bool {
if let to = to as? CachedLargeEmojiRepresentation {
return true
} else {
return false
}
}
}

View File

@ -214,7 +214,7 @@ private enum ChannelAdminsEntry: ItemListNodeEntry {
arguments.openAdmin(participant.participant)
}
}
return ItemListPeerItem(theme: theme, strings: strings, dateTimeFormat: dateTimeFormat, nameDisplayOrder: nameDisplayOrder, account: arguments.account, peer: participant.peer, presence: nil, text: .text(peerText), label: .none, editing: editing, switchValue: nil, enabled: enabled, sectionId: self.section, action: action, setPeerIdWithRevealedOptions: { previousId, id in
return ItemListPeerItem(theme: theme, strings: strings, dateTimeFormat: dateTimeFormat, nameDisplayOrder: nameDisplayOrder, account: arguments.account, peer: participant.peer, presence: nil, text: .text(peerText), label: .none, editing: editing, switchValue: nil, enabled: enabled, selectable: true, sectionId: self.section, action: action, setPeerIdWithRevealedOptions: { previousId, id in
arguments.setPeerIdWithRevealedOptions(previousId, id)
}, removePeer: { peerId in
arguments.removeAdmin(peerId)

View File

@ -157,7 +157,7 @@ private enum ChannelBlacklistEntry: ItemListNodeEntry {
default:
break
}
return ItemListPeerItem(theme: theme, strings: strings, dateTimeFormat: dateTimeFormat, nameDisplayOrder: nameDisplayOrder, account: arguments.account, peer: participant.peer, presence: nil, text: text, label: .none, editing: editing, switchValue: nil, enabled: enabled, sectionId: self.section, action: {
return ItemListPeerItem(theme: theme, strings: strings, dateTimeFormat: dateTimeFormat, nameDisplayOrder: nameDisplayOrder, account: arguments.account, peer: participant.peer, presence: nil, text: text, label: .none, editing: editing, switchValue: nil, enabled: enabled, selectable: true, sectionId: self.section, action: {
arguments.openPeer(participant)
}, setPeerIdWithRevealedOptions: { previousId, id in
arguments.setPeerIdWithRevealedOptions(previousId, id)

View File

@ -851,6 +851,8 @@ public func channelInfoController(context: AccountContext, peerId: PeerId) -> Vi
actionsDisposable.add(toggleShouldChannelMessagesSignatures(account: context.account, peerId: peerId, enabled: enabled).start())
})
let hapticFeedback = HapticFeedback()
let globalNotificationsKey: PostboxViewKey = .preferences(keys: Set<ValueBoxKey>([PreferencesKeys.globalNotifications]))
let signal = combineLatest(context.sharedContext.presentationData, statePromise.get(), context.account.viewTracker.peerView(peerId), context.account.postbox.combinedView(keys: [globalNotificationsKey]))
|> map { presentationData, state, view, combinedView -> (ItemListControllerState, (ItemListNodeState<ChannelInfoEntry>, ChannelInfoEntry.ItemGenerationArguments)) in
@ -901,15 +903,25 @@ public func channelInfoController(context: AccountContext, peerId: PeerId) -> Vi
} else {
rightNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Done), style: .bold, enabled: doneEnabled, action: {
var updateValues: (title: String?, description: String?) = (nil, nil)
var failed = false
updateState { state in
updateValues = valuesRequiringUpdate(state: state, view: view)
if updateValues.0 != nil || updateValues.1 != nil {
if (updateValues.description?.count ?? 0) > 255 {
failed = true
return state
}
return state.withUpdatedSavingData(true)
} else {
return state.withUpdatedEditingState(nil)
}
}
guard !failed else {
hapticFeedback.error()
return
}
let updateTitle: Signal<Void, Void>
if let titleValue = updateValues.title {
updateTitle = updatePeerTitle(account: context.account, peerId: peerId, title: titleValue)

View File

@ -181,7 +181,7 @@ private enum ChannelMembersEntry: ItemListNodeEntry {
case let .addMemberInfo(theme, text):
return ItemListTextItem(theme: theme, text: .plain(text), sectionId: self.section)
case let .peerItem(_, theme, strings, dateTimeFormat, nameDisplayOrder, participant, editing, enabled):
return ItemListPeerItem(theme: theme, strings: strings, dateTimeFormat: dateTimeFormat, nameDisplayOrder: nameDisplayOrder, account: arguments.account, peer: participant.peer, presence: participant.presences[participant.peer.id], text: .presence, label: .none, editing: editing, switchValue: nil, enabled: enabled, sectionId: self.section, action: {
return ItemListPeerItem(theme: theme, strings: strings, dateTimeFormat: dateTimeFormat, nameDisplayOrder: nameDisplayOrder, account: arguments.account, peer: participant.peer, presence: participant.presences[participant.peer.id], text: .presence, label: .none, editing: editing, switchValue: nil, enabled: enabled, selectable: true, sectionId: self.section, action: {
arguments.openPeer(participant.peer)
}, setPeerIdWithRevealedOptions: { previousId, id in
arguments.setPeerIdWithRevealedOptions(previousId, id)

View File

@ -213,7 +213,7 @@ private enum ChannelPermissionsEntry: ItemListNodeEntry {
default:
break
}
return ItemListPeerItem(theme: theme, strings: strings, dateTimeFormat: dateTimeFormat, nameDisplayOrder: nameDisplayOrder, account: arguments.account, peer: participant.peer, presence: nil, text: text, label: .none, editing: editing, switchValue: nil, enabled: enabled, sectionId: self.section, action: canOpen ? {
return ItemListPeerItem(theme: theme, strings: strings, dateTimeFormat: dateTimeFormat, nameDisplayOrder: nameDisplayOrder, account: arguments.account, peer: participant.peer, presence: nil, text: text, label: .none, editing: editing, switchValue: nil, enabled: enabled, selectable: true, sectionId: self.section, action: canOpen ? {
arguments.openPeer(participant.participant)
} : {
arguments.openPeerInfo(participant.peer)

View File

@ -315,7 +315,7 @@ private enum ChannelVisibilityEntry: ItemListNodeEntry {
if let addressName = peer.addressName {
label = "t.me/" + addressName
}
return ItemListPeerItem(theme: theme, strings: strings, dateTimeFormat: dateTimeFormat, nameDisplayOrder: nameDisplayOrder, account: arguments.account, peer: peer, presence: nil, text: .text(label), label: .none, editing: editing, switchValue: nil, enabled: enabled, sectionId: self.section, action: nil, setPeerIdWithRevealedOptions: { previousId, id in
return ItemListPeerItem(theme: theme, strings: strings, dateTimeFormat: dateTimeFormat, nameDisplayOrder: nameDisplayOrder, account: arguments.account, peer: peer, presence: nil, text: .text(label), label: .none, editing: editing, switchValue: nil, enabled: enabled, selectable: true, sectionId: self.section, action: nil, setPeerIdWithRevealedOptions: { previousId, id in
arguments.setPeerIdWithRevealedOptions(previousId, id)
}, removePeer: { peerId in
arguments.revokePeerId(peerId)

View File

@ -179,7 +179,9 @@ func inputPanelForChatPresentationIntefaceState(_ chatPresentationInterfaceState
var displayBotStartPanel = false
if let _ = chatPresentationInterfaceState.botStartPayload {
displayBotStartPanel = true
if let user = chatPresentationInterfaceState.renderedPeer?.peer as? TelegramUser, user.botInfo != nil {
displayBotStartPanel = true
}
} else if let chatHistoryState = chatPresentationInterfaceState.chatHistoryState, case .loaded(true) = chatHistoryState {
if let user = chatPresentationInterfaceState.renderedPeer?.peer as? TelegramUser, user.botInfo != nil {
displayBotStartPanel = true

View File

@ -2,9 +2,45 @@ import Foundation
import AsyncDisplayKit
import Display
enum ChatListBadgeContent: Equatable {
case none
case blank
case text(NSAttributedString)
case mention
var text: String? {
if case let .text(text) = self {
return text.string
}
return nil
}
var isEmpty: Bool {
if case .none = self {
return true
}
return false
}
}
private func measureString(_ string: String) -> String {
let wideChar = "8"
if string.count < 2 {
return wideChar
} else {
return string[string.startIndex ..< string.index(string.endIndex, offsetBy: -1)] + wideChar
}
}
private let badgeFont = Font.regular(14.0)
final class ChatListBadgeNode: ASDisplayNode {
private let backgroundNode: ASImageNode
private let textNode: TextNode
private let measureTextNode: TextNode
private var text: String?
private var content: ChatListBadgeContent?
override init() {
self.backgroundNode = ASImageNode()
@ -16,46 +52,139 @@ final class ChatListBadgeNode: ASDisplayNode {
self.textNode.isUserInteractionEnabled = false
self.textNode.displaysAsynchronously = true
self.measureTextNode = TextNode()
super.init()
self.addSubnode(self.backgroundNode)
self.addSubnode(self.textNode)
}
func asyncLayout() -> (CGSize, UIImage?, NSAttributedString?) -> (CGSize, () -> Void) {
func asyncLayout() -> (CGSize, UIImage?, ChatListBadgeContent) -> (CGSize, (Bool) -> Void) {
let textLayout = TextNode.asyncLayout(self.textNode)
let measureTextLayout = TextNode.asyncLayout(self.measureTextNode)
return { [weak self] boundingSize, backgroundImage, text in
let (layout, apply) = textLayout(TextNodeLayoutArguments(attributedString: text, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: boundingSize, alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
let currentContent = self.content
return { [weak self] boundingSize, backgroundImage, content in
var badgeWidth: CGFloat = 0.0
var badgeSize: CGFloat = 0.0
if let backgroundImage = backgroundImage {
badgeSize += max(backgroundImage.size.width, layout.size.width + 10.0) + 5.0
var textLayoutAndApply: (TextNodeLayout, () -> TextNode)?
switch content {
case let .text(text):
textLayoutAndApply = textLayout(TextNodeLayoutArguments(attributedString: text, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: boundingSize, alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
let (measureLayout, _) = measureTextLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: measureString(text.string), font: badgeFont, textColor: .black), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: boundingSize, alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
badgeWidth = max(20.0, measureLayout.size.width + 10.0)
case .mention, .blank:
badgeWidth = 20.0
case .none:
badgeWidth = 0.0
}
// if let currentMentionBadgeImage = currentMentionBadgeImage {
// if !badgeSize.isZero {
// badgeSize += currentMentionBadgeImage.size.width + 4.0
// } else {
// badgeSize += currentMentionBadgeImage.size.width + 5.0
// }
// }
//badgeSize = max(badgeSize, reorderInset)
return (CGSize(width: badgeSize, height: 20.0), {
return (CGSize(width: badgeWidth, height: 20.0), { animated in
if let strongSelf = self {
let _ = apply()
strongSelf.content = content
if let backgroundImage = backgroundImage {
strongSelf.backgroundNode.image = backgroundImage
}
strongSelf.backgroundNode.isHidden = backgroundImage == nil
let backgroundWidth = max(layout.size.width + 10.0, strongSelf.backgroundNode.image?.size.width ?? 0.0)
let backgroundFrame = CGRect(x: 0.0, y: 0.0, width: backgroundWidth, height: strongSelf.backgroundNode.image?.size.height ?? 0.0)
let badgeTextFrame = CGRect(origin: CGPoint(x: backgroundFrame.midX - layout.size.width / 2.0, y: backgroundFrame.minY + 2.0), size: layout.size)
if content == currentContent {
return
}
strongSelf.textNode.frame = badgeTextFrame
let badgeWidth = max(20.0, badgeWidth)
let previousBadgeWidth = !strongSelf.backgroundNode.frame.width.isZero ? strongSelf.backgroundNode.frame.width : badgeWidth
var animateTextNode = false
if animated {
strongSelf.isHidden = false
let currentIsEmpty = currentContent?.isEmpty ?? true
let nextIsEmpty = content.isEmpty
if !nextIsEmpty {
if case .text = content {
strongSelf.textNode.alpha = 1.0
} else {
strongSelf.textNode.alpha = 0.0
}
}
if currentIsEmpty && !nextIsEmpty {
strongSelf.layer.animateScale(from: 0.0001, to: 1.2, duration: 0.2, removeOnCompletion: false, completion: { [weak self] finished in
if let strongSelf = self {
strongSelf.layer.animateScale(from: 1.15, to: 1.0, duration: 0.12, removeOnCompletion: false)
}
})
} else if !currentIsEmpty && !nextIsEmpty && currentContent?.text != content.text {
var animateScale = true
if let currentText = currentContent?.text, let currentValue = Int(currentText), let text = content.text, let value = Int(text) {
if value < currentValue {
animateScale = false
}
}
if animateScale {
strongSelf.layer.animateScale(from: 1.0, to: 1.2, duration: 0.12, removeOnCompletion: false, completion: { [weak self] finished in
if let strongSelf = self {
strongSelf.layer.animateScale(from: 1.2, to: 1.0, duration: 0.12, removeOnCompletion: false)
}
})
}
var animateSnapshot = true
if let currentContent = currentContent, case .blank = currentContent {
animateSnapshot = false
}
if animateSnapshot, let snapshotView = strongSelf.textNode.view.snapshotContentTree() {
snapshotView.frame = strongSelf.textNode.frame
strongSelf.textNode.view.superview?.insertSubview(snapshotView, aboveSubview: strongSelf.textNode.view)
snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false, completion: { [weak snapshotView] _ in
snapshotView?.removeFromSuperview()
})
snapshotView.layer.animatePosition(from: CGPoint(), to: CGPoint(x: (badgeWidth - previousBadgeWidth) / 2.0, y: -8.0), duration: 0.15, timingFunction: kCAMediaTimingFunctionEaseInEaseOut, removeOnCompletion: false, additive: true)
}
animateTextNode = true
} else if !currentIsEmpty && nextIsEmpty {
strongSelf.layer.animateScale(from: 1.0, to: 0.0001, duration: 0.12, removeOnCompletion: true, completion: { [weak self] finished in
if let strongSelf = self {
strongSelf.isHidden = true
}
})
}
} else {
if case .none = content {
strongSelf.isHidden = true
} else {
strongSelf.isHidden = false
}
if case .text = content {
strongSelf.textNode.alpha = 1.0
} else {
strongSelf.textNode.alpha = 0.0
}
}
let _ = textLayoutAndApply?.1()
let backgroundFrame = CGRect(x: 0.0, y: 0.0, width: badgeWidth, height: strongSelf.backgroundNode.image?.size.height ?? 0.0)
if let (textLayout, _) = textLayoutAndApply {
let badgeTextFrame = CGRect(origin: CGPoint(x: backgroundFrame.midX - textLayout.size.width / 2.0, y: backgroundFrame.minY + 2.0), size: textLayout.size)
strongSelf.textNode.frame = badgeTextFrame
if animateTextNode {
strongSelf.textNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15)
strongSelf.textNode.layer.animatePosition(from: CGPoint(x: (previousBadgeWidth - badgeWidth) / 2.0, y: 8.0), to: CGPoint(), duration: 0.15, timingFunction: kCAMediaTimingFunctionEaseInEaseOut, removeOnCompletion: false, additive: true)
}
}
strongSelf.backgroundNode.frame = backgroundFrame
if animated && badgeWidth != previousBadgeWidth {
let previousBackgroundFrame = CGRect(x: 0.0, y: 0.0, width: previousBadgeWidth, height: backgroundFrame.height)
strongSelf.backgroundNode.layer.animateFrame(from: previousBackgroundFrame, to: backgroundFrame, duration: 0.15, timingFunction: kCAMediaTimingFunctionEaseInEaseOut)
}
}
})
}

View File

@ -260,16 +260,12 @@ public class ChatListController: TelegramController, KeyShortcutResponder, UIVie
})
}
self.badgeDisposable = (renderedTotalUnreadCount(accountManager: context.sharedContext.accountManager, postbox: context.account.postbox) |> deliverOnMainQueue).start(next: { [weak self] count in
self.badgeDisposable = (combineLatest(renderedTotalUnreadCount(accountManager: context.sharedContext.accountManager, postbox: context.account.postbox), self.presentationDataValue.get()) |> deliverOnMainQueue).start(next: { [weak self] count, presentationData in
if let strongSelf = self {
if count.0 == 0 {
strongSelf.tabBarItem.badgeValue = ""
} else {
if count.0 > 1000 {
strongSelf.tabBarItem.badgeValue = "\(count.0 / 1000)K"
} else {
strongSelf.tabBarItem.badgeValue = "\(count.0)"
}
strongSelf.tabBarItem.badgeValue = compactNumericCountString(Int(count.0), decimalSeparator: presentationData.dateTimeFormat.decimalSeparator)
}
}
})

View File

@ -248,11 +248,12 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
let textNode: TextNode
let inputActivitiesNode: ChatListInputActivitiesNode
let dateNode: TextNode
let statusNode: ASImageNode
let separatorNode: ASDisplayNode
let statusNode: ChatListStatusNode
let badgeNode: ChatListBadgeNode
let mentionBadgeNode: ChatListBadgeNode
let onlineNode: ChatListOnlineNode
let mentionBadgeNode: ASImageNode
let pinnedIconNode: ASImageNode
var secretIconNode: ASImageNode?
var verificationIconNode: ASImageNode?
let mutedIconNode: ASImageNode
@ -374,17 +375,15 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
self.dateNode.isUserInteractionEnabled = false
self.dateNode.displaysAsynchronously = true
self.statusNode = ASImageNode()
self.statusNode.displaysAsynchronously = false
self.statusNode.displayWithoutProcessing = true
self.statusNode = ChatListStatusNode()
self.badgeNode = ChatListBadgeNode()
self.mentionBadgeNode = ChatListBadgeNode()
self.onlineNode = ChatListOnlineNode()
self.mentionBadgeNode = ASImageNode()
self.mentionBadgeNode.isLayerBacked = true
self.mentionBadgeNode.displaysAsynchronously = false
self.mentionBadgeNode.displayWithoutProcessing = true
self.pinnedIconNode = ASImageNode()
self.pinnedIconNode.isLayerBacked = true
self.pinnedIconNode.displaysAsynchronously = false
self.pinnedIconNode.displayWithoutProcessing = true
self.mutedIconNode = ASImageNode()
self.mutedIconNode.isLayerBacked = true
@ -408,6 +407,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
self.addSubnode(self.textNode)
self.addSubnode(self.dateNode)
self.addSubnode(self.statusNode)
self.addSubnode(self.pinnedIconNode)
self.addSubnode(self.badgeNode)
self.addSubnode(self.mentionBadgeNode)
self.addSubnode(self.mutedIconNode)
@ -519,6 +519,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
let authorLayout = TextNode.asyncLayout(self.authorNode)
let inputActivitiesLayout = self.inputActivitiesNode.asyncLayout()
let badgeLayout = self.badgeNode.asyncLayout()
let mentionBadgeLayout = self.mentionBadgeNode.asyncLayout()
let onlineLayout = self.onlineNode.asyncLayout()
let selectableControlLayout = ItemListSelectableControlNode.asyncLayout(self.selectableControlNode)
let reorderControlLayout = ItemListEditableReorderControlNode.asyncLayout(self.reorderControlNode)
@ -602,11 +603,14 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
var textAttributedString: NSAttributedString?
var dateAttributedString: NSAttributedString?
var titleAttributedString: NSAttributedString?
var badgeAttributedString: NSAttributedString?
var badgeContent = ChatListBadgeContent.none
var mentionBadgeContent = ChatListBadgeContent.none
var statusState = ChatListStatusNodeState.none
var statusImage: UIImage?
var currentBadgeBackgroundImage: UIImage?
var currentMentionBadgeImage: UIImage?
var currentPinnedIconImage: UIImage?
var currentMutedIconImage: UIImage?
var currentVerificationIconImage: UIImage?
var currentSecretIconImage: UIImage?
@ -716,12 +720,12 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
if !isPeerGroup, let message = message, message.author?.id == account.peerId && !hasDraft {
if message.flags.isSending && !message.isSentOrAcknowledged {
statusImage = PresentationResourcesChatList.pendingImage(item.presentationData.theme)
statusState = .clock(PresentationResourcesChatList.clockFrameImage(item.presentationData.theme), PresentationResourcesChatList.clockMinImage(item.presentationData.theme))
} else {
if let combinedReadState = combinedReadState, combinedReadState.isOutgoingMessageIndexRead(message.index) {
statusImage = PresentationResourcesChatList.doubleCheckImage(item.presentationData.theme)
statusState = .read(item.presentationData.theme.chatList.checkmarkColor)
} else {
statusImage = PresentationResourcesChatList.singleCheckImage(item.presentationData.theme)
statusState = .delivered(item.presentationData.theme.chatList.checkmarkColor)
}
}
}
@ -737,14 +741,12 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
currentBadgeBackgroundImage = PresentationResourcesChatList.badgeBackgroundActive(item.presentationData.theme)
badgeTextColor = theme.unreadBadgeActiveTextColor
}
let unreadCountText: String
if unreadCount.count > 1000 {
unreadCountText = "\(unreadCount.count / 1000)K"
let unreadCountText = compactNumericCountString(Int(unreadCount.count), decimalSeparator: item.presentationData.dateTimeFormat.decimalSeparator)
if unreadCount.count > 0 {
badgeContent = .text(NSAttributedString(string: unreadCountText, font: badgeFont, textColor: badgeTextColor))
} else {
unreadCountText = "\(unreadCount.count)"
badgeContent = .blank
}
badgeAttributedString = NSAttributedString(string: unreadCount.count > 0 ? unreadCountText : " ", font: badgeFont, textColor: badgeTextColor)
}
}
@ -753,8 +755,9 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
let totalMentionCount = tagSummaryCount - actionsSummaryCount
if totalMentionCount > 0 {
currentMentionBadgeImage = PresentationResourcesChatList.badgeBackgroundMention(item.presentationData.theme)
mentionBadgeContent = .mention
} else if item.index.pinningIndex != nil && !isAd && !isPeerGroup && currentBadgeBackgroundImage == nil {
currentMentionBadgeImage = PresentationResourcesChatList.badgeBackgroundPinned(item.presentationData.theme)
currentPinnedIconImage = PresentationResourcesChatList.badgeBackgroundPinned(item.presentationData.theme)
}
if let notificationSettings = notificationSettings as? TelegramPeerNotificationSettings {
@ -809,18 +812,21 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
let (dateLayout, dateApply) = dateLayout(TextNodeLayoutArguments(attributedString: dateAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: rawContentRect.width, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
let (badgeLayout, badgeApply) = badgeLayout(CGSize(width: rawContentRect.width, height: CGFloat.greatestFiniteMagnitude), currentBadgeBackgroundImage, badgeAttributedString)
let (badgeLayout, badgeApply) = badgeLayout(CGSize(width: rawContentRect.width, height: CGFloat.greatestFiniteMagnitude), currentBadgeBackgroundImage, badgeContent)
let (mentionBadgeLayout, mentionBadgeApply) = mentionBadgeLayout(CGSize(width: rawContentRect.width, height: CGFloat.greatestFiniteMagnitude), currentMentionBadgeImage, mentionBadgeContent)
var badgeSize: CGFloat = 0.0
badgeSize += badgeLayout.width
if let currentMentionBadgeImage = currentMentionBadgeImage {
if !badgeLayout.width.isZero {
badgeSize += badgeLayout.width + 5.0
}
if !mentionBadgeLayout.width.isZero {
if !badgeSize.isZero {
badgeSize += currentMentionBadgeImage.size.width + 4.0
badgeSize += badgeLayout.width + 4.0
} else {
badgeSize += currentMentionBadgeImage.size.width + 5.0
badgeSize += badgeLayout.width + 5.0
}
}
badgeSize = max(badgeSize, reorderInset)
let (authorLayout, authorApply) = authorLayout(TextNodeLayoutArguments(attributedString: hideAuthor ? nil : authorAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: rawContentRect.width - badgeSize, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets(top: 2.0, left: 1.0, bottom: 2.0, right: 1.0)))
@ -873,9 +879,9 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
}
let (onlineLayout, onlineApply) = onlineLayout(online)
var animateOnline = false
var animateContent = false
if let currentItem = currentItem, currentItem.content.chatLocation == item.content.chatLocation {
animateOnline = true
animateContent = true
}
let insets = ChatListItemNode.insets(first: first, last: last, firstWithHeader: firstWithHeader)
@ -940,6 +946,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
transition.updateAlpha(node: strongSelf.dateNode, alpha: 0.0)
transition.updateAlpha(node: strongSelf.badgeNode, alpha: 0.0)
transition.updateAlpha(node: strongSelf.mentionBadgeNode, alpha: 0.0)
transition.updateAlpha(node: strongSelf.pinnedIconNode, alpha: 0.0)
transition.updateAlpha(node: strongSelf.statusNode, alpha: 0.0)
} else if let reorderControlNode = strongSelf.reorderControlNode {
let _ = reorderControlSizeAndApply.1(false)
@ -953,6 +960,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
transition.updateAlpha(node: strongSelf.dateNode, alpha: 1.0)
transition.updateAlpha(node: strongSelf.badgeNode, alpha: 1.0)
transition.updateAlpha(node: strongSelf.mentionBadgeNode, alpha: 1.0)
transition.updateAlpha(node: strongSelf.pinnedIconNode, alpha: 1.0)
transition.updateAlpha(node: strongSelf.statusNode, alpha: 1.0)
}
@ -992,64 +1000,59 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
let _ = textApply()
let _ = authorApply()
let _ = titleApply()
let _ = badgeApply()
let _ = onlineApply(animateOnline)
let _ = badgeApply(animateContent)
let _ = mentionBadgeApply(animateContent)
let _ = onlineApply(animateContent)
let contentRect = rawContentRect.offsetBy(dx: editingOffset + leftInset + revealOffset, dy: 0.0)
strongSelf.dateNode.frame = CGRect(origin: CGPoint(x: contentRect.origin.x + contentRect.size.width - dateLayout.size.width, y: contentRect.origin.y + 2.0), size: dateLayout.size)
if let statusImage = statusImage {
strongSelf.statusNode.image = statusImage
strongSelf.statusNode.isHidden = false
let statusSize = statusImage.size
strongSelf.statusNode.frame = CGRect(origin: CGPoint(x: contentRect.origin.x + contentRect.size.width - dateLayout.size.width - 2.0 - statusSize.width, y: contentRect.origin.y + 2.0 + floor((dateLayout.size.height - statusSize.height) / 2.0)), size: statusSize)
} else {
strongSelf.statusNode.image = nil
strongSelf.statusNode.isHidden = true
}
let statusSize = CGSize(width: 24.0, height: 24.0)
strongSelf.statusNode.frame = CGRect(origin: CGPoint(x: contentRect.origin.x + contentRect.size.width - dateLayout.size.width - statusSize.width, y: contentRect.origin.y + 2.0 - UIScreenPixel + floor((dateLayout.size.height - statusSize.height) / 2.0)), size: statusSize)
let _ = strongSelf.statusNode.transitionToState(statusState, animated: animateContent)
let badgeWidth = badgeLayout.width
if let _ = currentBadgeBackgroundImage {
let badgeFrame = CGRect(x: contentRect.maxX - badgeWidth, y: contentRect.maxY - badgeLayout.height - 2.0, width: badgeWidth, height: badgeLayout.height)
let previousBadgeFrame = strongSelf.badgeNode.frame
let badgeFrame = CGRect(x: contentRect.maxX - badgeLayout.width, y: contentRect.maxY - badgeLayout.height - 2.0, width: badgeLayout.width, height: badgeLayout.height)
strongSelf.badgeNode.frame = badgeFrame
}
//: CGFloat
// if let currentBadgeBackgroundImage = currentBadgeBackgroundImage {
// strongSelf.badgeBackgroundNode.image = currentBadgeBackgroundImage
// strongSelf.badgeBackgroundNode.isHidden = false
//
// badgeBackgroundWidth = max(badgeLayout.size.width + 10.0, currentBadgeBackgroundImage.size.width)
// let badgeBackgroundFrame = CGRect(x: contentRect.maxX - badgeBackgroundWidth, y: contentRect.maxY - currentBadgeBackgroundImage.size.height - 2.0, width: badgeBackgroundWidth, height: currentBadgeBackgroundImage.size.height)
// let badgeTextFrame = CGRect(origin: CGPoint(x: badgeBackgroundFrame.midX - badgeLayout.size.width / 2.0, y: badgeBackgroundFrame.minY + 2.0), size: badgeLayout.size)
//
// strongSelf.badgeTextNode.frame = badgeTextFrame
// strongSelf.badgeBackgroundNode.frame = badgeBackgroundFrame
// } else {
// badgeBackgroundWidth = 0.0
// strongSelf.badgeBackgroundNode.image = nil
// strongSelf.badgeBackgroundNode.isHidden = true
// }
if let currentMentionBadgeImage = currentMentionBadgeImage {
strongSelf.mentionBadgeNode.image = currentMentionBadgeImage
strongSelf.mentionBadgeNode.isHidden = false
let mentionBadgeSize = currentMentionBadgeImage.size
if animateContent && !previousBadgeFrame.width.isZero && !badgeFrame.width.isZero && badgeFrame != previousBadgeFrame {
strongSelf.badgeNode.layer.animateFrame(from: previousBadgeFrame, to: badgeFrame, duration: 0.15, timingFunction: kCAMediaTimingFunctionEaseInEaseOut)
}
}
if currentMentionBadgeImage != nil || currentBadgeBackgroundImage != nil {
let previousBadgeFrame = strongSelf.mentionBadgeNode.frame
let mentionBadgeOffset: CGFloat
if badgeWidth.isZero {
mentionBadgeOffset = contentRect.maxX - mentionBadgeSize.width
if badgeLayout.width.isZero {
mentionBadgeOffset = contentRect.maxX - mentionBadgeLayout.width
} else {
mentionBadgeOffset = contentRect.maxX - badgeWidth - 6.0 - mentionBadgeSize.width
mentionBadgeOffset = contentRect.maxX - badgeLayout.width - 6.0 - mentionBadgeLayout.height
}
let badgeBackgroundWidth = mentionBadgeSize.width
let badgeBackgroundFrame = CGRect(x: mentionBadgeOffset, y: contentRect.maxY - mentionBadgeSize.height - 2.0, width: badgeBackgroundWidth, height: mentionBadgeSize.height)
let badgeFrame = CGRect(x: mentionBadgeOffset, y: contentRect.maxY - mentionBadgeLayout.height - 2.0, width: mentionBadgeLayout.height, height: mentionBadgeLayout.height)
strongSelf.mentionBadgeNode.position = badgeFrame.center
strongSelf.mentionBadgeNode.bounds = CGRect(origin: CGPoint(), size: badgeFrame.size)
strongSelf.mentionBadgeNode.frame = badgeBackgroundFrame
if animateContent && !previousBadgeFrame.width.isZero && !badgeFrame.width.isZero && badgeFrame != previousBadgeFrame {
strongSelf.mentionBadgeNode.layer.animatePosition(from: previousBadgeFrame.center, to: badgeFrame.center, duration: 0.15, timingFunction: kCAMediaTimingFunctionEaseInEaseOut)
strongSelf.mentionBadgeNode.layer.animateBounds(from: CGRect(origin: CGPoint(), size: previousBadgeFrame.size), to: CGRect(origin: CGPoint(), size: badgeFrame.size), duration: 0.15, timingFunction: kCAMediaTimingFunctionEaseInEaseOut)
}
}
if let currentPinnedIconImage = currentPinnedIconImage {
strongSelf.pinnedIconNode.image = currentPinnedIconImage
strongSelf.pinnedIconNode.isHidden = false
let pinnedIconSize = currentPinnedIconImage.size
let pinnedIconFrame = CGRect(x: contentRect.maxX - pinnedIconSize.width, y: contentRect.maxY - pinnedIconSize.height - 2.0, width: pinnedIconSize.width, height: pinnedIconSize.height)
strongSelf.pinnedIconNode.frame = pinnedIconFrame
} else {
strongSelf.mentionBadgeNode.image = nil
strongSelf.mentionBadgeNode.isHidden = true
strongSelf.pinnedIconNode.image = nil
strongSelf.pinnedIconNode.isHidden = true
}
var titleOffset: CGFloat = 0.0
@ -1125,7 +1128,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
strongSelf.textNode.alpha = 0.0
strongSelf.authorNode.alpha = 0.0
if animated {
if animated || animateContent {
strongSelf.inputActivitiesNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15)
strongSelf.textNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15)
strongSelf.authorNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15)
@ -1136,7 +1139,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
strongSelf.inputActivitiesNode.alpha = 0.0
strongSelf.textNode.alpha = 1.0
strongSelf.authorNode.alpha = 1.0
if animated {
if animated || animateContent {
strongSelf.inputActivitiesNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, completion: { value in
if let strongSelf = self, value {
strongSelf.inputActivitiesNode.removeFromSupernode()
@ -1291,7 +1294,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
transition.updateFrame(node: self.dateNode, frame: CGRect(origin: CGPoint(x: contentRect.origin.x + contentRect.size.width - dateFrame.size.width, y: dateFrame.minY), size: dateFrame.size))
let statusFrame = self.statusNode.frame
transition.updateFrame(node: self.statusNode, frame: CGRect(origin: CGPoint(x: contentRect.origin.x + contentRect.size.width - dateFrame.size.width - 2.0 - statusFrame.size.width, y: statusFrame.minY), size: statusFrame.size))
transition.updateFrame(node: self.statusNode, frame: CGRect(origin: CGPoint(x: contentRect.origin.x + contentRect.size.width - dateFrame.size.width - statusFrame.size.width, y: statusFrame.minY), size: statusFrame.size))
var nextTitleIconOrigin: CGFloat = contentRect.origin.x + titleFrame.size.width + 3.0 + titleOffset
@ -1308,7 +1311,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
let updatedBadgeFrame = CGRect(origin: CGPoint(x: contentRect.maxX - badgeFrame.size.width, y: contentRect.maxY - badgeFrame.size.height - 2.0), size: badgeFrame.size)
transition.updateFrame(node: self.badgeNode, frame: updatedBadgeFrame)
let mentionBadgeSize = self.mentionBadgeNode.bounds.size
let mentionBadgeSize = self.pinnedIconNode.bounds.size
if mentionBadgeSize != CGSize.zero {
let mentionBadgeOffset: CGFloat
if updatedBadgeFrame.size.width.isZero {
@ -1318,8 +1321,8 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
}
let badgeBackgroundWidth = mentionBadgeSize.width
let badgeBackgroundFrame = CGRect(x: mentionBadgeOffset, y: self.mentionBadgeNode.frame.origin.y, width: badgeBackgroundWidth, height: mentionBadgeSize.height)
transition.updateFrame(node: self.mentionBadgeNode, frame: badgeBackgroundFrame)
let badgeBackgroundFrame = CGRect(x: mentionBadgeOffset, y: self.pinnedIconNode.frame.origin.y, width: badgeBackgroundWidth, height: mentionBadgeSize.height)
transition.updateFrame(node: self.pinnedIconNode, frame: badgeBackgroundFrame)
}
// let badgeTextFrame = self.badgeTextNode.frame

View File

@ -30,16 +30,14 @@ final class ChatListOnlineNode: ASDisplayNode {
strongSelf.iconNode.frame = CGRect(x: 0.0, y: 0.0, width: 14.0, height: 14.0)
if animated {
if online || !strongSelf.iconNode.isHidden {
let initialScale: CGFloat = CGFloat((strongSelf.iconNode.value(forKeyPath: "layer.presentationLayer.transform.scale.x") as? NSNumber)?.floatValue ?? 1.0)
let targetScale: CGFloat = online ? 1.0 : 0.0
strongSelf.iconNode.isHidden = false
strongSelf.iconNode.layer.animateScale(from: initialScale, to: targetScale, duration: 0.2, removeOnCompletion: false, completion: { [weak self] finished in
if let strongSelf = self, finished {
strongSelf.iconNode.isHidden = !online
}
})
}
let initialScale: CGFloat = strongSelf.iconNode.isHidden ? 0.0 : CGFloat((strongSelf.iconNode.value(forKeyPath: "layer.presentationLayer.transform.scale.x") as? NSNumber)?.floatValue ?? 1.0)
let targetScale: CGFloat = online ? 1.0 : 0.0
strongSelf.iconNode.isHidden = false
strongSelf.iconNode.layer.animateScale(from: initialScale, to: targetScale, duration: 0.2, removeOnCompletion: false, completion: { [weak self] finished in
if let strongSelf = self, finished {
strongSelf.iconNode.isHidden = !online
}
})
} else {
strongSelf.iconNode.isHidden = !online
}

View File

@ -1,59 +1,363 @@
import Foundation
import AsyncDisplayKit
import Display
import LegacyComponents
enum ChatListStatusNodeState {
enum ChatListStatusNodeState: Equatable {
case none
case clock
case delivered
case read
case progress(CGFloat)
}
final class ChatListStatusNode: ASDisplayNode {
private let iconNode: ASImageNode
private let checksNode: ASImageNode
private let clockNode: ASImageNode
private let progressNode: RadialStatusNode
case clock(UIImage?, UIImage?)
case delivered(UIColor)
case read(UIColor)
case progress(UIColor, CGFloat)
override init() {
self.iconNode = ASImageNode()
self.iconNode.isLayerBacked = true
self.iconNode.displaysAsynchronously = false
self.iconNode.displayWithoutProcessing = true
self.checksNode = ASImageNode()
self.clockNode = ASImageNode()
self.progressNode = RadialStatusNode(backgroundNodeColor: .clear)
super.init()
self.addSubnode(self.iconNode)
self.addSubnode(self.checksNode)
self.addSubnode(self.clockNode)
self.addSubnode(self.progressNode)
}
func asyncLayout() -> (CGSize) -> (CGSize, (Bool) -> Void) {
return { [weak self] constrainedSize in
return (CGSize(width: 14.0, height: 14.0), { animated in
if let strongSelf = self {
strongSelf.iconNode.frame = CGRect(x: 0.0, y: 0.0, width: 14.0, height: 14.0)
if animated {
let initialScale: CGFloat = CGFloat((strongSelf.iconNode.value(forKeyPath: "layer.presentationLayer.transform.scale.x") as? NSNumber)?.floatValue ?? 1.0)
let targetScale: CGFloat = 1.0
strongSelf.iconNode.isHidden = false
strongSelf.iconNode.layer.animateScale(from: initialScale, to: targetScale, duration: 0.2, removeOnCompletion: false, completion: { [weak self] finished in
if let strongSelf = self, finished {
}
})
} else {
}
}
})
func contentNode() -> ChatListStatusContentNode? {
switch self {
case .none:
return nil
case let .clock(frameImage, minImage):
return ChatListStatusClockNode(frameImage: frameImage, minImage: minImage)
case let .delivered(color):
return ChatListStatusChecksNode(color: color)
case let .read(color):
return ChatListStatusChecksNode(color: color)
case let .progress(color, progress):
return ChatListStatusProgressNode(color: color, progress: progress)
}
}
}
private let transitionDuration = 0.2
class ChatListStatusContentNode: ASDisplayNode {
override init() {
super.init()
self.isOpaque = false
}
func updateWithState(_ state: ChatListStatusNodeState, animated: Bool) {
}
func animateOut(to: ChatListStatusNodeState, completion: @escaping () -> Void) {
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: transitionDuration, removeOnCompletion: false, completion: { _ in
completion()
})
}
func animateIn(from: ChatListStatusNodeState) {
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: transitionDuration)
}
}
final class ChatListStatusNode: ASDisplayNode {
private(set) var state: ChatListStatusNodeState = .none
private var contentNode: ChatListStatusContentNode?
private var nextContentNode: ChatListStatusContentNode?
public func transitionToState(_ state: ChatListStatusNodeState, animated: Bool = false, completion: @escaping () -> Void = {}) -> Bool {
if self.state != state {
let currentState = self.state
self.state = state
let contentNode = state.contentNode()
if contentNode?.classForCoder != self.contentNode?.classForCoder {
contentNode?.updateWithState(state, animated: animated)
self.transitionToContentNode(contentNode, state: state, fromState: currentState, animated: animated, completion: completion)
} else {
self.contentNode?.updateWithState(state, animated: animated)
}
return true
} else {
completion()
return false
}
}
private func transitionToContentNode(_ node: ChatListStatusContentNode?, state: ChatListStatusNodeState, fromState: ChatListStatusNodeState, animated: Bool, completion: @escaping () -> Void) {
if let previousContentNode = self.contentNode {
if !animated {
previousContentNode.removeFromSupernode()
self.contentNode = node
if let contentNode = self.contentNode {
self.addSubnode(contentNode)
}
} else {
self.contentNode = node
if let contentNode = self.contentNode {
self.addSubnode(contentNode)
contentNode.frame = self.bounds
if self.isNodeLoaded {
contentNode.animateIn(from: fromState)
contentNode.layout()
}
}
previousContentNode.animateOut(to: state) {
previousContentNode.removeFromSupernode()
}
}
} else {
self.contentNode = node
if let contentNode = self.contentNode {
contentNode.frame = self.bounds
self.addSubnode(contentNode)
if self.isNodeLoaded {
contentNode.layout()
}
}
}
}
override public func layout() {
if let contentNode = self.contentNode {
contentNode.frame = self.bounds
}
}
}
class ChatListStatusClockNode: ChatListStatusContentNode {
private var clockFrameNode: ASImageNode
private var clockMinNode: ASImageNode
init(frameImage: UIImage?, minImage: UIImage?) {
self.clockFrameNode = ASImageNode()
self.clockMinNode = ASImageNode()
super.init()
self.clockFrameNode.image = frameImage
self.clockMinNode.image = minImage
self.addSubnode(self.clockFrameNode)
self.addSubnode(self.clockMinNode)
}
override func updateWithState(_ state: ChatListStatusNodeState, animated: Bool) {
if case let .clock(frameImage, minImage) = state {
self.clockFrameNode.image = frameImage
self.clockMinNode.image = minImage
}
}
override func didEnterHierarchy() {
super.didEnterHierarchy()
maybeAddRotationAnimation(self.clockFrameNode.layer, duration: 6.0)
maybeAddRotationAnimation(self.clockMinNode.layer, duration: 1.0)
}
override func didExitHierarchy() {
super.didExitHierarchy()
self.clockFrameNode.layer.removeAllAnimations()
self.clockMinNode.layer.removeAllAnimations()
}
override func layout() {
super.layout()
let bounds = self.bounds
if let frameImage = self.clockFrameNode.image {
self.clockFrameNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((bounds.width - frameImage.size.width) / 2.0), y: floorToScreenPixels((bounds.height - frameImage.size.height) / 2.0)), size: frameImage.size)
}
if let minImage = self.clockMinNode.image {
self.clockMinNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((bounds.width - minImage.size.width) / 2.0), y: floorToScreenPixels((bounds.height - minImage.size.height) / 2.0)), size: minImage.size)
}
}
}
private func maybeAddRotationAnimation(_ layer: CALayer, duration: Double) {
if let _ = layer.animation(forKey: "clockFrameAnimation") {
return
}
let basicAnimation = CABasicAnimation(keyPath: "transform.rotation.z")
basicAnimation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
basicAnimation.duration = duration
basicAnimation.fromValue = NSNumber(value: Float(0.0))
basicAnimation.toValue = NSNumber(value: Float(Double.pi * 2.0))
basicAnimation.repeatCount = Float.infinity
basicAnimation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionLinear)
basicAnimation.beginTime = 1.0
layer.add(basicAnimation, forKey: "clockFrameAnimation")
}
private final class StatusChecksNodeParameters: NSObject {
let color: UIColor
let progress: CGFloat
init(color: UIColor, progress: CGFloat) {
self.color = color
self.progress = progress
super.init()
}
}
private class ChatListStatusChecksNode: ChatListStatusContentNode {
private var state: ChatListStatusNodeState?
var color: UIColor {
didSet {
self.setNeedsDisplay()
}
}
private var effectiveProgress: CGFloat = 1.0 {
didSet {
self.setNeedsDisplay()
}
}
init(color: UIColor) {
self.color = color
super.init()
}
func animateProgress(from: CGFloat, to: CGFloat) {
self.pop_removeAllAnimations()
let animation = POPBasicAnimation()
animation.property = POPAnimatableProperty.property(withName: "progress", initializer: { property in
property?.readBlock = { node, values in
values?.pointee = (node as! ChatListStatusChecksNode).effectiveProgress
}
property?.writeBlock = { node, values in
(node as! ChatListStatusChecksNode).effectiveProgress = values!.pointee
}
property?.threshold = 0.01
}) as! POPAnimatableProperty
animation.fromValue = from as NSNumber
animation.toValue = to as NSNumber
animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionLinear)
animation.duration = 0.2
self.pop_add(animation, forKey: "progress")
}
override func drawParameters(forAsyncLayer layer: _ASDisplayLayer) -> NSObjectProtocol? {
return StatusChecksNodeParameters(color: self.color, progress: self.effectiveProgress)
}
@objc override class func draw(_ bounds: CGRect, withParameters parameters: Any?, isCancelled: () -> Bool, isRasterizing: Bool) {
let context = UIGraphicsGetCurrentContext()!
if !isRasterizing {
context.setBlendMode(.copy)
context.setFillColor(UIColor.clear.cgColor)
context.fill(bounds)
}
guard let parameters = parameters as? StatusChecksNodeParameters else {
return
}
let progress = parameters.progress
context.setStrokeColor(parameters.color.cgColor)
context.setLineWidth(1.0 + UIScreenPixel)
context.setLineCap(.round)
context.setLineJoin(.round)
context.setMiterLimit(10.0)
context.saveGState()
var s1 = CGPoint(x: 9.0, y: 13.0)
var s2 = CGPoint(x: 5.0, y: 13.0)
let p1 = CGPoint(x: 3.5, y: 3.5)
let p2 = CGPoint(x: 7.5 - UIScreenPixel, y: -8.0)
let check1FirstSegment: CGFloat = max(0.0, min(1.0, progress * 3.0))
let check2FirstSegment: CGFloat = max(0.0, min(1.0, (progress - 1.0) * 3.0))
let firstProgress = max(0.0, min(1.0, progress))
let secondProgress = max(0.0, min(1.0, progress - 1.0))
let scale: CGFloat = 1.2
context.translateBy(x: 16.0, y: 13.0)
context.scaleBy(x: scale - abs((scale - 1.0) * (firstProgress - 0.5) / 0.5), y: scale - abs((scale - 1.0) * (firstProgress - 0.5) / 0.5))
s1 = s1.offsetBy(dx: -16.0, dy: -13.0)
if !check1FirstSegment.isZero {
if check1FirstSegment < 1.0 {
context.move(to: CGPoint(x: s1.x + p1.x * check1FirstSegment, y: s1.y + p1.y * check1FirstSegment))
context.addLine(to: s1)
} else {
let secondSegment = (min(1.0, progress) - 0.33) * 1.5
context.move(to: CGPoint(x: s1.x + p1.x + p2.x * secondSegment, y: s1.y + p1.y + p2.y * secondSegment))
context.addLine(to: CGPoint(x: s1.x + p1.x, y: s1.y + p1.y))
context.addLine(to: CGPoint(x: s1.x + p1.x * min(1.0, check2FirstSegment), y: s1.y + p1.y * min(1.0, check2FirstSegment)))
}
}
context.strokePath()
context.restoreGState()
context.translateBy(x: 12.0, y: 13.0)
context.scaleBy(x: scale - abs((scale - 1.0) * (secondProgress - 0.5) / 0.5), y: scale - abs((scale - 1.0) * (secondProgress - 0.5) / 0.5))
s2 = s2.offsetBy(dx: -12.0, dy: -13.0)
if !check2FirstSegment.isZero {
if check2FirstSegment < 1.0 {
context.move(to: CGPoint(x: s2.x + p1.x * check2FirstSegment, y: s2.y + p1.y * check2FirstSegment))
context.addLine(to: s2)
} else {
let secondSegment = (max(0.0, (progress - 1.0)) - 0.33) * 1.5
context.move(to: CGPoint(x: s2.x + p1.x + p2.x * secondSegment, y: s2.y + p1.y + p2.y * secondSegment))
context.addLine(to: CGPoint(x: s2.x + p1.x, y: s2.y + p1.y))
context.addLine(to: s2)
}
}
context.strokePath()
}
override func updateWithState(_ state: ChatListStatusNodeState, animated: Bool) {
if let previousState = self.state, case .delivered = previousState, case .read = state, animated {
self.animateProgress(from: 1.0, to: 2.0)
}
if !animated {
if case .delivered = state {
self.effectiveProgress = 1.0
} else if case .read = state {
self.effectiveProgress = 2.0
}
}
self.state = state
}
override func animateIn(from: ChatListStatusNodeState) {
if let state = self.state, case .delivered = state {
self.animateProgress(from: 0.0, to: 1.0)
} else {
super.animateIn(from: from)
}
}
}
private class ChatListStatusProgressNode: ChatListStatusContentNode {
private let statusNode: RadialStatusNode
init(color: UIColor, progress: CGFloat) {
self.statusNode = RadialStatusNode(backgroundNodeColor: .clear)
super.init()
self.statusNode.transitionToState(.progress(color: color, lineWidth: 1.0, value: progress, cancelEnabled: false))
self.addSubnode(self.statusNode)
}
override func updateWithState(_ state: ChatListStatusNodeState, animated: Bool) {
if case let .progress(color, progress) = state {
self.statusNode.transitionToState(.progress(color: color, lineWidth: 1.0, value: progress, cancelEnabled: false), animated: animated, completion: {})
}
}
override func layout() {
super.layout()
let bounds = self.bounds
let size = CGSize(width: 12.0, height: 12.0)
self.statusNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((bounds.width - size.width) / 2.0), y: floorToScreenPixels((bounds.height - size.height) / 2.0)), size: size)
}
}

View File

@ -31,6 +31,8 @@ final class ChatListInputActivitiesNode: ASDisplayNode {
}
}
let lightColor = color.withAlphaComponent(0.85)
if activities.count == 1 {
if activities[0].0.id == peerId {
let text: String
@ -56,15 +58,15 @@ final class ChatListInputActivitiesNode: ASDisplayNode {
switch activities[0].1 {
case .typingText:
state = .typingText(string, color)
state = .typingText(string, lightColor)
case .recordingVoice:
state = .recordingVoice(string, color)
state = .recordingVoice(string, lightColor)
case .recordingInstantVideo:
state = .recordingVideo(string, color)
state = .recordingVideo(string, lightColor)
case .uploadingFile, .uploadingInstantVideo, .uploadingPhoto, .uploadingVideo:
state = .uploading(string, color)
state = .uploading(string, lightColor)
case .playingGame:
state = .playingGame(string, color)
state = .playingGame(string, lightColor)
}
} else {
let text: String
@ -95,15 +97,15 @@ final class ChatListInputActivitiesNode: ASDisplayNode {
switch activities[0].1 {
case .typingText:
state = .typingText(string, color)
state = .typingText(string, lightColor)
case .recordingVoice:
state = .recordingVoice(string, color)
state = .recordingVoice(string, lightColor)
case .recordingInstantVideo:
state = .recordingVideo(string, color)
state = .recordingVideo(string, lightColor)
case .uploadingFile, .uploadingInstantVideo, .uploadingPhoto, .uploadingVideo:
state = .uploading(string, color)
state = .uploading(string, lightColor)
case .playingGame:
state = .playingGame(string, color)
state = .playingGame(string, lightColor)
}
}
} else {
@ -114,7 +116,7 @@ final class ChatListInputActivitiesNode: ASDisplayNode {
} else {
string = NSAttributedString(string: strings.DialogList_MultipleTypingSuffix(activities.count).0, font: textFont, textColor: color)
}
state = .typingText(string, color)
state = .typingText(string, lightColor)
}
}

View File

@ -352,7 +352,7 @@ public final class ChatMessageItem: ListViewItem, CustomStringConvertible {
}
}
if viewClassName == ChatMessageBubbleItemNode.self, !self.message.text.isEmpty && self.message.text.containsOnlyEmoji && self.presentationData.largeEmoji, self.message.text.emojis.count < 4 {
if viewClassName == ChatMessageBubbleItemNode.self && self.message.media.isEmpty && !self.message.text.isEmpty && self.message.text.containsOnlyEmoji && self.presentationData.largeEmoji, self.message.text.emojis.count < 4 {
viewClassName = ChatMessageStickerItemNode.self
}

View File

@ -21,11 +21,15 @@ class ChatMessageStickerItemNode: ChatMessageItemView {
private var deliveryFailedNode: ChatMessageDeliveryFailedNode?
private var shareButtonNode: HighlightableButtonNode?
var telegramFile: TelegramMediaFile?
private let fetchDisposable = MetaDisposable()
private var appliedForwardInfo: (Peer?, String?)?
private var forwardInfoNode: ChatMessageForwardInfoNode?
private var forwardBackgroundNode: ASImageNode?
private var viaBotNode: TextNode?
private let dateAndStatusNode: ChatMessageDateAndStatusNode
private var replyInfoNode: ChatMessageReplyInfoNode?
@ -98,6 +102,23 @@ class ChatMessageStickerItemNode: ChatMessageItemView {
break
}
}
if self.telegramFile == nil && !item.message.text.isEmpty && item.message.text.containsOnlyEmoji && item.presentationData.largeEmoji {
var textFont = item.presentationData.messageFont
let emojis = item.message.text.emojiString
switch emojis.count {
case 1:
textFont = item.presentationData.messageEmojiFont1
case 2:
textFont = item.presentationData.messageEmojiFont2
case 3:
textFont = item.presentationData.messageEmojiFont3
default:
break
}
self.imageNode.setSignal(largeEmoji(postbox: item.context.account.postbox, emoji: item.message.text, fontSize: textFont.pointSize))
}
}
override func asyncLayout() -> (_ item: ChatMessageItem, _ params: ListViewItemLayoutParams, _ mergedTop: ChatMessageMerge, _ mergedBottom: ChatMessageMerge, _ dateHeaderAtBottom: Bool) -> (ListViewItemNodeLayout, (ListViewItemUpdateAnimation, Bool) -> Void) {
@ -127,6 +148,7 @@ class ChatMessageStickerItemNode: ChatMessageItemView {
}
var textLayoutAndApply: (TextNodeLayout, () -> TextNode)?
var isEmoji = false
if !item.message.text.isEmpty && item.message.text.containsOnlyEmoji && item.presentationData.largeEmoji {
var textFont = item.presentationData.messageFont
let emojis = item.message.text.emojis
@ -142,9 +164,10 @@ class ChatMessageStickerItemNode: ChatMessageItemView {
}
let attributedText = NSAttributedString(string: item.message.text, font: textFont, textColor: .black)
textLayoutAndApply = textLayout(TextNodeLayoutArguments(attributedString: attributedText, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: 120.0, height: 60.0), alignment: .natural))
textLayoutAndApply = textLayout(TextNodeLayoutArguments(attributedString: attributedText, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: 180.0, height: 90.0), alignment: .natural))
imageSize = CGSize(width: textLayoutAndApply!.0.size.width, height: 100.0)
imageSize = CGSize(width: textLayoutAndApply!.0.size.width, height: textLayoutAndApply!.0.size.height)
isEmoji = true
}
let avatarInset: CGFloat
@ -293,7 +316,6 @@ class ChatMessageStickerItemNode: ChatMessageItemView {
}
}
if let replyAttribute = attribute as? ReplyMessageAttribute, let replyMessage = item.message.associatedMessages[replyAttribute.messageId] {
replyInfoApply = makeReplyInfoLayout(item.presentationData, item.presentationData.strings, item.context, .standalone, replyMessage, CGSize(width: availableWidth, height: CGFloat.greatestFiniteMagnitude))
} else if let attribute = attribute as? ReplyMarkupMessageAttribute, attribute.flags.contains(.inline), !attribute.rows.isEmpty {
replyMarkup = attribute
@ -354,6 +376,9 @@ class ChatMessageStickerItemNode: ChatMessageItemView {
}
var layoutSize = CGSize(width: params.width, height: contentHeight)
if isEmoji && !incoming {
layoutSize.height += dateAndStatusSize.height
}
if let actionButtonsSizeAndApply = actionButtonsSizeAndApply {
layoutSize.height += actionButtonsSizeAndApply.0.height
}
@ -366,20 +391,9 @@ class ChatMessageStickerItemNode: ChatMessageItemView {
}
let updatedImageFrame = imageFrame.offsetBy(dx: 0.0, dy: floor((contentHeight - imageSize.height) / 2.0))
transition.updateFrame(node: strongSelf.imageNode, frame: updatedImageFrame)
imageApply()
if let (textLayout, textApply) = textLayoutAndApply {
let textNode = textApply()
if textNode !== strongSelf.textNode {
strongSelf.textNode?.removeFromSupernode()
strongSelf.addSubnode(textNode)
strongSelf.textNode = textNode
}
transition.updateFrame(node: textNode, frame: CGRect(x: updatedImageFrame.maxX - textLayout.size.width - 10.0, y: updatedImageFrame.maxY - textLayout.size.height - 30.0, width: textLayout.size.width, height: textLayout.size.height))
}
if let updatedShareButtonNode = updatedShareButtonNode {
if updatedShareButtonNode !== strongSelf.shareButtonNode {
if let shareButtonNode = strongSelf.shareButtonNode {
@ -402,7 +416,15 @@ class ChatMessageStickerItemNode: ChatMessageItemView {
}
dateAndStatusApply(false)
transition.updateFrame(node: strongSelf.dateAndStatusNode, frame: CGRect(origin: CGPoint(x: max(displayLeftInset, updatedImageFrame.maxX - dateAndStatusSize.width - 4.0), y: updatedImageFrame.maxY - dateAndStatusSize.height - 16.0), size: dateAndStatusSize))
var dateOffset: CGPoint = CGPoint(x: dateAndStatusSize.width + 4.0, y: dateAndStatusSize.height + 16.0)
if isEmoji {
if incoming {
dateOffset.x = 12.0
} else {
dateOffset.y = 12.0
}
}
transition.updateFrame(node: strongSelf.dateAndStatusNode, frame: CGRect(origin: CGPoint(x: max(displayLeftInset, updatedImageFrame.maxX - dateOffset.x), y: updatedImageFrame.maxY - dateOffset.y), size: dateAndStatusSize))
if let updatedReplyBackgroundNode = updatedReplyBackgroundNode {
if strongSelf.replyBackgroundNode == nil {

View File

@ -215,24 +215,8 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
let bubbleTheme = item.presentationData.theme.theme.chat.bubble
var textFont = item.presentationData.messageFont
var forceStatusNewline = false
if rawText.containsOnlyEmoji && item.presentationData.largeEmoji {
let emojis = rawText.emojis
switch emojis.count {
case 1:
textFont = item.presentationData.messageEmojiFont1
forceStatusNewline = true
case 2:
textFont = item.presentationData.messageEmojiFont2
forceStatusNewline = true
case 3:
textFont = item.presentationData.messageEmojiFont3
forceStatusNewline = true
default:
break
}
}
let textFont = item.presentationData.messageFont
let forceStatusNewline = false
if let entities = entities {
attributedText = stringWithAppliedEntities(rawText, entities: entities, baseColor: incoming ? bubbleTheme.incomingPrimaryTextColor : bubbleTheme.outgoingPrimaryTextColor, linkColor: incoming ? bubbleTheme.incomingLinkTextColor : bubbleTheme.outgoingLinkTextColor, baseFont: textFont, linkFont: textFont, boldFont: item.presentationData.messageBoldFont, italicFont: item.presentationData.messageItalicFont, fixedFont: item.presentationData.messageFixedFont)

View File

@ -43,8 +43,8 @@ private class ChatPlayingActivityIndicatorNode: ChatTitleActivityIndicatorNode {
return
}
let dotsColor = parameters.color.withAlphaComponent(0.5)
context.setFillColor(dotsColor.cgColor)
let color = parameters.color.withAlphaComponent(parameters.color.alpha * 0.5)
context.setFillColor(color.cgColor)
let distance: CGFloat = 4.0
var origin = CGPoint(x: (bounds.size.width - distance * 2.0) / 2.0 + 4.0, y: bounds.size.height / 2.0 + 1.0)

View File

@ -224,7 +224,7 @@ private enum ChatRecentActionsFilterEntry: ItemListNodeEntry {
case .member:
peerText = strings.ChatAdmins_AdminLabel.capitalized
}
return ItemListPeerItem(theme: theme, strings: strings, dateTimeFormat: dateTimeFormat, nameDisplayOrder: nameDisplayOrder, account: arguments.account, peer: participant.peer, presence: nil, text: .text(peerText), label: .none, editing: ItemListPeerItemEditing(editable: false, editing: false, revealed: false), switchValue: ItemListPeerItemSwitch(value: checked, style: .check), enabled: true, sectionId: self.section, action: {
return ItemListPeerItem(theme: theme, strings: strings, dateTimeFormat: dateTimeFormat, nameDisplayOrder: nameDisplayOrder, account: arguments.account, peer: participant.peer, presence: nil, text: .text(peerText), label: .none, editing: ItemListPeerItemEditing(editable: false, editing: false, revealed: false), switchValue: ItemListPeerItemSwitch(value: checked, style: .check), enabled: true, selectable: true, sectionId: self.section, action: {
arguments.toggleAdmin(participant.peer.id)
}, setPeerIdWithRevealedOptions: { _, _ in
}, removePeer: { _ in })

View File

@ -86,6 +86,9 @@ class ChatTitleActivityNode: ASDisplayNode {
if case let .info(_, fromType) = fromState, case let .info(_, toType) = state, fromType == toType {
animation = .none
}
if case .typingText = fromState, case .typingText = state {
animation = .none
}
self.contentNode = node
if let contentNode = self.contentNode {

View File

@ -70,7 +70,8 @@ private class ChatTypingActivityIndicatorNode: ChatTitleActivityIndicatorNode {
radius = (max(minDiameter, radius) - minDiameter) / (maxDiameter - minDiameter)
radius = radius * 1.5
var dotsColor = parameters.color.withAlphaComponent(radius * deltaAlpha + minAlpha)
let initialAlpha = parameters.color.alpha
var dotsColor = parameters.color.withAlphaComponent((radius * deltaAlpha + minAlpha) * initialAlpha)
context.setFillColor(dotsColor.cgColor)
context.fillEllipse(in: CGRect(x: leftPadding - minDiameter / 2.0 - radius / 2.0, y: topPadding - minDiameter / 2.0 - radius / 2.0, width: minDiameter + radius, height: minDiameter + radius))
@ -79,7 +80,7 @@ private class ChatTypingActivityIndicatorNode: ChatTitleActivityIndicatorNode {
radius = (max(minDiameter, radius) - minDiameter) / (maxDiameter - minDiameter)
radius = radius * 1.5
dotsColor = parameters.color.withAlphaComponent(radius * deltaAlpha + minAlpha)
dotsColor = parameters.color.withAlphaComponent((radius * deltaAlpha + minAlpha) * initialAlpha)
context.setFillColor(dotsColor.cgColor)
context.fillEllipse(in: CGRect(x: leftPadding + distance - minDiameter / 2.0 - radius / 2.0, y: topPadding - minDiameter / 2.0 - radius / 2.0, width: minDiameter + radius, height: minDiameter + radius))
@ -88,7 +89,7 @@ private class ChatTypingActivityIndicatorNode: ChatTitleActivityIndicatorNode {
radius = (max(minDiameter, radius) - minDiameter) / (maxDiameter - minDiameter)
radius = radius * 1.5
dotsColor = parameters.color.withAlphaComponent(radius * deltaAlpha + minAlpha)
dotsColor = parameters.color.withAlphaComponent((radius * deltaAlpha + minAlpha) * initialAlpha)
context.setFillColor(dotsColor.cgColor)
context.fillEllipse(in: CGRect(x: leftPadding + distance * 2.0 - minDiameter / 2.0 - radius / 2.0, y: topPadding - minDiameter / 2.0 - radius / 2.0, width: minDiameter + radius, height: minDiameter + radius))

View File

@ -51,8 +51,8 @@ private class ChatUploadingActivityIndicatorNode: ChatTitleActivityIndicatorNode
let size = CGSize(width: 13.0, height: 4.0)
let radius: CGFloat = 1.25
var dotsColor = parameters.color.withAlphaComponent(0.3)
context.setFillColor(dotsColor.cgColor)
var color = parameters.color.withAlphaComponent(parameters.color.alpha * 0.3)
context.setFillColor(color.cgColor)
var path = UIBezierPath(roundedRect: CGRect(origin: origin, size: size), cornerRadius: radius)
path.fill(with: .normal, alpha: 1.0)
@ -60,8 +60,8 @@ private class ChatUploadingActivityIndicatorNode: ChatTitleActivityIndicatorNode
let progress = interpolate(from: 0.0, to: size.width * 2.0, value: parameters.progress)
dotsColor = parameters.color
context.setFillColor(dotsColor.cgColor)
color = parameters.color
context.setFillColor(color.cgColor)
path = UIBezierPath(roundedRect: CGRect(origin: origin.offsetBy(dx: -size.width + progress, dy: 0.0), size: size), cornerRadius: radius)
path.fill(with: .normal, alpha: 1.0)

View File

@ -150,7 +150,7 @@ private enum CreateGroupEntry: ItemListNodeEntry {
arguments.changeProfilePhoto()
})
case let .member(_, theme, strings, dateTimeFormat, nameDisplayOrder, peer, presence):
return ItemListPeerItem(theme: theme, strings: strings, dateTimeFormat: dateTimeFormat, nameDisplayOrder: nameDisplayOrder, account: arguments.account, peer: peer, presence: presence, text: .presence, label: .none, editing: ItemListPeerItemEditing(editable: false, editing: false, revealed: false), switchValue: nil, enabled: true, sectionId: self.section, action: nil, setPeerIdWithRevealedOptions: { _, _ in }, removePeer: { _ in })
return ItemListPeerItem(theme: theme, strings: strings, dateTimeFormat: dateTimeFormat, nameDisplayOrder: nameDisplayOrder, account: arguments.account, peer: peer, presence: presence, text: .presence, label: .none, editing: ItemListPeerItemEditing(editable: false, editing: false, revealed: false), switchValue: nil, enabled: true, selectable: true, sectionId: self.section, action: nil, setPeerIdWithRevealedOptions: { _, _ in }, removePeer: { _ in })
}
}
}

View File

@ -121,7 +121,7 @@ private let chatList = PresentationThemeChatList(
itemHighlightedBackgroundColor: UIColor(rgb: 0x191919),
itemSelectedBackgroundColor: UIColor(rgb: 0x191919),
titleColor: UIColor(rgb: 0xffffff),
secretTitleColor: UIColor(rgb: 0xb2b2b2), //!!!
secretTitleColor: secretColor, //!!!
dateTextColor: UIColor(rgb: 0x8e8e93),
authorNameColor: UIColor(rgb: 0xffffff),
messageTextColor: UIColor(rgb: 0x8e8e93),

View File

@ -328,6 +328,8 @@ func editSettingsController(context: AccountContext, currentName: ItemListAvatar
var changeProfilePhotoImpl: (() -> Void)?
var getNavigationController: (() -> NavigationController?)?
let hapticFeedback = HapticFeedback()
let arguments = EditSettingsItemArguments(context: context, accountManager: accountManager, avatarAndNameInfoContext: avatarAndNameInfoContext, avatarTapAction: {
var updating = false
@ -356,6 +358,7 @@ func editSettingsController(context: AccountContext, currentName: ItemListAvatar
}, saveEditingState: {
var updateName: ItemListAvatarAndNameInfoItemName?
var updateBio: String?
var failed = false
updateState { state in
if state.editingName != currentName {
updateName = state.editingName
@ -363,12 +366,24 @@ func editSettingsController(context: AccountContext, currentName: ItemListAvatar
if state.editingBioText != currentBioText {
updateBio = state.editingBioText
}
if (updateBio?.count ?? 0) > 70 {
failed = true
return state
}
if updateName != nil || updateBio != nil {
return state.withUpdatedUpdatingName(state.editingName).withUpdatedUpdatingBioText(true)
} else {
return state
}
}
guard !failed else {
hapticFeedback.error()
return
}
var updateNameSignal: Signal<Void, NoError> = .complete()
if let updateName = updateName, case let .personName(firstName, lastName) = updateName {
updateNameSignal = updateAccountPeerName(account: context.account, firstName: firstName, lastName: lastName)

View File

@ -170,7 +170,7 @@ private enum FeedGroupingEntry: ItemListNodeEntry {
case let .groupHeader(theme, text):
return ItemListSectionHeaderItem(theme: theme, text: text, sectionId: self.section)
case let .peer(theme, strings, dateTimeFormat, nameDisplayOrder, _, peer, value):
return ItemListPeerItem(theme: theme, strings: strings, dateTimeFormat: dateTimeFormat, nameDisplayOrder: nameDisplayOrder, account: arguments.account, peer: peer, presence: nil, text: .none, label: .none, editing: ItemListPeerItemEditing(editable: false, editing: false, revealed: false), switchValue: ItemListPeerItemSwitch(value: value, style: .standard), enabled: true, sectionId: self.section, action: nil, setPeerIdWithRevealedOptions: { _, _ in }, removePeer: { _ in }, toggleUpdated: { value in
return ItemListPeerItem(theme: theme, strings: strings, dateTimeFormat: dateTimeFormat, nameDisplayOrder: nameDisplayOrder, account: arguments.account, peer: peer, presence: nil, text: .none, label: .none, editing: ItemListPeerItemEditing(editable: false, editing: false, revealed: false), switchValue: ItemListPeerItemSwitch(value: value, style: .standard), enabled: true, selectable: true, sectionId: self.section, action: nil, setPeerIdWithRevealedOptions: { _, _ in }, removePeer: { _ in }, toggleUpdated: { value in
arguments.togglePeer(peer, value)
})
case let .ungroup(theme, text):

View File

@ -101,6 +101,8 @@ public func fetchCachedResourceRepresentation(account: Account, resource: MediaR
return .complete()
}
}
} else if let representation = representation as? CachedLargeEmojiRepresentation {
return fetchLargeEmojiRepresentation(account: account, resource: resource, representation: representation)
}
return .never()
}
@ -671,3 +673,63 @@ private func fetchCachedAlbumArtworkRepresentation(account: Account, resource: M
return EmptyDisposable
}) |> runOn(Queue.concurrentDefaultQueue())
}
private func fetchLargeEmojiRepresentation(account: Account, resource: MediaResource, representation: CachedLargeEmojiRepresentation) -> Signal<CachedMediaResourceRepresentationResult, NoError> {
guard let resource = resource as? LargeEmojiResource else {
return .never()
}
return Signal({ subscriber in
var randomId: Int64 = 0
arc4random_buf(&randomId, 8)
let path = NSTemporaryDirectory() + "\(randomId)"
let url = URL(fileURLWithPath: path)
let nsString = (resource.emoji as NSString)
let font = Font.regular(resource.fontSize)
let stringAttributes = [NSAttributedStringKey.font: font]
var emojiSize = nsString.size(withAttributes: stringAttributes)
emojiSize = CGSize(width: ceil(emojiSize.width) + 2.0, height: ceil(emojiSize.height) + 2.0)
let image = generateImage(emojiSize, contextGenerator: { size, context in
let bounds = CGRect(origin: CGPoint(), size: size)
context.clear(CGRect(origin: CGPoint(), size: size))
context.textMatrix = .identity
let path = CGMutablePath()
path.addRect(bounds)
let string = NSAttributedString(string: resource.emoji, font: font, textColor: .black)
let framesetter = CTFramesetterCreateWithAttributedString(string as CFAttributedString)
let frame = CTFramesetterCreateFrame(framesetter, CFRangeMake(0, string.length), path, nil)
CTFrameDraw(frame, context)
})!
let borderImage = generateTintedImage(image: image, color: .white)!
let lineWidth: CGFloat = 1.0
let colorImage = generateImage(CGSize(width: emojiSize.width + lineWidth * 2.0, height: emojiSize.height + lineWidth * 2.0), contextGenerator: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
let vectors: [CGPoint] = [CGPoint(x: -1.0, y: -1.0), CGPoint(x: -1.0, y: 0.0), CGPoint(x: -1.0, y: 1.0), CGPoint(x: 0.0, y: 1.0), CGPoint(x: 1.0, y: 1.0), CGPoint(x: 1.0, y: 0.0), CGPoint(x: 1.0, y: -1.0), CGPoint(x: 0.0, y: -1.0)]
if let image = image.cgImage, let borderImage = borderImage.cgImage {
let rect = CGRect(x: lineWidth, y: lineWidth, width: emojiSize.width, height: emojiSize.height)
let step = UIScreenPixel
for vector in vectors {
for i in stride(from: step, through: lineWidth, by: step) {
drawImage(context: context, image: borderImage, orientation: .up, in: rect.offsetBy(dx: vector.x * i, dy: vector.y * i))
}
}
drawImage(context: context, image: image, orientation: .up, in: rect)
}
})!
if let colorDestination = CGImageDestinationCreateWithURL(url as CFURL, kUTTypePNG, 1, nil) {
let options = NSMutableDictionary()
CGImageDestinationAddImage(colorDestination, colorImage.cgImage!, options as CFDictionary)
if CGImageDestinationFinalize(colorDestination) {
subscriber.putNext(CachedMediaResourceRepresentationResult(temporaryPath: path))
}
}
subscriber.putCompletion()
return EmptyDisposable
}) |> runOn(Queue.concurrentDefaultQueue())
}

View File

@ -142,7 +142,7 @@ private enum GroupInfoEntry: ItemListNodeEntry {
case administrators(PresentationTheme, String, String)
case permissions(PresentationTheme, String, String)
case addMember(PresentationTheme, String, editing: Bool)
case member(PresentationTheme, PresentationStrings, PresentationDateTimeFormat, PresentationPersonNameOrder, index: Int, peerId: PeerId, peer: Peer, participant: RenderedChannelParticipant?, presence: PeerPresence?, memberStatus: GroupInfoMemberStatus, editing: ItemListPeerItemEditing, revealActions: [ParticipantRevealAction], enabled: Bool)
case member(PresentationTheme, PresentationStrings, PresentationDateTimeFormat, PresentationPersonNameOrder, index: Int, peerId: PeerId, peer: Peer, participant: RenderedChannelParticipant?, presence: PeerPresence?, memberStatus: GroupInfoMemberStatus, editing: ItemListPeerItemEditing, revealActions: [ParticipantRevealAction], enabled: Bool, selectable: Bool)
case leave(PresentationTheme, String)
var section: ItemListSectionId {
@ -303,8 +303,8 @@ private enum GroupInfoEntry: ItemListNodeEntry {
} else {
return false
}
case let .member(lhsTheme, lhsStrings, lhsDateTimeFormat, lhsNameDisplayOrder, lhsIndex, lhsPeerId, lhsPeer, lhsParticipant, lhsPresence, lhsMemberStatus, lhsEditing, lhsActions, lhsEnabled):
if case let .member(rhsTheme, rhsStrings, rhsDateTimeFormat, rhsNameDisplayOrder, rhsIndex, rhsPeerId, rhsPeer, rhsParticipant, rhsPresence, rhsMemberStatus, rhsEditing, rhsActions, rhsEnabled) = rhs {
case let .member(lhsTheme, lhsStrings, lhsDateTimeFormat, lhsNameDisplayOrder, lhsIndex, lhsPeerId, lhsPeer, lhsParticipant, lhsPresence, lhsMemberStatus, lhsEditing, lhsActions, lhsEnabled, lhsSelectable):
if case let .member(rhsTheme, rhsStrings, rhsDateTimeFormat, rhsNameDisplayOrder, rhsIndex, rhsPeerId, rhsPeer, rhsParticipant, rhsPresence, rhsMemberStatus, rhsEditing, rhsActions, rhsEnabled, rhsSelectable) = rhs {
if lhsTheme !== rhsTheme {
return false
}
@ -348,6 +348,9 @@ private enum GroupInfoEntry: ItemListNodeEntry {
if lhsEnabled != rhsEnabled {
return false
}
if lhsSelectable != rhsSelectable {
return false
}
return true
} else {
return false
@ -357,7 +360,7 @@ private enum GroupInfoEntry: ItemListNodeEntry {
var stableId: GroupEntryStableId {
switch self {
case let .member(_, _, _, _, _, peerId, _, _, _, _, _, _, _):
case let .member(_, _, _, _, _, peerId, _, _, _, _, _, _, _, _):
return .peer(peerId)
default:
return .index(self.sortIndex)
@ -394,7 +397,7 @@ private enum GroupInfoEntry: ItemListNodeEntry {
return 15
case .addMember:
return 17
case let .member(_, _, _, _, index, _, _, _, _, _, _, _, _):
case let .member(_, _, _, _, index, _, _, _, _, _, _, _, _, _):
return 20 + index
case .leave:
return 100000 + 1
@ -469,7 +472,7 @@ private enum GroupInfoEntry: ItemListNodeEntry {
return ItemListDisclosureItem(theme: theme, icon: PresentationResourcesChat.groupInfoAdminsIcon(theme), title: title, label: text, sectionId: self.section, style: .blocks, action: {
arguments.openAdministrators()
})
case let .member(theme, strings, dateTimeFormat, nameDisplayOrder, _, _, peer, participant, presence, memberStatus, editing, actions, enabled):
case let .member(theme, strings, dateTimeFormat, nameDisplayOrder, _, _, peer, participant, presence, memberStatus, editing, actions, enabled, selectable):
let label: String?
switch memberStatus {
case .admin:
@ -494,8 +497,8 @@ private enum GroupInfoEntry: ItemListNodeEntry {
}
}))
}
return ItemListPeerItem(theme: theme, strings: strings, dateTimeFormat: dateTimeFormat, nameDisplayOrder: nameDisplayOrder, account: arguments.context.account, peer: peer, presence: presence, text: .presence, label: label == nil ? .none : .text(label!), editing: editing, revealOptions: ItemListPeerItemRevealOptions(options: options), switchValue: nil, enabled: enabled, sectionId: self.section, action: {
if let infoController = peerInfoController(context: arguments.context, peer: peer) {
return ItemListPeerItem(theme: theme, strings: strings, dateTimeFormat: dateTimeFormat, nameDisplayOrder: nameDisplayOrder, account: arguments.context.account, peer: peer, presence: presence, text: .presence, label: label == nil ? .none : .text(label!), editing: editing, revealOptions: ItemListPeerItemRevealOptions(options: options), switchValue: nil, enabled: enabled, selectable: selectable, sectionId: self.section, action: {
if let infoController = peerInfoController(context: arguments.context, peer: peer), selectable {
arguments.pushController(infoController)
}
}, setPeerIdWithRevealedOptions: { peerId, fromPeerId in
@ -973,7 +976,7 @@ private func groupInfoEntries(account: Account, presentationData: PresentationDa
peerActions.append(ParticipantRevealAction(type: .destructive, title: presentationData.strings.Common_Delete, action: .remove))
}
entries.append(GroupInfoEntry.member(presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, presentationData.nameDisplayOrder, index: i, peerId: peer.id, peer: peer, participant: RenderedChannelParticipant(participant: participant, peer: peer), presence: peerPresences[peer.id], memberStatus: memberStatus, editing: ItemListPeerItemEditing(editable: canRemoveParticipant(account: account, isAdmin: canEditMembers, participantId: peer.id, invitedBy: sortedParticipants[i].invitedBy), editing: state.editingState != nil && canRemoveAnyMember, revealed: state.peerIdWithRevealedOptions == peer.id), revealActions: peerActions, enabled: !disabledPeerIds.contains(peer.id)))
entries.append(GroupInfoEntry.member(presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, presentationData.nameDisplayOrder, index: i, peerId: peer.id, peer: peer, participant: RenderedChannelParticipant(participant: participant, peer: peer), presence: peerPresences[peer.id], memberStatus: memberStatus, editing: ItemListPeerItemEditing(editable: canRemoveParticipant(account: account, isAdmin: canEditMembers, participantId: peer.id, invitedBy: sortedParticipants[i].invitedBy), editing: state.editingState != nil && canRemoveAnyMember, revealed: state.peerIdWithRevealedOptions == peer.id), revealActions: peerActions, enabled: !disabledPeerIds.contains(peer.id), selectable: peer.id != account.peerId))
}
}
} else if let channel = view.peers[view.peerId] as? TelegramChannel, let cachedChannelData = view.cachedData as? CachedChannelData, let memberCount = cachedChannelData.participantsSummary.memberCount {
@ -1095,7 +1098,7 @@ private func groupInfoEntries(account: Account, presentationData: PresentationDa
peerActions.append(ParticipantRevealAction(type: .destructive, title: presentationData.strings.Common_Delete, action: .remove))
}
entries.append(GroupInfoEntry.member(presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, presentationData.nameDisplayOrder, index: i, peerId: participant.peer.id, peer: participant.peer, participant: participant, presence: participant.presences[participant.peer.id], memberStatus: memberStatus, editing: ItemListPeerItemEditing(editable: !peerActions.isEmpty, editing: state.editingState != nil && canRemoveAnyMember, revealed: state.peerIdWithRevealedOptions == participant.peer.id), revealActions: peerActions, enabled: true))
entries.append(GroupInfoEntry.member(presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, presentationData.nameDisplayOrder, index: i, peerId: participant.peer.id, peer: participant.peer, participant: participant, presence: participant.presences[participant.peer.id], memberStatus: memberStatus, editing: ItemListPeerItemEditing(editable: !peerActions.isEmpty, editing: state.editingState != nil && canRemoveAnyMember, revealed: state.peerIdWithRevealedOptions == participant.peer.id), revealActions: peerActions, enabled: true, selectable: participant.peer.id != account.peerId))
}
}
@ -1892,6 +1895,8 @@ public func groupInfoController(context: AccountContext, peerId originalPeerId:
let searchContext = GroupMembersSearchContext(context: context, peerId: originalPeerId)
let hapticFeedback = HapticFeedback()
let globalNotificationsKey: PostboxViewKey = .preferences(keys: Set<ValueBoxKey>([PreferencesKeys.globalNotifications]))
let signal = combineLatest(queue: .mainQueue(), context.sharedContext.presentationData, statePromise.get(), peerView.get(), context.account.postbox.combinedView(keys: [globalNotificationsKey]), channelMembersPromise.get())
|> map { presentationData, state, view, combinedView, channelMembers -> (ItemListControllerState, (ItemListNodeState<GroupInfoEntry>, GroupInfoEntry.ItemGenerationArguments)) in
@ -1942,15 +1947,25 @@ public func groupInfoController(context: AccountContext, peerId originalPeerId:
} else {
rightNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Done), style: .bold, enabled: doneEnabled, action: {
var updateValues: (title: String?, description: String?) = (nil, nil)
var failed = false
updateState { state in
updateValues = valuesRequiringUpdate(state: state, view: view)
if updateValues.0 != nil || updateValues.1 != nil {
if (updateValues.description?.count ?? 0) > 255 {
failed = true
return state
}
return state.withUpdatedSavingData(true)
} else {
return state.withUpdatedEditingState(nil)
}
}
guard !failed else {
hapticFeedback.error()
return
}
let updateTitle: Signal<Void, Void>
if let titleValue = updateValues.title {
updateTitle = updatePeerTitle(account: context.account, peerId: view.peerId, title: titleValue)

View File

@ -100,7 +100,7 @@ private enum GroupsInCommonEntry: ItemListNodeEntry {
func item(_ arguments: GroupsInCommonControllerArguments) -> ListViewItem {
switch self {
case let .peerItem(_, theme, strings, dateTimeFormat, nameDisplayOrder, peer):
return ItemListPeerItem(theme: theme, strings: strings, dateTimeFormat: dateTimeFormat, nameDisplayOrder: nameDisplayOrder, account: arguments.account, peer: peer, presence: nil, text: .none, label: .none, editing: ItemListPeerItemEditing(editable: false, editing: false, revealed: false), switchValue: nil, enabled: true, sectionId: self.section, action: {
return ItemListPeerItem(theme: theme, strings: strings, dateTimeFormat: dateTimeFormat, nameDisplayOrder: nameDisplayOrder, account: arguments.account, peer: peer, presence: nil, text: .none, label: .none, editing: ItemListPeerItemEditing(editable: false, editing: false, revealed: false), switchValue: nil, enabled: true, selectable: true, sectionId: self.section, action: {
arguments.openPeer(peer.id)
}, setPeerIdWithRevealedOptions: { _, _ in
}, removePeer: { _ in

View File

@ -82,6 +82,7 @@ final class ItemListPeerItem: ListViewItem, ItemListItem {
let revealOptions: ItemListPeerItemRevealOptions?
let switchValue: ItemListPeerItemSwitch?
let enabled: Bool
let selectable: Bool
let sectionId: ItemListSectionId
let action: (() -> Void)?
let setPeerIdWithRevealedOptions: (PeerId?, PeerId?) -> Void
@ -91,7 +92,7 @@ final class ItemListPeerItem: ListViewItem, ItemListItem {
let hasTopGroupInset: Bool
let tag: ItemListItemTag?
init(theme: PresentationTheme, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, nameDisplayOrder: PresentationPersonNameOrder, account: Account, peer: Peer, aliasHandling: ItemListPeerItemAliasHandling = .standard, nameColor: ItemListPeerItemNameColor = .primary, nameStyle: ItemListPeerItemNameStyle = .distinctBold, presence: PeerPresence?, text: ItemListPeerItemText, label: ItemListPeerItemLabel, editing: ItemListPeerItemEditing, revealOptions: ItemListPeerItemRevealOptions? = nil, switchValue: ItemListPeerItemSwitch?, enabled: Bool, sectionId: ItemListSectionId, action: (() -> Void)?, setPeerIdWithRevealedOptions: @escaping (PeerId?, PeerId?) -> Void, removePeer: @escaping (PeerId) -> Void, toggleUpdated: ((Bool) -> Void)? = nil, hasTopStripe: Bool = true, hasTopGroupInset: Bool = true, tag: ItemListItemTag? = nil) {
init(theme: PresentationTheme, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, nameDisplayOrder: PresentationPersonNameOrder, account: Account, peer: Peer, aliasHandling: ItemListPeerItemAliasHandling = .standard, nameColor: ItemListPeerItemNameColor = .primary, nameStyle: ItemListPeerItemNameStyle = .distinctBold, presence: PeerPresence?, text: ItemListPeerItemText, label: ItemListPeerItemLabel, editing: ItemListPeerItemEditing, revealOptions: ItemListPeerItemRevealOptions? = nil, switchValue: ItemListPeerItemSwitch?, enabled: Bool, selectable: Bool, sectionId: ItemListSectionId, action: (() -> Void)?, setPeerIdWithRevealedOptions: @escaping (PeerId?, PeerId?) -> Void, removePeer: @escaping (PeerId) -> Void, toggleUpdated: ((Bool) -> Void)? = nil, hasTopStripe: Bool = true, hasTopGroupInset: Bool = true, tag: ItemListItemTag? = nil) {
self.theme = theme
self.strings = strings
self.dateTimeFormat = dateTimeFormat
@ -108,6 +109,7 @@ final class ItemListPeerItem: ListViewItem, ItemListItem {
self.revealOptions = revealOptions
self.switchValue = switchValue
self.enabled = enabled
self.selectable = selectable
self.sectionId = sectionId
self.action = action
self.setPeerIdWithRevealedOptions = setPeerIdWithRevealedOptions
@ -156,8 +158,6 @@ final class ItemListPeerItem: ListViewItem, ItemListItem {
}
}
var selectable: Bool = true
func selected(listView: ListView){
listView.clearHighlightAnimated(true)
self.action?()

View File

@ -0,0 +1,57 @@
import Foundation
import TelegramCore
import SwiftSignalKit
import Postbox
public struct LargeEmojiResourceId: MediaResourceId {
public let emoji: String
public let fontSize: CGFloat
public var uniqueId: String {
return "large-emoji-\(emoji)-\(fontSize)"
}
public var hashValue: Int {
return self.emoji.hashValue &* 31 &+ self.fontSize.hashValue
}
public func isEqual(to: MediaResourceId) -> Bool {
if let to = to as? LargeEmojiResourceId {
return self.emoji == to.emoji && self.fontSize == to.fontSize
} else {
return false
}
}
}
public class LargeEmojiResource: TelegramMediaResource {
public let emoji: String
public let fontSize: CGFloat
public init(emoji: String, fontSize: CGFloat) {
self.emoji = emoji
self.fontSize = fontSize
}
public required init(decoder: PostboxDecoder) {
self.emoji = decoder.decodeStringForKey("e", orElse: "")
self.fontSize = CGFloat(decoder.decodeDoubleForKey("s", orElse: 0.0))
}
public func encode(_ encoder: PostboxEncoder) {
encoder.encodeString(self.emoji, forKey: "e")
encoder.encodeDouble(Double(self.fontSize), forKey: "s")
}
public var id: MediaResourceId {
return LargeEmojiResourceId(emoji: self.emoji, fontSize: self.fontSize)
}
public func isEqual(to: MediaResource) -> Bool {
if let to = to as? LargeEmojiResource {
return self.emoji == to.emoji && self.fontSize == to.fontSize
} else {
return false
}
}
}

View File

@ -384,7 +384,7 @@ private enum NotificationExceptionEntry : ItemListNodeEntry {
arguments.selectPeer()
})
case let .peer(_, peer, theme, strings, dateTimeFormat, nameDisplayOrder, value, _, revealed, editing):
return ItemListPeerItem(theme: theme, strings: strings, dateTimeFormat: dateTimeFormat, nameDisplayOrder: nameDisplayOrder, account: arguments.account, peer: peer, presence: nil, text: .text(value), label: .none, editing: ItemListPeerItemEditing(editable: true, editing: editing, revealed: revealed), switchValue: nil, enabled: true, sectionId: self.section, action: {
return ItemListPeerItem(theme: theme, strings: strings, dateTimeFormat: dateTimeFormat, nameDisplayOrder: nameDisplayOrder, account: arguments.account, peer: peer, presence: nil, text: .text(value), label: .none, editing: ItemListPeerItemEditing(editable: true, editing: editing, revealed: revealed), switchValue: nil, enabled: true, selectable: true, sectionId: self.section, action: {
arguments.openPeer(peer)
}, setPeerIdWithRevealedOptions: { peerId, fromPeerId in
arguments.updateRevealedPeerId(peerId)

View File

@ -2995,3 +2995,28 @@ func callDefaultBackground() -> Signal<(TransformImageArguments) -> DrawingConte
return context
})
}
func largeEmoji(postbox: Postbox, emoji: String, fontSize: CGFloat) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> {
let resource = LargeEmojiResource(emoji: emoji, fontSize: fontSize)
let representation = CachedLargeEmojiRepresentation()
return postbox.mediaBox.cachedResourceRepresentation(resource, representation: representation, complete: true, fetch: true)
|> map { data in
return { arguments in
let context = DrawingContext(size: arguments.drawingSize, clear: true)
var sourceImage: UIImage?
if let data = try? Data(contentsOf: URL(fileURLWithPath: data.path), options: []), let image = UIImage(data: data, scale: UIScreen.main.scale) {
sourceImage = image
}
if let sourceImage = sourceImage, let cgImage = sourceImage.cgImage {
let imageSize = sourceImage.size
context.withFlippedContext { c in
c.draw(cgImage, in: CGRect(origin: CGPoint(x: floor((arguments.drawingSize.width - imageSize.width) / 2.0), y: floor((arguments.drawingSize.height - imageSize.height) / 2.0)), size: imageSize))
}
}
return context
}
}
}

View File

@ -59,6 +59,8 @@ enum PresentationResourceKey: Int32 {
case chatListLockTopUnlockedImage
case chatListLockBottomUnlockedImage
case chatListPending
case chatListClockFrame
case chatListClockMin
case chatListSingleCheck
case chatListDoubleCheck
case chatListBadgeBackgroundActive

View File

@ -32,6 +32,27 @@ private func generateBadgeBackgroundImage(theme: PresentationTheme, active: Bool
})?.stretchableImage(withLeftCapWidth: 10, topCapHeight: 10)
}
private func generateClockFrameImage(color: UIColor) -> UIImage? {
return generateImage(CGSize(width: 11.0, height: 11.0), contextGenerator: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
context.setStrokeColor(color.cgColor)
context.setFillColor(color.cgColor)
let strokeWidth: CGFloat = 1.0
context.setLineWidth(strokeWidth)
context.strokeEllipse(in: CGRect(x: strokeWidth / 2.0, y: strokeWidth / 2.0, width: size.width - strokeWidth, height: size.height - strokeWidth))
context.fill(CGRect(x: (11.0 - strokeWidth) / 2.0, y: strokeWidth * 3.0, width: strokeWidth, height: 11.0 / 2.0 - strokeWidth * 3.0))
})
}
private func generateClockMinImage(color: UIColor) -> UIImage? {
return generateImage(CGSize(width: 11.0, height: 11.0), contextGenerator: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
context.setFillColor(color.cgColor)
let strokeWidth: CGFloat = 1.0
context.fill(CGRect(x: (11.0 - strokeWidth) / 2.0, y: (11.0 - strokeWidth) / 2.0, width: 11.0 / 2.0 - strokeWidth, height: strokeWidth))
})
}
enum RecentStatusOnlineIconState {
case regular
case highlighted
@ -66,6 +87,18 @@ struct PresentationResourcesChatList {
})
}
static func clockFrameImage(_ theme: PresentationTheme) -> UIImage? {
return theme.image(PresentationResourceKey.chatListClockFrame.rawValue, { theme in
return generateClockFrameImage(color: theme.chatList.pendingIndicatorColor)
})
}
static func clockMinImage(_ theme: PresentationTheme) -> UIImage? {
return theme.image(PresentationResourceKey.chatListClockMin.rawValue, { theme in
return generateClockMinImage(color: theme.chatList.pendingIndicatorColor)
})
}
static func lockTopLockedImage(_ theme: PresentationTheme) -> UIImage? {
return theme.image(PresentationResourceKey.chatListLockTopLockedImage.rawValue, { theme in
return generateImage(CGSize(width: 7.0, height: 6.0), rotatedContext: { size, context in

View File

@ -135,7 +135,7 @@ private enum SelectivePrivacyPeersEntry: ItemListNodeEntry {
func item(_ arguments: SelectivePrivacyPeersControllerArguments) -> ListViewItem {
switch self {
case let .peerItem(_, theme, strings, dateTimeFormat, nameDisplayOrder, peer, editing, enabled):
return ItemListPeerItem(theme: theme, strings: strings, dateTimeFormat: dateTimeFormat, nameDisplayOrder: nameDisplayOrder, account: arguments.account, peer: peer, presence: nil, text: .none, label: .none, editing: editing, switchValue: nil, enabled: enabled, sectionId: self.section, action: nil, setPeerIdWithRevealedOptions: { previousId, id in
return ItemListPeerItem(theme: theme, strings: strings, dateTimeFormat: dateTimeFormat, nameDisplayOrder: nameDisplayOrder, account: arguments.account, peer: peer, presence: nil, text: .none, label: .none, editing: editing, switchValue: nil, enabled: enabled, selectable: true, sectionId: self.section, action: nil, setPeerIdWithRevealedOptions: { previousId, id in
arguments.setPeerIdWithRevealedOptions(previousId, id)
}, removePeer: { peerId in
arguments.removePeer(peerId)

View File

@ -74,7 +74,7 @@ private enum SettingsEntry: ItemListNodeEntry {
case keepPhone(PresentationTheme, String)
case changePhone(PresentationTheme, String)
case account(Int, Account, PresentationTheme, PresentationStrings, Peer, Int32, Bool)
case account(Int, Account, PresentationTheme, PresentationStrings, PresentationDateTimeFormat, Peer, Int32, Bool)
case addAccount(PresentationTheme, String)
case proxy(PresentationTheme, UIImage?, String, String)
@ -202,8 +202,8 @@ private enum SettingsEntry: ItemListNodeEntry {
} else {
return false
}
case let .account(lhsIndex, lhsAccount, lhsTheme, lhsStrings, lhsPeer, lhsBadgeCount, lhsRevealed):
if case let .account(rhsIndex, rhsAccount, rhsTheme, rhsStrings, rhsPeer, rhsBadgeCount, rhsRevealed) = rhs, lhsIndex == rhsIndex, lhsAccount === rhsAccount, lhsTheme === rhsTheme, lhsStrings === rhsStrings, lhsPeer.isEqual(rhsPeer), lhsBadgeCount == rhsBadgeCount, lhsRevealed == rhsRevealed {
case let .account(lhsIndex, lhsAccount, lhsTheme, lhsStrings, lhsDateTimeFormat, lhsPeer, lhsBadgeCount, lhsRevealed):
if case let .account(rhsIndex, rhsAccount, rhsTheme, rhsStrings, rhsDateTimeFormat, rhsPeer, rhsBadgeCount, rhsRevealed) = rhs, lhsIndex == rhsIndex, lhsAccount === rhsAccount, lhsTheme === rhsTheme, lhsStrings === rhsStrings, lhsDateTimeFormat == rhsDateTimeFormat, lhsPeer.isEqual(rhsPeer), lhsBadgeCount == rhsBadgeCount, lhsRevealed == rhsRevealed {
return true
} else {
return false
@ -362,16 +362,12 @@ private enum SettingsEntry: ItemListNodeEntry {
return ItemListActionItem(theme: theme, title: text, kind: .generic, alignment: .natural, sectionId: ItemListSectionId(self.section), style: .blocks, action: {
arguments.openPhoneNumberChange()
})
case let .account(_, account, theme, strings, peer, badgeCount, revealed):
case let .account(_, account, theme, strings, dateTimeFormat, peer, badgeCount, revealed):
var label: ItemListPeerItemLabel = .none
if badgeCount > 0 {
if badgeCount > 1000 {
label = .badge("\(badgeCount / 1000)K")
} else {
label = .badge("\(badgeCount)")
}
label = .badge(compactNumericCountString(Int(badgeCount), decimalSeparator: dateTimeFormat.decimalSeparator))
}
return ItemListPeerItem(theme: theme, strings: strings, dateTimeFormat: PresentationDateTimeFormat(timeFormat: .regular, dateFormat: .dayFirst, dateSeparator: ".", decimalSeparator: ".", groupingSeparator: ""), nameDisplayOrder: .firstLast, account: account, peer: peer, aliasHandling: .standard, nameStyle: .plain, presence: nil, text: .none, label: label, editing: ItemListPeerItemEditing(editable: true, editing: false, revealed: revealed), revealOptions: nil, switchValue: nil, enabled: true, sectionId: self.section, action: {
return ItemListPeerItem(theme: theme, strings: strings, dateTimeFormat: PresentationDateTimeFormat(timeFormat: .regular, dateFormat: .dayFirst, dateSeparator: ".", decimalSeparator: ".", groupingSeparator: ""), nameDisplayOrder: .firstLast, account: account, peer: peer, aliasHandling: .standard, nameStyle: .plain, presence: nil, text: .none, label: label, editing: ItemListPeerItemEditing(editable: true, editing: false, revealed: revealed), revealOptions: nil, switchValue: nil, enabled: true, selectable: true, sectionId: self.section, action: {
arguments.switchToAccount(account.id)
}, setPeerIdWithRevealedOptions: { lhs, rhs in
var lhsAccountId: AccountRecordId?
@ -475,7 +471,7 @@ private func settingsEntries(account: Account, presentationData: PresentationDat
if !accountsAndPeers.isEmpty {
var index = 0
for (peerAccount, peer, badgeCount) in accountsAndPeers {
entries.append(.account(index, peerAccount, presentationData.theme, presentationData.strings, peer, inAppNotificationSettings.displayNotificationsFromAllAccounts ? badgeCount : 0, state.accountIdWithRevealedOptions == peerAccount.id))
entries.append(.account(index, peerAccount, presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, peer, inAppNotificationSettings.displayNotificationsFromAllAccounts ? badgeCount : 0, state.accountIdWithRevealedOptions == peerAccount.id))
index += 1
}
if accountsAndPeers.count + 1 < maximumNumberOfAccounts {
@ -1234,11 +1230,7 @@ public func settingsController(context: AccountContext, accountManager: AccountM
let notificationsWarning = shouldDisplayNotificationsPermissionWarning(status: notificationsAuthorizationStatus, suppressed: notificationsWarningSuppressed)
var otherAccountsBadge: String?
if accountTabBarAvatarBadge > 0 {
if accountTabBarAvatarBadge > 1000 {
otherAccountsBadge = "\(accountTabBarAvatarBadge / 1000)K"
} else {
otherAccountsBadge = "\(accountTabBarAvatarBadge)"
}
otherAccountsBadge = compactNumericCountString(Int(accountTabBarAvatarBadge), decimalSeparator: presentationData.dateTimeFormat.decimalSeparator)
}
return ItemListControllerTabBarItem(title: presentationData.strings.Settings_Title, image: accountTabBarAvatar ?? icon, selectedImage: accountTabBarAvatar ?? icon, tintImages: accountTabBarAvatar == nil, badgeValue: notificationsWarning ? "!" : otherAccountsBadge)
}

View File

@ -164,7 +164,7 @@ private enum StorageUsageEntry: ItemListNodeEntry {
arguments.openClearAll()
})
case let .peer(_, theme, strings, dateTimeFormat, nameDisplayOrder, peer, chatPeer, value):
return ItemListPeerItem(theme: theme, strings: strings, dateTimeFormat: dateTimeFormat, nameDisplayOrder: nameDisplayOrder, account: arguments.account, peer: peer, aliasHandling: .threatSelfAsSaved, nameColor: chatPeer == nil ? .primary : .secret, presence: nil, text: .none, label: .disclosure(value), editing: ItemListPeerItemEditing(editable: false, editing: false, revealed: false), switchValue: nil, enabled: true, sectionId: self.section, action: {
return ItemListPeerItem(theme: theme, strings: strings, dateTimeFormat: dateTimeFormat, nameDisplayOrder: nameDisplayOrder, account: arguments.account, peer: peer, aliasHandling: .threatSelfAsSaved, nameColor: chatPeer == nil ? .primary : .secret, presence: nil, text: .none, label: .disclosure(value), editing: ItemListPeerItemEditing(editable: false, editing: false, revealed: false), switchValue: nil, enabled: true, selectable: true, sectionId: self.section, action: {
let resolvedPeer = chatPeer ?? peer
arguments.openPeerMedia(resolvedPeer.id)
}, setPeerIdWithRevealedOptions: { previousId, id in

View File

@ -135,12 +135,7 @@ private final class SwitchAccountItemNode: ASDisplayNode, AbstractSwitchAccountI
self.badgeBackgroundNode.image = generateStretchableFilledCircleImage(diameter: 20.0, color: presentationData.theme.list.itemCheckColors.fillColor)
self.badgeTitleNode = ImmediateTextNode()
if unreadCount > 0 {
let countString: String
if unreadCount > 1000 {
countString = "\(unreadCount / 1000)K"
} else {
countString = "\(unreadCount)"
}
let countString = compactNumericCountString(Int(unreadCount), decimalSeparator: presentationData.dateTimeFormat.decimalSeparator)
self.badgeTitleNode.attributedText = NSAttributedString(string: countString, font: Font.regular(14.0), textColor: presentationData.theme.list.itemCheckColors.foregroundColor)
} else {
self.badgeBackgroundNode.isHidden = true

View File

@ -125,24 +125,26 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode {
let contentHeight: CGFloat = 49.0
let margin: CGFloat = 16.0
let panelFrame = CGRect(origin: CGPoint(x: margin, y: layout.size.height - contentHeight - layout.intrinsicInsets.bottom - margin - 49.0), size: CGSize(width: layout.size.width - margin * 2.0, height: contentHeight))
let panelWrapperFrame = CGRect(origin: CGPoint(x: margin, y: layout.size.height - contentHeight - layout.intrinsicInsets.bottom - margin - 49.0), size: CGSize(width: layout.size.width - margin * 2.0 + margin, height: contentHeight))
let insets = layout.insets(options: [.input])
let panelFrame = CGRect(origin: CGPoint(x: margin + layout.safeInsets.left, y: layout.size.height - contentHeight - insets.bottom - margin - 49.0), size: CGSize(width: layout.size.width - margin * 2.0 - layout.safeInsets.left - layout.safeInsets.right, height: contentHeight))
let panelWrapperFrame = CGRect(origin: CGPoint(x: margin + layout.safeInsets.left, y: layout.size.height - contentHeight - insets.bottom - margin - 49.0), size: CGSize(width: layout.size.width - margin * 2.0 - layout.safeInsets.left - layout.safeInsets.right, height: contentHeight))
transition.updateFrame(node: self.panelNode, frame: panelFrame)
transition.updateFrame(node: self.panelWrapperNode, frame: panelWrapperFrame)
self.effectView.frame = CGRect(x: 0.0, y: 0.0, width: layout.size.width - margin * 2.0, height: contentHeight)
self.effectView.frame = CGRect(x: 0.0, y: 0.0, width: layout.size.width - margin * 2.0 - layout.safeInsets.left - layout.safeInsets.right, height: contentHeight)
let buttonTextSize = self.buttonTextNode.updateLayout(CGSize(width: 200.0, height: .greatestFiniteMagnitude))
let buttonTextFrame = CGRect(origin: CGPoint(x: layout.size.width - layout.safeInsets.right - rightInset - buttonTextSize.width - margin * 2.0, y: floor((contentHeight - buttonTextSize.height) / 2.0)), size: buttonTextSize)
let buttonTextFrame = CGRect(origin: CGPoint(x: layout.size.width - layout.safeInsets.left - layout.safeInsets.right - rightInset - buttonTextSize.width - margin * 2.0, y: floor((contentHeight - buttonTextSize.height) / 2.0)), size: buttonTextSize)
transition.updateFrame(node: self.buttonTextNode, frame: buttonTextFrame)
self.buttonNode.frame = CGRect(origin: CGPoint(x: layout.size.width - layout.safeInsets.right - rightInset - buttonTextSize.width - 8.0 - margin, y: 0.0), size: CGSize(width: layout.safeInsets.right + rightInset + buttonTextSize.width + 8.0 + margin, height: contentHeight))
self.buttonNode.frame = CGRect(origin: CGPoint(x: layout.size.width - layout.safeInsets.left - layout.safeInsets.right - rightInset - buttonTextSize.width - 8.0 - margin * 2.0, y: 0.0), size: CGSize(width: layout.safeInsets.right + rightInset + buttonTextSize.width + 8.0 + margin, height: contentHeight))
let textSize = self.textNode.updateLayout(CGSize(width: buttonTextFrame.minX - 8.0 - leftInset - layout.safeInsets.left - layout.safeInsets.right - margin * 2.0, height: .greatestFiniteMagnitude))
transition.updateFrame(node: self.textNode, frame: CGRect(origin: CGPoint(x: layout.safeInsets.left + leftInset, y: floor((contentHeight - textSize.height) / 2.0)), size: textSize))
transition.updateFrame(node: self.textNode, frame: CGRect(origin: CGPoint(x: leftInset, y: floor((contentHeight - textSize.height) / 2.0)), size: textSize))
let timerTextSize = self.timerTextNode.updateLayout(CGSize(width: 100.0, height: 100.0))
transition.updateFrame(node: self.timerTextNode, frame: CGRect(origin: CGPoint(x: layout.safeInsets.left + floor((leftInset - timerTextSize.width) / 2.0), y: floor((contentHeight - timerTextSize.height) / 2.0)), size: timerTextSize))
transition.updateFrame(node: self.timerTextNode, frame: CGRect(origin: CGPoint(x: floor((leftInset - timerTextSize.width) / 2.0), y: floor((contentHeight - timerTextSize.height) / 2.0)), size: timerTextSize))
let statusSize: CGFloat = 30.0
transition.updateFrame(node: self.statusNode, frame: CGRect(origin: CGPoint(x: layout.safeInsets.left + floor((leftInset - statusSize) / 2.0), y: floor((contentHeight - statusSize) / 2.0)), size: CGSize(width: statusSize, height: statusSize)))
transition.updateFrame(node: self.statusNode, frame: CGRect(origin: CGPoint(x: floor((leftInset - statusSize) / 2.0), y: floor((contentHeight - statusSize) / 2.0)), size: CGSize(width: statusSize, height: statusSize)))
if firstLayout {
self.statusNode.transitionToState(.secretTimeout(color: .white, icon: nil, beginTime: CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970, timeout: Double(self.remainingSeconds), sparks: false), completion: {})
}
@ -150,6 +152,7 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode {
func animateIn() {
self.panelNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
self.panelWrapperNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
}
func animateOut(completion: @escaping () -> Void) {
@ -160,7 +163,7 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode {
}
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
if !self.panelNode.frame.contains(point) {
if !self.panelNode.frame.insetBy(dx: -60.0, dy: 0.0).contains(point) {
return nil
}
return super.hitTest(point, with: event)

View File

@ -129,7 +129,7 @@ final class WallpaperOptionButtonNode: HighlightTrackingButtonNode {
}
func setSelected(_ selected: Bool, animated: Bool = false) {
self.isSelected = selected
self._value = .check(selected)
self.checkNode.setSelected(selected, animated: animated)
}