[WIP] Stories

This commit is contained in:
Ali 2023-06-12 21:11:51 +03:00
parent d1817dca18
commit ab8d40b940
3 changed files with 341 additions and 34 deletions

View File

@ -593,10 +593,18 @@ public final class StoryItemSetContainerComponent: Component {
}
func activateInput() {
guard let component = self.component else {
return
}
if component.slice.peer.id == component.context.account.peerId {
self.displayViewList = true
self.state?.updated(transition: Transition(animation: .curve(duration: 0.4, curve: .spring)))
} else {
if let inputPanelView = self.inputPanel.view as? MessageInputPanelComponent.View {
inputPanelView.activateInput()
}
}
}
func animateIn(transitionIn: StoryContainerScreen.TransitionIn) {
self.closeButton.layer.animateScale(from: 0.001, to: 1.0, duration: 0.2, delay: 0.12, timingFunction: kCAMediaTimingFunctionSpring)
@ -621,6 +629,16 @@ public final class StoryItemSetContainerComponent: Component {
)
footerPanelView.layer.animateAlpha(from: 0.0, to: footerPanelView.alpha, duration: 0.28)
}
if let viewListView = self.viewList?.view.view {
viewListView.layer.animatePosition(
from: CGPoint(x: 0.0, y: self.bounds.height - self.contentContainerView.frame.maxY),
to: CGPoint(),
duration: 0.3,
timingFunction: kCAMediaTimingFunctionSpring,
additive: true
)
viewListView.layer.animateAlpha(from: 0.0, to: viewListView.alpha, duration: 0.28)
}
if let captionItemView = self.captionItem?.view.view {
captionItemView.layer.animatePosition(
from: CGPoint(x: 0.0, y: self.bounds.height - captionItemView.frame.minY),
@ -704,6 +722,16 @@ public final class StoryItemSetContainerComponent: Component {
)
footerPanelView.layer.animateAlpha(from: footerPanelView.alpha, to: 0.0, duration: 0.3, removeOnCompletion: false)
}
if let viewListView = self.viewList?.view.view {
viewListView.layer.animatePosition(
from: CGPoint(),
to: CGPoint(x: 0.0, y: self.bounds.height - self.contentContainerView.frame.maxY),
duration: 0.3,
timingFunction: kCAMediaTimingFunctionSpring,
additive: true
)
viewListView.layer.animateAlpha(from: 0.0, to: viewListView.alpha, duration: 0.28)
}
if let captionItemView = self.captionItem?.view.view {
captionItemView.layer.animatePosition(
from: CGPoint(),
@ -1270,6 +1298,13 @@ public final class StoryItemSetContainerComponent: Component {
self.viewList = viewList
}
let outerExpansionFraction: CGFloat
if self.displayViewList {
outerExpansionFraction = 1.0
} else {
outerExpansionFraction = component.verticalPanFraction
}
viewList.view.parentState = state
let viewListSize = viewList.view.update(
transition: viewListTransition,
@ -1280,13 +1315,235 @@ public final class StoryItemSetContainerComponent: Component {
strings: component.strings,
safeInsets: component.safeInsets,
storyItem: component.slice.item.storyItem,
outerExpansionFraction: component.verticalPanFraction,
outerExpansionFraction: outerExpansionFraction,
close: { [weak self] in
guard let self else {
return
}
self.displayViewList = false
self.state?.updated(transition: Transition(animation: .curve(duration: 0.4, curve: .spring)))
},
expandViewStats: { [weak self] in
guard let self else {
return
}
if !self.displayViewList {
self.displayViewList = true
self.state?.updated(transition: Transition(animation: .curve(duration: 0.4, curve: .spring)))
}
},
deleteAction: { [weak self] in
guard let self, let component = self.component else {
return
}
let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: component.theme)
let actionSheet = ActionSheetController(presentationData: presentationData)
actionSheet.setItemGroups([
ActionSheetItemGroup(items: [
ActionSheetButtonItem(title: "Delete", color: .destructive, action: { [weak self, weak actionSheet] in
actionSheet?.dismissAnimated()
guard let self, let component = self.component else {
return
}
component.delete()
/*if let currentSlice = self.currentSlice, let index = currentSlice.items.firstIndex(where: { $0.id == focusedItemId }) {
let item = currentSlice.items[index]
if currentSlice.items.count == 1 {
component.navigateToItemSet(.next)
} else {
var nextIndex: Int = index + 1
if nextIndex >= currentSlice.items.count {
nextIndex = currentSlice.items.count - 1
}
self.focusedItemId = currentSlice.items[nextIndex].id
currentSlice.items[nextIndex].markAsSeen?()
self.state?.updated(transition: .immediate)
}
item.delete?()
}*/
})
]),
ActionSheetItemGroup(items: [
ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
})
])
])
actionSheet.dismissed = { [weak self] _ in
guard let self else {
return
}
self.actionSheet = nil
self.updateIsProgressPaused()
}
self.actionSheet = actionSheet
self.updateIsProgressPaused()
component.presentController(actionSheet)
},
moreAction: { [weak self] sourceView, gesture in
guard let self, let component = self.component, let controller = component.controller() else {
return
}
var items: [ContextMenuItem] = []
let additionalCount = component.slice.item.storyItem.privacy?.additionallyIncludePeers.count ?? 0
let privacyText: String
switch component.slice.item.storyItem.privacy?.base {
case .closeFriends:
if additionalCount != 0 {
privacyText = "Close Friends (+\(additionalCount)"
} else {
privacyText = "Close Friends"
}
case .contacts:
if additionalCount != 0 {
privacyText = "Contacts (+\(additionalCount)"
} else {
privacyText = "Contacts"
}
case .nobody:
if additionalCount != 0 {
if additionalCount == 1 {
privacyText = "\(additionalCount) Person"
} else {
privacyText = "\(additionalCount) People"
}
} else {
privacyText = "Only Me"
}
default:
privacyText = "Everyone"
}
items.append(.action(ContextMenuActionItem(text: "Who can see", textLayout: .secondLineWithValue(privacyText), icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Channels"), color: theme.contextMenu.primaryColor)
}, action: { [weak self] _, a in
a(.default)
guard let self else {
return
}
self.openItemPrivacySettings()
})))
items.append(.action(ContextMenuActionItem(text: "Edit Story", icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Edit"), color: theme.contextMenu.primaryColor)
}, action: { [weak self] _, a in
a(.default)
guard let self else {
return
}
self.openStoryEditing()
})))
items.append(.separator)
component.controller()?.forEachController { c in
if let c = c as? UndoOverlayController {
c.dismiss()
}
return true
}
items.append(.action(ContextMenuActionItem(text: component.slice.item.storyItem.isPinned ? "Remove from profile" : "Save to profile", icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: component.slice.item.storyItem.isPinned ? "Chat/Context Menu/Check" : "Chat/Context Menu/Add"), color: theme.contextMenu.primaryColor)
}, action: { [weak self] _, a in
a(.default)
guard let self, let component = self.component else {
return
}
let _ = component.context.engine.messages.updateStoriesArePinned(ids: [component.slice.item.storyItem.id: component.slice.item.storyItem], isPinned: !component.slice.item.storyItem.isPinned).start()
if component.slice.item.storyItem.isPinned {
let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: component.theme)
self.component?.presentController(UndoOverlayController(
presentationData: presentationData,
content: .info(title: nil, text: "Story removed from your profile", timeout: nil),
elevatedLayout: false,
animateInAsReplacement: false,
action: { _ in return false }
))
} else {
let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: component.theme)
self.component?.presentController(UndoOverlayController(
presentationData: presentationData,
content: .info(title: "Story saved to your profile", text: "Saved stories can be viewed by others on your profile until you remove them.", timeout: nil),
elevatedLayout: false,
animateInAsReplacement: false,
action: { _ in return false }
))
}
})))
items.append(.action(ContextMenuActionItem(text: "Save image", icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Save"), color: theme.contextMenu.primaryColor)
}, action: { _, a in
a(.default)
})))
if component.slice.item.storyItem.isPublic {
items.append(.action(ContextMenuActionItem(text: "Copy link", icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Link"), color: theme.contextMenu.primaryColor)
}, action: { [weak self] _, a in
a(.default)
guard let self, let component = self.component else {
return
}
let _ = (component.context.engine.messages.exportStoryLink(peerId: component.slice.peer.id, id: component.slice.item.storyItem.id)
|> deliverOnMainQueue).start(next: { [weak self] link in
guard let self, let component = self.component else {
return
}
if let link {
UIPasteboard.general.string = link
let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: component.theme)
component.presentController(UndoOverlayController(
presentationData: presentationData,
content: .linkCopied(text: "Link copied."),
elevatedLayout: false,
animateInAsReplacement: false,
action: { _ in return false }
))
}
})
})))
items.append(.action(ContextMenuActionItem(text: "Share", icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Forward"), color: theme.contextMenu.primaryColor)
}, action: { _, a in
a(.default)
})))
}
let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: component.theme)
let contextController = ContextController(account: component.context.account, presentationData: presentationData, source: .reference(HeaderContextReferenceContentSource(controller: controller, sourceView: sourceView)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture)
contextController.dismissed = { [weak self] in
guard let self else {
return
}
self.contextController = nil
self.updateIsProgressPaused()
}
self.contextController = contextController
self.updateIsProgressPaused()
controller.present(contextController, in: .window(.root))
}
)),
environment: {},

View File

@ -29,6 +29,9 @@ final class StoryItemSetViewListComponent: Component {
let storyItem: EngineStoryItem
let outerExpansionFraction: CGFloat
let close: () -> Void
let expandViewStats: () -> Void
let deleteAction: () -> Void
let moreAction: (UIView, ContextGesture?) -> Void
init(
externalState: ExternalState,
@ -38,7 +41,10 @@ final class StoryItemSetViewListComponent: Component {
safeInsets: UIEdgeInsets,
storyItem: EngineStoryItem,
outerExpansionFraction: CGFloat,
close: @escaping () -> Void
close: @escaping () -> Void,
expandViewStats: @escaping () -> Void,
deleteAction: @escaping () -> Void,
moreAction: @escaping (UIView, ContextGesture?) -> Void
) {
self.externalState = externalState
self.context = context
@ -48,6 +54,9 @@ final class StoryItemSetViewListComponent: Component {
self.storyItem = storyItem
self.outerExpansionFraction = outerExpansionFraction
self.close = close
self.expandViewStats = expandViewStats
self.deleteAction = deleteAction
self.moreAction = moreAction
}
static func ==(lhs: StoryItemSetViewListComponent, rhs: StoryItemSetViewListComponent) -> Bool {
@ -284,6 +293,11 @@ final class StoryItemSetViewListComponent: Component {
}
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
if let navigationPanelView = self.navigationPanel.view {
if let result = navigationPanelView.hitTest(self.convert(point, to: navigationPanelView), with: event) {
return result
}
}
if !self.backgroundView.frame.contains(point) {
return nil
}
@ -478,7 +492,7 @@ final class StoryItemSetViewListComponent: Component {
self.component = component
self.state = state
let minimizedHeight = min(availableSize.height, 488.0)
let minimizedHeight = min(availableSize.height, 500.0)
if themeUpdated {
self.backgroundView.backgroundColor = component.theme.rootController.navigationBar.blurredBackgroundColor
@ -512,7 +526,7 @@ final class StoryItemSetViewListComponent: Component {
let sideInset: CGFloat = 16.0
let navigationHeight: CGFloat = 56.0
let navigationBarFrame = CGRect(origin: CGPoint(x: 0.0, y: availableSize.height - minimizedHeight), size: CGSize(width: availableSize.width, height: navigationHeight))
let navigationBarFrame = CGRect(origin: CGPoint(x: 0.0, y: availableSize.height - minimizedHeight + 12.0), size: CGSize(width: availableSize.width, height: navigationHeight))
transition.setFrame(view: self.navigationBarBackground, frame: navigationBarFrame)
self.navigationBarBackground.update(size: navigationBarFrame.size, cornerRadius: 10.0, maskedCorners: [.layerMinXMinYCorner, .layerMaxXMinYCorner], transition: transition.containedViewLayoutTransition)
@ -540,29 +554,41 @@ final class StoryItemSetViewListComponent: Component {
transition.setFrame(view: navigationLeftButtonView, frame: navigationLeftButtonFrame)
}
let expansionOffset = availableSize.height - self.navigationBarBackground.frame.minY
var dismissOffsetY: CGFloat = 0.0
if let dismissPanState = self.dismissPanState {
dismissOffsetY = -dismissPanState.accumulatedOffset
}
dismissOffsetY -= (1.0 - component.outerExpansionFraction) * expansionOffset
let dismissFraction: CGFloat = 1.0 - max(0.0, min(1.0, -dismissOffsetY / expansionOffset))
let navigationPanelSize = self.navigationPanel.update(
transition: transition,
component: AnyComponent(StoryFooterPanelComponent(
context: component.context,
storyItem: component.storyItem,
expandFraction: dismissFraction,
expandViewStats: { [weak self] in
guard let self else {
guard let self, let component = self.component else {
return
}
let _ = self
component.expandViewStats()
},
deleteAction: { [weak self] in
guard let self, let component = self.component else {
return
}
let _ = component
component.deleteAction()
},
moreAction: { [weak self] sourceView, gesture in
guard let self, let component = self.component else {
return
}
let _ = component
component.moreAction(sourceView, gesture)
}
)),
environment: {},
@ -573,21 +599,12 @@ final class StoryItemSetViewListComponent: Component {
self.addSubview(navigationPanelView)
}
let expandedNavigationPanelFrame = CGRect(origin: navigationBarFrame.origin, size: navigationPanelSize)
let collapsedNavigationPanelFrame = CGRect(origin: CGPoint(x: navigationBarFrame.minX, y: availableSize.height - navigationPanelSize.height), size: navigationPanelSize)
let expandedNavigationPanelFrame = CGRect(origin: CGPoint(x: navigationBarFrame.minX, y: navigationBarFrame.minY + 4.0), size: navigationPanelSize)
let collapsedNavigationPanelFrame = CGRect(origin: CGPoint(x: navigationBarFrame.minX, y: navigationBarFrame.minY - navigationPanelSize.height - component.safeInsets.bottom - 1.0), size: navigationPanelSize)
transition.setFrame(view: navigationPanelView, frame: collapsedNavigationPanelFrame.interpolate(to: expandedNavigationPanelFrame, amount: component.outerExpansionFraction))
transition.setFrame(view: navigationPanelView, frame: collapsedNavigationPanelFrame.interpolate(to: expandedNavigationPanelFrame, amount: dismissFraction))
}
/*let navigationTitleFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - navigationTitleSize.width) * 0.5), y: navigationBarFrame.minY + floor((navigationBarFrame.height - navigationTitleSize.height) * 0.5)), size: navigationTitleSize)
if let navigationTitleView = self.navigationTitle.view {
if navigationTitleView.superview == nil {
self.addSubview(navigationTitleView)
}
transition.setPosition(view: navigationTitleView, position: navigationTitleFrame.center)
transition.setBounds(view: navigationTitleView, bounds: CGRect(origin: CGPoint(), size: navigationTitleFrame.size))
}*/
transition.setFrame(view: self.backgroundView, frame: CGRect(origin: CGPoint(x: 0.0, y: navigationBarFrame.maxY), size: CGSize(width: availableSize.width, height: availableSize.height)))
let measureItemSize = self.measureItem.update(
@ -663,18 +680,12 @@ final class StoryItemSetViewListComponent: Component {
self.ignoreScrolling = false
self.updateScrolling(transition: transition)
var dismissOffsetY: CGFloat = 0.0
if let dismissPanState = self.dismissPanState {
dismissOffsetY = -dismissPanState.accumulatedOffset
}
let expansionOffset = availableSize.height - self.navigationBarBackground.frame.minY
dismissOffsetY -= (1.0 - component.outerExpansionFraction) * expansionOffset
transition.setBoundsOrigin(view: self, origin: CGPoint(x: 0.0, y: dismissOffsetY))
component.externalState.minimizedHeight = minimizedHeight
component.externalState.effectiveHeight = min(minimizedHeight, max(0.0, minimizedHeight + dismissOffsetY))
let effectiveHeight: CGFloat = minimizedHeight * dismissFraction + (1.0 - dismissFraction) * (60.0 + component.safeInsets.bottom + 1.0)
component.externalState.effectiveHeight = min(minimizedHeight, max(0.0, effectiveHeight))
return availableSize
}

View File

@ -12,6 +12,7 @@ import MoreHeaderButton
public final class StoryFooterPanelComponent: Component {
public let context: AccountContext
public let storyItem: EngineStoryItem?
public let expandFraction: CGFloat
public let expandViewStats: () -> Void
public let deleteAction: () -> Void
public let moreAction: (UIView, ContextGesture?) -> Void
@ -19,6 +20,7 @@ public final class StoryFooterPanelComponent: Component {
public init(
context: AccountContext,
storyItem: EngineStoryItem?,
expandFraction: CGFloat,
expandViewStats: @escaping () -> Void,
deleteAction: @escaping () -> Void,
moreAction: @escaping (UIView, ContextGesture?) -> Void
@ -26,6 +28,7 @@ public final class StoryFooterPanelComponent: Component {
self.context = context
self.storyItem = storyItem
self.expandViewStats = expandViewStats
self.expandFraction = expandFraction
self.deleteAction = deleteAction
self.moreAction = moreAction
}
@ -37,12 +40,16 @@ public final class StoryFooterPanelComponent: Component {
if lhs.storyItem != rhs.storyItem {
return false
}
if lhs.expandFraction != rhs.expandFraction {
return false
}
return true
}
public final class View: UIView {
private let viewStatsButton: HighlightableButton
private let viewStatsText = ComponentView<Empty>()
private let viewStatsExpandedText = ComponentView<Empty>()
private let deleteButton = ComponentView<Empty>()
private var moreButton: MoreHeaderButton?
@ -98,6 +105,8 @@ public final class StoryFooterPanelComponent: Component {
let avatarsNodeFrame = CGRect(origin: CGPoint(x: leftOffset, y: floor((size.height - avatarsSize.height) * 0.5)), size: avatarsSize)
self.avatarsNode.frame = avatarsNodeFrame
//transition.setScale(view: self.avatarsNode.view, scale: CGFloat(1.0).interpolate(to: 0.001, amount: component.expandFraction))
transition.setAlpha(view: self.avatarsNode.view, alpha: pow(1.0 - component.expandFraction, 1.0))
if !avatarsSize.width.isZero {
leftOffset = avatarsNodeFrame.maxX + avatarSpacing
}
@ -124,18 +133,45 @@ public final class StoryFooterPanelComponent: Component {
environment: {},
containerSize: CGSize(width: availableSize.width, height: size.height)
)
let viewStatsTextFrame = CGRect(origin: CGPoint(x: leftOffset, y: floor((size.height - viewStatsTextSize.height) * 0.5)), size: viewStatsTextSize)
let viewStatsExpandedTextSize = self.viewStatsExpandedText.update(
transition: .immediate,
component: AnyComponent(Text(text: viewsText, font: Font.semibold(17.0), color: .white)),
environment: {},
containerSize: CGSize(width: availableSize.width, height: size.height)
)
let viewStatsCollapsedFrame = CGRect(origin: CGPoint(x: leftOffset, y: floor((size.height - viewStatsTextSize.height) * 0.5)), size: viewStatsTextSize)
let viewStatsExpandedFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - viewStatsExpandedTextSize.width) * 0.5), y: floor((size.height - viewStatsExpandedTextSize.height) * 0.5)), size: viewStatsExpandedTextSize)
let viewStatsCurrentFrame = viewStatsCollapsedFrame.interpolate(to: viewStatsExpandedFrame, amount: component.expandFraction)
let viewStatsTextCenter = viewStatsCollapsedFrame.center.interpolate(to: viewStatsExpandedFrame.center, amount: component.expandFraction)
let viewStatsTextFrame = viewStatsCollapsedFrame.size.centered(around: viewStatsTextCenter)
if let viewStatsTextView = self.viewStatsText.view {
if viewStatsTextView.superview == nil {
viewStatsTextView.layer.anchorPoint = CGPoint()
viewStatsTextView.isUserInteractionEnabled = false
self.viewStatsButton.addSubview(viewStatsTextView)
}
transition.setPosition(view: viewStatsTextView, position: viewStatsTextFrame.origin)
transition.setPosition(view: viewStatsTextView, position: viewStatsTextFrame.center)
transition.setBounds(view: viewStatsTextView, bounds: CGRect(origin: CGPoint(), size: viewStatsTextFrame.size))
transition.setAlpha(view: viewStatsTextView, alpha: pow(1.0 - component.expandFraction, 1.2))
transition.setScale(view: viewStatsTextView, scale: viewStatsCurrentFrame.width / viewStatsTextFrame.width)
}
let viewStatsExpandedTextFrame = viewStatsExpandedFrame.size.centered(around: viewStatsTextCenter)
if let viewStatsExpandedTextView = self.viewStatsExpandedText.view {
if viewStatsExpandedTextView.superview == nil {
viewStatsExpandedTextView.isUserInteractionEnabled = false
self.viewStatsButton.addSubview(viewStatsExpandedTextView)
}
transition.setPosition(view: viewStatsExpandedTextView, position: viewStatsExpandedTextFrame.center)
transition.setBounds(view: viewStatsExpandedTextView, bounds: CGRect(origin: CGPoint(), size: viewStatsExpandedTextFrame.size))
transition.setAlpha(view: viewStatsExpandedTextView, alpha: pow(component.expandFraction, 1.2))
transition.setScale(view: viewStatsExpandedTextView, scale: viewStatsCurrentFrame.width / viewStatsExpandedTextFrame.width)
}
transition.setFrame(view: self.viewStatsButton, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: viewStatsTextFrame.maxX, height: viewStatsTextFrame.maxY + 8.0)))
self.viewStatsButton.isUserInteractionEnabled = component.expandFraction == 0.0
var rightContentOffset: CGFloat = availableSize.width - 12.0
@ -162,6 +198,8 @@ public final class StoryFooterPanelComponent: Component {
}
transition.setFrame(view: deleteButtonView, frame: CGRect(origin: CGPoint(x: rightContentOffset - deleteButtonSize.width, y: floor((size.height - deleteButtonSize.height) * 0.5)), size: deleteButtonSize))
rightContentOffset -= deleteButtonSize.width + 8.0
transition.setAlpha(view: deleteButtonView, alpha: pow(1.0 - component.expandFraction, 1.0))
}
let moreButton: MoreHeaderButton
@ -197,6 +235,7 @@ public final class StoryFooterPanelComponent: Component {
let buttonSize = CGSize(width: 32.0, height: 44.0)
moreButton.setContent(.more(MoreHeaderButton.optionsCircleImage(color: .white)))
transition.setFrame(view: moreButton.view, frame: CGRect(origin: CGPoint(x: rightContentOffset - buttonSize.width, y: floor((size.height - buttonSize.height) / 2.0)), size: buttonSize))
transition.setAlpha(view: moreButton.view, alpha: pow(1.0 - component.expandFraction, 1.0))
return size
}