[WIP] Stories

This commit is contained in:
Ali 2023-05-10 00:36:37 +04:00
parent 50b6c18065
commit e8dab90584
48 changed files with 4980 additions and 2180 deletions

View File

@ -6,8 +6,8 @@ import TelegramCore
import TelegramPresentationData
public struct ChatListNodeAdditionalCategory {
public enum Appearance {
case option
public enum Appearance: Equatable {
case option(sectionTitle: String?)
case action
}
@ -17,7 +17,7 @@ public struct ChatListNodeAdditionalCategory {
public var title: String
public var appearance: Appearance
public init(id: Int, icon: UIImage?, smallIcon: UIImage?, title: String, appearance: Appearance = .option) {
public init(id: Int, icon: UIImage?, smallIcon: UIImage?, title: String, appearance: Appearance = .option(sectionTitle: nil)) {
self.id = id
self.icon = icon
self.smallIcon = smallIcon
@ -44,6 +44,7 @@ public enum ContactMultiselectionControllerMode {
public var additionalCategories: ContactMultiselectionControllerAdditionalCategories?
public var chatListFilters: [ChatListFilter]?
public var displayAutoremoveTimeout: Bool
public var displayPresence: Bool
public init(
title: String,
@ -51,7 +52,8 @@ public enum ContactMultiselectionControllerMode {
selectedChats: Set<EnginePeer.Id>,
additionalCategories: ContactMultiselectionControllerAdditionalCategories?,
chatListFilters: [ChatListFilter]?,
displayAutoremoveTimeout: Bool = false
displayAutoremoveTimeout: Bool = false,
displayPresence: Bool = false
) {
self.title = title
self.searchPlaceholder = searchPlaceholder
@ -59,6 +61,7 @@ public enum ContactMultiselectionControllerMode {
self.additionalCategories = additionalCategories
self.chatListFilters = chatListFilters
self.displayAutoremoveTimeout = displayAutoremoveTimeout
self.displayPresence = displayPresence
}
}

View File

@ -30,6 +30,7 @@ public enum ChatListSearchItemHeaderType {
case downloading
case recentDownloads
case topics
case text(String, AnyHashable)
fileprivate func title(strings: PresentationStrings) -> String {
switch self {
@ -87,6 +88,8 @@ public enum ChatListSearchItemHeaderType {
return strings.DownloadList_DownloadedHeader
case .topics:
return strings.DialogList_SearchSectionTopics
case let .text(text, _):
return text
}
}
@ -146,11 +149,13 @@ public enum ChatListSearchItemHeaderType {
return .recentDownloads
case .topics:
return .topics
case let .text(_, id):
return .text(id)
}
}
}
private enum ChatListSearchItemHeaderId: Int32 {
private enum ChatListSearchItemHeaderId: Hashable {
case localPeers
case members
case contacts
@ -181,6 +186,7 @@ private enum ChatListSearchItemHeaderId: Int32 {
case downloading
case recentDownloads
case topics
case text(AnyHashable)
}
public final class ChatListSearchItemHeader: ListViewItemHeader {
@ -197,7 +203,7 @@ public final class ChatListSearchItemHeader: ListViewItemHeader {
public init(type: ChatListSearchItemHeaderType, theme: PresentationTheme, strings: PresentationStrings, actionTitle: String? = nil, action: (() -> Void)? = nil) {
self.type = type
self.id = ListViewItemNode.HeaderId(space: 0, id: Int64(self.type.id.rawValue))
self.id = ListViewItemNode.HeaderId(space: 0, id: Int64(self.type.id.hashValue))
self.theme = theme
self.strings = strings
self.actionTitle = actionTitle

View File

@ -45,8 +45,12 @@ public class ChatListAdditionalCategoryItem: ItemListItem, ListViewItemWithHeade
self.action = action
switch appearance {
case .option:
self.header = ChatListSearchItemHeader(type: .chatTypes, theme: presentationData.theme, strings: presentationData.strings, actionTitle: nil, action: nil)
case let .option(sectionTitle):
if let sectionTitle {
self.header = ChatListSearchItemHeader(type: .text(sectionTitle, AnyHashable(0)), theme: presentationData.theme, strings: presentationData.strings, actionTitle: nil, action: nil)
} else {
self.header = ChatListSearchItemHeader(type: .chatTypes, theme: presentationData.theme, strings: presentationData.strings, actionTitle: nil, action: nil)
}
case .action:
self.header = header
}

View File

@ -1358,7 +1358,10 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
context: self.context,
initialFocusedId: AnyHashable(peerId),
initialContent: initialContent,
transitionIn: nil
transitionIn: nil,
transitionOut: { _ in
return nil
}
)
self.push(storyContainerScreen)
})
@ -2412,7 +2415,24 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
context: self.context,
initialFocusedId: AnyHashable(peer.id),
initialContent: initialContent,
transitionIn: transitionIn
transitionIn: transitionIn,
transitionOut: { [weak self] peerId in
guard let self else {
return nil
}
if let storyPeerListView = self.storyPeerListView?.view as? StoryPeerListComponent.View {
if let transitionView = storyPeerListView.transitionViewForItem(peerId: peerId) {
return StoryContainerScreen.TransitionOut(
destinationView: transitionView,
destinationRect: transitionView.bounds,
destinationCornerRadius: transitionView.bounds.height * 0.5
)
}
}
return nil
}
)
self.push(storyContainerScreen)
})

View File

@ -22,7 +22,7 @@ import StoryContainerScreen
public enum ChatListNodeMode {
case chatList(appendContacts: Bool)
case peers(filter: ChatListNodePeersFilter, isSelecting: Bool, additionalCategories: [ChatListNodeAdditionalCategory], chatListFilters: [ChatListFilter]?, displayAutoremoveTimeout: Bool)
case peers(filter: ChatListNodePeersFilter, isSelecting: Bool, additionalCategories: [ChatListNodeAdditionalCategory], chatListFilters: [ChatListFilter]?, displayAutoremoveTimeout: Bool, displayPresence: Bool)
case peerType(type: [ReplyMarkupButtonRequestPeerType], hasCreate: Bool)
}
@ -405,7 +405,7 @@ private func mappedInsertEntries(context: AccountContext, nodeInteraction: ChatL
hiddenOffset: threadInfo?.isHidden == true && !revealed,
interaction: nodeInteraction
), directionHint: entry.directionHint)
case let .peers(filter, isSelecting, _, filters, displayAutoremoveTimeout):
case let .peers(filter, isSelecting, _, filters, displayAutoremoveTimeout, displayPresence):
let itemPeer = peer.chatMainPeer
var chatPeer: EnginePeer?
if let peer = peer.peers[peer.peerId] {
@ -488,7 +488,7 @@ private func mappedInsertEntries(context: AccountContext, nodeInteraction: ChatL
var header: ChatListSearchItemHeader?
switch mode {
case let .peers(_, _, additionalCategories, _, _):
case let .peers(_, _, additionalCategories, _, _, _):
if !additionalCategories.isEmpty {
let headerType: ChatListSearchItemHeaderType
if case .action = additionalCategories[0].appearance {
@ -505,7 +505,9 @@ private func mappedInsertEntries(context: AccountContext, nodeInteraction: ChatL
var status: ContactsPeerItemStatus = .none
if isSelecting, let itemPeer = itemPeer {
if let (string, multiline, isActive, icon) = statusStringForPeerType(accountPeerId: context.account.peerId, strings: presentationData.strings, peer: itemPeer, isMuted: isRemovedFromTotalUnreadCount, isUnread: combinedReadState?.isUnread ?? false, isContact: isContact, hasUnseenMentions: hasUnseenMentions, chatListFilters: filters, displayAutoremoveTimeout: displayAutoremoveTimeout, autoremoveTimeout: peerEntry.autoremoveTimeout) {
if displayPresence, let presence = presence {
status = .presence(presence, presentationData.dateTimeFormat)
} else if let (string, multiline, isActive, icon) = statusStringForPeerType(accountPeerId: context.account.peerId, strings: presentationData.strings, peer: itemPeer, isMuted: isRemovedFromTotalUnreadCount, isUnread: combinedReadState?.isUnread ?? false, isContact: isContact, hasUnseenMentions: hasUnseenMentions, chatListFilters: filters, displayAutoremoveTimeout: displayAutoremoveTimeout, autoremoveTimeout: peerEntry.autoremoveTimeout) {
status = .custom(string: string, multiline: multiline, isActive: isActive, icon: icon)
} else {
status = .none
@ -749,7 +751,7 @@ private func mappedUpdateEntries(context: AccountContext, nodeInteraction: ChatL
hiddenOffset: threadInfo?.isHidden == true && !revealed,
interaction: nodeInteraction
), directionHint: entry.directionHint)
case let .peers(filter, isSelecting, _, filters, displayAutoremoveTimeout):
case let .peers(filter, isSelecting, _, filters, displayAutoremoveTimeout, displayPresence):
let itemPeer = peer.chatMainPeer
var chatPeer: EnginePeer?
if let peer = peer.peers[peer.peerId] {
@ -786,7 +788,7 @@ private func mappedUpdateEntries(context: AccountContext, nodeInteraction: ChatL
var header: ChatListSearchItemHeader?
switch mode {
case let .peers(_, _, additionalCategories, _, _):
case let .peers(_, _, additionalCategories, _, _, _):
if !additionalCategories.isEmpty {
let headerType: ChatListSearchItemHeaderType
if case .action = additionalCategories[0].appearance {
@ -803,7 +805,9 @@ private func mappedUpdateEntries(context: AccountContext, nodeInteraction: ChatL
var status: ContactsPeerItemStatus = .none
if isSelecting, let itemPeer = itemPeer {
if let (string, multiline, isActive, icon) = statusStringForPeerType(accountPeerId: context.account.peerId, strings: presentationData.strings, peer: itemPeer, isMuted: isRemovedFromTotalUnreadCount, isUnread: combinedReadState?.isUnread ?? false, isContact: isContact, hasUnseenMentions: hasUnseenMentions, chatListFilters: filters, displayAutoremoveTimeout: displayAutoremoveTimeout, autoremoveTimeout: peerEntry.autoremoveTimeout) {
if displayPresence, let presence = presence {
status = .presence(presence, presentationData.dateTimeFormat)
} else if let (string, multiline, isActive, icon) = statusStringForPeerType(accountPeerId: context.account.peerId, strings: presentationData.strings, peer: itemPeer, isMuted: isRemovedFromTotalUnreadCount, isUnread: combinedReadState?.isUnread ?? false, isContact: isContact, hasUnseenMentions: hasUnseenMentions, chatListFilters: filters, displayAutoremoveTimeout: displayAutoremoveTimeout, autoremoveTimeout: peerEntry.autoremoveTimeout) {
status = .custom(string: string, multiline: multiline, isActive: isActive, icon: icon)
} else {
status = .none
@ -1191,7 +1195,7 @@ public final class ChatListNode: ListView {
self.animationRenderer = animationRenderer
var isSelecting = false
if case .peers(_, true, _, _, _) = mode {
if case .peers(_, true, _, _, _, _) = mode {
isSelecting = true
}
@ -1555,7 +1559,7 @@ public final class ChatListNode: ListView {
let currentRemovingItemId = self.currentRemovingItemId
let savedMessagesPeer: Signal<EnginePeer?, NoError>
if case let .peers(filter, _, _, _, _) = mode, filter.contains(.onlyWriteable), case .chatList = location, self.chatListFilter == nil {
if case let .peers(filter, _, _, _, _, _) = mode, filter.contains(.onlyWriteable), case .chatList = location, self.chatListFilter == nil {
savedMessagesPeer = context.account.postbox.loadedPeerWithId(context.account.peerId)
|> map(Optional.init)
|> map { peer in
@ -1882,7 +1886,7 @@ public final class ChatListNode: ListView {
case .chatList:
isEmpty = false
return true
case let .peers(filter, _, _, _, _):
case let .peers(filter, _, _, _, _, _):
guard !filter.contains(.excludeSavedMessages) || peer.peerId != currentPeerId else { return false }
guard !filter.contains(.excludeSavedMessages) || !peer.peerId.isReplies else { return false }
guard !filter.contains(.excludeSecretChats) || peer.peerId.namespace != Namespaces.Peer.SecretChat else { return false }
@ -3721,10 +3725,12 @@ private func statusStringForPeerType(accountPeerId: EnginePeer.Id, strings: Pres
} else if case let .user(user) = peer {
if user.botInfo != nil || user.flags.contains(.isSupport) {
return (strings.ChatList_PeerTypeBot, false, false, nil)
} else if isContact {
return (strings.ChatList_PeerTypeContact, false, false, nil)
} else {
return (strings.ChatList_PeerTypeNonContact, false, false, nil)
if isContact {
return (strings.ChatList_PeerTypeContact, false, false, nil)
} else {
return (strings.ChatList_PeerTypeNonContact, false, false, nil)
}
}
} else if case .secretChat = peer {
if isContact {

View File

@ -819,7 +819,7 @@ func chatListNodeEntriesForView(_ view: EngineChatList, state: ChatListNodeState
}
if !view.hasLater {
if case let .peers(_, _, additionalCategories, _, _) = mode {
if case let .peers(_, _, additionalCategories, _, _, _) = mode {
var index = 0
for category in additionalCategories.reversed() {
result.append(.AdditionalCategory(index: index, id: category.id, title: category.title, image: category.icon, appearance: category.appearance, selected: state.selectedAdditionalCategoryIds.contains(category.id), presentationData: state.presentationData))

View File

@ -280,6 +280,10 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo
self.scrollNode.addSubnode(self.actionsContainerNode)
self.actionsContainerNode.addSubnode(self.additionalActionsStackNode)
self.actionsContainerNode.addSubnode(self.actionsStackNode)
#if DEBUG
//self.addSubnode(self.contentRectDebugNode)
#endif
self.scroller.delegate = self
@ -609,7 +613,7 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo
}
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let undoController = UndoOverlayController(presentationData: presentationData, content: .sticker(context: context, file: file, title: nil, text: presentationData.strings.Chat_PremiumReactionToastTitle, undoText: presentationData.strings.Chat_PremiumReactionToastAction, customAction: { [weak controller] in
let undoController = UndoOverlayController(presentationData: presentationData, content: .sticker(context: context, file: file, loop: true, title: nil, text: presentationData.strings.Chat_PremiumReactionToastTitle, undoText: presentationData.strings.Chat_PremiumReactionToastAction, customAction: { [weak controller] in
controller?.premiumReactionsSelected?()
}), elevatedLayout: false, position: position, animateInAsReplacement: animateInAsReplacement, action: { _ in true })
strongSelf.currentUndoController = undoController

View File

@ -162,6 +162,7 @@ public class ImageNode: ASDisplayNode {
public func setSignal(_ signal: Signal<UIImage?, NoError>) {
var reportedHasImage = false
var wasSynchronous = true
self.disposable.set((signal |> deliverOnMainQueue).start(next: {[weak self] next in
dispatcher.dispatch {
if let strongSelf = self {
@ -169,12 +170,12 @@ public class ImageNode: ASDisplayNode {
if strongSelf.first && next != nil {
strongSelf.first = false
animate = false
if strongSelf.isNodeLoaded && strongSelf.animateFirstTransition {
if strongSelf.isNodeLoaded && strongSelf.animateFirstTransition && !wasSynchronous {
strongSelf.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.18)
}
}
if let image = next?.cgImage {
if animate, let previousContents = strongSelf.contents {
if animate, let previousContents = strongSelf.contents, !wasSynchronous {
strongSelf.contents = image
let tempLayer = CALayer()
tempLayer.contents = previousContents
@ -207,6 +208,7 @@ public class ImageNode: ASDisplayNode {
}
}
}))
wasSynchronous = false
}
public override func clearContents() {

View File

@ -49,6 +49,7 @@ public struct InteractiveTransitionGestureRecognizerDirections: OptionSet {
public static let rightEdge = InteractiveTransitionGestureRecognizerDirections(rawValue: 1 << 3)
public static let leftCenter = InteractiveTransitionGestureRecognizerDirections(rawValue: 1 << 0)
public static let rightCenter = InteractiveTransitionGestureRecognizerDirections(rawValue: 1 << 1)
public static let down = InteractiveTransitionGestureRecognizerDirections(rawValue: 1 << 4)
public static let left: InteractiveTransitionGestureRecognizerDirections = [.leftEdge, .leftCenter]
public static let right: InteractiveTransitionGestureRecognizerDirections = [.rightEdge, .rightCenter]
@ -105,11 +106,14 @@ public class InteractiveTransitionGestureRecognizer: UIPanGestureRecognizer {
let horizontalGestures = hasHorizontalGestures(target, point: self.view?.convert(self.firstLocation, to: target))
switch horizontalGestures {
case .some, .strict:
if case .strict = horizontalGestures {
allowedDirections = []
} else if allowedDirections.contains(.leftEdge) || allowedDirections.contains(.rightEdge) {
allowedDirections.remove(.leftCenter)
allowedDirections.remove(.rightCenter)
if allowedDirections.contains(.down) {
} else {
if case .strict = horizontalGestures {
allowedDirections = []
} else if allowedDirections.contains(.leftEdge) || allowedDirections.contains(.rightEdge) {
allowedDirections.remove(.leftCenter)
allowedDirections.remove(.rightCenter)
}
}
case .none:
break
@ -132,36 +136,46 @@ public class InteractiveTransitionGestureRecognizer: UIPanGestureRecognizer {
let size = self.view?.bounds.size ?? CGSize()
let edgeWidth: CGFloat
switch self.edgeWidth {
case let .constant(value):
edgeWidth = value
case let .widthMultiplier(factor, minValue, maxValue):
edgeWidth = max(minValue, min(size.width * factor, maxValue))
}
if !self.validatedGesture {
if self.firstLocation.x < edgeWidth && !self.currentAllowedDirections.contains(.rightEdge) {
self.state = .failed
return
if self.currentAllowedDirections.contains(.down) {
if !self.validatedGesture {
if absTranslationX > 2.0 && absTranslationX > absTranslationY * 2.0 {
self.state = .failed
} else if absTranslationY > 2.0 && absTranslationX * 2.0 < absTranslationY {
self.validatedGesture = true
}
}
if self.firstLocation.x > size.width - edgeWidth && !self.currentAllowedDirections.contains(.leftEdge) {
self.state = .failed
return
} else {
let edgeWidth: CGFloat
switch self.edgeWidth {
case let .constant(value):
edgeWidth = value
case let .widthMultiplier(factor, minValue, maxValue):
edgeWidth = max(minValue, min(size.width * factor, maxValue))
}
if self.currentAllowedDirections.contains(.rightEdge) && self.firstLocation.x < edgeWidth {
self.validatedGesture = true
} else if self.currentAllowedDirections.contains(.leftEdge) && self.firstLocation.x > size.width - edgeWidth {
self.validatedGesture = true
} else if !self.currentAllowedDirections.contains(.leftCenter) && translation.x < 0.0 {
self.state = .failed
} else if !self.currentAllowedDirections.contains(.rightCenter) && translation.x > 0.0 {
self.state = .failed
} else if absTranslationY > 2.0 && absTranslationY > absTranslationX * 2.0 {
self.state = .failed
} else if absTranslationX > 2.0 && absTranslationY * 2.0 < absTranslationX {
self.validatedGesture = true
if !self.validatedGesture {
if self.firstLocation.x < edgeWidth && !self.currentAllowedDirections.contains(.rightEdge) {
self.state = .failed
return
}
if self.firstLocation.x > size.width - edgeWidth && !self.currentAllowedDirections.contains(.leftEdge) {
self.state = .failed
return
}
if self.currentAllowedDirections.contains(.rightEdge) && self.firstLocation.x < edgeWidth {
self.validatedGesture = true
} else if self.currentAllowedDirections.contains(.leftEdge) && self.firstLocation.x > size.width - edgeWidth {
self.validatedGesture = true
} else if !self.currentAllowedDirections.contains(.leftCenter) && translation.x < 0.0 {
self.state = .failed
} else if !self.currentAllowedDirections.contains(.rightCenter) && translation.x > 0.0 {
self.state = .failed
} else if absTranslationY > 2.0 && absTranslationY > absTranslationX * 2.0 {
self.state = .failed
} else if absTranslationX > 2.0 && absTranslationY * 2.0 < absTranslationX {
self.validatedGesture = true
}
}
}

View File

@ -297,7 +297,7 @@ open class BlurredBackgroundView: UIView {
private var enableBlur: Bool
private var effectView: UIVisualEffectView?
public private(set) var effectView: UIVisualEffectView?
private let backgroundView: UIView
private var validLayout: (CGSize, CGFloat)?

View File

@ -502,7 +502,7 @@ private final class FeaturedStickersScreenNode: ViewControllerTracingNode {
|> deliverOnMainQueue).start(next: { result in
switch result {
case .generic:
strongSelf.controller?.presentInGlobalOverlay(UndoOverlayController(presentationData: strongSelf.presentationData, content: .sticker(context: strongSelf.context, file: item.file, title: nil, text: !isStarred ? strongSelf.presentationData.strings.Conversation_StickerAddedToFavorites : strongSelf.presentationData.strings.Conversation_StickerRemovedFromFavorites, undoText: nil, customAction: nil), elevatedLayout: false, action: { _ in return false }), with: nil)
strongSelf.controller?.presentInGlobalOverlay(UndoOverlayController(presentationData: strongSelf.presentationData, content: .sticker(context: strongSelf.context, file: item.file, loop: true, title: nil, text: !isStarred ? strongSelf.presentationData.strings.Conversation_StickerAddedToFavorites : strongSelf.presentationData.strings.Conversation_StickerRemovedFromFavorites, undoText: nil, customAction: nil), elevatedLayout: false, action: { _ in return false }), with: nil)
case let .limitExceeded(limit, premiumLimit):
let premiumConfiguration = PremiumConfiguration.with(appConfiguration: strongSelf.context.currentAppConfiguration.with { $0 })
let text: String
@ -511,7 +511,7 @@ private final class FeaturedStickersScreenNode: ViewControllerTracingNode {
} else {
text = strongSelf.presentationData.strings.Premium_MaxFavedStickersText("\(premiumLimit)").string
}
strongSelf.controller?.presentInGlobalOverlay(UndoOverlayController(presentationData: strongSelf.presentationData, content: .sticker(context: strongSelf.context, file: item.file, title: strongSelf.presentationData.strings.Premium_MaxFavedStickersTitle("\(limit)").string, text: text, undoText: nil, customAction: nil), elevatedLayout: false, action: { [weak self] action in
strongSelf.controller?.presentInGlobalOverlay(UndoOverlayController(presentationData: strongSelf.presentationData, content: .sticker(context: strongSelf.context, file: item.file, loop: true, title: strongSelf.presentationData.strings.Premium_MaxFavedStickersTitle("\(limit)").string, text: text, undoText: nil, customAction: nil), elevatedLayout: false, action: { [weak self] action in
if let strongSelf = self {
if case .info = action {
let controller = PremiumIntroScreen(context: strongSelf.context, source: .savedStickers)
@ -590,7 +590,7 @@ private final class FeaturedStickersScreenNode: ViewControllerTracingNode {
|> deliverOnMainQueue).start(next: { result in
switch result {
case .generic:
strongSelf.controller?.presentInGlobalOverlay(UndoOverlayController(presentationData: strongSelf.presentationData, content: .sticker(context: strongSelf.context, file: item.file, title: nil, text: !isStarred ? strongSelf.presentationData.strings.Conversation_StickerAddedToFavorites : strongSelf.presentationData.strings.Conversation_StickerRemovedFromFavorites, undoText: nil, customAction: nil), elevatedLayout: false, action: { _ in return false }), with: nil)
strongSelf.controller?.presentInGlobalOverlay(UndoOverlayController(presentationData: strongSelf.presentationData, content: .sticker(context: strongSelf.context, file: item.file, loop: true, title: nil, text: !isStarred ? strongSelf.presentationData.strings.Conversation_StickerAddedToFavorites : strongSelf.presentationData.strings.Conversation_StickerRemovedFromFavorites, undoText: nil, customAction: nil), elevatedLayout: false, action: { _ in return false }), with: nil)
case let .limitExceeded(limit, premiumLimit):
let premiumConfiguration = PremiumConfiguration.with(appConfiguration: strongSelf.context.currentAppConfiguration.with { $0 })
let text: String
@ -599,7 +599,7 @@ private final class FeaturedStickersScreenNode: ViewControllerTracingNode {
} else {
text = strongSelf.presentationData.strings.Premium_MaxFavedStickersText("\(premiumLimit)").string
}
strongSelf.controller?.presentInGlobalOverlay(UndoOverlayController(presentationData: strongSelf.presentationData, content: .sticker(context: strongSelf.context, file: item.file, title: strongSelf.presentationData.strings.Premium_MaxFavedStickersTitle("\(limit)").string, text: text, undoText: nil, customAction: nil), elevatedLayout: false, action: { [weak self] action in
strongSelf.controller?.presentInGlobalOverlay(UndoOverlayController(presentationData: strongSelf.presentationData, content: .sticker(context: strongSelf.context, file: item.file, loop: true, title: strongSelf.presentationData.strings.Premium_MaxFavedStickersTitle("\(limit)").string, text: text, undoText: nil, customAction: nil), elevatedLayout: false, action: { [weak self] action in
if let strongSelf = self {
if case .info = action {
let controller = PremiumIntroScreen(context: strongSelf.context, source: .savedStickers)

View File

@ -4,7 +4,7 @@ import AsyncDisplayKit
import Display
import TelegramPresentationData
private let titleFont = Font.bold(13.0)
private let titleFont = Font.regular(13.0)
private let actionFont = Font.regular(13.0)
public enum ListSectionHeaderActionType {

View File

@ -119,6 +119,7 @@ final class ReactionContextBackgroundNode: ASDisplayNode {
isLeftAligned: Bool,
isMinimized: Bool,
isCoveredByInput: Bool,
displayTail: Bool,
transition: ContainedViewLayoutTransition
) {
let shadowInset: CGFloat = 15.0
@ -188,10 +189,10 @@ final class ReactionContextBackgroundNode: ASDisplayNode {
transition.updateFrame(layer: self.backgroundShadowLayer, frame: backgroundFrame.insetBy(dx: -shadowInset, dy: -shadowInset), beginWithCurrentState: true)
transition.updateFrame(layer: self.largeCircleShadowLayer, frame: largeCircleFrame.insetBy(dx: -shadowInset, dy: -shadowInset), beginWithCurrentState: true)
transition.updateAlpha(layer: self.largeCircleLayer, alpha: isCoveredByInput ? 0.0 : 1.0)
transition.updateAlpha(layer: self.largeCircleShadowLayer, alpha: isCoveredByInput ? 0.0 : 1.0)
transition.updateAlpha(layer: self.smallCircleLayer, alpha: isCoveredByInput ? 0.0 : 1.0)
transition.updateAlpha(layer: self.smallCircleShadowLayer, alpha: isCoveredByInput ? 0.0 : 1.0)
transition.updateAlpha(layer: self.largeCircleLayer, alpha: (isCoveredByInput || !displayTail) ? 0.0 : 1.0)
transition.updateAlpha(layer: self.largeCircleShadowLayer, alpha: (isCoveredByInput || !displayTail) ? 0.0 : 1.0)
transition.updateAlpha(layer: self.smallCircleLayer, alpha: (isCoveredByInput || !displayTail) ? 0.0 : 1.0)
transition.updateAlpha(layer: self.smallCircleShadowLayer, alpha: (isCoveredByInput || !displayTail) ? 0.0 : 1.0)
transition.updateFrame(layer: self.smallCircleShadowLayer, frame: smallCircleFrame.insetBy(dx: -shadowInset, dy: -shadowInset), beginWithCurrentState: true)

View File

@ -232,7 +232,11 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate {
private weak var animationTargetView: UIView?
private var animationHideNode: Bool = false
public var displayTail: Bool = true
private var didAnimateIn: Bool = false
public private(set) var isAnimatingOut: Bool = false
public private(set) var isAnimatingOutToReaction: Bool = false
public var contentHeight: CGFloat {
return self.currentContentHeight
@ -1179,6 +1183,7 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate {
isLeftAligned: isLeftAligned,
isMinimized: self.highlightedReaction != nil && !self.highlightedByHover,
isCoveredByInput: isCoveredByInput,
displayTail: self.displayTail,
transition: transition
)
@ -1654,6 +1659,8 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate {
}
public func animateOut(to targetAnchorRect: CGRect?, animatingOutToReaction: Bool) {
self.isAnimatingOut = true
self.backgroundNode.animateOut()
for (_, itemNode) in self.visibleItemNodes {
@ -1760,6 +1767,8 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate {
}
public func animateOutToReaction(value: MessageReaction.Reaction, targetView: UIView, hideNode: Bool, animateTargetContainer: UIView?, addStandaloneReactionAnimation: ((StandaloneReactionAnimation) -> Void)?, completion: @escaping () -> Void) {
self.isAnimatingOutToReaction = true
var foundItemNode: ReactionNode?
for (_, itemNode) in self.visibleItemNodes {
if let itemNode = itemNode as? ReactionNode, itemNode.item.reaction.rawValue == value {

View File

@ -621,7 +621,7 @@ private final class StickerPackContainer: ASDisplayNode {
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let undoController = UndoOverlayController(presentationData: presentationData, content: .sticker(context: context, file: file, title: nil, text: presentationData.strings.EmojiStatus_AppliedText, undoText: nil, customAction: nil), elevatedLayout: false, animateInAsReplacement: animateInAsReplacement, action: { _ in return false })
let undoController = UndoOverlayController(presentationData: presentationData, content: .sticker(context: context, file: file, loop: true, title: nil, text: presentationData.strings.EmojiStatus_AppliedText, undoText: nil, customAction: nil), elevatedLayout: false, animateInAsReplacement: animateInAsReplacement, action: { _ in return false })
controller.present(undoController, in: .window(.root))
}
let copyEmoji: (TelegramMediaFile) -> Void = { file in
@ -1998,7 +1998,7 @@ public final class StickerPackScreenImpl: ViewController {
if let strongSelf = self, let file = attribute.file {
let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 }
strongSelf.present(UndoOverlayController(presentationData: presentationData, content: .sticker(context: strongSelf.context, file: file, title: nil, text: presentationData.strings.Conversation_EmojiCopied, undoText: nil, customAction: nil), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .current)
strongSelf.present(UndoOverlayController(presentationData: presentationData, content: .sticker(context: strongSelf.context, file: file, loop: true, title: nil, text: presentationData.strings.Conversation_EmojiCopied, undoText: nil, customAction: nil), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .current)
}
}))

View File

@ -4336,7 +4336,7 @@ func replayFinalState(
switch storyItem {
case let .storyItemDeleted(id):
storyUpdates.append(InternalStoryUpdate.deleted(id))
case let .storyItem(flags, id, date, _, _, media, _, recentViewers, viewCount):
case let .storyItem(flags, id, date, _, _, media, privacy, recentViewers, viewCount):
let (parsedMedia, _, _, _) = textMediaAndExpirationTimerFromApiMedia(media, peerId)
if let parsedMedia = parsedMedia {
var seenPeers: [EnginePeer] = []
@ -4347,13 +4347,46 @@ func replayFinalState(
}
}
}
var parsedPrivacy: EngineStoryPrivacy?
if let privacy = privacy {
var base: EngineStoryPrivacy.Base = .everyone
var additionalPeerIds: [EnginePeer.Id] = []
for rule in privacy {
switch rule {
case .privacyValueAllowAll:
base = .everyone
case .privacyValueAllowContacts:
base = .contacts
case .privacyValueAllowCloseFriends:
base = .closeFriends
case let .privacyValueAllowUsers(users):
for id in users {
additionalPeerIds.append(EnginePeer.Id(namespace: Namespaces.Peer.CloudUser, id: EnginePeer.Id.Id._internalFromInt64Value(id)))
}
case let .privacyValueAllowChatParticipants(chats):
for id in chats {
if let peer = transaction.getPeer(EnginePeer.Id(namespace: Namespaces.Peer.CloudGroup, id: EnginePeer.Id.Id._internalFromInt64Value(id))) {
additionalPeerIds.append(peer.id)
} else if let peer = transaction.getPeer(EnginePeer.Id(namespace: Namespaces.Peer.CloudChannel, id: EnginePeer.Id.Id._internalFromInt64Value(id))) {
additionalPeerIds.append(peer.id)
}
}
default:
break
}
}
parsedPrivacy = EngineStoryPrivacy(base: base, additionallyIncludePeers: additionalPeerIds)
}
storyUpdates.append(InternalStoryUpdate.added(peerId: peerId, item: StoryListContext.Item(
id: id,
timestamp: date,
media: EngineMedia(parsedMedia),
isSeen: (flags & (1 << 4)) == 0,
seenCount: viewCount.flatMap(Int.init) ?? 0,
seenPeers: seenPeers
seenPeers: seenPeers,
privacy: parsedPrivacy
)))
}
}

View File

@ -7,7 +7,23 @@ public enum EngineStoryInputMedia {
case image(dimensions: PixelDimensions, data: Data)
}
func _internal_uploadStory(account: Account, media: EngineStoryInputMedia) -> Signal<Never, NoError> {
public struct EngineStoryPrivacy: Equatable {
public enum Base {
case everyone
case contacts
case closeFriends
}
public var base: Base
public var additionallyIncludePeers: [EnginePeer.Id]
public init(base: Base, additionallyIncludePeers: [EnginePeer.Id]) {
self.base = base
self.additionallyIncludePeers = additionallyIncludePeers
}
}
func _internal_uploadStory(account: Account, media: EngineStoryInputMedia, privacy: EngineStoryPrivacy) -> Signal<Never, NoError> {
switch media {
case let .image(dimensions, data):
let resource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max))
@ -51,47 +67,79 @@ func _internal_uploadStory(account: Account, media: EngineStoryInputMedia) -> Si
return .single(nil)
}
|> mapToSignal { result -> Signal<Never, NoError> in
switch result {
case let .content(content):
switch content.content {
case let .media(inputMedia, _):
return account.network.request(Api.functions.stories.sendStory(flags: 0, media: inputMedia, caption: nil, entities: nil, privacyRules: [.inputPrivacyValueAllowAll]))
|> map(Optional.init)
|> `catch` { _ -> Signal<Api.Updates?, NoError> in
return .single(nil)
return account.postbox.transaction { transaction -> Signal<Never, NoError> in
var privacyRules: [Api.InputPrivacyRule]
switch privacy.base {
case .everyone:
privacyRules = [.inputPrivacyValueAllowAll]
case .contacts:
privacyRules = [.inputPrivacyValueAllowContacts]
case .closeFriends:
privacyRules = [.inputPrivacyValueAllowCloseFriends]
}
var privacyUsers: [Api.InputUser] = []
var privacyChats: [Int64] = []
for peerId in privacy.additionallyIncludePeers {
if let peer = transaction.getPeer(peerId) {
if let _ = peer as? TelegramUser {
if let inputUser = apiInputUser(peer) {
privacyUsers.append(inputUser)
}
} else if peer is TelegramGroup || peer is TelegramChannel {
privacyChats.append(peer.id.id._internalGetInt64Value())
}
}
|> mapToSignal { updates -> Signal<Never, NoError> in
if let updates = updates {
for update in updates.allUpdates {
if case let .updateStories(stories) = update {
switch stories {
case .userStories(let userId, let apiStories), .userStoriesShort(let userId, let apiStories, _):
if PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(userId)) == account.peerId, apiStories.count == 1 {
switch apiStories[0] {
case let .storyItem(_, _, _, _, _, media, _, _, _):
let (parsedMedia, _, _, _) = textMediaAndExpirationTimerFromApiMedia(media, account.peerId)
if let parsedMedia = parsedMedia {
applyMediaResourceChanges(from: imageMedia, to: parsedMedia, postbox: account.postbox, force: false)
}
if !privacyUsers.isEmpty {
privacyRules.append(.inputPrivacyValueAllowUsers(users: privacyUsers))
}
if !privacyChats.isEmpty {
privacyRules.append(.inputPrivacyValueAllowChatParticipants(chats: privacyChats))
}
switch result {
case let .content(content):
switch content.content {
case let .media(inputMedia, _):
return account.network.request(Api.functions.stories.sendStory(flags: 0, media: inputMedia, caption: nil, entities: nil, privacyRules: privacyRules))
|> map(Optional.init)
|> `catch` { _ -> Signal<Api.Updates?, NoError> in
return .single(nil)
}
|> mapToSignal { updates -> Signal<Never, NoError> in
if let updates = updates {
for update in updates.allUpdates {
if case let .updateStories(stories) = update {
switch stories {
case .userStories(let userId, let apiStories), .userStoriesShort(let userId, let apiStories, _):
if PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(userId)) == account.peerId, apiStories.count == 1 {
switch apiStories[0] {
case let .storyItem(_, _, _, _, _, media, _, _, _):
let (parsedMedia, _, _, _) = textMediaAndExpirationTimerFromApiMedia(media, account.peerId)
if let parsedMedia = parsedMedia {
applyMediaResourceChanges(from: imageMedia, to: parsedMedia, postbox: account.postbox, force: false)
}
default:
break
}
default:
break
}
}
}
}
account.stateManager.addUpdates(updates)
}
account.stateManager.addUpdates(updates)
return .complete()
}
default:
return .complete()
}
default:
return .complete()
}
default:
return .complete()
}
|> switchToLatest
}
}
}

View File

@ -22,14 +22,16 @@ public final class StoryListContext {
public let isSeen: Bool
public let seenCount: Int
public let seenPeers: [EnginePeer]
public let privacy: EngineStoryPrivacy?
public init(id: Int64, timestamp: Int32, media: EngineMedia, isSeen: Bool, seenCount: Int, seenPeers: [EnginePeer]) {
public init(id: Int64, timestamp: Int32, media: EngineMedia, isSeen: Bool, seenCount: Int, seenPeers: [EnginePeer], privacy: EngineStoryPrivacy?) {
self.id = id
self.timestamp = timestamp
self.media = media
self.isSeen = isSeen
self.seenCount = seenCount
self.seenPeers = seenPeers
self.privacy = privacy
}
public static func ==(lhs: Item, rhs: Item) -> Bool {
@ -51,6 +53,9 @@ public final class StoryListContext {
if lhs.seenPeers != rhs.seenPeers {
return false
}
if lhs.privacy != rhs.privacy {
return false
}
return true
}
}
@ -228,7 +233,8 @@ public final class StoryListContext {
media: item.media,
isSeen: true,
seenCount: item.seenCount,
seenPeers: item.seenPeers
seenPeers: item.seenPeers,
privacy: item.privacy
)
itemSets[i] = PeerItemSet(
peerId: itemSets[i].peerId,
@ -307,7 +313,7 @@ public final class StoryListContext {
for apiStory in apiStories {
switch apiStory {
case let .storyItem(flags, id, date, _, _, media, _, recentViewers, viewCount):
case let .storyItem(flags, id, date, _, _, media, privacy, recentViewers, viewCount):
let (parsedMedia, _, _, _) = textMediaAndExpirationTimerFromApiMedia(media, peerId)
if let parsedMedia = parsedMedia {
var seenPeers: [EnginePeer] = []
@ -318,13 +324,46 @@ public final class StoryListContext {
}
}
}
var parsedPrivacy: EngineStoryPrivacy?
if let privacy = privacy {
var base: EngineStoryPrivacy.Base = .everyone
var additionalPeerIds: [EnginePeer.Id] = []
for rule in privacy {
switch rule {
case .privacyValueAllowAll:
base = .everyone
case .privacyValueAllowContacts:
base = .contacts
case .privacyValueAllowCloseFriends:
base = .closeFriends
case let .privacyValueAllowUsers(users):
for id in users {
additionalPeerIds.append(EnginePeer.Id(namespace: Namespaces.Peer.CloudUser, id: EnginePeer.Id.Id._internalFromInt64Value(id)))
}
case let .privacyValueAllowChatParticipants(chats):
for id in chats {
if let peer = transaction.getPeer(EnginePeer.Id(namespace: Namespaces.Peer.CloudGroup, id: EnginePeer.Id.Id._internalFromInt64Value(id))) {
additionalPeerIds.append(peer.id)
} else if let peer = transaction.getPeer(EnginePeer.Id(namespace: Namespaces.Peer.CloudChannel, id: EnginePeer.Id.Id._internalFromInt64Value(id))) {
additionalPeerIds.append(peer.id)
}
}
default:
break
}
}
parsedPrivacy = EngineStoryPrivacy(base: base, additionallyIncludePeers: additionalPeerIds)
}
let item = StoryListContext.Item(
id: id,
timestamp: date,
media: EngineMedia(parsedMedia),
isSeen: (flags & (1 << 4)) == 0,
seenCount: viewCount.flatMap(Int.init) ?? 0,
seenPeers: seenPeers
seenPeers: seenPeers,
privacy: parsedPrivacy
)
if !parsedItemSets.isEmpty && parsedItemSets[parsedItemSets.count - 1].peerId == peerId {
parsedItemSets[parsedItemSets.count - 1].items.append(item)
@ -425,7 +464,7 @@ public final class StoryListContext {
let peerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(apiUserId))
for apiStory in apiStories {
switch apiStory {
case let .storyItem(flags, id, date, _, _, media, _, recentViewers, viewCount):
case let .storyItem(flags, id, date, _, _, media, privacy, recentViewers, viewCount):
let (parsedMedia, _, _, _) = textMediaAndExpirationTimerFromApiMedia(media, peerId)
if let parsedMedia = parsedMedia {
var seenPeers: [EnginePeer] = []
@ -436,13 +475,46 @@ public final class StoryListContext {
}
}
}
var parsedPrivacy: EngineStoryPrivacy?
if let privacy = privacy {
var base: EngineStoryPrivacy.Base = .everyone
var additionalPeerIds: [EnginePeer.Id] = []
for rule in privacy {
switch rule {
case .privacyValueAllowAll:
base = .everyone
case .privacyValueAllowContacts:
base = .contacts
case .privacyValueAllowCloseFriends:
base = .closeFriends
case let .privacyValueAllowUsers(users):
for id in users {
additionalPeerIds.append(EnginePeer.Id(namespace: Namespaces.Peer.CloudUser, id: EnginePeer.Id.Id._internalFromInt64Value(id)))
}
case let .privacyValueAllowChatParticipants(chats):
for id in chats {
if let peer = transaction.getPeer(EnginePeer.Id(namespace: Namespaces.Peer.CloudGroup, id: EnginePeer.Id.Id._internalFromInt64Value(id))) {
additionalPeerIds.append(peer.id)
} else if let peer = transaction.getPeer(EnginePeer.Id(namespace: Namespaces.Peer.CloudChannel, id: EnginePeer.Id.Id._internalFromInt64Value(id))) {
additionalPeerIds.append(peer.id)
}
}
default:
break
}
}
parsedPrivacy = EngineStoryPrivacy(base: base, additionallyIncludePeers: additionalPeerIds)
}
let item = StoryListContext.Item(
id: id,
timestamp: date,
media: EngineMedia(parsedMedia),
isSeen: (flags & (1 << 4)) == 0,
seenCount: viewCount.flatMap(Int.init) ?? 0,
seenPeers: seenPeers
seenPeers: seenPeers,
privacy: parsedPrivacy
)
if !parsedItemSets.isEmpty && parsedItemSets[parsedItemSets.count - 1].peerId == peerId {
parsedItemSets[parsedItemSets.count - 1].items.append(item)

View File

@ -577,8 +577,8 @@ public extension TelegramEngine {
return StoryListContext(account: self.account, scope: .peer(id))
}
public func uploadStory(media: EngineStoryInputMedia) -> Signal<Never, NoError> {
return _internal_uploadStory(account: self.account, media: media)
public func uploadStory(media: EngineStoryInputMedia, privacy: EngineStoryPrivacy) -> Signal<Never, NoError> {
return _internal_uploadStory(account: self.account, media: media, privacy: privacy)
}
public func deleteStory(id: Int64) -> Signal<Never, NoError> {

View File

@ -657,7 +657,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode {
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let controller = UndoOverlayController(presentationData: presentationData, content: .sticker(context: context, file: file, title: nil, text: presentationData.strings.EmojiStatus_AppliedText, undoText: nil, customAction: nil), elevatedLayout: false, animateInAsReplacement: animateInAsReplacement, action: { _ in return false })
let controller = UndoOverlayController(presentationData: presentationData, content: .sticker(context: context, file: file, loop: true, title: nil, text: presentationData.strings.EmojiStatus_AppliedText, undoText: nil, customAction: nil), elevatedLayout: false, animateInAsReplacement: animateInAsReplacement, action: { _ in return false })
strongSelf.currentUndoOverlayController = controller
controllerInteraction.presentController(controller, nil)
},
@ -692,7 +692,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode {
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let controller = UndoOverlayController(presentationData: presentationData, content: .sticker(context: context, file: file, title: nil, text: presentationData.strings.EmojiPreview_CopyEmoji, undoText: nil, customAction: nil), elevatedLayout: false, animateInAsReplacement: animateInAsReplacement, action: { _ in return false })
let controller = UndoOverlayController(presentationData: presentationData, content: .sticker(context: context, file: file, loop: true, title: nil, text: presentationData.strings.EmojiPreview_CopyEmoji, undoText: nil, customAction: nil), elevatedLayout: false, animateInAsReplacement: animateInAsReplacement, action: { _ in return false })
strongSelf.currentUndoOverlayController = controller
controllerInteraction.presentController(controller, nil)
}
@ -831,7 +831,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode {
actionTitle = presentationData.strings.EmojiInput_PremiumEmojiToast_Action
}
let controller = UndoOverlayController(presentationData: presentationData, content: .sticker(context: context, file: file, title: nil, text: text, undoText: actionTitle, customAction: { [weak controllerInteraction] in
let controller = UndoOverlayController(presentationData: presentationData, content: .sticker(context: context, file: file, loop: true, title: nil, text: text, undoText: actionTitle, customAction: { [weak controllerInteraction] in
guard let controllerInteraction = controllerInteraction else {
return
}
@ -2321,7 +2321,7 @@ public final class EntityInputView: UIInputView, AttachmentTextInputPanelInputVi
if file.isPremiumEmoji && !hasPremium {
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
strongSelf.presentController?(UndoOverlayController(presentationData: presentationData, content: .sticker(context: context, file: file, title: nil, text: presentationData.strings.EmojiInput_PremiumEmojiToast_Text, undoText: presentationData.strings.EmojiInput_PremiumEmojiToast_Action, customAction: {
strongSelf.presentController?(UndoOverlayController(presentationData: presentationData, content: .sticker(context: context, file: file, loop: true, title: nil, text: presentationData.strings.EmojiInput_PremiumEmojiToast_Text, undoText: presentationData.strings.EmojiInput_PremiumEmojiToast_Action, customAction: {
guard let strongSelf = self else {
return
}
@ -2765,7 +2765,7 @@ public final class EmojiContentPeekBehaviorImpl: EmojiContentPeekBehavior {
|> deliverOnMainQueue).start(next: { result in
switch result {
case .generic:
interaction.presentGlobalOverlayController(UndoOverlayController(presentationData: presentationData, content: .sticker(context: context, file: file, title: nil, text: !isStarred ? presentationData.strings.Conversation_StickerAddedToFavorites : presentationData.strings.Conversation_StickerRemovedFromFavorites, undoText: nil, customAction: nil), elevatedLayout: false, action: { _ in return false }), nil)
interaction.presentGlobalOverlayController(UndoOverlayController(presentationData: presentationData, content: .sticker(context: context, file: file, loop: true, title: nil, text: !isStarred ? presentationData.strings.Conversation_StickerAddedToFavorites : presentationData.strings.Conversation_StickerRemovedFromFavorites, undoText: nil, customAction: nil), elevatedLayout: false, action: { _ in return false }), nil)
case let .limitExceeded(limit, premiumLimit):
let premiumConfiguration = PremiumConfiguration.with(appConfiguration: context.currentAppConfiguration.with { $0 })
let text: String
@ -2774,7 +2774,7 @@ public final class EmojiContentPeekBehaviorImpl: EmojiContentPeekBehavior {
} else {
text = presentationData.strings.Premium_MaxFavedStickersText("\(premiumLimit)").string
}
interaction.presentGlobalOverlayController(UndoOverlayController(presentationData: presentationData, content: .sticker(context: context, file: file, title: presentationData.strings.Premium_MaxFavedStickersTitle("\(limit)").string, text: text, undoText: nil, customAction: nil), elevatedLayout: false, action: { action in
interaction.presentGlobalOverlayController(UndoOverlayController(presentationData: presentationData, content: .sticker(context: context, file: file, loop: true, title: presentationData.strings.Premium_MaxFavedStickersTitle("\(limit)").string, text: text, undoText: nil, customAction: nil), elevatedLayout: false, action: { action in
if case .info = action {
let controller = PremiumIntroScreen(context: context, source: .savedStickers)
interaction.navigationController()?.pushViewController(controller)

View File

@ -16,15 +16,18 @@ public final class MediaRecordingPanelComponent: Component {
public let audioRecorder: ManagedAudioRecorder?
public let videoRecordingStatus: InstantVideoControllerRecordingStatus?
public let cancelFraction: CGFloat
public let insets: UIEdgeInsets
public init(
audioRecorder: ManagedAudioRecorder?,
videoRecordingStatus: InstantVideoControllerRecordingStatus?,
cancelFraction: CGFloat
cancelFraction: CGFloat,
insets: UIEdgeInsets
) {
self.audioRecorder = audioRecorder
self.videoRecordingStatus = videoRecordingStatus
self.cancelFraction = cancelFraction
self.insets = insets
}
public static func ==(lhs: MediaRecordingPanelComponent, rhs: MediaRecordingPanelComponent) -> Bool {
@ -37,6 +40,9 @@ public final class MediaRecordingPanelComponent: Component {
if lhs.cancelFraction != rhs.cancelFraction {
return false
}
if lhs.insets != rhs.insets {
return false
}
return true
}
@ -234,7 +240,7 @@ public final class MediaRecordingPanelComponent: Component {
if indicatorView.superview == nil {
self.addSubview(indicatorView)
}
transition.setFrame(view: indicatorView, frame: CGRect(origin: CGPoint(x: 3.0, y: floor((availableSize.height - indicatorSize.height) * 0.5)), size: indicatorSize))
transition.setFrame(view: indicatorView, frame: CGRect(origin: CGPoint(x: 3.0, y: component.insets.top + floor((availableSize.height - component.insets.top - component.insets.bottom - indicatorSize.height) * 0.5)), size: indicatorSize))
}
let timerTextSize = self.timerText.update(
@ -248,7 +254,7 @@ public final class MediaRecordingPanelComponent: Component {
self.addSubview(timerTextView)
timerTextView.layer.anchorPoint = CGPoint(x: 0.0, y: 0.5)
}
let timerTextFrame = CGRect(origin: CGPoint(x: 38.0, y: floor((availableSize.height - timerTextSize.height) * 0.5)), size: timerTextSize)
let timerTextFrame = CGRect(origin: CGPoint(x: 38.0, y: component.insets.top + floor((availableSize.height - component.insets.top - component.insets.bottom - timerTextSize.height) * 0.5)), size: timerTextSize)
transition.setPosition(view: timerTextView, position: CGPoint(x: timerTextFrame.minX, y: timerTextFrame.midY))
timerTextView.bounds = CGRect(origin: CGPoint(), size: timerTextFrame.size)
}
@ -266,7 +272,7 @@ public final class MediaRecordingPanelComponent: Component {
containerSize: CGSize(width: max(30.0, availableSize.width - 100.0), height: 44.0)
)
var textFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - cancelTextSize.width) * 0.5), y: floor((availableSize.height - cancelTextSize.height) * 0.5)), size: cancelTextSize)
var textFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - cancelTextSize.width) * 0.5), y: component.insets.top + floor((availableSize.height - component.insets.top - component.insets.bottom - cancelTextSize.height) * 0.5)), size: cancelTextSize)
let bandingStart: CGFloat = 0.0
let bandedOffset = abs(component.cancelFraction) - bandingStart

View File

@ -29,6 +29,8 @@ public final class MessageInputPanelComponent: Component {
public let reactionAction: (UIView) -> Void
public let audioRecorder: ManagedAudioRecorder?
public let videoRecordingStatus: InstantVideoControllerRecordingStatus?
public let displayGradient: Bool
public let bottomInset: CGFloat
public init(
externalState: ExternalState,
@ -41,7 +43,9 @@ public final class MessageInputPanelComponent: Component {
attachmentAction: @escaping () -> Void,
reactionAction: @escaping (UIView) -> Void,
audioRecorder: ManagedAudioRecorder?,
videoRecordingStatus: InstantVideoControllerRecordingStatus?
videoRecordingStatus: InstantVideoControllerRecordingStatus?,
displayGradient: Bool,
bottomInset: CGFloat
) {
self.externalState = externalState
self.context = context
@ -54,6 +58,8 @@ public final class MessageInputPanelComponent: Component {
self.reactionAction = reactionAction
self.audioRecorder = audioRecorder
self.videoRecordingStatus = videoRecordingStatus
self.displayGradient = displayGradient
self.bottomInset = bottomInset
}
public static func ==(lhs: MessageInputPanelComponent, rhs: MessageInputPanelComponent) -> Bool {
@ -75,6 +81,12 @@ public final class MessageInputPanelComponent: Component {
if lhs.videoRecordingStatus !== rhs.videoRecordingStatus {
return false
}
if lhs.displayGradient != rhs.displayGradient {
return false
}
if lhs.bottomInset != rhs.bottomInset {
return false
}
return true
}
@ -83,7 +95,12 @@ public final class MessageInputPanelComponent: Component {
}
public final class View: UIView {
private let fieldBackgroundView: UIImageView
private let fieldBackgroundView: BlurredBackgroundView
private let vibrancyEffectView: UIVisualEffectView
private let gradientView: UIImageView
private let bottomGradientView: UIView
private let placeholder = ComponentView<Empty>()
private let textField = ComponentView<Empty>()
private let textFieldExternalState = TextFieldComponent.ExternalState()
@ -103,10 +120,22 @@ public final class MessageInputPanelComponent: Component {
private weak var state: EmptyComponentState?
override init(frame: CGRect) {
self.fieldBackgroundView = UIImageView()
self.fieldBackgroundView = BlurredBackgroundView(color: UIColor(white: 0.0, alpha: 0.5), enableBlur: true)
let style: UIBlurEffect.Style = .dark
let blurEffect = UIBlurEffect(style: style)
let vibrancyEffect = UIVibrancyEffect(blurEffect: blurEffect)
let vibrancyEffectView = UIVisualEffectView(effect: vibrancyEffect)
self.vibrancyEffectView = vibrancyEffectView
self.gradientView = UIImageView()
self.bottomGradientView = UIView()
super.init(frame: frame)
self.addSubview(self.bottomGradientView)
self.addSubview(self.gradientView)
self.fieldBackgroundView.addSubview(self.vibrancyEffectView)
self.addSubview(self.fieldBackgroundView)
}
@ -136,15 +165,41 @@ public final class MessageInputPanelComponent: Component {
}
func update(component: MessageInputPanelComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
let baseHeight: CGFloat = 44.0
let insets = UIEdgeInsets(top: 5.0, left: 41.0, bottom: 5.0, right: 41.0)
let fieldCornerRadius: CGFloat = 16.0
let insets = UIEdgeInsets(top: 14.0, left: 50.0, bottom: 6.0, right: 50.0)
let baseFieldHeight: CGFloat = 40.0
self.component = component
self.state = state
if self.fieldBackgroundView.image == nil {
self.fieldBackgroundView.image = generateStretchableFilledCircleImage(diameter: fieldCornerRadius * 2.0, color: nil, strokeColor: UIColor(white: 1.0, alpha: 0.16), strokeWidth: 1.0, backgroundColor: nil)
let hasMediaRecording = component.audioRecorder != nil || component.videoRecordingStatus != nil
let topGradientHeight: CGFloat = 32.0
if self.gradientView.image == nil {
let baseAlpha: CGFloat = 0.7
self.gradientView.image = generateImage(CGSize(width: insets.left + insets.right + baseFieldHeight, height: topGradientHeight + insets.top + baseFieldHeight + insets.bottom), rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
var locations: [CGFloat] = []
var colors: [CGColor] = []
let numStops = 10
for i in 0 ..< numStops {
let step = 1.0 - CGFloat(i) / CGFloat(numStops - 1)
locations.append((1.0 - step))
let alphaStep: CGFloat = pow(step, 1.5)
colors.append(UIColor.black.withAlphaComponent(alphaStep * baseAlpha).cgColor)
}
if let gradient = CGGradient(colorsSpace: context.colorSpace, colors: colors as CFArray, locations: &locations) {
context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: size.height), end: CGPoint(x: 0.0, y: 0.0), options: CGGradientDrawingOptions())
}
context.setBlendMode(.copy)
context.setFillColor(UIColor.clear.cgColor)
context.fillEllipse(in: CGRect(origin: CGPoint(x: insets.left, y: topGradientHeight + insets.top), size: CGSize(width: baseFieldHeight, height: baseFieldHeight)).insetBy(dx: 3.0, dy: 3.0))
})?.resizableImage(withCapInsets: UIEdgeInsets(top: topGradientHeight + insets.top + baseFieldHeight * 0.5, left: insets.left + baseFieldHeight * 0.5, bottom: insets.bottom + baseFieldHeight * 0.5, right: insets.right + baseFieldHeight * 0.5))
self.bottomGradientView.backgroundColor = UIColor.black.withAlphaComponent(baseAlpha)
}
let availableTextFieldSize = CGSize(width: availableSize.width - insets.left - insets.right, height: availableSize.height - insets.top - insets.bottom)
@ -154,17 +209,45 @@ public final class MessageInputPanelComponent: Component {
transition: .immediate,
component: AnyComponent(TextFieldComponent(
externalState: self.textFieldExternalState,
placeholder: "Reply Privately..."
placeholder: ""
)),
environment: {},
containerSize: availableTextFieldSize
)
let placeholderSize = self.placeholder.update(
transition: .immediate,
component: AnyComponent(Text(
text: "Reply Privately",
font: Font.regular(17.0),
color: .white
)),
environment: {},
containerSize: availableTextFieldSize
)
let fieldFrame = CGRect(origin: CGPoint(x: insets.left, y: insets.top), size: CGSize(width: availableSize.width - insets.left - insets.right, height: textFieldSize.height))
transition.setFrame(view: self.fieldBackgroundView, frame: fieldFrame)
transition.setAlpha(view: self.fieldBackgroundView, alpha: (component.audioRecorder != nil || component.videoRecordingStatus != nil) ? 0.0 : 1.0)
transition.setFrame(view: self.vibrancyEffectView, frame: CGRect(origin: CGPoint(), size: fieldFrame.size))
transition.setAlpha(view: self.vibrancyEffectView, alpha: (component.audioRecorder != nil || component.videoRecordingStatus != nil) ? 0.0 : 1.0)
//let rightFieldInset: CGFloat = 34.0
transition.setFrame(view: self.fieldBackgroundView, frame: fieldFrame)
self.fieldBackgroundView.update(size: fieldFrame.size, cornerRadius: baseFieldHeight * 0.5, transition: transition.containedViewLayoutTransition)
let gradientFrame = CGRect(origin: CGPoint(x: 0.0, y: -topGradientHeight), size: CGSize(width: availableSize.width, height: topGradientHeight + fieldFrame.maxY + insets.bottom))
transition.setFrame(view: self.gradientView, frame: gradientFrame)
transition.setFrame(view: self.bottomGradientView, frame: CGRect(origin: CGPoint(x: 0.0, y: gradientFrame.maxY), size: CGSize(width: availableSize.width, height: component.bottomInset)))
transition.setAlpha(view: self.gradientView, alpha: component.displayGradient ? 1.0 : 0.0)
transition.setAlpha(view: self.bottomGradientView, alpha: component.displayGradient ? 1.0 : 0.0)
let placeholderFrame = CGRect(origin: CGPoint(x: 16.0, y: floor((fieldFrame.height - placeholderSize.height) * 0.5)), size: placeholderSize)
if let placeholderView = self.placeholder.view {
if placeholderView.superview == nil {
placeholderView.layer.anchorPoint = CGPoint()
self.vibrancyEffectView.contentView.addSubview(placeholderView)
}
transition.setPosition(view: placeholderView, position: placeholderFrame.origin)
placeholderView.bounds = CGRect(origin: CGPoint(), size: placeholderFrame.size)
}
let size = CGSize(width: availableSize.width, height: textFieldSize.height + insets.top + insets.bottom)
@ -189,15 +272,15 @@ public final class MessageInputPanelComponent: Component {
}
self.component?.attachmentAction()
}
).minSize(CGSize(width: 41.0, height: baseHeight))),
).minSize(CGSize(width: 41.0, height: baseFieldHeight))),
environment: {},
containerSize: CGSize(width: 41.0, height: baseHeight)
containerSize: CGSize(width: 41.0, height: baseFieldHeight)
)
if let attachmentButtonView = self.attachmentButton.view {
if attachmentButtonView.superview == nil {
self.addSubview(attachmentButtonView)
}
transition.setFrame(view: attachmentButtonView, frame: CGRect(origin: CGPoint(x: floor((insets.left - attachmentButtonSize.width) * 0.5), y: size.height - baseHeight + floor((baseHeight - attachmentButtonSize.height) * 0.5)), size: attachmentButtonSize))
transition.setFrame(view: attachmentButtonView, frame: CGRect(origin: CGPoint(x: floor((insets.left - attachmentButtonSize.width) * 0.5), y: size.height - insets.bottom - baseFieldHeight + floor((baseFieldHeight - attachmentButtonSize.height) * 0.5)), size: attachmentButtonSize))
}
let inputActionButtonSize = self.inputActionButton.update(
@ -251,7 +334,7 @@ public final class MessageInputPanelComponent: Component {
if inputActionButtonView.superview == nil {
self.addSubview(inputActionButtonView)
}
transition.setFrame(view: inputActionButtonView, frame: CGRect(origin: CGPoint(x: size.width - insets.right + floorToScreenPixels((insets.right - inputActionButtonSize.width) * 0.5), y: size.height - baseHeight + floorToScreenPixels((baseHeight - inputActionButtonSize.height) * 0.5)), size: inputActionButtonSize))
transition.setFrame(view: inputActionButtonView, frame: CGRect(origin: CGPoint(x: size.width - insets.right + floorToScreenPixels((insets.right - inputActionButtonSize.width) * 0.5), y: size.height - insets.bottom - baseFieldHeight + floorToScreenPixels((baseFieldHeight - inputActionButtonSize.height) * 0.5)), size: inputActionButtonSize))
}
var fieldIconNextX = fieldFrame.maxX - 2.0
let stickerButtonSize = self.stickerButton.update(
@ -279,7 +362,7 @@ public final class MessageInputPanelComponent: Component {
transition.setPosition(view: stickerButtonView, position: stickerIconFrame.center)
transition.setBounds(view: stickerButtonView, bounds: CGRect(origin: CGPoint(), size: stickerIconFrame.size))
transition.setAlpha(view: stickerButtonView, alpha: self.textFieldExternalState.hasText ? 0.0 : 1.0)
transition.setAlpha(view: stickerButtonView, alpha: (self.textFieldExternalState.hasText || hasMediaRecording) ? 0.0 : 1.0)
transition.setScale(view: stickerButtonView, scale: self.textFieldExternalState.hasText ? 0.1 : 1.0)
fieldIconNextX -= stickerButtonSize.width + 2.0
@ -310,22 +393,17 @@ public final class MessageInputPanelComponent: Component {
transition.setPosition(view: reactionButtonView, position: reactionIconFrame.center)
transition.setBounds(view: reactionButtonView, bounds: CGRect(origin: CGPoint(), size: reactionIconFrame.size))
transition.setAlpha(view: reactionButtonView, alpha: self.textFieldExternalState.hasText ? 0.0 : 1.0)
transition.setAlpha(view: reactionButtonView, alpha: (self.textFieldExternalState.hasText || hasMediaRecording) ? 0.0 : 1.0)
transition.setScale(view: reactionButtonView, scale: self.textFieldExternalState.hasText ? 0.1 : 1.0)
fieldIconNextX -= reactionButtonSize.width + 2.0
}
/*if let image = self.reactionIconView.image {
let stickerIconFrame = CGRect(origin: CGPoint(x: fieldIconNextX - image.size.width, y: fieldFrame.minY + floor((fieldFrame.height - image.size.height) * 0.5)), size: image.size)
transition.setPosition(view: self.reactionIconView, position: stickerIconFrame.center)
transition.setBounds(view: self.reactionIconView, bounds: CGRect(origin: CGPoint(), size: stickerIconFrame.size))
transition.setAlpha(view: self.reactionIconView, alpha: self.textFieldExternalState.hasText ? 0.0 : 1.0)
transition.setScale(view: self.reactionIconView, scale: self.textFieldExternalState.hasText ? 0.1 : 1.0)
fieldIconNextX -= image.size.width + 4.0
}*/
self.fieldBackgroundView.updateColor(color: self.textFieldExternalState.isEditing ? UIColor(white: 0.0, alpha: 0.5) : UIColor(white: 1.0, alpha: 0.09), transition: transition.containedViewLayoutTransition)
transition.setAlpha(view: self.fieldBackgroundView, alpha: hasMediaRecording ? 0.0 : 1.0)
if let placeholderView = self.placeholder.view {
placeholderView.isHidden = self.textFieldExternalState.hasText
}
component.externalState.isEditing = self.textFieldExternalState.isEditing
component.externalState.hasText = self.textFieldExternalState.hasText
@ -353,7 +431,8 @@ public final class MessageInputPanelComponent: Component {
component: AnyComponent(MediaRecordingPanelComponent(
audioRecorder: component.audioRecorder,
videoRecordingStatus: component.videoRecordingStatus,
cancelFraction: self.mediaCancelFraction
cancelFraction: self.mediaCancelFraction,
insets: insets
)),
environment: {},
containerSize: size

View File

@ -0,0 +1,37 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "ShareWithPeersScreen",
module_name = "ShareWithPeersScreen",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/AsyncDisplayKit",
"//submodules/Display",
"//submodules/Postbox",
"//submodules/TelegramCore",
"//submodules/SSignalKit/SwiftSignalKit",
"//submodules/ComponentFlow",
"//submodules/Components/ViewControllerComponent",
"//submodules/Components/ComponentDisplayAdapters",
"//submodules/Components/MultilineTextComponent",
"//submodules/TelegramPresentationData",
"//submodules/AccountContext",
"//submodules/AppBundle",
"//submodules/TelegramStringFormatting",
"//submodules/PresentationDataUtils",
"//submodules/Components/SolidRoundedButtonComponent",
"//submodules/TelegramUI/Components/ButtonComponent",
"//submodules/TelegramUI/Components/PlainButtonComponent",
"//submodules/TelegramUI/Components/AnimatedCounterComponent",
"//submodules/AvatarNode",
"//submodules/CheckNode",
],
visibility = [
"//visibility:public",
],
)

View File

@ -0,0 +1,196 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import ComponentFlow
import MultilineTextComponent
import TelegramPresentationData
final class ActionListItemComponent: Component {
let theme: PresentationTheme
let sideInset: CGFloat
let iconName: String?
let title: String
let hasNext: Bool
let action: () -> Void
init(
theme: PresentationTheme,
sideInset: CGFloat,
iconName: String?,
title: String,
hasNext: Bool,
action: @escaping () -> Void
) {
self.theme = theme
self.sideInset = sideInset
self.iconName = iconName
self.title = title
self.hasNext = hasNext
self.action = action
}
static func ==(lhs: ActionListItemComponent, rhs: ActionListItemComponent) -> Bool {
if lhs.theme !== rhs.theme {
return false
}
if lhs.sideInset != rhs.sideInset {
return false
}
if lhs.iconName != rhs.iconName {
return false
}
if lhs.title != rhs.title {
return false
}
if lhs.hasNext != rhs.hasNext {
return false
}
return true
}
final class View: UIView {
private let containerButton: HighlightTrackingButton
private let title = ComponentView<Empty>()
private let iconView: UIImageView
private let separatorLayer: SimpleLayer
private var highlightBackgroundFrame: CGRect?
private var highlightBackgroundLayer: SimpleLayer?
private var component: ActionListItemComponent?
private weak var state: EmptyComponentState?
override init(frame: CGRect) {
self.separatorLayer = SimpleLayer()
self.containerButton = HighlightTrackingButton()
self.iconView = UIImageView()
super.init(frame: frame)
self.layer.addSublayer(self.separatorLayer)
self.addSubview(self.containerButton)
self.containerButton.addSubview(self.iconView)
self.containerButton.highligthedChanged = { [weak self] isHighlighted in
guard let self, let component = self.component, let highlightBackgroundFrame = self.highlightBackgroundFrame else {
return
}
if isHighlighted {
self.superview?.bringSubviewToFront(self)
let highlightBackgroundLayer: SimpleLayer
if let current = self.highlightBackgroundLayer {
highlightBackgroundLayer = current
} else {
highlightBackgroundLayer = SimpleLayer()
self.highlightBackgroundLayer = highlightBackgroundLayer
self.layer.insertSublayer(highlightBackgroundLayer, above: self.separatorLayer)
highlightBackgroundLayer.backgroundColor = component.theme.list.itemHighlightedBackgroundColor.cgColor
}
highlightBackgroundLayer.frame = highlightBackgroundFrame
highlightBackgroundLayer.opacity = 1.0
} else {
if let highlightBackgroundLayer = self.highlightBackgroundLayer {
self.highlightBackgroundLayer = nil
highlightBackgroundLayer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { [weak highlightBackgroundLayer] _ in
highlightBackgroundLayer?.removeFromSuperlayer()
})
}
}
}
self.containerButton.addTarget(self, action: #selector(self.pressed), for: .touchUpInside)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
@objc private func pressed() {
guard let component = self.component else {
return
}
component.action()
}
func update(component: ActionListItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
let themeUpdated = self.component?.theme !== component.theme
if self.component?.iconName != component.iconName {
if let iconName = component.iconName {
self.iconView.image = UIImage(bundleImageName: iconName)?.withRenderingMode(.alwaysTemplate)
} else {
self.iconView.image = nil
}
}
if themeUpdated {
self.iconView.tintColor = component.theme.list.itemAccentColor
}
self.component = component
self.state = state
let contextInset: CGFloat = 0.0
let height: CGFloat = 44.0
let verticalInset: CGFloat = 1.0
let leftInset: CGFloat = 62.0 + component.sideInset
let rightInset: CGFloat = contextInset * 2.0 + 8.0 + component.sideInset
let previousTitleFrame = self.title.view?.frame
let titleSize = self.title.update(
transition: .immediate,
component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(string: component.title, font: Font.regular(17.0), textColor: component.theme.list.itemAccentColor))
)),
environment: {},
containerSize: CGSize(width: availableSize.width - leftInset - rightInset, height: 100.0)
)
let centralContentHeight: CGFloat = titleSize.height
let titleFrame = CGRect(origin: CGPoint(x: leftInset, y: floor((height - verticalInset * 2.0 - centralContentHeight) / 2.0)), size: titleSize)
if let titleView = self.title.view {
if titleView.superview == nil {
titleView.isUserInteractionEnabled = false
self.containerButton.addSubview(titleView)
}
titleView.frame = titleFrame
if let previousTitleFrame, previousTitleFrame.origin.x != titleFrame.origin.x {
transition.animatePosition(view: titleView, from: CGPoint(x: previousTitleFrame.origin.x - titleFrame.origin.x, y: 0.0), to: CGPoint(), additive: true)
}
}
if let iconImage = self.iconView.image {
transition.setFrame(view: self.iconView, frame: CGRect(origin: CGPoint(x: floor((leftInset - iconImage.size.width) / 2.0), y: floor((height - iconImage.size.height) / 2.0)), size: iconImage.size))
}
if themeUpdated {
self.separatorLayer.backgroundColor = component.theme.list.itemPlainSeparatorColor.cgColor
}
transition.setFrame(layer: self.separatorLayer, frame: CGRect(origin: CGPoint(x: leftInset, y: height), size: CGSize(width: availableSize.width - leftInset, height: UIScreenPixel)))
self.separatorLayer.isHidden = !component.hasNext
self.highlightBackgroundFrame = CGRect(origin: CGPoint(), size: CGSize(width: availableSize.width, height: height + ((component.hasNext) ? UIScreenPixel : 0.0)))
let containerFrame = CGRect(origin: CGPoint(x: contextInset, y: verticalInset), size: CGSize(width: availableSize.width - contextInset * 2.0, height: height - verticalInset * 2.0))
transition.setFrame(view: self.containerButton, frame: containerFrame)
return CGSize(width: availableSize.width, height: height)
}
}
func makeView() -> View {
return View(frame: CGRect())
}
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}

View File

@ -0,0 +1,312 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import ComponentFlow
import SwiftSignalKit
import AccountContext
import TelegramCore
import MultilineTextComponent
import AvatarNode
import TelegramPresentationData
import CheckNode
import TelegramStringFormatting
private let avatarFont = avatarPlaceholderFont(size: 15.0)
final class PeerListItemComponent: Component {
enum SelectionState: Equatable {
case none
case editing(isSelected: Bool, isTinted: Bool)
}
let context: AccountContext
let theme: PresentationTheme
let strings: PresentationStrings
let sideInset: CGFloat
let title: String
let peer: EnginePeer?
let subtitle: String?
let selectionState: SelectionState
let hasNext: Bool
let action: (EnginePeer) -> Void
init(
context: AccountContext,
theme: PresentationTheme,
strings: PresentationStrings,
sideInset: CGFloat,
title: String,
peer: EnginePeer?,
subtitle: String?,
selectionState: SelectionState,
hasNext: Bool,
action: @escaping (EnginePeer) -> Void
) {
self.context = context
self.theme = theme
self.strings = strings
self.sideInset = sideInset
self.title = title
self.peer = peer
self.subtitle = subtitle
self.selectionState = selectionState
self.hasNext = hasNext
self.action = action
}
static func ==(lhs: PeerListItemComponent, rhs: PeerListItemComponent) -> Bool {
if lhs.context !== rhs.context {
return false
}
if lhs.theme !== rhs.theme {
return false
}
if lhs.strings !== rhs.strings {
return false
}
if lhs.sideInset != rhs.sideInset {
return false
}
if lhs.title != rhs.title {
return false
}
if lhs.peer != rhs.peer {
return false
}
if lhs.subtitle != rhs.subtitle {
return false
}
if lhs.selectionState != rhs.selectionState {
return false
}
if lhs.hasNext != rhs.hasNext {
return false
}
return true
}
final class View: UIView {
private let containerButton: HighlightTrackingButton
private let title = ComponentView<Empty>()
private let label = ComponentView<Empty>()
private let separatorLayer: SimpleLayer
private let avatarNode: AvatarNode
private var checkLayer: CheckLayer?
private var component: PeerListItemComponent?
private weak var state: EmptyComponentState?
override init(frame: CGRect) {
self.separatorLayer = SimpleLayer()
self.containerButton = HighlightTrackingButton()
self.avatarNode = AvatarNode(font: avatarFont)
self.avatarNode.isLayerBacked = true
super.init(frame: frame)
self.layer.addSublayer(self.separatorLayer)
self.addSubview(self.containerButton)
self.containerButton.layer.addSublayer(self.avatarNode.layer)
self.containerButton.addTarget(self, action: #selector(self.pressed), for: .touchUpInside)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
@objc private func pressed() {
guard let component = self.component, let peer = component.peer else {
return
}
component.action(peer)
}
func update(component: PeerListItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
let themeUpdated = self.component?.theme !== component.theme
var hasSelectionUpdated = false
if let previousComponent = self.component {
switch previousComponent.selectionState {
case .none:
if case .none = component.selectionState {
} else {
hasSelectionUpdated = true
}
case .editing:
if case .editing = component.selectionState {
} else {
hasSelectionUpdated = true
}
}
}
self.component = component
self.state = state
let contextInset: CGFloat = 0.0
let height: CGFloat = 60.0
let verticalInset: CGFloat = 1.0
var leftInset: CGFloat = 62.0 + component.sideInset
let rightInset: CGFloat = contextInset * 2.0 + 8.0 + component.sideInset
var avatarLeftInset: CGFloat = component.sideInset + 10.0
if case let .editing(isSelected, isTinted) = component.selectionState {
leftInset += 44.0
avatarLeftInset += 44.0
let checkSize: CGFloat = 22.0
let checkLayer: CheckLayer
if let current = self.checkLayer {
checkLayer = current
if themeUpdated {
var theme = CheckNodeTheme(theme: component.theme, style: .plain)
if isTinted {
theme.backgroundColor = theme.backgroundColor.mixedWith(component.theme.list.itemBlocksBackgroundColor, alpha: 0.5)
}
checkLayer.theme = theme
}
checkLayer.setSelected(isSelected, animated: !transition.animation.isImmediate)
} else {
var theme = CheckNodeTheme(theme: component.theme, style: .plain)
if isTinted {
theme.backgroundColor = theme.backgroundColor.mixedWith(component.theme.list.itemBlocksBackgroundColor, alpha: 0.5)
}
checkLayer = CheckLayer(theme: theme)
self.checkLayer = checkLayer
self.containerButton.layer.addSublayer(checkLayer)
checkLayer.frame = CGRect(origin: CGPoint(x: -checkSize, y: floor((height - verticalInset * 2.0 - checkSize) / 2.0)), size: CGSize(width: checkSize, height: checkSize))
checkLayer.setSelected(isSelected, animated: false)
checkLayer.setNeedsDisplay()
}
transition.setFrame(layer: checkLayer, frame: CGRect(origin: CGPoint(x: floor((54.0 - checkSize) * 0.5), y: floor((height - verticalInset * 2.0 - checkSize) / 2.0)), size: CGSize(width: checkSize, height: checkSize)))
} else {
if let checkLayer = self.checkLayer {
self.checkLayer = nil
transition.setPosition(layer: checkLayer, position: CGPoint(x: -checkLayer.bounds.width * 0.5, y: checkLayer.position.y), completion: { [weak checkLayer] _ in
checkLayer?.removeFromSuperlayer()
})
}
}
let avatarSize: CGFloat = 40.0
let avatarFrame = CGRect(origin: CGPoint(x: avatarLeftInset, y: floor((height - verticalInset * 2.0 - avatarSize) / 2.0)), size: CGSize(width: avatarSize, height: avatarSize))
if self.avatarNode.bounds.isEmpty {
self.avatarNode.frame = avatarFrame
} else {
transition.setFrame(layer: self.avatarNode.layer, frame: avatarFrame)
}
if let peer = component.peer {
let clipStyle: AvatarNodeClipStyle
if case let .channel(channel) = peer, channel.flags.contains(.isForum) {
clipStyle = .roundedRect
} else {
clipStyle = .round
}
if peer.id == component.context.account.peerId {
self.avatarNode.setPeer(context: component.context, theme: component.theme, peer: peer, overrideImage: .savedMessagesIcon, clipStyle: clipStyle, displayDimensions: CGSize(width: avatarSize, height: avatarSize))
} else {
self.avatarNode.setPeer(context: component.context, theme: component.theme, peer: peer, clipStyle: clipStyle, displayDimensions: CGSize(width: avatarSize, height: avatarSize))
}
}
let labelData: (String, Bool)
if let subtitle = component.subtitle {
labelData = (subtitle, false)
} else if case .legacyGroup = component.peer {
labelData = (component.strings.Group_Status, false)
} else if case let .channel(channel) = component.peer {
if case .group = channel.info {
labelData = (component.strings.Group_Status, false)
} else {
labelData = (component.strings.Channel_Status, false)
}
} else {
labelData = (component.strings.Group_Status, false)
}
let labelSize = self.label.update(
transition: .immediate,
component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(string: labelData.0, font: Font.regular(15.0), textColor: labelData.1 ? component.theme.list.itemAccentColor : component.theme.list.itemSecondaryTextColor))
)),
environment: {},
containerSize: CGSize(width: availableSize.width - leftInset - rightInset, height: 100.0)
)
let previousTitleFrame = self.title.view?.frame
var previousTitleContents: UIView?
if hasSelectionUpdated && !"".isEmpty {
previousTitleContents = self.title.view?.snapshotView(afterScreenUpdates: false)
}
let titleSize = self.title.update(
transition: .immediate,
component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(string: component.title, font: Font.semibold(17.0), textColor: component.theme.list.itemPrimaryTextColor))
)),
environment: {},
containerSize: CGSize(width: availableSize.width - leftInset - rightInset, height: 100.0)
)
let titleSpacing: CGFloat = 1.0
let centralContentHeight: CGFloat = titleSize.height + labelSize.height + titleSpacing
let titleFrame = CGRect(origin: CGPoint(x: leftInset, y: floor((height - verticalInset * 2.0 - centralContentHeight) / 2.0)), size: titleSize)
if let titleView = self.title.view {
if titleView.superview == nil {
titleView.isUserInteractionEnabled = false
self.containerButton.addSubview(titleView)
}
titleView.frame = titleFrame
if let previousTitleFrame, previousTitleFrame.origin.x != titleFrame.origin.x {
transition.animatePosition(view: titleView, from: CGPoint(x: previousTitleFrame.origin.x - titleFrame.origin.x, y: 0.0), to: CGPoint(), additive: true)
}
if let previousTitleFrame, let previousTitleContents, previousTitleFrame.size != titleSize {
previousTitleContents.frame = CGRect(origin: previousTitleFrame.origin, size: previousTitleFrame.size)
self.addSubview(previousTitleContents)
transition.setFrame(view: previousTitleContents, frame: CGRect(origin: titleFrame.origin, size: previousTitleFrame.size))
transition.setAlpha(view: previousTitleContents, alpha: 0.0, completion: { [weak previousTitleContents] _ in
previousTitleContents?.removeFromSuperview()
})
transition.animateAlpha(view: titleView, from: 0.0, to: 1.0)
}
}
if let labelView = self.label.view {
if labelView.superview == nil {
labelView.isUserInteractionEnabled = false
self.containerButton.addSubview(labelView)
}
transition.setFrame(view: labelView, frame: CGRect(origin: CGPoint(x: titleFrame.minX, y: titleFrame.maxY + titleSpacing), size: labelSize))
}
if themeUpdated {
self.separatorLayer.backgroundColor = component.theme.list.itemPlainSeparatorColor.cgColor
}
transition.setFrame(layer: self.separatorLayer, frame: CGRect(origin: CGPoint(x: leftInset, y: height), size: CGSize(width: availableSize.width - leftInset, height: UIScreenPixel)))
self.separatorLayer.isHidden = !component.hasNext
let containerFrame = CGRect(origin: CGPoint(x: contextInset, y: verticalInset), size: CGSize(width: availableSize.width - contextInset * 2.0, height: height - verticalInset * 2.0))
transition.setFrame(view: self.containerButton, frame: containerFrame)
return CGSize(width: availableSize.width, height: height)
}
}
func makeView() -> View {
return View(frame: CGRect())
}
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}

View File

@ -45,9 +45,11 @@ swift_library(
"//submodules/TelegramUI/Components/LegacyInstantVideoController",
"//submodules/TelegramUI/Components/EntityKeyboard",
"//submodules/TelegramUI/Components/Stories/StoryFooterPanelComponent",
"//submodules/TelegramUI/Components/ShareWithPeersScreen",
"//submodules/TelegramPresentationData",
"//submodules/ReactionSelectionNode",
"//submodules/ContextUI",
"//submodules/AvatarNode",
],
visibility = [
"//visibility:public",

View File

@ -41,7 +41,7 @@ public final class StoryContentItem {
public let component: AnyComponent<StoryContentItem.Environment>
public let centerInfoComponent: AnyComponent<Empty>?
public let rightInfoComponent: AnyComponent<Empty>?
public let targetMessageId: EngineMessage.Id?
public let peerId: EnginePeer.Id?
public let storyItem: StoryListContext.Item?
public let preload: Signal<Never, NoError>?
public let delete: (() -> Void)?
@ -55,7 +55,7 @@ public final class StoryContentItem {
component: AnyComponent<StoryContentItem.Environment>,
centerInfoComponent: AnyComponent<Empty>?,
rightInfoComponent: AnyComponent<Empty>?,
targetMessageId: EngineMessage.Id?,
peerId: EnginePeer.Id?,
storyItem: StoryListContext.Item?,
preload: Signal<Never, NoError>?,
delete: (() -> Void)?,
@ -68,7 +68,7 @@ public final class StoryContentItem {
self.component = component
self.centerInfoComponent = centerInfoComponent
self.rightInfoComponent = rightInfoComponent
self.targetMessageId = targetMessageId
self.peerId = peerId
self.storyItem = storyItem
self.preload = preload
self.delete = delete

View File

@ -14,6 +14,8 @@ import AccountContext
import LegacyInstantVideoController
import UndoUI
import ContextUI
import TelegramCore
import AvatarNode
public final class StoryItemSetContainerComponent: Component {
public final class ExternalState {
@ -37,8 +39,6 @@ public final class StoryItemSetContainerComponent: Component {
public let safeInsets: UIEdgeInsets
public let inputHeight: CGFloat
public let isProgressPaused: Bool
public let audioRecorder: ManagedAudioRecorder?
public let videoRecorder: InstantVideoController?
public let hideUI: Bool
public let presentController: (ViewController) -> Void
public let close: () -> Void
@ -55,8 +55,6 @@ public final class StoryItemSetContainerComponent: Component {
safeInsets: UIEdgeInsets,
inputHeight: CGFloat,
isProgressPaused: Bool,
audioRecorder: ManagedAudioRecorder?,
videoRecorder: InstantVideoController?,
hideUI: Bool,
presentController: @escaping (ViewController) -> Void,
close: @escaping () -> Void,
@ -72,8 +70,6 @@ public final class StoryItemSetContainerComponent: Component {
self.safeInsets = safeInsets
self.inputHeight = inputHeight
self.isProgressPaused = isProgressPaused
self.audioRecorder = audioRecorder
self.videoRecorder = videoRecorder
self.hideUI = hideUI
self.presentController = presentController
self.close = close
@ -106,19 +102,13 @@ public final class StoryItemSetContainerComponent: Component {
if lhs.isProgressPaused != rhs.isProgressPaused {
return false
}
if lhs.audioRecorder !== rhs.audioRecorder {
return false
}
if lhs.videoRecorder !== rhs.videoRecorder {
return false
}
if lhs.hideUI != rhs.hideUI {
return false
}
return true
}
private final class ScrollView: UIScrollView {
final class ScrollView: UIScrollView {
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
return super.hitTest(point, with: event)
}
@ -128,7 +118,7 @@ public final class StoryItemSetContainerComponent: Component {
}
}
private struct ItemLayout {
struct ItemLayout {
var size: CGSize
init(size: CGSize) {
@ -136,7 +126,7 @@ public final class StoryItemSetContainerComponent: Component {
}
}
private final class VisibleItem {
final class VisibleItem {
let externalState = StoryContentItem.ExternalState()
let view = ComponentView<StoryContentItem.Environment>()
var currentProgress: Double = 0.0
@ -146,7 +136,7 @@ public final class StoryItemSetContainerComponent: Component {
}
}
private final class InfoItem {
final class InfoItem {
let component: AnyComponent<Empty>
let view = ComponentView<Empty>()
@ -156,47 +146,57 @@ public final class StoryItemSetContainerComponent: Component {
}
public final class View: UIView, UIScrollViewDelegate, UIGestureRecognizerDelegate {
private let scrollView: ScrollView
let sendMessageContext: StoryItemSetContainerSendMessage
private let contentContainerView: UIView
private let topContentGradientLayer: SimpleGradientLayer
private let bottomContentGradientLayer: SimpleGradientLayer
private let contentDimLayer: SimpleLayer
let scrollView: ScrollView
private let closeButton: HighlightableButton
private let closeButtonIconView: UIImageView
let contentContainerView: UIView
let topContentGradientLayer: SimpleGradientLayer
let bottomContentGradientLayer: SimpleGradientLayer
let contentDimLayer: SimpleLayer
private let navigationStrip = ComponentView<MediaNavigationStripComponent.EnvironmentType>()
private let inlineActions = ComponentView<Empty>()
let closeButton: HighlightableButton
let closeButtonIconView: UIImageView
private var centerInfoItem: InfoItem?
private var rightInfoItem: InfoItem?
let navigationStrip = ComponentView<MediaNavigationStripComponent.EnvironmentType>()
let inlineActions = ComponentView<Empty>()
private let inputPanel = ComponentView<Empty>()
private let footerPanel = ComponentView<Empty>()
private let inputPanelExternalState = MessageInputPanelComponent.ExternalState()
var centerInfoItem: InfoItem?
var rightInfoItem: InfoItem?
private var itemLayout: ItemLayout?
private var ignoreScrolling: Bool = false
let inputPanel = ComponentView<Empty>()
let footerPanel = ComponentView<Empty>()
let inputPanelExternalState = MessageInputPanelComponent.ExternalState()
private var focusedItemId: AnyHashable?
private var currentSlice: StoryContentItemSlice?
private var currentSliceDisposable: Disposable?
var itemLayout: ItemLayout?
var ignoreScrolling: Bool = false
private var visibleItems: [AnyHashable: VisibleItem] = [:]
var focusedItemId: AnyHashable?
var currentSlice: StoryContentItemSlice?
var currentSliceDisposable: Disposable?
private var preloadContexts: [AnyHashable: Disposable] = [:]
var visibleItems: [AnyHashable: VisibleItem] = [:]
private var reactionItems: [ReactionItem]?
private var reactionContextNode: ReactionContextNode?
var preloadContexts: [AnyHashable: Disposable] = [:]
private weak var actionSheet: ActionSheetController?
private weak var contextController: ContextController?
var displayReactions: Bool = false
var reactionItems: [ReactionItem]?
var reactionContextNode: ReactionContextNode?
weak var disappearingReactionContextNode: ReactionContextNode?
private var component: StoryItemSetContainerComponent?
private weak var state: EmptyComponentState?
weak var actionSheet: ActionSheetController?
weak var contextController: ContextController?
var component: StoryItemSetContainerComponent?
weak var state: EmptyComponentState?
private var audioRecorderDisposable: Disposable?
private var audioRecorderStatusDisposable: Disposable?
private var videoRecorderDisposable: Disposable?
override init(frame: CGRect) {
self.sendMessageContext = StoryItemSetContainerSendMessage()
self.scrollView = ScrollView()
self.contentContainerView = UIView()
@ -240,6 +240,105 @@ public final class StoryItemSetContainerComponent: Component {
let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:)))
tapRecognizer.delegate = self
self.contentContainerView.addGestureRecognizer(tapRecognizer)
self.audioRecorderDisposable = (self.sendMessageContext.audioRecorder.get()
|> deliverOnMainQueue).start(next: { [weak self] audioRecorder in
guard let self else {
return
}
if self.sendMessageContext.audioRecorderValue !== audioRecorder {
self.sendMessageContext.audioRecorderValue = audioRecorder
self.component?.controller()?.lockOrientation = audioRecorder != nil
/*strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, {
$0.updatedInputTextPanelState { panelState in
let isLocked = strongSelf.lockMediaRecordingRequestId == strongSelf.beginMediaRecordingRequestId
if let audioRecorder = audioRecorder {
if panelState.mediaRecordingState == nil {
return panelState.withUpdatedMediaRecordingState(.audio(recorder: audioRecorder, isLocked: isLocked))
}
} else {
if case .waitingForPreview = panelState.mediaRecordingState {
return panelState
}
return panelState.withUpdatedMediaRecordingState(nil)
}
return panelState
}
})*/
self.audioRecorderStatusDisposable?.dispose()
self.audioRecorderStatusDisposable = nil
if let audioRecorder = audioRecorder {
if !audioRecorder.beginWithTone {
HapticFeedback().impact(.light)
}
audioRecorder.start()
self.audioRecorderStatusDisposable = (audioRecorder.recordingState
|> deliverOnMainQueue).start(next: { [weak self] value in
guard let self else {
return
}
if case .stopped = value {
self.sendMessageContext.stopMediaRecorder()
}
})
}
self.state?.updated(transition: Transition(animation: .curve(duration: 0.4, curve: .spring)))
}
})
self.videoRecorderDisposable = (self.sendMessageContext.videoRecorder.get()
|> deliverOnMainQueue).start(next: { [weak self] videoRecorder in
guard let self else {
return
}
if self.sendMessageContext.videoRecorderValue !== videoRecorder {
let previousVideoRecorderValue = self.sendMessageContext.videoRecorderValue
self.sendMessageContext.videoRecorderValue = videoRecorder
if let videoRecorder = videoRecorder {
HapticFeedback().impact(.light)
videoRecorder.onDismiss = { [weak self] isCancelled in
guard let self else {
return
}
//self?.chatDisplayNode.updateRecordedMediaDeleted(isCancelled)
//self?.beginMediaRecordingRequestId += 1
//self?.lockMediaRecordingRequestId = nil
self.sendMessageContext.videoRecorder.set(.single(nil))
}
videoRecorder.onStop = { [weak self] in
guard let self else {
return
}
/*if let strongSelf = self {
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, {
$0.updatedInputTextPanelState { panelState in
return panelState.withUpdatedMediaRecordingState(.video(status: .editing, isLocked: false))
}
})
}*/
let _ = self
//TODO:editing
}
self.component?.controller()?.present(videoRecorder, in: .window(.root))
/*if strongSelf.lockMediaRecordingRequestId == strongSelf.beginMediaRecordingRequestId {
videoRecorder.lockVideo()
}*/
}
if let previousVideoRecorderValue {
previousVideoRecorderValue.dismissVideo()
}
self.state?.updated(transition: .immediate)
}
})
}
required init?(coder: NSCoder) {
@ -248,6 +347,13 @@ public final class StoryItemSetContainerComponent: Component {
deinit {
self.currentSliceDisposable?.dispose()
self.audioRecorderDisposable?.dispose()
self.audioRecorderStatusDisposable?.dispose()
self.audioRecorderStatusDisposable?.dispose()
}
func isPointInsideContentArea(point: CGPoint) -> Bool {
return self.contentContainerView.frame.contains(point)
}
@objc public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRequireFailureOf otherGestureRecognizer: UIGestureRecognizer) -> Bool {
@ -260,10 +366,10 @@ public final class StoryItemSetContainerComponent: Component {
@objc private func tapGesture(_ recognizer: UITapGestureRecognizer) {
if case .ended = recognizer.state, let currentSlice = self.currentSlice, let focusedItemId = self.focusedItemId, let currentIndex = currentSlice.items.firstIndex(where: { $0.id == focusedItemId }), let itemLayout = self.itemLayout {
if hasFirstResponder(self) {
self.reactionItems = nil
self.displayReactions = false
self.endEditing(true)
} else if self.reactionItems != nil {
self.reactionItems = nil
} else if self.displayReactions {
self.displayReactions = false
self.state?.updated(transition: Transition(animation: .curve(duration: 0.4, curve: .spring)))
} else {
let point = recognizer.location(in: self)
@ -407,7 +513,7 @@ public final class StoryItemSetContainerComponent: Component {
itemTransition.setFrame(view: view, frame: CGRect(origin: CGPoint(), size: itemLayout.size))
if let view = view as? StoryContentItem.View {
view.setIsProgressPaused(self.inputPanelExternalState.isEditing || component.isProgressPaused || self.reactionItems != nil || self.actionSheet != nil || self.contextController != nil)
view.setIsProgressPaused(self.inputPanelExternalState.isEditing || component.isProgressPaused || self.displayReactions || self.actionSheet != nil || self.contextController != nil || self.sendMessageContext.audioRecorderValue != nil || self.sendMessageContext.videoRecorderValue != nil)
}
}
}
@ -426,14 +532,14 @@ public final class StoryItemSetContainerComponent: Component {
}
}
private func updateIsProgressPaused() {
func updateIsProgressPaused() {
guard let component = self.component else {
return
}
for (_, visibleItem) in self.visibleItems {
if let view = visibleItem.view.view {
if let view = view as? StoryContentItem.View {
view.setIsProgressPaused(self.inputPanelExternalState.isEditing || component.isProgressPaused || self.reactionItems != nil || self.actionSheet != nil || self.contextController != nil)
view.setIsProgressPaused(self.inputPanelExternalState.isEditing || component.isProgressPaused || self.reactionItems != nil || self.actionSheet != nil || self.contextController != nil || self.sendMessageContext.audioRecorderValue != nil || self.sendMessageContext.videoRecorderValue != nil)
}
}
}
@ -468,8 +574,8 @@ public final class StoryItemSetContainerComponent: Component {
let innerSourceLocalFrame = CGRect(origin: CGPoint(x: sourceLocalFrame.minX - self.contentContainerView.frame.minX, y: sourceLocalFrame.minY - self.contentContainerView.frame.minY), size: sourceLocalFrame.size)
if let rightInfoView = self.rightInfoItem?.view.view {
rightInfoView.layer.animatePosition(from: CGPoint(x: innerSourceLocalFrame.center.x - rightInfoView.layer.position.x, y: 0.0), to: CGPoint(), duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, additive: true)
rightInfoView.layer.animatePosition(from: CGPoint(x: 0.0, y: innerSourceLocalFrame.center.y - rightInfoView.layer.position.y), to: CGPoint(), duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, additive: true)
let positionKeyframes: [CGPoint] = generateParabollicMotionKeyframes(from: CGPoint(x: innerSourceLocalFrame.center.x - rightInfoView.layer.position.x, y: innerSourceLocalFrame.center.y - rightInfoView.layer.position.y), to: CGPoint(), elevation: 0.0, duration: 0.3, curve: .spring, reverse: false)
rightInfoView.layer.animateKeyframes(values: positionKeyframes.map { NSValue(cgPoint: $0) }, duration: 0.3, keyPath: "position", additive: true)
rightInfoView.layer.animateScale(from: innerSourceLocalFrame.width / rightInfoView.bounds.width, to: 1.0, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring)
}
@ -502,6 +608,76 @@ public final class StoryItemSetContainerComponent: Component {
}
}
func animateOut(transitionOut: StoryContainerScreen.TransitionOut, completion: @escaping () -> Void) {
self.closeButton.layer.animateScale(from: 1.0, to: 0.001, duration: 0.3, delay: 0.0, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, completion: { _ in
completion()
})
if let inputPanelView = self.inputPanel.view {
inputPanelView.layer.animatePosition(
from: CGPoint(),
to: CGPoint(x: 0.0, y: self.bounds.height - inputPanelView.frame.minY),
duration: 0.3,
timingFunction: kCAMediaTimingFunctionSpring,
removeOnCompletion: false,
additive: true
)
inputPanelView.layer.animateAlpha(from: inputPanelView.alpha, to: 0.0, duration: 0.3, removeOnCompletion: false)
}
if let footerPanelView = self.footerPanel.view {
footerPanelView.layer.animatePosition(
from: CGPoint(),
to: CGPoint(x: 0.0, y: self.bounds.height - footerPanelView.frame.minY),
duration: 0.3,
timingFunction: kCAMediaTimingFunctionSpring,
removeOnCompletion: false,
additive: true
)
footerPanelView.layer.animateAlpha(from: footerPanelView.alpha, to: 0.0, duration: 0.3, removeOnCompletion: false)
}
if let sourceView = transitionOut.destinationView {
let sourceLocalFrame = sourceView.convert(transitionOut.destinationRect, to: self)
let innerSourceLocalFrame = CGRect(origin: CGPoint(x: sourceLocalFrame.minX - self.contentContainerView.frame.minX, y: sourceLocalFrame.minY - self.contentContainerView.frame.minY), size: sourceLocalFrame.size)
if let rightInfoView = self.rightInfoItem?.view.view {
let positionKeyframes: [CGPoint] = generateParabollicMotionKeyframes(from: innerSourceLocalFrame.center, to: rightInfoView.layer.position, elevation: 0.0, duration: 0.3, curve: .spring, reverse: true)
rightInfoView.layer.position = positionKeyframes[positionKeyframes.count - 1]
rightInfoView.layer.animateKeyframes(values: positionKeyframes.map { NSValue(cgPoint: $0) }, duration: 0.3, keyPath: "position", removeOnCompletion: false, additive: false)
rightInfoView.layer.animateScale(from: 1.0, to: innerSourceLocalFrame.width / rightInfoView.bounds.width, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false)
}
self.contentContainerView.layer.animatePosition(from: self.contentContainerView.center, to: sourceLocalFrame.center, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false)
self.contentContainerView.layer.animateBounds(from: self.contentContainerView.bounds, to: CGRect(origin: CGPoint(x: innerSourceLocalFrame.minX, y: innerSourceLocalFrame.minY), size: sourceLocalFrame.size), duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false)
self.contentContainerView.layer.animate(
from: self.contentContainerView.layer.cornerRadius as NSNumber,
to: transitionOut.destinationCornerRadius as NSNumber,
keyPath: "cornerRadius",
timingFunction: kCAMediaTimingFunctionSpring,
duration: 0.3,
removeOnCompletion: false
)
if let focusedItemId = self.focusedItemId, let visibleItemView = self.visibleItems[focusedItemId]?.view.view {
let innerScale = innerSourceLocalFrame.width / visibleItemView.bounds.width
let innerFromFrame = CGRect(origin: CGPoint(x: innerSourceLocalFrame.minX, y: innerSourceLocalFrame.minY), size: CGSize(width: innerSourceLocalFrame.width, height: visibleItemView.bounds.height * innerScale))
visibleItemView.layer.animatePosition(
from: visibleItemView.layer.position,
to: CGPoint(
x: innerFromFrame.midX,
y: innerFromFrame.midY
),
duration: 0.3,
timingFunction: kCAMediaTimingFunctionSpring,
removeOnCompletion: false
)
visibleItemView.layer.animateScale(from: 1.0, to: innerScale, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false)
}
}
}
func update(component: StoryItemSetContainerComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
let isFirstTime = self.component == nil
@ -527,6 +703,25 @@ public final class StoryItemSetContainerComponent: Component {
self.state?.updated(transition: .immediate)
})
}
let _ = (allowedStoryReactions(context: component.context)
|> deliverOnMainQueue).start(next: { [weak self] reactionItems in
guard let self, let component = self.component else {
return
}
component.controller()?.forEachController { c in
if let c = c as? UndoOverlayController {
c.dismiss()
}
return true
}
self.reactionItems = reactionItems
if self.displayReactions {
self.state?.updated(transition: Transition(animation: .curve(duration: 0.25, curve: .easeInOut)))
}
})
}
if self.topContentGradientLayer.colors == nil {
@ -589,7 +784,7 @@ public final class StoryItemSetContainerComponent: Component {
var bottomContentInset: CGFloat
if !component.safeInsets.bottom.isZero {
bottomContentInset = component.safeInsets.bottom + 5.0
bottomContentInset = component.safeInsets.bottom + 1.0
} else {
bottomContentInset = 0.0
}
@ -612,22 +807,19 @@ public final class StoryItemSetContainerComponent: Component {
guard let self else {
return
}
let _ = self
//self.performSendMessageAction()
self.sendMessageContext.performSendMessageAction(view: self)
},
setMediaRecordingActive: { [weak self] isActive, isVideo, sendAction in
guard let self else {
return
}
let _ = self
//self.setMediaRecordingActive(isActive: isActive, isVideo: isVideo, sendAction: sendAction)
self.sendMessageContext.setMediaRecordingActive(view: self, isActive: isActive, isVideo: isVideo, sendAction: sendAction)
},
attachmentAction: { [weak self] in
guard let self else {
return
}
let _ = self
//self.presentAttachmentMenu(subject: .default)
self.sendMessageContext.presentAttachmentMenu(view: self, subject: .default)
},
reactionAction: { [weak self] sourceView in
guard let self, let component = self.component else {
@ -647,12 +839,14 @@ public final class StoryItemSetContainerComponent: Component {
return true
}
self.reactionItems = reactionItems
self.displayReactions = true
self.state?.updated(transition: Transition(animation: .curve(duration: 0.25, curve: .easeInOut)))
})
},
audioRecorder: component.audioRecorder,
videoRecordingStatus: component.videoRecorder?.audioStatus
audioRecorder: self.sendMessageContext.audioRecorderValue,
videoRecordingStatus: self.sendMessageContext.videoRecorderValue?.audioStatus,
displayGradient: component.inputHeight != 0.0,
bottomInset: component.inputHeight != 0.0 ? 0.0 : bottomContentInset
)),
environment: {},
containerSize: CGSize(width: availableSize.width, height: 200.0)
@ -759,8 +953,13 @@ public final class StoryItemSetContainerComponent: Component {
items.append(.action(ContextMenuActionItem(text: "Who can see", textLayout: .secondLineWithValue("Everyone"), icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Channels"), color: theme.contextMenu.primaryColor)
}, action: { _, a in
}, action: { [weak self] _, a in
a(.default)
guard let self else {
return
}
self.openItemPrivacySettings()
})))
items.append(.separator)
@ -809,7 +1008,7 @@ public final class StoryItemSetContainerComponent: Component {
}
self.contextController = contextController
self.updateIsProgressPaused()
controller.presentInGlobalOverlay(contextController)
controller.present(contextController, in: .window(.root))
}
)),
environment: {},
@ -820,7 +1019,7 @@ public final class StoryItemSetContainerComponent: Component {
let inputPanelBottomInset: CGFloat
let inputPanelIsOverlay: Bool
if component.inputHeight < bottomContentInset + inputPanelSize.height {
if component.inputHeight == 0.0 {
inputPanelBottomInset = bottomContentInset
bottomContentInset += inputPanelSize.height
inputPanelIsOverlay = false
@ -832,7 +1031,7 @@ public final class StoryItemSetContainerComponent: Component {
let contentFrame = CGRect(origin: CGPoint(x: 0.0, y: component.containerInsets.top), size: CGSize(width: availableSize.width, height: availableSize.height - component.containerInsets.top - bottomContentInset))
transition.setFrame(view: self.contentContainerView, frame: contentFrame)
transition.setCornerRadius(layer: self.contentContainerView.layer, cornerRadius: 14.0)
transition.setCornerRadius(layer: self.contentContainerView.layer, cornerRadius: 10.0)
if self.closeButtonIconView.image == nil {
self.closeButtonIconView.image = UIImage(bundleImageName: "Media Gallery/Close")?.withRenderingMode(.alwaysTemplate)
@ -909,7 +1108,7 @@ public final class StoryItemSetContainerComponent: Component {
}
transition.setFrame(view: view, frame: CGRect(origin: CGPoint(x: contentFrame.width - 6.0 - rightInfoItemSize.width, y: 14.0), size: rightInfoItemSize))
if animateIn, !isFirstTime {
if animateIn, !isFirstTime, !transition.animation.isImmediate {
view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
view.layer.animateScale(from: 0.5, to: 1.0, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring)
}
@ -960,7 +1159,9 @@ public final class StoryItemSetContainerComponent: Component {
transition.setAlpha(view: inputPanelView, alpha: focusedItem?.isMy == true ? 0.0 : 1.0)
}
if let reactionItems = self.reactionItems {
let reactionsAnchorRect = CGRect(origin: CGPoint(x: inputPanelFrame.maxX - 40.0, y: inputPanelFrame.minY + 9.0), size: CGSize(width: 32.0, height: 32.0)).insetBy(dx: -4.0, dy: -4.0)
if let reactionItems = self.reactionItems, (self.displayReactions || self.inputPanelExternalState.isEditing) {
let reactionContextNode: ReactionContextNode
var reactionContextNodeTransition = transition
if let current = self.reactionContextNode {
@ -1017,23 +1218,72 @@ public final class StoryItemSetContainerComponent: Component {
self.state?.updated(transition: Transition(transition))
}
)
reactionContextNode.displayTail = false
self.reactionContextNode = reactionContextNode
reactionContextNode.reactionSelected = { [weak self] updateReaction, _ in
guard let self, let component = self.component else {
return
}
self.reactionItems = nil
self.state?.updated(transition: Transition(animation: .curve(duration: 0.25, curve: .easeInOut)))
let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: component.theme)
component.presentController(UndoOverlayController(
presentationData: presentationData,
content: .succeed(text: "Reaction Sent"),
elevatedLayout: false,
animateInAsReplacement: false,
action: { _ in return false }
))
let _ = (component.context.engine.stickers.availableReactions()
|> take(1)
|> deliverOnMainQueue).start(next: { [weak self] availableReactions in
guard let self, let component = self.component, let availableReactions else {
return
}
var selectedReaction: AvailableReactions.Reaction?
for reaction in availableReactions.reactions {
if reaction.value == updateReaction.reaction {
selectedReaction = reaction
break
}
}
guard let reaction = selectedReaction else {
return
}
let targetView = UIView(frame: CGRect(origin: CGPoint(x: floor((self.bounds.width - 100.0) * 0.5), y: floor((self.bounds.height - 100.0) * 0.5)), size: CGSize(width: 100.0, height: 100.0)))
targetView.isUserInteractionEnabled = false
self.addSubview(targetView)
reactionContextNode.willAnimateOutToReaction(value: updateReaction.reaction)
reactionContextNode.animateOutToReaction(value: updateReaction.reaction, targetView: targetView, hideNode: false, animateTargetContainer: nil, addStandaloneReactionAnimation: { [weak self] standaloneReactionAnimation in
guard let self else {
return
}
standaloneReactionAnimation.frame = self.bounds
self.addSubview(standaloneReactionAnimation.view)
}, completion: { [weak targetView, weak reactionContextNode] in
targetView?.removeFromSuperview()
if let reactionContextNode {
reactionContextNode.layer.animateScale(from: 1.0, to: 0.001, duration: 0.3, removeOnCompletion: false)
reactionContextNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { [weak reactionContextNode] _ in
reactionContextNode?.view.removeFromSuperview()
})
}
})
self.displayReactions = false
if hasFirstResponder(self) {
self.endEditing(true)
}
self.state?.updated(transition: Transition(animation: .curve(duration: 0.25, curve: .easeInOut)))
if let centerAnimation = reaction.centerAnimation {
let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: component.theme)
component.presentController(UndoOverlayController(
presentationData: presentationData,
content: .sticker(context: component.context, file: centerAnimation, loop: false, title: nil, text: "Reaction Sent.", undoText: "View in Chat", customAction: {
}),
elevatedLayout: false,
animateInAsReplacement: false,
action: { _ in return false }
))
}
})
}
}
@ -1043,19 +1293,41 @@ public final class StoryItemSetContainerComponent: Component {
self.addSubnode(reactionContextNode)
}
let anchorRect = CGRect(origin: CGPoint(x: inputPanelFrame.maxX - 44.0 - 32.0, y: inputPanelFrame.minY), size: CGSize(width: 32.0, height: 32.0)).insetBy(dx: -4.0, dy: -4.0)
reactionContextNodeTransition.setFrame(view: reactionContextNode.view, frame: CGRect(origin: CGPoint(), size: availableSize))
reactionContextNode.updateLayout(size: availableSize, insets: UIEdgeInsets(), anchorRect: anchorRect, isCoveredByInput: false, isAnimatingOut: false, transition: reactionContextNodeTransition.containedViewLayoutTransition)
if animateReactionsIn {
reactionContextNode.animateIn(from: anchorRect)
if reactionContextNode.isAnimatingOutToReaction {
if !reactionContextNode.isAnimatingOut {
reactionContextNode.animateOut(to: reactionsAnchorRect, animatingOutToReaction: true)
}
} else {
reactionContextNodeTransition.setFrame(view: reactionContextNode.view, frame: CGRect(origin: CGPoint(), size: availableSize))
reactionContextNode.updateLayout(size: availableSize, insets: UIEdgeInsets(), anchorRect: reactionsAnchorRect, isCoveredByInput: false, isAnimatingOut: false, transition: reactionContextNodeTransition.containedViewLayoutTransition)
if animateReactionsIn {
reactionContextNode.animateIn(from: reactionsAnchorRect)
}
}
} else {
if let reactionContextNode = self.reactionContextNode {
if let disappearingReactionContextNode = self.disappearingReactionContextNode {
disappearingReactionContextNode.view.removeFromSuperview()
}
self.disappearingReactionContextNode = reactionContextNode
self.reactionContextNode = nil
transition.setAlpha(view: reactionContextNode.view, alpha: 0.0, completion: { [weak reactionContextNode] _ in
reactionContextNode?.view.removeFromSuperview()
})
if reactionContextNode.isAnimatingOutToReaction {
if !reactionContextNode.isAnimatingOut {
reactionContextNode.animateOut(to: reactionsAnchorRect, animatingOutToReaction: true)
}
} else {
transition.setAlpha(view: reactionContextNode.view, alpha: 0.0, completion: { [weak reactionContextNode] _ in
reactionContextNode?.view.removeFromSuperview()
})
}
}
}
if let reactionContextNode = self.disappearingReactionContextNode {
if !reactionContextNode.isAnimatingOutToReaction {
transition.setFrame(view: reactionContextNode.view, frame: CGRect(origin: CGPoint(), size: availableSize))
reactionContextNode.updateLayout(size: availableSize, insets: UIEdgeInsets(), anchorRect: reactionsAnchorRect, isCoveredByInput: false, isAnimatingOut: false, transition: transition.containedViewLayoutTransition)
}
}
@ -1070,7 +1342,8 @@ public final class StoryItemSetContainerComponent: Component {
let bottomGradientHeight = inputPanelSize.height + 32.0
transition.setFrame(layer: self.bottomContentGradientLayer, frame: CGRect(origin: CGPoint(x: contentFrame.minX, y: availableSize.height - component.inputHeight - bottomGradientHeight), size: CGSize(width: contentFrame.width, height: bottomGradientHeight)))
transition.setAlpha(layer: self.bottomContentGradientLayer, alpha: inputPanelIsOverlay ? 1.0 : 0.0)
//transition.setAlpha(layer: self.bottomContentGradientLayer, alpha: inputPanelIsOverlay ? 1.0 : 0.0)
transition.setAlpha(layer: self.bottomContentGradientLayer, alpha: 0.0)
transition.setFrame(layer: self.contentDimLayer, frame: CGRect(origin: CGPoint(), size: contentFrame.size))
transition.setAlpha(layer: self.contentDimLayer, alpha: (inputPanelIsOverlay || self.inputPanelExternalState.isEditing) ? 1.0 : 0.0)
@ -1134,8 +1407,7 @@ public final class StoryItemSetContainerComponent: Component {
guard let self else {
return
}
let _ = self
//self.performInlineAction(item: item)
self.sendMessageContext.performInlineAction(view: self, item: item)
}
)),
environment: {},
@ -1148,7 +1420,7 @@ public final class StoryItemSetContainerComponent: Component {
transition.setFrame(view: inlineActionsView, frame: CGRect(origin: CGPoint(x: contentFrame.width - 10.0 - inlineActionsSize.width, y: contentFrame.height - 20.0 - inlineActionsSize.height), size: inlineActionsSize))
var inlineActionsAlpha: CGFloat = inputPanelIsOverlay ? 0.0 : 1.0
if component.audioRecorder != nil || component.videoRecorder != nil {
if self.sendMessageContext.audioRecorderValue != nil || self.sendMessageContext.videoRecorderValue != nil {
inlineActionsAlpha = 0.0
}
if self.reactionItems != nil {
@ -1167,6 +1439,90 @@ public final class StoryItemSetContainerComponent: Component {
return contentSize
}
private func openItemPrivacySettings() {
guard let component = self.component else {
return
}
guard let focusedItemId = self.focusedItemId, let focusedItem = self.currentSlice?.items.first(where: { $0.id == focusedItemId }) else {
return
}
guard let storyItem = focusedItem.storyItem else {
return
}
enum AdditionalCategoryId: Int {
case everyone
case contacts
case closeFriends
}
let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: component.theme)
let additionalCategories: [ChatListNodeAdditionalCategory] = [
ChatListNodeAdditionalCategory(
id: AdditionalCategoryId.everyone.rawValue,
icon: generateAvatarImage(size: CGSize(width: 40.0, height: 40.0), icon: generateTintedImage(image: UIImage(bundleImageName: "Chat List/Filters/Channel"), color: .white), cornerRadius: nil, color: .blue),
smallIcon: generateAvatarImage(size: CGSize(width: 22.0, height: 22.0), icon: generateTintedImage(image: UIImage(bundleImageName: "Chat List/Filters/Channel"), color: .white), iconScale: 0.6, cornerRadius: 6.0, circleCorners: true, color: .blue),
title: "Everyone",
appearance: .option(sectionTitle: "WHO CAN VIEW FOR 24 HOURS")
),
ChatListNodeAdditionalCategory(
id: AdditionalCategoryId.contacts.rawValue,
icon: generateAvatarImage(size: CGSize(width: 40.0, height: 40.0), icon: generateTintedImage(image: UIImage(bundleImageName: "Chat List/Tabs/IconContacts"), color: .white), iconScale: 1.0 * 0.8, cornerRadius: nil, color: .yellow),
smallIcon: generateAvatarImage(size: CGSize(width: 22.0, height: 22.0), icon: generateTintedImage(image: UIImage(bundleImageName: "Chat List/Tabs/IconContacts"), color: .white), iconScale: 0.6 * 0.8, cornerRadius: 6.0, circleCorners: true, color: .yellow),
title: presentationData.strings.ChatListFolder_CategoryContacts,
appearance: .option(sectionTitle: "WHO CAN VIEW FOR 24 HOURS")
),
ChatListNodeAdditionalCategory(
id: AdditionalCategoryId.closeFriends.rawValue,
icon: generateAvatarImage(size: CGSize(width: 40.0, height: 40.0), icon: generateTintedImage(image: UIImage(bundleImageName: "Call/StarHighlighted"), color: .white), iconScale: 1.0 * 0.6, cornerRadius: nil, color: .green),
smallIcon: generateAvatarImage(size: CGSize(width: 22.0, height: 22.0), icon: generateTintedImage(image: UIImage(bundleImageName: "Call/StarHighlighted"), color: .white), iconScale: 0.6 * 0.6, cornerRadius: 6.0, circleCorners: true, color: .green),
title: "Close Friends",
appearance: .option(sectionTitle: "WHO CAN VIEW FOR 24 HOURS")
)
]
var selectedChats = Set<EnginePeer.Id>()
var selectedCategories = Set<Int>()
if let privacy = storyItem.privacy {
selectedChats.formUnion(privacy.additionallyIncludePeers)
switch privacy.base {
case .everyone:
selectedCategories.insert(AdditionalCategoryId.everyone.rawValue)
case .contacts:
selectedCategories.insert(AdditionalCategoryId.contacts.rawValue)
case .closeFriends:
selectedCategories.insert(AdditionalCategoryId.closeFriends.rawValue)
}
}
let selectionController = component.context.sharedContext.makeContactMultiselectionController(ContactMultiselectionControllerParams(context: component.context, mode: .chatSelection(ContactMultiselectionControllerMode.ChatSelection(
title: "Share Story",
searchPlaceholder: "Search contacts",
selectedChats: selectedChats,
additionalCategories: ContactMultiselectionControllerAdditionalCategories(categories: additionalCategories, selectedCategories: selectedCategories),
chatListFilters: nil,
displayPresence: true
)), options: [], filters: [.excludeSelf], alwaysEnabled: true, limit: 1000, reachedLimit: { _ in
}))
component.controller()?.present(selectionController, in: .window(.root), with: ViewControllerPresentationArguments(presentationAnimation: .modalSheet))
let _ = (selectionController.result
|> take(1)
|> deliverOnMainQueue).start(next: { [weak selectionController] result in
guard case let .result(peerIds, additionalCategoryIds) = result else {
selectionController?.dismiss()
return
}
let _ = peerIds
let _ = additionalCategoryIds
selectionController?.dismiss()
})
}
}
public func makeView() -> View {
@ -1191,6 +1547,53 @@ private final class HeaderContextReferenceContentSource: ContextReferenceContent
}
func transitionInfo() -> ContextControllerReferenceViewInfo? {
return ContextControllerReferenceViewInfo(referenceView: self.sourceView, contentAreaInScreenSpace: UIScreen.main.bounds)
return ContextControllerReferenceViewInfo(referenceView: self.sourceView, contentAreaInScreenSpace: UIScreen.main.bounds, actionsPosition: .top)
}
}
private func generateParabollicMotionKeyframes(from sourcePoint: CGPoint, to targetPosition: CGPoint, elevation: CGFloat, duration: Double, curve: Transition.Animation.Curve, reverse: Bool) -> [CGPoint] {
let midPoint = CGPoint(x: (sourcePoint.x + targetPosition.x) / 2.0, y: sourcePoint.y - elevation)
let x1 = sourcePoint.x
let y1 = sourcePoint.y
let x2 = midPoint.x
let y2 = midPoint.y
let x3 = targetPosition.x
let y3 = targetPosition.y
let numPoints: Int = Int(ceil(Double(UIScreen.main.maximumFramesPerSecond) * duration))
var keyframes: [CGPoint] = []
if abs(y1 - y3) < 5.0 && abs(x1 - x3) < 5.0 {
for rawI in 0 ..< numPoints {
let i = reverse ? (numPoints - 1 - rawI) : rawI
let ks = CGFloat(i) / CGFloat(numPoints - 1)
var k = curve.solve(at: reverse ? (1.0 - ks) : ks)
if reverse {
k = 1.0 - k
}
let x = sourcePoint.x * (1.0 - k) + targetPosition.x * k
let y = sourcePoint.y * (1.0 - k) + targetPosition.y * k
keyframes.append(CGPoint(x: x, y: y))
}
} else {
let a = (x3 * (y2 - y1) + x2 * (y1 - y3) + x1 * (y3 - y2)) / ((x1 - x2) * (x1 - x3) * (x2 - x3))
let b = (x1 * x1 * (y2 - y3) + x3 * x3 * (y1 - y2) + x2 * x2 * (y3 - y1)) / ((x1 - x2) * (x1 - x3) * (x2 - x3))
let c = (x2 * x2 * (x3 * y1 - x1 * y3) + x2 * (x1 * x1 * y3 - x3 * x3 * y1) + x1 * x3 * (x3 - x1) * y2) / ((x1 - x2) * (x1 - x3) * (x2 - x3))
for rawI in 0 ..< numPoints {
let i = reverse ? (numPoints - 1 - rawI) : rawI
let ks = CGFloat(i) / CGFloat(numPoints - 1)
var k = curve.solve(at: reverse ? (1.0 - ks) : ks)
if reverse {
k = 1.0 - k
}
let x = sourcePoint.x * (1.0 - k) + targetPosition.x * k
let y = a * x * x + b * x + c
keyframes.append(CGPoint(x: x, y: y))
}
}
return keyframes
}

View File

@ -33,7 +33,7 @@ final class StoryAvatarInfoComponent: Component {
private weak var state: EmptyComponentState?
override init(frame: CGRect) {
self.avatarNode = AvatarNode(font: avatarPlaceholderFont(size: 15.0))
self.avatarNode = AvatarNode(font: avatarPlaceholderFont(size: 18.0))
super.init(frame: frame)

View File

@ -37,7 +37,7 @@ public enum StoryChatContent {
peer: author
))
},
targetMessageId: nil,
peerId: itemSet.peerId,
storyItem: item,
preload: nil,
delete: { [weak storyList] in

View File

@ -213,7 +213,12 @@ final class StoryItemContentComponent: Component {
if self.videoNode != nil {
self.updateVideoPlaybackProgress()
} else {
#if DEBUG && false
let currentProgressTimerLimit: Double = 5 * 60.0
#else
let currentProgressTimerLimit: Double = 5.0
#endif
var currentProgressTimerValue = self.currentProgressTimerValue + 1.0 / 60.0
currentProgressTimerValue = max(0.0, min(currentProgressTimerLimit, currentProgressTimerValue))
self.currentProgressTimerValue = currentProgressTimerValue

View File

@ -73,7 +73,7 @@ public final class TextFieldComponent: Component {
self.textView = UITextView(frame: CGRect(), textContainer: self.textContainer)
self.textView.translatesAutoresizingMaskIntoConstraints = false
self.textView.textContainerInset = UIEdgeInsets(top: 6.0, left: 8.0, bottom: 7.0, right: 8.0)
self.textView.textContainerInset = UIEdgeInsets(top: 9.0, left: 8.0, bottom: 10.0, right: 8.0)
self.textView.backgroundColor = nil
self.textView.layer.isOpaque = false
self.textView.keyboardAppearance = .dark

View File

@ -4091,7 +4091,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|> timeout(2.0, queue: Queue.mainQueue(), alternate: .single(false))).start(next: { [weak self] responded in
if let strongSelf = self {
if !responded {
strongSelf.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .sticker(context: strongSelf.context, file: file, title: nil, text: strongSelf.presentationData.strings.Conversation_InteractiveEmojiSyncTip(EnginePeer(peer).compactDisplayTitle).string, undoText: nil, customAction: nil), elevatedLayout: false, action: { _ in return false }), in: .current)
strongSelf.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .sticker(context: strongSelf.context, file: file, loop: true, title: nil, text: strongSelf.presentationData.strings.Conversation_InteractiveEmojiSyncTip(EnginePeer(peer).compactDisplayTitle).string, undoText: nil, customAction: nil), elevatedLayout: false, action: { _ in return false }), in: .current)
let _ = ApplicationSpecificNotice.incrementInteractiveEmojiSyncTip(accountManager: strongSelf.context.sharedContext.accountManager, timestamp: currentTimestamp).start()
}
@ -8601,7 +8601,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
if let firstLockedPremiumEmoji = firstLockedPremiumEmoji {
let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 }
strongSelf.controllerInteraction?.displayUndo(.sticker(context: strongSelf.context, file: firstLockedPremiumEmoji, title: nil, text: presentationData.strings.EmojiInput_PremiumEmojiToast_Text, undoText: presentationData.strings.EmojiInput_PremiumEmojiToast_Action, customAction: {
strongSelf.controllerInteraction?.displayUndo(.sticker(context: strongSelf.context, file: firstLockedPremiumEmoji, loop: true, title: nil, text: presentationData.strings.EmojiInput_PremiumEmojiToast_Text, undoText: presentationData.strings.EmojiInput_PremiumEmojiToast_Action, customAction: {
guard let strongSelf = self else {
return
}
@ -9748,7 +9748,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
if let strongSelf = self {
switch result {
case .generic:
strongSelf.presentInGlobalOverlay(UndoOverlayController(presentationData: strongSelf.presentationData, content: .sticker(context: strongSelf.context, file: stickerFile, title: nil, text: added ? strongSelf.presentationData.strings.Conversation_StickerAddedToFavorites : strongSelf.presentationData.strings.Conversation_StickerRemovedFromFavorites, undoText: nil, customAction: nil), elevatedLayout: true, action: { _ in return false }), with: nil)
strongSelf.presentInGlobalOverlay(UndoOverlayController(presentationData: strongSelf.presentationData, content: .sticker(context: strongSelf.context, file: stickerFile, loop: true, title: nil, text: added ? strongSelf.presentationData.strings.Conversation_StickerAddedToFavorites : strongSelf.presentationData.strings.Conversation_StickerRemovedFromFavorites, undoText: nil, customAction: nil), elevatedLayout: true, action: { _ in return false }), with: nil)
case let .limitExceeded(limit, premiumLimit):
let premiumConfiguration = PremiumConfiguration.with(appConfiguration: context.currentAppConfiguration.with { $0 })
let text: String
@ -9757,7 +9757,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
} else {
text = strongSelf.presentationData.strings.Premium_MaxFavedStickersText("\(premiumLimit)").string
}
strongSelf.presentInGlobalOverlay(UndoOverlayController(presentationData: strongSelf.presentationData, content: .sticker(context: strongSelf.context, file: stickerFile, title: strongSelf.presentationData.strings.Premium_MaxFavedStickersTitle("\(limit)").string, text: text, undoText: nil, customAction: nil), elevatedLayout: true, action: { [weak self] action in
strongSelf.presentInGlobalOverlay(UndoOverlayController(presentationData: strongSelf.presentationData, content: .sticker(context: strongSelf.context, file: stickerFile, loop: true, title: strongSelf.presentationData.strings.Premium_MaxFavedStickersTitle("\(limit)").string, text: text, undoText: nil, customAction: nil), elevatedLayout: true, action: { [weak self] action in
if let strongSelf = self {
if case .info = action {
let controller = PremiumIntroScreen(context: strongSelf.context, source: .savedStickers)
@ -14762,7 +14762,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
let _ = (self.context.engine.stickers.loadedStickerPack(reference: stickerPackReference, forceActualized: false)
|> deliverOnMainQueue).start(next: { [weak self] stickerPack in
if let strongSelf = self, case let .result(info, _, _) = stickerPack {
strongSelf.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .sticker(context: strongSelf.context, file: file, title: info.title, text: strongSelf.presentationData.strings.Stickers_PremiumPackInfoText, undoText: strongSelf.presentationData.strings.Stickers_PremiumPackView, customAction: nil), elevatedLayout: false, action: { [weak self] action in
strongSelf.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .sticker(context: strongSelf.context, file: file, loop: true, title: info.title, text: strongSelf.presentationData.strings.Stickers_PremiumPackInfoText, undoText: strongSelf.presentationData.strings.Stickers_PremiumPackView, customAction: nil), elevatedLayout: false, action: { [weak self] action in
if let strongSelf = self, action == .undo {
let _ = strongSelf.controllerInteraction?.openMessage(message, .default)
}
@ -14814,7 +14814,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
/*let _ = (self.context.engine.stickers.loadedStickerPack(reference: stickerPackReference, forceActualized: false)
|> deliverOnMainQueue).start(next: { [weak self] stickerPack in
if let strongSelf = self, case let .result(info, _, _) = stickerPack {
strongSelf.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .sticker(context: strongSelf.context, file: file, title: nil, text: strongSelf.presentationData.strings.Stickers_EmojiPackInfoText(info.title).string, undoText: strongSelf.presentationData.strings.Stickers_PremiumPackView, customAction: nil), elevatedLayout: false, action: { [weak self] action in
strongSelf.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .sticker(context: strongSelf.context, file: file, loop: true, title: nil, text: strongSelf.presentationData.strings.Stickers_EmojiPackInfoText(info.title).string, undoText: strongSelf.presentationData.strings.Stickers_PremiumPackView, customAction: nil), elevatedLayout: false, action: { [weak self] action in
if let strongSelf = self, action == .undo {
strongSelf.presentEmojiList(references: [stickerPackReference])
}

View File

@ -3150,7 +3150,7 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate {
if let firstLockedPremiumEmoji = firstLockedPremiumEmoji {
let presentationData = self.context.sharedContext.currentPresentationData.with { $0 }
self.controllerInteraction.displayUndo(.sticker(context: context, file: firstLockedPremiumEmoji, title: nil, text: presentationData.strings.EmojiInput_PremiumEmojiToast_Text, undoText: presentationData.strings.EmojiInput_PremiumEmojiToast_Action, customAction: { [weak self] in
self.controllerInteraction.displayUndo(.sticker(context: context, file: firstLockedPremiumEmoji, loop: true, title: nil, text: presentationData.strings.EmojiInput_PremiumEmojiToast_Text, undoText: presentationData.strings.EmojiInput_PremiumEmojiToast_Action, customAction: { [weak self] in
guard let strongSelf = self else {
return
}

View File

@ -3136,7 +3136,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate {
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let undoController = UndoOverlayController(presentationData: presentationData, content: .sticker(context: context, file: file, title: nil, text: presentationData.strings.EmojiStatus_AppliedText, undoText: nil, customAction: nil), elevatedLayout: false, animateInAsReplacement: animateInAsReplacement, action: { _ in return false })
let undoController = UndoOverlayController(presentationData: presentationData, content: .sticker(context: context, file: file, loop: true, title: nil, text: presentationData.strings.EmojiStatus_AppliedText, undoText: nil, customAction: nil), elevatedLayout: false, animateInAsReplacement: animateInAsReplacement, action: { _ in return false })
//strongSelf.currentUndoOverlayController = controller
controller.controllerInteraction?.presentController(undoController, nil)
}

View File

@ -440,7 +440,7 @@ class ContactMultiselectionControllerImpl: ViewController, ContactMultiselection
return
}
var addedToken: EditableTokenListToken?
var removedTokenId: AnyHashable?
var removedTokenIds: [AnyHashable] = []
switch strongSelf.contactsNode.contentNode {
case .contacts:
break
@ -458,12 +458,23 @@ class ContactMultiselectionControllerImpl: ViewController, ContactMultiselection
}
chatsNode.updateState { state in
var state = state
if state.selectedAdditionalCategoryIds.contains(id) {
state.selectedAdditionalCategoryIds.remove(id)
removedTokenId = id
if "".isEmpty {
if !state.selectedAdditionalCategoryIds.contains(id) {
for id in state.selectedAdditionalCategoryIds {
removedTokenIds.append(id)
state.selectedAdditionalCategoryIds.remove(id)
}
state.selectedAdditionalCategoryIds.insert(id)
addedToken = categoryToken
}
} else {
state.selectedAdditionalCategoryIds.insert(id)
addedToken = categoryToken
if state.selectedAdditionalCategoryIds.contains(id) {
state.selectedAdditionalCategoryIds.remove(id)
removedTokenIds.append(id)
} else {
state.selectedAdditionalCategoryIds.insert(id)
addedToken = categoryToken
}
}
return state
@ -486,9 +497,13 @@ class ContactMultiselectionControllerImpl: ViewController, ContactMultiselection
if !added {
strongSelf.contactsNode.editableTokens.append(addedToken)
}
} else if let removedTokenId = removedTokenId {
strongSelf.contactsNode.editableTokens = strongSelf.contactsNode.editableTokens.filter { token in
return token.id != removedTokenId
return !removedTokenIds.contains(token.id)
}
} else if !removedTokenIds.isEmpty {
strongSelf.contactsNode.editableTokens = strongSelf.contactsNode.editableTokens.filter { token in
return !removedTokenIds.contains(token.id)
}
}
strongSelf.requestLayout(transition: ContainedViewLayoutTransition.animated(duration: 0.4, curve: .spring))

View File

@ -105,7 +105,7 @@ final class ContactMultiselectionControllerNode: ASDisplayNode {
let chatListFilters = chatSelection.chatListFilters
placeholder = placeholderValue
let chatListNode = ChatListNode(context: context, location: .chatList(groupId: .root), previewing: false, fillPreloadItems: false, mode: .peers(filter: [.excludeSecretChats], isSelecting: true, additionalCategories: additionalCategories?.categories ?? [], chatListFilters: chatListFilters, displayAutoremoveTimeout: chatSelection.displayAutoremoveTimeout), isPeerEnabled: isPeerEnabled, theme: self.presentationData.theme, fontSize: self.presentationData.listsFontSize, strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat, nameSortOrder: self.presentationData.nameSortOrder, nameDisplayOrder: self.presentationData.nameDisplayOrder, animationCache: self.animationCache, animationRenderer: self.animationRenderer, disableAnimations: true, isInlineMode: false)
let chatListNode = ChatListNode(context: context, location: .chatList(groupId: .root), previewing: false, fillPreloadItems: false, mode: .peers(filter: [.excludeSecretChats], isSelecting: true, additionalCategories: additionalCategories?.categories ?? [], chatListFilters: chatListFilters, displayAutoremoveTimeout: chatSelection.displayAutoremoveTimeout, displayPresence: chatSelection.displayPresence), isPeerEnabled: isPeerEnabled, theme: self.presentationData.theme, fontSize: self.presentationData.listsFontSize, strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat, nameSortOrder: self.presentationData.nameSortOrder, nameDisplayOrder: self.presentationData.nameDisplayOrder, animationCache: self.animationCache, animationRenderer: self.animationRenderer, disableAnimations: true, isInlineMode: false)
chatListNode.passthroughPeerSelection = true
chatListNode.disabledPeerSelected = { peer, _ in
attemptDisabledItemSelection?(peer)

View File

@ -342,7 +342,7 @@ final class EmojisChatInputContextPanelNode: ChatInputContextPanelNode {
let presentationData = self.context.sharedContext.currentPresentationData.with { $0 }
let undoController = UndoOverlayController(presentationData: presentationData, content: .sticker(context: self.context, file: file, title: nil, text: presentationData.strings.EmojiStatus_AppliedText, undoText: nil, customAction: nil), elevatedLayout: false, animateInAsReplacement: animateInAsReplacement, action: { _ in return false })
let undoController = UndoOverlayController(presentationData: presentationData, content: .sticker(context: self.context, file: file, loop: true, title: nil, text: presentationData.strings.EmojiStatus_AppliedText, undoText: nil, customAction: nil), elevatedLayout: false, animateInAsReplacement: animateInAsReplacement, action: { _ in return false })
//strongSelf.currentUndoOverlayController = controller
controller.controllerInteraction?.presentController(undoController, nil)
}

View File

@ -193,7 +193,7 @@ final class HorizontalStickersChatContextPanelNode: ChatInputContextPanelNode {
|> deliverOnMainQueue).start(next: { result in
switch result {
case .generic:
strongSelf.interfaceInteraction?.presentGlobalOverlayController(UndoOverlayController(presentationData: presentationData, content: .sticker(context: strongSelf.context, file: item.file, title: nil, text: !isStarred ? strongSelf.strings.Conversation_StickerAddedToFavorites : strongSelf.strings.Conversation_StickerRemovedFromFavorites, undoText: nil, customAction: nil), elevatedLayout: false, action: { _ in return false }), nil)
strongSelf.interfaceInteraction?.presentGlobalOverlayController(UndoOverlayController(presentationData: presentationData, content: .sticker(context: strongSelf.context, file: item.file, loop: true, title: nil, text: !isStarred ? strongSelf.strings.Conversation_StickerAddedToFavorites : strongSelf.strings.Conversation_StickerRemovedFromFavorites, undoText: nil, customAction: nil), elevatedLayout: false, action: { _ in return false }), nil)
case let .limitExceeded(limit, premiumLimit):
let premiumConfiguration = PremiumConfiguration.with(appConfiguration: strongSelf.context.currentAppConfiguration.with { $0 })
let text: String
@ -202,7 +202,7 @@ final class HorizontalStickersChatContextPanelNode: ChatInputContextPanelNode {
} else {
text = strongSelf.strings.Premium_MaxFavedStickersText("\(premiumLimit)").string
}
strongSelf.interfaceInteraction?.presentGlobalOverlayController(UndoOverlayController(presentationData: presentationData, content: .sticker(context: strongSelf.context, file: item.file, title: strongSelf.strings.Premium_MaxFavedStickersTitle("\(limit)").string, text: text, undoText: nil, customAction: nil), elevatedLayout: false, action: { [weak self] action in
strongSelf.interfaceInteraction?.presentGlobalOverlayController(UndoOverlayController(presentationData: presentationData, content: .sticker(context: strongSelf.context, file: item.file, loop: true, title: strongSelf.strings.Premium_MaxFavedStickersTitle("\(limit)").string, text: text, undoText: nil, customAction: nil), elevatedLayout: false, action: { [weak self] action in
if let strongSelf = self {
if case .info = action {
let controller = PremiumIntroScreen(context: strongSelf.context, source: .savedStickers)

View File

@ -145,7 +145,7 @@ private final class InlineReactionSearchStickersNode: ASDisplayNode, UIScrollVie
|> deliverOnMainQueue).start(next: { result in
switch result {
case .generic:
strongSelf.getControllerInteraction?()?.presentGlobalOverlayController(UndoOverlayController(presentationData: presentationData, content: .sticker(context: strongSelf.context, file: item.file, title: nil, text: !isStarred ? strongSelf.strings.Conversation_StickerAddedToFavorites : strongSelf.strings.Conversation_StickerRemovedFromFavorites, undoText: nil, customAction: nil), elevatedLayout: false, action: { _ in return false }), nil)
strongSelf.getControllerInteraction?()?.presentGlobalOverlayController(UndoOverlayController(presentationData: presentationData, content: .sticker(context: strongSelf.context, file: item.file, loop: true, title: nil, text: !isStarred ? strongSelf.strings.Conversation_StickerAddedToFavorites : strongSelf.strings.Conversation_StickerRemovedFromFavorites, undoText: nil, customAction: nil), elevatedLayout: false, action: { _ in return false }), nil)
case let .limitExceeded(limit, premiumLimit):
let premiumConfiguration = PremiumConfiguration.with(appConfiguration: strongSelf.context.currentAppConfiguration.with { $0 })
let text: String
@ -154,7 +154,7 @@ private final class InlineReactionSearchStickersNode: ASDisplayNode, UIScrollVie
} else {
text = strongSelf.strings.Premium_MaxFavedStickersText("\(premiumLimit)").string
}
strongSelf.getControllerInteraction?()?.presentGlobalOverlayController(UndoOverlayController(presentationData: presentationData, content: .sticker(context: strongSelf.context, file: item.file, title: strongSelf.strings.Premium_MaxFavedStickersTitle("\(limit)").string, text: text, undoText: nil, customAction: nil), elevatedLayout: false, action: { [weak self] action in
strongSelf.getControllerInteraction?()?.presentGlobalOverlayController(UndoOverlayController(presentationData: presentationData, content: .sticker(context: strongSelf.context, file: item.file, loop: true, title: strongSelf.strings.Premium_MaxFavedStickersTitle("\(limit)").string, text: text, undoText: nil, customAction: nil), elevatedLayout: false, action: { [weak self] action in
if let strongSelf = self {
if case .info = action {
let controller = PremiumIntroScreen(context: strongSelf.context, source: .savedStickers)

View File

@ -3762,7 +3762,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate
let _ = (strongSelf.context.engine.stickers.loadedStickerPack(reference: stickerPackReference, forceActualized: false)
|> deliverOnMainQueue).start(next: { [weak self] stickerPack in
if let strongSelf = self, case let .result(info, _, _) = stickerPack {
strongSelf.controller?.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .sticker(context: strongSelf.context, file: file, title: nil, text: strongSelf.presentationData.strings.PeerInfo_TopicIconInfoText(info.title).string, undoText: strongSelf.presentationData.strings.Stickers_PremiumPackView, customAction: nil), elevatedLayout: false, action: { [weak self] action in
strongSelf.controller?.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .sticker(context: strongSelf.context, file: file, loop: true, title: nil, text: strongSelf.presentationData.strings.PeerInfo_TopicIconInfoText(info.title).string, undoText: strongSelf.presentationData.strings.Stickers_PremiumPackView, customAction: nil), elevatedLayout: false, action: { [weak self] action in
if let strongSelf = self, action == .undo {
strongSelf.presentEmojiList(packReference: stickerPackReference)
}

View File

@ -199,7 +199,7 @@ final class PeerSelectionControllerNode: ASDisplayNode {
if let requestPeerType = self.requestPeerType {
chatListMode = .peerType(type: requestPeerType, hasCreate: hasCreation)
} else {
chatListMode = .peers(filter: filter, isSelecting: false, additionalCategories: chatListCategories, chatListFilters: nil, displayAutoremoveTimeout: false)
chatListMode = .peers(filter: filter, isSelecting: false, additionalCategories: chatListCategories, chatListFilters: nil, displayAutoremoveTimeout: false, displayPresence: false)
}
if hasFilters {

View File

@ -21,6 +21,7 @@ import CameraScreen
import LegacyComponents
import LegacyMediaPickerUI
import LegacyCamera
import AvatarNode
private class DetailsChatPlaceholderNode: ASDisplayNode, NavigationDetailsPlaceholderNode {
private var presentationData: PresentationData
@ -269,25 +270,114 @@ public final class TelegramRootController: NavigationController {
item = TGMediaAsset(phAsset: asset)
}
let context = self.context
legacyStoryMediaEditor(context: self.context, item: item, getCaptionPanelView: { return nil }, completion: { result in
legacyStoryMediaEditor(context: self.context, item: item, getCaptionPanelView: { return nil }, completion: { [weak self] mediaResult in
dismissCameraImpl?()
switch result {
case let .image(image):
_ = image
case let .video(path):
_ = path
case let .asset(asset):
let options = PHImageRequestOptions()
options.deliveryMode = .highQualityFormat
options.isNetworkAccessAllowed = true
PHImageManager.default().requestImageData(for: asset, options:options, resultHandler: { data, _, _, _ in
if let data, let image = UIImage(data: data) {
Queue.mainQueue().async {
let _ = context.engine.messages.uploadStory(media: .image(dimensions: PixelDimensions(image.size), data: data)).start()
}
}
})
guard let self else {
return
}
enum AdditionalCategoryId: Int {
case everyone
case contacts
case closeFriends
}
let presentationData = self.context.sharedContext.currentPresentationData.with({ $0 })
let additionalCategories: [ChatListNodeAdditionalCategory] = [
ChatListNodeAdditionalCategory(
id: AdditionalCategoryId.everyone.rawValue,
icon: generateAvatarImage(size: CGSize(width: 40.0, height: 40.0), icon: generateTintedImage(image: UIImage(bundleImageName: "Chat List/Filters/Channel"), color: .white), cornerRadius: nil, color: .blue),
smallIcon: generateAvatarImage(size: CGSize(width: 22.0, height: 22.0), icon: generateTintedImage(image: UIImage(bundleImageName: "Chat List/Filters/Channel"), color: .white), iconScale: 0.6, cornerRadius: 6.0, circleCorners: true, color: .blue),
title: "Everyone",
appearance: .option(sectionTitle: "WHO CAN VIEW FOR 24 HOURS")
),
ChatListNodeAdditionalCategory(
id: AdditionalCategoryId.contacts.rawValue,
icon: generateAvatarImage(size: CGSize(width: 40.0, height: 40.0), icon: generateTintedImage(image: UIImage(bundleImageName: "Chat List/Tabs/IconContacts"), color: .white), iconScale: 1.0 * 0.8, cornerRadius: nil, color: .yellow),
smallIcon: generateAvatarImage(size: CGSize(width: 22.0, height: 22.0), icon: generateTintedImage(image: UIImage(bundleImageName: "Chat List/Tabs/IconContacts"), color: .white), iconScale: 0.6 * 0.8, cornerRadius: 6.0, circleCorners: true, color: .yellow),
title: presentationData.strings.ChatListFolder_CategoryContacts,
appearance: .option(sectionTitle: "WHO CAN VIEW FOR 24 HOURS")
),
ChatListNodeAdditionalCategory(
id: AdditionalCategoryId.closeFriends.rawValue,
icon: generateAvatarImage(size: CGSize(width: 40.0, height: 40.0), icon: generateTintedImage(image: UIImage(bundleImageName: "Call/StarHighlighted"), color: .white), iconScale: 1.0 * 0.6, cornerRadius: nil, color: .green),
smallIcon: generateAvatarImage(size: CGSize(width: 22.0, height: 22.0), icon: generateTintedImage(image: UIImage(bundleImageName: "Call/StarHighlighted"), color: .white), iconScale: 0.6 * 0.6, cornerRadius: 6.0, circleCorners: true, color: .green),
title: "Close Friends",
appearance: .option(sectionTitle: "WHO CAN VIEW FOR 24 HOURS")
)
]
let selectionController = self.context.sharedContext.makeContactMultiselectionController(ContactMultiselectionControllerParams(context: self.context, mode: .chatSelection(ContactMultiselectionControllerMode.ChatSelection(
title: "Share Story",
searchPlaceholder: "Search contacts",
selectedChats: Set(),
additionalCategories: ContactMultiselectionControllerAdditionalCategories(categories: additionalCategories, selectedCategories: Set([AdditionalCategoryId.everyone.rawValue])),
chatListFilters: nil,
displayPresence: true
)), options: [], filters: [.excludeSelf], alwaysEnabled: true, limit: 1000, reachedLimit: { _ in
}))
selectionController.navigationPresentation = .modal
self.pushViewController(selectionController)
let _ = (selectionController.result
|> take(1)
|> deliverOnMainQueue).start(next: { [weak self, weak selectionController] result in
guard let self else {
return
}
guard case let .result(peerIds, additionalCategoryIds) = result else {
selectionController?.dismiss()
return
}
var privacy = EngineStoryPrivacy(base: .everyone, additionallyIncludePeers: [])
if additionalCategoryIds.contains(AdditionalCategoryId.everyone.rawValue) {
privacy.base = .everyone
} else if additionalCategoryIds.contains(AdditionalCategoryId.contacts.rawValue) {
privacy.base = .contacts
} else if additionalCategoryIds.contains(AdditionalCategoryId.closeFriends.rawValue) {
privacy.base = .closeFriends
}
privacy.additionallyIncludePeers = peerIds.compactMap { id -> EnginePeer.Id? in
switch id {
case let .peer(peerId):
return peerId
default:
return nil
}
}
selectionController?.displayProgress = true
switch mediaResult {
case let .image(image):
_ = image
selectionController?.dismiss()
case let .video(path):
_ = path
selectionController?.dismiss()
case let .asset(asset):
let options = PHImageRequestOptions()
options.deliveryMode = .highQualityFormat
options.isNetworkAccessAllowed = true
PHImageManager.default().requestImageData(for: asset, options:options, resultHandler: { [weak self] data, _, _, _ in
if let data, let image = UIImage(data: data) {
Queue.mainQueue().async {
let _ = (context.engine.messages.uploadStory(media: .image(dimensions: PixelDimensions(image.size), data: data), privacy: privacy)
|> deliverOnMainQueue).start(completed: {
guard let self else {
return
}
let _ = self
selectionController?.dismiss()
})
}
}
})
}
})
}, present: { c, a in
presentImpl?(c)
})

View File

@ -34,7 +34,7 @@ public enum UndoOverlayContent {
case voiceChatRecording(text: String)
case voiceChatFlag(text: String)
case voiceChatCanSpeak(text: String)
case sticker(context: AccountContext, file: TelegramMediaFile, title: String?, text: String, undoText: String?, customAction: (() -> Void)?)
case sticker(context: AccountContext, file: TelegramMediaFile, loop: Bool, title: String?, text: String, undoText: String?, customAction: (() -> Void)?)
case copy(text: String)
case mediaSaved(text: String)
case paymentSent(currencyValue: String, itemTitle: String)

View File

@ -672,7 +672,7 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode {
displayUndo = false
self.originalRemainingSeconds = 3
case let .sticker(context, file, title, text, customUndoText, _):
case let .sticker(context, file, loop, title, text, customUndoText, _):
self.avatarNode = nil
self.iconNode = nil
self.iconCheckNode = nil
@ -769,7 +769,7 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode {
case let .animated(resource):
let animatedStickerNode = DefaultAnimatedStickerNodeImpl()
self.animatedStickerNode = animatedStickerNode
animatedStickerNode.setup(source: AnimatedStickerResourceSource(account: context.account, resource: resource._asResource(), isVideo: file.isVideoSticker), width: 80, height: 80, mode: .cached)
animatedStickerNode.setup(source: AnimatedStickerResourceSource(account: context.account, resource: resource._asResource(), isVideo: file.isVideoSticker), width: 80, height: 80, playbackMode: loop ? .loop : .once, mode: .cached)
}
}
case let .copy(text):
@ -1088,7 +1088,7 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode {
@objc private func undoButtonPressed() {
switch self.content {
case let .sticker(_, _, _, _, _, customAction):
case let .sticker(_, _, _, _, _, _, customAction):
if let customAction = customAction {
customAction()
} else {