Merge branch 'master' of gitlab.com:peter-iakovlev/telegram-ios

This commit is contained in:
Ilya Laktyushin 2023-06-27 16:29:11 +02:00
commit b10ae1b12a
36 changed files with 1198 additions and 257 deletions

View File

@ -2604,6 +2604,18 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
}) })
}))) })))
} else { } 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 items.append(.action(ContextMenuActionItem(text: "View Profile", icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/User"), color: theme.contextMenu.primaryColor) return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/User"), color: theme.contextMenu.primaryColor)
}, action: { [weak self] c, _ in }, 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) return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/MoveToContacts"), color: theme.contextMenu.primaryColor)
}, action: { [weak self] _, f in }, action: { [weak self] _, f in
f(.dismissWithoutContent) f(.dismissWithoutContent)
@ -3727,7 +3739,9 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
searchContentNode.placeholderNode.frame = previousFrame searchContentNode.placeholderNode.frame = previousFrame
} }
self.chatListDisplayNode.tempAllowAvatarExpansion = true
self.requestLayout(transition: .animated(duration: 0.5, curve: .spring)) self.requestLayout(transition: .animated(duration: 0.5, curve: .spring))
self.chatListDisplayNode.tempAllowAvatarExpansion = false
//TODO:swap tabs //TODO:swap tabs

View File

@ -1690,7 +1690,7 @@ final class ChatListControllerNode: ASDisplayNode, UIGestureRecognizerDelegate {
var didBeginSelectingChatsWhileEditing: Bool = false var didBeginSelectingChatsWhileEditing: Bool = false
var isEditing: Bool = false var isEditing: Bool = false
private var tempAllowAvatarExpansion: Bool = false var tempAllowAvatarExpansion: Bool = false
private var tempDisableStoriesAnimations: Bool = false private var tempDisableStoriesAnimations: Bool = false
private var tempNavigationScrollingTransition: ContainedViewLayoutTransition? private var tempNavigationScrollingTransition: ContainedViewLayoutTransition?

View File

@ -2807,7 +2807,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
component: AnyComponent(AvatarStoryIndicatorComponent( component: AnyComponent(AvatarStoryIndicatorComponent(
hasUnseen: storyState.hasUnseen, hasUnseen: storyState.hasUnseen,
hasUnseenCloseFriendsItems: storyState.hasUnseenCloseFriends, hasUnseenCloseFriendsItems: storyState.hasUnseenCloseFriends,
isDarkTheme: item.presentationData.theme.overallDarkAppearance, theme: item.presentationData.theme,
activeLineWidth: 2.0, activeLineWidth: 2.0,
inactiveLineWidth: 1.0 + UIScreenPixel, inactiveLineWidth: 1.0 + UIScreenPixel,
counters: nil counters: nil

View File

@ -43,6 +43,7 @@ swift_library(
"//submodules/TelegramUI/Components/ChatListHeaderComponent", "//submodules/TelegramUI/Components/ChatListHeaderComponent",
"//submodules/ComponentFlow", "//submodules/ComponentFlow",
"//submodules/TooltipUI", "//submodules/TooltipUI",
"//submodules/UndoUI",
], ],
visibility = [ visibility = [
"//visibility:public", "//visibility:public",

View File

@ -9,6 +9,7 @@ import AlertUI
import PresentationDataUtils import PresentationDataUtils
import OverlayStatusController import OverlayStatusController
import LocalizedPeerData import LocalizedPeerData
import UndoUI
import TooltipUI import TooltipUI
func contactContextMenuItems(context: AccountContext, peerId: EnginePeer.Id, contactsController: ContactsController?, isStories: Bool) -> Signal<[ContextMenuItem], NoError> { 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( return context.engine.data.get(
TelegramEngine.EngineData.Item.Peer.Peer(id: peerId), TelegramEngine.EngineData.Item.Peer.Peer(id: peerId),
TelegramEngine.EngineData.Item.Peer.AreVoiceCallsAvailable(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] = [] var items: [ContextMenuItem] = []
if isStories { if isStories {
//TODO:localize //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) return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/MoveToChats"), color: theme.contextMenu.primaryColor)
}, action: { [weak contactsController] _, f in }, action: { _, f in
f(.default) f(.default)
context.engine.peers.updatePeerStoriesHidden(id: peerId, isHidden: false) 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)) 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 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

View File

@ -559,67 +559,6 @@ public class ContactsController: ViewController {
self.push(storyContainerScreen) 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() { @objc private func sortPressed() {

View File

@ -527,11 +527,19 @@ final class ContactsControllerNode: ASDisplayNode, UIGestureRecognizerDelegate {
guard let contactsController = self.controller else { guard let contactsController = self.controller else {
return return
} }
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)) 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) 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) let contextController = ContextController(account: self.context.account, presentationData: self.presentationData, source: .controller(ContextControllerContentSourceImpl(controller: chatController, sourceNode: node)), items: items, gesture: gesture)
contactsController.presentInGlobalOverlay(contextController) contactsController.presentInGlobalOverlay(contextController)
} }
}
func activateSearch(placeholderNode: SearchBarPlaceholderNode) { func activateSearch(placeholderNode: SearchBarPlaceholderNode) {
guard let (containerLayout, navigationBarHeight) = self.containerLayout, self.searchDisplayController == nil else { guard let (containerLayout, navigationBarHeight) = self.containerLayout, self.searchDisplayController == nil else {
@ -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)
}
}

View File

@ -1115,7 +1115,7 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode {
component: AnyComponent(AvatarStoryIndicatorComponent( component: AnyComponent(AvatarStoryIndicatorComponent(
hasUnseen: storyStats.unseen != 0, hasUnseen: storyStats.unseen != 0,
hasUnseenCloseFriendsItems: storyStats.hasUnseenCloseFriends, hasUnseenCloseFriendsItems: storyStats.hasUnseenCloseFriends,
isDarkTheme: item.presentationData.theme.overallDarkAppearance, theme: item.presentationData.theme,
activeLineWidth: 1.0 + UIScreenPixel, activeLineWidth: 1.0 + UIScreenPixel,
inactiveLineWidth: 1.0 + UIScreenPixel, inactiveLineWidth: 1.0 + UIScreenPixel,
counters: AvatarStoryIndicatorComponent.Counters(totalCount: storyStats.total, unseenCount: storyStats.unseen) counters: AvatarStoryIndicatorComponent.Counters(totalCount: storyStats.total, unseenCount: storyStats.unseen)

View File

@ -105,6 +105,9 @@ func telegramMediaFileAttributesFromApiAttributes(_ attributes: [Api.DocumentAtt
if (flags & (1 << 1)) != 0 { if (flags & (1 << 1)) != 0 {
videoFlags.insert(.supportsStreaming) 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)) result.append(.Video(duration: Double(duration), size: PixelDimensions(width: w, height: h), flags: videoFlags, preloadSize: preloadSize))
case let .documentAttributeAudio(flags, duration, title, performer, waveform): case let .documentAttributeAudio(flags, duration, title, performer, waveform):
let isVoice = (flags & (1 << 10)) != 0 let isVoice = (flags & (1 << 10)) != 0

View File

@ -569,6 +569,9 @@ func inputDocumentAttributesFromFileAttributes(_ fileAttributes: [TelegramMediaF
if preloadSize != nil { if preloadSize != nil {
flags |= (1 << 2) 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)) 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): case let .Audio(isVoice, duration, title, performer, waveform):

View File

@ -188,6 +188,7 @@ public struct TelegramMediaVideoFlags: OptionSet {
public static let instantRoundVideo = TelegramMediaVideoFlags(rawValue: 1 << 0) public static let instantRoundVideo = TelegramMediaVideoFlags(rawValue: 1 << 0)
public static let supportsStreaming = TelegramMediaVideoFlags(rawValue: 1 << 1) public static let supportsStreaming = TelegramMediaVideoFlags(rawValue: 1 << 1)
public static let isSilent = TelegramMediaVideoFlags(rawValue: 1 << 3)
} }
public struct StickerMaskCoords: PostboxCoding, Equatable { public struct StickerMaskCoords: PostboxCoding, Equatable {

View File

@ -13,6 +13,7 @@ public extension Stories {
case entities case entities
case pin case pin
case privacy case privacy
case isForwardingDisabled
case period case period
case randomId case randomId
} }
@ -24,6 +25,7 @@ public extension Stories {
public let entities: [MessageTextEntity] public let entities: [MessageTextEntity]
public let pin: Bool public let pin: Bool
public let privacy: EngineStoryPrivacy public let privacy: EngineStoryPrivacy
public let isForwardingDisabled: Bool
public let period: Int32 public let period: Int32
public let randomId: Int64 public let randomId: Int64
@ -35,6 +37,7 @@ public extension Stories {
entities: [MessageTextEntity], entities: [MessageTextEntity],
pin: Bool, pin: Bool,
privacy: EngineStoryPrivacy, privacy: EngineStoryPrivacy,
isForwardingDisabled: Bool,
period: Int32, period: Int32,
randomId: Int64 randomId: Int64
) { ) {
@ -45,6 +48,7 @@ public extension Stories {
self.entities = entities self.entities = entities
self.pin = pin self.pin = pin
self.privacy = privacy self.privacy = privacy
self.isForwardingDisabled = isForwardingDisabled
self.period = period self.period = period
self.randomId = randomId self.randomId = randomId
} }
@ -62,6 +66,7 @@ public extension Stories {
self.entities = try container.decode([MessageTextEntity].self, forKey: .entities) self.entities = try container.decode([MessageTextEntity].self, forKey: .entities)
self.pin = try container.decode(Bool.self, forKey: .pin) self.pin = try container.decode(Bool.self, forKey: .pin)
self.privacy = try container.decode(EngineStoryPrivacy.self, forKey: .privacy) 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.period = try container.decode(Int32.self, forKey: .period)
self.randomId = try container.decode(Int64.self, forKey: .randomId) 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.entities, forKey: .entities)
try container.encode(self.pin, forKey: .pin) try container.encode(self.pin, forKey: .pin)
try container.encode(self.privacy, forKey: .privacy) 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.period, forKey: .period)
try container.encode(self.randomId, forKey: .randomId) try container.encode(self.randomId, forKey: .randomId)
} }
@ -106,6 +112,9 @@ public extension Stories {
if lhs.privacy != rhs.privacy { if lhs.privacy != rhs.privacy {
return false return false
} }
if lhs.isForwardingDisabled != rhs.isForwardingDisabled {
return false
}
if lhs.period != rhs.period { if lhs.period != rhs.period {
return false return false
} }
@ -260,7 +269,7 @@ final class PendingStoryManager {
self.currentPendingItemContext = pendingItemContext self.currentPendingItemContext = pendingItemContext
let stableId = firstItem.stableId 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 |> deliverOn(self.queue)).start(next: { [weak self] event in
guard let `self` = self else { guard let `self` = self else {
return return

View File

@ -125,6 +125,7 @@ public enum Stories {
case isExpired case isExpired
case isPublic case isPublic
case isCloseFriends case isCloseFriends
case isForwardingDisabled
} }
public let id: Int32 public let id: Int32
@ -139,6 +140,7 @@ public enum Stories {
public let isExpired: Bool public let isExpired: Bool
public let isPublic: Bool public let isPublic: Bool
public let isCloseFriends: Bool public let isCloseFriends: Bool
public let isForwardingDisabled: Bool
public init( public init(
id: Int32, id: Int32,
@ -152,7 +154,8 @@ public enum Stories {
isPinned: Bool, isPinned: Bool,
isExpired: Bool, isExpired: Bool,
isPublic: Bool, isPublic: Bool,
isCloseFriends: Bool isCloseFriends: Bool,
isForwardingDisabled: Bool
) { ) {
self.id = id self.id = id
self.timestamp = timestamp self.timestamp = timestamp
@ -166,6 +169,7 @@ public enum Stories {
self.isExpired = isExpired self.isExpired = isExpired
self.isPublic = isPublic self.isPublic = isPublic
self.isCloseFriends = isCloseFriends self.isCloseFriends = isCloseFriends
self.isForwardingDisabled = isForwardingDisabled
} }
public init(from decoder: Decoder) throws { public init(from decoder: Decoder) throws {
@ -189,6 +193,7 @@ public enum Stories {
self.isExpired = try container.decodeIfPresent(Bool.self, forKey: .isExpired) ?? false self.isExpired = try container.decodeIfPresent(Bool.self, forKey: .isExpired) ?? false
self.isPublic = try container.decodeIfPresent(Bool.self, forKey: .isPublic) ?? false self.isPublic = try container.decodeIfPresent(Bool.self, forKey: .isPublic) ?? false
self.isCloseFriends = try container.decodeIfPresent(Bool.self, forKey: .isCloseFriends) ?? 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 { 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.isExpired, forKey: .isExpired)
try container.encode(self.isPublic, forKey: .isPublic) try container.encode(self.isPublic, forKey: .isPublic)
try container.encode(self.isCloseFriends, forKey: .isCloseFriends) try container.encode(self.isCloseFriends, forKey: .isCloseFriends)
try container.encode(self.isForwardingDisabled, forKey: .isForwardingDisabled)
} }
public static func ==(lhs: Item, rhs: Item) -> Bool { public static func ==(lhs: Item, rhs: Item) -> Bool {
@ -260,6 +266,9 @@ public enum Stories {
if lhs.isCloseFriends != rhs.isCloseFriends { if lhs.isCloseFriends != rhs.isCloseFriends {
return false return false
} }
if lhs.isForwardingDisabled != rhs.isForwardingDisabled {
return false
}
return true return true
} }
@ -680,7 +689,7 @@ private func apiInputPrivacyRules(privacy: EngineStoryPrivacy, transaction: Tran
return privacyRules 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 inputMedia = prepareUploadStoryContent(account: account, media: media)
let _ = (account.postbox.transaction { transaction in let _ = (account.postbox.transaction { transaction in
@ -702,6 +711,7 @@ func _internal_uploadStory(account: Account, media: EngineStoryInputMedia, text:
entities: entities, entities: entities,
pin: pin, pin: pin,
privacy: privacy, privacy: privacy,
isForwardingDisabled: isForwardingDisabled,
period: Int32(period), period: Int32(period),
randomId: randomId 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) let (contentSignal, originalMedia) = uploadedStoryContent(postbox: postbox, network: network, media: media, accountPeerId: accountPeerId, messageMediaPreuploadManager: messageMediaPreuploadManager, revalidationContext: revalidationContext, auxiliaryMethods: auxiliaryMethods)
return contentSignal return contentSignal
|> mapToSignal { result -> Signal<StoryUploadResult, NoError> in |> mapToSignal { result -> Signal<StoryUploadResult, NoError> in
@ -787,6 +797,10 @@ func _internal_uploadStoryImpl(postbox: Postbox, network: Network, accountPeerId
flags |= 1 << 3 flags |= 1 << 3
if isForwardingDisabled {
flags |= 1 << 4
}
return network.request(Api.functions.stories.sendStory( return network.request(Api.functions.stories.sendStory(
flags: flags, flags: flags,
media: inputMedia, media: inputMedia,
@ -835,7 +849,8 @@ func _internal_uploadStoryImpl(postbox: Postbox, network: Network, accountPeerId
isPinned: item.isPinned, isPinned: item.isPinned,
isExpired: item.isExpired, isExpired: item.isExpired,
isPublic: item.isPublic, isPublic: item.isPublic,
isCloseFriends: item.isCloseFriends isCloseFriends: item.isCloseFriends,
isForwardingDisabled: item.isForwardingDisabled
) )
if let entry = CodableEntry(Stories.StoredItem.item(updatedItem)) { if let entry = CodableEntry(Stories.StoredItem.item(updatedItem)) {
items.append(StoryItemsTableEntry(value: entry, id: item.id, expirationTimestamp: updatedItem.expirationTimestamp)) 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, isPinned: item.isPinned,
isExpired: item.isExpired, isExpired: item.isExpired,
isPublic: item.isPublic, isPublic: item.isPublic,
isCloseFriends: item.isCloseFriends isCloseFriends: item.isCloseFriends,
isForwardingDisabled: item.isForwardingDisabled
) )
if let entry = CodableEntry(Stories.StoredItem.item(updatedItem)) { if let entry = CodableEntry(Stories.StoredItem.item(updatedItem)) {
transaction.setStory(id: storyId, value: entry) transaction.setStory(id: storyId, value: entry)
@ -1005,7 +1021,8 @@ func _internal_editStoryPrivacy(account: Account, id: Int32, privacy: EngineStor
isPinned: item.isPinned, isPinned: item.isPinned,
isExpired: item.isExpired, isExpired: item.isExpired,
isPublic: item.isPublic, isPublic: item.isPublic,
isCloseFriends: item.isCloseFriends isCloseFriends: item.isCloseFriends,
isForwardingDisabled: item.isForwardingDisabled
) )
if let entry = CodableEntry(Stories.StoredItem.item(updatedItem)) { if let entry = CodableEntry(Stories.StoredItem.item(updatedItem)) {
items[index] = StoryItemsTableEntry(value: entry, id: item.id, expirationTimestamp: updatedItem.expirationTimestamp) 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, isPinned: isPinned,
isExpired: item.isExpired, isExpired: item.isExpired,
isPublic: item.isPublic, isPublic: item.isPublic,
isCloseFriends: item.isCloseFriends isCloseFriends: item.isCloseFriends,
isForwardingDisabled: item.isForwardingDisabled
) )
if let entry = CodableEntry(Stories.StoredItem.item(updatedItem)) { if let entry = CodableEntry(Stories.StoredItem.item(updatedItem)) {
items[index] = StoryItemsTableEntry(value: entry, id: item.id, expirationTimestamp: updatedItem.expirationTimestamp) 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, isPinned: isPinned,
isExpired: item.isExpired, isExpired: item.isExpired,
isPublic: item.isPublic, isPublic: item.isPublic,
isCloseFriends: item.isCloseFriends isCloseFriends: item.isCloseFriends,
isForwardingDisabled: item.isForwardingDisabled
) )
updatedItems.append(updatedItem) updatedItems.append(updatedItem)
} }
@ -1253,6 +1272,7 @@ extension Stories.StoredItem {
let isExpired = (flags & (1 << 6)) != 0 let isExpired = (flags & (1 << 6)) != 0
let isPublic = (flags & (1 << 7)) != 0 let isPublic = (flags & (1 << 7)) != 0
let isCloseFriends = (flags & (1 << 8)) != 0 let isCloseFriends = (flags & (1 << 8)) != 0
let isForwardingDisabled = (flags & (1 << 10)) != 0
let item = Stories.Item( let item = Stories.Item(
id: id, id: id,
@ -1266,7 +1286,8 @@ extension Stories.StoredItem {
isPinned: isPinned, isPinned: isPinned,
isExpired: isExpired, isExpired: isExpired,
isPublic: isPublic, isPublic: isPublic,
isCloseFriends: isCloseFriends isCloseFriends: isCloseFriends,
isForwardingDisabled: isForwardingDisabled
) )
self = .item(item) self = .item(item)
} else { } else {

View File

@ -44,8 +44,9 @@ public final class EngineStoryItem: Equatable {
public let isPublic: Bool public let isPublic: Bool
public let isPending: Bool public let isPending: Bool
public let isCloseFriends: 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.id = id
self.timestamp = timestamp self.timestamp = timestamp
self.expirationTimestamp = expirationTimestamp self.expirationTimestamp = expirationTimestamp
@ -59,6 +60,7 @@ public final class EngineStoryItem: Equatable {
self.isPublic = isPublic self.isPublic = isPublic
self.isPending = isPending self.isPending = isPending
self.isCloseFriends = isCloseFriends self.isCloseFriends = isCloseFriends
self.isForwardingDisabled = isForwardingDisabled
} }
public static func ==(lhs: EngineStoryItem, rhs: EngineStoryItem) -> Bool { public static func ==(lhs: EngineStoryItem, rhs: EngineStoryItem) -> Bool {
@ -101,6 +103,9 @@ public final class EngineStoryItem: Equatable {
if lhs.isCloseFriends != rhs.isCloseFriends { if lhs.isCloseFriends != rhs.isCloseFriends {
return false return false
} }
if lhs.isForwardingDisabled != rhs.isForwardingDisabled {
return false
}
return true return true
} }
} }
@ -129,7 +134,8 @@ extension EngineStoryItem {
isPinned: self.isPinned, isPinned: self.isPinned,
isExpired: self.isExpired, isExpired: self.isExpired,
isPublic: self.isPublic, isPublic: self.isPublic,
isCloseFriends: self.isCloseFriends isCloseFriends: self.isCloseFriends,
isForwardingDisabled: self.isForwardingDisabled
) )
} }
} }
@ -487,7 +493,8 @@ public final class PeerStoryListContext {
isExpired: item.isExpired, isExpired: item.isExpired,
isPublic: item.isPublic, isPublic: item.isPublic,
isPending: false, isPending: false,
isCloseFriends: item.isCloseFriends isCloseFriends: item.isCloseFriends,
isForwardingDisabled: item.isForwardingDisabled
) )
items.append(mappedItem) items.append(mappedItem)
} }
@ -593,7 +600,8 @@ public final class PeerStoryListContext {
isExpired: item.isExpired, isExpired: item.isExpired,
isPublic: item.isPublic, isPublic: item.isPublic,
isPending: false, isPending: false,
isCloseFriends: item.isCloseFriends isCloseFriends: item.isCloseFriends,
isForwardingDisabled: item.isForwardingDisabled
) )
storyItems.append(mappedItem) storyItems.append(mappedItem)
} }
@ -726,7 +734,8 @@ public final class PeerStoryListContext {
isExpired: item.isExpired, isExpired: item.isExpired,
isPublic: item.isPublic, isPublic: item.isPublic,
isPending: false, isPending: false,
isCloseFriends: item.isCloseFriends isCloseFriends: item.isCloseFriends,
isForwardingDisabled: item.isForwardingDisabled
) )
finalUpdatedState = updatedState finalUpdatedState = updatedState
} }
@ -764,7 +773,8 @@ public final class PeerStoryListContext {
isExpired: item.isExpired, isExpired: item.isExpired,
isPublic: item.isPublic, isPublic: item.isPublic,
isPending: false, isPending: false,
isCloseFriends: item.isCloseFriends isCloseFriends: item.isCloseFriends,
isForwardingDisabled: item.isForwardingDisabled
)) ))
updatedState.items.sort(by: { lhs, rhs in updatedState.items.sort(by: { lhs, rhs in
return lhs.timestamp > rhs.timestamp return lhs.timestamp > rhs.timestamp
@ -911,7 +921,8 @@ public final class PeerExpiringStoryListContext {
isExpired: item.isExpired, isExpired: item.isExpired,
isPublic: item.isPublic, isPublic: item.isPublic,
isPending: false, isPending: false,
isCloseFriends: item.isCloseFriends isCloseFriends: item.isCloseFriends,
isForwardingDisabled: item.isForwardingDisabled
) )
items.append(.item(mappedItem)) items.append(.item(mappedItem))
} }

View File

@ -966,7 +966,8 @@ public extension TelegramEngine {
isPinned: item.isPinned, isPinned: item.isPinned,
isExpired: item.isExpired, isExpired: item.isExpired,
isPublic: item.isPublic, isPublic: item.isPublic,
isCloseFriends: item.isCloseFriends isCloseFriends: item.isCloseFriends,
isForwardingDisabled: item.isForwardingDisabled
)) ))
if let entry = CodableEntry(updatedItem) { if let entry = CodableEntry(updatedItem) {
currentItems[i] = StoryItemsTableEntry(value: entry, id: updatedItem.id, expirationTimestamp: updatedItem.expirationTimestamp) 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) { 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, period: period, randomId: randomId) _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? { public func lookUpPendingStoryIdMapping(stableId: Int32) -> Int32? {

View File

@ -214,7 +214,7 @@ public final class ChatAvatarNavigationNode: ASDisplayNode {
component: AnyComponent(AvatarStoryIndicatorComponent( component: AnyComponent(AvatarStoryIndicatorComponent(
hasUnseen: storyData.hasUnseen, hasUnseen: storyData.hasUnseen,
hasUnseenCloseFriendsItems: storyData.hasUnseenCloseFriends, hasUnseenCloseFriendsItems: storyData.hasUnseenCloseFriends,
isDarkTheme: theme.overallDarkAppearance, theme: theme,
activeLineWidth: 1.0, activeLineWidth: 1.0,
inactiveLineWidth: 1.0, inactiveLineWidth: 1.0,
counters: nil counters: nil

View File

@ -312,6 +312,7 @@ public final class ChatListHeaderComponent: Component {
var contentOffsetFraction: CGFloat = 0.0 var contentOffsetFraction: CGFloat = 0.0
private(set) var centerContentWidth: 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 centerContentRightInset: CGFloat = 0.0
private(set) var centerContentOffsetX: 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 var centerContentRightInset: CGFloat = 0.0
centerContentRightInset = size.width - rightOffset + 16.0 centerContentRightInset = size.width - rightOffset - 8.0
var centerContentWidth: CGFloat = 0.0 var centerContentWidth: CGFloat = 0.0
var centerContentOffsetX: CGFloat = 0.0 var centerContentOffsetX: CGFloat = 0.0
@ -705,6 +709,7 @@ public final class ChatListHeaderComponent: Component {
self.centerContentOffsetX = centerContentOffsetX self.centerContentOffsetX = centerContentOffsetX
self.centerContentOrigin = centerContentOrigin self.centerContentOrigin = centerContentOrigin
self.centerContentRightInset = centerContentRightInset self.centerContentRightInset = centerContentRightInset
self.centerContentLeftInset = centerContentLeftInset
} }
} }
@ -815,7 +820,7 @@ public final class ChatListHeaderComponent: Component {
let previousComponent = self.component let previousComponent = self.component
self.component = component self.component = component
if let primaryContent = component.primaryContent { if var primaryContent = component.primaryContent {
var primaryContentTransition = transition var primaryContentTransition = transition
let primaryContentView: ContentView let primaryContentView: ContentView
if let current = self.primaryContentView { if let current = self.primaryContentView {
@ -848,6 +853,19 @@ public final class ChatListHeaderComponent: Component {
let sideContentWidth: CGFloat = 0.0 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) 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)) primaryContentTransition.setFrame(view: primaryContentView, frame: CGRect(origin: CGPoint(), size: availableSize))
@ -868,6 +886,28 @@ public final class ChatListHeaderComponent: Component {
self.storyPeerList = storyPeerList 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( let _ = storyPeerList.update(
transition: storyListTransition, transition: storyListTransition,
component: AnyComponent(StoryPeerListComponent( component: AnyComponent(StoryPeerListComponent(
@ -876,7 +916,11 @@ public final class ChatListHeaderComponent: Component {
theme: component.theme, theme: component.theme,
strings: component.strings, strings: component.strings,
sideInset: component.sideInset, 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), maxTitleX: availableSize.width - (self.primaryContentView?.centerContentRightInset ?? 0.0),
useHiddenList: component.storiesIncludeHidden, useHiddenList: component.storiesIncludeHidden,
storySubscriptions: storySubscriptions, storySubscriptions: storySubscriptions,
@ -895,14 +939,11 @@ public final class ChatListHeaderComponent: Component {
} }
self.storyContextPeerAction?(sourceNode, gesture, peer) self.storyContextPeerAction?(sourceNode, gesture, peer)
}, },
updateTitleContentOffset: { [weak self] offset, transition in openStatusSetup: { [weak self] sourceView in
guard let self, let primaryContentView = self.primaryContentView else { guard let self else {
return return
} }
guard let chatListTitleView = primaryContentView.chatListTitleView else { self.component?.openStatusSetup(sourceView)
return
}
transition.setSublayerTransform(view: chatListTitleView, transform: CATransform3DMakeTranslation(offset, 0.0, 0.0))
} }
)), )),
environment: {}, 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))) 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 let storyListNormalAlpha: CGFloat = 1.0
if let chatListTitle = component.primaryContent?.chatListTitle {
if chatListTitle.activity {
storyListNormalAlpha = component.storiesFraction
}
}
let storyListAlpha: CGFloat = (1.0 - component.secondaryTransition) * storyListNormalAlpha let storyListAlpha: CGFloat = (1.0 - component.secondaryTransition) * storyListNormalAlpha
storyListTransition.setAlpha(view: storyPeerListComponentView, alpha: storyListAlpha) storyListTransition.setAlpha(view: storyPeerListComponentView, alpha: storyListAlpha)

View File

@ -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",
],
)

View File

@ -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)
}
}

View File

@ -131,7 +131,8 @@ final class PeerInfoStoryGridScreenComponent: Component {
paneNode.clearSelection() paneNode.clearSelection()
}))) })))
} else { } else if let paneNode = self.paneNode {
if !paneNode.isEmpty {
var recurseGenerateAction: ((Bool) -> ContextMenuActionItem)? var recurseGenerateAction: ((Bool) -> ContextMenuActionItem)?
let generateAction: (Bool) -> ContextMenuActionItem = { [weak pane] isZoomIn in let generateAction: (Bool) -> ContextMenuActionItem = { [weak pane] isZoomIn in
let nextZoomLevel = isZoomIn ? pane?.availableZoomLevels().increment : pane?.availableZoomLevels().decrement let nextZoomLevel = isZoomIn ? pane?.availableZoomLevels().increment : pane?.availableZoomLevels().decrement
@ -156,6 +157,7 @@ final class PeerInfoStoryGridScreenComponent: Component {
items.append(.action(generateAction(true))) items.append(.action(generateAction(true)))
items.append(.action(generateAction(false))) items.append(.action(generateAction(false)))
}
if component.peerId == component.context.account.peerId, case .saved = component.scope { if component.peerId == component.context.account.peerId, case .saved = component.scope {
var ignoreNextActions = false var ignoreNextActions = false
@ -392,6 +394,13 @@ final class PeerInfoStoryGridScreenComponent: Component {
self.paneNode = paneNode self.paneNode = paneNode
self.addSubview(paneNode.view) 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 self.paneStatusDisposable = (paneNode.status
|> deliverOnMainQueue).start(next: { [weak self] status in |> deliverOnMainQueue).start(next: { [weak self] status in
guard let self else { guard let self else {

View File

@ -36,6 +36,7 @@ swift_library(
"//submodules/InvisibleInkDustNode", "//submodules/InvisibleInkDustNode",
"//submodules/MediaPickerUI", "//submodules/MediaPickerUI",
"//submodules/TelegramUI/Components/Stories/StoryContainerScreen", "//submodules/TelegramUI/Components/Stories/StoryContainerScreen",
"//submodules/TelegramUI/Components/EmptyStateIndicatorComponent",
], ],
visibility = [ visibility = [
"//visibility:public", "//visibility:public",

View File

@ -26,6 +26,7 @@ import AppBundle
import InvisibleInkDustNode import InvisibleInkDustNode
import MediaPickerUI import MediaPickerUI
import StoryContainerScreen import StoryContainerScreen
import EmptyStateIndicatorComponent
private let mediaBadgeBackgroundColor = UIColor(white: 0.0, alpha: 0.6) private let mediaBadgeBackgroundColor = UIColor(white: 0.0, alpha: 0.6)
private let mediaBadgeTextColor = UIColor.white private let mediaBadgeTextColor = UIColor.white
@ -849,6 +850,14 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr
return result 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 var currentParams: (size: CGSize, topInset: CGFloat, sideInset: CGFloat, bottomInset: CGFloat, visibleHeight: CGFloat, isScrollingLockedAtTop: Bool, expandProgress: CGFloat, presentationData: PresentationData)?
private let ready = Promise<Bool>() private let ready = Promise<Bool>()
@ -883,6 +892,7 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr
public var openCurrentDate: (() -> Void)? public var openCurrentDate: (() -> Void)?
public var paneDidScroll: (() -> Void)? public var paneDidScroll: (() -> Void)?
public var emptyAction: (() -> Void)?
private weak var currentGestureItem: SparseItemGridDisplayItem? private weak var currentGestureItem: SparseItemGridDisplayItem?
@ -893,6 +903,8 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr
private var preloadArchiveListContext: PeerStoryListContext? 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?) { public init(context: AccountContext, peerId: PeerId, chatLocation: ChatLocation, contentType: ContentType, captureProtected: Bool, isSaved: Bool, isArchive: Bool, navigationController: @escaping () -> NavigationController?, listContext: PeerStoryListContext?) {
self.context = context self.context = context
self.peerId = peerId self.peerId = peerId
@ -1864,6 +1876,67 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr
transition.updateFrame(node: self.contextGestureContainerNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: size.height))) 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))) 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 { if let items = self.items {
let wasFirstTime = !self.didUpdateItemsOnce let wasFirstTime = !self.didUpdateItemsOnce

View File

@ -17,7 +17,7 @@ public final class AvatarStoryIndicatorComponent: Component {
public let hasUnseen: Bool public let hasUnseen: Bool
public let hasUnseenCloseFriendsItems: Bool public let hasUnseenCloseFriendsItems: Bool
public let isDarkTheme: Bool public let theme: PresentationTheme
public let activeLineWidth: CGFloat public let activeLineWidth: CGFloat
public let inactiveLineWidth: CGFloat public let inactiveLineWidth: CGFloat
public let counters: Counters? public let counters: Counters?
@ -25,14 +25,14 @@ public final class AvatarStoryIndicatorComponent: Component {
public init( public init(
hasUnseen: Bool, hasUnseen: Bool,
hasUnseenCloseFriendsItems: Bool, hasUnseenCloseFriendsItems: Bool,
isDarkTheme: Bool, theme: PresentationTheme,
activeLineWidth: CGFloat, activeLineWidth: CGFloat,
inactiveLineWidth: CGFloat, inactiveLineWidth: CGFloat,
counters: Counters? counters: Counters?
) { ) {
self.hasUnseen = hasUnseen self.hasUnseen = hasUnseen
self.hasUnseenCloseFriendsItems = hasUnseenCloseFriendsItems self.hasUnseenCloseFriendsItems = hasUnseenCloseFriendsItems
self.isDarkTheme = isDarkTheme self.theme = theme
self.activeLineWidth = activeLineWidth self.activeLineWidth = activeLineWidth
self.inactiveLineWidth = inactiveLineWidth self.inactiveLineWidth = inactiveLineWidth
self.counters = counters self.counters = counters
@ -45,7 +45,7 @@ public final class AvatarStoryIndicatorComponent: Component {
if lhs.hasUnseenCloseFriendsItems != rhs.hasUnseenCloseFriendsItems { if lhs.hasUnseenCloseFriendsItems != rhs.hasUnseenCloseFriendsItems {
return false return false
} }
if lhs.isDarkTheme != rhs.isDarkTheme { if lhs.theme !== rhs.theme {
return false return false
} }
if lhs.activeLineWidth != rhs.activeLineWidth { if lhs.activeLineWidth != rhs.activeLineWidth {
@ -112,8 +112,8 @@ public final class AvatarStoryIndicatorComponent: Component {
] ]
} }
if component.isDarkTheme { if component.theme.overallDarkAppearance {
inactiveColors = [UIColor(rgb: 0x48484A).cgColor, UIColor(rgb: 0x48484A).cgColor] inactiveColors = [component.theme.rootController.tabBar.textColor.cgColor, component.theme.rootController.tabBar.textColor.cgColor]
} else { } else {
inactiveColors = [UIColor(rgb: 0xD8D8E1).cgColor, UIColor(rgb: 0xD8D8E1).cgColor] inactiveColors = [UIColor(rgb: 0xD8D8E1).cgColor, UIColor(rgb: 0xD8D8E1).cgColor]
} }

View File

@ -138,7 +138,8 @@ public final class StoryContentContextImpl: StoryContentContext {
isExpired: item.isExpired, isExpired: item.isExpired,
isPublic: item.isPublic, isPublic: item.isPublic,
isPending: false, 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) { 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, isExpired: false,
isPublic: false, isPublic: false,
isPending: true, isPending: true,
isCloseFriends: false isCloseFriends: false,
isForwardingDisabled: false
)) ))
} }
} }
@ -954,7 +956,8 @@ public final class SingleStoryContentContextImpl: StoryContentContext {
isExpired: itemValue.isExpired, isExpired: itemValue.isExpired,
isPublic: itemValue.isPublic, isPublic: itemValue.isPublic,
isPending: false, isPending: false,
isCloseFriends: itemValue.isCloseFriends isCloseFriends: itemValue.isCloseFriends,
isForwardingDisabled: itemValue.isForwardingDisabled
) )
let mainItem = StoryContentItem( let mainItem = StoryContentItem(

View File

@ -392,7 +392,22 @@ private final class StoryContainerScreenComponent: Component {
let translation = recognizer.translation(in: self) let translation = recognizer.translation(in: self)
self.verticalPanState = ItemSetPanState(fraction: max(-1.0, min(1.0, translation.y / self.bounds.height)), didBegin: true) self.verticalPanState = ItemSetPanState(fraction: max(-1.0, min(1.0, translation.y / self.bounds.height)), didBegin: true)
self.state?.updated(transition: .immediate) self.state?.updated(transition: .immediate)
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 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: case .cancelled, .ended:
if self.verticalPanState != nil {
let translation = recognizer.translation(in: self) let translation = recognizer.translation(in: self)
let velocity = recognizer.velocity(in: self) let velocity = recognizer.velocity(in: self)
@ -417,6 +432,7 @@ private final class StoryContainerScreenComponent: Component {
} else { } else {
self.state?.updated(transition: Transition(animation: .curve(duration: 0.3, curve: .spring))) self.state?.updated(transition: Transition(animation: .curve(duration: 0.3, curve: .spring)))
} }
}
default: default:
break break
} }

View File

@ -141,7 +141,7 @@ final class StoryItemContentComponent: Component {
useLargeThumbnail: true, useLargeThumbnail: true,
autoFetchFullSizeThumbnail: true, autoFetchFullSizeThumbnail: true,
tempFilePath: nil, tempFilePath: nil,
captureProtected: false, captureProtected: component.item.isForwardingDisabled,
hintDimensions: file.dimensions?.cgSize, hintDimensions: file.dimensions?.cgSize,
storeAfterDownload: nil, storeAfterDownload: nil,
displayImage: false displayImage: false
@ -487,9 +487,17 @@ final class StoryItemContentComponent: Component {
} }
if let dimensions { 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( let apply = self.imageNode.asyncLayout()(TransformImageArguments(
corners: ImageCorners(), corners: ImageCorners(),
imageSize: dimensions.aspectFilled(availableSize), imageSize: imageSize,
boundingSize: availableSize, boundingSize: availableSize,
intrinsicInsets: UIEdgeInsets() intrinsicInsets: UIEdgeInsets()
)) ))

View File

@ -30,6 +30,7 @@ import LocalMediaResources
import SaveToCameraRoll import SaveToCameraRoll
import BundleIconComponent import BundleIconComponent
import PeerListItemComponent import PeerListItemComponent
import PremiumUI
public final class StoryItemSetContainerComponent: Component { public final class StoryItemSetContainerComponent: Component {
public final class ExternalState { public final class ExternalState {
@ -182,16 +183,16 @@ public final class StoryItemSetContainerComponent: Component {
} }
struct ItemLayout { struct ItemLayout {
var size: CGSize var containerSize: CGSize
var contentFrame: CGRect var contentFrame: CGRect
var contentVisualScale: CGFloat var contentVisualScale: CGFloat
init( init(
size: CGSize, containerSize: CGSize,
contentFrame: CGRect, contentFrame: CGRect,
contentVisualScale: CGFloat contentVisualScale: CGFloat
) { ) {
self.size = size self.containerSize = containerSize
self.contentFrame = contentFrame self.contentFrame = contentFrame
self.contentVisualScale = contentVisualScale self.contentVisualScale = contentVisualScale
} }
@ -256,10 +257,6 @@ public final class StoryItemSetContainerComponent: Component {
override func touchesShouldCancel(in view: UIView) -> Bool { override func touchesShouldCancel(in view: UIView) -> Bool {
return true return true
} }
/*@objc func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return false
}*/
} }
public final class View: UIView, UIScrollViewDelegate, UIGestureRecognizerDelegate { public final class View: UIView, UIScrollViewDelegate, UIGestureRecognizerDelegate {
@ -567,7 +564,7 @@ public final class StoryItemSetContainerComponent: Component {
let point = recognizer.location(in: self) let point = recognizer.location(in: self)
var direction: NavigationDirection? var direction: NavigationDirection?
if point.x < itemLayout.size.width * 0.25 { if point.x < itemLayout.containerSize.width * 0.25 {
direction = .previous direction = .previous
} else { } else {
direction = .next direction = .next
@ -670,7 +667,7 @@ public final class StoryItemSetContainerComponent: Component {
} }
self.scrollingCenterX = leftWidth 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 { if !self.initializedOffset {
self.initializedOffset = true self.initializedOffset = true
@ -862,7 +859,7 @@ public final class StoryItemSetContainerComponent: Component {
environment: { environment: {
itemEnvironment itemEnvironment
}, },
containerSize: itemLayout.size containerSize: itemLayout.contentFrame.size
) )
if let view = visibleItem.view.view { if let view = visibleItem.view.view {
if visibleItem.contentContainerView.superview == nil { if visibleItem.contentContainerView.superview == nil {
@ -969,6 +966,22 @@ public final class StoryItemSetContainerComponent: Component {
return false 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) { func animateIn(transitionIn: StoryContainerScreen.TransitionIn) {
self.closeButton.layer.animateScale(from: 0.001, to: 1.0, duration: 0.2, delay: 0.12, timingFunction: kCAMediaTimingFunctionSpring) 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) self.state?.updated(transition: .immediate)
}, },
timeoutAction: nil, 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 { guard let self else {
return 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 contentFrame = CGRect(origin: CGPoint(x: 0.0, y: component.containerInsets.top - (contentSize.height - contentVisualHeight) * 0.5), size: contentSize)
let itemLayout = ItemLayout( let itemLayout = ItemLayout(
size: contentFrame.size, containerSize: availableSize,
contentFrame: contentFrame, contentFrame: contentFrame,
contentVisualScale: contentVisualScale 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 var animateReactionsIn = false

View File

@ -22,6 +22,8 @@ swift_library(
"//submodules/ContextUI", "//submodules/ContextUI",
"//submodules/TelegramUI/Components/Stories/StoryContainerScreen", "//submodules/TelegramUI/Components/Stories/StoryContainerScreen",
"//submodules/Components/MultilineTextComponent", "//submodules/Components/MultilineTextComponent",
"//submodules/ActivityIndicator",
"//submodules/TelegramUI/Components/EmojiStatusComponent",
], ],
visibility = [ visibility = [
"//visibility:public", "//visibility:public",

View File

@ -10,6 +10,7 @@ import Postbox
import SwiftSignalKit import SwiftSignalKit
import TelegramPresentationData import TelegramPresentationData
import StoryContainerScreen import StoryContainerScreen
import EmojiStatusComponent
public func shouldDisplayStoriesInChatListHeader(storySubscriptions: EngineStorySubscriptions) -> Bool { public func shouldDisplayStoriesInChatListHeader(storySubscriptions: EngineStorySubscriptions) -> Bool {
if !storySubscriptions.items.isEmpty { if !storySubscriptions.items.isEmpty {
@ -48,6 +49,11 @@ private let modelSpringAnimation: CABasicAnimation = {
}() }()
public final class StoryPeerListComponent: Component { public final class StoryPeerListComponent: Component {
public enum PeerStatus: Equatable {
case premium
case emoji(PeerEmojiStatus)
}
public final class ExternalState { public final class ExternalState {
public fileprivate(set) var collapsedWidth: CGFloat = 0.0 public fileprivate(set) var collapsedWidth: CGFloat = 0.0
@ -74,7 +80,11 @@ public final class StoryPeerListComponent: Component {
public let theme: PresentationTheme public let theme: PresentationTheme
public let strings: PresentationStrings public let strings: PresentationStrings
public let sideInset: CGFloat 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 maxTitleX: CGFloat
public let useHiddenList: Bool public let useHiddenList: Bool
public let storySubscriptions: EngineStorySubscriptions? public let storySubscriptions: EngineStorySubscriptions?
@ -83,7 +93,7 @@ public final class StoryPeerListComponent: Component {
public let uploadProgress: Float? public let uploadProgress: Float?
public let peerAction: (EnginePeer?) -> Void public let peerAction: (EnginePeer?) -> Void
public let contextPeerAction: (ContextExtractedContentContainingNode, ContextGesture, EnginePeer) -> Void public let contextPeerAction: (ContextExtractedContentContainingNode, ContextGesture, EnginePeer) -> Void
public let updateTitleContentOffset: (CGFloat, Transition) -> Void public let openStatusSetup: (UIView) -> Void
public init( public init(
externalState: ExternalState, externalState: ExternalState,
@ -91,7 +101,11 @@ public final class StoryPeerListComponent: Component {
theme: PresentationTheme, theme: PresentationTheme,
strings: PresentationStrings, strings: PresentationStrings,
sideInset: CGFloat, sideInset: CGFloat,
titleContentWidth: CGFloat, title: String,
titleHasLock: Bool,
titleHasActivity: Bool,
titlePeerStatus: PeerStatus?,
minTitleX: CGFloat,
maxTitleX: CGFloat, maxTitleX: CGFloat,
useHiddenList: Bool, useHiddenList: Bool,
storySubscriptions: EngineStorySubscriptions?, storySubscriptions: EngineStorySubscriptions?,
@ -100,14 +114,18 @@ public final class StoryPeerListComponent: Component {
uploadProgress: Float?, uploadProgress: Float?,
peerAction: @escaping (EnginePeer?) -> Void, peerAction: @escaping (EnginePeer?) -> Void,
contextPeerAction: @escaping (ContextExtractedContentContainingNode, ContextGesture, EnginePeer) -> Void, contextPeerAction: @escaping (ContextExtractedContentContainingNode, ContextGesture, EnginePeer) -> Void,
updateTitleContentOffset: @escaping (CGFloat, Transition) -> Void openStatusSetup: @escaping (UIView) -> Void
) { ) {
self.externalState = externalState self.externalState = externalState
self.context = context self.context = context
self.theme = theme self.theme = theme
self.strings = strings self.strings = strings
self.sideInset = sideInset 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.maxTitleX = maxTitleX
self.useHiddenList = useHiddenList self.useHiddenList = useHiddenList
self.storySubscriptions = storySubscriptions self.storySubscriptions = storySubscriptions
@ -116,7 +134,7 @@ public final class StoryPeerListComponent: Component {
self.uploadProgress = uploadProgress self.uploadProgress = uploadProgress
self.peerAction = peerAction self.peerAction = peerAction
self.contextPeerAction = contextPeerAction self.contextPeerAction = contextPeerAction
self.updateTitleContentOffset = updateTitleContentOffset self.openStatusSetup = openStatusSetup
} }
public static func ==(lhs: StoryPeerListComponent, rhs: StoryPeerListComponent) -> Bool { public static func ==(lhs: StoryPeerListComponent, rhs: StoryPeerListComponent) -> Bool {
@ -132,7 +150,19 @@ public final class StoryPeerListComponent: Component {
if lhs.sideInset != rhs.sideInset { if lhs.sideInset != rhs.sideInset {
return false 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 return false
} }
if lhs.maxTitleX != rhs.maxTitleX { if lhs.maxTitleX != rhs.maxTitleX {
@ -195,9 +225,20 @@ public final class StoryPeerListComponent: Component {
} }
func frame(at index: Int) -> CGRect { func frame(at index: Int) -> CGRect {
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) return CGRect(origin: CGPoint(x: self.containerInsets.left + (self.itemSize.width + self.itemSpacing) * CGFloat(index), y: self.containerInsets.top), size: self.itemSize)
} }
} }
}
private final class AnimationState { private final class AnimationState {
let duration: Double let duration: Double
@ -240,12 +281,19 @@ public final class StoryPeerListComponent: Component {
public final class View: UIView, UIScrollViewDelegate { public final class View: UIView, UIScrollViewDelegate {
private let collapsedButton: HighlightableButton private let collapsedButton: HighlightableButton
private let scrollView: ScrollView private let scrollView: ScrollView
private let scrollContainerView: UIView
private var ignoreScrolling: Bool = false private var ignoreScrolling: Bool = false
private var itemLayout: ItemLayout? private var itemLayout: ItemLayout?
private var sortedItems: [EngineStorySubscriptions.Item] = [] private var sortedItems: [EngineStorySubscriptions.Item] = []
private var visibleItems: [EnginePeer.Id: VisibleItem] = [:] 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 var component: StoryPeerListComponent?
private weak var state: EmptyComponentState? private weak var state: EmptyComponentState?
@ -275,10 +323,15 @@ public final class StoryPeerListComponent: Component {
self.scrollView.alwaysBounceHorizontal = true self.scrollView.alwaysBounceHorizontal = true
self.scrollView.clipsToBounds = false self.scrollView.clipsToBounds = false
self.scrollContainerView = UIView()
super.init(frame: frame) super.init(frame: frame)
self.scrollView.delegate = self self.scrollView.delegate = self
self.scrollView.alpha = 0.0
self.scrollContainerView.addGestureRecognizer(self.scrollView.panGestureRecognizer)
self.addSubview(self.scrollView) self.addSubview(self.scrollView)
self.addSubview(self.scrollContainerView)
self.addSubview(self.collapsedButton) self.addSubview(self.collapsedButton)
self.collapsedButton.highligthedChanged = { [weak self] highlighted in self.collapsedButton.highligthedChanged = { [weak self] highlighted in
@ -373,20 +426,116 @@ public final class StoryPeerListComponent: Component {
public func scrollViewDidScroll(_ scrollView: UIScrollView) { public func scrollViewDidScroll(_ scrollView: UIScrollView) {
if !self.ignoreScrolling { 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 { guard let component = self.component, let itemLayout = self.itemLayout else {
return return
} }
var hasStories: Bool = false let titleIconSpacing: CGFloat = 4.0
if let storySubscriptions = component.storySubscriptions, shouldDisplayStoriesInChatListHeader(storySubscriptions: storySubscriptions) { let titleIndicatorSpacing: CGFloat = 8.0
hasStories = true
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 let collapseStartIndex: Int
if component.useHiddenList { if component.useHiddenList {
@ -415,13 +564,28 @@ public final class StoryPeerListComponent: Component {
let collapsedItemOffsetY: CGFloat let collapsedItemOffsetY: CGFloat
let titleContentSpacing: CGFloat = 8.0 let titleContentSpacing: CGFloat = 8.0
var combinedTitleContentWidth = component.titleContentWidth var combinedTitleContentWidth = titleContentWidth
if !combinedTitleContentWidth.isZero { if !combinedTitleContentWidth.isZero {
combinedTitleContentWidth += titleContentSpacing combinedTitleContentWidth += titleContentSpacing
} }
let centralContentWidth: CGFloat = collapsedContentWidth + combinedTitleContentWidth
let centralContentWidth: CGFloat
centralContentWidth = collapsedContentWidth + combinedTitleContentWidth
collapsedContentOrigin = floor((itemLayout.containerSize.width - centralContentWidth) * 0.5) collapsedContentOrigin = floor((itemLayout.containerSize.width - centralContentWidth) * 0.5)
if component.titleHasActivity {
collapsedContentOrigin -= (collapsedContentWidth + titleContentSpacing) * 0.5
}
collapsedContentOrigin = min(collapsedContentOrigin, component.maxTitleX - centralContentWidth - 4.0) 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 collapsedItemOffsetY = -59.0
struct CollapseState { struct CollapseState {
@ -432,11 +596,6 @@ public final class StoryPeerListComponent: Component {
var sideAlphaFraction: CGFloat 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 targetExpandedFraction = component.collapseFraction
let targetFraction: CGFloat = component.collapseFraction let targetFraction: CGFloat = component.collapseFraction
@ -504,7 +663,7 @@ public final class StoryPeerListComponent: Component {
var rawProgress = CGFloat((timestamp - animationState.startTime) / animationState.duration) var rawProgress = CGFloat((timestamp - animationState.startTime) / animationState.duration)
rawProgress = max(0.0, min(1.0, rawProgress)) 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) expandBoundsFraction = animationState.interpolatedFraction(at: timestamp, effectiveFromFraction: 1.0, toFraction: 0.0)
} else { } else {
expandBoundsFraction = 0.0 expandBoundsFraction = 0.0
@ -520,15 +679,6 @@ public final class StoryPeerListComponent: Component {
expandBoundsFraction = 0.0 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 self.currentFraction = collapsedState.globalFraction
component.externalState.collapsedWidth = collapsedContentWidth component.externalState.collapsedWidth = collapsedContentWidth
@ -536,32 +686,47 @@ public final class StoryPeerListComponent: Component {
let effectiveVisibleBounds = self.scrollView.bounds let effectiveVisibleBounds = self.scrollView.bounds
let visibleBounds = effectiveVisibleBounds.insetBy(dx: -200.0, dy: 0.0) 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 { struct MeasuredItem {
var itemFrame: CGRect var itemFrame: CGRect
var itemScale: CGFloat var itemScale: CGFloat
} }
let calculateItem: (Int) -> MeasuredItem = { i in let calculateItem: (Int) -> MeasuredItem = { index in
let regularItemFrame = itemLayout.frame(at: i) let frameIndex = index
let regularItemFrame = itemLayout.frame(at: frameIndex)
let isReallyVisible = effectiveVisibleBounds.intersects(regularItemFrame) let isReallyVisible = effectiveVisibleBounds.intersects(regularItemFrame)
let collapseIndex = index - effectiveFirstVisibleIndex
let collapsedItemX: CGFloat let collapsedItemX: CGFloat
if i < collapseStartIndex { if collapseIndex < collapseStartIndex {
collapsedItemX = collapsedContentOrigin collapsedItemX = collapsedContentOrigin
} else if i > collapseEndIndex { } else if collapseIndex > collapseEndIndex {
collapsedItemX = collapsedContentOrigin + CGFloat(collapseEndIndex) * collapsedItemDistance - collapsedItemWidth * 0.5 collapsedItemX = collapsedContentOrigin + CGFloat(collapseEndIndex) * collapsedItemDistance - collapsedItemWidth * 0.5
} else { } 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)) let collapsedItemFrame = CGRect(origin: CGPoint(x: collapsedItemX, y: regularItemFrame.minY + collapsedItemOffsetY), size: CGSize(width: collapsedItemWidth, height: regularItemFrame.height))
var collapsedMaxItemFrame = collapsedItemFrame var collapsedMaxItemFrame = collapsedItemFrame
var collapseDistance: CGFloat = CGFloat(i - collapseStartIndex) / CGFloat(collapseEndIndex - collapseStartIndex) if itemLayout.itemCount > 1 {
var collapseDistance: CGFloat = CGFloat(collapseIndex - collapseStartIndex) / CGFloat(collapseEndIndex - collapseStartIndex)
collapseDistance = max(0.0, min(1.0, collapseDistance)) collapseDistance = max(0.0, min(1.0, collapseDistance))
collapsedMaxItemFrame.origin.x -= collapsedState.minFraction * 4.0 collapsedMaxItemFrame.origin.x -= collapsedState.minFraction * 4.0
collapsedMaxItemFrame.origin.x += collapseDistance * 20.0 collapsedMaxItemFrame.origin.x += collapseDistance * 20.0
collapsedMaxItemFrame.origin.y += collapseDistance * 20.0 collapsedMaxItemFrame.origin.y += collapseDistance * 20.0
collapsedMaxItemFrame.origin.y += collapsedState.minFraction * 10.0 collapsedMaxItemFrame.origin.y += collapsedState.minFraction * 10.0
}
let minimizedItemScale: CGFloat = 24.0 / 52.0 let minimizedItemScale: CGFloat = 24.0 / 52.0
let minimizedMaxItemScale: CGFloat = (24.0 + 4.0) / 52.0 let minimizedMaxItemScale: CGFloat = (24.0 + 4.0) / 52.0
@ -574,11 +739,12 @@ public final class StoryPeerListComponent: Component {
let itemFrame: CGRect let itemFrame: CGRect
if isReallyVisible { if isReallyVisible {
var adjustedRegularFrame = regularItemFrame var adjustedRegularFrame = regularItemFrame
if i < collapseStartIndex { if index < collapseStartIndex {
adjustedRegularFrame = adjustedRegularFrame.interpolate(to: itemLayout.frame(at: collapseStartIndex), amount: 0.0) adjustedRegularFrame = adjustedRegularFrame.interpolate(to: itemLayout.frame(at: effectiveFirstVisibleIndex + collapseStartIndex), amount: 0.0)
} else if i > collapseEndIndex { } else if index > collapseEndIndex {
adjustedRegularFrame = adjustedRegularFrame.interpolate(to: itemLayout.frame(at: collapseEndIndex), amount: 0.0) 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) 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)) bounceOffsetFraction = max(-1.0, min(1.0, bounceOffsetFraction))
itemPosition.x += min(10.0, expandBoundsFraction * collapsedState.maxFraction * 1200.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) let itemSize = CGSize(width: adjustedRegularFrame.width * itemScale, height: adjustedRegularFrame.height)
itemFrame = itemSize.centered(around: itemPosition) itemFrame = itemSize.centered(around: itemPosition)
} else { } else {
itemFrame = regularItemFrame itemFrame = regularItemFrame.offsetBy(dx: -effectiveVisibleBounds.minX, dy: 0.0)
} }
return MeasuredItem( return MeasuredItem(
@ -604,12 +768,20 @@ public final class StoryPeerListComponent: Component {
} }
var validIds: [EnginePeer.Id] = [] var validIds: [EnginePeer.Id] = []
var validCollapsableIds: [EnginePeer.Id] = []
for i in 0 ..< self.sortedItems.count { for i in 0 ..< self.sortedItems.count {
let itemSet = self.sortedItems[i] let itemSet = self.sortedItems[i]
let peer = itemSet.peer let peer = itemSet.peer
let regularItemFrame = itemLayout.frame(at: i) let regularItemFrame = itemLayout.frame(at: i)
var isItemVisible = true
if !visibleBounds.intersects(regularItemFrame) { if !visibleBounds.intersects(regularItemFrame) {
isItemVisible = false
}
if !isItemVisible {
continue continue
} }
@ -654,19 +826,35 @@ public final class StoryPeerListComponent: Component {
var itemAlpha: CGFloat = 1.0 var itemAlpha: CGFloat = 1.0
var isCollapsable: Bool = false 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 isCollapsable = true
if i != collapseStartIndex { if collapseIndex != collapseStartIndex {
leftItemFrame = calculateItem(i - 1).itemFrame leftItemFrame = calculateItem(i - 1).itemFrame
} }
if i != collapseEndIndex { if collapseIndex != collapseEndIndex {
rightItemFrame = calculateItem(i + 1).itemFrame rightItemFrame = calculateItem(i + 1).itemFrame
} }
if effectiveFirstVisibleIndex == 0 && !component.titleHasActivity {
itemAlpha = 1.0
} else { } else {
itemAlpha = collapsedState.sideAlphaFraction itemAlpha = collapsedState.sideAlphaFraction
} }
} else {
if itemLayout.itemCount == 1 {
itemAlpha = min(1.0, (collapsedState.minFraction + collapsedState.maxFraction) * 4.0)
} else {
itemAlpha = collapsedState.sideAlphaFraction
}
}
var leftNeighborDistance: CGPoint? var leftNeighborDistance: CGPoint?
var rightNeighborDistance: CGPoint? var rightNeighborDistance: CGPoint?
@ -690,7 +878,7 @@ public final class StoryPeerListComponent: Component {
hasItems: hasItems, hasItems: hasItems,
ringAnimation: itemRingAnimation, ringAnimation: itemRingAnimation,
collapseFraction: isReallyVisible ? (1.0 - collapsedState.maxFraction) : 0.0, collapseFraction: isReallyVisible ? (1.0 - collapsedState.maxFraction) : 0.0,
scale: measuredItem.itemScale, scale: itemScale,
collapsedWidth: collapsedItemWidth, collapsedWidth: collapsedItemWidth,
expandedAlphaFraction: collapsedState.sideAlphaFraction, expandedAlphaFraction: collapsedState.sideAlphaFraction,
leftNeighborDistance: leftNeighborDistance, leftNeighborDistance: leftNeighborDistance,
@ -704,8 +892,151 @@ public final class StoryPeerListComponent: Component {
if let itemView = visibleItem.view.view as? StoryPeerListItemComponent.View { if let itemView = visibleItem.view.view as? StoryPeerListItemComponent.View {
if itemView.superview == nil { if itemView.superview == nil {
self.scrollView.addSubview(itemView) self.scrollContainerView.addSubview(itemView)
self.scrollView.addSubview(itemView.backgroundContainer) 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 { if isCollapsable {
@ -733,30 +1064,86 @@ public final class StoryPeerListComponent: Component {
if !validIds.contains(id) { if !validIds.contains(id) {
removedIds.append(id) removedIds.append(id)
if let itemView = visibleItem.view.view as? StoryPeerListItemComponent.View { 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.backgroundContainer.removeFromSuperview()
itemView.removeFromSuperview() itemView.removeFromSuperview()
} }
} }
} }
}
for id in removedIds { for id in removedIds {
self.visibleItems.removeValue(forKey: id) 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? { override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
if self.alpha.isZero { if self.alpha.isZero {
return nil 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? var result: UIView?
for view in self.subviews.reversed() { for view in self.subviews.reversed() {
if let resultValue = view.hitTest(self.convert(point, to: view), with: event), resultValue.isUserInteractionEnabled { 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 return nil
} }
} else { } else {
if !result.isDescendant(of: self.scrollView) { if !result.isDescendant(of: self.scrollContainerView) {
return nil return nil
} }
} }
@ -784,20 +1171,6 @@ public final class StoryPeerListComponent: Component {
var transition = transition var transition = transition
transition.animation = .none 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) let animationHint = transition.userData(AnimationHint.self)
var useAnimation = false var useAnimation = false
if let previousComponent = self.component, component.unlocked != previousComponent.unlocked { if let previousComponent = self.component, component.unlocked != previousComponent.unlocked {
@ -892,12 +1265,13 @@ public final class StoryPeerListComponent: Component {
self.ignoreScrolling = true 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.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 { if self.scrollView.contentSize != itemLayout.contentSize {
self.scrollView.contentSize = itemLayout.contentSize self.scrollView.contentSize = itemLayout.contentSize
} }
self.ignoreScrolling = false self.ignoreScrolling = false
self.updateScrolling(transition: transition, keepVisibleUntilCompletion: false) self.updateScrolling(transition: transition)
return availableSize return availableSize
} }

View File

@ -665,7 +665,7 @@ public final class StoryPeerListItemComponent: Component {
} }
} else { } else {
if component.theme.overallDarkAppearance { 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 { } else {
colors = [UIColor(rgb: 0xD8D8E1).cgColor, UIColor(rgb: 0xD8D8E1).cgColor] colors = [UIColor(rgb: 0xD8D8E1).cgColor, UIColor(rgb: 0xD8D8E1).cgColor]
} }

View File

@ -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)
}
}

View File

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

View File

@ -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

View File

@ -469,7 +469,7 @@ final class PeerInfoAvatarTransformContainerNode: ASDisplayNode {
component: AnyComponent(AvatarStoryIndicatorComponent( component: AnyComponent(AvatarStoryIndicatorComponent(
hasUnseen: storyData.hasUnseen, hasUnseen: storyData.hasUnseen,
hasUnseenCloseFriendsItems: storyData.hasUnseenCloseFriends, hasUnseenCloseFriendsItems: storyData.hasUnseenCloseFriends,
isDarkTheme: theme.overallDarkAppearance, theme: theme,
activeLineWidth: 3.0, activeLineWidth: 3.0,
inactiveLineWidth: 2.0, inactiveLineWidth: 2.0,
counters: nil counters: nil

View File

@ -369,7 +369,7 @@ public final class TelegramRootController: NavigationController, TelegramRootCon
case let .image(image, dimensions): case let .image(image, dimensions):
if let imageData = compressImageToJPEG(image, quality: 0.7) { if let imageData = compressImageToJPEG(image, quality: 0.7) {
let entities = generateChatInputTextEntities(caption) 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 { Queue.mainQueue().justDispatch {
commit({}) commit({})
} }
@ -392,7 +392,7 @@ public final class TelegramRootController: NavigationController, TelegramRootCon
} }
let imageData = firstFrameImage.flatMap { compressImageToJPEG($0, quality: 0.6) } let imageData = firstFrameImage.flatMap { compressImageToJPEG($0, quality: 0.6) }
let entities = generateChatInputTextEntities(caption) 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 { Queue.mainQueue().justDispatch {
commit({}) commit({})
} }