mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-12-03 21:16:35 +00:00
Merge commit '1c4500fcf89d52a7f1a00334065d5402ca829e37'
This commit is contained in:
commit
90b4d379d5
@ -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 */,
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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 })
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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))
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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 })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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),
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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):
|
||||
|
||||
@ -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())
|
||||
}
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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?()
|
||||
|
||||
57
TelegramUI/LargeEmojiResource.swift
Normal file
57
TelegramUI/LargeEmojiResource.swift
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -59,6 +59,8 @@ enum PresentationResourceKey: Int32 {
|
||||
case chatListLockTopUnlockedImage
|
||||
case chatListLockBottomUnlockedImage
|
||||
case chatListPending
|
||||
case chatListClockFrame
|
||||
case chatListClockMin
|
||||
case chatListSingleCheck
|
||||
case chatListDoubleCheck
|
||||
case chatListBadgeBackgroundActive
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user