Story improvements

This commit is contained in:
Ali 2023-09-08 19:22:12 +04:00
parent 53f6d62e8b
commit 6910ec9334
22 changed files with 511 additions and 208 deletions

View File

@ -795,7 +795,7 @@ public struct StoryCameraTransitionInCoordinator {
public protocol TelegramRootControllerInterface: NavigationController {
@discardableResult
func openStoryCamera(transitionIn: StoryCameraTransitionIn?, transitionedIn: @escaping () -> Void, transitionOut: @escaping (Stories.PendingTarget?) -> StoryCameraTransitionOut?) -> StoryCameraTransitionInCoordinator?
func openStoryCamera(transitionIn: StoryCameraTransitionIn?, transitionedIn: @escaping () -> Void, transitionOut: @escaping (Stories.PendingTarget?, Bool) -> StoryCameraTransitionOut?) -> StoryCameraTransitionInCoordinator?
func getContactsController() -> ViewController?
func getChatsController() -> ViewController?

View File

@ -1366,9 +1366,13 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
return
}
if let storyPeerListView = self.chatListHeaderView()?.storyPeerListView() {
storyPeerListView.cancelLoadingItem()
}
switch subject {
case .archive:
StoryContainerScreen.openArchivedStories(context: self.context, parentController: self, avatarNode: itemNode.avatarNode)
StoryContainerScreen.openArchivedStories(context: self.context, parentController: self, avatarNode: itemNode.avatarNode, sharedProgressDisposable: self.sharedOpenStoryProgressDisposable)
case let .peer(peerId):
StoryContainerScreen.openPeerStories(context: self.context, peerId: peerId, parentController: self, avatarNode: itemNode.avatarNode, sharedProgressDisposable: self.sharedOpenStoryProgressDisposable)
}
@ -2487,6 +2491,12 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
self.fullScreenEffectView = nil
fullScreenEffectView.removeFromSuperview()
}
self.sharedOpenStoryProgressDisposable.set(nil)
if let storyPeerListView = self.chatListHeaderView()?.storyPeerListView() {
storyPeerListView.cancelLoadingItem()
}
}
func updateHeaderContent() -> (primaryContent: ChatListHeaderComponent.Content?, secondaryContent: ChatListHeaderComponent.Content?) {
@ -2692,39 +2702,37 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
}
if let rootController = self.context.sharedContext.mainWindow?.viewController as? TelegramRootControllerInterface {
let coordinator = rootController.openStoryCamera(transitionIn: cameraTransitionIn, transitionedIn: {}, transitionOut: { [weak self] target in
guard let self, let target else {
return nil
}
if let componentView = self.chatListHeaderView() {
let peerId: EnginePeer.Id
switch target {
case .myStories:
peerId = self.context.account.peerId
case let .peer(id):
peerId = id
}
if let (transitionView, _) = componentView.storyPeerListView()?.transitionViewForItem(peerId: peerId) {
return StoryCameraTransitionOut(
destinationView: transitionView,
destinationRect: transitionView.bounds,
destinationCornerRadius: transitionView.bounds.height * 0.5
)
} else if let rightButtonView = componentView.rightButtonViews["story"] {
return StoryCameraTransitionOut(
destinationView: rightButtonView,
destinationRect: rightButtonView.bounds,
destinationCornerRadius: rightButtonView.bounds.height * 0.5
)
}
}
return nil
})
let coordinator = rootController.openStoryCamera(transitionIn: cameraTransitionIn, transitionedIn: {}, transitionOut: self.storyCameraTransitionOut())
coordinator?.animateIn()
}
}
public func storyCameraTransitionOut() -> (Stories.PendingTarget?, Bool) -> StoryCameraTransitionOut? {
return { [weak self] target, isArchived in
guard let self, let target else {
return nil
}
if let componentView = self.chatListHeaderView() {
let peerId: EnginePeer.Id
switch target {
case .myStories:
peerId = self.context.account.peerId
case let .peer(id):
peerId = id
}
if let (transitionView, _) = componentView.storyPeerListView()?.transitionViewForItem(peerId: peerId) {
return StoryCameraTransitionOut(
destinationView: transitionView,
destinationRect: transitionView.bounds,
destinationCornerRadius: transitionView.bounds.height * 0.5
)
}
}
return nil
}
}
override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
super.containerLayoutUpdated(layout, transition: transition)
@ -3947,6 +3955,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
return
}
if let componentView = self.chatListHeaderView() {
self.sharedOpenStoryProgressDisposable.set(nil)
componentView.storyPeerListView()?.setLoadingItem(peerId: peerId, signal: signal)
}
}
@ -5667,7 +5676,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
if let current = self.storyCameraTransitionInCoordinator {
coordinator = current
} else {
coordinator = rootController.openStoryCamera(transitionIn: nil, transitionedIn: {}, transitionOut: { [weak self] target in
coordinator = rootController.openStoryCamera(transitionIn: nil, transitionedIn: {}, transitionOut: { [weak self] target, _ in
guard let self, let target else {
return nil
}

View File

@ -260,6 +260,10 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate {
private var isLeftAligned: Bool = true
private var itemLayout: ItemLayout?
public var centerAligned: Bool {
return self.validLayout?.4 ?? false
}
private var customReactionSource: (view: UIView, rect: CGRect, layer: CALayer, item: ReactionItem)?
public var reactionSelected: ((UpdateMessageReaction, Bool) -> Void)?

View File

@ -4579,7 +4579,7 @@ func replayFinalState(
mediaAreas: item.mediaAreas,
text: item.text,
entities: item.entities,
views: _internal_updateStoryViewsForMyReaction(views: item.views, previousReaction: item.myReaction, reaction: updatedReaction),
views: _internal_updateStoryViewsForMyReaction(isChannel: peerId.namespace == Namespaces.Peer.CloudChannel, views: item.views, previousReaction: item.myReaction, reaction: updatedReaction),
privacy: item.privacy,
isPinned: item.isPinned,
isExpired: item.isExpired,
@ -4610,7 +4610,7 @@ func replayFinalState(
mediaAreas: item.mediaAreas,
text: item.text,
entities: item.entities,
views: _internal_updateStoryViewsForMyReaction(views: item.views, previousReaction: item.myReaction, reaction: updatedReaction),
views: _internal_updateStoryViewsForMyReaction(isChannel: peerId.namespace == Namespaces.Peer.CloudChannel, views: item.views, previousReaction: item.myReaction, reaction: updatedReaction),
privacy: item.privacy,
isPinned: item.isPinned,
isExpired: item.isExpired,

View File

@ -23,10 +23,10 @@ public struct CachedChannelFlags: OptionSet {
}
public struct CachedChannelParticipantsSummary: PostboxCoding, Equatable {
public let memberCount: Int32?
public let adminCount: Int32?
public let bannedCount: Int32?
public let kickedCount: Int32?
public var memberCount: Int32?
public var adminCount: Int32?
public var bannedCount: Int32?
public var kickedCount: Int32?
public init(memberCount: Int32?, adminCount: Int32?, bannedCount: Int32?, kickedCount: Int32?) {
self.memberCount = memberCount

View File

@ -16,6 +16,6 @@ public final class CachedLocalizationInfos: Codable {
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: StringCodingKey.self)
try container.encode(self.list, forKey: "l")
try container.encode(self.list, forKey: "t")
}
}

View File

@ -109,6 +109,7 @@ public struct Namespaces {
public static let cachedEmojiQueryResults: Int8 = 26
public static let cachedPeerStoryListHeads: Int8 = 27
public static let displayedStoryNotifications: Int8 = 28
public static let storySendAsPeerIds: Int8 = 29
}
public struct UnorderedItemList {

View File

@ -1047,7 +1047,7 @@ func _internal_uploadStoryImpl(postbox: Postbox, network: Network, accountPeerId
if !peerIds.contains(toPeerId) {
peerIds.append(toPeerId)
}
transaction.replaceAllStorySubscriptions(key: .filtered, state: state, peerIds: peerIds)
transaction.replaceAllStorySubscriptions(key: subscriptionsKey, state: state, peerIds: peerIds)
}
}
@ -1976,7 +1976,11 @@ public func _internal_setStoryNotificationWasDisplayed(transaction: Transaction,
transaction.putItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.displayedStoryNotifications, key: key), entry: CodableEntry(data: Data()))
}
func _internal_updateStoryViewsForMyReaction(views: Stories.Item.Views?, previousReaction: MessageReaction.Reaction?, reaction: MessageReaction.Reaction?) -> Stories.Item.Views? {
func _internal_updateStoryViewsForMyReaction(isChannel: Bool, views: Stories.Item.Views?, previousReaction: MessageReaction.Reaction?, reaction: MessageReaction.Reaction?) -> Stories.Item.Views? {
if !isChannel {
return views
}
var views = views ?? Stories.Item.Views(seenCount: 0, reactedCount: 0, forwardCount: 0, seenPeerIds: [], reactions: [], hasList: false)
if let reaction {
@ -2040,7 +2044,7 @@ func _internal_setStoryReaction(account: Account, peerId: EnginePeer.Id, id: Int
var updatedItemValue: Stories.StoredItem?
let updateViews: (Stories.Item.Views?, MessageReaction.Reaction?) -> Stories.Item.Views? = { views, previousReaction in
return _internal_updateStoryViewsForMyReaction(views: views, previousReaction: previousReaction, reaction: reaction)
return _internal_updateStoryViewsForMyReaction(isChannel: peerId.namespace == Namespaces.Peer.CloudChannel, views: views, previousReaction: previousReaction, reaction: reaction)
}
var currentItems = transaction.getStoryItems(peerId: peerId)

View File

@ -839,6 +839,17 @@ public extension TelegramEngine {
guard let peer = peerView.peer else {
continue
}
var isPeerHidden = false
if let user = peer as? TelegramUser {
isPeerHidden = user.storiesHidden ?? false
} else if let channel = peer as? TelegramChannel {
isPeerHidden = channel.storiesHidden ?? false
}
if isPeerHidden != isHidden {
continue
}
guard let itemsView = views.views[PostboxViewKey.storyItems(peerId: peerId)] as? StoryItemsView else {
continue
}
@ -912,6 +923,16 @@ public extension TelegramEngine {
continue
}
var isPeerHidden = false
if let user = peer as? TelegramUser {
isPeerHidden = user.storiesHidden ?? false
} else if let channel = peer as? TelegramChannel {
isPeerHidden = channel.storiesHidden ?? false
}
if isPeerHidden != isHidden {
continue
}
let item = EngineStorySubscriptions.Item(
peer: EnginePeer(peer),
hasUnseen: false,

View File

@ -546,29 +546,81 @@ func _internal_adminedPublicChannels(account: Account, scope: AdminedPublicChann
}
}
final class CachedStorySendAsPeers: Codable {
public let peerIds: [PeerId]
public init(peerIds: [PeerId]) {
self.peerIds = peerIds
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: StringCodingKey.self)
self.peerIds = try container.decode([Int64].self, forKey: "l").map(PeerId.init)
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: StringCodingKey.self)
try container.encode(self.peerIds.map { $0.toInt64() }, forKey: "l")
}
}
func _internal_channelsForStories(account: Account) -> Signal<[Peer], NoError> {
let accountPeerId = account.peerId
return account.network.request(Api.functions.stories.getChatsToSend())
|> retryRequest
|> mapToSignal { result -> Signal<[Peer], NoError> in
return account.postbox.transaction { transaction -> [Peer] in
let chats: [Api.Chat]
let parsedPeers: AccumulatedPeers
switch result {
case let .chats(apiChats):
chats = apiChats
case let .chatsSlice(_, apiChats):
chats = apiChats
}
parsedPeers = AccumulatedPeers(transaction: transaction, chats: chats, users: [])
updatePeers(transaction: transaction, accountPeerId: accountPeerId, peers: parsedPeers)
var peers: [Peer] = []
for chat in chats {
if let peer = transaction.getPeer(chat.peerId) {
peers.append(peer)
return account.postbox.transaction { transaction -> [Peer]? in
if let entry = transaction.retrieveItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.storySendAsPeerIds, key: ValueBoxKey(length: 0)))?.get(CachedStorySendAsPeers.self) {
return entry.peerIds.compactMap(transaction.getPeer)
} else {
return nil
}
}
|> mapToSignal { cachedPeers in
let remote: Signal<[Peer], NoError> = account.network.request(Api.functions.stories.getChatsToSend())
|> retryRequest
|> mapToSignal { result -> Signal<[Peer], NoError> in
return account.postbox.transaction { transaction -> [Peer] in
let chats: [Api.Chat]
let parsedPeers: AccumulatedPeers
switch result {
case let .chats(apiChats):
chats = apiChats
case let .chatsSlice(_, apiChats):
chats = apiChats
}
parsedPeers = AccumulatedPeers(transaction: transaction, chats: chats, users: [])
updatePeers(transaction: transaction, accountPeerId: accountPeerId, peers: parsedPeers)
var peers: [Peer] = []
for chat in chats {
if let peer = transaction.getPeer(chat.peerId) {
peers.append(peer)
if case let .channel(_, _, _, _, _, _, _, _, _, _, _, _, participantsCount, _, _) = chat, let participantsCount = participantsCount {
transaction.updatePeerCachedData(peerIds: Set([peer.id]), update: { _, current in
var current = current as? CachedChannelData ?? CachedChannelData()
var participantsSummary = current.participantsSummary
participantsSummary.memberCount = participantsCount
current = current.withUpdatedParticipantsSummary(participantsSummary)
return current
})
}
}
}
if let entry = CodableEntry(CachedStorySendAsPeers(peerIds: peers.map(\.id))) {
transaction.putItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.storySendAsPeerIds, key: ValueBoxKey(length: 0)), entry: entry)
}
return peers
}
return peers
}
if let cachedPeers {
return .single(cachedPeers) |> then(remote)
} else {
return remote
}
}
}

View File

@ -809,6 +809,7 @@ private final class GroupHeaderLayer: UIView {
func update(
context: AccountContext,
theme: PresentationTheme,
forceNeedsVibrancy: Bool,
layoutType: EmojiPagerContentComponent.ItemLayoutType,
hasTopSeparator: Bool,
actionButtonTitle: String?,
@ -830,7 +831,7 @@ private final class GroupHeaderLayer: UIView {
themeUpdated = true
}
let needsVibrancy = !theme.overallDarkAppearance
let needsVibrancy = !theme.overallDarkAppearance || forceNeedsVibrancy
let textOffsetY: CGFloat
if hasTopSeparator {
@ -839,16 +840,22 @@ private final class GroupHeaderLayer: UIView {
textOffsetY = 0.0
}
let subtitleColor: UIColor
if theme.overallDarkAppearance && forceNeedsVibrancy {
subtitleColor = theme.chat.inputMediaPanel.panelContentVibrantOverlayColor.withMultipliedAlpha(0.2)
} else {
subtitleColor = theme.chat.inputMediaPanel.panelContentVibrantOverlayColor
}
let color: UIColor
let needsTintText: Bool
if subtitle != nil {
color = theme.chat.inputPanel.primaryTextColor
needsTintText = false
} else {
color = theme.chat.inputMediaPanel.panelContentVibrantOverlayColor
color = subtitleColor
needsTintText = true
}
let subtitleColor = theme.chat.inputMediaPanel.panelContentVibrantOverlayColor
let titleHorizontalOffset: CGFloat
if isPremiumLocked {
@ -903,7 +910,7 @@ private final class GroupHeaderLayer: UIView {
tintClearIconLayer.isHidden = !needsVibrancy
clearSize = clearIconLayer.bounds.size
if updateImage, let image = PresentationResourcesChat.chatInputMediaPanelGridDismissImage(theme, color: theme.chat.inputMediaPanel.panelContentVibrantOverlayColor) {
if updateImage, let image = PresentationResourcesChat.chatInputMediaPanelGridDismissImage(theme, color: subtitleColor) {
clearSize = image.size
clearIconLayer.contents = image.cgImage
}
@ -1144,7 +1151,7 @@ private final class GroupHeaderLayer: UIView {
self.separatorLayer = separatorLayer
self.layer.addSublayer(separatorLayer)
}
separatorLayer.backgroundColor = theme.chat.inputMediaPanel.panelContentVibrantOverlayColor.cgColor
separatorLayer.backgroundColor = subtitleColor.cgColor
separatorLayer.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: UIScreenPixel))
let tintSeparatorLayer: SimpleLayer
@ -1517,6 +1524,7 @@ public final class EmojiSearchHeaderView: UIView, UITextFieldDelegate {
private struct Params: Equatable {
var context: AccountContext
var theme: PresentationTheme
var forceNeedsVibrancy: Bool
var strings: PresentationStrings
var text: String
var useOpaqueTheme: Bool
@ -1535,6 +1543,9 @@ public final class EmojiSearchHeaderView: UIView, UITextFieldDelegate {
if lhs.theme !== rhs.theme {
return false
}
if lhs.forceNeedsVibrancy != rhs.forceNeedsVibrancy {
return false
}
if lhs.strings !== rhs.strings {
return false
}
@ -1823,10 +1834,10 @@ public final class EmojiSearchHeaderView: UIView, UITextFieldDelegate {
return
}
self.params = nil
self.update(context: params.context, theme: params.theme, strings: params.strings, text: params.text, useOpaqueTheme: params.useOpaqueTheme, isActive: params.isActive, size: params.size, canFocus: params.canFocus, searchCategories: params.searchCategories, searchState: params.searchState, transition: transition)
self.update(context: params.context, theme: params.theme, forceNeedsVibrancy: params.forceNeedsVibrancy, strings: params.strings, text: params.text, useOpaqueTheme: params.useOpaqueTheme, isActive: params.isActive, size: params.size, canFocus: params.canFocus, searchCategories: params.searchCategories, searchState: params.searchState, transition: transition)
}
public func update(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, text: String, useOpaqueTheme: Bool, isActive: Bool, size: CGSize, canFocus: Bool, searchCategories: EmojiSearchCategories?, searchState: EmojiPagerContentComponent.SearchState, transition: Transition) {
public func update(context: AccountContext, theme: PresentationTheme, forceNeedsVibrancy: Bool, strings: PresentationStrings, text: String, useOpaqueTheme: Bool, isActive: Bool, size: CGSize, canFocus: Bool, searchCategories: EmojiSearchCategories?, searchState: EmojiPagerContentComponent.SearchState, transition: Transition) {
let textInputState: EmojiSearchSearchBarComponent.TextInputState
if let textField = self.textField {
textInputState = .active(hasText: !(textField.text ?? "").isEmpty)
@ -1837,6 +1848,7 @@ public final class EmojiSearchHeaderView: UIView, UITextFieldDelegate {
let params = Params(
context: context,
theme: theme,
forceNeedsVibrancy: forceNeedsVibrancy,
strings: strings,
text: text,
useOpaqueTheme: useOpaqueTheme,
@ -1880,7 +1892,10 @@ public final class EmojiSearchHeaderView: UIView, UITextFieldDelegate {
let sideTextInset: CGFloat = sideInset + 4.0 + 24.0
if useOpaqueTheme {
if theme.overallDarkAppearance && forceNeedsVibrancy {
self.backgroundLayer.backgroundColor = theme.chat.inputMediaPanel.panelContentControlVibrantSelectionColor.withMultipliedAlpha(0.3).cgColor
self.tintBackgroundLayer.backgroundColor = UIColor(white: 1.0, alpha: 0.2).cgColor
} else if useOpaqueTheme {
self.backgroundLayer.backgroundColor = theme.chat.inputMediaPanel.panelContentControlOpaqueSelectionColor.cgColor
self.tintBackgroundLayer.backgroundColor = UIColor.white.cgColor
} else {
@ -1891,12 +1906,19 @@ public final class EmojiSearchHeaderView: UIView, UITextFieldDelegate {
self.backgroundLayer.cornerRadius = inputHeight * 0.5
self.tintBackgroundLayer.cornerRadius = inputHeight * 0.5
let cancelColor: UIColor
if theme.overallDarkAppearance && forceNeedsVibrancy {
cancelColor = theme.chat.inputMediaPanel.panelContentVibrantSearchOverlayColor.withMultipliedAlpha(0.3)
} else {
cancelColor = useOpaqueTheme ? theme.list.itemAccentColor : theme.chat.inputMediaPanel.panelContentVibrantSearchOverlayColor
}
let cancelTextSize = self.cancelButtonTitle.update(
transition: .immediate,
component: AnyComponent(Text(
text: strings.Common_Cancel,
font: Font.regular(17.0),
color: useOpaqueTheme ? theme.list.itemAccentColor : theme.chat.inputMediaPanel.panelContentVibrantSearchOverlayColor
color: cancelColor
)),
environment: {},
containerSize: CGSize(width: size.width - 32.0, height: 100.0)
@ -1942,6 +1964,7 @@ public final class EmojiSearchHeaderView: UIView, UITextFieldDelegate {
transition: transition,
component: AnyComponent(EmojiSearchStatusComponent(
theme: theme,
forceNeedsVibrancy: forceNeedsVibrancy,
strings: strings,
useOpaqueTheme: useOpaqueTheme,
content: statusContent
@ -1990,6 +2013,7 @@ public final class EmojiSearchHeaderView: UIView, UITextFieldDelegate {
component: AnyComponent(EmojiSearchSearchBarComponent(
context: context,
theme: theme,
forceNeedsVibrancy: forceNeedsVibrancy,
strings: strings,
useOpaqueTheme: useOpaqueTheme,
textInputState: textInputState,
@ -5426,6 +5450,7 @@ public final class EmojiPagerContentComponent: Component {
let (groupHeaderSize, centralContentWidth) = groupHeaderView.update(
context: component.context,
theme: keyboardChildEnvironment.theme,
forceNeedsVibrancy: component.inputInteractionHolder.inputInteraction?.externalBackground != nil,
layoutType: itemLayout.layoutType,
hasTopSeparator: hasTopSeparator,
actionButtonTitle: actionButtonTitle,
@ -5467,7 +5492,14 @@ public final class EmojiPagerContentComponent: Component {
self.scrollView.layer.insertSublayer(groupBorderLayer, at: 0)
self.mirrorContentScrollView.layer.addSublayer(groupBorderLayer.tintContainerLayer)
groupBorderLayer.strokeColor = keyboardChildEnvironment.theme.chat.inputMediaPanel.panelContentVibrantOverlayColor.cgColor
let borderColor: UIColor
if keyboardChildEnvironment.theme.overallDarkAppearance && component.inputInteractionHolder.inputInteraction?.externalBackground != nil {
borderColor = keyboardChildEnvironment.theme.chat.inputMediaPanel.panelContentVibrantOverlayColor.withMultipliedAlpha(0.2)
} else {
borderColor = keyboardChildEnvironment.theme.chat.inputMediaPanel.panelContentVibrantOverlayColor
}
groupBorderLayer.strokeColor = borderColor.cgColor
groupBorderLayer.tintContainerLayer.strokeColor = UIColor.white.cgColor
groupBorderLayer.lineWidth = 1.6
groupBorderLayer.lineCap = .round
@ -6854,7 +6886,7 @@ public final class EmojiPagerContentComponent: Component {
}
let searchHeaderFrame = CGRect(origin: CGPoint(x: itemLayout.searchInsets.left, y: itemLayout.searchInsets.top), size: CGSize(width: itemLayout.width - itemLayout.searchInsets.left - itemLayout.searchInsets.right, height: itemLayout.searchHeight))
visibleSearchHeader.update(context: component.context, theme: keyboardChildEnvironment.theme, strings: keyboardChildEnvironment.strings, text: displaySearchWithPlaceholder, useOpaqueTheme: useOpaqueTheme, isActive: self.isSearchActivated, size: searchHeaderFrame.size, canFocus: !component.searchIsPlaceholderOnly, searchCategories: component.searchCategories, searchState: component.searchState, transition: transition)
visibleSearchHeader.update(context: component.context, theme: keyboardChildEnvironment.theme, forceNeedsVibrancy: component.inputInteractionHolder.inputInteraction?.externalBackground != nil, strings: keyboardChildEnvironment.strings, text: displaySearchWithPlaceholder, useOpaqueTheme: useOpaqueTheme, isActive: self.isSearchActivated, size: searchHeaderFrame.size, canFocus: !component.searchIsPlaceholderOnly, searchCategories: component.searchCategories, searchState: component.searchState, transition: transition)
transition.setFrame(view: visibleSearchHeader, frame: searchHeaderFrame)
// Temporary workaround for status selection; use a separate search container (see GIF)

View File

@ -99,6 +99,7 @@ final class EmojiSearchSearchBarComponent: Component {
let context: AccountContext
let theme: PresentationTheme
let forceNeedsVibrancy: Bool
let strings: PresentationStrings
let useOpaqueTheme: Bool
let textInputState: TextInputState
@ -109,6 +110,7 @@ final class EmojiSearchSearchBarComponent: Component {
init(
context: AccountContext,
theme: PresentationTheme,
forceNeedsVibrancy: Bool,
strings: PresentationStrings,
useOpaqueTheme: Bool,
textInputState: TextInputState,
@ -118,6 +120,7 @@ final class EmojiSearchSearchBarComponent: Component {
) {
self.context = context
self.theme = theme
self.forceNeedsVibrancy = forceNeedsVibrancy
self.strings = strings
self.useOpaqueTheme = useOpaqueTheme
self.textInputState = textInputState
@ -130,6 +133,9 @@ final class EmojiSearchSearchBarComponent: Component {
if lhs.theme !== rhs.theme {
return false
}
if lhs.forceNeedsVibrancy != rhs.forceNeedsVibrancy {
return false
}
if lhs.strings !== rhs.strings {
return false
}
@ -456,7 +462,10 @@ final class EmojiSearchSearchBarComponent: Component {
}
let color: UIColor
if component.useOpaqueTheme {
if component.theme.overallDarkAppearance && component.forceNeedsVibrancy {
let tempColor = self.selectedItem == AnyHashable(item.id) ? component.theme.chat.inputMediaPanel.panelContentVibrantSearchOverlaySelectedColor : component.theme.chat.inputMediaPanel.panelContentVibrantSearchOverlayColor
color = tempColor.withMultipliedAlpha(0.3)
} else if component.useOpaqueTheme {
color = self.selectedItem == AnyHashable(item.id) ? component.theme.chat.inputMediaPanel.panelContentOpaqueSearchOverlaySelectedColor : component.theme.chat.inputMediaPanel.panelContentOpaqueSearchOverlayColor
} else {
color = self.selectedItem == AnyHashable(item.id) ? component.theme.chat.inputMediaPanel.panelContentVibrantSearchOverlaySelectedColor : component.theme.chat.inputMediaPanel.panelContentVibrantSearchOverlayColor
@ -541,10 +550,18 @@ final class EmojiSearchSearchBarComponent: Component {
self.visibleItemViews.removeValue(forKey: id)
}
let selectedColor: UIColor
if component.theme.overallDarkAppearance && component.forceNeedsVibrancy {
let tempColor = component.useOpaqueTheme ? component.theme.chat.inputMediaPanel.panelContentOpaqueSearchOverlayHighlightColor : component.theme.chat.inputMediaPanel.panelContentVibrantSearchOverlayHighlightColor
selectedColor = tempColor.withMultipliedAlpha(0.3)
} else {
selectedColor = component.useOpaqueTheme ? component.theme.chat.inputMediaPanel.panelContentOpaqueSearchOverlayHighlightColor : component.theme.chat.inputMediaPanel.panelContentVibrantSearchOverlayHighlightColor
}
if let selectedItem = self.selectedItem, let index = items.firstIndex(where: { AnyHashable($0.id) == selectedItem }) {
let selectedItemCenter = itemLayout.frame(at: index).center
let selectionSize = CGSize(width: 28.0, height: 28.0)
self.selectedItemBackground.backgroundColor = component.useOpaqueTheme ? component.theme.chat.inputMediaPanel.panelContentOpaqueSearchOverlayHighlightColor.cgColor : component.theme.chat.inputMediaPanel.panelContentVibrantSearchOverlayHighlightColor.cgColor
self.selectedItemBackground.backgroundColor = selectedColor.cgColor
self.selectedItemTintBackground.backgroundColor = UIColor(white: 1.0, alpha: 0.15).cgColor
self.selectedItemBackground.cornerRadius = selectionSize.height * 0.5
self.selectedItemTintBackground.cornerRadius = selectionSize.height * 0.5
@ -609,12 +626,19 @@ final class EmojiSearchSearchBarComponent: Component {
self.component = component
self.componentState = state
let textColor: UIColor
if component.theme.overallDarkAppearance && component.forceNeedsVibrancy {
textColor = component.theme.chat.inputMediaPanel.panelContentVibrantSearchOverlayColor.withMultipliedAlpha(0.3)
} else {
textColor = component.useOpaqueTheme ? component.theme.chat.inputMediaPanel.panelContentOpaqueSearchOverlayColor : component.theme.chat.inputMediaPanel.panelContentVibrantSearchOverlayColor
}
let textSize = self.textView.update(
transition: .immediate,
component: AnyComponent(Text(
text: component.strings.Common_Search,
font: Font.regular(17.0),
color: component.useOpaqueTheme ? component.theme.chat.inputMediaPanel.panelContentOpaqueSearchOverlayColor : component.theme.chat.inputMediaPanel.panelContentVibrantSearchOverlayColor
color: textColor
)),
environment: {},
containerSize: CGSize(width: availableSize.width - 32.0, height: 100.0)

View File

@ -70,17 +70,20 @@ final class EmojiSearchStatusComponent: Component {
}
let theme: PresentationTheme
let forceNeedsVibrancy: Bool
let strings: PresentationStrings
let useOpaqueTheme: Bool
let content: Content
init(
theme: PresentationTheme,
forceNeedsVibrancy: Bool,
strings: PresentationStrings,
useOpaqueTheme: Bool,
content: Content
) {
self.theme = theme
self.forceNeedsVibrancy = forceNeedsVibrancy
self.strings = strings
self.useOpaqueTheme = useOpaqueTheme
self.content = content
@ -90,6 +93,9 @@ final class EmojiSearchStatusComponent: Component {
if lhs.theme !== rhs.theme {
return false
}
if lhs.forceNeedsVibrancy != rhs.forceNeedsVibrancy {
return false
}
if lhs.strings !== rhs.strings {
return false
}
@ -430,7 +436,13 @@ final class EmojiSearchStatusComponent: Component {
let displaySize = CGSize(width: availableSize.width * UIScreenScale, height: availableSize.height * UIScreenScale)
self.displaySize = displaySize
let overlayColor = component.useOpaqueTheme ? component.theme.chat.inputMediaPanel.panelContentOpaqueSearchOverlayColor : component.theme.chat.inputMediaPanel.panelContentVibrantSearchOverlayColor
let overlayColor: UIColor
if component.theme.overallDarkAppearance && component.forceNeedsVibrancy {
overlayColor = component.theme.chat.inputMediaPanel.panelContentVibrantSearchOverlayColor.withMultipliedAlpha(0.3)
} else {
overlayColor = component.useOpaqueTheme ? component.theme.chat.inputMediaPanel.panelContentOpaqueSearchOverlayColor : component.theme.chat.inputMediaPanel.panelContentVibrantSearchOverlayColor
}
let baseColor: UIColor = .white
if self.contentView.tintColor != overlayColor {

View File

@ -1090,7 +1090,7 @@ public final class GifPagerContentComponent: Component {
}
let searchHeaderFrame = CGRect(origin: CGPoint(x: itemLayout.searchInsets.left, y: itemLayout.searchInsets.top), size: CGSize(width: itemLayout.width - itemLayout.searchInsets.left - itemLayout.searchInsets.right, height: itemLayout.searchHeight))
visibleSearchHeader.update(context: component.context, theme: keyboardChildEnvironment.theme, strings: keyboardChildEnvironment.strings, text: displaySearchWithPlaceholder, useOpaqueTheme: false, isActive: false, size: searchHeaderFrame.size, canFocus: false, searchCategories: component.searchCategories, searchState: component.searchState, transition: transition)
visibleSearchHeader.update(context: component.context, theme: keyboardChildEnvironment.theme, forceNeedsVibrancy: false, strings: keyboardChildEnvironment.strings, text: displaySearchWithPlaceholder, useOpaqueTheme: false, isActive: false, size: searchHeaderFrame.size, canFocus: false, searchCategories: component.searchCategories, searchState: component.searchState, transition: transition)
transition.setFrame(view: visibleSearchHeader, frame: searchHeaderFrame, completion: { [weak self] completed in
let _ = self
let _ = completed

View File

@ -291,7 +291,7 @@ public final class LottieComponent: Component {
}
}
if self.scheduledPlayOnce {
if self.scheduledPlayOnce && self.isEffectivelyVisible {
self.scheduledPlayOnce = false
self.playOnce()
} else {

View File

@ -718,7 +718,6 @@ final class ShareWithPeersScreenComponent: Component {
return result
}
} else {
let _ = context.engine.peers.fetchAndUpdateCachedPeerData(peerId: peer.id).start()
return .complete()
}
}
@ -2693,12 +2692,6 @@ public class ShareWithPeersScreen: ViewControllerComponentContainer {
)
self.stateValue = state
self.stateSubject.set(.single(state))
for peer in peers {
if case let .channel(channel) = peer, participants[channel.id] == nil {
let _ = context.engine.peers.fetchAndUpdateCachedPeerData(peerId: channel.id).start()
}
}
self.readySubject.set(true)
})
@ -2811,7 +2804,9 @@ public class ShareWithPeersScreen: ViewControllerComponentContainer {
}
for channel in adminedChannels {
if case let .channel(channel) = channel, channel.hasPermission(.postStories) {
sendAsPeers.append(contentsOf: adminedChannels)
if !sendAsPeers.contains(where: { $0.id == channel.id }) {
sendAsPeers.append(contentsOf: adminedChannels)
}
}
}
@ -2832,12 +2827,6 @@ public class ShareWithPeersScreen: ViewControllerComponentContainer {
)
self.stateValue = state
self.stateSubject.set(.single(state))
for peer in adminedChannels {
if case let .channel(channel) = peer, participants[channel.id] == nil {
let _ = context.engine.peers.fetchAndUpdateCachedPeerData(peerId: channel.id).start()
}
}
self.readySubject.set(true)
})
@ -2958,12 +2947,6 @@ public class ShareWithPeersScreen: ViewControllerComponentContainer {
self.stateValue = state
self.stateSubject.set(.single(state))
for peer in state.peers {
if case let .channel(channel) = peer, participants[channel.id] == nil {
let _ = context.engine.peers.fetchAndUpdateCachedPeerData(peerId: channel.id).start()
}
}
self.readySubject.set(true)
})
case let .contacts(base):
@ -3107,12 +3090,6 @@ public class ShareWithPeersScreen: ViewControllerComponentContainer {
self.stateValue = state
self.stateSubject.set(.single(state))
for peer in state.peers {
if case let .channel(channel) = peer, participants[channel.id] == nil {
let _ = context.engine.peers.fetchAndUpdateCachedPeerData(peerId: channel.id).start()
}
}
self.readySubject.set(true)
})
}

View File

@ -8,7 +8,7 @@ import Postbox
import AvatarNode
public extension StoryContainerScreen {
static func openArchivedStories(context: AccountContext, parentController: ViewController, avatarNode: AvatarNode) {
static func openArchivedStories(context: AccountContext, parentController: ViewController, avatarNode: AvatarNode, sharedProgressDisposable: MetaDisposable?) {
let storyContent = StoryContentContextImpl(context: context, isHidden: true, focusedPeerId: nil, singlePeer: false)
let signal = storyContent.state
|> take(1)
@ -82,7 +82,10 @@ public extension StoryContainerScreen {
}
|> ignoreValues
let _ = avatarNode.pushLoadingStatus(signal: signal)
let disposable = avatarNode.pushLoadingStatus(signal: signal)
if let sharedProgressDisposable {
sharedProgressDisposable.set(disposable)
}
}
static func openPeerStories(context: AccountContext, peerId: EnginePeer.Id, parentController: ViewController, avatarNode: AvatarNode?, sharedProgressDisposable: MetaDisposable? = nil) {

View File

@ -91,7 +91,7 @@ private final class MuteMonitor {
private final class StoryLongPressRecognizer: UILongPressGestureRecognizer {
var shouldBegin: ((UITouch) -> Bool)?
var updateIsTracking: ((Bool) -> Void)?
var updateIsTracking: ((CGPoint?) -> Void)?
override var state: UIGestureRecognizer.State {
didSet {
@ -116,7 +116,7 @@ private final class StoryLongPressRecognizer: UILongPressGestureRecognizer {
self.isValidated = false
if self.isTracking {
self.isTracking = false
self.updateIsTracking?(false)
self.updateIsTracking?(nil)
}
}
@ -134,7 +134,7 @@ private final class StoryLongPressRecognizer: UILongPressGestureRecognizer {
if !self.isTracking {
self.isTracking = true
self.updateIsTracking?(true)
self.updateIsTracking?(touches.first?.location(in: self.view))
}
}
}
@ -450,12 +450,38 @@ private final class StoryContainerScreenComponent: Component {
let longPressRecognizer = StoryLongPressRecognizer(target: self, action: #selector(self.longPressGesture(_:)))
longPressRecognizer.delegate = self
longPressRecognizer.updateIsTracking = { [weak self] isTracking in
longPressRecognizer.updateIsTracking = { [weak self] point in
guard let self else {
return
}
self.isHoldingTouch = isTracking
self.state?.updated(transition: .immediate)
guard let stateValue = self.stateValue, let slice = stateValue.slice, let itemSetView = self.visibleItemSetViews[slice.peer.id], let itemSetComponentView = itemSetView.view.view as? StoryItemSetContainerComponent.View else {
return
}
var point = point
if let pointValue = point {
if !itemSetComponentView.allowsInstantPauseOnTouch(point: self.convert(pointValue, to: itemSetComponentView)) {
point = nil
}
}
if point != nil {
if !self.isHoldingTouch {
self.isHoldingTouch = true
self.state?.updated(transition: .immediate)
}
} else {
DispatchQueue.main.async { [weak self] in
guard let self else {
return
}
if self.isHoldingTouch {
self.isHoldingTouch = false
self.state?.updated(transition: .immediate)
}
}
}
}
longPressRecognizer.shouldBegin = { [weak self] touch in
guard let self else {

View File

@ -162,6 +162,13 @@ final class StoryItemContentComponent: Component {
self.currentFetchPriority?.disposable.dispose()
}
func allowsInstantPauseOnTouch(point: CGPoint) -> Bool {
if let _ = self.overlaysView.hitTest(self.convert(self.convert(point, to: self.overlaysView), to: self.overlaysView), with: nil) {
return false
}
return true
}
private func performActionAfterImageContentLoaded(update: Bool) {
self.initializeVideoIfReady(update: update)
}

View File

@ -449,6 +449,7 @@ public final class StoryItemSetContainerComponent: Component {
var reactionContextNode: ReactionContextNode?
weak var disappearingReactionContextNode: ReactionContextNode?
weak var willDismissReactionContextNode: ReactionContextNode?
var displayLikeReactions: Bool = false
var tempReactionsGesture: ContextGesture?
var waitingForReactionAnimateOutToLike: MessageReaction.Reaction?
@ -711,7 +712,32 @@ public final class StoryItemSetContainerComponent: Component {
return true
}
func allowsInstantPauseOnTouch(point: CGPoint) -> Bool {
guard let component = self.component else {
return false
}
guard let visibleItem = self.visibleItems[component.slice.item.storyItem.id] else {
return false
}
guard let itemView = visibleItem.view.view as? StoryItemContentComponent.View else {
return false
}
let localPoint = self.convert(point, to: itemView)
if itemView.bounds.contains(localPoint) {
if !itemView.allowsInstantPauseOnTouch(point: localPoint) {
return false
}
}
return true
}
func isPointInsideContentArea(point: CGPoint) -> Bool {
if self.reactionContextNode != nil {
return false
}
if let inputPanelView = self.inputPanel.view, inputPanelView.alpha != 0.0 {
if inputPanelView.frame.contains(point) {
return false
@ -874,6 +900,13 @@ public final class StoryItemSetContainerComponent: Component {
}
if self.displayLikeReactions {
self.displayLikeReactions = false
self.sendMessageContext.currentInputMode = .text
self.willDismissReactionContextNode = self.reactionContextNode
if hasFirstResponder(self) {
self.endEditing(true)
}
self.state?.updated(transition: Transition(animation: .curve(duration: 0.25, curve: .easeInOut)))
self.updateIsProgressPaused()
} else if self.hasActiveDeactivateableInput() {
@ -1240,6 +1273,8 @@ public final class StoryItemSetContainerComponent: Component {
return self
}
return self.itemsContainerView
} else if self.viewListDisplayState == .half && result.isDescendant(of: self.itemsContainerView) {
return self.itemsContainerView
}
return result
@ -4077,18 +4112,17 @@ public final class StoryItemSetContainerComponent: Component {
}
let reactionsAnchorRect: CGRect
if self.displayLikeReactions, let inputPanelView = self.inputPanel.view as? MessageInputPanelComponent.View, let likeButtonView = inputPanelView.likeButtonView {
if self.inputPanelExternalState.isEditing, let inputPanelFrameValue {
reactionsAnchorRect = CGRect(origin: CGPoint(x: inputPanelFrameValue.maxX - 40.0, y: inputPanelFrameValue.minY + 9.0), size: CGSize(width: 32.0, height: 32.0)).insetBy(dx: -4.0, dy: -4.0)
} else if let inputPanelView = self.inputPanel.view as? MessageInputPanelComponent.View, let likeButtonView = inputPanelView.likeButtonView {
var likeRect = likeButtonView.convert(likeButtonView.bounds, to: self)
likeRect.origin.y -= 15.0
likeRect.size.height += 15.0
likeRect.origin.x -= 30.0
reactionsAnchorRect = likeRect
} else {
if let inputPanelFrameValue {
reactionsAnchorRect = CGRect(origin: CGPoint(x: inputPanelFrameValue.maxX - 40.0, y: inputPanelFrameValue.minY + 9.0), size: CGSize(width: 32.0, height: 32.0)).insetBy(dx: -4.0, dy: -4.0)
} else {
reactionsAnchorRect = CGRect()
}
reactionsAnchorRect = CGRect()
}
var effectiveDisplayReactions = false
@ -4114,7 +4148,7 @@ public final class StoryItemSetContainerComponent: Component {
effectiveDisplayReactions = false
}
if let reactionContextNode = self.reactionContextNode, (reactionContextNode.isReactionSearchActive && !reactionContextNode.isAnimatingOutToReaction && !reactionContextNode.isAnimatingOut) {
if let reactionContextNode = self.reactionContextNode, self.willDismissReactionContextNode !== reactionContextNode, (reactionContextNode.isReactionSearchActive && !reactionContextNode.isAnimatingOutToReaction && !reactionContextNode.isAnimatingOut) {
effectiveDisplayReactions = true
}
@ -4203,11 +4237,11 @@ public final class StoryItemSetContainerComponent: Component {
}
}
reactionContextNode.reactionSelected = { [weak self] updateReaction, _ in
guard let self else {
reactionContextNode.reactionSelected = { [weak self, weak reactionContextNode] updateReaction, _ in
guard let self, let reactionContextNode else {
return
}
let action: () -> Void = { [weak self] in
let action: () -> Void = { [weak self, weak reactionContextNode] in
guard let self, let component = self.component else {
return
}
@ -4265,7 +4299,6 @@ public final class StoryItemSetContainerComponent: Component {
}, completion: { [weak targetView, weak reactionContextNode] in
targetView?.removeFromSuperview()
if let reactionContextNode {
reactionContextNode.layer.animateScale(from: 1.0, to: 0.001, duration: 0.3, removeOnCompletion: false)
reactionContextNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { [weak reactionContextNode] _ in
reactionContextNode?.view.removeFromSuperview()
})
@ -4486,7 +4519,7 @@ public final class StoryItemSetContainerComponent: Component {
if let reactionContextNode = self.disappearingReactionContextNode {
if !reactionContextNode.isAnimatingOutToReaction {
transition.setFrame(view: reactionContextNode.view, frame: CGRect(origin: CGPoint(), size: availableSize))
reactionContextNode.updateLayout(size: availableSize, insets: UIEdgeInsets(), anchorRect: reactionsAnchorRect, centerAligned: true, isCoveredByInput: false, isAnimatingOut: false, transition: transition.containedViewLayoutTransition)
reactionContextNode.updateLayout(size: availableSize, insets: UIEdgeInsets(), anchorRect: reactionsAnchorRect, centerAligned: reactionContextNode.centerAligned, isCoveredByInput: false, isAnimatingOut: false, transition: transition.containedViewLayoutTransition)
}
}
@ -5852,6 +5885,51 @@ public final class StoryItemSetContainerComponent: Component {
})))
}
var isHidden = false
if case let .channel(channel) = component.slice.peer, let storiesHidden = channel.storiesHidden {
isHidden = storiesHidden
}
items.append(.action(ContextMenuActionItem(text: isHidden ? component.strings.StoryFeed_ContextUnarchive : component.strings.StoryFeed_ContextArchive, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: isHidden ? "Chat/Context Menu/Unarchive" : "Chat/Context Menu/Archive"), color: theme.contextMenu.primaryColor)
}, action: { [weak self] _, a in
a(.default)
guard let self, let component = self.component else {
return
}
let _ = component.context.engine.peers.updatePeerStoriesHidden(id: component.slice.peer.id, isHidden: !isHidden)
let text = !isHidden ? component.strings.StoryFeed_TooltipArchive(component.slice.peer.compactDisplayTitle).string : component.strings.StoryFeed_TooltipUnarchive(component.slice.peer.compactDisplayTitle).string
let tooltipScreen = TooltipScreen(
context: component.context,
account: component.context.account,
sharedContext: component.context.sharedContext,
text: .markdown(text: text),
style: .customBlur(UIColor(rgb: 0x1c1c1c), 0.0),
icon: .peer(peer: component.slice.peer, isStory: true),
action: TooltipScreen.Action(
title: component.strings.Undo_Undo,
action: {
component.context.engine.peers.updatePeerStoriesHidden(id: component.slice.peer.id, isHidden: isHidden)
}
),
location: .bottom,
shouldDismissOnTouch: { _, _ in return .dismiss(consume: false) }
)
tooltipScreen.willBecomeDismissed = { [weak self] _ in
guard let self else {
return
}
self.sendMessageContext.tooltipScreen = nil
self.updateIsProgressPaused()
}
self.sendMessageContext.tooltipScreen?.dismiss()
self.sendMessageContext.tooltipScreen = tooltipScreen
self.updateIsProgressPaused()
component.controller()?.present(tooltipScreen, in: .current)
})))
if (component.slice.item.storyItem.isMy && channel.hasPermission(.postStories)) || channel.hasPermission(.deleteStories) {
items.append(.action(ContextMenuActionItem(text: component.strings.Story_ContextDeleteStory, textColor: .destructive, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.contextMenu.destructiveColor)
@ -6004,14 +6082,27 @@ public final class StoryItemSetContainerComponent: Component {
), nil)
}
})))
var isHidden = false
if case let .user(user) = component.slice.peer, let storiesHidden = user.storiesHidden {
isHidden = storiesHidden
} else if case let .channel(channel) = component.slice.peer, let storiesHidden = channel.storiesHidden {
isHidden = storiesHidden
}
var isHidden = false
if case let .user(user) = component.slice.peer, let storiesHidden = user.storiesHidden {
isHidden = storiesHidden
} else if case let .channel(channel) = component.slice.peer, let storiesHidden = channel.storiesHidden {
isHidden = storiesHidden
}
var canArchive = false
if isHidden {
canArchive = true
} else {
if case .user = component.slice.peer, !component.slice.peer.isService {
canArchive = true
} else if case .channel = component.slice.peer {
canArchive = true
}
}
if canArchive {
items.append(.action(ContextMenuActionItem(text: isHidden ? component.strings.StoryFeed_ContextUnarchive : component.strings.StoryFeed_ContextArchive, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: isHidden ? "Chat/Context Menu/Unarchive" : "Chat/Context Menu/Archive"), color: theme.contextMenu.primaryColor)
}, action: { [weak self] _, a in

View File

@ -472,10 +472,21 @@ public final class StoryPeerListComponent: Component {
}
}
public func cancelLoadingItem() {
self.loadingItemDisposable?.dispose()
self.loadingItemDisposable = nil
if self.loadingItemId != nil {
self.loadingItemId = nil
self.state?.updated(transition: .immediate)
}
}
public func setLoadingItem(peerId: EnginePeer.Id, signal: Signal<Never, NoError>) {
var applyLoadingItem = true
self.loadingItemDisposable?.dispose()
self.loadingItemDisposable = (signal |> deliverOnMainQueue).start(completed: { [weak self] in
let loadingItemDisposable = (signal |> deliverOnMainQueue).start(completed: { [weak self] in
guard let self else {
return
}
@ -483,6 +494,7 @@ public final class StoryPeerListComponent: Component {
applyLoadingItem = false
self.state?.updated(transition: .immediate)
})
self.loadingItemDisposable = loadingItemDisposable
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.2, execute: { [weak self] in
guard let self else {
@ -1285,10 +1297,9 @@ public final class StoryPeerListComponent: Component {
titleContentOffset = collapsedTitleOffset
} else {
titleContentOffset = titleMinContentOffset.interpolate(to: ((itemLayout.containerSize.width - collapsedState.titleWidth) * 0.5) as CGFloat, amount: min(1.0, collapsedState.maxFraction) * (1.0 - collapsedState.activityFraction))
titleContentOffset += -expandBoundsFraction * 4.0
}
titleContentOffset += -expandBoundsFraction * 4.0
var titleIndicatorSize: CGSize?
if collapsedState.activityFraction != 0.0 {
let collapsedItemMinX = collapsedContentOrigin - collapsedItemWidth * 0.5

View File

@ -266,7 +266,7 @@ public final class TelegramRootController: NavigationController, TelegramRootCon
}
@discardableResult
public func openStoryCamera(transitionIn: StoryCameraTransitionIn?, transitionedIn: @escaping () -> Void, transitionOut: @escaping (Stories.PendingTarget?) -> StoryCameraTransitionOut?) -> StoryCameraTransitionInCoordinator? {
public func openStoryCamera(transitionIn: StoryCameraTransitionIn?, transitionedIn: @escaping () -> Void, transitionOut: @escaping (Stories.PendingTarget?, Bool) -> StoryCameraTransitionOut?) -> StoryCameraTransitionInCoordinator? {
guard let controller = self.viewControllers.last as? ViewController else {
return nil
}
@ -275,6 +275,8 @@ public final class TelegramRootController: NavigationController, TelegramRootCon
let context = self.context
var storyTarget: Stories.PendingTarget?
var isPeerArchived = false
var updatedTransitionOut: ((Stories.PendingTarget?, Bool) -> StoryCameraTransitionOut?)?
var presentImpl: ((ViewController) -> Void)?
var returnToCameraImpl: (() -> Void)?
@ -295,7 +297,7 @@ public final class TelegramRootController: NavigationController, TelegramRootCon
}
},
transitionOut: { finished in
if let transitionOut = transitionOut(finished ? storyTarget : nil), let destinationView = transitionOut.destinationView {
if let transitionOut = (updatedTransitionOut ?? transitionOut)(finished ? storyTarget : nil, isPeerArchived), let destinationView = transitionOut.destinationView {
return CameraScreen.TransitionOut(
destinationView: destinationView,
destinationRect: transitionOut.destinationRect,
@ -353,7 +355,7 @@ public final class TelegramRootController: NavigationController, TelegramRootCon
isEditing: false,
transitionIn: transitionIn,
transitionOut: { finished, isNew in
if finished, let transitionOut = transitionOut(storyTarget), let destinationView = transitionOut.destinationView {
if finished, let transitionOut = (updatedTransitionOut ?? transitionOut)(storyTarget, false), let destinationView = transitionOut.destinationView {
return MediaEditorScreen.TransitionOut(
destinationView: destinationView,
destinationRect: transitionOut.destinationRect,
@ -375,13 +377,6 @@ public final class TelegramRootController: NavigationController, TelegramRootCon
return
}
let context = self.context
if let rootTabController = self.rootTabController {
if let index = rootTabController.controllers.firstIndex(where: { $0 is ChatListController}) {
rootTabController.selectedIndex = index
}
}
let target: Stories.PendingTarget
let targetPeerId: EnginePeer.Id
if let sendAsPeerId = options.sendAsPeerId {
@ -393,84 +388,118 @@ public final class TelegramRootController: NavigationController, TelegramRootCon
}
storyTarget = target
let completionImpl: () -> Void = { [weak self] in
guard let self else {
let _ = (self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: targetPeerId))
|> deliverOnMainQueue).start(next: { [weak self] peer in
guard let self, let peer else {
return
}
if let chatListController = self.chatListController as? ChatListControllerImpl {
let _ = (chatListController.hasPendingStories
|> filter { $0 }
|> take(1)
|> timeout(0.25, queue: .mainQueue(), alternate: .single(true))
|> deliverOnMainQueue).start(completed: { [weak chatListController] in
guard let chatListController else {
return
}
if case let .user(user) = peer {
isPeerArchived = user.storiesHidden ?? false
} else if case let .channel(channel) = peer {
isPeerArchived = channel.storiesHidden ?? false
}
let context = self.context
if let rootTabController = self.rootTabController {
if let index = rootTabController.controllers.firstIndex(where: { $0 is ChatListController}) {
rootTabController.selectedIndex = index
}
}
let completionImpl: () -> Void = { [weak self] in
guard let self else {
return
}
var chatListController: ChatListControllerImpl?
if isPeerArchived {
var viewControllers = self.viewControllers
chatListController.scrollToStories(peerId: targetPeerId)
let archiveController = ChatListControllerImpl(context: context, location: .chatList(groupId: .archive), controlsHistoryPreload: false, hideNetworkActivityStatus: false, previewing: false, enableDebugActions: false)
updatedTransitionOut = archiveController.storyCameraTransitionOut()
chatListController = archiveController
viewControllers.insert(archiveController, at: 1)
self.setViewControllers(viewControllers, animated: false)
} else {
chatListController = self.chatListController as? ChatListControllerImpl
}
if let chatListController {
let _ = (chatListController.hasPendingStories
|> filter { $0 }
|> take(1)
|> timeout(isPeerArchived ? 0.5 : 0.25, queue: .mainQueue(), alternate: .single(true))
|> deliverOnMainQueue).start(completed: { [weak chatListController] in
guard let chatListController else {
return
}
chatListController.scrollToStories(peerId: targetPeerId)
Queue.mainQueue().justDispatch {
commit({})
}
})
} else {
Queue.mainQueue().justDispatch {
commit({})
}
})
} else {
Queue.mainQueue().justDispatch {
commit({})
}
}
}
if let _ = self.chatListController as? ChatListControllerImpl {
switch mediaResult {
case let .image(image, dimensions):
if let imageData = compressImageToJPEG(image, quality: 0.7) {
let entities = generateChatInputTextEntities(caption)
Logger.shared.log("MediaEditor", "Calling uploadStory for image, randomId \(randomId)")
let _ = (context.engine.messages.uploadStory(target: target, media: .image(dimensions: dimensions, data: imageData, stickers: stickers), mediaAreas: mediaAreas, text: caption.string, entities: entities, pin: options.pin, privacy: options.privacy, isForwardingDisabled: options.isForwardingDisabled, period: options.timeout, randomId: randomId)
|> deliverOnMainQueue).start(next: { stableId in
moveStorySource(engine: context.engine, peerId: context.account.peerId, from: randomId, to: Int64(stableId))
})
completionImpl()
}
case let .video(content, firstFrameImage, values, duration, dimensions):
let adjustments: VideoMediaResourceAdjustments
if let valuesData = try? JSONEncoder().encode(values) {
let data = MemoryBuffer(data: valuesData)
let digest = MemoryBuffer(data: data.md5Digest())
adjustments = VideoMediaResourceAdjustments(data: data, digest: digest, isStory: true)
let resource: TelegramMediaResource
switch content {
case let .imageFile(path):
resource = LocalFileVideoMediaResource(randomId: Int64.random(in: .min ... .max), path: path, adjustments: adjustments)
case let .videoFile(path):
resource = LocalFileVideoMediaResource(randomId: Int64.random(in: .min ... .max), path: path, adjustments: adjustments)
case let .asset(localIdentifier):
resource = VideoLibraryMediaResource(localIdentifier: localIdentifier, conversion: .compress(adjustments))
if let _ = self.chatListController as? ChatListControllerImpl {
switch mediaResult {
case let .image(image, dimensions):
if let imageData = compressImageToJPEG(image, quality: 0.7) {
let entities = generateChatInputTextEntities(caption)
Logger.shared.log("MediaEditor", "Calling uploadStory for image, randomId \(randomId)")
let _ = (context.engine.messages.uploadStory(target: target, media: .image(dimensions: dimensions, data: imageData, stickers: stickers), mediaAreas: mediaAreas, text: caption.string, entities: entities, pin: options.pin, privacy: options.privacy, isForwardingDisabled: options.isForwardingDisabled, period: options.timeout, randomId: randomId)
|> deliverOnMainQueue).start(next: { stableId in
moveStorySource(engine: context.engine, peerId: context.account.peerId, from: randomId, to: Int64(stableId))
})
completionImpl()
}
let imageData = firstFrameImage.flatMap { compressImageToJPEG($0, quality: 0.6) }
let firstFrameFile = imageData.flatMap { data -> TempBoxFile? in
let file = TempBox.shared.tempFile(fileName: "image.jpg")
if let _ = try? data.write(to: URL(fileURLWithPath: file.path)) {
return file
} else {
return nil
case let .video(content, firstFrameImage, values, duration, dimensions):
let adjustments: VideoMediaResourceAdjustments
if let valuesData = try? JSONEncoder().encode(values) {
let data = MemoryBuffer(data: valuesData)
let digest = MemoryBuffer(data: data.md5Digest())
adjustments = VideoMediaResourceAdjustments(data: data, digest: digest, isStory: true)
let resource: TelegramMediaResource
switch content {
case let .imageFile(path):
resource = LocalFileVideoMediaResource(randomId: Int64.random(in: .min ... .max), path: path, adjustments: adjustments)
case let .videoFile(path):
resource = LocalFileVideoMediaResource(randomId: Int64.random(in: .min ... .max), path: path, adjustments: adjustments)
case let .asset(localIdentifier):
resource = VideoLibraryMediaResource(localIdentifier: localIdentifier, conversion: .compress(adjustments))
}
let imageData = firstFrameImage.flatMap { compressImageToJPEG($0, quality: 0.6) }
let firstFrameFile = imageData.flatMap { data -> TempBoxFile? in
let file = TempBox.shared.tempFile(fileName: "image.jpg")
if let _ = try? data.write(to: URL(fileURLWithPath: file.path)) {
return file
} else {
return nil
}
}
Logger.shared.log("MediaEditor", "Calling uploadStory for video, randomId \(randomId)")
let entities = generateChatInputTextEntities(caption)
let _ = (context.engine.messages.uploadStory(target: target, media: .video(dimensions: dimensions, duration: duration, resource: resource, firstFrameFile: firstFrameFile, stickers: stickers), mediaAreas: mediaAreas, text: caption.string, entities: entities, pin: options.pin, privacy: options.privacy, isForwardingDisabled: options.isForwardingDisabled, period: options.timeout, randomId: randomId)
|> deliverOnMainQueue).start(next: { stableId in
moveStorySource(engine: context.engine, peerId: context.account.peerId, from: randomId, to: Int64(stableId))
})
completionImpl()
}
Logger.shared.log("MediaEditor", "Calling uploadStory for video, randomId \(randomId)")
let entities = generateChatInputTextEntities(caption)
let _ = (context.engine.messages.uploadStory(target: target, media: .video(dimensions: dimensions, duration: duration, resource: resource, firstFrameFile: firstFrameFile, stickers: stickers), mediaAreas: mediaAreas, text: caption.string, entities: entities, pin: options.pin, privacy: options.privacy, isForwardingDisabled: options.isForwardingDisabled, period: options.timeout, randomId: randomId)
|> deliverOnMainQueue).start(next: { stableId in
moveStorySource(engine: context.engine, peerId: context.account.peerId, from: randomId, to: Int64(stableId))
})
completionImpl()
}
}
}
dismissCameraImpl?()
dismissCameraImpl?()
})
} as (Int64, MediaEditorScreen.Result?, [MediaArea], NSAttributedString, MediaEditorResultPrivacy, [TelegramMediaFile], @escaping (@escaping () -> Void) -> Void) -> Void
)
controller.cancelled = { showDraftTooltip in