mirror of
https://github.com/Dimillian/IceCubesApp.git
synced 2024-12-26 00:50:38 +00:00
News trending links experience
This commit is contained in:
parent
35d249f7c9
commit
e7bc857231
17 changed files with 214 additions and 61 deletions
|
@ -60,7 +60,7 @@ extension View {
|
|||
scrollToTopSignal: .constant(0),
|
||||
canFilterTimeline: false)
|
||||
case let .trendingLinks(cards):
|
||||
CardsListView(cards: cards)
|
||||
TrendingLinksListView(cards: cards)
|
||||
case let .tagsList(tags):
|
||||
TagsListView(tags: tags)
|
||||
}
|
||||
|
@ -82,6 +82,9 @@ extension View {
|
|||
case let .quoteStatusEditor(status):
|
||||
StatusEditor.MainView(mode: .quote(status: status))
|
||||
.withEnvironments()
|
||||
case let .quoteLinkStatusEditor(link):
|
||||
StatusEditor.MainView(mode: .quoteLink(link: link))
|
||||
.withEnvironments()
|
||||
case let .mentionStatusEditor(account, visibility):
|
||||
StatusEditor.MainView(mode: .mention(account: account, visibility: visibility))
|
||||
.withEnvironments()
|
||||
|
|
|
@ -83,6 +83,8 @@ extension IceCubesApp {
|
|||
StatusEditor.MainView(mode: .replyTo(status: status))
|
||||
case let .mentionStatusEditor(account, visibility):
|
||||
StatusEditor.MainView(mode: .mention(account: account, visibility: visibility))
|
||||
case let .quoteLinkStatusEditor(link):
|
||||
StatusEditor.MainView(mode: .quoteLink(link: link))
|
||||
case .none:
|
||||
EmptyView()
|
||||
}
|
||||
|
|
|
@ -15,6 +15,7 @@ enum Tab: Int, Identifiable, Hashable, CaseIterable, Codable {
|
|||
case post
|
||||
case followedTags
|
||||
case lists
|
||||
case links
|
||||
|
||||
nonisolated var id: Int {
|
||||
rawValue
|
||||
|
@ -67,6 +68,8 @@ enum Tab: Int, Identifiable, Hashable, CaseIterable, Codable {
|
|||
NavigationTab {
|
||||
ListsListView()
|
||||
}
|
||||
case .links:
|
||||
NavigationTab { TrendingLinksListView(cards: []) }
|
||||
case .post:
|
||||
VStack { }
|
||||
case .other:
|
||||
|
@ -107,6 +110,8 @@ enum Tab: Int, Identifiable, Hashable, CaseIterable, Codable {
|
|||
Label("timeline.filter.tags", systemImage: iconName)
|
||||
case .lists:
|
||||
Label("timeline.filter.lists", systemImage: iconName)
|
||||
case .links:
|
||||
Label("explore.section.trending.links", systemImage: iconName)
|
||||
case .other:
|
||||
EmptyView()
|
||||
|
||||
|
@ -145,6 +150,8 @@ enum Tab: Int, Identifiable, Hashable, CaseIterable, Codable {
|
|||
"tag"
|
||||
case .lists:
|
||||
"list.bullet"
|
||||
case .links:
|
||||
"newspaper"
|
||||
case .other:
|
||||
""
|
||||
}
|
||||
|
|
|
@ -76555,6 +76555,35 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"trending-tag-people-talking %lld" : {
|
||||
"extractionState" : "manual",
|
||||
"localizations" : {
|
||||
"be" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "%lld people talking"
|
||||
}
|
||||
},
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "%lld people talking"
|
||||
}
|
||||
},
|
||||
"en-GB" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "%lld people talking"
|
||||
}
|
||||
},
|
||||
"eu" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "%lld people talking"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"version" : "1.0"
|
||||
|
|
|
@ -3,7 +3,7 @@ import Models
|
|||
import SwiftUI
|
||||
|
||||
public struct TagChartView: View {
|
||||
@State private var sortedHistory: [Tag.History] = []
|
||||
@State private var sortedHistory: [History] = []
|
||||
|
||||
public init(tag: Tag) {
|
||||
_sortedHistory = .init(initialValue: tag.history.sorted {
|
||||
|
|
|
@ -31,6 +31,7 @@ public enum WindowDestinationEditor: Hashable, Codable {
|
|||
case replyToStatusEditor(status: Status)
|
||||
case quoteStatusEditor(status: Status)
|
||||
case mentionStatusEditor(account: Account, visibility: Models.Visibility)
|
||||
case quoteLinkStatusEditor(link: URL)
|
||||
}
|
||||
|
||||
public enum WindowDestinationMedia: Hashable, Codable {
|
||||
|
@ -42,6 +43,7 @@ public enum SheetDestination: Identifiable {
|
|||
case editStatusEditor(status: Status)
|
||||
case replyToStatusEditor(status: Status)
|
||||
case quoteStatusEditor(status: Status)
|
||||
case quoteLinkStatusEditor(link: URL)
|
||||
case mentionStatusEditor(account: Account, visibility: Models.Visibility)
|
||||
case listCreate
|
||||
case listEdit(list: Models.List)
|
||||
|
@ -62,7 +64,7 @@ public enum SheetDestination: Identifiable {
|
|||
public var id: String {
|
||||
switch self {
|
||||
case .editStatusEditor, .newStatusEditor, .replyToStatusEditor, .quoteStatusEditor,
|
||||
.mentionStatusEditor:
|
||||
.mentionStatusEditor, .quoteLinkStatusEditor:
|
||||
"statusEditor"
|
||||
case .listCreate:
|
||||
"listCreate"
|
||||
|
|
|
@ -1,29 +0,0 @@
|
|||
import DesignSystem
|
||||
import Models
|
||||
import StatusKit
|
||||
import SwiftUI
|
||||
|
||||
public struct CardsListView: View {
|
||||
@Environment(Theme.self) private var theme
|
||||
|
||||
let cards: [Card]
|
||||
|
||||
public init(cards: [Card]) {
|
||||
self.cards = cards
|
||||
}
|
||||
|
||||
public var body: some View {
|
||||
List {
|
||||
ForEach(cards) { card in
|
||||
StatusRowCardView(card: card)
|
||||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
.padding(.vertical, 8)
|
||||
}
|
||||
}
|
||||
.scrollContentBackground(.hidden)
|
||||
.background(theme.primaryBackgroundColor)
|
||||
.listStyle(.plain)
|
||||
.navigationTitle("explore.section.trending.links")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
}
|
||||
}
|
|
@ -98,6 +98,7 @@ public struct ExploreView: View {
|
|||
.background(theme.secondaryBackgroundColor)
|
||||
#endif
|
||||
.navigationTitle("explore.navigation-title")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.searchable(text: $viewModel.searchQuery,
|
||||
isPresented: $viewModel.isSearchPresented,
|
||||
placement: .navigationBarDrawer(displayMode: .always),
|
||||
|
@ -125,16 +126,20 @@ public struct ExploreView: View {
|
|||
private var quickAccessView: some View {
|
||||
ScrollView(.horizontal) {
|
||||
HStack {
|
||||
Button("explore.section.trending.tags") {
|
||||
routerPath.navigate(to: RouterDestination.tagsList(tags: viewModel.trendingTags))
|
||||
Button("explore.section.trending.links") {
|
||||
routerPath.navigate(to: RouterDestination.trendingLinks(cards: viewModel.trendingLinks))
|
||||
}
|
||||
.buttonStyle(.bordered)
|
||||
Button("explore.section.trending.posts") {
|
||||
routerPath.navigate(to: RouterDestination.trendingTimeline)
|
||||
}
|
||||
.buttonStyle(.bordered)
|
||||
Button("explore.section.suggested-users") {
|
||||
routerPath.navigate(to: RouterDestination.accountsList(accounts: viewModel.suggestedAccounts))
|
||||
}
|
||||
.buttonStyle(.bordered)
|
||||
Button("explore.section.trending.posts") {
|
||||
routerPath.navigate(to: RouterDestination.trendingTimeline)
|
||||
Button("explore.section.trending.tags") {
|
||||
routerPath.navigate(to: RouterDestination.tagsList(tags: viewModel.trendingTags))
|
||||
}
|
||||
.buttonStyle(.bordered)
|
||||
}
|
||||
|
@ -305,6 +310,7 @@ public struct ExploreView: View {
|
|||
.prefix(upTo: viewModel.trendingLinks.count > 3 ? 3 : viewModel.trendingLinks.count))
|
||||
{ card in
|
||||
StatusRowCardView(card: card)
|
||||
.environment(\.isCompact, true)
|
||||
#if !os(visionOS)
|
||||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
#else
|
||||
|
|
|
@ -88,7 +88,7 @@ import SwiftUI
|
|||
async let suggestedAccounts: [Account] = client.get(endpoint: Accounts.suggestions)
|
||||
async let trendingTags: [Tag] = client.get(endpoint: Trends.tags)
|
||||
async let trendingStatuses: [Status] = client.get(endpoint: Trends.statuses(offset: nil))
|
||||
async let trendingLinks: [Card] = client.get(endpoint: Trends.links)
|
||||
async let trendingLinks: [Card] = client.get(endpoint: Trends.links(offset: nil))
|
||||
return try await .init(suggestedAccounts: suggestedAccounts,
|
||||
trendingTags: trendingTags,
|
||||
trendingStatuses: trendingStatuses,
|
||||
|
|
62
Packages/Explore/Sources/Explore/TrendingLinksListView.swift
Normal file
62
Packages/Explore/Sources/Explore/TrendingLinksListView.swift
Normal file
|
@ -0,0 +1,62 @@
|
|||
import DesignSystem
|
||||
import Models
|
||||
import StatusKit
|
||||
import SwiftUI
|
||||
import Network
|
||||
|
||||
public struct TrendingLinksListView: View {
|
||||
@Environment(Theme.self) private var theme
|
||||
@Environment(Client.self) private var client
|
||||
|
||||
@State private var links: [Card]
|
||||
@State private var isLoadingNextPage = false
|
||||
|
||||
public init(cards: [Card]) {
|
||||
_links = .init(initialValue: cards)
|
||||
}
|
||||
|
||||
public var body: some View {
|
||||
List {
|
||||
ForEach(links) { card in
|
||||
StatusRowCardView(card: card)
|
||||
.environment(\.isCompact, true)
|
||||
#if !os(visionOS)
|
||||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
#endif
|
||||
.padding(.vertical, 8)
|
||||
}
|
||||
HStack {
|
||||
Spacer()
|
||||
ProgressView()
|
||||
Spacer()
|
||||
}
|
||||
.task {
|
||||
defer {
|
||||
isLoadingNextPage = false
|
||||
}
|
||||
guard !isLoadingNextPage else { return }
|
||||
isLoadingNextPage = true
|
||||
do {
|
||||
let nextPage: [Card] = try await client.get(endpoint: Trends.links(offset: links.count))
|
||||
links.append(contentsOf: nextPage)
|
||||
} catch { }
|
||||
}
|
||||
}
|
||||
#if os(visionOS)
|
||||
.listStyle(.insetGrouped)
|
||||
#else
|
||||
.listStyle(.plain)
|
||||
#endif
|
||||
.refreshable {
|
||||
do {
|
||||
links = try await client.get(endpoint: Trends.links(offset: nil))
|
||||
} catch {}
|
||||
}
|
||||
#if !os(visionOS)
|
||||
.scrollContentBackground(.hidden)
|
||||
.background(theme.primaryBackgroundColor)
|
||||
#endif
|
||||
.navigationTitle("explore.section.trending.links")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
}
|
||||
}
|
|
@ -12,6 +12,7 @@ public struct Card: Codable, Identifiable, Equatable, Hashable {
|
|||
public let image: URL?
|
||||
public let width: CGFloat
|
||||
public let height: CGFloat
|
||||
public let history: [History]?
|
||||
}
|
||||
|
||||
extension Card: Sendable {}
|
||||
|
|
11
Packages/Models/Sources/Models/History.swift
Normal file
11
Packages/Models/Sources/Models/History.swift
Normal file
|
@ -0,0 +1,11 @@
|
|||
import Foundation
|
||||
|
||||
public struct History: Codable, Identifiable, Sendable, Equatable, Hashable {
|
||||
public var id: String {
|
||||
day
|
||||
}
|
||||
|
||||
public let day: String
|
||||
public let accounts: String
|
||||
public let uses: String
|
||||
}
|
|
@ -1,16 +1,6 @@
|
|||
import Foundation
|
||||
|
||||
public struct Tag: Codable, Identifiable, Equatable, Hashable {
|
||||
public struct History: Codable, Identifiable {
|
||||
public var id: String {
|
||||
day
|
||||
}
|
||||
|
||||
public let day: String
|
||||
public let accounts: String
|
||||
public let uses: String
|
||||
}
|
||||
|
||||
public func hash(into hasher: inout Hasher) {
|
||||
hasher.combine(name)
|
||||
}
|
||||
|
@ -65,5 +55,4 @@ public struct FeaturedTag: Codable, Identifiable {
|
|||
}
|
||||
|
||||
extension Tag: Sendable {}
|
||||
extension Tag.History: Sendable {}
|
||||
extension FeaturedTag: Sendable {}
|
||||
|
|
|
@ -3,7 +3,7 @@ import Foundation
|
|||
public enum Trends: Endpoint {
|
||||
case tags
|
||||
case statuses(offset: Int?)
|
||||
case links
|
||||
case links(offset: Int?)
|
||||
|
||||
public func path() -> String {
|
||||
switch self {
|
||||
|
@ -18,7 +18,7 @@ public enum Trends: Endpoint {
|
|||
|
||||
public func queryItems() -> [URLQueryItem]? {
|
||||
switch self {
|
||||
case let .statuses(offset):
|
||||
case let .statuses(offset), let .links(offset):
|
||||
if let offset {
|
||||
return [.init(name: "offset", value: String(offset))]
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@ public extension StatusEditor.ViewModel {
|
|||
case new(visibility: Models.Visibility)
|
||||
case edit(status: Status)
|
||||
case quote(status: Status)
|
||||
case quoteLink(link: URL)
|
||||
case mention(account: Account, visibility: Models.Visibility)
|
||||
case shareExtension(items: [NSItemProvider])
|
||||
|
||||
|
@ -40,7 +41,7 @@ public extension StatusEditor.ViewModel {
|
|||
|
||||
var title: LocalizedStringKey {
|
||||
switch self {
|
||||
case .new, .mention, .shareExtension:
|
||||
case .new, .mention, .shareExtension, .quoteLink:
|
||||
"status.editor.mode.new"
|
||||
case .edit:
|
||||
"status.editor.mode.edit"
|
||||
|
|
|
@ -234,7 +234,7 @@ extension StatusEditor {
|
|||
language: selectedLanguage,
|
||||
mediaAttributes: mediaAttributes)
|
||||
switch mode {
|
||||
case .new, .replyTo, .quote, .mention, .shareExtension:
|
||||
case .new, .replyTo, .quote, .mention, .shareExtension, .quoteLink:
|
||||
postStatus = try await client.post(endpoint: Statuses.postStatus(json: data))
|
||||
if let postStatus {
|
||||
StreamWatcher.shared.emmitPostEvent(for: postStatus)
|
||||
|
@ -362,6 +362,9 @@ extension StatusEditor {
|
|||
statusText = .init(string: "\n\nFrom: @\(status.reblog?.account.acct ?? status.account.acct)\n\(url)")
|
||||
selectedRange = .init(location: 0, length: 0)
|
||||
}
|
||||
case let .quoteLink(link):
|
||||
statusText = .init(string: "\n\n\(link)")
|
||||
selectedRange = .init(location: 0, length: 0)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -3,13 +3,17 @@ import Models
|
|||
import Nuke
|
||||
import NukeUI
|
||||
import SwiftUI
|
||||
import Env
|
||||
|
||||
@MainActor
|
||||
public struct StatusRowCardView: View {
|
||||
@Environment(\.openURL) private var openURL
|
||||
@Environment(\.openWindow) private var openWindow
|
||||
@Environment(\.isInCaptureMode) private var isInCaptureMode: Bool
|
||||
@Environment(\.isCompact) private var isCompact: Bool
|
||||
|
||||
@Environment(Theme.self) private var theme
|
||||
@Environment(RouterPath.self) private var routerPath
|
||||
|
||||
let card: Card
|
||||
|
||||
|
@ -32,7 +36,7 @@ public struct StatusRowCardView: View {
|
|||
}
|
||||
|
||||
private var imageHeight: CGFloat {
|
||||
if theme.statusDisplayStyle == .medium {
|
||||
if theme.statusDisplayStyle == .medium || isCompact {
|
||||
return 100
|
||||
}
|
||||
return 200
|
||||
|
@ -47,7 +51,9 @@ public struct StatusRowCardView: View {
|
|||
if let title = card.title, let url = URL(string: card.url) {
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
let sitesWithIcons = ["apps.apple.com", "music.apple.com", "open.spotify.com"]
|
||||
if (UIDevice.current.userInterfaceIdiom == .pad ||
|
||||
if isCompact {
|
||||
compactLinkPreview(title, url)
|
||||
} else if (UIDevice.current.userInterfaceIdiom == .pad ||
|
||||
UIDevice.current.userInterfaceIdiom == .mac ||
|
||||
UIDevice.current.userInterfaceIdiom == .vision),
|
||||
let host = url.host(), sitesWithIcons.contains(host) {
|
||||
|
@ -59,16 +65,20 @@ public struct StatusRowCardView: View {
|
|||
.frame(maxWidth: maxWidth)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
#if os(visionOS)
|
||||
.background(.background)
|
||||
.if(!isCompact, transform: { view in
|
||||
view.background(.background)
|
||||
})
|
||||
.hoverEffect()
|
||||
#else
|
||||
.background(theme.secondaryBackgroundColor)
|
||||
.background(isCompact ? .clear : theme.secondaryBackgroundColor)
|
||||
#endif
|
||||
.cornerRadius(10)
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 10)
|
||||
.stroke(.gray.opacity(0.35), lineWidth: 1)
|
||||
)
|
||||
.cornerRadius(isCompact ? 0 : 10)
|
||||
.overlay {
|
||||
if !isCompact {
|
||||
RoundedRectangle(cornerRadius: 10)
|
||||
.stroke(.gray.opacity(0.35), lineWidth: 1)
|
||||
}
|
||||
}
|
||||
.contextMenu {
|
||||
ShareLink(item: url) {
|
||||
Label("status.card.share", systemImage: "square.and.arrow.up")
|
||||
|
@ -115,6 +125,62 @@ public struct StatusRowCardView: View {
|
|||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.padding(10)
|
||||
}
|
||||
|
||||
private func compactLinkPreview(_ title: String, _ url: URL) -> some View {
|
||||
HStack(alignment: .top) {
|
||||
if let imageURL = card.image, !isInCaptureMode {
|
||||
LazyResizableImage(url: imageURL) { state, _ in
|
||||
if let image = state.image {
|
||||
image
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fill)
|
||||
.frame(width: imageHeight, height: imageHeight)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 8))
|
||||
.clipped()
|
||||
} else if state.isLoading {
|
||||
Rectangle()
|
||||
.fill(Color.gray)
|
||||
.frame(width: imageHeight, height: imageHeight)
|
||||
}
|
||||
}
|
||||
// This image is decorative
|
||||
.accessibilityHidden(true)
|
||||
.frame(width: imageHeight, height: imageHeight)
|
||||
}
|
||||
VStack(alignment: .leading, spacing: 6) {
|
||||
Text(title)
|
||||
.font(.scaledHeadline)
|
||||
.lineLimit(3)
|
||||
Text(url.host() ?? url.absoluteString)
|
||||
.font(.scaledFootnote)
|
||||
.foregroundColor(theme.tintColor)
|
||||
.lineLimit(1)
|
||||
if let history = card.history {
|
||||
let uses = history.compactMap{ Int($0.accounts )}.reduce(0, +)
|
||||
HStack(spacing: 4) {
|
||||
Image(systemName: "bubble.left.and.text.bubble.right")
|
||||
Text("trending-tag-people-talking \(uses)")
|
||||
Spacer()
|
||||
Button {
|
||||
#if targetEnvironment(macCatalyst)
|
||||
openWindow(value: WindowDestinationEditor.quoteLinkStatusEditor(link: url))
|
||||
#else
|
||||
routerPath.presentedSheet = .quoteLinkStatusEditor(link: url)
|
||||
#endif
|
||||
} label: {
|
||||
Image(systemName: "quote.opening")
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
.font(.scaledCaption)
|
||||
.foregroundStyle(.secondary)
|
||||
.lineLimit(1)
|
||||
.padding(.top, 12)
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 8)
|
||||
}
|
||||
}
|
||||
|
||||
private func iconLinkPreview(_ title: String, _ url: URL) -> some View {
|
||||
// ..where the image is known to be a square icon
|
||||
|
|
Loading…
Reference in a new issue