Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

added pop-up tip for feed customization #101 #1715

Merged
merged 2 commits into from
Dec 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Make feed source selector work.
- Add empty state for lists/relays drop-down.
- Added support for decrypting private tags in kind 30000 lists.
- Added pop-up tip for feed customization. [#101](https://github.com/verse-pbc/issues/issues/101)

### Internal Changes
- Upgraded to Xcode 16. [#1570](https://github.com/planetary-social/nos/issues/1570)
Expand Down
4 changes: 4 additions & 0 deletions Nos.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,7 @@
50089A172C98678600834588 /* View+ListRowGradientBackground.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50089A162C98678600834588 /* View+ListRowGradientBackground.swift */; };
501728B42D16EFB000CF2A07 /* FeedPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 501728B32D16EFAC00CF2A07 /* FeedPicker.swift */; };
5022F9462D2186380012FF4B /* follow_set_private.json in Resources */ = {isa = PBXBuildFile; fileRef = 5022F9452D2186300012FF4B /* follow_set_private.json */; };
5022FB352D2303F30012FF4B /* FeedSelectorTip.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5022FB342D2303F00012FF4B /* FeedSelectorTip.swift */; };
502B6C3D2C9462A400446316 /* PushNotificationRegistrar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 502B6C3C2C9462A400446316 /* PushNotificationRegistrar.swift */; };
503CA9532D19ACCC00805EF8 /* HorizontalLine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 503CA9522D19ACC800805EF8 /* HorizontalLine.swift */; };
503CA9792D19C39F00805EF8 /* FeedCustomizerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 503CA9782D19C39800805EF8 /* FeedCustomizerView.swift */; };
Expand Down Expand Up @@ -766,6 +767,7 @@
501728B32D16EFAC00CF2A07 /* FeedPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedPicker.swift; sourceTree = "<group>"; };
5022F9452D2186300012FF4B /* follow_set_private.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = follow_set_private.json; sourceTree = "<group>"; };
5022F9472D2188650012FF4B /* Nos 23.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "Nos 23.xcdatamodel"; sourceTree = "<group>"; };
5022FB342D2303F00012FF4B /* FeedSelectorTip.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedSelectorTip.swift; sourceTree = "<group>"; };
502B6C3C2C9462A400446316 /* PushNotificationRegistrar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PushNotificationRegistrar.swift; sourceTree = "<group>"; };
503CA9522D19ACC800805EF8 /* HorizontalLine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HorizontalLine.swift; sourceTree = "<group>"; };
503CA9782D19C39800805EF8 /* FeedCustomizerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedCustomizerView.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -1718,6 +1720,7 @@
children = (
503CA9782D19C39800805EF8 /* FeedCustomizerView.swift */,
501728B32D16EFAC00CF2A07 /* FeedPicker.swift */,
5022FB342D2303F00012FF4B /* FeedSelectorTip.swift */,
503CAC602D1EF71700805EF8 /* FeedSourceToggleView.swift */,
503CAB6D2D1DA17100805EF8 /* FeedToggleRow.swift */,
C9DEBFD8298941000078B43A /* HomeFeedView.swift */,
Expand Down Expand Up @@ -2656,6 +2659,7 @@
CD09A74829A51EFC0063464F /* Router.swift in Sources */,
2D4010A22AD87DF300F93AD4 /* KnownFollowersView.swift in Sources */,
CD2CF38E299E67F900332116 /* CardButtonStyle.swift in Sources */,
5022FB352D2303F30012FF4B /* FeedSelectorTip.swift in Sources */,
03E181392C75467C00886CC6 /* GalleryView.swift in Sources */,
A336DD3C299FD78000A0CBA0 /* Filter.swift in Sources */,
0315B5EF2C7E451C0020E707 /* MockMediaService.swift in Sources */,
Expand Down
20 changes: 20 additions & 0 deletions Nos/Assets/Colors.xcassets/tip-shadow.colorset/Contents.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0x2D",
"green" : "0x39",
"red" : "0xAA"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
19 changes: 15 additions & 4 deletions Nos/Views/Components/PagedNoteListView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,10 @@ struct PagedNoteListView<Header: View, EmptyPlaceholder: View>: UIViewRepresenta
/// Allows us to refresh the PagedNoteListView from outside this view itself, such as with a separate button.
@Binding var refreshController: RefreshController

/// A fetch request that specifies the events that should be shown. The events should be sorted in
/// Allows parent views to act when the offset reaches a certain point.
@Binding var scrollOffsetY: CGFloat

/// A fetch request that specifies the events that should be shown. The events should be sorted in
/// reverse-chronological order and should match the events returned by `relayFilter`.
let databaseFilter: NSFetchRequest<Event>

Expand All @@ -45,7 +48,7 @@ struct PagedNoteListView<Header: View, EmptyPlaceholder: View>: UIViewRepresenta
let emptyPlaceholder: () -> EmptyPlaceholder

func makeCoordinator() -> Coordinator<Header, EmptyPlaceholder> {
Coordinator(refreshController: refreshController)
Coordinator(refreshController: refreshController, scrollOffsetY: $scrollOffsetY)
}

func makeUIView(context: Context) -> UICollectionView {
Expand All @@ -65,6 +68,7 @@ struct PagedNoteListView<Header: View, EmptyPlaceholder: View>: UIViewRepresenta
emptyPlaceholder: emptyPlaceholder
)
collectionView.dataSource = dataSource
collectionView.delegate = context.coordinator
collectionView.prefetchDataSource = dataSource

let refreshControl = UIRefreshControl()
Expand Down Expand Up @@ -173,11 +177,12 @@ struct PagedNoteListView<Header: View, EmptyPlaceholder: View>: UIViewRepresenta

// swiftlint:disable generic_type_name
/// The coordinator mainly holds a strong reference to the `dataSource` and proxies pull-to-refresh events.
class Coordinator<CoordinatorHeader: View, CoordinatorEmptyPlaceholder: View> {
class Coordinator<CoordinatorHeader: View, CoordinatorEmptyPlaceholder: View>: NSObject, UICollectionViewDelegate {
// swiftlint:enable generic_type_name

/// Controls refresh actions. Used for setting the `lastRefreshDate` whenever the data is refreshed.
let refreshController: RefreshController
@Binding var scrollOffsetY: CGFloat

var dataSource: PagedNoteDataSource<CoordinatorHeader, CoordinatorEmptyPlaceholder>?
var collectionView: UICollectionView?
Expand All @@ -186,8 +191,9 @@ struct PagedNoteListView<Header: View, EmptyPlaceholder: View>: UIViewRepresenta
/// Initializes a coordinator with the given refresh controller.
/// - Parameter refreshController: Controls refresh actions. Used for setting the `lastRefreshDate`
/// whenever the data is refreshed.
init(refreshController: RefreshController) {
init(refreshController: RefreshController, scrollOffsetY: Binding<CGFloat>) {
self.refreshController = refreshController
self._scrollOffsetY = scrollOffsetY
}

func dataSource(
Expand Down Expand Up @@ -229,6 +235,10 @@ struct PagedNoteListView<Header: View, EmptyPlaceholder: View>: UIViewRepresenta
}
}
}

func scrollViewDidScroll(_ scrollView: UIScrollView) {
scrollOffsetY = scrollView.contentOffset.y
}
}
}

Expand All @@ -243,6 +253,7 @@ extension Notification.Name {

return PagedNoteListView(
refreshController: $refreshController,
scrollOffsetY: .constant(0),
databaseFilter: previewData.alice.allPostsRequest(onlyRootPosts: false),
relayFilter: Filter(),
relay: nil,
Expand Down
20 changes: 20 additions & 0 deletions Nos/Views/Home/FeedSelectorTip.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import Dependencies
import SwiftUI

struct FeedSelectorTip {
@Dependency(\.userDefaults) private var userDefaults

static let hasShownFeedTipKey = "com.verse.nos.Home.hasShownFeedTip"

static var minimumScrollOffset: CGFloat = 1500
static var maximumDelay: TimeInterval = 30

var hasShown: Bool {
get {
userDefaults.bool(forKey: Self.hasShownFeedTipKey)
}
set {
userDefaults.set(newValue, forKey: Self.hasShownFeedTipKey)
}
}
}
6 changes: 5 additions & 1 deletion Nos/Views/Home/HomeFeedView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ struct HomeFeedView: View {
private let stackSpacing: CGFloat = 8

let user: Author
@Binding var showFeedTip: Bool
@Binding var scrollOffsetY: CGFloat

/// A tip to display at the top of the feed.
private let welcomeTip = WelcomeToFeedTip()
Expand Down Expand Up @@ -77,6 +79,7 @@ struct HomeFeedView: View {

PagedNoteListView(
refreshController: $refreshController,
scrollOffsetY: $scrollOffsetY,
databaseFilter: homeFeedFetchRequest,
relayFilter: homeFeedFilter,
relay: feedController.selectedRelay,
Expand Down Expand Up @@ -147,6 +150,7 @@ struct HomeFeedView: View {
Button {
withAnimation {
showFeedSelector.toggle()
showFeedTip = false
}
} label: {
Image(systemName: showFeedSelector ? "xmark.circle.fill" : "line.3.horizontal.decrease.circle")
Expand Down Expand Up @@ -207,7 +211,7 @@ struct HomeFeedView: View {
}

return NavigationStack {
HomeFeedView(user: previewData.alice)
HomeFeedView(user: previewData.alice, showFeedTip: .constant(false), scrollOffsetY: .constant(0))
}
.inject(previewData: previewData)
.onAppear {
Expand Down
126 changes: 120 additions & 6 deletions Nos/Views/Home/HomeTab.swift
Original file line number Diff line number Diff line change
@@ -1,16 +1,126 @@
import SwiftUI
import Dependencies

struct HomeTab: View {
/// A styled tip view that contains the text provided.
///
/// Caution: As of iOS 18, TipKit does not allow styling of popover-style tips, so this
/// is a custom replication of TipKit's popover with custom styling. This is a bespoke
/// solution for the specific view it is in and will need to be modified to suit other views.
fileprivate struct PopoverTipView: View {
let text: String

var body: some View {
VStack(spacing: 0) {
HStack {
Spacer()

Image(systemName: "triangle.fill")
.resizable()
.foregroundStyle(Color.actionPrimaryGradientTop)
.frame(width: 20, height: 10)
.padding(.trailing, 23)
.offset(y: 4)
}

HStack {
Spacer()

HStack(alignment: .top) {
Text(text)
.font(.clarityBold(.headline))
.padding(.horizontal, 2)

Image(systemName: "xmark")
.padding(.trailing, 6)
}
.font(.headline)
.foregroundColor(.white)
.padding(.horizontal, 12)
.padding(.vertical, 20)
.background(
RoundedRectangle(cornerRadius: 16)
.fill(LinearGradient.horizontalAccentReversed)
)
.padding(.bottom, 4)
.background(
RoundedRectangle(cornerRadius: 16)
.fill(Color.tipShadow)
)
.frame(idealWidth: 320)

Spacer()
.frame(width: 8)
}
}
}
}

struct HomeTab: View {
@ObservedObject var user: Author

@EnvironmentObject private var router: Router

@State private var feedTip = FeedSelectorTip()
@State private var showFeedTip = false
@State private var timer: Timer?
@State private var scrollOffsetY: CGFloat = 0

var body: some View {
NosNavigationStack(path: $router.homeFeedPath) {
HomeFeedView(user: user)
ZStack {
NosNavigationStack(path: $router.homeFeedPath) {
HomeFeedView(
user: user,
showFeedTip: $showFeedTip,
scrollOffsetY: $scrollOffsetY
)
}

if showFeedTip {
VStack {
Spacer()
.frame(height: 24)

HStack {
Spacer()

PopoverTipView(text: "Curate your feed with lists, custom feeds, and relays.")
.onTapGesture {
withAnimation {
showFeedTip.toggle()
}
}
}
Spacer()
}
.transition(.opacity)
}
}
.onAppear {
if !feedTip.hasShown {
timer = Timer.scheduledTimer(withTimeInterval: FeedSelectorTip.maximumDelay, repeats: false) { _ in
showTip()
}
}
}
.onDisappear {
timer?.invalidate()
timer = nil
}
.onChange(of: scrollOffsetY) {
if scrollOffsetY > FeedSelectorTip.minimumScrollOffset {
showTip()
}
}
}

private func showTip() {
guard !feedTip.hasShown else {
return
}

withAnimation {
showFeedTip = true
}
feedTip.hasShown = true
}
}

Expand All @@ -20,8 +130,12 @@ struct HomeTab_Previews: PreviewProvider {

static var previews: some View {
NavigationView {
HomeFeedView(user: previewData.currentUser.author!)
.inject(previewData: previewData)
HomeFeedView(
user: previewData.currentUser.author!,
showFeedTip: .constant(false),
scrollOffsetY: .constant(0)
)
.inject(previewData: previewData)
}
}
}
2 changes: 2 additions & 0 deletions Nos/Views/Profile/ProfileView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ struct ProfileView: View {
@State private var selectedTab: ProfileFeedType = .notes

@State private var alert: AlertState<Never>?
@State private var scrollOffsetY: CGFloat = 0

var isShowingLoggedInUser: Bool {
author.hexadecimalPublicKey == currentUser.publicKeyHex
Expand Down Expand Up @@ -202,6 +203,7 @@ struct ProfileView: View {
var noteListView: some View {
PagedNoteListView(
refreshController: $refreshController,
scrollOffsetY: .constant(0),
databaseFilter: databaseFilter,
relayFilter: selectedTab.relayFilter(author: author),
relay: nil,
Expand Down
Loading