Redesign News + support links attributions

This commit is contained in:
Thomas Ricouard 2024-07-02 19:59:21 +02:00
parent 2d04433783
commit 478a788f87
9 changed files with 158 additions and 52 deletions

View file

@ -44,6 +44,12 @@ extension View {
selectedTagGroup: .constant(nil), selectedTagGroup: .constant(nil),
scrollToTopSignal: .constant(0), scrollToTopSignal: .constant(0),
canFilterTimeline: false) canFilterTimeline: false)
case let .linkTimeline(url, title):
TimelineView(timeline: .constant(.link(url: url, title: title)),
pinnedFilters: .constant([]),
selectedTagGroup: .constant(nil),
scrollToTopSignal: .constant(0),
canFilterTimeline: false)
case let .following(id): case let .following(id):
AccountsListView(mode: .following(accountId: id)) AccountsListView(mode: .following(accountId: id))
case let .followers(id): case let .followers(id):

View file

@ -221,7 +221,7 @@ class iOSTabs {
@AppStorage(TabEntries.first.rawValue) var firstTab = Tab.timeline @AppStorage(TabEntries.first.rawValue) var firstTab = Tab.timeline
@AppStorage(TabEntries.second.rawValue) var secondTab = Tab.notifications @AppStorage(TabEntries.second.rawValue) var secondTab = Tab.notifications
@AppStorage(TabEntries.third.rawValue) var thirdTab = Tab.explore @AppStorage(TabEntries.third.rawValue) var thirdTab = Tab.explore
@AppStorage(TabEntries.fourth.rawValue) var fourthTab = Tab.messages @AppStorage(TabEntries.fourth.rawValue) var fourthTab = Tab.links
@AppStorage(TabEntries.fifth.rawValue) var fifthTab = Tab.profile @AppStorage(TabEntries.fifth.rawValue) var fifthTab = Tab.profile
} }

View file

@ -28457,115 +28457,115 @@
"localizations" : { "localizations" : {
"be" : { "be" : {
"stringUnit" : { "stringUnit" : {
"state" : "translated", "state" : "needs_review",
"value" : "Трэндавыя спасылкі" "value" : "Трэндавыя спасылкі"
} }
}, },
"ca" : { "ca" : {
"stringUnit" : { "stringUnit" : {
"state" : "translated", "state" : "needs_review",
"value" : "Enllaços populars" "value" : "Enllaços populars"
} }
}, },
"de" : { "de" : {
"stringUnit" : { "stringUnit" : {
"state" : "translated", "state" : "needs_review",
"value" : "Links im Trend" "value" : "Links im Trend"
} }
}, },
"en" : { "en" : {
"stringUnit" : { "stringUnit" : {
"state" : "translated", "state" : "translated",
"value" : "Trending Links" "value" : "News"
} }
}, },
"en-GB" : { "en-GB" : {
"stringUnit" : { "stringUnit" : {
"state" : "translated", "state" : "translated",
"value" : "Trending Links" "value" : "News"
} }
}, },
"es" : { "es" : {
"stringUnit" : { "stringUnit" : {
"state" : "translated", "state" : "needs_review",
"value" : "Enlaces que son tendencia" "value" : "Enlaces que son tendencia"
} }
}, },
"eu" : { "eu" : {
"stringUnit" : { "stringUnit" : {
"state" : "translated", "state" : "needs_review",
"value" : "Gori-gorian dauden estekak" "value" : "Gori-gorian dauden estekak"
} }
}, },
"fr" : { "fr" : {
"stringUnit" : { "stringUnit" : {
"state" : "translated", "state" : "needs_review",
"value" : "Liens tendance" "value" : "Liens tendance"
} }
}, },
"it" : { "it" : {
"stringUnit" : { "stringUnit" : {
"state" : "translated", "state" : "needs_review",
"value" : "Link in tendenza" "value" : "Link in tendenza"
} }
}, },
"ja" : { "ja" : {
"stringUnit" : { "stringUnit" : {
"state" : "translated", "state" : "needs_review",
"value" : "人気のリンク" "value" : "人気のリンク"
} }
}, },
"ko" : { "ko" : {
"stringUnit" : { "stringUnit" : {
"state" : "translated", "state" : "needs_review",
"value" : "뜨고 있는 링크" "value" : "뜨고 있는 링크"
} }
}, },
"nb" : { "nb" : {
"stringUnit" : { "stringUnit" : {
"state" : "translated", "state" : "needs_review",
"value" : "Trendende lenker" "value" : "Trendende lenker"
} }
}, },
"nl" : { "nl" : {
"stringUnit" : { "stringUnit" : {
"state" : "translated", "state" : "needs_review",
"value" : "Trending links" "value" : "Trending links"
} }
}, },
"pl" : { "pl" : {
"stringUnit" : { "stringUnit" : {
"state" : "translated", "state" : "needs_review",
"value" : "Popularne linki" "value" : "Popularne linki"
} }
}, },
"pt-BR" : { "pt-BR" : {
"stringUnit" : { "stringUnit" : {
"state" : "translated", "state" : "needs_review",
"value" : "Links em Tendência" "value" : "Links em Tendência"
} }
}, },
"tr" : { "tr" : {
"stringUnit" : { "stringUnit" : {
"state" : "translated", "state" : "needs_review",
"value" : "Yükselişteki Bağlantılar" "value" : "Yükselişteki Bağlantılar"
} }
}, },
"uk" : { "uk" : {
"stringUnit" : { "stringUnit" : {
"state" : "translated", "state" : "needs_review",
"value" : "Популярні посилання" "value" : "Популярні посилання"
} }
}, },
"zh-Hans" : { "zh-Hans" : {
"stringUnit" : { "stringUnit" : {
"state" : "translated", "state" : "needs_review",
"value" : "当下流行的网页" "value" : "当下流行的网页"
} }
}, },
"zh-Hant" : { "zh-Hant" : {
"stringUnit" : { "stringUnit" : {
"state" : "translated", "state" : "needs_review",
"value" : "流行連結" "value" : "流行連結"
} }
} }
@ -82387,7 +82387,7 @@
}, },
"other" : { "other" : {
"stringUnit" : { "stringUnit" : {
"state" : "translated", "state" : "needs_review",
"value" : "%lld people talking" "value" : "%lld people talking"
} }
} }
@ -82417,13 +82417,13 @@
"plural" : { "plural" : {
"one" : { "one" : {
"stringUnit" : { "stringUnit" : {
"state" : "translated", "state" : "needs_review",
"value" : "%lld Person redet" "value" : "%lld Person redet"
} }
}, },
"other" : { "other" : {
"stringUnit" : { "stringUnit" : {
"state" : "translated", "state" : "needs_review",
"value" : "%lld Personen reden" "value" : "%lld Personen reden"
} }
} }
@ -82436,13 +82436,13 @@
"one" : { "one" : {
"stringUnit" : { "stringUnit" : {
"state" : "translated", "state" : "translated",
"value" : "%lld person talking" "value" : "%lld post"
} }
}, },
"other" : { "other" : {
"stringUnit" : { "stringUnit" : {
"state" : "translated", "state" : "translated",
"value" : "%lld people talking" "value" : "%lld posts"
} }
} }
} }
@ -82454,13 +82454,13 @@
"one" : { "one" : {
"stringUnit" : { "stringUnit" : {
"state" : "translated", "state" : "translated",
"value" : "%lld people talking" "value" : "%lld post"
} }
}, },
"other" : { "other" : {
"stringUnit" : { "stringUnit" : {
"state" : "translated", "state" : "translated",
"value" : "%lld people talking" "value" : "%lld posts"
} }
} }
} }
@ -82471,13 +82471,13 @@
"plural" : { "plural" : {
"one" : { "one" : {
"stringUnit" : { "stringUnit" : {
"state" : "translated", "state" : "needs_review",
"value" : "%lld persona hablando" "value" : "%lld persona hablando"
} }
}, },
"other" : { "other" : {
"stringUnit" : { "stringUnit" : {
"state" : "translated", "state" : "needs_review",
"value" : "%lld personas hablando" "value" : "%lld personas hablando"
} }
} }
@ -82489,13 +82489,13 @@
"plural" : { "plural" : {
"one" : { "one" : {
"stringUnit" : { "stringUnit" : {
"state" : "translated", "state" : "needs_review",
"value" : "Pertsona batek hizpide" "value" : "Pertsona batek hizpide"
} }
}, },
"other" : { "other" : {
"stringUnit" : { "stringUnit" : {
"state" : "translated", "state" : "needs_review",
"value" : "%lld pertsonak hizpide" "value" : "%lld pertsonak hizpide"
} }
} }
@ -82507,13 +82507,13 @@
"plural" : { "plural" : {
"one" : { "one" : {
"stringUnit" : { "stringUnit" : {
"state" : "translated", "state" : "needs_review",
"value" : "%lld personne parle" "value" : "%lld personne parle"
} }
}, },
"other" : { "other" : {
"stringUnit" : { "stringUnit" : {
"state" : "translated", "state" : "needs_review",
"value" : "%lld personnes parlent" "value" : "%lld personnes parlent"
} }
} }
@ -82525,13 +82525,13 @@
"plural" : { "plural" : {
"one" : { "one" : {
"stringUnit" : { "stringUnit" : {
"state" : "translated", "state" : "needs_review",
"value" : "%lld persona ne parla" "value" : "%lld persona ne parla"
} }
}, },
"other" : { "other" : {
"stringUnit" : { "stringUnit" : {
"state" : "translated", "state" : "needs_review",
"value" : "%lld persone ne parlano" "value" : "%lld persone ne parlano"
} }
} }
@ -82615,25 +82615,25 @@
"plural" : { "plural" : {
"few" : { "few" : {
"stringUnit" : { "stringUnit" : {
"state" : "translated", "state" : "needs_review",
"value" : "%lld osoby rozmawiają" "value" : "%lld osoby rozmawiają"
} }
}, },
"many" : { "many" : {
"stringUnit" : { "stringUnit" : {
"state" : "translated", "state" : "needs_review",
"value" : "%lld osób rozmawia" "value" : "%lld osób rozmawia"
} }
}, },
"one" : { "one" : {
"stringUnit" : { "stringUnit" : {
"state" : "translated", "state" : "needs_review",
"value" : "%lld osoba rozmawia" "value" : "%lld osoba rozmawia"
} }
}, },
"other" : { "other" : {
"stringUnit" : { "stringUnit" : {
"state" : "translated", "state" : "needs_review",
"value" : "%lld osób rozmawia" "value" : "%lld osób rozmawia"
} }
} }
@ -82663,13 +82663,13 @@
"plural" : { "plural" : {
"one" : { "one" : {
"stringUnit" : { "stringUnit" : {
"state" : "translated", "state" : "needs_review",
"value" : "%lld kişi konuşuyor" "value" : "%lld kişi konuşuyor"
} }
}, },
"other" : { "other" : {
"stringUnit" : { "stringUnit" : {
"state" : "translated", "state" : "needs_review",
"value" : "%lld kişi konuşuyor" "value" : "%lld kişi konuşuyor"
} }
} }
@ -82699,13 +82699,13 @@
"plural" : { "plural" : {
"one" : { "one" : {
"stringUnit" : { "stringUnit" : {
"state" : "translated", "state" : "needs_review",
"value" : "%lld 人正在参与讨论" "value" : "%lld 人正在参与讨论"
} }
}, },
"other" : { "other" : {
"stringUnit" : { "stringUnit" : {
"state" : "translated", "state" : "needs_review",
"value" : "%lld 人正在参与讨论" "value" : "%lld 人正在参与讨论"
} }
} }
@ -82717,13 +82717,13 @@
"plural" : { "plural" : {
"one" : { "one" : {
"stringUnit" : { "stringUnit" : {
"state" : "translated", "state" : "needs_review",
"value" : "%lld 人議論中" "value" : "%lld 人議論中"
} }
}, },
"other" : { "other" : {
"stringUnit" : { "stringUnit" : {
"state" : "translated", "state" : "needs_review",
"value" : "%lld 人議論中" "value" : "%lld 人議論中"
} }
} }

View file

@ -39,6 +39,10 @@ import Observation
version >= 4.3 version >= 4.3
} }
public var isLinkTimelineSupported: Bool {
version >= 4.3
}
private init() {} private init() {}
public func setClient(client: Client) { public func setClient(client: Client) {

View file

@ -21,6 +21,7 @@ public enum RouterDestination: Hashable {
case rebloggedBy(id: String) case rebloggedBy(id: String)
case accountsList(accounts: [Account]) case accountsList(accounts: [Account])
case trendingTimeline case trendingTimeline
case linkTimeline(url: URL, title: String)
case trendingLinks(cards: [Card]) case trendingLinks(cards: [Card])
case tagsList(tags: [Tag]) case tagsList(tags: [Tag])
case notificationsRequests case notificationsRequests

View file

@ -5,14 +5,26 @@ public struct Card: Codable, Identifiable, Equatable, Hashable {
url url
} }
public struct CardAuthor: Codable, Sendable, Identifiable, Equatable, Hashable {
public var id: String {
url
}
public let name: String
public let url: String
public let account: Account?
}
public let url: String public let url: String
public let title: String? public let title: String?
public let authorName: String?
public let description: String? public let description: String?
public let providerName: String?
public let type: String public let type: String
public let image: URL? public let image: URL?
public let width: CGFloat public let width: CGFloat
public let height: CGFloat public let height: CGFloat
public let history: [History]? public let history: [History]?
public let authors: [CardAuthor]?
} }
extension Card: Sendable {} extension Card: Sendable {}

View file

@ -5,6 +5,7 @@ public enum Timelines: Endpoint {
case home(sinceId: String?, maxId: String?, minId: String?) case home(sinceId: String?, maxId: String?, minId: String?)
case list(listId: String, sinceId: String?, maxId: String?, minId: String?) case list(listId: String, sinceId: String?, maxId: String?, minId: String?)
case hashtag(tag: String, additional: [String]?, maxId: String?, minId: String?) case hashtag(tag: String, additional: [String]?, maxId: String?, minId: String?)
case link(url: URL, sinceId: String?, maxId: String?, minId: String?)
public func path() -> String { public func path() -> String {
switch self { switch self {
@ -16,6 +17,8 @@ public enum Timelines: Endpoint {
"timelines/list/\(listId)" "timelines/list/\(listId)"
case let .hashtag(tag, _, _, _): case let .hashtag(tag, _, _, _):
"timelines/tag/\(tag)" "timelines/tag/\(tag)"
case .link:
"timelines/link"
} }
} }
@ -34,6 +37,10 @@ public enum Timelines: Endpoint {
params.append(contentsOf: (additional ?? []) params.append(contentsOf: (additional ?? [])
.map { URLQueryItem(name: "any[]", value: $0) }) .map { URLQueryItem(name: "any[]", value: $0) })
return params return params
case let .link(url, sinceId, maxId, minId):
var params = makePaginationParam(sinceId: sinceId, maxId: maxId, mindId: minId) ?? []
params.append(.init(name: "url", value: url.absoluteString))
return params
} }
} }
} }

View file

@ -14,6 +14,7 @@ public struct StatusRowCardView: View {
@Environment(Theme.self) private var theme @Environment(Theme.self) private var theme
@Environment(RouterPath.self) private var routerPath @Environment(RouterPath.self) private var routerPath
@Environment(CurrentInstance.self) private var currentInstance
let card: Card let card: Card
@ -122,6 +123,9 @@ public struct StatusRowCardView: View {
.font(.scaledFootnote) .font(.scaledFootnote)
.foregroundColor(theme.tintColor) .foregroundColor(theme.tintColor)
.lineLimit(1) .lineLimit(1)
if let account = card.authors?.first?.account {
moreFromAccountView(account)
}
} }
.frame(maxWidth: .infinity, alignment: .leading) .frame(maxWidth: .infinity, alignment: .leading)
.padding(10) .padding(10)
@ -149,18 +153,39 @@ public struct StatusRowCardView: View {
.frame(width: imageHeight, height: imageHeight) .frame(width: imageHeight, height: imageHeight)
} }
VStack(alignment: .leading, spacing: 6) { VStack(alignment: .leading, spacing: 6) {
Text(title) Text(card.providerName ?? url.host() ?? url.absoluteString)
.font(.scaledHeadline)
.lineLimit(3)
Text(url.host() ?? url.absoluteString)
.font(.scaledFootnote) .font(.scaledFootnote)
.foregroundColor(theme.tintColor) .foregroundColor(theme.tintColor)
.lineLimit(1) .lineLimit(1)
Text(title)
.font(.scaledHeadline)
.lineLimit(3)
if let account = card.authors?.first?.account {
moreFromAccountView(account, divider: false)
} else if let authorName = card.authorName, !authorName.isEmpty {
Text("by \(authorName)")
.font(.scaledFootnote)
.foregroundStyle(.secondary)
.lineLimit(1)
}
if let history = card.history { if let history = card.history {
let uses = history.compactMap { Int($0.accounts) }.reduce(0, +) let uses = history.compactMap { Int($0.accounts) }.reduce(0, +)
HStack(spacing: 4) { HStack(spacing: 4) {
Image(systemName: "bubble.left.and.text.bubble.right") Button {
Text("trending-tag-people-talking \(uses)") if currentInstance.isLinkTimelineSupported {
routerPath.navigate(to: .linkTimeline(url: url, title: title))
}
} label: {
HStack(spacing: 4) {
Image(systemName: "bubble.left.and.text.bubble.right")
Text("trending-tag-people-talking \(uses)")
if currentInstance.isLinkTimelineSupported {
Image(systemName: "chevron.right")
}
}
}
.buttonStyle(.bordered)
Spacer() Spacer()
Button { Button {
#if targetEnvironment(macCatalyst) #if targetEnvironment(macCatalyst)
@ -170,11 +195,11 @@ public struct StatusRowCardView: View {
#endif #endif
} label: { } label: {
Image(systemName: "quote.opening") Image(systemName: "quote.opening")
.foregroundStyle(.secondary)
} }
.buttonStyle(.plain) .buttonStyle(.plain)
} }
.font(.scaledCaption) .font(.scaledCaption)
.foregroundStyle(.secondary)
.lineLimit(1) .lineLimit(1)
.padding(.top, 12) .padding(.top, 12)
} }
@ -222,6 +247,37 @@ public struct StatusRowCardView: View {
}.padding(16) }.padding(16)
} }
} }
@ViewBuilder
private func moreFromAccountView(_ account: Account, divider: Bool = true) -> some View {
if divider {
Divider()
}
Button {
routerPath.navigate(to: .accountDetailWithAccount(account: account))
} label: {
HStack(alignment: .center, spacing: 8) {
AvatarView(account.avatar, config: .list)
.padding(.top, 2)
HStack(alignment: .firstTextBaseline, spacing: 0) {
Text("More from ")
EmojiTextApp(account.cachedDisplayName, emojis: account.emojis)
.fontWeight(.semibold)
.emojiText.size(Font.scaledFootnoteFont.emojiSize)
.emojiText.baselineOffset(Font.scaledFootnoteFont.emojiBaselineOffset)
}
.font(.scaledFootnote)
.lineLimit(1)
.padding(.top, 3)
Spacer()
Image(systemName: "chevron.right")
}
.padding(.vertical, 4)
}
.buttonStyle(.plain)
}
} }
struct DefaultPreviewImage: View { struct DefaultPreviewImage: View {

View file

@ -35,6 +35,7 @@ public enum TimelineFilter: Hashable, Equatable, Identifiable, Sendable {
case tagGroup(title: String, tags: [String], symbolName: String?) case tagGroup(title: String, tags: [String], symbolName: String?)
case list(list: Models.List) case list(list: Models.List)
case remoteLocal(server: String, filter: RemoteTimelineFilter) case remoteLocal(server: String, filter: RemoteTimelineFilter)
case link(url: URL, title: String)
case latest case latest
case resume case resume
@ -46,6 +47,8 @@ public enum TimelineFilter: Hashable, Equatable, Identifiable, Sendable {
return list.id return list.id
case let .tagGroup(title, tags, _): case let .tagGroup(title, tags, _):
return title + tags.joined() return title + tags.joined()
case let .link(url, _):
return url.absoluteString
default: default:
return title return title
} }
@ -87,6 +90,8 @@ public enum TimelineFilter: Hashable, Equatable, Identifiable, Sendable {
"Trending" "Trending"
case .home: case .home:
"Home" "Home"
case let .link(_, title):
title
case let .hashtag(tag, _): case let .hashtag(tag, _):
"#\(tag)" "#\(tag)"
case let .tagGroup(title, _, _): case let .tagGroup(title, _, _):
@ -112,6 +117,8 @@ public enum TimelineFilter: Hashable, Equatable, Identifiable, Sendable {
"timeline.trending" "timeline.trending"
case .home: case .home:
"timeline.home" "timeline.home"
case let .link(_, title):
LocalizedStringKey(title)
case let .hashtag(tag, _): case let .hashtag(tag, _):
"#\(tag)" "#\(tag)"
case let .tagGroup(title, _, _): case let .tagGroup(title, _, _):
@ -145,6 +152,8 @@ public enum TimelineFilter: Hashable, Equatable, Identifiable, Sendable {
symbolName ?? "tag" symbolName ?? "tag"
case .hashtag: case .hashtag:
"number" "number"
case .link:
"link"
} }
} }
@ -165,6 +174,7 @@ public enum TimelineFilter: Hashable, Equatable, Identifiable, Sendable {
case .resume: return Timelines.home(sinceId: nil, maxId: nil, minId: nil) case .resume: return Timelines.home(sinceId: nil, maxId: nil, minId: nil)
case .home: return Timelines.home(sinceId: sinceId, maxId: maxId, minId: minId) case .home: return Timelines.home(sinceId: sinceId, maxId: maxId, minId: minId)
case .trending: return Trends.statuses(offset: offset) case .trending: return Trends.statuses(offset: offset)
case let .link(url, _): return Timelines.link(url: url, sinceId: sinceId, maxId: maxId, minId: minId)
case let .list(list): return Timelines.list(listId: list.id, sinceId: sinceId, maxId: maxId, minId: minId) case let .list(list): return Timelines.list(listId: list.id, sinceId: sinceId, maxId: maxId, minId: minId)
case let .hashtag(tag, accountId): case let .hashtag(tag, accountId):
if let accountId { if let accountId {
@ -202,6 +212,7 @@ extension TimelineFilter: Codable {
case remoteLocal case remoteLocal
case latest case latest
case resume case resume
case link
} }
public init(from decoder: Decoder) throws { public init(from decoder: Decoder) throws {
@ -250,6 +261,11 @@ extension TimelineFilter: Codable {
) )
case .latest: case .latest:
self = .latest self = .latest
case .link:
var nestedContainer = try container.nestedUnkeyedContainer(forKey: .link)
let url = try nestedContainer.decode(URL.self)
let title = try nestedContainer.decode(String.self)
self = .link(url: url, title: title)
default: default:
throw DecodingError.dataCorrupted( throw DecodingError.dataCorrupted(
DecodingError.Context( DecodingError.Context(
@ -290,6 +306,10 @@ extension TimelineFilter: Codable {
try container.encode(CodingKeys.latest.rawValue, forKey: .latest) try container.encode(CodingKeys.latest.rawValue, forKey: .latest)
case .resume: case .resume:
try container.encode(CodingKeys.resume.rawValue, forKey: .latest) try container.encode(CodingKeys.resume.rawValue, forKey: .latest)
case let .link(url, title):
var nestedContainer = container.nestedUnkeyedContainer(forKey: .link)
try nestedContainer.encode(url)
try nestedContainer.encode(title)
} }
} }
} }