diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index a2669e9ca9..a1077e05a6 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -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"; @@ -10868,5 +10870,14 @@ Sorry for the inconvenience."; "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"; + "Conversation.ContactAddContact" = "ADD"; "Conversation.ContactMessage" = "MESSAGE"; diff --git a/Tests/CallUITest/BUILD b/Tests/CallUITest/BUILD index a809c0ad08..7cb591d917 100644 --- a/Tests/CallUITest/BUILD +++ b/Tests/CallUITest/BUILD @@ -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" - ) diff --git a/Tests/CallUITest/Sources/ViewController.swift b/Tests/CallUITest/Sources/ViewController.swift index 7641bd93e7..3178bdc66b 100644 --- a/Tests/CallUITest/Sources/ViewController.swift +++ b/Tests/CallUITest/Sources/ViewController.swift @@ -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", diff --git a/submodules/AccountContext/Sources/AccountContext.swift b/submodules/AccountContext/Sources/AccountContext.swift index 44762593d8..a3f413e354 100644 --- a/submodules/AccountContext/Sources/AccountContext.swift +++ b/submodules/AccountContext/Sources/AccountContext.swift @@ -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 + } } } } diff --git a/submodules/AvatarNode/Sources/AvatarNode.swift b/submodules/AvatarNode/Sources/AvatarNode.swift index 1f2b11790c..b9428f29a9 100644 --- a/submodules/AvatarNode/Sources/AvatarNode.swift +++ b/submodules/AvatarNode/Sources/AvatarNode.swift @@ -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) diff --git a/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift b/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift index 16c58d9e39..9911b6671e 100644 --- a/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift +++ b/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift @@ -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: []) diff --git a/submodules/ChatListUI/Sources/Node/ChatListItem.swift b/submodules/ChatListUI/Sources/Node/ChatListItem.swift index 073dc6b043..77d1077e14 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListItem.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListItem.swift @@ -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)) diff --git a/submodules/ChatListUI/Sources/Node/ChatListNode.swift b/submodules/ChatListUI/Sources/Node/ChatListNode.swift index 9c74232736..16dff19e41 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListNode.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListNode.swift @@ -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, diff --git a/submodules/Components/ReactionButtonListComponent/BUILD b/submodules/Components/ReactionButtonListComponent/BUILD index 93fc5e91c1..f8167464d8 100644 --- a/submodules/Components/ReactionButtonListComponent/BUILD +++ b/submodules/Components/ReactionButtonListComponent/BUILD @@ -24,6 +24,7 @@ swift_library( "//submodules/TelegramUI/Components/AnimationCache:AnimationCache", "//submodules/TelegramUI/Components/MultiAnimationRenderer:MultiAnimationRenderer", "//submodules/TextFormat:TextFormat", + "//submodules/AppBundle", ], visibility = [ "//visibility:public", diff --git a/submodules/Components/ReactionButtonListComponent/Sources/ReactionButtonListComponent.swift b/submodules/Components/ReactionButtonListComponent/Sources/ReactionButtonListComponent.swift index f01e91d457..64f9946729 100644 --- a/submodules/Components/ReactionButtonListComponent/Sources/ReactionButtonListComponent.swift +++ b/submodules/Components/ReactionButtonListComponent/Sources/ReactionButtonListComponent.swift @@ -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 )) diff --git a/submodules/ContactListUI/Sources/ContactListNode.swift b/submodules/ContactListUI/Sources/ContactListNode.swift index ed1c0318e3..44bd193ef4 100644 --- a/submodules/ContactListUI/Sources/ContactListNode.swift +++ b/submodules/ContactListUI/Sources/ContactListNode.swift @@ -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 { diff --git a/submodules/ContactsPeerItem/Sources/ContactsPeerItem.swift b/submodules/ContactsPeerItem/Sources/ContactsPeerItem.swift index 5b834e9c73..26b5e00c3d 100644 --- a/submodules/ContactsPeerItem/Sources/ContactsPeerItem.swift +++ b/submodules/ContactsPeerItem/Sources/ContactsPeerItem.swift @@ -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 { diff --git a/submodules/ContextUI/Sources/ContextController.swift b/submodules/ContextUI/Sources/ContextController.swift index 024be3fb41..c93fd1fbb0 100644 --- a/submodules/ContextUI/Sources/ContextController.swift +++ b/submodules/ContextUI/Sources/ContextController.swift @@ -2249,6 +2249,7 @@ public final class ContextController: ViewController, StandalonePresentableContr public var context: AccountContext? public var reactionItems: [ReactionContextItem] public var selectedReactionItems: Set + public var reactionsTitle: String? public var animationCache: AnimationCache? public var alwaysAllowPremiumReactions: Bool public var getEmojiContent: ((AnimationCache, MultiAnimationRenderer) -> Signal)? @@ -2263,6 +2264,7 @@ public final class ContextController: ViewController, StandalonePresentableContr context: AccountContext? = nil, reactionItems: [ReactionContextItem] = [], selectedReactionItems: Set = Set(), + reactionsTitle: String? = nil, animationCache: AnimationCache? = nil, alwaysAllowPremiumReactions: Bool = false, getEmojiContent: ((AnimationCache, MultiAnimationRenderer) -> Signal)? = 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 diff --git a/submodules/ContextUI/Sources/ContextControllerActionsStackNode.swift b/submodules/ContextUI/Sources/ContextControllerActionsStackNode.swift index bc065566e2..24fa282f17 100644 --- a/submodules/ContextUI/Sources/ContextControllerActionsStackNode.swift +++ b/submodules/ContextUI/Sources/ContextControllerActionsStackNode.swift @@ -46,7 +46,7 @@ public protocol ContextControllerActionsStackItem: AnyObject { var id: AnyHashable? { get } var tip: ContextController.Tip? { get } var tipSignal: Signal? { get } - var reactionItems: (context: AccountContext, reactionItems: [ReactionContextItem], selectedReactionItems: Set, animationCache: AnimationCache, alwaysAllowPremiumReactions: Bool, getEmojiContent: ((AnimationCache, MultiAnimationRenderer) -> Signal)?)? { get } + var reactionItems: (context: AccountContext, reactionItems: [ReactionContextItem], selectedReactionItems: Set, reactionsTitle: String?, animationCache: AnimationCache, alwaysAllowPremiumReactions: Bool, getEmojiContent: ((AnimationCache, MultiAnimationRenderer) -> Signal)?)? { 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, animationCache: AnimationCache, alwaysAllowPremiumReactions: Bool, getEmojiContent: ((AnimationCache, MultiAnimationRenderer) -> Signal)?)? + let reactionItems: (context: AccountContext, reactionItems: [ReactionContextItem], selectedReactionItems: Set, reactionsTitle: String?, animationCache: AnimationCache, alwaysAllowPremiumReactions: Bool, getEmojiContent: ((AnimationCache, MultiAnimationRenderer) -> Signal)?)? let tip: ContextController.Tip? let tipSignal: Signal? let dismissed: (() -> Void)? @@ -919,7 +919,7 @@ final class ContextControllerActionsListStackItem: ContextControllerActionsStack init( id: AnyHashable?, items: [ContextMenuItem], - reactionItems: (context: AccountContext, reactionItems: [ReactionContextItem], selectedReactionItems: Set, animationCache: AnimationCache, alwaysAllowPremiumReactions: Bool, getEmojiContent: ((AnimationCache, MultiAnimationRenderer) -> Signal)?)?, + reactionItems: (context: AccountContext, reactionItems: [ReactionContextItem], selectedReactionItems: Set, reactionsTitle: String?, animationCache: AnimationCache, alwaysAllowPremiumReactions: Bool, getEmojiContent: ((AnimationCache, MultiAnimationRenderer) -> Signal)?)?, tip: ContextController.Tip?, tipSignal: Signal?, 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, animationCache: AnimationCache, alwaysAllowPremiumReactions: Bool, getEmojiContent: ((AnimationCache, MultiAnimationRenderer) -> Signal)?)? + let reactionItems: (context: AccountContext, reactionItems: [ReactionContextItem], selectedReactionItems: Set, reactionsTitle: String?, animationCache: AnimationCache, alwaysAllowPremiumReactions: Bool, getEmojiContent: ((AnimationCache, MultiAnimationRenderer) -> Signal)?)? let tip: ContextController.Tip? let tipSignal: Signal? let dismissed: (() -> Void)? @@ -1017,7 +1017,7 @@ final class ContextControllerActionsCustomStackItem: ContextControllerActionsSta init( id: AnyHashable?, content: ContextControllerItemsContent, - reactionItems: (context: AccountContext, reactionItems: [ReactionContextItem], selectedReactionItems: Set, animationCache: AnimationCache, alwaysAllowPremiumReactions: Bool, getEmojiContent: ((AnimationCache, MultiAnimationRenderer) -> Signal)?)?, + reactionItems: (context: AccountContext, reactionItems: [ReactionContextItem], selectedReactionItems: Set, reactionsTitle: String?, animationCache: AnimationCache, alwaysAllowPremiumReactions: Bool, getEmojiContent: ((AnimationCache, MultiAnimationRenderer) -> Signal)?)?, tip: ContextController.Tip?, tipSignal: Signal?, dismissed: (() -> Void)? @@ -1046,9 +1046,9 @@ final class ContextControllerActionsCustomStackItem: ContextControllerActionsSta } func makeContextControllerActionsStackItem(items: ContextController.Items) -> [ContextControllerActionsStackItem] { - var reactionItems: (context: AccountContext, reactionItems: [ReactionContextItem], selectedReactionItems: Set, animationCache: AnimationCache, alwaysAllowPremiumReactions: Bool, getEmojiContent: ((AnimationCache, MultiAnimationRenderer) -> Signal)?)? + var reactionItems: (context: AccountContext, reactionItems: [ReactionContextItem], selectedReactionItems: Set, reactionsTitle: String?, animationCache: AnimationCache, alwaysAllowPremiumReactions: Bool, getEmojiContent: ((AnimationCache, MultiAnimationRenderer) -> Signal)?)? 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? var tipNode: InnerTextSelectionTipContainerNode? - let reactionItems: (context: AccountContext, reactionItems: [ReactionContextItem], selectedReactionItems: Set, animationCache: AnimationCache, alwaysAllowPremiumReactions: Bool, getEmojiContent: ((AnimationCache, MultiAnimationRenderer) -> Signal)?)? + let reactionItems: (context: AccountContext, reactionItems: [ReactionContextItem], selectedReactionItems: Set, reactionsTitle: String?, animationCache: AnimationCache, alwaysAllowPremiumReactions: Bool, getEmojiContent: ((AnimationCache, MultiAnimationRenderer) -> Signal)?)? let itemDismissed: (() -> Void)? var storedScrollingState: CGFloat? let positionLock: CGFloat? @@ -1187,7 +1187,7 @@ final class ContextControllerActionsStackNode: ASDisplayNode { item: ContextControllerActionsStackItem, tip: ContextController.Tip?, tipSignal: Signal?, - reactionItems: (context: AccountContext, reactionItems: [ReactionContextItem], selectedReactionItems: Set, animationCache: AnimationCache, alwaysAllowPremiumReactions: Bool, getEmojiContent: ((AnimationCache, MultiAnimationRenderer) -> Signal)?)?, + reactionItems: (context: AccountContext, reactionItems: [ReactionContextItem], selectedReactionItems: Set, reactionsTitle: String?, animationCache: AnimationCache, alwaysAllowPremiumReactions: Bool, getEmojiContent: ((AnimationCache, MultiAnimationRenderer) -> Signal)?)?, itemDismissed: (() -> Void)?, positionLock: CGFloat? ) { @@ -1338,7 +1338,7 @@ final class ContextControllerActionsStackNode: ASDisplayNode { private var selectionPanGesture: UIPanGestureRecognizer? - var topReactionItems: (context: AccountContext, reactionItems: [ReactionContextItem], selectedReactionItems: Set, animationCache: AnimationCache, alwaysAllowPremiumReactions: Bool, getEmojiContent: ((AnimationCache, MultiAnimationRenderer) -> Signal)?)? { + var topReactionItems: (context: AccountContext, reactionItems: [ReactionContextItem], selectedReactionItems: Set, reactionsTitle: String?, animationCache: AnimationCache, alwaysAllowPremiumReactions: Bool, getEmojiContent: ((AnimationCache, MultiAnimationRenderer) -> Signal)?)? { return self.itemContainers.last?.reactionItems } diff --git a/submodules/ContextUI/Sources/ContextControllerExtractedPresentationNode.swift b/submodules/ContextUI/Sources/ContextControllerExtractedPresentationNode.swift index 9f18b5b3f4..fbeb50e9f8 100644 --- a/submodules/ContextUI/Sources/ContextControllerExtractedPresentationNode.swift +++ b/submodules/ContextUI/Sources/ContextControllerExtractedPresentationNode.swift @@ -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 diff --git a/submodules/SearchBarNode/Sources/SearchBarNode.swift b/submodules/SearchBarNode/Sources/SearchBarNode.swift index d55566ef4c..fff5306619 100644 --- a/submodules/SearchBarNode/Sources/SearchBarNode.swift +++ b/submodules/SearchBarNode/Sources/SearchBarNode.swift @@ -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 { diff --git a/submodules/TelegramCallsUI/Sources/CallControllerNodeV2.swift b/submodules/TelegramCallsUI/Sources/CallControllerNodeV2.swift index 812b95aee7..5fa6b1cd5a 100644 --- a/submodules/TelegramCallsUI/Sources/CallControllerNodeV2.swift +++ b/submodules/TelegramCallsUI/Sources/CallControllerNodeV2.swift @@ -157,6 +157,7 @@ final class CallControllerNodeV2: ViewControllerTracingNode, CallControllerNodeP } self.callScreenState = PrivateCallScreen.State( + strings: presentationData.strings, lifecycleState: .connecting, name: " ", shortName: " ", diff --git a/submodules/TelegramCore/Sources/Utils/PeerUtils.swift b/submodules/TelegramCore/Sources/Utils/PeerUtils.swift index 5abb9df028..bd0238c832 100644 --- a/submodules/TelegramCore/Sources/Utils/PeerUtils.swift +++ b/submodules/TelegramCore/Sources/Utils/PeerUtils.swift @@ -433,4 +433,7 @@ public extension PeerId { } return false } + var isSecretChat: Bool { + return self.namespace == Namespaces.Peer.SecretChat + } } diff --git a/submodules/TelegramUI/BUILD b/submodules/TelegramUI/BUILD index e320ebaecf..d05ee13e10 100644 --- a/submodules/TelegramUI/BUILD +++ b/submodules/TelegramUI/BUILD @@ -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": [], diff --git a/submodules/TelegramUI/Components/Calls/CallScreen/BUILD b/submodules/TelegramUI/Components/Calls/CallScreen/BUILD index ac35b7c0f2..6773bd07e8 100644 --- a/submodules/TelegramUI/Components/Calls/CallScreen/BUILD +++ b/submodules/TelegramUI/Components/Calls/CallScreen/BUILD @@ -68,6 +68,7 @@ swift_library( "//submodules/TelegramUI/Components/AnimatedTextComponent", "//submodules/AppBundle", "//submodules/UIKitRuntimeUtils", + "//submodules/TelegramPresentationData", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/Calls/CallScreen/CallScreenAssets.xcassets/Call/Snow.imageset/Contents.json b/submodules/TelegramUI/Components/Calls/CallScreen/CallScreenAssets.xcassets/Call/Snow.imageset/Contents.json new file mode 100644 index 0000000000..de2d610148 --- /dev/null +++ b/submodules/TelegramUI/Components/Calls/CallScreen/CallScreenAssets.xcassets/Call/Snow.imageset/Contents.json @@ -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 + } +} diff --git a/submodules/TelegramUI/Components/Calls/CallScreen/CallScreenAssets.xcassets/Call/Snow.imageset/Snow.png b/submodules/TelegramUI/Components/Calls/CallScreen/CallScreenAssets.xcassets/Call/Snow.imageset/Snow.png new file mode 100644 index 0000000000..2dddf3fa21 Binary files /dev/null and b/submodules/TelegramUI/Components/Calls/CallScreen/CallScreenAssets.xcassets/Call/Snow.imageset/Snow.png differ diff --git a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/BackButtonView.swift b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/BackButtonView.swift index 0fed60cf04..696d0a5727 100644 --- a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/BackButtonView.swift +++ b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/BackButtonView.swift @@ -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 + } } diff --git a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/ButtonGroupView.swift b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/ButtonGroupView.swift index 180fa18dca..73c36b48ce 100644 --- a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/ButtonGroupView.swift +++ b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/ButtonGroupView.swift @@ -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 diff --git a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/PrivateCallScreen.swift b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/PrivateCallScreen.swift index a53cfd48ad..b974b71dc7 100644 --- a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/PrivateCallScreen.swift +++ b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/PrivateCallScreen.swift @@ -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) } } diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageAnimatedStickerItemNode/Sources/ChatMessageAnimatedStickerItemNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageAnimatedStickerItemNode/Sources/ChatMessageAnimatedStickerItemNode.swift index f70e9ade84..2a3178bbd8 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageAnimatedStickerItemNode/Sources/ChatMessageAnimatedStickerItemNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageAnimatedStickerItemNode/Sources/ChatMessageAnimatedStickerItemNode.swift @@ -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, diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageAttachedContentNode/Sources/ChatMessageAttachedContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageAttachedContentNode/Sources/ChatMessageAttachedContentNode.swift index 98ce2ab888..541b9742a9 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageAttachedContentNode/Sources/ChatMessageAttachedContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageAttachedContentNode/Sources/ChatMessageAttachedContentNode.swift @@ -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, diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift index 7f2a940148..51edc4886e 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift @@ -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, diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageContactBubbleContentNode/Sources/ChatMessageContactBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageContactBubbleContentNode/Sources/ChatMessageContactBubbleContentNode.swift index a08014ab8f..2f9134dc60 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageContactBubbleContentNode/Sources/ChatMessageContactBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageContactBubbleContentNode/Sources/ChatMessageContactBubbleContentNode.swift @@ -280,6 +280,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, diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageDateAndStatusNode/Sources/ChatMessageDateAndStatusNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageDateAndStatusNode/Sources/ChatMessageDateAndStatusNode.swift index 0b9cee1b0a..824cc1772d 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageDateAndStatusNode/Sources/ChatMessageDateAndStatusNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageDateAndStatusNode/Sources/ChatMessageDateAndStatusNode.swift @@ -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 ) } diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageGiveawayBubbleContentNode/Sources/ChatMessageGiveawayBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageGiveawayBubbleContentNode/Sources/ChatMessageGiveawayBubbleContentNode.swift index 9c67e031c4..a206105c42 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageGiveawayBubbleContentNode/Sources/ChatMessageGiveawayBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageGiveawayBubbleContentNode/Sources/ChatMessageGiveawayBubbleContentNode.swift @@ -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, diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveFileNode/Sources/ChatMessageInteractiveFileNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveFileNode/Sources/ChatMessageInteractiveFileNode.swift index a851caebff..0aca23b715 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveFileNode/Sources/ChatMessageInteractiveFileNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveFileNode/Sources/ChatMessageInteractiveFileNode.swift @@ -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, diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveInstantVideoNode/Sources/ChatMessageInteractiveInstantVideoNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveInstantVideoNode/Sources/ChatMessageInteractiveInstantVideoNode.swift index 355eab8cf1..6144637ec1 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveInstantVideoNode/Sources/ChatMessageInteractiveInstantVideoNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveInstantVideoNode/Sources/ChatMessageInteractiveInstantVideoNode.swift @@ -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, diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveMediaNode/Sources/ChatMessageInteractiveMediaNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveMediaNode/Sources/ChatMessageInteractiveMediaNode.swift index 52721bf13f..5ac8ce5904 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveMediaNode/Sources/ChatMessageInteractiveMediaNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveMediaNode/Sources/ChatMessageInteractiveMediaNode.swift @@ -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, diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageMapBubbleContentNode/Sources/ChatMessageMapBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageMapBubbleContentNode/Sources/ChatMessageMapBubbleContentNode.swift index 64e2a050f1..f0c7e86898 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageMapBubbleContentNode/Sources/ChatMessageMapBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageMapBubbleContentNode/Sources/ChatMessageMapBubbleContentNode.swift @@ -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, diff --git a/submodules/TelegramUI/Components/Chat/ChatMessagePollBubbleContentNode/Sources/ChatMessagePollBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessagePollBubbleContentNode/Sources/ChatMessagePollBubbleContentNode.swift index 0fb5e22a96..6318eb0633 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessagePollBubbleContentNode/Sources/ChatMessagePollBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessagePollBubbleContentNode/Sources/ChatMessagePollBubbleContentNode.swift @@ -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, diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageReactionsFooterContentNode/Sources/ChatMessageReactionsFooterContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageReactionsFooterContentNode/Sources/ChatMessageReactionsFooterContentNode.swift index 86b938b6b2..ae9356212d 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageReactionsFooterContentNode/Sources/ChatMessageReactionsFooterContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageReactionsFooterContentNode/Sources/ChatMessageReactionsFooterContentNode.swift @@ -178,6 +178,7 @@ public final class MessageReactionButtonsNode: ASDisplayNode { ) }, colors: reactionColors, + isTag: message.id.peerId == context.account.peerId, constrainedWidth: constrainedWidth ) diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageRestrictedBubbleContentNode/Sources/ChatMessageRestrictedBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageRestrictedBubbleContentNode/Sources/ChatMessageRestrictedBubbleContentNode.swift index c11a20eba4..4ba96ab7cc 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageRestrictedBubbleContentNode/Sources/ChatMessageRestrictedBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageRestrictedBubbleContentNode/Sources/ChatMessageRestrictedBubbleContentNode.swift @@ -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, diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageStickerItemNode/Sources/ChatMessageStickerItemNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageStickerItemNode/Sources/ChatMessageStickerItemNode.swift index f9d6593249..22d2c1b2c0 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageStickerItemNode/Sources/ChatMessageStickerItemNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageStickerItemNode/Sources/ChatMessageStickerItemNode.swift @@ -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, diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageTextBubbleContentNode/Sources/ChatMessageTextBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageTextBubbleContentNode/Sources/ChatMessageTextBubbleContentNode.swift index 8a98990b74..0d47147dc2 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageTextBubbleContentNode/Sources/ChatMessageTextBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageTextBubbleContentNode/Sources/ChatMessageTextBubbleContentNode.swift @@ -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, diff --git a/submodules/TelegramUI/Components/ChatTitleView/Sources/ChatTitleView.swift b/submodules/TelegramUI/Components/ChatTitleView/Sources/ChatTitleView.swift index 26a2ac8bc4..8b8d14630b 100644 --- a/submodules/TelegramUI/Components/ChatTitleView/Sources/ChatTitleView.swift +++ b/submodules/TelegramUI/Components/ChatTitleView/Sources/ChatTitleView.swift @@ -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 { diff --git a/submodules/TelegramUI/Components/PlainButtonComponent/Sources/PlainButtonComponent.swift b/submodules/TelegramUI/Components/PlainButtonComponent/Sources/PlainButtonComponent.swift index a6e568adb8..1152c2cefd 100644 --- a/submodules/TelegramUI/Components/PlainButtonComponent/Sources/PlainButtonComponent.swift +++ b/submodules/TelegramUI/Components/PlainButtonComponent/Sources/PlainButtonComponent.swift @@ -13,6 +13,7 @@ public final class PlainButtonComponent: Component { public let content: AnyComponent 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, 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) diff --git a/submodules/TelegramUI/Images.xcassets/Avatar/MyNotesIcon.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Avatar/MyNotesIcon.imageset/Contents.json new file mode 100644 index 0000000000..040e468b10 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Avatar/MyNotesIcon.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "mynotes.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Avatar/MyNotesIcon.imageset/mynotes.pdf b/submodules/TelegramUI/Images.xcassets/Avatar/MyNotesIcon.imageset/mynotes.pdf new file mode 100644 index 0000000000..d6d2788e2a --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Avatar/MyNotesIcon.imageset/mynotes.pdf @@ -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 \ No newline at end of file diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Message/ReactionTagBackground.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Chat/Message/ReactionTagBackground.imageset/Contents.json new file mode 100644 index 0000000000..94301dd104 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chat/Message/ReactionTagBackground.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "MessageTagBackground.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Message/ReactionTagBackground.imageset/MessageTagBackground.svg b/submodules/TelegramUI/Images.xcassets/Chat/Message/ReactionTagBackground.imageset/MessageTagBackground.svg new file mode 100644 index 0000000000..d4221d3c16 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chat/Message/ReactionTagBackground.imageset/MessageTagBackground.svg @@ -0,0 +1,3 @@ + + + diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Title Panels/SearchTagTokenBackground.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Chat/Title Panels/SearchTagTokenBackground.imageset/Contents.json new file mode 100644 index 0000000000..04f56c7a35 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chat/Title Panels/SearchTagTokenBackground.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "SearchTagTokenBackground.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Title Panels/SearchTagTokenBackground.imageset/SearchTagTokenBackground.svg b/submodules/TelegramUI/Images.xcassets/Chat/Title Panels/SearchTagTokenBackground.imageset/SearchTagTokenBackground.svg new file mode 100644 index 0000000000..4f3a05598a --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chat/Title Panels/SearchTagTokenBackground.imageset/SearchTagTokenBackground.svg @@ -0,0 +1,4 @@ + + + + diff --git a/submodules/TelegramUI/Sources/Chat/ChatControllerOpenMessageContextMenu.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerOpenMessageContextMenu.swift index e9707619da..02f609d4db 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatControllerOpenMessageContextMenu.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerOpenMessageContextMenu.swift @@ -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 { diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index 24cacd040a..446926061b 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -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 @@ -15817,12 +15820,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)) } } diff --git a/submodules/TelegramUI/Sources/ChatControllerNode.swift b/submodules/TelegramUI/Sources/ChatControllerNode.swift index d161f7e91b..f724ecbdc7 100644 --- a/submodules/TelegramUI/Sources/ChatControllerNode.swift +++ b/submodules/TelegramUI/Sources/ChatControllerNode.swift @@ -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) diff --git a/submodules/TelegramUI/Sources/ChatInterfaceTitlePanelNodes.swift b/submodules/TelegramUI/Sources/ChatInterfaceTitlePanelNodes.swift index 8abe141e24..bb226a90da 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceTitlePanelNodes.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceTitlePanelNodes.swift @@ -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 diff --git a/submodules/TelegramUI/Sources/ChatSearchNavigationContentNode.swift b/submodules/TelegramUI/Sources/ChatSearchNavigationContentNode.swift index aded8da70e..38ffcd0589 100644 --- a/submodules/TelegramUI/Sources/ChatSearchNavigationContentNode.swift +++ b/submodules/TelegramUI/Sources/ChatSearchNavigationContentNode.swift @@ -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 { diff --git a/submodules/TelegramUI/Sources/ChatSearchResultsContollerNode.swift b/submodules/TelegramUI/Sources/ChatSearchResultsContollerNode.swift index a99bd19f41..371f10956c 100644 --- a/submodules/TelegramUI/Sources/ChatSearchResultsContollerNode.swift +++ b/submodules/TelegramUI/Sources/ChatSearchResultsContollerNode.swift @@ -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) } })) diff --git a/submodules/TelegramUI/Sources/ChatSearchTitleAccessoryPanelNode.swift b/submodules/TelegramUI/Sources/ChatSearchTitleAccessoryPanelNode.swift new file mode 100644 index 0000000000..84e24f7e6f --- /dev/null +++ b/submodules/TelegramUI/Sources/ChatSearchTitleAccessoryPanelNode.swift @@ -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() + + 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) + } +}