News trending links experience

This commit is contained in:
Thomas Ricouard 2024-02-10 11:26:22 +01:00
parent 35d249f7c9
commit e7bc857231
17 changed files with 214 additions and 61 deletions

View file

@ -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()

View file

@ -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()
}

View file

@ -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:
""
}

View file

@ -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"

View file

@ -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 {

View file

@ -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"

View file

@ -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)
}
}

View file

@ -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

View file

@ -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,

View 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)
}
}

View file

@ -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 {}

View 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
}

View file

@ -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 {}

View file

@ -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))]
}

View file

@ -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"

View file

@ -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)
}
}

View file

@ -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