diff --git a/Bubble.xcodeproj/project.pbxproj b/Bubble.xcodeproj/project.pbxproj index e2b9d57..6426c5b 100644 --- a/Bubble.xcodeproj/project.pbxproj +++ b/Bubble.xcodeproj/project.pbxproj @@ -7,7 +7,6 @@ objects = { /* Begin PBXBuildFile section */ - B9029FC22B81259400AA9B68 /* Secret.plist in Resources */ = {isa = PBXBuildFile; fileRef = B9029FC12B81259400AA9B68 /* Secret.plist */; }; B9029FC42B8125CE00AA9B68 /* HuggingFace.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9029FC32B8125CE00AA9B68 /* HuggingFace.swift */; }; B90DEB1F2C822C2700D06121 /* StatusDraft.swift in Sources */ = {isa = PBXBuildFile; fileRef = B90DEB1E2C822C2700D06121 /* StatusDraft.swift */; }; B90DEB202C822C2700D06121 /* StatusDraft.swift in Sources */ = {isa = PBXBuildFile; fileRef = B90DEB1E2C822C2700D06121 /* StatusDraft.swift */; }; @@ -272,7 +271,6 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ - B9029FC12B81259400AA9B68 /* Secret.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Secret.plist; sourceTree = ""; }; B9029FC32B8125CE00AA9B68 /* HuggingFace.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HuggingFace.swift; sourceTree = ""; }; B90DEB1E2C822C2700D06121 /* StatusDraft.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusDraft.swift; sourceTree = ""; }; B90DEB212C822ED400D06121 /* PostDraftView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostDraftView.swift; sourceTree = ""; }; @@ -587,7 +585,6 @@ children = ( B9C20D592B923D53004DC9B3 /* Bubble.entitlements */, B9FB94A02B2EF23100D81C07 /* Info.plist */, - B9029FC12B81259400AA9B68 /* Secret.plist */, B9FB945A2B2DEECE00D81C07 /* BubbleApp.swift */, B9EBE8572B474FD600FB594D /* AppDelegate.swift */, B9FB946E2B2DF3BB00D81C07 /* Components */, @@ -873,7 +870,6 @@ B9DC69302B79378400E625B9 /* BubblePlus.storekit in Resources */, B9FB94642B2DEECF00D81C07 /* Preview Assets.xcassets in Resources */, B9FB94612B2DEECF00D81C07 /* Assets.xcassets in Resources */, - B9029FC22B81259400AA9B68 /* Secret.plist in Resources */, B95ED2372B87C9550055F5BD /* StoreKitTestCertificate.cer in Resources */, B9FB94902B2E2B0E00D81C07 /* Localizable.xcstrings in Resources */, B9CFC43B2B4F08C9004CFCB7 /* LaunchStoryboard.storyboard in Resources */, diff --git a/Bubble/BubbleApp.swift b/Bubble/BubbleApp.swift index a7f015c..952334f 100644 --- a/Bubble/BubbleApp.swift +++ b/Bubble/BubbleApp.swift @@ -7,7 +7,9 @@ import RevenueCat @main struct BubbleApp: App { init() { - guard let plist = AppDelegate.readSecret() else { fatalError("Missing Secret.plist file") } + BubbleShortcuts.updateAppShortcutParameters() //might not work? + + guard let plist = AppDelegate.readSecret() else { print("Missing Secret.plist file"); return } if let apiKey = plist["RevenueCat_public"], let deviceId = UIDevice.current.identifierForVendor?.uuidString { #if DEBUG @@ -15,8 +17,6 @@ struct BubbleApp: App { #endif Purchases.configure(withAPIKey: apiKey, appUserID: deviceId) } - - BubbleShortcuts.updateAppShortcutParameters() //might not work? } var body: some Scene { diff --git a/Bubble/Components/Post/CompactPostView.swift b/Bubble/Components/Post/CompactPostView.swift index f91507b..518c0d4 100644 --- a/Bubble/Components/Post/CompactPostView.swift +++ b/Bubble/Components/Post/CompactPostView.swift @@ -23,20 +23,14 @@ struct CompactPostView: View { @State private var quoteStatus: Status? = nil var body: some View { - VStack(alignment: .leading) { + VStack(alignment: .leading, spacing: 4.0) { notices statusPost(status.reblogAsAsStatus ?? status) - - if !quoted && !imaging { - Rectangle() - .fill(Color.gray.opacity(0.2)) - .frame(width: .infinity, height: 1) - .padding(.bottom, 3) - } } .withCovers(sheetDestination: $navigator.presentedCover) .containerShape(Rectangle()) + .padding(.vertical, 6.0) .background(postBackground()) .contextMenu { PostMenu(status: status) @@ -300,7 +294,13 @@ struct CompactPostView: View { ) .opacity(0.2) } else { - Color.appBackground +// Color.appBackground + LinearGradient( + stops: [.init(color: Color.subClub, location: 0.0), .init(color: Color.appBackground, location: 0.2)], + startPoint: .topTrailing, + endPoint: .bottomLeading + ) + .opacity(0.2) } } } diff --git a/Bubble/Views/Post/PostDetailsView.swift b/Bubble/Views/Post/PostDetailsView.swift index 65eed4b..1797cba 100644 --- a/Bubble/Views/Post/PostDetailsView.swift +++ b/Bubble/Views/Post/PostDetailsView.swift @@ -27,20 +27,25 @@ struct PostDetailsView: View { var body: some View { ScrollView(.vertical) { ScrollViewReader { proxy in - VStack(alignment: .leading) { + VStack(alignment: .leading, spacing: 1.5) { if statuses.isEmpty { - statusPost(detailedStatus) + CompactPostView(status: detailedStatus) } else { ForEach(statuses) { status in + let isLast: Bool = status.id == statuses.last?.id ?? "" + if status.id == detailedStatus.id { - statusPost(detailedStatus) - .padding(.horizontal, 15) - .padding(statuses.first!.id == detailedStatus.id ? .bottom : .vertical) + CompactPostView(status: detailedStatus) .onAppear { proxy.scrollTo("\(detailedStatus.id)@\(detailedStatus.account.id)", anchor: .bottom) } } else { - CompactPostView(status: status) + repPost(status) + } + + if !isLast { + Divider() + .frame(maxWidth: .infinity) } } } @@ -55,82 +60,18 @@ struct PostDetailsView: View { .safeAreaPadding() .navigationBarTitleDisplayMode(.inline) } - - @ViewBuilder - func statusPost(_ status: Status) -> some View { - VStack(alignment: .leading) { - HStack { - profilePicture - .onTapGesture { - navigator.navigate(to: .account(acc: status.account)) - } - - Text(status.account.username) - .multilineTextAlignment(.leading) - .bold() - .onTapGesture { - navigator.navigate(to: .account(acc: status.account)) - } - - Spacer() - Menu { - PostMenu(status: status) - } label: { - Image(systemName: "ellipsis") - .foregroundStyle(Color.white.opacity(0.3)) - .font(.body) - .contentShape(Rectangle()) - .padding(7.5) - } - .padding([.trailing, .top]) - } - - VStack(alignment: .leading) { - // MARK: Status main content - VStack(alignment: .leading, spacing: 10) { - if !status.content.asRawText.isEmpty { - TextEmoji(status.content, emojis: status.emojis, language: status.language) - .multilineTextAlignment(.leading) - .frame(width: 300, alignment: .topLeading) - .fixedSize(horizontal: false, vertical: true) - .font(.callout) - .id("\(detailedStatus.id)@\(detailedStatus.account.id)") - } - - if status.poll != nil { - PostPoll(poll: status.poll!) - } - - if status.card != nil && status.mediaAttachments.isEmpty { - PostCardView(card: status.card!) - } - - if !status.mediaAttachments.isEmpty { - ForEach(status.mediaAttachments) { attachment in - PostAttachment(attachment: attachment) - } - } - - if hasQuote { - if quoteStatus != nil { - QuotePostView(status: quoteStatus!) - } else { - ProgressView() - .progressViewStyle(.circular) - } - } - } - - //MARK: Action buttons - PostInteractor(status: status, isLiked: $isLiked, isReposted: $isReposted, isBookmarked: $isBookmarked) - - // MARK: Status stats - stats.padding(.top, 5) - } + @ViewBuilder + func repPost(_ status: Status) -> some View { + HStack(alignment: .center, spacing: 8.0) { + Rectangle() + .fill(Color.gray.opacity(0.4)) + .frame(maxWidth: 2.0, maxHeight: .infinity, alignment: .leading) + + CompactPostView(status: status) } } - + private func fetchStatusDetail() async { guard let client = accountManager.getClient() else { return } do { @@ -160,74 +101,7 @@ struct PostDetailsView: View { let status: Status let context: StatusContext } - - var profilePicture: some View { - if detailedStatus.reblog != nil { - OnlineImage(url: detailedStatus.reblog!.account.avatar, size: 50, useNuke: true) - .frame(width: 40, height: 40) - .clipShape(.circle) - } else { - OnlineImage(url: detailedStatus.account.avatar, size: 50, useNuke: true) - .frame(width: 40, height: 40) - .clipShape(.circle) - } - } - - var stats: some View { - //MARK: I acknowledge the existance of a count bug here - if detailedStatus.reblog == nil { - HStack { - if detailedStatus.repliesCount > 0 { - Text("status.replies-\(detailedStatus.repliesCount)") - .monospacedDigit() - .foregroundStyle(.gray) - } - - if detailedStatus.repliesCount > 0 && (detailedStatus.favouritesCount > 0 || isLiked) { - Text(verbatim: "•") - .foregroundStyle(.gray) - } - - if detailedStatus.favouritesCount > 0 || isLiked { - let likeCount: Int = detailedStatus.favouritesCount - (initialLike ? 1 : 0) - let incrLike: Int = isLiked ? 1 : 0 - Text("status.favourites-\(likeCount + incrLike)") - .monospacedDigit() - .foregroundStyle(.gray) - .contentTransition(.numericText(value: Double(likeCount + incrLike))) - .transaction { t in - t.animation = .default - } - } - } - } else { - HStack { - if detailedStatus.reblog!.repliesCount > 0 { - Text("status.replies-\(detailedStatus.reblog!.repliesCount)") - .monospacedDigit() - .foregroundStyle(.gray) - } - - if detailedStatus.reblog!.repliesCount > 0 && (detailedStatus.reblog!.favouritesCount > 0 || isLiked) { - Text(verbatim: "•") - .foregroundStyle(.gray) - } - - if detailedStatus.reblog!.favouritesCount > 0 || isLiked { - let likeCount: Int = detailedStatus.reblog!.favouritesCount - (initialLike ? 1 : 0) - let incrLike: Int = isLiked ? 1 : 0 - Text("status.favourites-\(likeCount + incrLike)") - .monospacedDigit() - .foregroundStyle(.gray) - .contentTransition(.numericText(value: Double(likeCount + incrLike))) - .transaction { t in - t.animation = .default - } - } - } - } - } - + private func embededStatusURL() -> URL? { let content = detailedStatus.content if let client = accountManager.getClient() { diff --git a/Bubble/Views/Post/PostsView.swift b/Bubble/Views/Post/PostsView.swift index be90076..e1b7a42 100644 --- a/Bubble/Views/Post/PostsView.swift +++ b/Bubble/Views/Post/PostsView.swift @@ -4,33 +4,33 @@ import SwiftUI struct PostsView: View { @Environment(AccountManager.self) private var accountManager: AccountManager -// @EnvironmentObject private var navigator: Navigator - + // @EnvironmentObject private var navigator: Navigator + @State private var showPicker: Bool = false @State private var timelines: [TimelineFilter] = [.home, .trending, .local, .federated] - + @State private var loadingStatuses: Bool = false @State private var statuses: [Status]? @State private var lastSeen: Int? - + @State var filter: TimelineFilter @State var showHero: Bool = false @State var timelineModel: FetchTimeline // home timeline by default - + init(timelineModel: FetchTimeline, filter: TimelineFilter, showHero: Bool = false) { self.timelineModel = timelineModel self.filter = filter self.timelineModel.setTimelineFilter(filter) self.showHero = showHero } - + init(filter: TimelineFilter, showHero: Bool = false) { self.timelineModel = .init(client: AccountManager.shared.forceClient()) self.filter = filter self.timelineModel.setTimelineFilter(filter) self.showHero = showHero } - + var body: some View { ZStack { if statuses != nil { @@ -49,54 +49,12 @@ struct PostsView: View { .padding(.bottom) } } - + if showPicker { - ViewThatFits { - HStack { - ForEach(timelines, id: \.self) { t in - Button { - Task { - await reloadTimeline(t) - } - } label: { - Text(t.localizedTitle()) - .padding(.horizontal) - } - .buttonStyle(LargeButton(filled: t == filter, height: 7.5)) - .disabled(t == filter) - } - } - - ScrollView(.horizontal) { - HStack { - ForEach(timelines, id: \.self) { t in - Button { - Task { - await reloadTimeline(t) - } - } label: { - Text(t.localizedTitle()) - .padding(.horizontal) - } - .buttonStyle(LargeButton(filled: t == filter, height: 7.5)) - .disabled(t == filter) - } - } - } - .padding(.vertical) - .scrollIndicators(.hidden) - } - } - - ForEach(statuses!, id: \.id) { status in - LazyVStack(alignment: .leading, spacing: 2) { - CompactPostView(status: status) - .onDisappear { - guard statuses != nil else { return } - lastSeen = statuses!.firstIndex(where: { $0.id == status.id }) - } - } + picker } + + posts } .refreshable { if let client = accountManager.getClient() { @@ -118,46 +76,10 @@ struct PostsView: View { // .padding(.top) .background(Color.appBackground) } else { - ZStack { - Color.appBackground - .ignoresSafeArea() - - VStack { - Image("HeroIcon") - .resizable() - .aspectRatio(contentMode: .fit) - .frame(width: 50) - .padding(.bottom) - - ContentUnavailableView { - Text("timeline.empty") - .bold() - } description: { - Text("timeline.empty.description") - } - .scrollDisabled(true) - } - .scrollDisabled(true) - .background(Color.appBackground) - .frame(height: 200) - } + emptyView } } else { - ZStack { - Color.appBackground - .ignoresSafeArea() - .onAppear { - timelineModel.setTimelineFilter(filter) - if let client = accountManager.getClient() { - Task { - statuses = await timelineModel.fetch(client: client) - } - } - } - - ProgressView() - .progressViewStyle(.circular) - } + loadingView } } .navigationTitle(filter.localizedTitle()) @@ -165,7 +87,112 @@ struct PostsView: View { .toolbarBackground(Color.appBackground, for: .navigationBar) .toolbarBackground(.visible, for: .navigationBar) } - + + @ViewBuilder + private var posts: some View { + ForEach(statuses!, id: \.id) { status in + let isLast: Bool = status.id == statuses!.last?.id ?? "" + + LazyVStack(alignment: .leading, spacing: 0.0) { + CompactPostView(status: status) + .onDisappear { + guard statuses != nil else { return } + lastSeen = statuses!.firstIndex(where: { $0.id == status.id }) + } + + if !isLast { + Divider() + .frame(maxWidth: .infinity) + } + } + } + } + + @ViewBuilder + private var picker: some View { + ViewThatFits { + HStack { + ForEach(timelines, id: \.self) { t in + Button { + Task { + await reloadTimeline(t) + } + } label: { + Text(t.localizedTitle()) + .padding(.horizontal) + } + .buttonStyle(LargeButton(filled: t == filter, height: 7.5)) + .disabled(t == filter) + } + } + + ScrollView(.horizontal) { + HStack { + ForEach(timelines, id: \.self) { t in + Button { + Task { + await reloadTimeline(t) + } + } label: { + Text(t.localizedTitle()) + .padding(.horizontal) + } + .buttonStyle(LargeButton(filled: t == filter, height: 7.5)) + .disabled(t == filter) + } + } + } + .padding(.vertical) + .scrollIndicators(.hidden) + } + } + + @ViewBuilder + private var emptyView: some View { + ZStack { + Color.appBackground + .ignoresSafeArea() + + VStack { + Image("HeroIcon") + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: 50) + .padding(.bottom) + + ContentUnavailableView { + Text("timeline.empty") + .bold() + } description: { + Text("timeline.empty.description") + } + .scrollDisabled(true) + } + .scrollDisabled(true) + .background(Color.appBackground) + .frame(height: 200) + } + } + + @ViewBuilder + private var loadingView: some View { + ZStack { + Color.appBackground + .ignoresSafeArea() + .onAppear { + timelineModel.setTimelineFilter(filter) + if let client = accountManager.getClient() { + Task { + statuses = await timelineModel.fetch(client: client) + } + } + } + + ProgressView() + .progressViewStyle(.circular) + } + } + private func reloadTimeline(_ filter: TimelineFilter) async { guard let client = accountManager.getClient() else { return } statuses = nil diff --git a/Bubble/Views/Post/TimelineView.swift b/Bubble/Views/Post/TimelineView.swift index 5a26edb..1cbfbd4 100644 --- a/Bubble/Views/Post/TimelineView.swift +++ b/Bubble/Views/Post/TimelineView.swift @@ -37,113 +37,133 @@ struct TimelineView: View { NavigationStack(path: $navigator.path) { if statuses != nil { if !statuses!.isEmpty { - ScrollView(showsIndicators: false) { - picker - - ForEach(statuses!, id: \.id) { status in - LazyVStack(alignment: .leading, spacing: 2) { - CompactPostView(status: status) - .onDisappear { - guard statuses != nil else { return } - lastSeen = statuses!.firstIndex(where: { $0.id == status.id }) - } - } - } - } - .refreshable { - if let client = accountManager.getClient() { - statuses = nil - - Task { - loadingStatuses = true - statuses = await timelineModel.fetch(client: client) - loadingStatuses = false - } - } - } - .onChange(of: lastSeen ?? 0) { _, new in - guard !loadingStatuses else { return } - Task { - loadingStatuses = true - statuses = await timelineModel.addStatuses(lastStatusIndex: new) - loadingStatuses = false - } - } - .toolbar { - if UserDefaults.standard.bool(forKey: "allowFilter") { - ToolbarItem(placement: .primaryAction) { - Button { - statuses = nil - - Task { - loadingStatuses = true - statuses = await self.timelineModel.toggleContentFilter(filter: wordsFilter) - loadingStatuses = false - } - } label: { - Image(systemName: self.timelineModel.filtering ? "line.3.horizontal.decrease.circle.fill" : "line.3.horizontal.decrease.circle") - .symbolEffect(.pulse.wholeSymbol, isActive: self.timelineModel.filtering) - } - .tint(Color(uiColor: UIColor.label)) - } - } - } - .padding(.top) - .background(Color.appBackground) - .withAppRouter(navigator) + statusesView } else { - ZStack { - Color.appBackground - .ignoresSafeArea() - - VStack { - picker - - ContentUnavailableView { - Text("timeline.empty") - .bold() - } description: { - Text("timeline.empty.description") - } - .scrollDisabled(true) - } - .scrollDisabled(true) - .background(Color.appBackground) - .frame(height: 200) - } + emptyView } } else { - ZStack { - Color.appBackground - .ignoresSafeArea() - .onAppear { - if UserDefaults.standard.bool(forKey: "allowFilter") { - self.wordsFilter = filters.compactMap({ ContentFilter.WordFilter(model: $0) }).first ?? ContentFilter.defaultFilter - } - - if let client = accountManager.getClient() { - Task { - statuses = await timelineModel.fetch(client: client) - - if UserDefaults.standard.bool(forKey: "autoOnFilter") { - statuses = await self.timelineModel.useContentFilter(wordsFilter) - } - } - } - } - - ProgressView() - .progressViewStyle(.circular) - } + loadingView } } .environmentObject(navigator) .background(Color.appBackground) .toolbarBackground(Color.appBackground, for: .navigationBar) .toolbarBackground(.visible, for: .navigationBar) - .safeAreaPadding() } - + + // MARK: Views + private var statusesView: some View { + ScrollView(showsIndicators: false) { + picker + + ForEach(statuses!, id: \.id) { status in + let isLast: Bool = status.id == statuses!.last?.id ?? "" + + LazyVStack(alignment: .leading, spacing: 0.0) { + CompactPostView(status: status) + .onDisappear { + guard statuses != nil else { return } + lastSeen = statuses!.firstIndex(where: { $0.id == status.id }) + } + + if !isLast { + Divider() + .frame(maxWidth: .infinity) + } + } + } + } + .refreshable { + if let client = accountManager.getClient() { + statuses = nil + + Task { + loadingStatuses = true + statuses = await timelineModel.fetch(client: client) + loadingStatuses = false + } + } + } + .onChange(of: lastSeen ?? 0) { _, new in + guard !loadingStatuses else { return } + Task { + loadingStatuses = true + statuses = await timelineModel.addStatuses(lastStatusIndex: new) + loadingStatuses = false + } + } + .toolbar { + if UserDefaults.standard.bool(forKey: "allowFilter") { + ToolbarItem(placement: .primaryAction) { + Button { + statuses = nil + + Task { + loadingStatuses = true + statuses = await self.timelineModel.toggleContentFilter(filter: wordsFilter) + loadingStatuses = false + } + } label: { + Image(systemName: self.timelineModel.filtering ? "line.3.horizontal.decrease.circle.fill" : "line.3.horizontal.decrease.circle") + .symbolEffect(.pulse.wholeSymbol, isActive: self.timelineModel.filtering) + } + .tint(Color(uiColor: UIColor.label)) + } + } + } +// .padding(.top) + .background(Color.appBackground) + .withAppRouter(navigator) + } + + private var emptyView: some View { + ZStack { + Color.appBackground + .ignoresSafeArea() + + VStack { + picker + + ContentUnavailableView { + Text("timeline.empty") + .bold() + } description: { + Text("timeline.empty.description") + } + .scrollDisabled(true) + } + .scrollDisabled(true) + .background(Color.appBackground) + .frame(height: 200) + } + } + + private var loadingView: some View { + ZStack { + Color.appBackground + .ignoresSafeArea() + .onAppear { + if UserDefaults.standard.bool(forKey: "allowFilter") { + self.wordsFilter = filters.compactMap({ ContentFilter.WordFilter(model: $0) }).first ?? ContentFilter.defaultFilter + } + + if let client = accountManager.getClient() { + Task { + statuses = await timelineModel.fetch(client: client) + + if UserDefaults.standard.bool(forKey: "autoOnFilter") { + statuses = await self.timelineModel.useContentFilter(wordsFilter) + } + } + } + } + + ProgressView() + .progressViewStyle(.circular) + } + } + + // MARK: - View Component private var picker: some View { VStack { if showHero { diff --git a/Bubble/Views/Profile/ProfileView.swift b/Bubble/Views/Profile/ProfileView.swift index 5e31ee4..64c83d2 100644 --- a/Bubble/Views/Profile/ProfileView.swift +++ b/Bubble/Views/Profile/ProfileView.swift @@ -262,11 +262,6 @@ struct ProfileView: View { .padding(.horizontal) VStack { - Rectangle() - .fill(Color.gray.opacity(0.2)) - .frame(width: .infinity, height: 1) - .padding(.bottom, 3) - statusesList } } @@ -307,15 +302,21 @@ struct ProfileView: View { } var statusesList: some View { - LazyVStack { + LazyVStack(spacing: 0) { if loadingStatuses == false { if !(statusesPinned?.isEmpty ?? true) { ForEach(statusesPinned!, id: \.id) { status in + Divider() + .frame(maxWidth: .infinity) + CompactPostView(status: status, pinned: true) } } if !(statuses?.isEmpty ?? true) { ForEach(statuses!, id: \.id) { status in + Divider() + .frame(maxWidth: .infinity) + CompactPostView(status: status) .onDisappear() { lastSeen = statuses!.firstIndex(where: { $0.id == status.id })