mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-15 21:45:19 +00:00
[WIP] Stories
This commit is contained in:
parent
50b6c18065
commit
e8dab90584
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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)
|
||||
})
|
||||
|
@ -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 {
|
||||
|
@ -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))
|
||||
|
@ -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
|
||||
|
@ -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() {
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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)?
|
||||
|
@ -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)
|
||||
|
@ -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 {
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -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)
|
||||
}
|
||||
}))
|
||||
|
||||
|
@ -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
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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> {
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
37
submodules/TelegramUI/Components/ShareWithPeersScreen/BUILD
Normal file
37
submodules/TelegramUI/Components/ShareWithPeersScreen/BUILD
Normal 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",
|
||||
],
|
||||
)
|
@ -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)
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@ -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",
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -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)
|
||||
|
||||
|
@ -37,7 +37,7 @@ public enum StoryChatContent {
|
||||
peer: author
|
||||
))
|
||||
},
|
||||
targetMessageId: nil,
|
||||
peerId: itemSet.peerId,
|
||||
storyItem: item,
|
||||
preload: nil,
|
||||
delete: { [weak storyList] in
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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])
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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))
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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)
|
||||
})
|
||||
|
@ -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)
|
||||
|
@ -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 {
|
||||
|
Loading…
x
Reference in New Issue
Block a user