mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
Merge branch 'master' of gitlab.com:peter-iakovlev/telegram-ios
This commit is contained in:
commit
b10ae1b12a
@ -2604,6 +2604,18 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
|
||||
})
|
||||
})))
|
||||
} else {
|
||||
items.append(.action(ContextMenuActionItem(text: "Send Message", icon: { theme in
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/MessageBubble"), color: theme.contextMenu.primaryColor)
|
||||
}, action: { [weak self] c, _ in
|
||||
c.dismiss(completion: {
|
||||
guard let self, let navigationController = self.navigationController as? NavigationController else {
|
||||
return
|
||||
}
|
||||
|
||||
self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: self.context, chatLocation: .peer(peer)))
|
||||
})
|
||||
})))
|
||||
|
||||
items.append(.action(ContextMenuActionItem(text: "View Profile", icon: { theme in
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/User"), color: theme.contextMenu.primaryColor)
|
||||
}, action: { [weak self] c, _ in
|
||||
@ -2671,7 +2683,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
|
||||
}
|
||||
})))
|
||||
|
||||
items.append(.action(ContextMenuActionItem(text: "Hide", icon: { theme in
|
||||
items.append(.action(ContextMenuActionItem(text: "Move to Contacts", icon: { theme in
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/MoveToContacts"), color: theme.contextMenu.primaryColor)
|
||||
}, action: { [weak self] _, f in
|
||||
f(.dismissWithoutContent)
|
||||
@ -3727,7 +3739,9 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
|
||||
searchContentNode.placeholderNode.frame = previousFrame
|
||||
}
|
||||
|
||||
self.chatListDisplayNode.tempAllowAvatarExpansion = true
|
||||
self.requestLayout(transition: .animated(duration: 0.5, curve: .spring))
|
||||
self.chatListDisplayNode.tempAllowAvatarExpansion = false
|
||||
|
||||
//TODO:swap tabs
|
||||
|
||||
|
@ -1690,7 +1690,7 @@ final class ChatListControllerNode: ASDisplayNode, UIGestureRecognizerDelegate {
|
||||
var didBeginSelectingChatsWhileEditing: Bool = false
|
||||
var isEditing: Bool = false
|
||||
|
||||
private var tempAllowAvatarExpansion: Bool = false
|
||||
var tempAllowAvatarExpansion: Bool = false
|
||||
private var tempDisableStoriesAnimations: Bool = false
|
||||
private var tempNavigationScrollingTransition: ContainedViewLayoutTransition?
|
||||
|
||||
|
@ -2807,7 +2807,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
|
||||
component: AnyComponent(AvatarStoryIndicatorComponent(
|
||||
hasUnseen: storyState.hasUnseen,
|
||||
hasUnseenCloseFriendsItems: storyState.hasUnseenCloseFriends,
|
||||
isDarkTheme: item.presentationData.theme.overallDarkAppearance,
|
||||
theme: item.presentationData.theme,
|
||||
activeLineWidth: 2.0,
|
||||
inactiveLineWidth: 1.0 + UIScreenPixel,
|
||||
counters: nil
|
||||
|
@ -43,6 +43,7 @@ swift_library(
|
||||
"//submodules/TelegramUI/Components/ChatListHeaderComponent",
|
||||
"//submodules/ComponentFlow",
|
||||
"//submodules/TooltipUI",
|
||||
"//submodules/UndoUI",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
@ -9,6 +9,7 @@ import AlertUI
|
||||
import PresentationDataUtils
|
||||
import OverlayStatusController
|
||||
import LocalizedPeerData
|
||||
import UndoUI
|
||||
import TooltipUI
|
||||
|
||||
func contactContextMenuItems(context: AccountContext, peerId: EnginePeer.Id, contactsController: ContactsController?, isStories: Bool) -> Signal<[ContextMenuItem], NoError> {
|
||||
@ -17,16 +18,76 @@ func contactContextMenuItems(context: AccountContext, peerId: EnginePeer.Id, con
|
||||
return context.engine.data.get(
|
||||
TelegramEngine.EngineData.Item.Peer.Peer(id: peerId),
|
||||
TelegramEngine.EngineData.Item.Peer.AreVoiceCallsAvailable(id: peerId),
|
||||
TelegramEngine.EngineData.Item.Peer.AreVideoCallsAvailable(id: peerId)
|
||||
TelegramEngine.EngineData.Item.Peer.AreVideoCallsAvailable(id: peerId),
|
||||
TelegramEngine.EngineData.Item.Peer.NotificationSettings(id: peerId)
|
||||
)
|
||||
|> map { [weak contactsController] peer, areVoiceCallsAvailable, areVideoCallsAvailable -> [ContextMenuItem] in
|
||||
|> map { [weak contactsController] peer, areVoiceCallsAvailable, areVideoCallsAvailable, notificationSettings -> [ContextMenuItem] in
|
||||
var items: [ContextMenuItem] = []
|
||||
|
||||
if isStories {
|
||||
//TODO:localize
|
||||
items.append(.action(ContextMenuActionItem(text: "Unhide", icon: { theme in
|
||||
items.append(.action(ContextMenuActionItem(text: "View Profile", icon: { theme in
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/User"), color: theme.contextMenu.primaryColor)
|
||||
}, action: { c, _ in
|
||||
c.dismiss(completion: {
|
||||
let _ = (context.engine.data.get(
|
||||
TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)
|
||||
)
|
||||
|> deliverOnMainQueue).start(next: { peer in
|
||||
guard let peer = peer, let controller = context.sharedContext.makePeerInfoController(context: context, updatedPresentationData: nil, peer: peer._asPeer(), mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) else {
|
||||
return
|
||||
}
|
||||
(contactsController?.navigationController as? NavigationController)?.pushViewController(controller)
|
||||
})
|
||||
})
|
||||
})))
|
||||
|
||||
let isMuted = notificationSettings.storiesMuted == true
|
||||
items.append(.action(ContextMenuActionItem(text: isMuted ? "Notify" : "Not Notify", icon: { theme in
|
||||
return generateTintedImage(image: UIImage(bundleImageName: isMuted ? "Chat/Context Menu/Unmute" : "Chat/Context Menu/Muted"), color: theme.contextMenu.primaryColor)
|
||||
}, action: { _, f in
|
||||
f(.default)
|
||||
|
||||
let _ = context.engine.peers.togglePeerStoriesMuted(peerId: peerId).start()
|
||||
|
||||
if let peer {
|
||||
let iconColor = UIColor.white
|
||||
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
if isMuted {
|
||||
contactsController?.present(UndoOverlayController(
|
||||
presentationData: presentationData,
|
||||
content: .universal(animation: "anim_profileunmute", scale: 0.075, colors: [
|
||||
"Middle.Group 1.Fill 1": iconColor,
|
||||
"Top.Group 1.Fill 1": iconColor,
|
||||
"Bottom.Group 1.Fill 1": iconColor,
|
||||
"EXAMPLE.Group 1.Fill 1": iconColor,
|
||||
"Line.Group 1.Stroke 1": iconColor
|
||||
], title: nil, text: "You will now get a notification whenever **\(peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder))** posts a story.", customUndoText: nil, timeout: nil),
|
||||
elevatedLayout: false,
|
||||
animateInAsReplacement: false,
|
||||
action: { _ in return false }
|
||||
), in: .current)
|
||||
} else {
|
||||
contactsController?.present(UndoOverlayController(
|
||||
presentationData: presentationData,
|
||||
content: .universal(animation: "anim_profilemute", scale: 0.075, colors: [
|
||||
"Middle.Group 1.Fill 1": iconColor,
|
||||
"Top.Group 1.Fill 1": iconColor,
|
||||
"Bottom.Group 1.Fill 1": iconColor,
|
||||
"EXAMPLE.Group 1.Fill 1": iconColor,
|
||||
"Line.Group 1.Stroke 1": iconColor
|
||||
], title: nil, text: "You will no longer receive a notification when **\(peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder))** posts a story.", customUndoText: nil, timeout: nil),
|
||||
elevatedLayout: false,
|
||||
animateInAsReplacement: false,
|
||||
action: { _ in return false }
|
||||
), in: .current)
|
||||
}
|
||||
}
|
||||
})))
|
||||
|
||||
items.append(.action(ContextMenuActionItem(text: "Move to chats", icon: { theme in
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/MoveToChats"), color: theme.contextMenu.primaryColor)
|
||||
}, action: { [weak contactsController] _, f in
|
||||
}, action: { _, f in
|
||||
f(.default)
|
||||
|
||||
context.engine.peers.updatePeerStoriesHidden(id: peerId, isHidden: false)
|
||||
@ -45,6 +106,8 @@ func contactContextMenuItems(context: AccountContext, peerId: EnginePeer.Id, con
|
||||
)
|
||||
contactsController?.present(tooltipController, in: .window(.root))
|
||||
})))
|
||||
|
||||
return items
|
||||
}
|
||||
|
||||
items.append(.action(ContextMenuActionItem(text: strings.ContactList_Context_SendMessage, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Message"), color: theme.contextMenu.primaryColor) }, action: { _, f in
|
||||
|
@ -559,67 +559,6 @@ public class ContactsController: ViewController {
|
||||
self.push(storyContainerScreen)
|
||||
})
|
||||
}
|
||||
|
||||
/*componentView.storyContextPeerAction = { [weak self] sourceNode, gesture, peer in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
|
||||
var items: [ContextMenuItem] = []
|
||||
|
||||
//TODO:localize
|
||||
if peer.id == self.context.account.peerId {
|
||||
} else {
|
||||
items.append(.action(ContextMenuActionItem(text: "View Profile", icon: { theme in
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/User"), color: theme.contextMenu.primaryColor)
|
||||
}, action: { [weak self] c, _ in
|
||||
c.dismiss(completion: {
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
|
||||
let _ = (self.context.engine.data.get(
|
||||
TelegramEngine.EngineData.Item.Peer.Peer(id: peer.id)
|
||||
)
|
||||
|> deliverOnMainQueue).start(next: { [weak self] peer in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
guard let peer = peer, let controller = self.context.sharedContext.makePeerInfoController(context: self.context, updatedPresentationData: nil, peer: peer._asPeer(), mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) else {
|
||||
return
|
||||
}
|
||||
(self.navigationController as? NavigationController)?.pushViewController(controller)
|
||||
})
|
||||
})
|
||||
})))
|
||||
/*items.append(.action(ContextMenuActionItem(text: "Mute", icon: { theme in
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Unmute"), color: theme.contextMenu.primaryColor)
|
||||
}, action: { _, f in
|
||||
f(.default)
|
||||
})))*/
|
||||
|
||||
if case let .user(user) = peer, let storiesHidden = user.storiesHidden, storiesHidden {
|
||||
items.append(.action(ContextMenuActionItem(text: "Unarchive", icon: { theme in
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Unarchive"), color: theme.contextMenu.primaryColor)
|
||||
}, action: { [weak self] _, f in
|
||||
f(.default)
|
||||
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.context.engine.peers.updatePeerStoriesHidden(id: peer.id, isHidden: false)
|
||||
})))
|
||||
}
|
||||
}
|
||||
|
||||
if items.isEmpty {
|
||||
return
|
||||
}
|
||||
|
||||
let controller = ContextController(account: self.context.account, presentationData: self.presentationData, source: .extracted(ChatListHeaderBarContextExtractedContentSource(controller: self, sourceNode: sourceNode, keepInPlace: false)), items: .single(ContextController.Items(content: .list(items))), recognizer: nil, gesture: gesture)
|
||||
self.context.sharedContext.mainWindow?.presentInGlobalOverlay(controller)
|
||||
}
|
||||
}*/
|
||||
}
|
||||
|
||||
@objc private func sortPressed() {
|
||||
|
@ -527,10 +527,18 @@ final class ContactsControllerNode: ASDisplayNode, UIGestureRecognizerDelegate {
|
||||
guard let contactsController = self.controller else {
|
||||
return
|
||||
}
|
||||
let chatController = self.context.sharedContext.makeChatController(context: self.context, chatLocation: .peer(id: peer.id), subject: nil, botStart: nil, mode: .standard(previewing: true))
|
||||
chatController.canReadHistory.set(false)
|
||||
let contextController = ContextController(account: self.context.account, presentationData: self.presentationData, source: .controller(ContextControllerContentSourceImpl(controller: chatController, sourceNode: node)), items: contactContextMenuItems(context: self.context, peerId: peer.id, contactsController: contactsController, isStories: isStories) |> map { ContextController.Items(content: .list($0)) }, gesture: gesture)
|
||||
contactsController.presentInGlobalOverlay(contextController)
|
||||
|
||||
let items = contactContextMenuItems(context: self.context, peerId: peer.id, contactsController: contactsController, isStories: isStories) |> map { ContextController.Items(content: .list($0)) }
|
||||
|
||||
if isStories, let node = node?.subnodes?.first(where: { $0 is ContextExtractedContentContainingNode }) as? ContextExtractedContentContainingNode {
|
||||
let controller = ContextController(account: self.context.account, presentationData: self.presentationData, source: .extracted(ContactContextExtractedContentSource(sourceNode: node, shouldBeDismissed: .single(false))), items: items, recognizer: nil, gesture: gesture)
|
||||
contactsController.presentInGlobalOverlay(controller)
|
||||
} else {
|
||||
let chatController = self.context.sharedContext.makeChatController(context: self.context, chatLocation: .peer(id: peer.id), subject: nil, botStart: nil, mode: .standard(previewing: true))
|
||||
chatController.canReadHistory.set(false)
|
||||
let contextController = ContextController(account: self.context.account, presentationData: self.presentationData, source: .controller(ContextControllerContentSourceImpl(controller: chatController, sourceNode: node)), items: items, gesture: gesture)
|
||||
contactsController.presentInGlobalOverlay(contextController)
|
||||
}
|
||||
}
|
||||
|
||||
func activateSearch(placeholderNode: SearchBarPlaceholderNode) {
|
||||
@ -616,3 +624,26 @@ final class ContactsControllerNode: ASDisplayNode, UIGestureRecognizerDelegate {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private final class ContactContextExtractedContentSource: ContextExtractedContentSource {
|
||||
let keepInPlace: Bool = false
|
||||
let ignoreContentTouches: Bool = true
|
||||
let blurBackground: Bool = true
|
||||
|
||||
let shouldBeDismissed: Signal<Bool, NoError>
|
||||
|
||||
private let sourceNode: ContextExtractedContentContainingNode
|
||||
|
||||
init(sourceNode: ContextExtractedContentContainingNode, shouldBeDismissed: Signal<Bool, NoError>? = nil) {
|
||||
self.sourceNode = sourceNode
|
||||
self.shouldBeDismissed = shouldBeDismissed ?? .single(false)
|
||||
}
|
||||
|
||||
func takeView() -> ContextControllerTakeViewInfo? {
|
||||
return ContextControllerTakeViewInfo(containingItem: .node(self.sourceNode), contentAreaInScreenSpace: UIScreen.main.bounds)
|
||||
}
|
||||
|
||||
func putBack() -> ContextControllerPutBackViewInfo? {
|
||||
return ContextControllerPutBackViewInfo(contentAreaInScreenSpace: UIScreen.main.bounds)
|
||||
}
|
||||
}
|
||||
|
@ -1115,7 +1115,7 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode {
|
||||
component: AnyComponent(AvatarStoryIndicatorComponent(
|
||||
hasUnseen: storyStats.unseen != 0,
|
||||
hasUnseenCloseFriendsItems: storyStats.hasUnseenCloseFriends,
|
||||
isDarkTheme: item.presentationData.theme.overallDarkAppearance,
|
||||
theme: item.presentationData.theme,
|
||||
activeLineWidth: 1.0 + UIScreenPixel,
|
||||
inactiveLineWidth: 1.0 + UIScreenPixel,
|
||||
counters: AvatarStoryIndicatorComponent.Counters(totalCount: storyStats.total, unseenCount: storyStats.unseen)
|
||||
|
@ -105,6 +105,9 @@ func telegramMediaFileAttributesFromApiAttributes(_ attributes: [Api.DocumentAtt
|
||||
if (flags & (1 << 1)) != 0 {
|
||||
videoFlags.insert(.supportsStreaming)
|
||||
}
|
||||
if (flags & (1 << 3)) != 0 {
|
||||
videoFlags.insert(.isSilent)
|
||||
}
|
||||
result.append(.Video(duration: Double(duration), size: PixelDimensions(width: w, height: h), flags: videoFlags, preloadSize: preloadSize))
|
||||
case let .documentAttributeAudio(flags, duration, title, performer, waveform):
|
||||
let isVoice = (flags & (1 << 10)) != 0
|
||||
|
@ -569,6 +569,9 @@ func inputDocumentAttributesFromFileAttributes(_ fileAttributes: [TelegramMediaF
|
||||
if preloadSize != nil {
|
||||
flags |= (1 << 2)
|
||||
}
|
||||
if videoFlags.contains(.isSilent) {
|
||||
flags |= (1 << 3)
|
||||
}
|
||||
|
||||
attributes.append(.documentAttributeVideo(flags: flags, duration: duration, w: Int32(size.width), h: Int32(size.height), preloadPrefixSize: preloadSize))
|
||||
case let .Audio(isVoice, duration, title, performer, waveform):
|
||||
|
@ -188,6 +188,7 @@ public struct TelegramMediaVideoFlags: OptionSet {
|
||||
|
||||
public static let instantRoundVideo = TelegramMediaVideoFlags(rawValue: 1 << 0)
|
||||
public static let supportsStreaming = TelegramMediaVideoFlags(rawValue: 1 << 1)
|
||||
public static let isSilent = TelegramMediaVideoFlags(rawValue: 1 << 3)
|
||||
}
|
||||
|
||||
public struct StickerMaskCoords: PostboxCoding, Equatable {
|
||||
|
@ -13,6 +13,7 @@ public extension Stories {
|
||||
case entities
|
||||
case pin
|
||||
case privacy
|
||||
case isForwardingDisabled
|
||||
case period
|
||||
case randomId
|
||||
}
|
||||
@ -24,6 +25,7 @@ public extension Stories {
|
||||
public let entities: [MessageTextEntity]
|
||||
public let pin: Bool
|
||||
public let privacy: EngineStoryPrivacy
|
||||
public let isForwardingDisabled: Bool
|
||||
public let period: Int32
|
||||
public let randomId: Int64
|
||||
|
||||
@ -35,6 +37,7 @@ public extension Stories {
|
||||
entities: [MessageTextEntity],
|
||||
pin: Bool,
|
||||
privacy: EngineStoryPrivacy,
|
||||
isForwardingDisabled: Bool,
|
||||
period: Int32,
|
||||
randomId: Int64
|
||||
) {
|
||||
@ -45,6 +48,7 @@ public extension Stories {
|
||||
self.entities = entities
|
||||
self.pin = pin
|
||||
self.privacy = privacy
|
||||
self.isForwardingDisabled = isForwardingDisabled
|
||||
self.period = period
|
||||
self.randomId = randomId
|
||||
}
|
||||
@ -62,6 +66,7 @@ public extension Stories {
|
||||
self.entities = try container.decode([MessageTextEntity].self, forKey: .entities)
|
||||
self.pin = try container.decode(Bool.self, forKey: .pin)
|
||||
self.privacy = try container.decode(EngineStoryPrivacy.self, forKey: .privacy)
|
||||
self.isForwardingDisabled = try container.decodeIfPresent(Bool.self, forKey: .isForwardingDisabled) ?? false
|
||||
self.period = try container.decode(Int32.self, forKey: .period)
|
||||
self.randomId = try container.decode(Int64.self, forKey: .randomId)
|
||||
}
|
||||
@ -80,6 +85,7 @@ public extension Stories {
|
||||
try container.encode(self.entities, forKey: .entities)
|
||||
try container.encode(self.pin, forKey: .pin)
|
||||
try container.encode(self.privacy, forKey: .privacy)
|
||||
try container.encode(self.isForwardingDisabled, forKey: .isForwardingDisabled)
|
||||
try container.encode(self.period, forKey: .period)
|
||||
try container.encode(self.randomId, forKey: .randomId)
|
||||
}
|
||||
@ -106,6 +112,9 @@ public extension Stories {
|
||||
if lhs.privacy != rhs.privacy {
|
||||
return false
|
||||
}
|
||||
if lhs.isForwardingDisabled != rhs.isForwardingDisabled {
|
||||
return false
|
||||
}
|
||||
if lhs.period != rhs.period {
|
||||
return false
|
||||
}
|
||||
@ -260,7 +269,7 @@ final class PendingStoryManager {
|
||||
self.currentPendingItemContext = pendingItemContext
|
||||
|
||||
let stableId = firstItem.stableId
|
||||
pendingItemContext.disposable = (_internal_uploadStoryImpl(postbox: self.postbox, network: self.network, accountPeerId: self.accountPeerId, stateManager: self.stateManager, messageMediaPreuploadManager: self.messageMediaPreuploadManager, revalidationContext: self.revalidationContext, auxiliaryMethods: self.auxiliaryMethods, stableId: stableId, media: firstItem.media, text: firstItem.text, entities: firstItem.entities, pin: firstItem.pin, privacy: firstItem.privacy, period: Int(firstItem.period), randomId: firstItem.randomId)
|
||||
pendingItemContext.disposable = (_internal_uploadStoryImpl(postbox: self.postbox, network: self.network, accountPeerId: self.accountPeerId, stateManager: self.stateManager, messageMediaPreuploadManager: self.messageMediaPreuploadManager, revalidationContext: self.revalidationContext, auxiliaryMethods: self.auxiliaryMethods, stableId: stableId, media: firstItem.media, text: firstItem.text, entities: firstItem.entities, pin: firstItem.pin, privacy: firstItem.privacy, isForwardingDisabled: firstItem.isForwardingDisabled, period: Int(firstItem.period), randomId: firstItem.randomId)
|
||||
|> deliverOn(self.queue)).start(next: { [weak self] event in
|
||||
guard let `self` = self else {
|
||||
return
|
||||
|
@ -125,6 +125,7 @@ public enum Stories {
|
||||
case isExpired
|
||||
case isPublic
|
||||
case isCloseFriends
|
||||
case isForwardingDisabled
|
||||
}
|
||||
|
||||
public let id: Int32
|
||||
@ -139,6 +140,7 @@ public enum Stories {
|
||||
public let isExpired: Bool
|
||||
public let isPublic: Bool
|
||||
public let isCloseFriends: Bool
|
||||
public let isForwardingDisabled: Bool
|
||||
|
||||
public init(
|
||||
id: Int32,
|
||||
@ -152,7 +154,8 @@ public enum Stories {
|
||||
isPinned: Bool,
|
||||
isExpired: Bool,
|
||||
isPublic: Bool,
|
||||
isCloseFriends: Bool
|
||||
isCloseFriends: Bool,
|
||||
isForwardingDisabled: Bool
|
||||
) {
|
||||
self.id = id
|
||||
self.timestamp = timestamp
|
||||
@ -166,6 +169,7 @@ public enum Stories {
|
||||
self.isExpired = isExpired
|
||||
self.isPublic = isPublic
|
||||
self.isCloseFriends = isCloseFriends
|
||||
self.isForwardingDisabled = isForwardingDisabled
|
||||
}
|
||||
|
||||
public init(from decoder: Decoder) throws {
|
||||
@ -189,6 +193,7 @@ public enum Stories {
|
||||
self.isExpired = try container.decodeIfPresent(Bool.self, forKey: .isExpired) ?? false
|
||||
self.isPublic = try container.decodeIfPresent(Bool.self, forKey: .isPublic) ?? false
|
||||
self.isCloseFriends = try container.decodeIfPresent(Bool.self, forKey: .isCloseFriends) ?? false
|
||||
self.isForwardingDisabled = try container.decodeIfPresent(Bool.self, forKey: .isForwardingDisabled) ?? false
|
||||
}
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
@ -213,6 +218,7 @@ public enum Stories {
|
||||
try container.encode(self.isExpired, forKey: .isExpired)
|
||||
try container.encode(self.isPublic, forKey: .isPublic)
|
||||
try container.encode(self.isCloseFriends, forKey: .isCloseFriends)
|
||||
try container.encode(self.isForwardingDisabled, forKey: .isForwardingDisabled)
|
||||
}
|
||||
|
||||
public static func ==(lhs: Item, rhs: Item) -> Bool {
|
||||
@ -260,6 +266,9 @@ public enum Stories {
|
||||
if lhs.isCloseFriends != rhs.isCloseFriends {
|
||||
return false
|
||||
}
|
||||
if lhs.isForwardingDisabled != rhs.isForwardingDisabled {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
@ -680,7 +689,7 @@ private func apiInputPrivacyRules(privacy: EngineStoryPrivacy, transaction: Tran
|
||||
return privacyRules
|
||||
}
|
||||
|
||||
func _internal_uploadStory(account: Account, media: EngineStoryInputMedia, text: String, entities: [MessageTextEntity], pin: Bool, privacy: EngineStoryPrivacy, period: Int, randomId: Int64) {
|
||||
func _internal_uploadStory(account: Account, media: EngineStoryInputMedia, text: String, entities: [MessageTextEntity], pin: Bool, privacy: EngineStoryPrivacy, isForwardingDisabled: Bool, period: Int, randomId: Int64) {
|
||||
let inputMedia = prepareUploadStoryContent(account: account, media: media)
|
||||
|
||||
let _ = (account.postbox.transaction { transaction in
|
||||
@ -702,6 +711,7 @@ func _internal_uploadStory(account: Account, media: EngineStoryInputMedia, text:
|
||||
entities: entities,
|
||||
pin: pin,
|
||||
privacy: privacy,
|
||||
isForwardingDisabled: isForwardingDisabled,
|
||||
period: Int32(period),
|
||||
randomId: randomId
|
||||
))
|
||||
@ -747,7 +757,7 @@ private func _internal_putPendingStoryIdMapping(accountPeerId: PeerId, stableId:
|
||||
}
|
||||
}
|
||||
|
||||
func _internal_uploadStoryImpl(postbox: Postbox, network: Network, accountPeerId: PeerId, stateManager: AccountStateManager, messageMediaPreuploadManager: MessageMediaPreuploadManager, revalidationContext: MediaReferenceRevalidationContext, auxiliaryMethods: AccountAuxiliaryMethods, stableId: Int32, media: Media, text: String, entities: [MessageTextEntity], pin: Bool, privacy: EngineStoryPrivacy, period: Int, randomId: Int64) -> Signal<StoryUploadResult, NoError> {
|
||||
func _internal_uploadStoryImpl(postbox: Postbox, network: Network, accountPeerId: PeerId, stateManager: AccountStateManager, messageMediaPreuploadManager: MessageMediaPreuploadManager, revalidationContext: MediaReferenceRevalidationContext, auxiliaryMethods: AccountAuxiliaryMethods, stableId: Int32, media: Media, text: String, entities: [MessageTextEntity], pin: Bool, privacy: EngineStoryPrivacy, isForwardingDisabled: Bool, period: Int, randomId: Int64) -> Signal<StoryUploadResult, NoError> {
|
||||
let (contentSignal, originalMedia) = uploadedStoryContent(postbox: postbox, network: network, media: media, accountPeerId: accountPeerId, messageMediaPreuploadManager: messageMediaPreuploadManager, revalidationContext: revalidationContext, auxiliaryMethods: auxiliaryMethods)
|
||||
return contentSignal
|
||||
|> mapToSignal { result -> Signal<StoryUploadResult, NoError> in
|
||||
@ -787,6 +797,10 @@ func _internal_uploadStoryImpl(postbox: Postbox, network: Network, accountPeerId
|
||||
|
||||
flags |= 1 << 3
|
||||
|
||||
if isForwardingDisabled {
|
||||
flags |= 1 << 4
|
||||
}
|
||||
|
||||
return network.request(Api.functions.stories.sendStory(
|
||||
flags: flags,
|
||||
media: inputMedia,
|
||||
@ -835,7 +849,8 @@ func _internal_uploadStoryImpl(postbox: Postbox, network: Network, accountPeerId
|
||||
isPinned: item.isPinned,
|
||||
isExpired: item.isExpired,
|
||||
isPublic: item.isPublic,
|
||||
isCloseFriends: item.isCloseFriends
|
||||
isCloseFriends: item.isCloseFriends,
|
||||
isForwardingDisabled: item.isForwardingDisabled
|
||||
)
|
||||
if let entry = CodableEntry(Stories.StoredItem.item(updatedItem)) {
|
||||
items.append(StoryItemsTableEntry(value: entry, id: item.id, expirationTimestamp: updatedItem.expirationTimestamp))
|
||||
@ -983,7 +998,8 @@ func _internal_editStoryPrivacy(account: Account, id: Int32, privacy: EngineStor
|
||||
isPinned: item.isPinned,
|
||||
isExpired: item.isExpired,
|
||||
isPublic: item.isPublic,
|
||||
isCloseFriends: item.isCloseFriends
|
||||
isCloseFriends: item.isCloseFriends,
|
||||
isForwardingDisabled: item.isForwardingDisabled
|
||||
)
|
||||
if let entry = CodableEntry(Stories.StoredItem.item(updatedItem)) {
|
||||
transaction.setStory(id: storyId, value: entry)
|
||||
@ -1005,7 +1021,8 @@ func _internal_editStoryPrivacy(account: Account, id: Int32, privacy: EngineStor
|
||||
isPinned: item.isPinned,
|
||||
isExpired: item.isExpired,
|
||||
isPublic: item.isPublic,
|
||||
isCloseFriends: item.isCloseFriends
|
||||
isCloseFriends: item.isCloseFriends,
|
||||
isForwardingDisabled: item.isForwardingDisabled
|
||||
)
|
||||
if let entry = CodableEntry(Stories.StoredItem.item(updatedItem)) {
|
||||
items[index] = StoryItemsTableEntry(value: entry, id: item.id, expirationTimestamp: updatedItem.expirationTimestamp)
|
||||
@ -1136,7 +1153,8 @@ func _internal_updateStoriesArePinned(account: Account, ids: [Int32: EngineStory
|
||||
isPinned: isPinned,
|
||||
isExpired: item.isExpired,
|
||||
isPublic: item.isPublic,
|
||||
isCloseFriends: item.isCloseFriends
|
||||
isCloseFriends: item.isCloseFriends,
|
||||
isForwardingDisabled: item.isForwardingDisabled
|
||||
)
|
||||
if let entry = CodableEntry(Stories.StoredItem.item(updatedItem)) {
|
||||
items[index] = StoryItemsTableEntry(value: entry, id: item.id, expirationTimestamp: updatedItem.expirationTimestamp)
|
||||
@ -1157,7 +1175,8 @@ func _internal_updateStoriesArePinned(account: Account, ids: [Int32: EngineStory
|
||||
isPinned: isPinned,
|
||||
isExpired: item.isExpired,
|
||||
isPublic: item.isPublic,
|
||||
isCloseFriends: item.isCloseFriends
|
||||
isCloseFriends: item.isCloseFriends,
|
||||
isForwardingDisabled: item.isForwardingDisabled
|
||||
)
|
||||
updatedItems.append(updatedItem)
|
||||
}
|
||||
@ -1253,6 +1272,7 @@ extension Stories.StoredItem {
|
||||
let isExpired = (flags & (1 << 6)) != 0
|
||||
let isPublic = (flags & (1 << 7)) != 0
|
||||
let isCloseFriends = (flags & (1 << 8)) != 0
|
||||
let isForwardingDisabled = (flags & (1 << 10)) != 0
|
||||
|
||||
let item = Stories.Item(
|
||||
id: id,
|
||||
@ -1266,7 +1286,8 @@ extension Stories.StoredItem {
|
||||
isPinned: isPinned,
|
||||
isExpired: isExpired,
|
||||
isPublic: isPublic,
|
||||
isCloseFriends: isCloseFriends
|
||||
isCloseFriends: isCloseFriends,
|
||||
isForwardingDisabled: isForwardingDisabled
|
||||
)
|
||||
self = .item(item)
|
||||
} else {
|
||||
|
@ -44,8 +44,9 @@ public final class EngineStoryItem: Equatable {
|
||||
public let isPublic: Bool
|
||||
public let isPending: Bool
|
||||
public let isCloseFriends: Bool
|
||||
public let isForwardingDisabled: Bool
|
||||
|
||||
public init(id: Int32, timestamp: Int32, expirationTimestamp: Int32, media: EngineMedia, text: String, entities: [MessageTextEntity], views: Views?, privacy: EngineStoryPrivacy?, isPinned: Bool, isExpired: Bool, isPublic: Bool, isPending: Bool, isCloseFriends: Bool) {
|
||||
public init(id: Int32, timestamp: Int32, expirationTimestamp: Int32, media: EngineMedia, text: String, entities: [MessageTextEntity], views: Views?, privacy: EngineStoryPrivacy?, isPinned: Bool, isExpired: Bool, isPublic: Bool, isPending: Bool, isCloseFriends: Bool, isForwardingDisabled: Bool) {
|
||||
self.id = id
|
||||
self.timestamp = timestamp
|
||||
self.expirationTimestamp = expirationTimestamp
|
||||
@ -59,6 +60,7 @@ public final class EngineStoryItem: Equatable {
|
||||
self.isPublic = isPublic
|
||||
self.isPending = isPending
|
||||
self.isCloseFriends = isCloseFriends
|
||||
self.isForwardingDisabled = isForwardingDisabled
|
||||
}
|
||||
|
||||
public static func ==(lhs: EngineStoryItem, rhs: EngineStoryItem) -> Bool {
|
||||
@ -101,6 +103,9 @@ public final class EngineStoryItem: Equatable {
|
||||
if lhs.isCloseFriends != rhs.isCloseFriends {
|
||||
return false
|
||||
}
|
||||
if lhs.isForwardingDisabled != rhs.isForwardingDisabled {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
@ -129,7 +134,8 @@ extension EngineStoryItem {
|
||||
isPinned: self.isPinned,
|
||||
isExpired: self.isExpired,
|
||||
isPublic: self.isPublic,
|
||||
isCloseFriends: self.isCloseFriends
|
||||
isCloseFriends: self.isCloseFriends,
|
||||
isForwardingDisabled: self.isForwardingDisabled
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -487,7 +493,8 @@ public final class PeerStoryListContext {
|
||||
isExpired: item.isExpired,
|
||||
isPublic: item.isPublic,
|
||||
isPending: false,
|
||||
isCloseFriends: item.isCloseFriends
|
||||
isCloseFriends: item.isCloseFriends,
|
||||
isForwardingDisabled: item.isForwardingDisabled
|
||||
)
|
||||
items.append(mappedItem)
|
||||
}
|
||||
@ -593,7 +600,8 @@ public final class PeerStoryListContext {
|
||||
isExpired: item.isExpired,
|
||||
isPublic: item.isPublic,
|
||||
isPending: false,
|
||||
isCloseFriends: item.isCloseFriends
|
||||
isCloseFriends: item.isCloseFriends,
|
||||
isForwardingDisabled: item.isForwardingDisabled
|
||||
)
|
||||
storyItems.append(mappedItem)
|
||||
}
|
||||
@ -726,7 +734,8 @@ public final class PeerStoryListContext {
|
||||
isExpired: item.isExpired,
|
||||
isPublic: item.isPublic,
|
||||
isPending: false,
|
||||
isCloseFriends: item.isCloseFriends
|
||||
isCloseFriends: item.isCloseFriends,
|
||||
isForwardingDisabled: item.isForwardingDisabled
|
||||
)
|
||||
finalUpdatedState = updatedState
|
||||
}
|
||||
@ -764,7 +773,8 @@ public final class PeerStoryListContext {
|
||||
isExpired: item.isExpired,
|
||||
isPublic: item.isPublic,
|
||||
isPending: false,
|
||||
isCloseFriends: item.isCloseFriends
|
||||
isCloseFriends: item.isCloseFriends,
|
||||
isForwardingDisabled: item.isForwardingDisabled
|
||||
))
|
||||
updatedState.items.sort(by: { lhs, rhs in
|
||||
return lhs.timestamp > rhs.timestamp
|
||||
@ -911,7 +921,8 @@ public final class PeerExpiringStoryListContext {
|
||||
isExpired: item.isExpired,
|
||||
isPublic: item.isPublic,
|
||||
isPending: false,
|
||||
isCloseFriends: item.isCloseFriends
|
||||
isCloseFriends: item.isCloseFriends,
|
||||
isForwardingDisabled: item.isForwardingDisabled
|
||||
)
|
||||
items.append(.item(mappedItem))
|
||||
}
|
||||
|
@ -966,7 +966,8 @@ public extension TelegramEngine {
|
||||
isPinned: item.isPinned,
|
||||
isExpired: item.isExpired,
|
||||
isPublic: item.isPublic,
|
||||
isCloseFriends: item.isCloseFriends
|
||||
isCloseFriends: item.isCloseFriends,
|
||||
isForwardingDisabled: item.isForwardingDisabled
|
||||
))
|
||||
if let entry = CodableEntry(updatedItem) {
|
||||
currentItems[i] = StoryItemsTableEntry(value: entry, id: updatedItem.id, expirationTimestamp: updatedItem.expirationTimestamp)
|
||||
@ -980,8 +981,8 @@ public extension TelegramEngine {
|
||||
}
|
||||
}
|
||||
|
||||
public func uploadStory(media: EngineStoryInputMedia, text: String, entities: [MessageTextEntity], pin: Bool, privacy: EngineStoryPrivacy, period: Int, randomId: Int64) {
|
||||
_internal_uploadStory(account: self.account, media: media, text: text, entities: entities, pin: pin, privacy: privacy, period: period, randomId: randomId)
|
||||
public func uploadStory(media: EngineStoryInputMedia, text: String, entities: [MessageTextEntity], pin: Bool, privacy: EngineStoryPrivacy, isForwardingDisabled: Bool, period: Int, randomId: Int64) {
|
||||
_internal_uploadStory(account: self.account, media: media, text: text, entities: entities, pin: pin, privacy: privacy, isForwardingDisabled: isForwardingDisabled, period: period, randomId: randomId)
|
||||
}
|
||||
|
||||
public func lookUpPendingStoryIdMapping(stableId: Int32) -> Int32? {
|
||||
|
@ -214,7 +214,7 @@ public final class ChatAvatarNavigationNode: ASDisplayNode {
|
||||
component: AnyComponent(AvatarStoryIndicatorComponent(
|
||||
hasUnseen: storyData.hasUnseen,
|
||||
hasUnseenCloseFriendsItems: storyData.hasUnseenCloseFriends,
|
||||
isDarkTheme: theme.overallDarkAppearance,
|
||||
theme: theme,
|
||||
activeLineWidth: 1.0,
|
||||
inactiveLineWidth: 1.0,
|
||||
counters: nil
|
||||
|
@ -312,6 +312,7 @@ public final class ChatListHeaderComponent: Component {
|
||||
|
||||
var contentOffsetFraction: CGFloat = 0.0
|
||||
private(set) var centerContentWidth: CGFloat = 0.0
|
||||
private(set) var centerContentLeftInset: CGFloat = 0.0
|
||||
private(set) var centerContentRightInset: CGFloat = 0.0
|
||||
|
||||
private(set) var centerContentOffsetX: CGFloat = 0.0
|
||||
@ -639,8 +640,11 @@ public final class ChatListHeaderComponent: Component {
|
||||
}
|
||||
}
|
||||
|
||||
var centerContentLeftInset: CGFloat = 0.0
|
||||
centerContentLeftInset = leftOffset - 4.0
|
||||
|
||||
var centerContentRightInset: CGFloat = 0.0
|
||||
centerContentRightInset = size.width - rightOffset + 16.0
|
||||
centerContentRightInset = size.width - rightOffset - 8.0
|
||||
|
||||
var centerContentWidth: CGFloat = 0.0
|
||||
var centerContentOffsetX: CGFloat = 0.0
|
||||
@ -705,6 +709,7 @@ public final class ChatListHeaderComponent: Component {
|
||||
self.centerContentOffsetX = centerContentOffsetX
|
||||
self.centerContentOrigin = centerContentOrigin
|
||||
self.centerContentRightInset = centerContentRightInset
|
||||
self.centerContentLeftInset = centerContentLeftInset
|
||||
}
|
||||
}
|
||||
|
||||
@ -815,7 +820,7 @@ public final class ChatListHeaderComponent: Component {
|
||||
let previousComponent = self.component
|
||||
self.component = component
|
||||
|
||||
if let primaryContent = component.primaryContent {
|
||||
if var primaryContent = component.primaryContent {
|
||||
var primaryContentTransition = transition
|
||||
let primaryContentView: ContentView
|
||||
if let current = self.primaryContentView {
|
||||
@ -848,6 +853,19 @@ public final class ChatListHeaderComponent: Component {
|
||||
|
||||
let sideContentWidth: CGFloat = 0.0
|
||||
|
||||
if component.storySubscriptions != nil {
|
||||
primaryContent = Content(
|
||||
title: "",
|
||||
navigationBackTitle: primaryContent.navigationBackTitle,
|
||||
titleComponent: nil,
|
||||
chatListTitle: nil,
|
||||
leftButton: primaryContent.leftButton,
|
||||
rightButtons: primaryContent.rightButtons,
|
||||
backTitle: primaryContent.backTitle,
|
||||
backPressed: primaryContent.backPressed
|
||||
)
|
||||
}
|
||||
|
||||
primaryContentView.update(context: component.context, theme: component.theme, strings: component.strings, content: primaryContent, backTitle: primaryContent.backTitle, sideInset: component.sideInset, sideContentWidth: sideContentWidth, sideContentFraction: (1.0 - component.storiesFraction), size: availableSize, transition: primaryContentTransition)
|
||||
primaryContentTransition.setFrame(view: primaryContentView, frame: CGRect(origin: CGPoint(), size: availableSize))
|
||||
|
||||
@ -868,6 +886,28 @@ public final class ChatListHeaderComponent: Component {
|
||||
self.storyPeerList = storyPeerList
|
||||
}
|
||||
|
||||
var primaryTitle = ""
|
||||
var primaryTitleHasLock = false
|
||||
var primaryTitleHasActivity = false
|
||||
var primaryTitlePeerStatus: StoryPeerListComponent.PeerStatus?
|
||||
if let primaryContent = component.primaryContent {
|
||||
if let chatListTitle = primaryContent.chatListTitle {
|
||||
primaryTitle = chatListTitle.text
|
||||
primaryTitleHasLock = chatListTitle.isPasscodeSet
|
||||
primaryTitleHasActivity = chatListTitle.activity
|
||||
if let peerStatus = chatListTitle.peerStatus {
|
||||
switch peerStatus {
|
||||
case .premium:
|
||||
primaryTitlePeerStatus = .premium
|
||||
case let .emoji(status):
|
||||
primaryTitlePeerStatus = .emoji(status)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
primaryTitle = primaryContent.title
|
||||
}
|
||||
}
|
||||
|
||||
let _ = storyPeerList.update(
|
||||
transition: storyListTransition,
|
||||
component: AnyComponent(StoryPeerListComponent(
|
||||
@ -876,7 +916,11 @@ public final class ChatListHeaderComponent: Component {
|
||||
theme: component.theme,
|
||||
strings: component.strings,
|
||||
sideInset: component.sideInset,
|
||||
titleContentWidth: self.primaryContentView?.centerContentWidth ?? 0.0,
|
||||
title: primaryTitle,
|
||||
titleHasLock: primaryTitleHasLock,
|
||||
titleHasActivity: primaryTitleHasActivity,
|
||||
titlePeerStatus: primaryTitlePeerStatus,
|
||||
minTitleX: self.primaryContentView?.centerContentLeftInset ?? 0.0,
|
||||
maxTitleX: availableSize.width - (self.primaryContentView?.centerContentRightInset ?? 0.0),
|
||||
useHiddenList: component.storiesIncludeHidden,
|
||||
storySubscriptions: storySubscriptions,
|
||||
@ -895,14 +939,11 @@ public final class ChatListHeaderComponent: Component {
|
||||
}
|
||||
self.storyContextPeerAction?(sourceNode, gesture, peer)
|
||||
},
|
||||
updateTitleContentOffset: { [weak self] offset, transition in
|
||||
guard let self, let primaryContentView = self.primaryContentView else {
|
||||
openStatusSetup: { [weak self] sourceView in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
guard let chatListTitleView = primaryContentView.chatListTitleView else {
|
||||
return
|
||||
}
|
||||
transition.setSublayerTransform(view: chatListTitleView, transform: CATransform3DMakeTranslation(offset, 0.0, 0.0))
|
||||
self.component?.openStatusSetup(sourceView)
|
||||
}
|
||||
)),
|
||||
environment: {},
|
||||
@ -1002,12 +1043,7 @@ public final class ChatListHeaderComponent: Component {
|
||||
|
||||
storyListTransition.setFrame(view: storyPeerListComponentView, frame: CGRect(origin: CGPoint(x: -1.0 * availableSize.width * component.secondaryTransition + 0.0, y: storyPeerListMaxOffset), size: CGSize(width: availableSize.width, height: 79.0)))
|
||||
|
||||
var storyListNormalAlpha: CGFloat = 1.0
|
||||
if let chatListTitle = component.primaryContent?.chatListTitle {
|
||||
if chatListTitle.activity {
|
||||
storyListNormalAlpha = component.storiesFraction
|
||||
}
|
||||
}
|
||||
let storyListNormalAlpha: CGFloat = 1.0
|
||||
|
||||
let storyListAlpha: CGFloat = (1.0 - component.secondaryTransition) * storyListNormalAlpha
|
||||
storyListTransition.setAlpha(view: storyPeerListComponentView, alpha: storyListAlpha)
|
||||
|
@ -0,0 +1,24 @@
|
||||
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
|
||||
|
||||
swift_library(
|
||||
name = "EmptyStateIndicatorComponent",
|
||||
module_name = "EmptyStateIndicatorComponent",
|
||||
srcs = glob([
|
||||
"Sources/**/*.swift",
|
||||
]),
|
||||
copts = [
|
||||
"-warnings-as-errors",
|
||||
],
|
||||
deps = [
|
||||
"//submodules/Display",
|
||||
"//submodules/ComponentFlow",
|
||||
"//submodules/Components/AnimatedStickerComponent",
|
||||
"//submodules/Components/MultilineTextComponent",
|
||||
"//submodules/TelegramUI/Components/ButtonComponent",
|
||||
"//submodules/TelegramPresentationData",
|
||||
"//submodules/AccountContext",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
],
|
||||
)
|
@ -0,0 +1,183 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import ComponentFlow
|
||||
import AnimatedStickerComponent
|
||||
import ButtonComponent
|
||||
import TelegramPresentationData
|
||||
import AccountContext
|
||||
import MultilineTextComponent
|
||||
|
||||
public final class EmptyStateIndicatorComponent: Component {
|
||||
public let context: AccountContext
|
||||
public let theme: PresentationTheme
|
||||
public let animationName: String
|
||||
public let title: String
|
||||
public let text: String
|
||||
public let actionTitle: String
|
||||
public let action: () -> Void
|
||||
|
||||
public init(
|
||||
context: AccountContext,
|
||||
theme: PresentationTheme,
|
||||
animationName: String,
|
||||
title: String,
|
||||
text: String,
|
||||
actionTitle: String,
|
||||
action: @escaping () -> Void
|
||||
) {
|
||||
self.context = context
|
||||
self.theme = theme
|
||||
self.animationName = animationName
|
||||
self.title = title
|
||||
self.text = text
|
||||
self.actionTitle = actionTitle
|
||||
self.action = action
|
||||
}
|
||||
|
||||
public static func ==(lhs: EmptyStateIndicatorComponent, rhs: EmptyStateIndicatorComponent) -> Bool {
|
||||
if lhs.context !== rhs.context {
|
||||
return false
|
||||
}
|
||||
if lhs.theme !== rhs.theme {
|
||||
return false
|
||||
}
|
||||
if lhs.animationName != rhs.animationName {
|
||||
return false
|
||||
}
|
||||
if lhs.title != rhs.title {
|
||||
return false
|
||||
}
|
||||
if lhs.text != rhs.text {
|
||||
return false
|
||||
}
|
||||
if lhs.actionTitle != rhs.actionTitle {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
public final class View: UIView {
|
||||
private var component: EmptyStateIndicatorComponent?
|
||||
private weak var componentState: EmptyComponentState?
|
||||
|
||||
private let animation = ComponentView<Empty>()
|
||||
private let title = ComponentView<Empty>()
|
||||
private let text = ComponentView<Empty>()
|
||||
private let button = ComponentView<Empty>()
|
||||
|
||||
override public init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
}
|
||||
|
||||
required public init(coder: NSCoder) {
|
||||
preconditionFailure()
|
||||
}
|
||||
|
||||
public func update(component: EmptyStateIndicatorComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
|
||||
self.component = component
|
||||
self.componentState = state
|
||||
|
||||
let animationSize = self.animation.update(
|
||||
transition: transition,
|
||||
component: AnyComponent(AnimatedStickerComponent(
|
||||
account: component.context.account,
|
||||
animation: AnimatedStickerComponent.Animation(source: .bundle(name: component.animationName), loop: true),
|
||||
size: CGSize(width: 120.0, height: 120.0)
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: 120.0, height: 120.0)
|
||||
)
|
||||
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)),
|
||||
horizontalAlignment: .center,
|
||||
maximumNumberOfLines: 0
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: min(300.0, availableSize.width - 16.0 * 2.0), height: 1000.0)
|
||||
)
|
||||
let textSize = self.text.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(MultilineTextComponent(
|
||||
text: .plain(NSAttributedString(string: component.text, font: Font.regular(15.0), textColor: component.theme.list.itemSecondaryTextColor)),
|
||||
horizontalAlignment: .center,
|
||||
maximumNumberOfLines: 0
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: min(300.0, availableSize.width - 16.0 * 2.0), height: 1000.0)
|
||||
)
|
||||
let buttonSize = self.button.update(
|
||||
transition: transition,
|
||||
component: AnyComponent(ButtonComponent(
|
||||
background: ButtonComponent.Background(
|
||||
color: component.theme.list.itemCheckColors.fillColor,
|
||||
foreground: component.theme.list.itemCheckColors.foregroundColor,
|
||||
pressedColor: component.theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.9)
|
||||
),
|
||||
content: AnyComponentWithIdentity(id: 0, component: AnyComponent(
|
||||
Text(text: component.actionTitle, font: Font.semibold(17.0), color: component.theme.list.itemCheckColors.foregroundColor)
|
||||
)),
|
||||
isEnabled: true,
|
||||
displaysProgress: false,
|
||||
action: { [weak self] in
|
||||
guard let self, let component = self.component else {
|
||||
return
|
||||
}
|
||||
component.action()
|
||||
}
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: 240.0, height: 50.0)
|
||||
)
|
||||
|
||||
let animationSpacing: CGFloat = 11.0
|
||||
let titleSpacing: CGFloat = 17.0
|
||||
let buttonSpacing: CGFloat = 17.0
|
||||
|
||||
let totalHeight: CGFloat = animationSize.height + animationSpacing + titleSize.height + titleSpacing + textSize.height + buttonSpacing + buttonSize.height
|
||||
|
||||
var contentY = floor((availableSize.height - totalHeight) * 0.5)
|
||||
|
||||
if let animationView = self.animation.view {
|
||||
if animationView.superview == nil {
|
||||
self.addSubview(animationView)
|
||||
}
|
||||
transition.setFrame(view: animationView, frame: CGRect(origin: CGPoint(x: floor((availableSize.width - animationSize.width) * 0.5), y: contentY), size: animationSize))
|
||||
contentY += animationSize.height + animationSpacing
|
||||
}
|
||||
if let titleView = self.title.view {
|
||||
if titleView.superview == nil {
|
||||
self.addSubview(titleView)
|
||||
}
|
||||
transition.setFrame(view: titleView, frame: CGRect(origin: CGPoint(x: floor((availableSize.width - titleSize.width) * 0.5), y: contentY), size: titleSize))
|
||||
contentY += titleSize.height + titleSpacing
|
||||
}
|
||||
if let textView = self.text.view {
|
||||
if textView.superview == nil {
|
||||
self.addSubview(textView)
|
||||
}
|
||||
transition.setFrame(view: textView, frame: CGRect(origin: CGPoint(x: floor((availableSize.width - textSize.width) * 0.5), y: contentY), size: textSize))
|
||||
contentY += textSize.height + buttonSpacing
|
||||
}
|
||||
if let buttonView = self.button.view {
|
||||
if buttonView.superview == nil {
|
||||
self.addSubview(buttonView)
|
||||
}
|
||||
transition.setFrame(view: buttonView, frame: CGRect(origin: CGPoint(x: floor((availableSize.width - buttonSize.width) * 0.5), y: contentY), size: buttonSize))
|
||||
contentY += buttonSize.height
|
||||
}
|
||||
|
||||
return availableSize
|
||||
}
|
||||
}
|
||||
|
||||
public func makeView() -> View {
|
||||
return View(frame: CGRect())
|
||||
}
|
||||
|
||||
public 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)
|
||||
}
|
||||
}
|
@ -131,31 +131,33 @@ final class PeerInfoStoryGridScreenComponent: Component {
|
||||
|
||||
paneNode.clearSelection()
|
||||
})))
|
||||
} else {
|
||||
var recurseGenerateAction: ((Bool) -> ContextMenuActionItem)?
|
||||
let generateAction: (Bool) -> ContextMenuActionItem = { [weak pane] isZoomIn in
|
||||
let nextZoomLevel = isZoomIn ? pane?.availableZoomLevels().increment : pane?.availableZoomLevels().decrement
|
||||
let canZoom: Bool = nextZoomLevel != nil
|
||||
} else if let paneNode = self.paneNode {
|
||||
if !paneNode.isEmpty {
|
||||
var recurseGenerateAction: ((Bool) -> ContextMenuActionItem)?
|
||||
let generateAction: (Bool) -> ContextMenuActionItem = { [weak pane] isZoomIn in
|
||||
let nextZoomLevel = isZoomIn ? pane?.availableZoomLevels().increment : pane?.availableZoomLevels().decrement
|
||||
let canZoom: Bool = nextZoomLevel != nil
|
||||
|
||||
return ContextMenuActionItem(id: isZoomIn ? 0 : 1, text: isZoomIn ? strings.SharedMedia_ZoomIn : strings.SharedMedia_ZoomOut, textColor: canZoom ? .primary : .disabled, icon: { theme in
|
||||
return generateTintedImage(image: UIImage(bundleImageName: isZoomIn ? "Chat/Context Menu/ZoomIn" : "Chat/Context Menu/ZoomOut"), color: canZoom ? theme.contextMenu.primaryColor : theme.contextMenu.primaryColor.withMultipliedAlpha(0.4))
|
||||
}, action: canZoom ? { action in
|
||||
guard let pane = pane, let zoomLevel = isZoomIn ? pane.availableZoomLevels().increment : pane.availableZoomLevels().decrement else {
|
||||
return
|
||||
}
|
||||
pane.updateZoomLevel(level: zoomLevel)
|
||||
if let recurseGenerateAction = recurseGenerateAction {
|
||||
action.updateAction(0, recurseGenerateAction(true))
|
||||
action.updateAction(1, recurseGenerateAction(false))
|
||||
}
|
||||
} : nil)
|
||||
}
|
||||
recurseGenerateAction = { isZoomIn in
|
||||
return generateAction(isZoomIn)
|
||||
}
|
||||
|
||||
return ContextMenuActionItem(id: isZoomIn ? 0 : 1, text: isZoomIn ? strings.SharedMedia_ZoomIn : strings.SharedMedia_ZoomOut, textColor: canZoom ? .primary : .disabled, icon: { theme in
|
||||
return generateTintedImage(image: UIImage(bundleImageName: isZoomIn ? "Chat/Context Menu/ZoomIn" : "Chat/Context Menu/ZoomOut"), color: canZoom ? theme.contextMenu.primaryColor : theme.contextMenu.primaryColor.withMultipliedAlpha(0.4))
|
||||
}, action: canZoom ? { action in
|
||||
guard let pane = pane, let zoomLevel = isZoomIn ? pane.availableZoomLevels().increment : pane.availableZoomLevels().decrement else {
|
||||
return
|
||||
}
|
||||
pane.updateZoomLevel(level: zoomLevel)
|
||||
if let recurseGenerateAction = recurseGenerateAction {
|
||||
action.updateAction(0, recurseGenerateAction(true))
|
||||
action.updateAction(1, recurseGenerateAction(false))
|
||||
}
|
||||
} : nil)
|
||||
items.append(.action(generateAction(true)))
|
||||
items.append(.action(generateAction(false)))
|
||||
}
|
||||
recurseGenerateAction = { isZoomIn in
|
||||
return generateAction(isZoomIn)
|
||||
}
|
||||
|
||||
items.append(.action(generateAction(true)))
|
||||
items.append(.action(generateAction(false)))
|
||||
|
||||
if component.peerId == component.context.account.peerId, case .saved = component.scope {
|
||||
var ignoreNextActions = false
|
||||
@ -392,6 +394,13 @@ final class PeerInfoStoryGridScreenComponent: Component {
|
||||
self.paneNode = paneNode
|
||||
self.addSubview(paneNode.view)
|
||||
|
||||
paneNode.emptyAction = { [weak self] in
|
||||
guard let self, let component = self.component else {
|
||||
return
|
||||
}
|
||||
self.environment?.controller()?.push(PeerInfoStoryGridScreen(context: component.context, peerId: component.peerId, scope: .archive))
|
||||
}
|
||||
|
||||
self.paneStatusDisposable = (paneNode.status
|
||||
|> deliverOnMainQueue).start(next: { [weak self] status in
|
||||
guard let self else {
|
||||
|
@ -36,6 +36,7 @@ swift_library(
|
||||
"//submodules/InvisibleInkDustNode",
|
||||
"//submodules/MediaPickerUI",
|
||||
"//submodules/TelegramUI/Components/Stories/StoryContainerScreen",
|
||||
"//submodules/TelegramUI/Components/EmptyStateIndicatorComponent",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
@ -26,6 +26,7 @@ import AppBundle
|
||||
import InvisibleInkDustNode
|
||||
import MediaPickerUI
|
||||
import StoryContainerScreen
|
||||
import EmptyStateIndicatorComponent
|
||||
|
||||
private let mediaBadgeBackgroundColor = UIColor(white: 0.0, alpha: 0.6)
|
||||
private let mediaBadgeTextColor = UIColor.white
|
||||
@ -849,6 +850,14 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr
|
||||
return result
|
||||
}
|
||||
|
||||
public var isEmpty: Bool {
|
||||
if let items = self.items, items.items.count != 0 {
|
||||
return false
|
||||
} else {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
private var currentParams: (size: CGSize, topInset: CGFloat, sideInset: CGFloat, bottomInset: CGFloat, visibleHeight: CGFloat, isScrollingLockedAtTop: Bool, expandProgress: CGFloat, presentationData: PresentationData)?
|
||||
|
||||
private let ready = Promise<Bool>()
|
||||
@ -883,6 +892,7 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr
|
||||
|
||||
public var openCurrentDate: (() -> Void)?
|
||||
public var paneDidScroll: (() -> Void)?
|
||||
public var emptyAction: (() -> Void)?
|
||||
|
||||
private weak var currentGestureItem: SparseItemGridDisplayItem?
|
||||
|
||||
@ -892,6 +902,8 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr
|
||||
private weak var pendingOpenListContext: PeerStoryListContentContextImpl?
|
||||
|
||||
private var preloadArchiveListContext: PeerStoryListContext?
|
||||
|
||||
private var emptyStateView: ComponentView<Empty>?
|
||||
|
||||
public init(context: AccountContext, peerId: PeerId, chatLocation: ChatLocation, contentType: ContentType, captureProtected: Bool, isSaved: Bool, isArchive: Bool, navigationController: @escaping () -> NavigationController?, listContext: PeerStoryListContext?) {
|
||||
self.context = context
|
||||
@ -1863,6 +1875,67 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr
|
||||
self.currentParams = (size, topInset, sideInset, bottomInset, visibleHeight, isScrollingLockedAtTop, expandProgress, presentationData)
|
||||
|
||||
transition.updateFrame(node: self.contextGestureContainerNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: size.height)))
|
||||
|
||||
if let items = self.items, items.items.isEmpty, items.count == 0, !self.isArchive {
|
||||
let emptyStateView: ComponentView<Empty>
|
||||
var emptyStateTransition = Transition(transition)
|
||||
if let current = self.emptyStateView {
|
||||
emptyStateView = current
|
||||
} else {
|
||||
emptyStateTransition = .immediate
|
||||
emptyStateView = ComponentView()
|
||||
self.emptyStateView = emptyStateView
|
||||
}
|
||||
//TODO:localize
|
||||
let emptyStateSize = emptyStateView.update(
|
||||
transition: emptyStateTransition,
|
||||
component: AnyComponent(EmptyStateIndicatorComponent(
|
||||
context: self.context,
|
||||
theme: presentationData.theme,
|
||||
animationName: "StoryListEmpty",
|
||||
title: "No saved stories",
|
||||
text: "Open the Archive to select stories you\nwant to be displayed in your profile.",
|
||||
actionTitle: "Open Archive",
|
||||
action: { [weak self] in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.emptyAction?()
|
||||
}
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: size.width, height: size.height - topInset - bottomInset)
|
||||
)
|
||||
if let emptyStateComponentView = emptyStateView.view {
|
||||
if emptyStateComponentView.superview == nil {
|
||||
self.view.addSubview(emptyStateComponentView)
|
||||
if self.didUpdateItemsOnce {
|
||||
emptyStateComponentView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||
}
|
||||
}
|
||||
emptyStateTransition.setFrame(view: emptyStateComponentView, frame: CGRect(origin: CGPoint(x: floor((size.width - emptyStateSize.width) * 0.5), y: topInset), size: emptyStateSize))
|
||||
}
|
||||
if self.didUpdateItemsOnce {
|
||||
Transition(animation: .curve(duration: 0.2, curve: .easeInOut)).setBackgroundColor(view: self.view, color: presentationData.theme.list.blocksBackgroundColor)
|
||||
} else {
|
||||
self.view.backgroundColor = presentationData.theme.list.blocksBackgroundColor
|
||||
}
|
||||
} else {
|
||||
if let emptyStateView = self.emptyStateView {
|
||||
let subTransition = Transition(animation: .curve(duration: 0.2, curve: .easeInOut))
|
||||
self.emptyStateView = nil
|
||||
|
||||
if let emptyStateComponentView = emptyStateView.view {
|
||||
subTransition.setAlpha(view: emptyStateComponentView, alpha: 0.0, completion: { [weak emptyStateComponentView] _ in
|
||||
emptyStateComponentView?.removeFromSuperview()
|
||||
})
|
||||
}
|
||||
|
||||
subTransition.setBackgroundColor(view: self.view, color: presentationData.theme.list.blocksBackgroundColor)
|
||||
} else {
|
||||
self.view.backgroundColor = .clear
|
||||
}
|
||||
}
|
||||
|
||||
transition.updateFrame(node: self.itemGrid, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: size.height)))
|
||||
if let items = self.items {
|
||||
|
@ -17,7 +17,7 @@ public final class AvatarStoryIndicatorComponent: Component {
|
||||
|
||||
public let hasUnseen: Bool
|
||||
public let hasUnseenCloseFriendsItems: Bool
|
||||
public let isDarkTheme: Bool
|
||||
public let theme: PresentationTheme
|
||||
public let activeLineWidth: CGFloat
|
||||
public let inactiveLineWidth: CGFloat
|
||||
public let counters: Counters?
|
||||
@ -25,14 +25,14 @@ public final class AvatarStoryIndicatorComponent: Component {
|
||||
public init(
|
||||
hasUnseen: Bool,
|
||||
hasUnseenCloseFriendsItems: Bool,
|
||||
isDarkTheme: Bool,
|
||||
theme: PresentationTheme,
|
||||
activeLineWidth: CGFloat,
|
||||
inactiveLineWidth: CGFloat,
|
||||
counters: Counters?
|
||||
) {
|
||||
self.hasUnseen = hasUnseen
|
||||
self.hasUnseenCloseFriendsItems = hasUnseenCloseFriendsItems
|
||||
self.isDarkTheme = isDarkTheme
|
||||
self.theme = theme
|
||||
self.activeLineWidth = activeLineWidth
|
||||
self.inactiveLineWidth = inactiveLineWidth
|
||||
self.counters = counters
|
||||
@ -45,7 +45,7 @@ public final class AvatarStoryIndicatorComponent: Component {
|
||||
if lhs.hasUnseenCloseFriendsItems != rhs.hasUnseenCloseFriendsItems {
|
||||
return false
|
||||
}
|
||||
if lhs.isDarkTheme != rhs.isDarkTheme {
|
||||
if lhs.theme !== rhs.theme {
|
||||
return false
|
||||
}
|
||||
if lhs.activeLineWidth != rhs.activeLineWidth {
|
||||
@ -112,8 +112,8 @@ public final class AvatarStoryIndicatorComponent: Component {
|
||||
]
|
||||
}
|
||||
|
||||
if component.isDarkTheme {
|
||||
inactiveColors = [UIColor(rgb: 0x48484A).cgColor, UIColor(rgb: 0x48484A).cgColor]
|
||||
if component.theme.overallDarkAppearance {
|
||||
inactiveColors = [component.theme.rootController.tabBar.textColor.cgColor, component.theme.rootController.tabBar.textColor.cgColor]
|
||||
} else {
|
||||
inactiveColors = [UIColor(rgb: 0xD8D8E1).cgColor, UIColor(rgb: 0xD8D8E1).cgColor]
|
||||
}
|
||||
|
@ -138,7 +138,8 @@ public final class StoryContentContextImpl: StoryContentContext {
|
||||
isExpired: item.isExpired,
|
||||
isPublic: item.isPublic,
|
||||
isPending: false,
|
||||
isCloseFriends: item.isCloseFriends
|
||||
isCloseFriends: item.isCloseFriends,
|
||||
isForwardingDisabled: item.isForwardingDisabled
|
||||
)
|
||||
}
|
||||
if peerId == context.account.peerId, let stateView = views.views[PostboxViewKey.storiesState(key: .local)] as? StoryStatesView, let localState = stateView.value?.get(Stories.LocalState.self) {
|
||||
@ -156,7 +157,8 @@ public final class StoryContentContextImpl: StoryContentContext {
|
||||
isExpired: false,
|
||||
isPublic: false,
|
||||
isPending: true,
|
||||
isCloseFriends: false
|
||||
isCloseFriends: false,
|
||||
isForwardingDisabled: false
|
||||
))
|
||||
}
|
||||
}
|
||||
@ -954,7 +956,8 @@ public final class SingleStoryContentContextImpl: StoryContentContext {
|
||||
isExpired: itemValue.isExpired,
|
||||
isPublic: itemValue.isPublic,
|
||||
isPending: false,
|
||||
isCloseFriends: itemValue.isCloseFriends
|
||||
isCloseFriends: itemValue.isCloseFriends,
|
||||
isForwardingDisabled: itemValue.isForwardingDisabled
|
||||
)
|
||||
|
||||
let mainItem = StoryContentItem(
|
||||
|
@ -392,30 +392,46 @@ private final class StoryContainerScreenComponent: Component {
|
||||
let translation = recognizer.translation(in: self)
|
||||
self.verticalPanState = ItemSetPanState(fraction: max(-1.0, min(1.0, translation.y / self.bounds.height)), didBegin: true)
|
||||
self.state?.updated(transition: .immediate)
|
||||
case .cancelled, .ended:
|
||||
let translation = recognizer.translation(in: self)
|
||||
let velocity = recognizer.velocity(in: self)
|
||||
|
||||
self.verticalPanState = nil
|
||||
var updateState = true
|
||||
|
||||
if translation.y > 100.0 || velocity.y > 10.0 {
|
||||
self.state?.updated(transition: Transition(animation: .curve(duration: 0.3, curve: .spring)))
|
||||
self.environment?.controller()?.dismiss()
|
||||
} else if translation.y < -100.0 || velocity.y < -40.0 {
|
||||
if translation.y < -40.0 {
|
||||
if let component = self.component, let stateValue = component.content.stateValue, let slice = stateValue.slice, let itemSetView = self.visibleItemSetViews[slice.peer.id] {
|
||||
if let itemSetComponentView = itemSetView.view.view as? StoryItemSetContainerComponent.View {
|
||||
if itemSetComponentView.activateInput() {
|
||||
updateState = false
|
||||
if let activateInputWhileDragging = itemSetComponentView.activateInputWhileDragging() {
|
||||
activateInputWhileDragging()
|
||||
|
||||
self.verticalPanState = nil
|
||||
recognizer.state = .cancelled
|
||||
self.state?.updated(transition: Transition(animation: .curve(duration: 0.3, curve: .spring)))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
case .cancelled, .ended:
|
||||
if self.verticalPanState != nil {
|
||||
let translation = recognizer.translation(in: self)
|
||||
let velocity = recognizer.velocity(in: self)
|
||||
|
||||
if updateState || "".isEmpty {
|
||||
self.verticalPanState = nil
|
||||
var updateState = true
|
||||
|
||||
if translation.y > 100.0 || velocity.y > 10.0 {
|
||||
self.state?.updated(transition: Transition(animation: .curve(duration: 0.3, curve: .spring)))
|
||||
self.environment?.controller()?.dismiss()
|
||||
} else if translation.y < -100.0 || velocity.y < -40.0 {
|
||||
if let component = self.component, let stateValue = component.content.stateValue, let slice = stateValue.slice, let itemSetView = self.visibleItemSetViews[slice.peer.id] {
|
||||
if let itemSetComponentView = itemSetView.view.view as? StoryItemSetContainerComponent.View {
|
||||
if itemSetComponentView.activateInput() {
|
||||
updateState = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if updateState || "".isEmpty {
|
||||
self.state?.updated(transition: Transition(animation: .curve(duration: 0.3, curve: .spring)))
|
||||
}
|
||||
} else {
|
||||
self.state?.updated(transition: Transition(animation: .curve(duration: 0.3, curve: .spring)))
|
||||
}
|
||||
} else {
|
||||
self.state?.updated(transition: Transition(animation: .curve(duration: 0.3, curve: .spring)))
|
||||
}
|
||||
default:
|
||||
break
|
||||
|
@ -141,7 +141,7 @@ final class StoryItemContentComponent: Component {
|
||||
useLargeThumbnail: true,
|
||||
autoFetchFullSizeThumbnail: true,
|
||||
tempFilePath: nil,
|
||||
captureProtected: false,
|
||||
captureProtected: component.item.isForwardingDisabled,
|
||||
hintDimensions: file.dimensions?.cgSize,
|
||||
storeAfterDownload: nil,
|
||||
displayImage: false
|
||||
@ -487,9 +487,17 @@ final class StoryItemContentComponent: Component {
|
||||
}
|
||||
|
||||
if let dimensions {
|
||||
var imageSize = dimensions.aspectFilled(availableSize)
|
||||
if imageSize.width < availableSize.width && imageSize.width >= availableSize.width - 5.0 {
|
||||
imageSize.width = availableSize.width
|
||||
}
|
||||
if imageSize.height < availableSize.height && imageSize.height >= availableSize.height - 5.0 {
|
||||
imageSize.height = availableSize.height
|
||||
}
|
||||
self.imageNode.captureProtected = component.item.isForwardingDisabled
|
||||
let apply = self.imageNode.asyncLayout()(TransformImageArguments(
|
||||
corners: ImageCorners(),
|
||||
imageSize: dimensions.aspectFilled(availableSize),
|
||||
imageSize: imageSize,
|
||||
boundingSize: availableSize,
|
||||
intrinsicInsets: UIEdgeInsets()
|
||||
))
|
||||
|
@ -30,6 +30,7 @@ import LocalMediaResources
|
||||
import SaveToCameraRoll
|
||||
import BundleIconComponent
|
||||
import PeerListItemComponent
|
||||
import PremiumUI
|
||||
|
||||
public final class StoryItemSetContainerComponent: Component {
|
||||
public final class ExternalState {
|
||||
@ -182,16 +183,16 @@ public final class StoryItemSetContainerComponent: Component {
|
||||
}
|
||||
|
||||
struct ItemLayout {
|
||||
var size: CGSize
|
||||
var containerSize: CGSize
|
||||
var contentFrame: CGRect
|
||||
var contentVisualScale: CGFloat
|
||||
|
||||
init(
|
||||
size: CGSize,
|
||||
containerSize: CGSize,
|
||||
contentFrame: CGRect,
|
||||
contentVisualScale: CGFloat
|
||||
) {
|
||||
self.size = size
|
||||
self.containerSize = containerSize
|
||||
self.contentFrame = contentFrame
|
||||
self.contentVisualScale = contentVisualScale
|
||||
}
|
||||
@ -256,10 +257,6 @@ public final class StoryItemSetContainerComponent: Component {
|
||||
override func touchesShouldCancel(in view: UIView) -> Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
/*@objc func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
|
||||
return false
|
||||
}*/
|
||||
}
|
||||
|
||||
public final class View: UIView, UIScrollViewDelegate, UIGestureRecognizerDelegate {
|
||||
@ -567,7 +564,7 @@ public final class StoryItemSetContainerComponent: Component {
|
||||
let point = recognizer.location(in: self)
|
||||
|
||||
var direction: NavigationDirection?
|
||||
if point.x < itemLayout.size.width * 0.25 {
|
||||
if point.x < itemLayout.containerSize.width * 0.25 {
|
||||
direction = .previous
|
||||
} else {
|
||||
direction = .next
|
||||
@ -670,7 +667,7 @@ public final class StoryItemSetContainerComponent: Component {
|
||||
}
|
||||
|
||||
self.scrollingCenterX = leftWidth
|
||||
self.scroller.contentSize = CGSize(width: leftWidth + itemLayout.size.width + rightWidth, height: 1.0)
|
||||
self.scroller.contentSize = CGSize(width: leftWidth + itemLayout.containerSize.width + rightWidth, height: 1.0)
|
||||
|
||||
if !self.initializedOffset {
|
||||
self.initializedOffset = true
|
||||
@ -862,7 +859,7 @@ public final class StoryItemSetContainerComponent: Component {
|
||||
environment: {
|
||||
itemEnvironment
|
||||
},
|
||||
containerSize: itemLayout.size
|
||||
containerSize: itemLayout.contentFrame.size
|
||||
)
|
||||
if let view = visibleItem.view.view {
|
||||
if visibleItem.contentContainerView.superview == nil {
|
||||
@ -969,6 +966,22 @@ public final class StoryItemSetContainerComponent: Component {
|
||||
return false
|
||||
}
|
||||
|
||||
func activateInputWhileDragging() -> (() -> Void)? {
|
||||
guard let component = self.component else {
|
||||
return nil
|
||||
}
|
||||
if component.slice.peer.id == component.context.account.peerId {
|
||||
} else {
|
||||
if let inputPanelView = self.inputPanel.view as? MessageInputPanelComponent.View {
|
||||
return { [weak inputPanelView] in
|
||||
inputPanelView?.activateInput()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func animateIn(transitionIn: StoryContainerScreen.TransitionIn) {
|
||||
self.closeButton.layer.animateScale(from: 0.001, to: 1.0, duration: 0.2, delay: 0.12, timingFunction: kCAMediaTimingFunctionSpring)
|
||||
|
||||
@ -1510,7 +1523,7 @@ public final class StoryItemSetContainerComponent: Component {
|
||||
self.state?.updated(transition: .immediate)
|
||||
},
|
||||
timeoutAction: nil,
|
||||
forwardAction: component.slice.item.storyItem.isPublic ? { [weak self] in
|
||||
forwardAction: component.slice.item.storyItem.isPublic && !component.slice.item.storyItem.isForwardingDisabled ? { [weak self] in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
@ -2048,7 +2061,7 @@ public final class StoryItemSetContainerComponent: Component {
|
||||
let contentFrame = CGRect(origin: CGPoint(x: 0.0, y: component.containerInsets.top - (contentSize.height - contentVisualHeight) * 0.5), size: contentSize)
|
||||
|
||||
let itemLayout = ItemLayout(
|
||||
size: contentFrame.size,
|
||||
containerSize: availableSize,
|
||||
contentFrame: contentFrame,
|
||||
contentVisualScale: contentVisualScale
|
||||
)
|
||||
@ -2539,6 +2552,32 @@ public final class StoryItemSetContainerComponent: Component {
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
reactionContextNode.premiumReactionsSelected = { [weak self] file in
|
||||
guard let self, let file, let component = self.component else {
|
||||
return
|
||||
}
|
||||
|
||||
let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }
|
||||
let undoController = UndoOverlayController(presentationData: presentationData, content: .sticker(context: component.context, file: file, loop: true, title: nil, text: presentationData.strings.Chat_PremiumReactionToastTitle, undoText: presentationData.strings.Chat_PremiumReactionToastAction, customAction: { [weak self] in
|
||||
guard let self, let component = self.component else {
|
||||
return
|
||||
}
|
||||
|
||||
let context = component.context
|
||||
var replaceImpl: ((ViewController) -> Void)?
|
||||
let controller = PremiumDemoScreen(context: context, subject: .uniqueReactions, action: {
|
||||
let controller = PremiumIntroScreen(context: context, source: .reactions)
|
||||
replaceImpl?(controller)
|
||||
})
|
||||
replaceImpl = { [weak controller] c in
|
||||
controller?.replace(with: c)
|
||||
}
|
||||
component.controller()?.push(controller)
|
||||
}), elevatedLayout: false, animateInAsReplacement: false, action: { _ in true })
|
||||
//strongSelf.currentUndoController = undoController
|
||||
component.controller()?.present(undoController, in: .current)
|
||||
}
|
||||
}
|
||||
|
||||
var animateReactionsIn = false
|
||||
|
@ -22,6 +22,8 @@ swift_library(
|
||||
"//submodules/ContextUI",
|
||||
"//submodules/TelegramUI/Components/Stories/StoryContainerScreen",
|
||||
"//submodules/Components/MultilineTextComponent",
|
||||
"//submodules/ActivityIndicator",
|
||||
"//submodules/TelegramUI/Components/EmojiStatusComponent",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
@ -10,6 +10,7 @@ import Postbox
|
||||
import SwiftSignalKit
|
||||
import TelegramPresentationData
|
||||
import StoryContainerScreen
|
||||
import EmojiStatusComponent
|
||||
|
||||
public func shouldDisplayStoriesInChatListHeader(storySubscriptions: EngineStorySubscriptions) -> Bool {
|
||||
if !storySubscriptions.items.isEmpty {
|
||||
@ -48,6 +49,11 @@ private let modelSpringAnimation: CABasicAnimation = {
|
||||
}()
|
||||
|
||||
public final class StoryPeerListComponent: Component {
|
||||
public enum PeerStatus: Equatable {
|
||||
case premium
|
||||
case emoji(PeerEmojiStatus)
|
||||
}
|
||||
|
||||
public final class ExternalState {
|
||||
public fileprivate(set) var collapsedWidth: CGFloat = 0.0
|
||||
|
||||
@ -74,7 +80,11 @@ public final class StoryPeerListComponent: Component {
|
||||
public let theme: PresentationTheme
|
||||
public let strings: PresentationStrings
|
||||
public let sideInset: CGFloat
|
||||
public let titleContentWidth: CGFloat
|
||||
public let title: String
|
||||
public let titleHasLock: Bool
|
||||
public let titleHasActivity: Bool
|
||||
public let titlePeerStatus: PeerStatus?
|
||||
public let minTitleX: CGFloat
|
||||
public let maxTitleX: CGFloat
|
||||
public let useHiddenList: Bool
|
||||
public let storySubscriptions: EngineStorySubscriptions?
|
||||
@ -83,7 +93,7 @@ public final class StoryPeerListComponent: Component {
|
||||
public let uploadProgress: Float?
|
||||
public let peerAction: (EnginePeer?) -> Void
|
||||
public let contextPeerAction: (ContextExtractedContentContainingNode, ContextGesture, EnginePeer) -> Void
|
||||
public let updateTitleContentOffset: (CGFloat, Transition) -> Void
|
||||
public let openStatusSetup: (UIView) -> Void
|
||||
|
||||
public init(
|
||||
externalState: ExternalState,
|
||||
@ -91,7 +101,11 @@ public final class StoryPeerListComponent: Component {
|
||||
theme: PresentationTheme,
|
||||
strings: PresentationStrings,
|
||||
sideInset: CGFloat,
|
||||
titleContentWidth: CGFloat,
|
||||
title: String,
|
||||
titleHasLock: Bool,
|
||||
titleHasActivity: Bool,
|
||||
titlePeerStatus: PeerStatus?,
|
||||
minTitleX: CGFloat,
|
||||
maxTitleX: CGFloat,
|
||||
useHiddenList: Bool,
|
||||
storySubscriptions: EngineStorySubscriptions?,
|
||||
@ -100,14 +114,18 @@ public final class StoryPeerListComponent: Component {
|
||||
uploadProgress: Float?,
|
||||
peerAction: @escaping (EnginePeer?) -> Void,
|
||||
contextPeerAction: @escaping (ContextExtractedContentContainingNode, ContextGesture, EnginePeer) -> Void,
|
||||
updateTitleContentOffset: @escaping (CGFloat, Transition) -> Void
|
||||
openStatusSetup: @escaping (UIView) -> Void
|
||||
) {
|
||||
self.externalState = externalState
|
||||
self.context = context
|
||||
self.theme = theme
|
||||
self.strings = strings
|
||||
self.sideInset = sideInset
|
||||
self.titleContentWidth = titleContentWidth
|
||||
self.title = title
|
||||
self.titleHasLock = titleHasLock
|
||||
self.titleHasActivity = titleHasActivity
|
||||
self.titlePeerStatus = titlePeerStatus
|
||||
self.minTitleX = minTitleX
|
||||
self.maxTitleX = maxTitleX
|
||||
self.useHiddenList = useHiddenList
|
||||
self.storySubscriptions = storySubscriptions
|
||||
@ -116,7 +134,7 @@ public final class StoryPeerListComponent: Component {
|
||||
self.uploadProgress = uploadProgress
|
||||
self.peerAction = peerAction
|
||||
self.contextPeerAction = contextPeerAction
|
||||
self.updateTitleContentOffset = updateTitleContentOffset
|
||||
self.openStatusSetup = openStatusSetup
|
||||
}
|
||||
|
||||
public static func ==(lhs: StoryPeerListComponent, rhs: StoryPeerListComponent) -> Bool {
|
||||
@ -132,7 +150,19 @@ public final class StoryPeerListComponent: Component {
|
||||
if lhs.sideInset != rhs.sideInset {
|
||||
return false
|
||||
}
|
||||
if lhs.titleContentWidth != rhs.titleContentWidth {
|
||||
if lhs.title != rhs.title {
|
||||
return false
|
||||
}
|
||||
if lhs.titleHasLock != rhs.titleHasLock {
|
||||
return false
|
||||
}
|
||||
if lhs.titleHasActivity != rhs.titleHasActivity {
|
||||
return false
|
||||
}
|
||||
if lhs.titlePeerStatus != rhs.titlePeerStatus {
|
||||
return false
|
||||
}
|
||||
if lhs.minTitleX != rhs.minTitleX {
|
||||
return false
|
||||
}
|
||||
if lhs.maxTitleX != rhs.maxTitleX {
|
||||
@ -195,7 +225,18 @@ public final class StoryPeerListComponent: Component {
|
||||
}
|
||||
|
||||
func frame(at index: Int) -> CGRect {
|
||||
return CGRect(origin: CGPoint(x: self.containerInsets.left + (self.itemSize.width + self.itemSpacing) * CGFloat(index), y: self.containerInsets.top), size: self.itemSize)
|
||||
if self.itemCount <= 1 {
|
||||
return CGRect(origin: CGPoint(x: floor((self.containerSize.width - self.itemSize.width) * 0.5), y: self.containerInsets.top), size: self.itemSize)
|
||||
} else if self.contentSize.width < self.containerSize.width {
|
||||
let usableWidth = self.containerSize.width - self.containerInsets.left - self.containerInsets.right
|
||||
let usableSpacingWidth = usableWidth - self.itemSize.width * CGFloat(self.itemCount)
|
||||
|
||||
var spacing = floor(usableSpacingWidth / CGFloat(self.itemCount + 1))
|
||||
spacing = min(120.0, spacing)
|
||||
return CGRect(origin: CGPoint(x: self.containerInsets.left + spacing + (self.itemSize.width + spacing) * CGFloat(index), y: self.containerInsets.top), size: self.itemSize)
|
||||
} else {
|
||||
return CGRect(origin: CGPoint(x: self.containerInsets.left + (self.itemSize.width + self.itemSpacing) * CGFloat(index), y: self.containerInsets.top), size: self.itemSize)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -240,12 +281,19 @@ public final class StoryPeerListComponent: Component {
|
||||
public final class View: UIView, UIScrollViewDelegate {
|
||||
private let collapsedButton: HighlightableButton
|
||||
private let scrollView: ScrollView
|
||||
private let scrollContainerView: UIView
|
||||
|
||||
private var ignoreScrolling: Bool = false
|
||||
private var itemLayout: ItemLayout?
|
||||
|
||||
private var sortedItems: [EngineStorySubscriptions.Item] = []
|
||||
|
||||
private var visibleItems: [EnginePeer.Id: VisibleItem] = [:]
|
||||
private var visibleCollapsableItems: [EnginePeer.Id: VisibleItem] = [:]
|
||||
|
||||
private var titleIndicatorView: ComponentView<Empty>?
|
||||
private let titleView = ComponentView<Empty>()
|
||||
private var titleIconView: ComponentView<Empty>?
|
||||
|
||||
private var component: StoryPeerListComponent?
|
||||
private weak var state: EmptyComponentState?
|
||||
@ -275,10 +323,15 @@ public final class StoryPeerListComponent: Component {
|
||||
self.scrollView.alwaysBounceHorizontal = true
|
||||
self.scrollView.clipsToBounds = false
|
||||
|
||||
self.scrollContainerView = UIView()
|
||||
|
||||
super.init(frame: frame)
|
||||
|
||||
self.scrollView.delegate = self
|
||||
self.scrollView.alpha = 0.0
|
||||
self.scrollContainerView.addGestureRecognizer(self.scrollView.panGestureRecognizer)
|
||||
self.addSubview(self.scrollView)
|
||||
self.addSubview(self.scrollContainerView)
|
||||
self.addSubview(self.collapsedButton)
|
||||
|
||||
self.collapsedButton.highligthedChanged = { [weak self] highlighted in
|
||||
@ -373,20 +426,116 @@ public final class StoryPeerListComponent: Component {
|
||||
|
||||
public func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
||||
if !self.ignoreScrolling {
|
||||
self.updateScrolling(transition: .immediate, keepVisibleUntilCompletion: false)
|
||||
self.updateScrolling(transition: .immediate)
|
||||
}
|
||||
}
|
||||
|
||||
private func updateScrolling(transition: Transition, keepVisibleUntilCompletion: Bool) {
|
||||
private func updateScrolling(transition: Transition) {
|
||||
guard let component = self.component, let itemLayout = self.itemLayout else {
|
||||
return
|
||||
}
|
||||
|
||||
var hasStories: Bool = false
|
||||
if let storySubscriptions = component.storySubscriptions, shouldDisplayStoriesInChatListHeader(storySubscriptions: storySubscriptions) {
|
||||
hasStories = true
|
||||
let titleIconSpacing: CGFloat = 4.0
|
||||
let titleIndicatorSpacing: CGFloat = 8.0
|
||||
|
||||
var titleContentWidth: CGFloat = 0.0
|
||||
|
||||
var titleIndicatorSize: CGSize?
|
||||
if component.titleHasActivity {
|
||||
let titleIndicatorView: ComponentView<Empty>
|
||||
if let current = self.titleIndicatorView {
|
||||
titleIndicatorView = current
|
||||
} else {
|
||||
titleIndicatorView = ComponentView()
|
||||
self.titleIndicatorView = titleIndicatorView
|
||||
}
|
||||
let titleIndicatorSizeValue = titleIndicatorView.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(TitleActivityIndicatorComponent(
|
||||
color: component.theme.rootController.navigationBar.accentTextColor
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: 22.0, height: 22.0)
|
||||
)
|
||||
titleIndicatorSize = titleIndicatorSizeValue
|
||||
titleContentWidth += titleIndicatorSizeValue.width + titleIndicatorSpacing
|
||||
} else {
|
||||
if let titleIndicatorView = self.titleIndicatorView {
|
||||
self.titleIndicatorView = nil
|
||||
titleIndicatorView.view?.removeFromSuperview()
|
||||
}
|
||||
}
|
||||
|
||||
let titleSize = self.titleView.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(Text(text: component.title, font: Font.semibold(17.0), color: component.theme.rootController.navigationBar.primaryTextColor)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: 200.0, height: 100.0)
|
||||
)
|
||||
titleContentWidth += titleSize.width
|
||||
|
||||
var titleIconSize: CGSize?
|
||||
if let peerStatus = component.titlePeerStatus {
|
||||
let statusContent: EmojiStatusComponent.Content
|
||||
switch peerStatus {
|
||||
case .premium:
|
||||
statusContent = .premium(color: component.theme.list.itemAccentColor)
|
||||
case let .emoji(emoji):
|
||||
statusContent = .animation(content: .customEmoji(fileId: emoji.fileId), size: CGSize(width: 22.0, height: 22.0), placeholderColor: component.theme.list.mediaPlaceholderColor, themeColor: component.theme.list.itemAccentColor, loopMode: .count(2))
|
||||
}
|
||||
|
||||
var animateStatusTransition = false
|
||||
|
||||
let titleIconView: ComponentView<Empty>
|
||||
if let current = self.titleIconView {
|
||||
animateStatusTransition = true
|
||||
titleIconView = current
|
||||
} else {
|
||||
titleIconView = ComponentView()
|
||||
self.titleIconView = titleIconView
|
||||
}
|
||||
|
||||
var titleIconTransition: Transition
|
||||
if animateStatusTransition {
|
||||
titleIconTransition = Transition(animation: .curve(duration: 0.2, curve: .easeInOut))
|
||||
} else {
|
||||
titleIconTransition = .immediate
|
||||
}
|
||||
|
||||
let titleIconSizeValue = titleIconView.update(
|
||||
transition: titleIconTransition,
|
||||
component: AnyComponent(EmojiStatusComponent(
|
||||
context: component.context,
|
||||
animationCache: component.context.animationCache,
|
||||
animationRenderer: component.context.animationRenderer,
|
||||
content: statusContent,
|
||||
isVisibleForAnimations: true,
|
||||
action: { [weak self] in
|
||||
guard let self, let component = self.component, let titleIconView = self.titleIconView?.view else {
|
||||
return
|
||||
}
|
||||
component.openStatusSetup(titleIconView)
|
||||
}
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: 22.0, height: 22.0)
|
||||
)
|
||||
|
||||
titleIconSize = titleIconSizeValue
|
||||
|
||||
if let titleIconComponentView = titleIconView.view {
|
||||
titleIconComponentView.isHidden = component.titleHasActivity
|
||||
}
|
||||
|
||||
if !component.titleHasActivity {
|
||||
titleContentWidth += titleIconSpacing + titleIconSizeValue.width
|
||||
}
|
||||
} else {
|
||||
if let titleIconView = self.titleIconView {
|
||||
self.titleIconView = nil
|
||||
titleIconView.view?.removeFromSuperview()
|
||||
}
|
||||
}
|
||||
let _ = hasStories
|
||||
|
||||
let collapseStartIndex: Int
|
||||
if component.useHiddenList {
|
||||
@ -415,13 +564,28 @@ public final class StoryPeerListComponent: Component {
|
||||
let collapsedItemOffsetY: CGFloat
|
||||
|
||||
let titleContentSpacing: CGFloat = 8.0
|
||||
var combinedTitleContentWidth = component.titleContentWidth
|
||||
var combinedTitleContentWidth = titleContentWidth
|
||||
if !combinedTitleContentWidth.isZero {
|
||||
combinedTitleContentWidth += titleContentSpacing
|
||||
}
|
||||
let centralContentWidth: CGFloat = collapsedContentWidth + combinedTitleContentWidth
|
||||
|
||||
let centralContentWidth: CGFloat
|
||||
centralContentWidth = collapsedContentWidth + combinedTitleContentWidth
|
||||
|
||||
collapsedContentOrigin = floor((itemLayout.containerSize.width - centralContentWidth) * 0.5)
|
||||
|
||||
if component.titleHasActivity {
|
||||
collapsedContentOrigin -= (collapsedContentWidth + titleContentSpacing) * 0.5
|
||||
}
|
||||
|
||||
collapsedContentOrigin = min(collapsedContentOrigin, component.maxTitleX - centralContentWidth - 4.0)
|
||||
|
||||
var collapsedContentOriginOffset: CGFloat = 0.0
|
||||
|
||||
if itemLayout.itemCount == 1 && collapsedContentWidth <= 0.1 {
|
||||
collapsedContentOriginOffset += 4.0
|
||||
}
|
||||
collapsedContentOrigin -= collapsedContentOriginOffset
|
||||
collapsedItemOffsetY = -59.0
|
||||
|
||||
struct CollapseState {
|
||||
@ -432,11 +596,6 @@ public final class StoryPeerListComponent: Component {
|
||||
var sideAlphaFraction: CGFloat
|
||||
}
|
||||
|
||||
/*let calculateCollapedFraction: (CGFloat) -> CGFloat = { t in
|
||||
let offset = scrollingRubberBandingOffset(offset: (1.0 - t) * 94.0, bandingStart: 0.0, range: 400.0, coefficient: 0.4)
|
||||
return 1.0 - max(0.0, min(1.0, offset / 94.0))
|
||||
}*/
|
||||
|
||||
let targetExpandedFraction = component.collapseFraction
|
||||
|
||||
let targetFraction: CGFloat = component.collapseFraction
|
||||
@ -504,7 +663,7 @@ public final class StoryPeerListComponent: Component {
|
||||
var rawProgress = CGFloat((timestamp - animationState.startTime) / animationState.duration)
|
||||
rawProgress = max(0.0, min(1.0, rawProgress))
|
||||
|
||||
if !animationState.fromIsUnlocked && animationState.bounce {
|
||||
if !animationState.fromIsUnlocked && animationState.bounce && itemLayout.itemCount > 3 {
|
||||
expandBoundsFraction = animationState.interpolatedFraction(at: timestamp, effectiveFromFraction: 1.0, toFraction: 0.0)
|
||||
} else {
|
||||
expandBoundsFraction = 0.0
|
||||
@ -520,15 +679,6 @@ public final class StoryPeerListComponent: Component {
|
||||
expandBoundsFraction = 0.0
|
||||
}
|
||||
|
||||
let defaultCollapsedTitleOffset = floor((itemLayout.containerSize.width - component.titleContentWidth) * 0.5)
|
||||
let targetCollapsedTitleOffset: CGFloat = collapsedContentOrigin + collapsedContentWidth + titleContentSpacing
|
||||
let collapsedTitleOffset = targetCollapsedTitleOffset - defaultCollapsedTitleOffset
|
||||
|
||||
let titleMinContentOffset: CGFloat = collapsedTitleOffset.interpolate(to: collapsedTitleOffset + 12.0, amount: collapsedState.minFraction)
|
||||
let titleContentOffset: CGFloat = titleMinContentOffset.interpolate(to: 0.0 as CGFloat, amount: collapsedState.maxFraction)
|
||||
|
||||
component.updateTitleContentOffset(titleContentOffset, transition)
|
||||
|
||||
self.currentFraction = collapsedState.globalFraction
|
||||
|
||||
component.externalState.collapsedWidth = collapsedContentWidth
|
||||
@ -536,32 +686,47 @@ public final class StoryPeerListComponent: Component {
|
||||
let effectiveVisibleBounds = self.scrollView.bounds
|
||||
let visibleBounds = effectiveVisibleBounds.insetBy(dx: -200.0, dy: 0.0)
|
||||
|
||||
var effectiveFirstVisibleIndex = 0
|
||||
for i in 0 ..< self.sortedItems.count {
|
||||
let regularItemFrame = itemLayout.frame(at: i)
|
||||
let isReallyVisible = effectiveVisibleBounds.intersects(regularItemFrame)
|
||||
if isReallyVisible {
|
||||
effectiveFirstVisibleIndex = i
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
struct MeasuredItem {
|
||||
var itemFrame: CGRect
|
||||
var itemScale: CGFloat
|
||||
}
|
||||
let calculateItem: (Int) -> MeasuredItem = { i in
|
||||
let regularItemFrame = itemLayout.frame(at: i)
|
||||
let calculateItem: (Int) -> MeasuredItem = { index in
|
||||
let frameIndex = index
|
||||
let regularItemFrame = itemLayout.frame(at: frameIndex)
|
||||
let isReallyVisible = effectiveVisibleBounds.intersects(regularItemFrame)
|
||||
|
||||
let collapseIndex = index - effectiveFirstVisibleIndex
|
||||
|
||||
let collapsedItemX: CGFloat
|
||||
if i < collapseStartIndex {
|
||||
if collapseIndex < collapseStartIndex {
|
||||
collapsedItemX = collapsedContentOrigin
|
||||
} else if i > collapseEndIndex {
|
||||
} else if collapseIndex > collapseEndIndex {
|
||||
collapsedItemX = collapsedContentOrigin + CGFloat(collapseEndIndex) * collapsedItemDistance - collapsedItemWidth * 0.5
|
||||
} else {
|
||||
collapsedItemX = collapsedContentOrigin + CGFloat(i - collapseStartIndex) * collapsedItemDistance
|
||||
collapsedItemX = collapsedContentOrigin + CGFloat(collapseIndex - collapseStartIndex) * collapsedItemDistance
|
||||
}
|
||||
let collapsedItemFrame = CGRect(origin: CGPoint(x: collapsedItemX, y: regularItemFrame.minY + collapsedItemOffsetY), size: CGSize(width: collapsedItemWidth, height: regularItemFrame.height))
|
||||
|
||||
var collapsedMaxItemFrame = collapsedItemFrame
|
||||
|
||||
var collapseDistance: CGFloat = CGFloat(i - collapseStartIndex) / CGFloat(collapseEndIndex - collapseStartIndex)
|
||||
collapseDistance = max(0.0, min(1.0, collapseDistance))
|
||||
collapsedMaxItemFrame.origin.x -= collapsedState.minFraction * 4.0
|
||||
collapsedMaxItemFrame.origin.x += collapseDistance * 20.0
|
||||
collapsedMaxItemFrame.origin.y += collapseDistance * 20.0
|
||||
collapsedMaxItemFrame.origin.y += collapsedState.minFraction * 10.0
|
||||
if itemLayout.itemCount > 1 {
|
||||
var collapseDistance: CGFloat = CGFloat(collapseIndex - collapseStartIndex) / CGFloat(collapseEndIndex - collapseStartIndex)
|
||||
collapseDistance = max(0.0, min(1.0, collapseDistance))
|
||||
collapsedMaxItemFrame.origin.x -= collapsedState.minFraction * 4.0
|
||||
collapsedMaxItemFrame.origin.x += collapseDistance * 20.0
|
||||
collapsedMaxItemFrame.origin.y += collapseDistance * 20.0
|
||||
collapsedMaxItemFrame.origin.y += collapsedState.minFraction * 10.0
|
||||
}
|
||||
|
||||
let minimizedItemScale: CGFloat = 24.0 / 52.0
|
||||
let minimizedMaxItemScale: CGFloat = (24.0 + 4.0) / 52.0
|
||||
@ -574,11 +739,12 @@ public final class StoryPeerListComponent: Component {
|
||||
let itemFrame: CGRect
|
||||
if isReallyVisible {
|
||||
var adjustedRegularFrame = regularItemFrame
|
||||
if i < collapseStartIndex {
|
||||
adjustedRegularFrame = adjustedRegularFrame.interpolate(to: itemLayout.frame(at: collapseStartIndex), amount: 0.0)
|
||||
} else if i > collapseEndIndex {
|
||||
adjustedRegularFrame = adjustedRegularFrame.interpolate(to: itemLayout.frame(at: collapseEndIndex), amount: 0.0)
|
||||
if index < collapseStartIndex {
|
||||
adjustedRegularFrame = adjustedRegularFrame.interpolate(to: itemLayout.frame(at: effectiveFirstVisibleIndex + collapseStartIndex), amount: 0.0)
|
||||
} else if index > collapseEndIndex {
|
||||
adjustedRegularFrame = adjustedRegularFrame.interpolate(to: itemLayout.frame(at: effectiveFirstVisibleIndex + collapseEndIndex), amount: 0.0)
|
||||
}
|
||||
adjustedRegularFrame.origin.x -= effectiveVisibleBounds.minX
|
||||
|
||||
let collapsedItemPosition: CGPoint = collapsedItemFrame.center.interpolate(to: collapsedMaxItemFrame.center, amount: collapsedState.minFraction)
|
||||
|
||||
@ -588,13 +754,11 @@ public final class StoryPeerListComponent: Component {
|
||||
bounceOffsetFraction = max(-1.0, min(1.0, bounceOffsetFraction))
|
||||
itemPosition.x += min(10.0, expandBoundsFraction * collapsedState.maxFraction * 1200.0) * bounceOffsetFraction
|
||||
|
||||
//let itemPosition = solveParabolicMotion(from: collapsedItemPosition, to: adjustedRegularFrame.center, progress: collapsedState.maxFraction)
|
||||
|
||||
let itemSize = CGSize(width: adjustedRegularFrame.width * itemScale, height: adjustedRegularFrame.height)
|
||||
|
||||
itemFrame = itemSize.centered(around: itemPosition)
|
||||
} else {
|
||||
itemFrame = regularItemFrame
|
||||
itemFrame = regularItemFrame.offsetBy(dx: -effectiveVisibleBounds.minX, dy: 0.0)
|
||||
}
|
||||
|
||||
return MeasuredItem(
|
||||
@ -604,12 +768,20 @@ public final class StoryPeerListComponent: Component {
|
||||
}
|
||||
|
||||
var validIds: [EnginePeer.Id] = []
|
||||
var validCollapsableIds: [EnginePeer.Id] = []
|
||||
|
||||
for i in 0 ..< self.sortedItems.count {
|
||||
let itemSet = self.sortedItems[i]
|
||||
let peer = itemSet.peer
|
||||
|
||||
let regularItemFrame = itemLayout.frame(at: i)
|
||||
|
||||
var isItemVisible = true
|
||||
if !visibleBounds.intersects(regularItemFrame) {
|
||||
isItemVisible = false
|
||||
}
|
||||
|
||||
if !isItemVisible {
|
||||
continue
|
||||
}
|
||||
|
||||
@ -654,18 +826,34 @@ public final class StoryPeerListComponent: Component {
|
||||
|
||||
var itemAlpha: CGFloat = 1.0
|
||||
var isCollapsable: Bool = false
|
||||
var itemScale = measuredItem.itemScale
|
||||
if itemLayout.itemCount == 1 {
|
||||
let singleScaleFactor = min(1.0, collapsedState.minFraction + collapsedState.maxFraction)
|
||||
itemScale = 0.001 * (1.0 - singleScaleFactor) + itemScale * singleScaleFactor
|
||||
}
|
||||
|
||||
if i >= collapseStartIndex && i <= collapseEndIndex {
|
||||
let collapseIndex = i - effectiveFirstVisibleIndex
|
||||
if collapseIndex >= collapseStartIndex && collapseIndex <= collapseEndIndex {
|
||||
isCollapsable = true
|
||||
|
||||
if i != collapseStartIndex {
|
||||
if collapseIndex != collapseStartIndex {
|
||||
leftItemFrame = calculateItem(i - 1).itemFrame
|
||||
}
|
||||
if i != collapseEndIndex {
|
||||
if collapseIndex != collapseEndIndex {
|
||||
rightItemFrame = calculateItem(i + 1).itemFrame
|
||||
}
|
||||
|
||||
if effectiveFirstVisibleIndex == 0 && !component.titleHasActivity {
|
||||
itemAlpha = 1.0
|
||||
} else {
|
||||
itemAlpha = collapsedState.sideAlphaFraction
|
||||
}
|
||||
} else {
|
||||
itemAlpha = collapsedState.sideAlphaFraction
|
||||
if itemLayout.itemCount == 1 {
|
||||
itemAlpha = min(1.0, (collapsedState.minFraction + collapsedState.maxFraction) * 4.0)
|
||||
} else {
|
||||
itemAlpha = collapsedState.sideAlphaFraction
|
||||
}
|
||||
}
|
||||
|
||||
var leftNeighborDistance: CGPoint?
|
||||
@ -690,7 +878,7 @@ public final class StoryPeerListComponent: Component {
|
||||
hasItems: hasItems,
|
||||
ringAnimation: itemRingAnimation,
|
||||
collapseFraction: isReallyVisible ? (1.0 - collapsedState.maxFraction) : 0.0,
|
||||
scale: measuredItem.itemScale,
|
||||
scale: itemScale,
|
||||
collapsedWidth: collapsedItemWidth,
|
||||
expandedAlphaFraction: collapsedState.sideAlphaFraction,
|
||||
leftNeighborDistance: leftNeighborDistance,
|
||||
@ -704,8 +892,151 @@ public final class StoryPeerListComponent: Component {
|
||||
|
||||
if let itemView = visibleItem.view.view as? StoryPeerListItemComponent.View {
|
||||
if itemView.superview == nil {
|
||||
self.scrollView.addSubview(itemView)
|
||||
self.scrollView.addSubview(itemView.backgroundContainer)
|
||||
self.scrollContainerView.addSubview(itemView)
|
||||
self.scrollContainerView.addSubview(itemView.backgroundContainer)
|
||||
}
|
||||
|
||||
if isCollapsable {
|
||||
itemView.layer.zPosition = 1000.0 - CGFloat(i) * 0.01
|
||||
itemView.backgroundContainer.layer.zPosition = 1.0
|
||||
} else {
|
||||
itemView.layer.zPosition = 0.5
|
||||
itemView.backgroundContainer.layer.zPosition = 0.0
|
||||
}
|
||||
|
||||
itemTransition.setFrame(view: itemView, frame: measuredItem.itemFrame)
|
||||
itemTransition.setAlpha(view: itemView, alpha: itemAlpha)
|
||||
itemTransition.setScale(view: itemView, scale: 1.0)
|
||||
|
||||
itemTransition.setFrame(view: itemView.backgroundContainer, frame: measuredItem.itemFrame)
|
||||
itemTransition.setAlpha(view: itemView.backgroundContainer, alpha: itemAlpha)
|
||||
itemTransition.setScale(view: itemView.backgroundContainer, scale: 1.0)
|
||||
|
||||
itemView.updateIsPreviewing(isPreviewing: self.previewedItemId == itemSet.peer.id)
|
||||
}
|
||||
}
|
||||
|
||||
for i in 0 ..< self.sortedItems.count {
|
||||
let itemSet = self.sortedItems[i]
|
||||
let peer = itemSet.peer
|
||||
|
||||
if i >= collapseStartIndex && i <= collapseEndIndex {
|
||||
} else {
|
||||
continue
|
||||
}
|
||||
|
||||
validCollapsableIds.append(itemSet.peer.id)
|
||||
|
||||
let visibleItem: VisibleItem
|
||||
var itemTransition = transition
|
||||
if let current = self.visibleCollapsableItems[itemSet.peer.id] {
|
||||
visibleItem = current
|
||||
} else {
|
||||
itemTransition = .immediate
|
||||
visibleItem = VisibleItem()
|
||||
self.visibleCollapsableItems[itemSet.peer.id] = visibleItem
|
||||
}
|
||||
|
||||
var hasUnseen = false
|
||||
hasUnseen = itemSet.hasUnseen
|
||||
|
||||
var hasUnseenCloseFriendsItems = itemSet.hasUnseenCloseFriends
|
||||
|
||||
var hasItems = true
|
||||
var itemRingAnimation: StoryPeerListItemComponent.RingAnimation?
|
||||
if peer.id == component.context.account.peerId {
|
||||
if let storySubscriptions = component.storySubscriptions, let accountItem = storySubscriptions.accountItem {
|
||||
hasItems = accountItem.storyCount != 0
|
||||
} else {
|
||||
hasItems = false
|
||||
}
|
||||
if let uploadProgress = component.uploadProgress {
|
||||
itemRingAnimation = .progress(uploadProgress)
|
||||
}
|
||||
|
||||
hasUnseenCloseFriendsItems = false
|
||||
}
|
||||
|
||||
let collapseIndex = i + effectiveFirstVisibleIndex
|
||||
let measuredItem = calculateItem(collapseIndex)
|
||||
|
||||
var leftItemFrame: CGRect?
|
||||
var rightItemFrame: CGRect?
|
||||
|
||||
var itemAlpha: CGFloat = 1.0
|
||||
var isCollapsable: Bool = false
|
||||
var itemScale = measuredItem.itemScale
|
||||
if itemLayout.itemCount == 1 {
|
||||
let singleScaleFactor = min(1.0, collapsedState.minFraction + collapsedState.maxFraction)
|
||||
itemScale = 0.001 * (1.0 - singleScaleFactor) + itemScale * singleScaleFactor
|
||||
}
|
||||
|
||||
if i >= collapseStartIndex && i <= collapseEndIndex {
|
||||
isCollapsable = true
|
||||
|
||||
if i != collapseStartIndex {
|
||||
leftItemFrame = calculateItem(collapseIndex - 1).itemFrame
|
||||
}
|
||||
if i != collapseEndIndex {
|
||||
rightItemFrame = calculateItem(collapseIndex + 1).itemFrame
|
||||
}
|
||||
|
||||
if effectiveFirstVisibleIndex == 0 {
|
||||
itemAlpha = 0.0
|
||||
} else {
|
||||
itemAlpha = 1.0 - collapsedState.sideAlphaFraction
|
||||
}
|
||||
} else {
|
||||
if itemLayout.itemCount == 1 {
|
||||
itemAlpha = min(1.0, (collapsedState.minFraction + collapsedState.maxFraction) * 4.0)
|
||||
} else {
|
||||
itemAlpha = collapsedState.sideAlphaFraction
|
||||
}
|
||||
}
|
||||
|
||||
if component.titleHasActivity {
|
||||
itemAlpha = 0.0
|
||||
}
|
||||
|
||||
var leftNeighborDistance: CGPoint?
|
||||
var rightNeighborDistance: CGPoint?
|
||||
|
||||
if let leftItemFrame {
|
||||
leftNeighborDistance = CGPoint(x: abs(leftItemFrame.midX - measuredItem.itemFrame.midX), y: leftItemFrame.minY - measuredItem.itemFrame.minY)
|
||||
}
|
||||
if let rightItemFrame {
|
||||
rightNeighborDistance = CGPoint(x: abs(rightItemFrame.midX - measuredItem.itemFrame.midX), y: rightItemFrame.minY - measuredItem.itemFrame.minY)
|
||||
}
|
||||
|
||||
let _ = visibleItem.view.update(
|
||||
transition: itemTransition,
|
||||
component: AnyComponent(StoryPeerListItemComponent(
|
||||
context: component.context,
|
||||
theme: component.theme,
|
||||
strings: component.strings,
|
||||
peer: peer,
|
||||
hasUnseen: hasUnseen,
|
||||
hasUnseenCloseFriendsItems: hasUnseenCloseFriendsItems,
|
||||
hasItems: hasItems,
|
||||
ringAnimation: itemRingAnimation,
|
||||
collapseFraction: 1.0 - collapsedState.maxFraction,
|
||||
scale: itemScale,
|
||||
collapsedWidth: collapsedItemWidth,
|
||||
expandedAlphaFraction: collapsedState.sideAlphaFraction,
|
||||
leftNeighborDistance: leftNeighborDistance,
|
||||
rightNeighborDistance: rightNeighborDistance,
|
||||
action: component.peerAction,
|
||||
contextGesture: component.contextPeerAction
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: itemLayout.itemSize
|
||||
)
|
||||
|
||||
if let itemView = visibleItem.view.view as? StoryPeerListItemComponent.View {
|
||||
if itemView.superview == nil {
|
||||
itemView.isUserInteractionEnabled = false
|
||||
self.scrollContainerView.addSubview(itemView)
|
||||
self.scrollContainerView.addSubview(itemView.backgroundContainer)
|
||||
}
|
||||
|
||||
if isCollapsable {
|
||||
@ -733,16 +1064,8 @@ public final class StoryPeerListComponent: Component {
|
||||
if !validIds.contains(id) {
|
||||
removedIds.append(id)
|
||||
if let itemView = visibleItem.view.view as? StoryPeerListItemComponent.View {
|
||||
if keepVisibleUntilCompletion && !transition.animation.isImmediate {
|
||||
let backgroundContainer = itemView.backgroundContainer
|
||||
transition.attachAnimation(view: itemView, id: "keep", completion: { [weak itemView, weak backgroundContainer] _ in
|
||||
itemView?.removeFromSuperview()
|
||||
backgroundContainer?.removeFromSuperview()
|
||||
})
|
||||
} else {
|
||||
itemView.backgroundContainer.removeFromSuperview()
|
||||
itemView.removeFromSuperview()
|
||||
}
|
||||
itemView.backgroundContainer.removeFromSuperview()
|
||||
itemView.removeFromSuperview()
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -750,13 +1073,77 @@ public final class StoryPeerListComponent: Component {
|
||||
self.visibleItems.removeValue(forKey: id)
|
||||
}
|
||||
|
||||
transition.setFrame(view: self.collapsedButton, frame: CGRect(origin: CGPoint(x: collapsedContentOrigin - 4.0, y: 6.0 - 59.0), size: CGSize(width: collapsedContentWidth + 4.0, height: 44.0)))
|
||||
var removedCollapsableIds: [EnginePeer.Id] = []
|
||||
for (id, visibleItem) in self.visibleCollapsableItems {
|
||||
if !validCollapsableIds.contains(id) {
|
||||
removedCollapsableIds.append(id)
|
||||
if let itemView = visibleItem.view.view as? StoryPeerListItemComponent.View {
|
||||
itemView.backgroundContainer.removeFromSuperview()
|
||||
itemView.removeFromSuperview()
|
||||
}
|
||||
}
|
||||
}
|
||||
for id in removedCollapsableIds {
|
||||
self.visibleCollapsableItems.removeValue(forKey: id)
|
||||
}
|
||||
|
||||
transition.setFrame(view: self.collapsedButton, frame: CGRect(origin: CGPoint(x: component.minTitleX, y: 6.0 - 59.0), size: CGSize(width: max(0.0, component.maxTitleX - component.minTitleX), height: 44.0)))
|
||||
|
||||
let defaultCollapsedTitleOffset: CGFloat = 0.0
|
||||
|
||||
var targetCollapsedTitleOffset: CGFloat = collapsedContentOrigin + collapsedContentOriginOffset + collapsedContentWidth + titleContentSpacing
|
||||
if itemLayout.itemCount == 1 && collapsedContentWidth <= 0.1 {
|
||||
let singleScaleFactor = min(1.0, collapsedState.minFraction)
|
||||
targetCollapsedTitleOffset += singleScaleFactor * 4.0
|
||||
}
|
||||
|
||||
let collapsedTitleOffset = targetCollapsedTitleOffset - defaultCollapsedTitleOffset
|
||||
|
||||
let titleMinContentOffset: CGFloat = collapsedTitleOffset.interpolate(to: collapsedTitleOffset + 12.0, amount: collapsedState.minFraction)
|
||||
var titleContentOffset: CGFloat = titleMinContentOffset.interpolate(to: floor((itemLayout.containerSize.width - titleContentWidth) * 0.5) as CGFloat, amount: collapsedState.maxFraction)
|
||||
|
||||
if let titleIndicatorSize, let titleIndicatorView = self.titleIndicatorView?.view {
|
||||
let titleIndicatorFrame = CGRect(origin: CGPoint(x: titleContentOffset, y: collapsedItemOffsetY + 1.0 + floor((56.0 - titleIndicatorSize.height) * 0.5)), size: titleIndicatorSize)
|
||||
if titleIndicatorView.superview == nil {
|
||||
self.addSubview(titleIndicatorView)
|
||||
}
|
||||
titleIndicatorView.frame = titleIndicatorFrame
|
||||
titleContentOffset += titleIndicatorSize.width + titleIndicatorSpacing
|
||||
}
|
||||
|
||||
let titleFrame = CGRect(origin: CGPoint(x: titleContentOffset, y: collapsedItemOffsetY + 1.0 + floor((56.0 - titleSize.height) * 0.5)), size: titleSize)
|
||||
if let titleComponentView = self.titleView.view {
|
||||
if titleComponentView.superview == nil {
|
||||
titleComponentView.isUserInteractionEnabled = false
|
||||
self.addSubview(titleComponentView)
|
||||
}
|
||||
titleComponentView.frame = titleFrame
|
||||
}
|
||||
titleContentOffset += titleSize.width
|
||||
|
||||
if let titleIconSize, let titleIconView = self.titleIconView?.view {
|
||||
titleContentOffset += titleIconSpacing
|
||||
|
||||
let titleIconFrame = CGRect(origin: CGPoint(x: titleContentOffset, y: collapsedItemOffsetY + 1.0 + floor((56.0 - titleIconSize.height) * 0.5)), size: titleIconSize)
|
||||
|
||||
if titleIconView.superview == nil {
|
||||
self.addSubview(titleIconView)
|
||||
}
|
||||
titleIconView.frame = titleIconFrame
|
||||
}
|
||||
}
|
||||
|
||||
override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
||||
if self.alpha.isZero {
|
||||
return nil
|
||||
}
|
||||
|
||||
if let titleIconView = self.titleIconView?.view {
|
||||
if let result = titleIconView.hitTest(self.convert(point, to: titleIconView), with: event) {
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
var result: UIView?
|
||||
for view in self.subviews.reversed() {
|
||||
if let resultValue = view.hitTest(self.convert(point, to: view), with: event), resultValue.isUserInteractionEnabled {
|
||||
@ -773,7 +1160,7 @@ public final class StoryPeerListComponent: Component {
|
||||
return nil
|
||||
}
|
||||
} else {
|
||||
if !result.isDescendant(of: self.scrollView) {
|
||||
if !result.isDescendant(of: self.scrollContainerView) {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
@ -784,20 +1171,6 @@ public final class StoryPeerListComponent: Component {
|
||||
var transition = transition
|
||||
transition.animation = .none
|
||||
|
||||
if self.component != nil {
|
||||
if !component.unlocked && self.scrollView.bounds.minX != 0.0 {
|
||||
self.ignoreScrolling = true
|
||||
|
||||
let scrollingDistance = self.scrollView.bounds.minX
|
||||
self.scrollView.bounds = CGRect(origin: CGPoint(), size: self.scrollView.bounds.size)
|
||||
let tempTransition = Transition(animation: .curve(duration: 0.3, curve: .spring))
|
||||
self.updateScrolling(transition: tempTransition, keepVisibleUntilCompletion: true)
|
||||
tempTransition.animateBoundsOrigin(view: self.scrollView, from: CGPoint(x: scrollingDistance, y: 0.0), to: CGPoint(), additive: true)
|
||||
|
||||
self.ignoreScrolling = false
|
||||
}
|
||||
}
|
||||
|
||||
let animationHint = transition.userData(AnimationHint.self)
|
||||
var useAnimation = false
|
||||
if let previousComponent = self.component, component.unlocked != previousComponent.unlocked {
|
||||
@ -892,12 +1265,13 @@ public final class StoryPeerListComponent: Component {
|
||||
self.ignoreScrolling = true
|
||||
|
||||
transition.setFrame(view: self.scrollView, frame: CGRect(origin: CGPoint(x: 0.0, y: -4.0), size: CGSize(width: availableSize.width, height: availableSize.height + 4.0)))
|
||||
transition.setFrame(view: self.scrollContainerView, frame: CGRect(origin: CGPoint(x: 0.0, y: -4.0), size: CGSize(width: availableSize.width, height: availableSize.height + 4.0)))
|
||||
if self.scrollView.contentSize != itemLayout.contentSize {
|
||||
self.scrollView.contentSize = itemLayout.contentSize
|
||||
}
|
||||
|
||||
self.ignoreScrolling = false
|
||||
self.updateScrolling(transition: transition, keepVisibleUntilCompletion: false)
|
||||
self.updateScrolling(transition: transition)
|
||||
|
||||
return availableSize
|
||||
}
|
||||
|
@ -665,7 +665,7 @@ public final class StoryPeerListItemComponent: Component {
|
||||
}
|
||||
} else {
|
||||
if component.theme.overallDarkAppearance {
|
||||
colors = [UIColor(rgb: 0x48484A).cgColor, UIColor(rgb: 0x48484A).cgColor]
|
||||
colors = [component.theme.rootController.tabBar.textColor.cgColor, component.theme.rootController.tabBar.textColor.cgColor]
|
||||
} else {
|
||||
colors = [UIColor(rgb: 0xD8D8E1).cgColor, UIColor(rgb: 0xD8D8E1).cgColor]
|
||||
}
|
||||
|
@ -0,0 +1,61 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import ComponentFlow
|
||||
import ActivityIndicator
|
||||
|
||||
public final class TitleActivityIndicatorComponent: Component {
|
||||
let color: UIColor
|
||||
|
||||
public init(
|
||||
color: UIColor
|
||||
) {
|
||||
self.color = color
|
||||
}
|
||||
|
||||
public static func ==(lhs: TitleActivityIndicatorComponent, rhs: TitleActivityIndicatorComponent) -> Bool {
|
||||
if lhs.color != rhs.color {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
public final class View: UIView {
|
||||
private var activityIndicator: ActivityIndicator?
|
||||
|
||||
public override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
}
|
||||
|
||||
required public init?(coder: NSCoder) {
|
||||
preconditionFailure()
|
||||
}
|
||||
|
||||
deinit {
|
||||
}
|
||||
|
||||
func update(component: TitleActivityIndicatorComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
|
||||
let activityIndicator: ActivityIndicator
|
||||
if let current = self.activityIndicator {
|
||||
activityIndicator = current
|
||||
} else {
|
||||
activityIndicator = ActivityIndicator(type: .custom(component.color, availableSize.width, 2.0, true))
|
||||
self.activityIndicator = activityIndicator
|
||||
self.addSubview(activityIndicator.view)
|
||||
}
|
||||
|
||||
activityIndicator.frame = CGRect(origin: CGPoint(), size: availableSize)
|
||||
activityIndicator.isHidden = false
|
||||
|
||||
return availableSize
|
||||
}
|
||||
}
|
||||
|
||||
public func makeView() -> View {
|
||||
return View(frame: CGRect())
|
||||
}
|
||||
|
||||
public 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)
|
||||
}
|
||||
}
|
12
submodules/TelegramUI/Images.xcassets/Chat/Context Menu/MessageBubble.imageset/Contents.json
vendored
Normal file
12
submodules/TelegramUI/Images.xcassets/Chat/Context Menu/MessageBubble.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "messagebubble.svg",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M12 4.66502C7.88795 4.66502 4.66502 7.68163 4.66502 11.2727C4.66502 13.329 5.61495 15.016 7.28375 16.2378C7.50459 16.3995 7.61824 16.6447 7.67716 16.8096C7.74628 17.003 7.78941 17.2274 7.80119 17.4635C7.82475 17.9355 7.72531 18.5159 7.37618 19.0511C7.32109 19.1355 7.26431 19.2162 7.20737 19.2928C7.45008 19.2413 7.68743 19.1624 7.88779 19.05C8.43439 18.7435 8.78169 18.4401 9.03437 18.2083C9.05375 18.1905 9.07401 18.1718 9.0949 18.1524C9.19597 18.0589 9.3118 17.9516 9.41317 17.8755C9.51328 17.8003 9.80413 17.5892 10.1854 17.6767C10.7649 17.8096 11.3728 17.8805 12 17.8805C16.1121 17.8805 19.335 14.8639 19.335 11.2727C19.335 7.68163 16.1121 4.66502 12 4.66502ZM6.17873 19.351C6.17875 19.3509 6.18007 19.3512 6.18254 19.3518C6.17992 19.3513 6.1787 19.351 6.17873 19.351ZM6.49475 20.1245C6.49316 20.1271 6.49223 20.1284 6.49219 20.1284C6.49214 20.1284 6.49293 20.1271 6.49475 20.1245ZM3.33502 11.2727C3.33502 6.83064 7.27554 3.33502 12 3.33502C16.7245 3.33502 20.665 6.83064 20.665 11.2727C20.665 15.7149 16.7245 19.2105 12 19.2105C11.3532 19.2105 10.7221 19.1455 10.1145 19.022C10.0893 19.0448 10.0598 19.0721 10.0238 19.1052C9.99742 19.1295 9.9676 19.157 9.93357 19.1883C9.64507 19.453 9.2118 19.8324 8.53835 20.21C8.00052 20.5117 7.38987 20.636 6.91077 20.6797C6.66653 20.702 6.44116 20.7047 6.25548 20.6946C6.16291 20.6897 6.07427 20.6812 5.9943 20.6688C5.92695 20.6584 5.82187 20.6389 5.72037 20.5963C5.60837 20.5493 5.41843 20.4446 5.30536 20.2201C5.18416 19.9794 5.22417 19.7499 5.27201 19.6141C5.31662 19.4875 5.38558 19.3872 5.43085 19.3263C5.48061 19.2594 5.53686 19.1946 5.58714 19.1386C5.62942 19.0915 5.6727 19.0445 5.71667 18.9967C5.89442 18.8038 6.08334 18.5986 6.26224 18.3244C6.43214 18.0639 6.48498 17.7728 6.47285 17.5298C6.46678 17.4083 6.44507 17.3141 6.42472 17.2571C6.42464 17.2569 6.42456 17.2567 6.42448 17.2565C4.49565 15.8148 3.33502 13.7665 3.33502 11.2727ZM6.40689 17.2161C6.407 17.2159 6.40865 17.2184 6.41149 17.2244C6.4082 17.2193 6.40678 17.2163 6.40689 17.2161ZM10.2318 18.9257C10.2316 18.9262 10.2262 18.9301 10.2162 18.9357C10.2269 18.9279 10.2319 18.9251 10.2318 18.9257Z" fill="white"/>
|
||||
</svg>
|
After Width: | Height: | Size: 2.2 KiB |
BIN
submodules/TelegramUI/Resources/Animations/StoryListEmpty.tgs
Normal file
BIN
submodules/TelegramUI/Resources/Animations/StoryListEmpty.tgs
Normal file
Binary file not shown.
@ -469,7 +469,7 @@ final class PeerInfoAvatarTransformContainerNode: ASDisplayNode {
|
||||
component: AnyComponent(AvatarStoryIndicatorComponent(
|
||||
hasUnseen: storyData.hasUnseen,
|
||||
hasUnseenCloseFriendsItems: storyData.hasUnseenCloseFriends,
|
||||
isDarkTheme: theme.overallDarkAppearance,
|
||||
theme: theme,
|
||||
activeLineWidth: 3.0,
|
||||
inactiveLineWidth: 2.0,
|
||||
counters: nil
|
||||
|
@ -369,7 +369,7 @@ public final class TelegramRootController: NavigationController, TelegramRootCon
|
||||
case let .image(image, dimensions):
|
||||
if let imageData = compressImageToJPEG(image, quality: 0.7) {
|
||||
let entities = generateChatInputTextEntities(caption)
|
||||
self.context.engine.messages.uploadStory(media: .image(dimensions: dimensions, data: imageData), text: caption.string, entities: entities, pin: privacy.archive, privacy: privacy.privacy, period: privacy.timeout, randomId: randomId)
|
||||
self.context.engine.messages.uploadStory(media: .image(dimensions: dimensions, data: imageData), text: caption.string, entities: entities, pin: privacy.archive, privacy: privacy.privacy, isForwardingDisabled: false, period: privacy.timeout, randomId: randomId)
|
||||
Queue.mainQueue().justDispatch {
|
||||
commit({})
|
||||
}
|
||||
@ -392,7 +392,7 @@ public final class TelegramRootController: NavigationController, TelegramRootCon
|
||||
}
|
||||
let imageData = firstFrameImage.flatMap { compressImageToJPEG($0, quality: 0.6) }
|
||||
let entities = generateChatInputTextEntities(caption)
|
||||
self.context.engine.messages.uploadStory(media: .video(dimensions: dimensions, duration: duration, resource: resource, firstFrameImageData: imageData), text: caption.string, entities: entities, pin: privacy.archive, privacy: privacy.privacy, period: privacy.timeout, randomId: randomId)
|
||||
self.context.engine.messages.uploadStory(media: .video(dimensions: dimensions, duration: duration, resource: resource, firstFrameImageData: imageData), text: caption.string, entities: entities, pin: privacy.archive, privacy: privacy.privacy, isForwardingDisabled: false, period: privacy.timeout, randomId: randomId)
|
||||
Queue.mainQueue().justDispatch {
|
||||
commit({})
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user