Merge branch 'beta'

This commit is contained in:
Isaac 2024-01-02 23:15:20 +04:00
commit f27952b7c2
54 changed files with 996 additions and 253 deletions

View File

@ -4418,6 +4418,7 @@ Sorry for the inconvenience.";
"Call.Mute" = "mute";
"Call.Camera" = "camera";
"Call.Video" = "video";
"Call.Flip" = "flip";
"Call.End" = "end";
"Call.Speaker" = "speaker";
@ -5646,6 +5647,7 @@ Sorry for the inconvenience.";
"Call.CameraConfirmationText" = "Switch to video call?";
"Call.CameraConfirmationConfirm" = "Switch";
"Call.YourCameraOff" = "Your camera is off";
"Call.YourMicrophoneOff" = "Your microphone is off";
"Call.MicrophoneOff" = "%@'s microphone is off";
"Call.CameraOff" = "%@'s camera is off";
@ -10867,3 +10869,12 @@ Sorry for the inconvenience.";
"Chat.PlayOnceMesasgeClose" = "Close";
"Chat.PlayOnceMesasgeCloseAndDelete" = "Close and Delete";
"Chat.PlayOnceMesasge.DisableScreenCapture" = "Sorry, you can't play this message while screen recording is active.";
"Call.EncryptedAlertTitle" = "This call is end-to-end encrypted";
"Call.EncryptedAlertText" = "If the emoji on %@'s screen are the same, this call is 100% secure.";
"Call.EncryptionKeyTooltip" = "Encryption key of this call";
"Call.StatusBusy" = "Line Busy";
"Call.StatusDeclined" = "Call Declined";
"Call.StatusFailed" = "Call Failed";
"Call.StatusEnded" = "Call Ended";
"Call.StatusMissed" = "Call Missed";

View File

@ -43,6 +43,7 @@ swift_library(
deps = [
"//submodules/Display",
"//submodules/MetalEngine",
"//submodules/TelegramPresentationData",
"//submodules/TelegramUI/Components/Calls/CallScreen",
],
)
@ -188,5 +189,4 @@ xcodeproj(
},
},
default_xcode_configuration = "Debug"
)

View File

@ -4,6 +4,7 @@ import MetalEngine
import Display
import CallScreen
import ComponentFlow
import TelegramPresentationData
private extension UIScreen {
private static let cornerRadiusKey: String = {
@ -24,6 +25,7 @@ private extension UIScreen {
public final class ViewController: UIViewController {
private var callScreenView: PrivateCallScreen?
private var callState: PrivateCallScreen.State = PrivateCallScreen.State(
strings: defaultPresentationStrings,
lifecycleState: .connecting,
name: "Emma Walters",
shortName: "Emma",

View File

@ -356,27 +356,34 @@ public enum ChatSearchDomain: Equatable {
case everything
case members
case member(Peer)
case tag(String)
public static func ==(lhs: ChatSearchDomain, rhs: ChatSearchDomain) -> Bool {
switch lhs {
case .everything:
if case .everything = rhs {
return true
} else {
return false
}
case .members:
if case .members = rhs {
return true
} else {
return false
}
case let .member(lhsPeer):
if case let .member(rhsPeer) = rhs, lhsPeer.isEqual(rhsPeer) {
return true
} else {
return false
}
case .everything:
if case .everything = rhs {
return true
} else {
return false
}
case .members:
if case .members = rhs {
return true
} else {
return false
}
case let .member(lhsPeer):
if case let .member(rhsPeer) = rhs, lhsPeer.isEqual(rhsPeer) {
return true
} else {
return false
}
case let .tag(tag):
if case .tag(tag) = rhs {
return true
} else {
return false
}
}
}
}

View File

@ -22,6 +22,7 @@ public let repostStoryIcon = generateTintedImage(image: UIImage(bundleImageName:
private let archivedChatsIcon = UIImage(bundleImageName: "Avatar/ArchiveAvatarIcon")?.precomposed()
private let repliesIcon = generateTintedImage(image: UIImage(bundleImageName: "Avatar/RepliesMessagesIcon"), color: .white)
private let anonymousSavedMessagesIcon = generateTintedImage(image: UIImage(bundleImageName: "Avatar/AnonymousSenderIcon"), color: .white)
private let myNotesIcon = generateTintedImage(image: UIImage(bundleImageName: "Avatar/MyNotesIcon"), color: .white)
public func avatarPlaceholderFont(size: CGFloat) -> UIFont {
return Font.with(size: size, design: .round, weight: .bold)
@ -95,6 +96,8 @@ private func calculateColors(context: AccountContext?, explicitColorIndex: Int?,
colors = AvatarNode.savedMessagesColors
} else if case .anonymousSavedMessagesIcon = icon {
colors = AvatarNode.savedMessagesColors
} else if case .myNotesIcon = icon {
colors = AvatarNode.savedMessagesColors
} else if case .editAvatarIcon = icon, let theme {
colors = [theme.list.itemAccentColor.withAlphaComponent(0.1), theme.list.itemAccentColor.withAlphaComponent(0.1)]
} else if case let .archivedChatsIcon(hiddenByDefault) = icon, let theme = theme {
@ -176,6 +179,7 @@ private enum AvatarNodeIcon: Equatable {
case savedMessagesIcon
case repliesIcon
case anonymousSavedMessagesIcon
case myNotesIcon
case archivedChatsIcon(hiddenByDefault: Bool)
case editAvatarIcon
case deletedIcon
@ -189,6 +193,7 @@ public enum AvatarNodeImageOverride: Equatable {
case savedMessagesIcon
case repliesIcon
case anonymousSavedMessagesIcon
case myNotesIcon
case archivedChatsIcon(hiddenByDefault: Bool)
case editAvatarIcon(forceNone: Bool)
case deletedIcon
@ -492,6 +497,9 @@ public final class AvatarNode: ASDisplayNode {
case .anonymousSavedMessagesIcon:
representation = nil
icon = .anonymousSavedMessagesIcon
case .myNotesIcon:
representation = nil
icon = .myNotesIcon
case let .archivedChatsIcon(hiddenByDefault):
representation = nil
icon = .archivedChatsIcon(hiddenByDefault: hiddenByDefault)
@ -662,6 +670,9 @@ public final class AvatarNode: ASDisplayNode {
case .anonymousSavedMessagesIcon:
representation = nil
icon = .anonymousSavedMessagesIcon
case .myNotesIcon:
representation = nil
icon = .myNotesIcon
case let .archivedChatsIcon(hiddenByDefault):
representation = nil
icon = .archivedChatsIcon(hiddenByDefault: hiddenByDefault)
@ -901,6 +912,15 @@ public final class AvatarNode: ASDisplayNode {
if let anonymousSavedMessagesIcon = anonymousSavedMessagesIcon {
context.draw(anonymousSavedMessagesIcon.cgImage!, in: CGRect(origin: CGPoint(x: floor((bounds.size.width - anonymousSavedMessagesIcon.size.width) / 2.0), y: floor((bounds.size.height - anonymousSavedMessagesIcon.size.height) / 2.0)), size: anonymousSavedMessagesIcon.size))
}
} else if case .myNotesIcon = parameters.icon {
let factor = bounds.size.width / 60.0
context.translateBy(x: bounds.size.width / 2.0, y: bounds.size.height / 2.0)
context.scaleBy(x: factor, y: -factor)
context.translateBy(x: -bounds.size.width / 2.0, y: -bounds.size.height / 2.0)
if let myNotesIcon = myNotesIcon {
context.draw(myNotesIcon.cgImage!, in: CGRect(origin: CGPoint(x: floor((bounds.size.width - myNotesIcon.size.width) / 2.0), y: floor((bounds.size.height - myNotesIcon.size.height) / 2.0)), size: myNotesIcon.size))
}
} else if case .editAvatarIcon = parameters.icon, let theme = parameters.theme, !parameters.hasImage {
context.translateBy(x: bounds.size.width / 2.0, y: bounds.size.height / 2.0)
context.scaleBy(x: 1.0, y: -1.0)

View File

@ -217,7 +217,7 @@ private enum ChatListRecentEntry: Comparable, Identifiable {
sortOrder: nameSortOrder,
displayOrder: nameDisplayOrder,
context: context,
peerMode: .generalSearch,
peerMode: .generalSearch(isSavedMessages: false),
peer: .peer(peer: primaryPeer, chatPeer: chatPeer),
status: status,
badge: badge,
@ -508,7 +508,7 @@ public enum ChatListSearchEntry: Comparable, Identifiable {
toggleExpandGlobalResults()
})
return ContactsPeerItem(presentationData: ItemListPresentationData(presentationData), sortOrder: .firstLast, displayOrder: .firstLast, context: context, peerMode: .generalSearch, peer: .thread(peer: peer, title: threadInfo.info.title, icon: threadInfo.info.icon, color: threadInfo.info.iconColor), status: .none, badge: nil, enabled: true, selection: .none, editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false), index: nil, header: header, action: { _ in
return ContactsPeerItem(presentationData: ItemListPresentationData(presentationData), sortOrder: .firstLast, displayOrder: .firstLast, context: context, peerMode: .generalSearch(isSavedMessages: false), peer: .thread(peer: peer, title: threadInfo.info.title, icon: threadInfo.info.icon, color: threadInfo.info.iconColor), status: .none, badge: nil, enabled: true, selection: .none, editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false), index: nil, header: header, action: { _ in
interaction.peerSelected(peer, nil, threadInfo.id, nil)
}, contextAction: nil, animationCache: interaction.animationCache, animationRenderer: interaction.animationRenderer)
case let .recentlySearchedPeer(peer, associatedPeer, unreadBadge, _, theme, strings, nameSortOrder, nameDisplayOrder, storyStats):
@ -572,7 +572,7 @@ public enum ChatListSearchEntry: Comparable, Identifiable {
header = ChatListSearchItemHeader(type: headerType, theme: theme, strings: strings, actionTitle: nil, action: nil)
}
return ContactsPeerItem(presentationData: ItemListPresentationData(presentationData), sortOrder: nameSortOrder, displayOrder: nameDisplayOrder, context: context, peerMode: .generalSearch, peer: .peer(peer: primaryPeer, chatPeer: chatPeer), status: .none, badge: badge, enabled: enabled, selection: .none, editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false), index: nil, header: header, action: { contactPeer in
return ContactsPeerItem(presentationData: ItemListPresentationData(presentationData), sortOrder: nameSortOrder, displayOrder: nameDisplayOrder, context: context, peerMode: .generalSearch(isSavedMessages: false), peer: .peer(peer: primaryPeer, chatPeer: chatPeer), status: .none, badge: badge, enabled: enabled, selection: .none, editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false), index: nil, header: header, action: { contactPeer in
if case let .peer(maybePeer, maybeChatPeer) = contactPeer, let peer = maybePeer, let chatPeer = maybeChatPeer {
interaction.peerSelected(chatPeer, peer, nil, nil)
} else {
@ -671,8 +671,12 @@ public enum ChatListSearchEntry: Comparable, Identifiable {
toggleExpandLocalResults()
})
}
var isSavedMessages = false
if case .savedMessagesChats = location {
isSavedMessages = true
}
return ContactsPeerItem(presentationData: ItemListPresentationData(presentationData), sortOrder: nameSortOrder, displayOrder: nameDisplayOrder, context: context, peerMode: .generalSearch, peer: .peer(peer: primaryPeer, chatPeer: chatPeer), status: .none, badge: badge, enabled: enabled, selection: .none, editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false), index: nil, header: header, action: { contactPeer in
return ContactsPeerItem(presentationData: ItemListPresentationData(presentationData), sortOrder: nameSortOrder, displayOrder: nameDisplayOrder, context: context, peerMode: .generalSearch(isSavedMessages: isSavedMessages), peer: .peer(peer: primaryPeer, chatPeer: chatPeer), status: .none, badge: badge, enabled: enabled, selection: .none, editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false), index: nil, header: header, action: { contactPeer in
if case let .peer(maybePeer, maybeChatPeer) = contactPeer, let peer = maybePeer, let chatPeer = maybeChatPeer {
interaction.peerSelected(chatPeer, peer, nil, nil)
} else {
@ -747,8 +751,13 @@ public enum ChatListSearchEntry: Comparable, Identifiable {
toggleExpandGlobalResults()
})
}
var isSavedMessages = false
if case .savedMessagesChats = location {
isSavedMessages = true
}
return ContactsPeerItem(presentationData: ItemListPresentationData(presentationData), sortOrder: nameSortOrder, displayOrder: nameDisplayOrder, context: context, peerMode: .generalSearch, peer: .peer(peer: EnginePeer(peer.peer), chatPeer: EnginePeer(peer.peer)), status: .addressName(suffixString), badge: badge, enabled: enabled, selection: .none, editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false), index: nil, header: header, action: { _ in
return ContactsPeerItem(presentationData: ItemListPresentationData(presentationData), sortOrder: nameSortOrder, displayOrder: nameDisplayOrder, context: context, peerMode: .generalSearch(isSavedMessages: isSavedMessages), peer: .peer(peer: EnginePeer(peer.peer), chatPeer: EnginePeer(peer.peer)), status: .addressName(suffixString), badge: badge, enabled: enabled, selection: .none, editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false), index: nil, header: header, action: { _ in
interaction.peerSelected(EnginePeer(peer.peer), nil, nil, nil)
}, contextAction: peerContextAction.flatMap { peerContextAction in
return { node, gesture, location in
@ -1414,8 +1423,9 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
if let query {
foundLocalPeers = context.engine.messages.searchLocalSavedMessagesPeers(query: query.lowercased(), indexNameMapping: [
context.account.peerId: [
PeerIndexNameRepresentation.title(title: "saved messages", addressNames: []),
PeerIndexNameRepresentation.title(title: presentationData.strings.DialogList_SavedMessages.lowercased(), addressNames: [])
PeerIndexNameRepresentation.title(title: "my notes", addressNames: []),
//TODO:localize
PeerIndexNameRepresentation.title(title: "my notes".lowercased(), addressNames: [])
],
PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(2666000)): [
PeerIndexNameRepresentation.title(title: presentationData.strings.ChatList_AuthorHidden.lowercased(), addressNames: [])

View File

@ -1406,7 +1406,11 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
} else if peer.id.isAnonymousSavedMessages {
overrideImage = .anonymousSavedMessagesIcon
} else if peer.id == item.context.account.peerId && !displayAsMessage {
overrideImage = .savedMessagesIcon
if case .savedMessagesChats = item.chatListLocation {
overrideImage = .myNotesIcon
} else {
overrideImage = .savedMessagesIcon
}
} else if peer.isDeleted {
overrideImage = .deletedIcon
}
@ -2302,7 +2306,12 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
} else if isPeerGroup {
titleAttributedString = NSAttributedString(string: item.presentationData.strings.ChatList_ArchivedChatsTitle, font: titleFont, textColor: theme.titleColor)
} else if itemPeer.chatMainPeer?.id == item.context.account.peerId {
titleAttributedString = NSAttributedString(string: item.presentationData.strings.DialogList_SavedMessages, font: titleFont, textColor: theme.titleColor)
if case .savedMessagesChats = item.chatListLocation {
//TODO:localize
titleAttributedString = NSAttributedString(string: "My Notes", font: titleFont, textColor: theme.titleColor)
} else {
titleAttributedString = NSAttributedString(string: item.presentationData.strings.DialogList_SavedMessages, font: titleFont, textColor: theme.titleColor)
}
} else if let id = itemPeer.chatMainPeer?.id, id.isReplies {
titleAttributedString = NSAttributedString(string: item.presentationData.strings.DialogList_Replies, font: titleFont, textColor: theme.titleColor)
} else if let id = itemPeer.chatMainPeer?.id, id.isAnonymousSavedMessages {
@ -2608,7 +2617,8 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
var isFirstForumThreadSelectable = false
var forumThreads: [(id: Int64, title: NSAttributedString, iconId: Int64?, iconColor: Int32)] = []
if forumThread != nil || !topForumTopicItems.isEmpty {
if case .savedMessagesChats = item.chatListLocation {
} else if forumThread != nil || !topForumTopicItems.isEmpty {
if let forumThread = forumThread {
isFirstForumThreadSelectable = forumThread.isUnread
forumThreads.append((id: forumThread.id, title: NSAttributedString(string: forumThread.title, font: textFont, textColor: forumThread.isUnread || isSearching ? theme.authorNameColor : theme.messageTextColor), iconId: forumThread.iconId, iconColor: forumThread.iconColor))

View File

@ -580,7 +580,7 @@ private func mappedInsertEntries(context: AccountContext, nodeInteraction: ChatL
sortOrder: presentationData.nameSortOrder,
displayOrder: presentationData.nameDisplayOrder,
context: context,
peerMode: .generalSearch,
peerMode: .generalSearch(isSavedMessages: false),
peer: peerContent,
status: status,
enabled: enabled,
@ -619,7 +619,7 @@ private func mappedInsertEntries(context: AccountContext, nodeInteraction: ChatL
sortOrder: presentationData.nameSortOrder,
displayOrder: presentationData.nameDisplayOrder,
context: context,
peerMode: .generalSearch,
peerMode: .generalSearch(isSavedMessages: false),
peer: peerContent,
status: status,
enabled: true,
@ -681,7 +681,7 @@ private func mappedInsertEntries(context: AccountContext, nodeInteraction: ChatL
sortOrder: presentationData.nameSortOrder,
displayOrder: presentationData.nameDisplayOrder,
context: context,
peerMode: .generalSearch,
peerMode: .generalSearch(isSavedMessages: false),
peer: peerContent,
status: status,
enabled: true,
@ -899,7 +899,7 @@ private func mappedUpdateEntries(context: AccountContext, nodeInteraction: ChatL
sortOrder: presentationData.nameSortOrder,
displayOrder: presentationData.nameDisplayOrder,
context: context,
peerMode: .generalSearch,
peerMode: .generalSearch(isSavedMessages: false),
peer: peerContent,
status: status,
enabled: enabled,
@ -938,7 +938,7 @@ private func mappedUpdateEntries(context: AccountContext, nodeInteraction: ChatL
sortOrder: presentationData.nameSortOrder,
displayOrder: presentationData.nameDisplayOrder,
context: context,
peerMode: .generalSearch,
peerMode: .generalSearch(isSavedMessages: false),
peer: peerContent,
status: status,
enabled: true,
@ -1000,7 +1000,7 @@ private func mappedUpdateEntries(context: AccountContext, nodeInteraction: ChatL
sortOrder: presentationData.nameSortOrder,
displayOrder: presentationData.nameDisplayOrder,
context: context,
peerMode: .generalSearch,
peerMode: .generalSearch(isSavedMessages: false),
peer: peerContent,
status: status,
enabled: true,

View File

@ -24,6 +24,7 @@ swift_library(
"//submodules/TelegramUI/Components/AnimationCache:AnimationCache",
"//submodules/TelegramUI/Components/MultiAnimationRenderer:MultiAnimationRenderer",
"//submodules/TextFormat:TextFormat",
"//submodules/AppBundle",
],
visibility = [
"//visibility:public",

View File

@ -14,6 +14,11 @@ import AnimationCache
import MultiAnimationRenderer
import EmojiTextAttachmentView
import TextFormat
import AppBundle
private let tagImage: UIImage? = {
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/ReactionTagBackground"), color: .white)?.stretchableImage(withLeftCapWidth: 8, topCapHeight: 15)
}()
public final class ReactionIconView: PortalSourceView {
private var animationLayer: InlineStickerItemLayer?
@ -273,6 +278,7 @@ public final class ReactionButtonAsyncNode: ContextControllerSourceView {
var colors: Colors
var size: CGSize
var counter: Counter?
var isTag: Bool
}
private struct AnimationState {
@ -366,98 +372,116 @@ public final class ReactionButtonAsyncNode: ContextControllerSourceView {
let animationState = self.animationState
DispatchQueue.global().async { [weak self] in
var image: UIImage?
if true {
image = generateImage(layout.size, rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
UIGraphicsPushContext(context)
let image = generateImage(layout.size, rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
UIGraphicsPushContext(context)
func drawContents(colors: Colors) {
let backgroundColor: UIColor
let foregroundColor: UIColor
if isExtracted {
backgroundColor = UIColor(argb: colors.extractedBackground)
foregroundColor = UIColor(argb: colors.extractedForeground)
} else {
backgroundColor = UIColor(argb: colors.background)
foregroundColor = UIColor(argb: colors.foreground)
}
func drawContents(colors: Colors) {
let backgroundColor: UIColor
let foregroundColor: UIColor
if isExtracted {
backgroundColor = UIColor(argb: colors.extractedBackground)
foregroundColor = UIColor(argb: colors.extractedForeground)
} else {
backgroundColor = UIColor(argb: colors.background)
foregroundColor = UIColor(argb: colors.foreground)
context.setBlendMode(.copy)
if layout.isTag {
if let tagImage {
let rect = CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: size.height))
context.setFillColor(UIColor(rgb: layout.colors.background).cgColor)
context.fill(rect)
UIGraphicsPushContext(context)
tagImage.draw(in: rect, blendMode: .destinationIn, alpha: 1.0)
UIGraphicsPopContext()
context.setBlendMode(.destinationIn)
context.setFillColor(UIColor(white: 1.0, alpha: 0.5).cgColor)
context.fillEllipse(in: CGRect(origin: CGPoint(x: rect.width - 6.0 - 6.0, y: floor((rect.height - 6.0) * 0.5)), size: CGSize(width: 6.0, height: 6.0)))
context.setBlendMode(.copy)
}
context.setBlendMode(.copy)
} else {
context.setFillColor(backgroundColor.cgColor)
context.fillEllipse(in: CGRect(origin: CGPoint(), size: CGSize(width: size.height, height: size.height)))
context.fillEllipse(in: CGRect(origin: CGPoint(x: size.width - size.height, y: 0.0), size: CGSize(width: size.height, height: size.height)))
context.fill(CGRect(origin: CGPoint(x: size.height / 2.0, y: 0.0), size: CGSize(width: size.width - size.height, height: size.height)))
if let counter = layout.counter {
let isForegroundTransparent = foregroundColor.alpha < 1.0
context.setBlendMode(isForegroundTransparent ? .copy : .normal)
let textOrigin: CGFloat = 36.0
var rightTextOrigin = textOrigin + totalComponentWidth
let animationFraction: CGFloat
if let animationState = animationState, animationState.fromCounter != nil {
animationFraction = max(0.0, min(1.0, (CACurrentMediaTime() - animationState.startTime) / animationState.duration))
} else {
animationFraction = 1.0
}
for i in (0 ..< counter.components.count).reversed() {
let component = counter.components[i]
var componentAlpha: CGFloat = 1.0
var componentVerticalOffset: CGFloat = 0.0
if let animationState = animationState, let fromCounter = animationState.fromCounter {
let reverseIndex = counter.components.count - 1 - i
if reverseIndex < fromCounter.components.count {
let previousComponent = fromCounter.components[fromCounter.components.count - 1 - reverseIndex]
if previousComponent != component {
componentAlpha = animationFraction
componentVerticalOffset = -(1.0 - animationFraction) * 8.0
if previousComponent.string < component.string {
componentVerticalOffset = -componentVerticalOffset
}
let previousComponentAlpha = 1.0 - componentAlpha
var previousComponentVerticalOffset = animationFraction * 8.0
if previousComponent.string < component.string {
previousComponentVerticalOffset = -previousComponentVerticalOffset
}
var componentOrigin = rightTextOrigin - previousComponent.bounds.width
componentOrigin = max(componentOrigin, layout.size.height / 2.0 + UIScreenPixel)
let previousColor: UIColor
if isForegroundTransparent {
previousColor = foregroundColor.mixedWith(backgroundColor, alpha: 1.0 - previousComponentAlpha)
} else {
previousColor = foregroundColor.withMultipliedAlpha(previousComponentAlpha)
}
let string = NSAttributedString(string: previousComponent.string, font: Font.medium(11.0), textColor: previousColor)
string.draw(at: previousComponent.bounds.origin.offsetBy(dx: componentOrigin, dy: floorToScreenPixels(size.height - previousComponent.bounds.height) / 2.0 + previousComponentVerticalOffset))
}
}
}
let componentOrigin = rightTextOrigin - component.bounds.width
let currentColor: UIColor
if isForegroundTransparent {
currentColor = foregroundColor.mixedWith(backgroundColor, alpha: 1.0 - componentAlpha)
} else {
currentColor = foregroundColor.withMultipliedAlpha(componentAlpha)
}
let string = NSAttributedString(string: component.string, font: Font.medium(11.0), textColor: currentColor)
string.draw(at: component.bounds.origin.offsetBy(dx: componentOrigin, dy: floorToScreenPixels(size.height - component.bounds.height) / 2.0 + componentVerticalOffset))
rightTextOrigin -= component.bounds.width
}
}
}
if let counter = layout.counter {
let isForegroundTransparent = foregroundColor.alpha < 1.0
context.setBlendMode(isForegroundTransparent ? .copy : .normal)
let textOrigin: CGFloat = 36.0
var rightTextOrigin = textOrigin + totalComponentWidth
let animationFraction: CGFloat
if let animationState = animationState, animationState.fromCounter != nil {
animationFraction = max(0.0, min(1.0, (CACurrentMediaTime() - animationState.startTime) / animationState.duration))
} else {
animationFraction = 1.0
}
for i in (0 ..< counter.components.count).reversed() {
let component = counter.components[i]
var componentAlpha: CGFloat = 1.0
var componentVerticalOffset: CGFloat = 0.0
if let animationState = animationState, let fromCounter = animationState.fromCounter {
let reverseIndex = counter.components.count - 1 - i
if reverseIndex < fromCounter.components.count {
let previousComponent = fromCounter.components[fromCounter.components.count - 1 - reverseIndex]
if previousComponent != component {
componentAlpha = animationFraction
componentVerticalOffset = -(1.0 - animationFraction) * 8.0
if previousComponent.string < component.string {
componentVerticalOffset = -componentVerticalOffset
}
let previousComponentAlpha = 1.0 - componentAlpha
var previousComponentVerticalOffset = animationFraction * 8.0
if previousComponent.string < component.string {
previousComponentVerticalOffset = -previousComponentVerticalOffset
}
var componentOrigin = rightTextOrigin - previousComponent.bounds.width
componentOrigin = max(componentOrigin, layout.size.height / 2.0 + UIScreenPixel)
let previousColor: UIColor
if isForegroundTransparent {
previousColor = foregroundColor.mixedWith(backgroundColor, alpha: 1.0 - previousComponentAlpha)
} else {
previousColor = foregroundColor.withMultipliedAlpha(previousComponentAlpha)
}
let string = NSAttributedString(string: previousComponent.string, font: Font.medium(11.0), textColor: previousColor)
string.draw(at: previousComponent.bounds.origin.offsetBy(dx: componentOrigin, dy: floorToScreenPixels(size.height - previousComponent.bounds.height) / 2.0 + previousComponentVerticalOffset))
}
}
}
let componentOrigin = rightTextOrigin - component.bounds.width
let currentColor: UIColor
if isForegroundTransparent {
currentColor = foregroundColor.mixedWith(backgroundColor, alpha: 1.0 - componentAlpha)
} else {
currentColor = foregroundColor.withMultipliedAlpha(componentAlpha)
}
let string = NSAttributedString(string: component.string, font: Font.medium(11.0), textColor: currentColor)
string.draw(at: component.bounds.origin.offsetBy(dx: componentOrigin, dy: floorToScreenPixels(size.height - component.bounds.height) / 2.0 + componentVerticalOffset))
rightTextOrigin -= component.bounds.width
}
}
}
if layout.isTag {
drawContents(colors: layout.colors)
} else {
if let animationState = animationState, animationState.fromColors.isSelected != layout.colors.isSelected {
var animationFraction: CGFloat = max(0.0, min(1.0, (CACurrentMediaTime() - animationState.startTime) / animationState.duration))
if !layout.colors.isSelected {
@ -473,7 +497,7 @@ public final class ReactionButtonAsyncNode: ContextControllerSourceView {
drawContents(colors: layout.colors.isSelected ? layout.colors : animationState.fromColors)
context.resetClip()
context.beginPath()
context.addRect(CGRect(origin: CGPoint(), size: size))
context.addEllipse(in: CGRect(origin: CGPoint(x: center.x - diameter / 2.0, y: center.y - diameter / 2.0), size: CGSize(width: diameter, height: diameter)))
@ -482,10 +506,10 @@ public final class ReactionButtonAsyncNode: ContextControllerSourceView {
} else {
drawContents(colors: layout.colors)
}
UIGraphicsPopContext()
})
}
}
UIGraphicsPopContext()
})
DispatchQueue.main.async {
if let strongSelf = self, let image = image {
@ -641,7 +665,13 @@ public final class ReactionButtonAsyncNode: ContextControllerSourceView {
let backgroundColor = spec.component.chosenOrder != nil ? spec.component.colors.selectedBackground : spec.component.colors.deselectedBackground
let imageFrame = CGRect(origin: CGPoint(x: sideInsets + floorToScreenPixels((boundingImageSize.width - imageSize.width) / 2.0), y: floorToScreenPixels((height - imageSize.height) / 2.0)), size: imageSize)
let imageFrame: CGRect
if spec.component.isTag {
imageFrame = CGRect(origin: CGPoint(x: 6.0 + floorToScreenPixels((boundingImageSize.width - imageSize.width) / 2.0), y: floorToScreenPixels((height - imageSize.height) / 2.0)), size: imageSize)
} else {
imageFrame = CGRect(origin: CGPoint(x: sideInsets + floorToScreenPixels((boundingImageSize.width - imageSize.width) / 2.0), y: floorToScreenPixels((height - imageSize.height) / 2.0)), size: imageSize)
}
var counterLayout: CounterLayout?
@ -653,6 +683,8 @@ public final class ReactionButtonAsyncNode: ContextControllerSourceView {
} else {
size.width -= 2.0
}
} else if spec.component.isTag {
size.width += 2.0
} else {
let counterSpec = CounterLayout.Spec(
stringComponents: counterComponents
@ -686,7 +718,8 @@ public final class ReactionButtonAsyncNode: ContextControllerSourceView {
let backgroundLayout = ContainerButtonNode.Layout(
colors: backgroundColors,
size: size,
counter: backgroundCounter
counter: backgroundCounter,
isTag: spec.component.isTag
)
return Layout(
@ -1023,6 +1056,7 @@ public final class ReactionButtonComponent: Equatable {
public let colors: Colors
public let reaction: Reaction
public let avatarPeers: [EnginePeer]
public let isTag: Bool
public let count: Int
public let chosenOrder: Int?
public let action: (MessageReaction.Reaction) -> Void
@ -1032,6 +1066,7 @@ public final class ReactionButtonComponent: Equatable {
colors: Colors,
reaction: Reaction,
avatarPeers: [EnginePeer],
isTag: Bool,
count: Int,
chosenOrder: Int?,
action: @escaping (MessageReaction.Reaction) -> Void
@ -1040,6 +1075,7 @@ public final class ReactionButtonComponent: Equatable {
self.colors = colors
self.reaction = reaction
self.avatarPeers = avatarPeers
self.isTag = isTag
self.count = count
self.chosenOrder = chosenOrder
self.action = action
@ -1058,6 +1094,9 @@ public final class ReactionButtonComponent: Equatable {
if lhs.avatarPeers != rhs.avatarPeers {
return false
}
if lhs.isTag != rhs.isTag {
return false
}
if lhs.count != rhs.count {
return false
}
@ -1180,6 +1219,7 @@ public final class ReactionButtonsAsyncLayoutContainer {
action: @escaping (MessageReaction.Reaction) -> Void,
reactions: [ReactionButtonsAsyncLayoutContainer.Reaction],
colors: ReactionButtonComponent.Colors,
isTag: Bool,
constrainedWidth: CGFloat
) -> Result {
var items: [Result.Item] = []
@ -1228,8 +1268,9 @@ public final class ReactionButtonsAsyncLayoutContainer {
context: context,
colors: colors,
reaction: reaction.reaction,
avatarPeers: avatarPeers,
count: reaction.count,
avatarPeers: isTag ? [] : avatarPeers,
isTag: isTag,
count: isTag ? 0 : reaction.count,
chosenOrder: reaction.chosenOrder,
action: action
))

View File

@ -213,7 +213,7 @@ private enum ContactListNodeEntry: Comparable, Identifiable {
status = .custom(string: text, multiline: false, isActive: false, icon: nil)
}
return ContactsPeerItem(presentationData: ItemListPresentationData(presentationData), sortOrder: nameSortOrder, displayOrder: nameDisplayOrder, context: context, peerMode: isSearch ? .generalSearch : .peer, peer: itemPeer, status: status, enabled: enabled, selection: selection, selectionPosition: .left, editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false), additionalActions: additionalActions, index: nil, header: header, action: { _ in
return ContactsPeerItem(presentationData: ItemListPresentationData(presentationData), sortOrder: nameSortOrder, displayOrder: nameDisplayOrder, context: context, peerMode: isSearch ? .generalSearch(isSavedMessages: false) : .peer, peer: itemPeer, status: status, enabled: enabled, selection: selection, selectionPosition: .left, editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false), additionalActions: additionalActions, index: nil, header: header, action: { _ in
interaction.openPeer(peer, .generic)
}, itemHighlighting: interaction.itemHighlighting, contextAction: itemContextAction, storyStats: storyStats, openStories: { peer, sourceNode in
if case let .peer(peerValue, _) = peer, let peerValue {

View File

@ -63,8 +63,8 @@ public struct ContactsPeerItemEditing: Equatable {
}
}
public enum ContactsPeerItemPeerMode {
case generalSearch
public enum ContactsPeerItemPeerMode: Equatable {
case generalSearch(isSavedMessages: Bool)
case peer
}
@ -779,8 +779,13 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode {
textColor = item.presentationData.theme.list.itemPrimaryTextColor
}
if case let .user(user) = peer {
if peer.id == item.context.account.peerId, case .generalSearch = item.peerMode {
titleAttributedString = NSAttributedString(string: item.presentationData.strings.DialogList_SavedMessages, font: titleBoldFont, textColor: textColor)
if peer.id == item.context.account.peerId, case let .generalSearch(isSavedMessages) = item.peerMode {
if isSavedMessages {
//TODO:localize
titleAttributedString = NSAttributedString(string: "My Notes", font: titleBoldFont, textColor: textColor)
} else {
titleAttributedString = NSAttributedString(string: item.presentationData.strings.DialogList_SavedMessages, font: titleBoldFont, textColor: textColor)
}
} else if peer.id.isReplies {
titleAttributedString = NSAttributedString(string: item.presentationData.strings.DialogList_Replies, font: titleBoldFont, textColor: textColor)
} else if peer.id.isAnonymousSavedMessages {
@ -1028,8 +1033,12 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode {
case let .peer(peer, _):
if let peer = peer {
var overrideImage: AvatarNodeImageOverride?
if peer.id == item.context.account.peerId, case .generalSearch = item.peerMode {
overrideImage = .savedMessagesIcon
if peer.id == item.context.account.peerId, case let .generalSearch(isSavedMessages) = item.peerMode {
if isSavedMessages {
overrideImage = .myNotesIcon
} else {
overrideImage = .savedMessagesIcon
}
} else if peer.id.isReplies, case .generalSearch = item.peerMode {
overrideImage = .repliesIcon
} else if peer.id.isAnonymousSavedMessages, case .generalSearch = item.peerMode {

View File

@ -2249,6 +2249,7 @@ public final class ContextController: ViewController, StandalonePresentableContr
public var context: AccountContext?
public var reactionItems: [ReactionContextItem]
public var selectedReactionItems: Set<MessageReaction.Reaction>
public var reactionsTitle: String?
public var animationCache: AnimationCache?
public var alwaysAllowPremiumReactions: Bool
public var getEmojiContent: ((AnimationCache, MultiAnimationRenderer) -> Signal<EmojiPagerContentComponent, NoError>)?
@ -2263,6 +2264,7 @@ public final class ContextController: ViewController, StandalonePresentableContr
context: AccountContext? = nil,
reactionItems: [ReactionContextItem] = [],
selectedReactionItems: Set<MessageReaction.Reaction> = Set(),
reactionsTitle: String? = nil,
animationCache: AnimationCache? = nil,
alwaysAllowPremiumReactions: Bool = false,
getEmojiContent: ((AnimationCache, MultiAnimationRenderer) -> Signal<EmojiPagerContentComponent, NoError>)? = nil,
@ -2277,6 +2279,7 @@ public final class ContextController: ViewController, StandalonePresentableContr
self.animationCache = animationCache
self.reactionItems = reactionItems
self.selectedReactionItems = selectedReactionItems
self.reactionsTitle = reactionsTitle
self.alwaysAllowPremiumReactions = alwaysAllowPremiumReactions
self.getEmojiContent = getEmojiContent
self.disablePositionLock = disablePositionLock
@ -2291,6 +2294,7 @@ public final class ContextController: ViewController, StandalonePresentableContr
self.context = nil
self.reactionItems = []
self.selectedReactionItems = Set()
self.reactionsTitle = nil
self.alwaysAllowPremiumReactions = false
self.getEmojiContent = nil
self.disablePositionLock = false

View File

@ -46,7 +46,7 @@ public protocol ContextControllerActionsStackItem: AnyObject {
var id: AnyHashable? { get }
var tip: ContextController.Tip? { get }
var tipSignal: Signal<ContextController.Tip?, NoError>? { get }
var reactionItems: (context: AccountContext, reactionItems: [ReactionContextItem], selectedReactionItems: Set<MessageReaction.Reaction>, animationCache: AnimationCache, alwaysAllowPremiumReactions: Bool, getEmojiContent: ((AnimationCache, MultiAnimationRenderer) -> Signal<EmojiPagerContentComponent, NoError>)?)? { get }
var reactionItems: (context: AccountContext, reactionItems: [ReactionContextItem], selectedReactionItems: Set<MessageReaction.Reaction>, reactionsTitle: String?, animationCache: AnimationCache, alwaysAllowPremiumReactions: Bool, getEmojiContent: ((AnimationCache, MultiAnimationRenderer) -> Signal<EmojiPagerContentComponent, NoError>)?)? { get }
var dismissed: (() -> Void)? { get }
}
@ -911,7 +911,7 @@ final class ContextControllerActionsListStackItem: ContextControllerActionsStack
let id: AnyHashable?
let items: [ContextMenuItem]
let reactionItems: (context: AccountContext, reactionItems: [ReactionContextItem], selectedReactionItems: Set<MessageReaction.Reaction>, animationCache: AnimationCache, alwaysAllowPremiumReactions: Bool, getEmojiContent: ((AnimationCache, MultiAnimationRenderer) -> Signal<EmojiPagerContentComponent, NoError>)?)?
let reactionItems: (context: AccountContext, reactionItems: [ReactionContextItem], selectedReactionItems: Set<MessageReaction.Reaction>, reactionsTitle: String?, animationCache: AnimationCache, alwaysAllowPremiumReactions: Bool, getEmojiContent: ((AnimationCache, MultiAnimationRenderer) -> Signal<EmojiPagerContentComponent, NoError>)?)?
let tip: ContextController.Tip?
let tipSignal: Signal<ContextController.Tip?, NoError>?
let dismissed: (() -> Void)?
@ -919,7 +919,7 @@ final class ContextControllerActionsListStackItem: ContextControllerActionsStack
init(
id: AnyHashable?,
items: [ContextMenuItem],
reactionItems: (context: AccountContext, reactionItems: [ReactionContextItem], selectedReactionItems: Set<MessageReaction.Reaction>, animationCache: AnimationCache, alwaysAllowPremiumReactions: Bool, getEmojiContent: ((AnimationCache, MultiAnimationRenderer) -> Signal<EmojiPagerContentComponent, NoError>)?)?,
reactionItems: (context: AccountContext, reactionItems: [ReactionContextItem], selectedReactionItems: Set<MessageReaction.Reaction>, reactionsTitle: String?, animationCache: AnimationCache, alwaysAllowPremiumReactions: Bool, getEmojiContent: ((AnimationCache, MultiAnimationRenderer) -> Signal<EmojiPagerContentComponent, NoError>)?)?,
tip: ContextController.Tip?,
tipSignal: Signal<ContextController.Tip?, NoError>?,
dismissed: (() -> Void)?
@ -1009,7 +1009,7 @@ final class ContextControllerActionsCustomStackItem: ContextControllerActionsSta
let id: AnyHashable?
private let content: ContextControllerItemsContent
let reactionItems: (context: AccountContext, reactionItems: [ReactionContextItem], selectedReactionItems: Set<MessageReaction.Reaction>, animationCache: AnimationCache, alwaysAllowPremiumReactions: Bool, getEmojiContent: ((AnimationCache, MultiAnimationRenderer) -> Signal<EmojiPagerContentComponent, NoError>)?)?
let reactionItems: (context: AccountContext, reactionItems: [ReactionContextItem], selectedReactionItems: Set<MessageReaction.Reaction>, reactionsTitle: String?, animationCache: AnimationCache, alwaysAllowPremiumReactions: Bool, getEmojiContent: ((AnimationCache, MultiAnimationRenderer) -> Signal<EmojiPagerContentComponent, NoError>)?)?
let tip: ContextController.Tip?
let tipSignal: Signal<ContextController.Tip?, NoError>?
let dismissed: (() -> Void)?
@ -1017,7 +1017,7 @@ final class ContextControllerActionsCustomStackItem: ContextControllerActionsSta
init(
id: AnyHashable?,
content: ContextControllerItemsContent,
reactionItems: (context: AccountContext, reactionItems: [ReactionContextItem], selectedReactionItems: Set<MessageReaction.Reaction>, animationCache: AnimationCache, alwaysAllowPremiumReactions: Bool, getEmojiContent: ((AnimationCache, MultiAnimationRenderer) -> Signal<EmojiPagerContentComponent, NoError>)?)?,
reactionItems: (context: AccountContext, reactionItems: [ReactionContextItem], selectedReactionItems: Set<MessageReaction.Reaction>, reactionsTitle: String?, animationCache: AnimationCache, alwaysAllowPremiumReactions: Bool, getEmojiContent: ((AnimationCache, MultiAnimationRenderer) -> Signal<EmojiPagerContentComponent, NoError>)?)?,
tip: ContextController.Tip?,
tipSignal: Signal<ContextController.Tip?, NoError>?,
dismissed: (() -> Void)?
@ -1046,9 +1046,9 @@ final class ContextControllerActionsCustomStackItem: ContextControllerActionsSta
}
func makeContextControllerActionsStackItem(items: ContextController.Items) -> [ContextControllerActionsStackItem] {
var reactionItems: (context: AccountContext, reactionItems: [ReactionContextItem], selectedReactionItems: Set<MessageReaction.Reaction>, animationCache: AnimationCache, alwaysAllowPremiumReactions: Bool, getEmojiContent: ((AnimationCache, MultiAnimationRenderer) -> Signal<EmojiPagerContentComponent, NoError>)?)?
var reactionItems: (context: AccountContext, reactionItems: [ReactionContextItem], selectedReactionItems: Set<MessageReaction.Reaction>, reactionsTitle: String?, animationCache: AnimationCache, alwaysAllowPremiumReactions: Bool, getEmojiContent: ((AnimationCache, MultiAnimationRenderer) -> Signal<EmojiPagerContentComponent, NoError>)?)?
if let context = items.context, let animationCache = items.animationCache, !items.reactionItems.isEmpty {
reactionItems = (context, items.reactionItems, items.selectedReactionItems, animationCache, alwaysAllowPremiumReactions: items.alwaysAllowPremiumReactions, items.getEmojiContent)
reactionItems = (context, items.reactionItems, items.selectedReactionItems, reactionsTitle: items.reactionsTitle, animationCache, alwaysAllowPremiumReactions: items.alwaysAllowPremiumReactions, items.getEmojiContent)
}
switch items.content {
case let .list(listItems):
@ -1172,7 +1172,7 @@ final class ContextControllerActionsStackNode: ASDisplayNode {
var tip: ContextController.Tip?
let tipSignal: Signal<ContextController.Tip?, NoError>?
var tipNode: InnerTextSelectionTipContainerNode?
let reactionItems: (context: AccountContext, reactionItems: [ReactionContextItem], selectedReactionItems: Set<MessageReaction.Reaction>, animationCache: AnimationCache, alwaysAllowPremiumReactions: Bool, getEmojiContent: ((AnimationCache, MultiAnimationRenderer) -> Signal<EmojiPagerContentComponent, NoError>)?)?
let reactionItems: (context: AccountContext, reactionItems: [ReactionContextItem], selectedReactionItems: Set<MessageReaction.Reaction>, reactionsTitle: String?, animationCache: AnimationCache, alwaysAllowPremiumReactions: Bool, getEmojiContent: ((AnimationCache, MultiAnimationRenderer) -> Signal<EmojiPagerContentComponent, NoError>)?)?
let itemDismissed: (() -> Void)?
var storedScrollingState: CGFloat?
let positionLock: CGFloat?
@ -1187,7 +1187,7 @@ final class ContextControllerActionsStackNode: ASDisplayNode {
item: ContextControllerActionsStackItem,
tip: ContextController.Tip?,
tipSignal: Signal<ContextController.Tip?, NoError>?,
reactionItems: (context: AccountContext, reactionItems: [ReactionContextItem], selectedReactionItems: Set<MessageReaction.Reaction>, animationCache: AnimationCache, alwaysAllowPremiumReactions: Bool, getEmojiContent: ((AnimationCache, MultiAnimationRenderer) -> Signal<EmojiPagerContentComponent, NoError>)?)?,
reactionItems: (context: AccountContext, reactionItems: [ReactionContextItem], selectedReactionItems: Set<MessageReaction.Reaction>, reactionsTitle: String?, animationCache: AnimationCache, alwaysAllowPremiumReactions: Bool, getEmojiContent: ((AnimationCache, MultiAnimationRenderer) -> Signal<EmojiPagerContentComponent, NoError>)?)?,
itemDismissed: (() -> Void)?,
positionLock: CGFloat?
) {
@ -1338,7 +1338,7 @@ final class ContextControllerActionsStackNode: ASDisplayNode {
private var selectionPanGesture: UIPanGestureRecognizer?
var topReactionItems: (context: AccountContext, reactionItems: [ReactionContextItem], selectedReactionItems: Set<MessageReaction.Reaction>, animationCache: AnimationCache, alwaysAllowPremiumReactions: Bool, getEmojiContent: ((AnimationCache, MultiAnimationRenderer) -> Signal<EmojiPagerContentComponent, NoError>)?)? {
var topReactionItems: (context: AccountContext, reactionItems: [ReactionContextItem], selectedReactionItems: Set<MessageReaction.Reaction>, reactionsTitle: String?, animationCache: AnimationCache, alwaysAllowPremiumReactions: Bool, getEmojiContent: ((AnimationCache, MultiAnimationRenderer) -> Signal<EmojiPagerContentComponent, NoError>)?)? {
return self.itemContainers.last?.reactionItems
}

View File

@ -381,7 +381,16 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo
//TODO:
}
return self.scrollNode.hitTest(self.view.convert(point, to: self.scrollNode.view), with: event)
if let result = self.scrollNode.hitTest(self.view.convert(point, to: self.scrollNode.view), with: event) {
if let reactionContextNode = self.reactionContextNode, reactionContextNode.isExpanded {
if result === self.actionsContainerNode.view {
return self.dismissTapNode.view
}
}
return result
}
return nil
} else {
return nil
}
@ -639,6 +648,7 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo
presentationData: presentationData,
items: reactionItems.reactionItems,
selectedItems: reactionItems.selectedReactionItems,
title: reactionItems.reactionsTitle,
alwaysAllowPremiumReactions: reactionItems.alwaysAllowPremiumReactions,
getEmojiContent: reactionItems.getEmojiContent,
isExpandedUpdated: { [weak self] transition in

View File

@ -46,15 +46,17 @@ public struct SearchBarToken {
public let icon: UIImage?
public let iconOffset: CGFloat?
public let peer: (EnginePeer, AccountContext, PresentationTheme)?
public let isTag: Bool
public let title: String
public let style: Style?
public let permanent: Bool
public init(id: AnyHashable, icon: UIImage?, iconOffset: CGFloat? = 0.0, peer: (EnginePeer, AccountContext, PresentationTheme)? = nil, title: String, style: Style? = nil, permanent: Bool) {
public init(id: AnyHashable, icon: UIImage?, iconOffset: CGFloat? = 0.0, peer: (EnginePeer, AccountContext, PresentationTheme)? = nil, isTag: Bool = false, title: String, style: Style? = nil, permanent: Bool) {
self.id = id
self.icon = icon
self.iconOffset = iconOffset
self.peer = peer
self.isTag = isTag
self.title = title
self.style = style
self.permanent = permanent
@ -108,15 +110,20 @@ private final class TokenNode: ASDisplayNode {
} else {
self.containerNode.addSubnode(self.backgroundNode)
let backgroundColor = token.style?.backgroundColor ?? theme.inputIcon
let backgroundColor = token.isTag ? theme.inputIcon.withMultipliedAlpha(0.2) : (token.style?.backgroundColor ?? theme.inputIcon)
let strokeColor = token.style?.strokeColor ?? backgroundColor
self.backgroundNode.image = generateStretchableFilledCircleImage(diameter: 8.0, color: backgroundColor, strokeColor: strokeColor, strokeWidth: UIScreenPixel, backgroundColor: nil)
let foregroundColor = token.style?.foregroundColor ?? .white
if token.isTag {
self.backgroundNode.image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Title Panels/SearchTagTokenBackground"), color: backgroundColor)?.stretchableImage(withLeftCapWidth: 7, topCapHeight: 0)
} else {
self.backgroundNode.image = generateStretchableFilledCircleImage(diameter: 8.0, color: backgroundColor, strokeColor: strokeColor, strokeWidth: UIScreenPixel, backgroundColor: nil)
}
let foregroundColor = token.isTag ? theme.primaryText : (token.style?.foregroundColor ?? .white)
self.iconNode.image = generateTintedImage(image: token.icon, color: foregroundColor)
self.containerNode.addSubnode(self.iconNode)
self.titleNode.attributedText = NSAttributedString(string: token.title, font: Font.regular(17.0), textColor: foregroundColor)
self.titleNode.attributedText = NSAttributedString(string: token.title, font: Font.regular(token.isTag ? 14.0 : 17.0), textColor: foregroundColor)
self.containerNode.addSubnode(self.titleNode)
}
}
@ -132,19 +139,24 @@ private final class TokenNode: ASDisplayNode {
}
func animateIn() {
let targetFrame = self.containerNode.frame
self.containerNode.layer.animateFrame(from: CGRect(origin: targetFrame.origin, size: CGSize(width: 1.0, height: targetFrame.height)), to: targetFrame, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring)
self.backgroundNode.layer.animateFrame(from: CGRect(origin: targetFrame.origin, size: CGSize(width: 1.0, height: targetFrame.height)), to: targetFrame, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring)
if let avatarNode = self.avatarNode {
avatarNode.layer.animateScale(from: 0.1, to: 1.0, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring)
avatarNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15)
if self.token.isTag {
self.layer.animateScale(from: 0.1, to: 1.0, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring)
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15)
} else {
let targetFrame = self.containerNode.frame
self.containerNode.layer.animateFrame(from: CGRect(origin: targetFrame.origin, size: CGSize(width: 1.0, height: targetFrame.height)), to: targetFrame, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring)
self.backgroundNode.layer.animateFrame(from: CGRect(origin: targetFrame.origin, size: CGSize(width: 1.0, height: targetFrame.height)), to: targetFrame, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring)
if let avatarNode = self.avatarNode {
avatarNode.layer.animateScale(from: 0.1, to: 1.0, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring)
avatarNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15)
}
self.iconNode.layer.animateScale(from: 0.1, to: 1.0, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring)
self.iconNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15)
self.titleNode.layer.animateScale(from: 0.1, to: 1.0, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring)
self.titleNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15)
}
self.iconNode.layer.animateScale(from: 0.1, to: 1.0, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring)
self.iconNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15)
self.titleNode.layer.animateScale(from: 0.1, to: 1.0, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring)
self.titleNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15)
}
func animateOut() {
@ -160,11 +172,21 @@ private final class TokenNode: ASDisplayNode {
self.isCollapsed = isCollapsed
if theme !== self.theme || isSelected != wasSelected {
let backgroundColor = isSelected ? self.theme.accent : (token.style?.backgroundColor ?? self.theme.inputIcon)
let strokeColor = isSelected ? backgroundColor : (token.style?.strokeColor ?? backgroundColor)
self.backgroundNode.image = generateStretchableFilledCircleImage(diameter: 8.0, color: backgroundColor, strokeColor: strokeColor, strokeWidth: UIScreenPixel, backgroundColor: nil)
let backgroundColor: UIColor
if isSelected {
backgroundColor = self.theme.accent
} else {
backgroundColor = token.isTag ? theme.inputIcon.withMultipliedAlpha(0.2) : (token.style?.backgroundColor ?? self.theme.inputIcon)
}
var foregroundColor = isSelected ? .white : (token.style?.foregroundColor ?? .white)
let strokeColor = isSelected ? backgroundColor : (token.style?.strokeColor ?? backgroundColor)
if token.isTag {
self.backgroundNode.image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Title Panels/SearchTagTokenBackground"), color: backgroundColor)?.stretchableImage(withLeftCapWidth: 7, topCapHeight: 0)
} else {
self.backgroundNode.image = generateStretchableFilledCircleImage(diameter: 8.0, color: backgroundColor, strokeColor: strokeColor, strokeWidth: UIScreenPixel, backgroundColor: nil)
}
var foregroundColor = isSelected ? .white : (token.isTag ? self.theme.primaryText : (token.style?.foregroundColor ?? .white))
if foregroundColor.distance(to: backgroundColor) < 1 {
foregroundColor = .black
}
@ -172,12 +194,15 @@ private final class TokenNode: ASDisplayNode {
if let image = token.icon {
self.iconNode.image = generateTintedImage(image: image, color: foregroundColor)
}
self.titleNode.attributedText = NSAttributedString(string: token.title, font: Font.regular(17.0), textColor: foregroundColor)
self.titleNode.attributedText = NSAttributedString(string: token.title, font: Font.regular(token.isTag ? 14.0 : 17.0), textColor: foregroundColor)
}
}
func updateLayout(constrainedSize: CGSize, transition: ContainedViewLayoutTransition) -> CGSize {
let height: CGFloat = 24.0
var height: CGFloat = 24.0
if self.token.isTag {
height += 2.0
}
var leftInset: CGFloat = 3.0
if let icon = self.iconNode.image {
@ -189,6 +214,9 @@ private final class TokenNode: ASDisplayNode {
transition.updateFrame(node: self.iconNode, frame: iconFrame)
leftInset += icon.size.width + 3.0
}
if self.token.isTag {
leftInset += 2.0
}
let iconSize = self.token.icon?.size ?? CGSize()
let titleSize = self.titleNode.measure(CGSize(width: constrainedSize.width - 6.0, height: constrainedSize.height))
@ -196,6 +224,9 @@ private final class TokenNode: ASDisplayNode {
if !iconSize.width.isZero {
width += iconSize.width + 7.0
}
if self.token.isTag {
width += 19.0
}
let size: CGSize
if let avatarNode = self.avatarNode {
@ -358,6 +389,10 @@ private class SearchBarTextField: UITextField, UIScrollViewDelegate {
}
longTitlesWidth += resolvedSideInset
if !tokenSizes.isEmpty {
leftOffset -= 8.0
}
let verticalOffset: CGFloat = 0.0
var horizontalOffset: CGFloat = 0.0
for i in 0 ..< tokenSizes.count {

View File

@ -157,6 +157,7 @@ final class CallControllerNodeV2: ViewControllerTracingNode, CallControllerNodeP
}
self.callScreenState = PrivateCallScreen.State(
strings: presentationData.strings,
lifecycleState: .connecting,
name: " ",
shortName: " ",

View File

@ -420,6 +420,8 @@ swift_library(
"//submodules/TelegramUI/Components/Settings/WallpaperGalleryScreen",
"//submodules/TelegramUI/Components/Settings/WallpaperGridScreen",
"//submodules/TelegramUI/Components/Chat/ChatMessageNotificationItem",
"//submodules/Components/MultilineTextComponent",
"//submodules/TelegramUI/Components/PlainButtonComponent",
] + select({
"@build_bazel_rules_apple//apple:ios_arm64": appcenter_targets,
"//build-system:ios_sim_arm64": [],

View File

@ -68,6 +68,7 @@ swift_library(
"//submodules/TelegramUI/Components/AnimatedTextComponent",
"//submodules/AppBundle",
"//submodules/UIKitRuntimeUtils",
"//submodules/TelegramPresentationData",
],
visibility = [
"//visibility:public",

View File

@ -0,0 +1,21 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "Snow.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

View File

@ -3,34 +3,39 @@ import UIKit
import Display
final class BackButtonView: HighlightableButton {
private struct Params: Equatable {
var text: String
init(text: String) {
self.text = text
}
}
private struct Layout: Equatable {
var params: Params
var size: CGSize
init(params: Params, size: CGSize) {
self.params = params
self.size = size
}
}
private let iconView: UIImageView
private let textView: TextView
let size: CGSize
private var currentLayout: Layout?
var pressAction: (() -> Void)?
init(text: String) {
override init(frame: CGRect) {
self.iconView = UIImageView(image: NavigationBar.backArrowImage(color: .white))
self.iconView.isUserInteractionEnabled = false
self.textView = TextView()
self.textView.isUserInteractionEnabled = false
let spacing: CGFloat = 8.0
var iconSize: CGSize = self.iconView.image?.size ?? CGSize(width: 2.0, height: 2.0)
let iconScaleFactor: CGFloat = 0.9
iconSize.width = floor(iconSize.width * iconScaleFactor)
iconSize.height = floor(iconSize.height * iconScaleFactor)
let textSize = self.textView.update(string: text, fontSize: 17.0, fontWeight: UIFont.Weight.regular.rawValue, color: .white, constrainedWidth: 100.0, transition: .immediate)
self.size = CGSize(width: iconSize.width + spacing + textSize.width, height: textSize.height)
self.iconView.frame = CGRect(origin: CGPoint(x: 0.0, y: floorToScreenPixels((self.size.height - iconSize.height) * 0.5)), size: iconSize)
self.textView.frame = CGRect(origin: CGPoint(x: iconSize.width + spacing, y: floorToScreenPixels((self.size.height - textSize.height) * 0.5)), size: textSize)
super.init(frame: CGRect())
super.init(frame: frame)
self.addSubview(self.iconView)
self.addSubview(self.textView)
@ -53,4 +58,31 @@ final class BackButtonView: HighlightableButton {
return nil
}
}
func update(text: String) -> CGSize {
let params = Params(text: text)
if let currentLayout = self.currentLayout, currentLayout.params == params {
return currentLayout.size
}
let size = self.update(params: params)
self.currentLayout = Layout(params: params, size: size)
return size
}
private func update(params: Params) -> CGSize {
let spacing: CGFloat = 8.0
var iconSize: CGSize = self.iconView.image?.size ?? CGSize(width: 2.0, height: 2.0)
let iconScaleFactor: CGFloat = 0.9
iconSize.width = floor(iconSize.width * iconScaleFactor)
iconSize.height = floor(iconSize.height * iconScaleFactor)
let textSize = self.textView.update(string: params.text, fontSize: 17.0, fontWeight: UIFont.Weight.regular.rawValue, color: .white, constrainedWidth: 100.0, transition: .immediate)
let size = CGSize(width: iconSize.width + spacing + textSize.width, height: textSize.height)
self.iconView.frame = CGRect(origin: CGPoint(x: 0.0, y: floorToScreenPixels((size.height - iconSize.height) * 0.5)), size: iconSize)
self.textView.frame = CGRect(origin: CGPoint(x: iconSize.width + spacing, y: floorToScreenPixels((size.height - textSize.height) * 0.5)), size: textSize)
return size
}
}

View File

@ -3,6 +3,7 @@ import UIKit
import Display
import ComponentFlow
import AppBundle
import TelegramPresentationData
final class ButtonGroupView: OverlayMaskContainerView {
final class Button {
@ -85,7 +86,7 @@ final class ButtonGroupView: OverlayMaskContainerView {
return result
}
func update(size: CGSize, insets: UIEdgeInsets, minWidth: CGFloat, controlsHidden: Bool, displayClose: Bool, buttons: [Button], notices: [Notice], transition: Transition) -> CGFloat {
func update(size: CGSize, insets: UIEdgeInsets, minWidth: CGFloat, controlsHidden: Bool, displayClose: Bool, strings: PresentationStrings, buttons: [Button], notices: [Notice], transition: Transition) -> CGFloat {
self.buttons = buttons
let buttonSize: CGFloat = 56.0
@ -190,7 +191,7 @@ final class ButtonGroupView: OverlayMaskContainerView {
}
}
let closeButtonSize = CGSize(width: minWidth, height: buttonSize)
closeButtonView.update(text: "Close", size: closeButtonSize, transition: closeButtonTransition)
closeButtonView.update(text: strings.Common_Close, size: closeButtonSize, transition: closeButtonTransition)
closeButtonTransition.setFrame(view: closeButtonView, frame: CGRect(origin: CGPoint(x: floor((size.width - closeButtonSize.width) * 0.5), y: buttonY), size: closeButtonSize))
if animateIn && !transition.animation.isImmediate {
@ -218,9 +219,9 @@ final class ButtonGroupView: OverlayMaskContainerView {
case let .speaker(audioOutput):
switch audioOutput {
case .internalSpeaker, .speaker:
title = "speaker"
title = strings.Call_Speaker
default:
title = "audio"
title = strings.Call_Audio
}
switch audioOutput {
@ -247,19 +248,19 @@ final class ButtonGroupView: OverlayMaskContainerView {
isActive = true
}
case .flipCamera:
title = "flip"
title = strings.Call_Flip
image = UIImage(bundleImageName: "Call/Flip")
isActive = false
case let .video(isActiveValue):
title = "video"
title = strings.Call_Video
image = UIImage(bundleImageName: "Call/Video")
isActive = isActiveValue
case let .microphone(isActiveValue):
title = "mute"
title = strings.Call_Mute
image = UIImage(bundleImageName: "Call/Mute")
isActive = isActiveValue
case .end:
title = "end"
title = strings.Call_End
image = UIImage(bundleImageName: "Call/End")
isActive = false
isDestructive = true

View File

@ -7,6 +7,7 @@ import MetalEngine
import ComponentFlow
import SwiftSignalKit
import UIKitRuntimeUtils
import TelegramPresentationData
public final class PrivateCallScreen: OverlayMaskContainerView, AVPictureInPictureControllerDelegate {
public struct State: Equatable {
@ -67,6 +68,7 @@ public final class PrivateCallScreen: OverlayMaskContainerView, AVPictureInPictu
case bluetooth
}
public var strings: PresentationStrings
public var lifecycleState: LifecycleState
public var name: String
public var shortName: String
@ -77,8 +79,10 @@ public final class PrivateCallScreen: OverlayMaskContainerView, AVPictureInPictu
public var localVideo: VideoSource?
public var remoteVideo: VideoSource?
public var isRemoteBatteryLow: Bool
public var displaySnowEffect: Bool
public init(
strings: PresentationStrings,
lifecycleState: LifecycleState,
name: String,
shortName: String,
@ -88,8 +92,10 @@ public final class PrivateCallScreen: OverlayMaskContainerView, AVPictureInPictu
isRemoteAudioMuted: Bool,
localVideo: VideoSource?,
remoteVideo: VideoSource?,
isRemoteBatteryLow: Bool
isRemoteBatteryLow: Bool,
displaySnowEffect: Bool = false
) {
self.strings = strings
self.lifecycleState = lifecycleState
self.name = name
self.shortName = shortName
@ -100,9 +106,13 @@ public final class PrivateCallScreen: OverlayMaskContainerView, AVPictureInPictu
self.localVideo = localVideo
self.remoteVideo = remoteVideo
self.isRemoteBatteryLow = isRemoteBatteryLow
self.displaySnowEffect = displaySnowEffect
}
public static func ==(lhs: State, rhs: State) -> Bool {
if lhs.strings !== rhs.strings {
return false
}
if lhs.lifecycleState != rhs.lifecycleState {
return false
}
@ -133,6 +143,9 @@ public final class PrivateCallScreen: OverlayMaskContainerView, AVPictureInPictu
if lhs.isRemoteBatteryLow != rhs.isRemoteBatteryLow {
return false
}
if lhs.displaySnowEffect != rhs.displaySnowEffect {
return false
}
return true
}
}
@ -218,6 +231,8 @@ public final class PrivateCallScreen: OverlayMaskContainerView, AVPictureInPictu
private var pipVideoCallViewController: UIViewController?
private var pipController: AVPictureInPictureController?
private var snowEffectView: SnowEffectView?
public override init(frame: CGRect) {
self.overlayContentsView = UIView()
self.overlayContentsView.isUserInteractionEnabled = false
@ -240,7 +255,7 @@ public final class PrivateCallScreen: OverlayMaskContainerView, AVPictureInPictu
self.titleView = TextView()
self.statusView = StatusView()
self.backButtonView = BackButtonView(text: "Back")
self.backButtonView = BackButtonView(frame: CGRect())
self.pipView = PrivateCallPictureInPictureView(frame: CGRect(origin: CGPoint(), size: CGSize()))
@ -740,16 +755,16 @@ public final class PrivateCallScreen: OverlayMaskContainerView, AVPictureInPictu
var notices: [ButtonGroupView.Notice] = []
if !isTerminated {
if params.state.isLocalAudioMuted {
notices.append(ButtonGroupView.Notice(id: AnyHashable(0 as Int), icon: "Call/CallToastMicrophone", text: "Your microphone is turned off"))
notices.append(ButtonGroupView.Notice(id: AnyHashable(0 as Int), icon: "Call/CallToastMicrophone", text: params.state.strings.Call_YourMicrophoneOff))
}
if params.state.isRemoteAudioMuted {
notices.append(ButtonGroupView.Notice(id: AnyHashable(1 as Int), icon: "Call/CallToastMicrophone", text: "\(params.state.shortName)'s microphone is turned off"))
notices.append(ButtonGroupView.Notice(id: AnyHashable(1 as Int), icon: "Call/CallToastMicrophone", text: params.state.strings.Call_MicrophoneOff(params.state.shortName).string))
}
if params.state.remoteVideo != nil && params.state.localVideo == nil {
notices.append(ButtonGroupView.Notice(id: AnyHashable(2 as Int), icon: "Call/CallToastCamera", text: "Your camera is turned off"))
notices.append(ButtonGroupView.Notice(id: AnyHashable(2 as Int), icon: "Call/CallToastCamera", text: params.state.strings.Call_YourCameraOff))
}
if params.state.isRemoteBatteryLow {
notices.append(ButtonGroupView.Notice(id: AnyHashable(3 as Int), icon: "Call/CallToastBattery", text: "\(params.state.shortName)'s battery is low"))
notices.append(ButtonGroupView.Notice(id: AnyHashable(3 as Int), icon: "Call/CallToastBattery", text: params.state.strings.Call_BatteryLow(params.state.shortName).string))
}
}
@ -759,7 +774,7 @@ public final class PrivateCallScreen: OverlayMaskContainerView, AVPictureInPictu
}*/
let displayClose = false
let contentBottomInset = self.buttonGroupView.update(size: params.size, insets: params.insets, minWidth: wideContentWidth, controlsHidden: currentAreControlsHidden, displayClose: displayClose, buttons: buttons, notices: notices, transition: transition)
let contentBottomInset = self.buttonGroupView.update(size: params.size, insets: params.insets, minWidth: wideContentWidth, controlsHidden: currentAreControlsHidden, displayClose: displayClose, strings: params.state.strings, buttons: buttons, notices: notices, transition: transition)
var expandedEmojiKeyRect: CGRect?
if self.isEmojiKeyExpanded {
@ -777,7 +792,7 @@ public final class PrivateCallScreen: OverlayMaskContainerView, AVPictureInPictu
alphaTransition = genericAlphaTransition
}
emojiExpandedInfoView = EmojiExpandedInfoView(title: "This call is end-to-end encrypted", text: "If the emoji on \(params.state.shortName)'s screen are the same, this call is 100% secure.")
emojiExpandedInfoView = EmojiExpandedInfoView(title: params.state.strings.Call_EncryptedAlertTitle, text: params.state.strings.Call_EncryptedAlertText(params.state.shortName).string)
self.emojiExpandedInfoView = emojiExpandedInfoView
emojiExpandedInfoView.alpha = 0.0
Transition.immediate.setScale(view: emojiExpandedInfoView, scale: 0.5)
@ -824,13 +839,15 @@ public final class PrivateCallScreen: OverlayMaskContainerView, AVPictureInPictu
}
}
let backButtonSize = self.backButtonView.update(text: params.state.strings.Common_Back)
let backButtonY: CGFloat
if currentAreControlsHidden {
backButtonY = -self.backButtonView.size.height - 12.0
backButtonY = -backButtonSize.height - 12.0
} else {
backButtonY = params.insets.top + 12.0
}
let backButtonFrame = CGRect(origin: CGPoint(x: params.insets.left + 10.0, y: backButtonY), size: self.backButtonView.size)
let backButtonFrame = CGRect(origin: CGPoint(x: params.insets.left + 10.0, y: backButtonY), size: backButtonSize)
transition.setFrame(view: self.backButtonView, frame: backButtonFrame)
transition.setAlpha(view: self.backButtonView, alpha: currentAreControlsHidden ? 0.0 : 1.0)
@ -914,7 +931,7 @@ public final class PrivateCallScreen: OverlayMaskContainerView, AVPictureInPictu
emojiTooltipView = current
} else {
emojiTooltipTransition = emojiTooltipTransition.withAnimation(.none)
emojiTooltipView = EmojiTooltipView(text: "Encryption key of this call")
emojiTooltipView = EmojiTooltipView(text: params.state.strings.Call_EncryptionKeyTooltip)
animateIn = true
self.emojiTooltipView = emojiTooltipView
self.addSubview(emojiTooltipView)
@ -1192,15 +1209,15 @@ public final class PrivateCallScreen: OverlayMaskContainerView, AVPictureInPictu
switch terminatedState.reason {
case .busy:
titleString = "Line Busy"
titleString = params.state.strings.Call_StatusBusy
case .declined:
titleString = "Call Declined"
titleString = params.state.strings.Call_StatusDeclined
case .failed:
titleString = "Call Failed"
titleString = params.state.strings.Call_StatusFailed
case .hangUp:
titleString = "Call Ended"
titleString = params.state.strings.Call_StatusEnded
case .missed:
titleString = "Call Missed"
titleString = params.state.strings.Call_StatusMissed
}
default:
displayAudioLevelBlob = !params.state.isRemoteAudioMuted
@ -1358,5 +1375,75 @@ public final class PrivateCallScreen: OverlayMaskContainerView, AVPictureInPictu
})
}
}
/*if params.state.displaySnowEffect {
let snowEffectView: SnowEffectView
if let current = self.snowEffectView {
snowEffectView = current
} else {
snowEffectView = SnowEffectView(frame: CGRect())
self.snowEffectView = snowEffectView
self.maskContents.addSubview(snowEffectView)
}
transition.setFrame(view: snowEffectView, frame: CGRect(origin: CGPoint(), size: params.size))
snowEffectView.update(size: params.size)
} else {
if let snowEffectView = self.snowEffectView {
self.snowEffectView = nil
snowEffectView.removeFromSuperview()
}
}*/
}
}
final class SnowEffectView: UIView {
private let particlesLayer: CAEmitterLayer
override init(frame: CGRect) {
let particlesLayer = CAEmitterLayer()
self.particlesLayer = particlesLayer
self.particlesLayer.backgroundColor = nil
self.particlesLayer.isOpaque = false
particlesLayer.emitterShape = .circle
particlesLayer.emitterMode = .surface
particlesLayer.renderMode = .oldestLast
let image1 = UIImage(named: "Call/Snow")?.cgImage
let cell1 = CAEmitterCell()
cell1.contents = image1
cell1.name = "Snow"
cell1.birthRate = 92.0
cell1.lifetime = 20.0
cell1.velocity = 59.0
cell1.velocityRange = -15.0
cell1.xAcceleration = 5.0
cell1.yAcceleration = 40.0
cell1.emissionRange = 90.0 * (.pi / 180.0)
cell1.spin = -28.6 * (.pi / 180.0)
cell1.spinRange = 57.2 * (.pi / 180.0)
cell1.scale = 0.06
cell1.scaleRange = 0.3
cell1.color = UIColor(red: 255.0/255.0, green: 255.0/255.0, blue: 255.0/255.0, alpha: 1.0).cgColor
particlesLayer.emitterCells = [cell1]
super.init(frame: frame)
self.layer.addSublayer(particlesLayer)
self.clipsToBounds = true
self.backgroundColor = nil
self.isOpaque = false
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func update(size: CGSize) {
self.particlesLayer.frame = CGRect(x: 0.0, y: 0.0, width: size.width, height: size.height)
self.particlesLayer.emitterSize = CGSize(width: size.width * 3.0, height: size.height * 2.0)
self.particlesLayer.emitterPosition = CGPoint(x: size.width * 0.5, y: -325.0)
}
}

View File

@ -1045,6 +1045,7 @@ public class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
reactions: dateReactionsAndPeers.reactions,
reactionPeers: dateReactionsAndPeers.peers,
displayAllReactionPeers: item.message.id.peerId.namespace == Namespaces.Peer.CloudUser,
isSavedMessages: item.chatLocation.peerId == item.context.account.peerId,
replyCount: dateReplies,
isPinned: item.message.tags.contains(.pinned) && !item.associatedData.isInPinnedListMode && !isReplyThread,
hasAutoremove: item.message.isSelfExpiring,

View File

@ -657,6 +657,7 @@ public final class ChatMessageAttachedContentNode: ASDisplayNode {
reactions: dateReactionsAndPeers.reactions,
reactionPeers: dateReactionsAndPeers.peers,
displayAllReactionPeers: message.id.peerId.namespace == Namespaces.Peer.CloudUser,
isSavedMessages: chatLocation == .peer(id: context.account.peerId),
replyCount: dateReplies,
isPinned: message.tags.contains(.pinned) && !associatedData.isInPinnedListMode && !isReplyThread,
hasAutoremove: message.isSelfExpiring,

View File

@ -2113,6 +2113,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
reactions: dateReactionsAndPeers.reactions,
reactionPeers: dateReactionsAndPeers.peers,
displayAllReactionPeers: item.message.id.peerId.namespace == Namespaces.Peer.CloudUser,
isSavedMessages: item.chatLocation.peerId == item.context.account.peerId,
replyCount: dateReplies,
isPinned: message.tags.contains(.pinned) && !item.associatedData.isInPinnedListMode && !isReplyThread,
hasAutoremove: message.isSelfExpiring,

View File

@ -238,6 +238,7 @@ public class ChatMessageContactBubbleContentNode: ChatMessageBubbleContentNode {
reactions: dateReactionsAndPeers.reactions,
reactionPeers: dateReactionsAndPeers.peers,
displayAllReactionPeers: item.message.id.peerId.namespace == Namespaces.Peer.CloudUser,
isSavedMessages: item.chatLocation.peerId == item.context.account.peerId,
replyCount: dateReplies,
isPinned: item.message.tags.contains(.pinned) && !item.associatedData.isInPinnedListMode && isReplyThread,
hasAutoremove: item.message.isSelfExpiring,

View File

@ -228,6 +228,7 @@ public class ChatMessageDateAndStatusNode: ASDisplayNode {
var reactions: [MessageReaction]
var reactionPeers: [(MessageReaction.Reaction, EnginePeer)]
var displayAllReactionPeers: Bool
var isSavedMessages: Bool
var replyCount: Int
var isPinned: Bool
var hasAutoremove: Bool
@ -248,6 +249,7 @@ public class ChatMessageDateAndStatusNode: ASDisplayNode {
reactions: [MessageReaction],
reactionPeers: [(MessageReaction.Reaction, EnginePeer)],
displayAllReactionPeers: Bool,
isSavedMessages: Bool,
replyCount: Int,
isPinned: Bool,
hasAutoremove: Bool,
@ -267,6 +269,7 @@ public class ChatMessageDateAndStatusNode: ASDisplayNode {
self.reactions = reactions
self.reactionPeers = reactionPeers
self.displayAllReactionPeers = displayAllReactionPeers
self.isSavedMessages = isSavedMessages
self.replyCount = replyCount
self.isPinned = isPinned
self.hasAutoremove = hasAutoremove
@ -744,6 +747,7 @@ public class ChatMessageDateAndStatusNode: ASDisplayNode {
},
reactions: [],
colors: reactionColors,
isTag: arguments.isSavedMessages,
constrainedWidth: arguments.constrainedSize.width
)
case let .trailingContent(contentWidth, reactionSettings):
@ -805,6 +809,7 @@ public class ChatMessageDateAndStatusNode: ASDisplayNode {
)
},
colors: reactionColors,
isTag: arguments.isSavedMessages,
constrainedWidth: arguments.constrainedSize.width
)
} else {
@ -818,6 +823,7 @@ public class ChatMessageDateAndStatusNode: ASDisplayNode {
},
reactions: [],
colors: reactionColors,
isTag: arguments.isSavedMessages,
constrainedWidth: arguments.constrainedSize.width
)
}

View File

@ -519,6 +519,7 @@ public class ChatMessageGiveawayBubbleContentNode: ChatMessageBubbleContentNode,
reactions: dateReactionsAndPeers.reactions,
reactionPeers: dateReactionsAndPeers.peers,
displayAllReactionPeers: item.message.id.peerId.namespace == Namespaces.Peer.CloudUser,
isSavedMessages: item.chatLocation.peerId == item.context.account.peerId,
replyCount: dateReplies,
isPinned: item.message.tags.contains(.pinned) && !item.associatedData.isInPinnedListMode && isReplyThread,
hasAutoremove: item.message.isSelfExpiring,

View File

@ -937,6 +937,7 @@ public final class ChatMessageInteractiveFileNode: ASDisplayNode {
reactions: dateReactionsAndPeers.reactions,
reactionPeers: dateReactionsAndPeers.peers,
displayAllReactionPeers: arguments.message.id.peerId.namespace == Namespaces.Peer.CloudUser,
isSavedMessages: arguments.chatLocation == .peer(id: arguments.context.account.peerId),
replyCount: dateReplies,
isPinned: arguments.isPinned && !arguments.associatedData.isInPinnedListMode,
hasAutoremove: arguments.message.isSelfExpiring,

View File

@ -570,6 +570,7 @@ public class ChatMessageInteractiveInstantVideoNode: ASDisplayNode {
reactions: dateReactionsAndPeers.reactions,
reactionPeers: dateReactionsAndPeers.peers,
displayAllReactionPeers: item.message.id.peerId.namespace == Namespaces.Peer.CloudUser,
isSavedMessages: item.chatLocation.peerId == item.context.account.peerId,
replyCount: dateReplies,
isPinned: item.message.tags.contains(.pinned) && !item.associatedData.isInPinnedListMode && !isReplyThread,
hasAutoremove: item.message.isSelfExpiring,

View File

@ -872,6 +872,7 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr
reactions: dateAndStatus.dateReactions,
reactionPeers: dateAndStatus.dateReactionPeers,
displayAllReactionPeers: message.id.peerId.namespace == Namespaces.Peer.CloudUser,
isSavedMessages: message.id.peerId == context.account.peerId,
replyCount: dateAndStatus.dateReplies,
isPinned: dateAndStatus.isPinned,
hasAutoremove: message.isSelfExpiring,

View File

@ -276,6 +276,7 @@ public class ChatMessageMapBubbleContentNode: ChatMessageBubbleContentNode {
reactions: dateReactionsAndPeers.reactions,
reactionPeers: dateReactionsAndPeers.peers,
displayAllReactionPeers: item.message.id.peerId.namespace == Namespaces.Peer.CloudUser,
isSavedMessages: item.chatLocation.peerId == item.context.account.peerId,
replyCount: dateReplies,
isPinned: item.message.tags.contains(.pinned) && !item.associatedData.isInPinnedListMode && !isReplyThread,
hasAutoremove: item.message.isSelfExpiring,

View File

@ -1024,6 +1024,7 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode {
reactions: dateReactionsAndPeers.reactions,
reactionPeers: dateReactionsAndPeers.peers,
displayAllReactionPeers: item.message.id.peerId.namespace == Namespaces.Peer.CloudUser,
isSavedMessages: item.chatLocation.peerId == item.context.account.peerId,
replyCount: dateReplies,
isPinned: item.message.tags.contains(.pinned) && !item.associatedData.isInPinnedListMode && !isReplyThread,
hasAutoremove: item.message.isSelfExpiring,

View File

@ -178,6 +178,7 @@ public final class MessageReactionButtonsNode: ASDisplayNode {
)
},
colors: reactionColors,
isTag: message.id.peerId == context.account.peerId,
constrainedWidth: constrainedWidth
)

View File

@ -132,6 +132,7 @@ public class ChatMessageRestrictedBubbleContentNode: ChatMessageBubbleContentNod
reactions: dateReactionsAndPeers.reactions,
reactionPeers: dateReactionsAndPeers.peers,
displayAllReactionPeers: item.message.id.peerId.namespace == Namespaces.Peer.CloudUser,
isSavedMessages: item.chatLocation.peerId == item.context.account.peerId,
replyCount: dateReplies,
isPinned: item.message.tags.contains(.pinned) && !item.associatedData.isInPinnedListMode && isReplyThread,
hasAutoremove: item.message.isSelfExpiring,

View File

@ -628,6 +628,7 @@ public class ChatMessageStickerItemNode: ChatMessageItemView {
reactions: dateReactionsAndPeers.reactions,
reactionPeers: dateReactionsAndPeers.peers,
displayAllReactionPeers: item.message.id.peerId.namespace == Namespaces.Peer.CloudUser,
isSavedMessages: item.chatLocation.peerId == item.context.account.peerId,
replyCount: dateReplies,
isPinned: item.message.tags.contains(.pinned) && !item.associatedData.isInPinnedListMode && !isReplyThread,
hasAutoremove: item.message.isSelfExpiring,

View File

@ -573,6 +573,7 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
reactions: dateReactionsAndPeers.reactions,
reactionPeers: dateReactionsAndPeers.peers,
displayAllReactionPeers: item.message.id.peerId.namespace == Namespaces.Peer.CloudUser,
isSavedMessages: item.chatLocation.peerId == item.context.account.peerId,
replyCount: dateReplies,
isPinned: item.message.tags.contains(.pinned) && (!item.associatedData.isInPinnedListMode || isReplyThread),
hasAutoremove: item.message.isSelfExpiring,

View File

@ -31,21 +31,23 @@ public enum ChatTitleContent: Equatable {
public var peerId: PeerId
public var peer: Peer?
public var isContact: Bool
public var isSavedMessages: Bool
public var notificationSettings: TelegramPeerNotificationSettings?
public var peerPresences: [PeerId: PeerPresence]
public var cachedData: CachedPeerData?
public init(peerId: PeerId, peer: Peer?, isContact: Bool, notificationSettings: TelegramPeerNotificationSettings?, peerPresences: [PeerId: PeerPresence], cachedData: CachedPeerData?) {
public init(peerId: PeerId, peer: Peer?, isContact: Bool, isSavedMessages: Bool, notificationSettings: TelegramPeerNotificationSettings?, peerPresences: [PeerId: PeerPresence], cachedData: CachedPeerData?) {
self.peerId = peerId
self.peer = peer
self.isContact = isContact
self.isSavedMessages = isSavedMessages
self.notificationSettings = notificationSettings
self.peerPresences = peerPresences
self.cachedData = cachedData
}
public init(peerView: PeerView) {
self.init(peerId: peerView.peerId, peer: peerViewMainPeer(peerView), isContact: peerView.peerIsContact, notificationSettings: peerView.notificationSettings as? TelegramPeerNotificationSettings, peerPresences: peerView.peerPresences, cachedData: peerView.cachedData)
self.init(peerId: peerView.peerId, peer: peerViewMainPeer(peerView), isContact: peerView.peerIsContact, isSavedMessages: false, notificationSettings: peerView.notificationSettings as? TelegramPeerNotificationSettings, peerPresences: peerView.peerPresences, cachedData: peerView.cachedData)
}
public static func ==(lhs: PeerData, rhs: PeerData) -> Bool {
@ -59,6 +61,9 @@ public enum ChatTitleContent: Equatable {
if lhs.isContact != rhs.isContact {
return false
}
if lhs.isSavedMessages != rhs.isSavedMessages {
return false
}
if lhs.notificationSettings != rhs.notificationSettings {
return false
}
@ -246,7 +251,12 @@ public final class ChatTitleView: UIView, NavigationBarTitleView {
if let customTitle = customTitle {
segments = [.text(0, NSAttributedString(string: customTitle, font: titleFont, textColor: titleTheme.rootController.navigationBar.primaryTextColor))]
} else if peerView.peerId == self.context.account.peerId {
segments = [.text(0, NSAttributedString(string: self.strings.Conversation_SavedMessages, font: titleFont, textColor: titleTheme.rootController.navigationBar.primaryTextColor))]
if peerView.isSavedMessages {
//TODO:localize
segments = [.text(0, NSAttributedString(string: "My Notes", font: titleFont, textColor: titleTheme.rootController.navigationBar.primaryTextColor))]
} else {
segments = [.text(0, NSAttributedString(string: self.strings.Conversation_SavedMessages, font: titleFont, textColor: titleTheme.rootController.navigationBar.primaryTextColor))]
}
} else if peerView.peerId.isAnonymousSavedMessages {
segments = [.text(0, NSAttributedString(string: self.strings.ChatList_AuthorHidden, font: titleFont, textColor: titleTheme.rootController.navigationBar.primaryTextColor))]
} else {

View File

@ -13,6 +13,7 @@ public final class PlainButtonComponent: Component {
public let content: AnyComponent<Empty>
public let effectAlignment: EffectAlignment
public let minSize: CGSize?
public let contentInsets: UIEdgeInsets
public let action: () -> Void
public let isEnabled: Bool
@ -20,12 +21,14 @@ public final class PlainButtonComponent: Component {
content: AnyComponent<Empty>,
effectAlignment: EffectAlignment,
minSize: CGSize? = nil,
contentInsets: UIEdgeInsets = UIEdgeInsets(),
action: @escaping () -> Void,
isEnabled: Bool = true
) {
self.content = content
self.effectAlignment = effectAlignment
self.minSize = minSize
self.contentInsets = contentInsets
self.action = action
self.isEnabled = isEnabled
}
@ -40,6 +43,9 @@ public final class PlainButtonComponent: Component {
if lhs.minSize != rhs.minSize {
return false
}
if lhs.contentInsets != rhs.contentInsets {
return false
}
if lhs.isEnabled != rhs.isEnabled {
return false
}
@ -143,6 +149,8 @@ public final class PlainButtonComponent: Component {
size.width = max(size.width, minSize.width)
size.height = max(size.height, minSize.height)
}
size.width += component.contentInsets.left + component.contentInsets.right
size.height += component.contentInsets.top + component.contentInsets.bottom
if let contentView = self.content.view {
var contentTransition = transition
@ -151,7 +159,7 @@ public final class PlainButtonComponent: Component {
contentView.isUserInteractionEnabled = false
self.contentContainer.addSubview(contentView)
}
let contentFrame = CGRect(origin: CGPoint(x: floor((size.width - contentSize.width) * 0.5), y: floor((size.height - contentSize.height) * 0.5)), size: contentSize)
let contentFrame = CGRect(origin: CGPoint(x: component.contentInsets.left + floor((size.width - component.contentInsets.left - component.contentInsets.right - contentSize.width) * 0.5), y: component.contentInsets.top + floor((size.height - component.contentInsets.top - component.contentInsets.bottom - contentSize.height) * 0.5)), size: contentSize)
contentTransition.setFrame(view: contentView, frame: contentFrame)
contentTransition.setAlpha(view: contentView, alpha: contentAlpha)

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "mynotes.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,131 @@
%PDF-1.7
1 0 obj
<< >>
endobj
2 0 obj
<< /Length 3 0 R >>
stream
/DeviceRGB CS
/DeviceRGB cs
q
1.000000 0.000000 -0.000000 1.000000 14.110840 14.817200 cm
0.000000 0.000000 0.000000 scn
0.348888 21.606785 m
0.014841 23.983654 -0.152183 25.172089 0.182797 26.144941 c
0.477454 27.000687 1.035458 27.741184 1.776835 28.260302 c
2.619668 28.850460 3.808101 29.017483 6.184968 29.351530 c
15.946176 30.723377 l
18.323046 31.057425 19.511480 31.224449 20.484333 30.889469 c
21.340078 30.594812 22.080576 30.036808 22.599693 29.295431 c
23.167820 28.484060 23.343815 27.352407 23.654181 25.148548 c
14.146844 25.148548 l
14.146827 25.148548 l
12.996770 25.148567 12.047814 25.148581 11.274830 25.085426 c
10.472054 25.019836 9.734429 24.879089 9.041680 24.526117 c
7.967140 23.978611 7.093512 23.104984 6.546007 22.030443 c
6.193034 21.337694 6.052288 20.600069 5.986698 19.797295 c
5.923542 19.024311 5.923557 18.075354 5.923575 16.925297 c
5.923575 16.925280 l
5.923575 6.950388 l
5.923575 6.950371 l
5.923568 6.490812 5.923562 6.063360 5.927587 5.666420 c
5.553736 5.683525 5.230090 5.739155 4.927324 5.843405 c
4.071579 6.138062 3.331082 6.696066 2.811965 7.437443 c
2.221807 8.280275 2.054783 9.468710 1.720736 11.845575 c
0.348888 21.606785 l
h
7.348551 16.866423 m
7.348551 19.266651 7.348551 20.466766 7.815666 21.383530 c
8.226552 22.189939 8.882182 22.845570 9.688591 23.256456 c
10.605356 23.723572 11.805469 23.723572 14.205694 23.723572 c
24.062830 23.723572 l
26.463058 23.723572 27.663174 23.723572 28.579939 23.256456 c
29.386347 22.845570 30.041979 22.189939 30.452864 21.383530 c
30.919979 20.466766 30.919979 19.266653 30.919979 16.866428 c
30.919979 7.009293 l
30.919979 4.609062 30.919979 3.408947 30.452864 2.492184 c
30.041979 1.685776 29.386347 1.030144 28.579939 0.619259 c
27.663174 0.152142 26.463060 0.152142 24.062836 0.152142 c
14.205700 0.152142 l
11.805470 0.152142 10.605356 0.152142 9.688591 0.619259 c
8.882182 1.030144 8.226552 1.685776 7.815666 2.492184 c
7.348551 3.408947 7.348551 4.609062 7.348551 7.009285 c
7.348551 16.866423 l
h
12.706041 17.294989 m
12.706041 17.886723 13.185737 18.366417 13.777471 18.366417 c
24.491756 18.366417 l
25.083490 18.366417 25.563185 17.886723 25.563185 17.294989 c
25.563185 16.703255 25.083488 16.223560 24.491756 16.223560 c
13.777470 16.223560 l
13.185736 16.223560 12.706041 16.703255 12.706041 17.294989 c
h
12.706041 11.937845 m
12.706041 12.529579 13.185737 13.009274 13.777471 13.009274 c
24.491756 13.009274 l
25.083490 13.009274 25.563185 12.529579 25.563185 11.937845 c
25.563185 11.346111 25.083488 10.866417 24.491756 10.866417 c
13.777470 10.866417 l
13.185736 10.866417 12.706041 11.346111 12.706041 11.937845 c
h
13.777471 7.652130 m
13.185737 7.652130 12.706041 7.172436 12.706041 6.580704 c
12.706041 5.988968 13.185737 5.509274 13.777471 5.509274 c
21.277472 5.509274 l
21.869204 5.509274 22.348900 5.988968 22.348900 6.580704 c
22.348900 7.172436 21.869204 7.652130 21.277472 7.652130 c
13.777471 7.652130 l
h
f*
n
Q
endstream
endobj
3 0 obj
2941
endobj
4 0 obj
<< /Annots []
/Type /Page
/MediaBox [ 0.000000 0.000000 60.000000 60.000000 ]
/Resources 1 0 R
/Contents 2 0 R
/Parent 5 0 R
>>
endobj
5 0 obj
<< /Kids [ 4 0 R ]
/Count 1
/Type /Pages
>>
endobj
6 0 obj
<< /Pages 5 0 R
/Type /Catalog
>>
endobj
xref
0 7
0000000000 65535 f
0000000010 00000 n
0000000034 00000 n
0000003031 00000 n
0000003054 00000 n
0000003227 00000 n
0000003301 00000 n
trailer
<< /ID [ (some) (id) ]
/Root 6 0 R
/Size 7
>>
startxref
3360
%%EOF

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "MessageTagBackground.svg",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,3 @@
<svg width="23" height="30" viewBox="0 0 23 30" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0 6C0 2.68629 2.68629 0 6 0H8.155C10.5853 0 12.8838 1.10472 14.402 3.00244L22.001 12.5012C23.1697 13.9621 23.1697 16.0379 22.001 17.4988L14.402 26.9976C12.8838 28.8953 10.5853 30 8.155 30H6C2.68629 30 0 27.3137 0 24V6Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 348 B

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "SearchTagTokenBackground.svg",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,4 @@
<svg width="30" height="26" viewBox="0 0 30 26" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M6 0C2.68628 0 0 2.68628 0 6V20.0024C0 23.316 2.68628 26.0024 6 26.0024H15.155C17.5853 26.0024 19.8838 24.8977 21.402 23L29.001 15.5012C30.1697 14.0403 30.1697 11.9645 29.001 10.5035L21.402 3.00244C19.8838 1.10474 17.5853 0 15.155 0H6ZM18 16C19.6569 16 21 14.6569 21 13C21 11.3431 19.6569 10 18 10C16.3431 10 15 11.3431 15 13C15 14.6569 16.3431 16 18 16Z" fill="white"/>
<circle cx="18" cy="13" r="3" fill="white" fill-opacity="0.5"/>
</svg>

After

Width:  |  Height:  |  Size: 587 B

View File

@ -110,6 +110,10 @@ extension ChatControllerImpl {
if canAddMessageReactions(message: topMessage), let allowedReactions = allowedReactions, !topReactions.isEmpty {
actions.reactionItems = topReactions.map(ReactionContextItem.reaction)
if case .peer(self.context.account.peerId) = self.presentationInterfaceState.chatLocation {
//TODO:localize
actions.reactionsTitle = "Tag the message with an emoji for quick access later"
}
actions.selectedReactionItems = selectedReactions.reactions
if let channel = self.presentationInterfaceState.renderedPeer?.peer as? TelegramChannel, case .broadcast = channel.info {

View File

@ -5920,6 +5920,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
peerId: savedMessagesPeerId,
peer: savedMessagesPeer?.peer?._asPeer(),
isContact: true,
isSavedMessages: true,
notificationSettings: nil,
peerPresences: [:],
cachedData: nil
@ -5930,7 +5931,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
let imageOverride: AvatarNodeImageOverride?
if strongSelf.context.account.peerId == savedMessagesPeerId {
imageOverride = .savedMessagesIcon
imageOverride = .myNotesIcon
} else if savedMessagesPeerId.isReplies {
imageOverride = .repliesIcon
} else if savedMessagesPeerId.isAnonymousSavedMessages {
@ -9274,12 +9275,14 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
return state.updatedSearch(ChatSearchData(query: "", domain: .members, domainSuggestionContext: .none, resultsState: nil))
} else if let search = state.search {
switch search.domain {
case .everything:
return state
case .members:
return state.updatedSearch(ChatSearchData(query: "", domain: .everything, domainSuggestionContext: .none, resultsState: nil))
case .member:
return state.updatedSearch(ChatSearchData(query: "", domain: .members, domainSuggestionContext: .none, resultsState: nil))
case .everything:
return state
case .tag:
return state.updatedSearch(ChatSearchData(query: "", domain: .everything, domainSuggestionContext: .none, resultsState: nil))
case .members:
return state.updatedSearch(ChatSearchData(query: "", domain: .everything, domainSuggestionContext: .none, resultsState: nil))
case .member:
return state.updatedSearch(ChatSearchData(query: "", domain: .members, domainSuggestionContext: .none, resultsState: nil))
}
} else {
return state
@ -15792,12 +15795,14 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
break
}
switch search.domain {
case .everything:
derivedSearchState = ChatSearchState(query: search.query, location: .peer(peerId: peerId, fromId: nil, tags: nil, threadId: threadId, minDate: nil, maxDate: nil), loadMoreState: loadMoreStateFromResultsState(search.resultsState))
case .members:
derivedSearchState = nil
case let .member(peer):
derivedSearchState = ChatSearchState(query: search.query, location: .peer(peerId: peerId, fromId: peer.id, tags: nil, threadId: threadId, minDate: nil, maxDate: nil), loadMoreState: loadMoreStateFromResultsState(search.resultsState))
case .everything:
derivedSearchState = ChatSearchState(query: search.query, location: .peer(peerId: peerId, fromId: nil, tags: nil, threadId: threadId, minDate: nil, maxDate: nil), loadMoreState: loadMoreStateFromResultsState(search.resultsState))
case .members:
derivedSearchState = nil
case let .member(peer):
derivedSearchState = ChatSearchState(query: search.query, location: .peer(peerId: peerId, fromId: peer.id, tags: nil, threadId: threadId, minDate: nil, maxDate: nil), loadMoreState: loadMoreStateFromResultsState(search.resultsState))
case let .tag(tag):
derivedSearchState = ChatSearchState(query: "@#\(tag) " + search.query, location: .peer(peerId: peerId, fromId: nil, tags: nil, threadId: threadId, minDate: nil, maxDate: nil), loadMoreState: loadMoreStateFromResultsState(search.resultsState))
}
}

View File

@ -2655,7 +2655,7 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate {
var activate = false
if self.searchNavigationNode == nil {
activate = true
self.searchNavigationNode = ChatSearchNavigationContentNode(theme: self.chatPresentationInterfaceState.theme, strings: self.chatPresentationInterfaceState.strings, chatLocation: self.chatPresentationInterfaceState.chatLocation, interaction: interfaceInteraction)
self.searchNavigationNode = ChatSearchNavigationContentNode(context: self.context, theme: self.chatPresentationInterfaceState.theme, strings: self.chatPresentationInterfaceState.strings, chatLocation: self.chatPresentationInterfaceState.chatLocation, interaction: interfaceInteraction)
}
self.navigationBar?.setContentNode(self.searchNavigationNode, animated: transitionIsAnimated)
self.searchNavigationNode?.update(presentationInterfaceState: self.chatPresentationInterfaceState)

View File

@ -12,8 +12,18 @@ func titlePanelForChatPresentationInterfaceState(_ chatPresentationInterfaceStat
if chatPresentationInterfaceState.renderedPeer?.peer?.restrictionText(platform: "ios", contentSettings: context.currentContentSettings.with { $0 }) != nil {
return nil
}
if chatPresentationInterfaceState.search != nil {
return nil
if let search = chatPresentationInterfaceState.search {
if chatPresentationInterfaceState.chatLocation.peerId == context.account.peerId, case .everything = search.domain {
if let currentPanel = currentPanel as? ChatSearchTitleAccessoryPanelNode {
return currentPanel
} else {
let panel = ChatSearchTitleAccessoryPanelNode(context: context)
panel.interfaceInteraction = interfaceInteraction
return panel
}
} else {
return nil
}
}
var inhibitTitlePanelDisplay = false

View File

@ -14,6 +14,7 @@ import ChatPresentationInterfaceState
private let searchBarFont = Font.regular(17.0)
final class ChatSearchNavigationContentNode: NavigationBarContentNode {
private let context: AccountContext
private let theme: PresentationTheme
private let strings: PresentationStrings
private let chatLocation: ChatLocation
@ -23,7 +24,8 @@ final class ChatSearchNavigationContentNode: NavigationBarContentNode {
private var searchingActivityDisposable: Disposable?
init(theme: PresentationTheme, strings: PresentationStrings, chatLocation: ChatLocation, interaction: ChatPanelInterfaceInteraction) {
init(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, chatLocation: ChatLocation, interaction: ChatPanelInterfaceInteraction) {
self.context = context
self.theme = theme
self.strings = strings
self.chatLocation = chatLocation
@ -33,7 +35,12 @@ final class ChatSearchNavigationContentNode: NavigationBarContentNode {
let placeholderText: String
switch chatLocation {
case .peer, .replyThread, .feed:
placeholderText = strings.Conversation_SearchPlaceholder
if chatLocation.peerId == context.account.peerId {
//TODO:localize
placeholderText = "Search messages or tags"
} else {
placeholderText = strings.Conversation_SearchPlaceholder
}
}
self.searchBar.placeholderString = NSAttributedString(string: placeholderText, font: searchBarFont, textColor: theme.rootController.navigationSearchBar.inputPlaceholderTextColor)
@ -99,23 +106,34 @@ final class ChatSearchNavigationContentNode: NavigationBarContentNode {
self.searchBar.updateThemeAndStrings(theme: SearchBarNodeTheme(theme: presentationInterfaceState.theme, hasBackground: false, hasSeparator: false), strings: presentationInterfaceState.strings)
switch search.domain {
case .everything:
self.searchBar.tokens = []
self.searchBar.prefixString = nil
let placeholderText: String
switch self.chatLocation {
case .peer, .replyThread, .feed:
case .everything:
self.searchBar.tokens = []
self.searchBar.prefixString = nil
let placeholderText: String
switch self.chatLocation {
case .peer, .replyThread, .feed:
if self.chatLocation.peerId == self.context.account.peerId {
//TODO:localize
placeholderText = "Search messages or tags"
} else {
placeholderText = self.strings.Conversation_SearchPlaceholder
}
self.searchBar.placeholderString = NSAttributedString(string: placeholderText, font: searchBarFont, textColor: theme.rootController.navigationSearchBar.inputPlaceholderTextColor)
case .members:
self.searchBar.tokens = []
self.searchBar.prefixString = NSAttributedString(string: strings.Conversation_SearchByName_Prefix, font: searchBarFont, textColor: theme.rootController.navigationSearchBar.inputTextColor)
self.searchBar.placeholderString = nil
case let .member(peer):
self.searchBar.tokens = [SearchBarToken(id: peer.id, icon: UIImage(bundleImageName: "Chat List/Search/User"), title: EnginePeer(peer).compactDisplayTitle, permanent: false)]
self.searchBar.prefixString = nil
self.searchBar.placeholderString = nil
}
self.searchBar.placeholderString = NSAttributedString(string: placeholderText, font: searchBarFont, textColor: theme.rootController.navigationSearchBar.inputPlaceholderTextColor)
case let .tag(tag):
//TODO:localize
let placeholderText = "Search"
self.searchBar.placeholderString = NSAttributedString(string: placeholderText, font: searchBarFont, textColor: theme.rootController.navigationSearchBar.inputPlaceholderTextColor)
//TODO:localize
self.searchBar.tokens = [SearchBarToken(id: AnyHashable(tag), icon: nil, isTag: true, title: "\(tag) Tag", permanent: false)]
case .members:
self.searchBar.tokens = []
self.searchBar.prefixString = NSAttributedString(string: strings.Conversation_SearchByName_Prefix, font: searchBarFont, textColor: theme.rootController.navigationSearchBar.inputTextColor)
self.searchBar.placeholderString = nil
case let .member(peer):
self.searchBar.tokens = [SearchBarToken(id: peer.id, icon: UIImage(bundleImageName: "Chat List/Search/User"), title: EnginePeer(peer).compactDisplayTitle, permanent: false)]
self.searchBar.prefixString = nil
self.searchBar.placeholderString = nil
}
if self.searchBar.text != search.query {

View File

@ -76,13 +76,13 @@ private enum ChatListSearchEntry: Comparable, Identifiable {
return false
}
public func item(context: AccountContext, interaction: ChatListNodeInteraction) -> ListViewItem {
public func item(context: AccountContext, interaction: ChatListNodeInteraction, location: ChatListControllerLocation) -> ListViewItem {
switch self {
case let .message(message, peer, readState, presentationData):
return ChatListItem(
presentationData: presentationData,
context: context,
chatListLocation: .chatList(groupId: .root),
chatListLocation: location,
filterData: nil,
index: .chatList(EngineChatList.Item.Index.ChatList(pinningIndex: nil, messageIndex: message.index)),
content: .peer(ChatListItemContent.PeerData(
@ -129,12 +129,12 @@ public struct ChatListSearchContainerTransition {
}
}
private func chatListSearchContainerPreparedTransition(from fromEntries: [ChatListSearchEntry], to toEntries: [ChatListSearchEntry], context: AccountContext, interaction: ChatListNodeInteraction) -> ChatListSearchContainerTransition {
private func chatListSearchContainerPreparedTransition(from fromEntries: [ChatListSearchEntry], to toEntries: [ChatListSearchEntry], context: AccountContext, interaction: ChatListNodeInteraction, location: ChatListControllerLocation) -> ChatListSearchContainerTransition {
let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries)
let deletions = deleteIndices.map { ListViewDeleteItem(index: $0, directionHint: nil) }
let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, interaction: interaction), directionHint: nil) }
let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, interaction: interaction), directionHint: nil) }
let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, interaction: interaction, location: location), directionHint: nil) }
let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, interaction: interaction, location: location), directionHint: nil) }
return ChatListSearchContainerTransition(deletions: deletions, insertions: insertions, updates: updates)
}
@ -148,6 +148,7 @@ class ChatSearchResultsControllerNode: ViewControllerTracingNode, UIScrollViewDe
private let searchQuery: String
private var searchResult: SearchMessagesResult
private var searchState: SearchMessagesState
private let mappedLocation: ChatListControllerLocation
private var interaction: ChatListNodeInteraction?
@ -173,6 +174,12 @@ class ChatSearchResultsControllerNode: ViewControllerTracingNode, UIScrollViewDe
self.searchQuery = searchQuery
self.searchResult = searchResult
self.searchState = searchState
if case let .peer(peerId, _, _, _, _, _) = location, peerId == context.account.peerId {
self.mappedLocation = .savedMessagesChats
} else {
self.mappedLocation = .chatList(groupId: .root)
}
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
self.presentationData = presentationData
@ -283,7 +290,7 @@ class ChatSearchResultsControllerNode: ViewControllerTracingNode, UIScrollViewDe
let previousEntries = strongSelf.previousEntries.swap(entries)
let firstTime = previousEntries == nil
let transition = chatListSearchContainerPreparedTransition(from: previousEntries ?? [], to: entries, context: context, interaction: interaction)
let transition = chatListSearchContainerPreparedTransition(from: previousEntries ?? [], to: entries, context: context, interaction: interaction, location: strongSelf.mappedLocation)
strongSelf.enqueueTransition(transition, firstTime: firstTime)
}
}))
@ -352,7 +359,7 @@ class ChatSearchResultsControllerNode: ViewControllerTracingNode, UIScrollViewDe
let previousEntries = strongSelf.previousEntries.swap(entries)
let firstTime = previousEntries == nil
let transition = chatListSearchContainerPreparedTransition(from: previousEntries ?? [], to: entries, context: context, interaction: interaction)
let transition = chatListSearchContainerPreparedTransition(from: previousEntries ?? [], to: entries, context: context, interaction: interaction, location: strongSelf.mappedLocation)
strongSelf.enqueueTransition(transition, firstTime: firstTime)
}
}))

View File

@ -0,0 +1,182 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import TelegramPresentationData
import ChatPresentationInterfaceState
import AccountContext
import ComponentFlow
import MultilineTextComponent
import PlainButtonComponent
import UIKitRuntimeUtils
final class ChatSearchTitleAccessoryPanelNode: ChatTitleAccessoryPanelNode, UIScrollViewDelegate {
private final class Item {
let tag: String
init(tag: String) {
self.tag = tag
}
}
private final class ItemView: UIView {
private let context: AccountContext
private let item: Item
private let action: (String) -> Void
private let view = ComponentView<Empty>()
init(context: AccountContext, item: Item, action: @escaping ((String) -> Void)) {
self.context = context
self.item = item
self.action = action
super.init(frame: CGRect())
}
required init?(coder: NSCoder) {
preconditionFailure()
}
func update(theme: PresentationTheme, height: CGFloat, transition: Transition) -> CGSize {
let viewSize = self.view.update(
transition: transition,
component: AnyComponent(PlainButtonComponent(
content: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(string: self.item.tag, font: Font.regular(15.0), textColor: theme.rootController.navigationBar.primaryTextColor)),
insets: UIEdgeInsets(top: 2.0, left: 2.0, bottom: 2.0, right: 2.0)
)),
effectAlignment: .center,
minSize: CGSize(width: 0.0, height: height),
contentInsets: UIEdgeInsets(top: 0.0, left: 8.0, bottom: 0.0, right: 8.0),
action: { [weak self] in
guard let self else {
return
}
self.action(self.item.tag)
},
isEnabled: true
)),
environment: {},
containerSize: CGSize(width: 100.0, height: 100.0)
)
if let componentView = self.view.view {
if componentView.superview == nil {
self.addSubview(componentView)
}
transition.setFrame(view: componentView, frame: CGRect(origin: CGPoint(), size: viewSize))
}
return viewSize
}
}
private final class ScrollView: UIScrollView {
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
return super.hitTest(point, with: event)
}
override func touchesShouldCancel(in view: UIView) -> Bool {
return true
}
}
private let context: AccountContext
private var theme: PresentationTheme?
private var strings: PresentationStrings?
private let scrollView: ScrollView
private let itemViews: [ItemView]
init(context: AccountContext) {
self.context = context
self.scrollView = ScrollView(frame: CGRect())
let tags: [String] = [
"⭐️", "❤️", "", "", "💭", "❗️", "👍", "👎", "🤩", "⚡️", "🤡", "👌", "👏"
]
let items = tags.map {
Item(tag: $0)
}
var itemAction: ((String) -> Void)?
self.itemViews = items.map { item in
return ItemView(context: context, item: item, action: { tag in
itemAction?(tag)
})
}
super.init()
self.scrollView.delaysContentTouches = false
self.scrollView.canCancelContentTouches = true
self.scrollView.clipsToBounds = false
self.scrollView.contentInsetAdjustmentBehavior = .never
if #available(iOS 13.0, *) {
self.scrollView.automaticallyAdjustsScrollIndicatorInsets = false
}
self.scrollView.showsVerticalScrollIndicator = false
self.scrollView.showsHorizontalScrollIndicator = false
self.scrollView.alwaysBounceHorizontal = false
self.scrollView.alwaysBounceVertical = false
self.scrollView.scrollsToTop = false
self.scrollView.delegate = self
self.view.addSubview(self.scrollView)
self.scrollView.disablesInteractiveTransitionGestureRecognizer = true
for itemView in self.itemViews {
self.scrollView.addSubview(itemView)
}
itemAction = { [weak self] tag in
guard let self, let interfaceInteraction = self.interfaceInteraction else {
return
}
interfaceInteraction.beginMessageSearch(.tag(tag), "")
}
}
override func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState) -> LayoutResult {
if interfaceState.strings !== self.strings {
self.strings = interfaceState.strings
}
if interfaceState.theme !== self.theme {
self.theme = interfaceState.theme
}
let panelHeight: CGFloat = 33.0
let containerInsets = UIEdgeInsets(top: 0.0, left: leftInset + 2.0, bottom: 0.0, right: rightInset + 2.0)
let itemSpacing: CGFloat = 2.0
var contentSize = CGSize(width: 0.0, height: panelHeight)
contentSize.width += containerInsets.left
var isFirst = true
for itemView in self.itemViews {
if isFirst {
isFirst = false
} else {
contentSize.width += itemSpacing
}
let itemSize = itemView.update(theme: interfaceState.theme, height: panelHeight, transition: .immediate)
itemView.frame = CGRect(origin: CGPoint(x: contentSize.width, y: 0.0), size: itemSize)
contentSize.width += itemSize.width
}
contentSize.width += containerInsets.right
let scrollSize = CGSize(width: width, height: contentSize.height)
if self.scrollView.bounds.size != scrollSize {
self.scrollView.frame = CGRect(origin: CGPoint(x: 0.0, y: -5.0), size: scrollSize)
}
if self.scrollView.contentSize != contentSize {
self.scrollView.contentSize = contentSize
}
return LayoutResult(backgroundHeight: panelHeight, insetHeight: panelHeight, hitTestSlop: 0.0)
}
}