mirror of
https://github.com/Dimillian/IceCubesApp.git
synced 2024-11-22 08:20:59 +00:00
Add support for sub.club support (#2162)
* Account tipping * Tryout full flow * Add link params * WIP * Progress flow * Fixes * More progress * Refresh user profile on notification * Tweaks * Fix follow button not refreshing * Refactor proxy url * Code cleanup * Subscribe to a premium account from a standard linked account * Premium posts tab on linked standard account * Fix flow * New domain * Fix flow * More fixes to follow flow * Update so to sub.club * Add colorScheme in URL * rollback domain * Back to sub.club * Use SubClub API for Subscription info * Fix * Merge * Merge branch 'iOS-18' + fixes
This commit is contained in:
parent
9d11814e49
commit
1ad4a245f3
19 changed files with 450 additions and 38 deletions
|
@ -143,6 +143,15 @@
|
||||||
"revision" : "668a65735751432b640260c56dfa621cec568368",
|
"revision" : "668a65735751432b640260c56dfa621cec568368",
|
||||||
"version" : "1.2.0"
|
"version" : "1.2.0"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity" : "wrappinghstack",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/dkk/WrappingHStack",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "425d9488ba55f58f0b34498c64c054c77fc2a44b",
|
||||||
|
"version" : "2.2.11"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"version" : 2
|
"version" : 2
|
||||||
|
|
|
@ -4,6 +4,8 @@ import Models
|
||||||
import Observation
|
import Observation
|
||||||
import SafariServices
|
import SafariServices
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
import AppAccount
|
||||||
|
import WebKit
|
||||||
|
|
||||||
extension View {
|
extension View {
|
||||||
@MainActor func withSafariRouter() -> some View {
|
@MainActor func withSafariRouter() -> some View {
|
||||||
|
@ -17,6 +19,7 @@ private struct SafariRouter: ViewModifier {
|
||||||
@Environment(Theme.self) private var theme
|
@Environment(Theme.self) private var theme
|
||||||
@Environment(UserPreferences.self) private var preferences
|
@Environment(UserPreferences.self) private var preferences
|
||||||
@Environment(RouterPath.self) private var routerPath
|
@Environment(RouterPath.self) private var routerPath
|
||||||
|
@Environment(AppAccountsManager.self) private var appAccount
|
||||||
|
|
||||||
#if !os(visionOS)
|
#if !os(visionOS)
|
||||||
@State private var safariManager = InAppSafariManager()
|
@State private var safariManager = InAppSafariManager()
|
||||||
|
@ -32,6 +35,10 @@ private struct SafariRouter: ViewModifier {
|
||||||
.onOpenURL { url in
|
.onOpenURL { url in
|
||||||
// Open external URL (from icecubesapp://)
|
// Open external URL (from icecubesapp://)
|
||||||
guard !isSecondaryColumn else { return }
|
guard !isSecondaryColumn else { return }
|
||||||
|
if url.absoluteString == "icecubesapp://subclub" {
|
||||||
|
safariManager.dismiss()
|
||||||
|
return
|
||||||
|
}
|
||||||
let urlString = url.absoluteString.replacingOccurrences(of: AppInfo.scheme, with: "https://")
|
let urlString = url.absoluteString.replacingOccurrences(of: AppInfo.scheme, with: "https://")
|
||||||
guard let url = URL(string: urlString), url.host != nil else { return }
|
guard let url = URL(string: urlString), url.host != nil else { return }
|
||||||
_ = routerPath.handleDeepLink(url: url)
|
_ = routerPath.handleDeepLink(url: url)
|
||||||
|
@ -47,6 +54,14 @@ private struct SafariRouter: ViewModifier {
|
||||||
UIApplication.shared.open(url)
|
UIApplication.shared.open(url)
|
||||||
return .handled
|
return .handled
|
||||||
}
|
}
|
||||||
|
} else if url.query()?.contains("callback=") == false,
|
||||||
|
url.host() == AppInfo.premiumInstance,
|
||||||
|
let accountName = appAccount.currentAccount.accountName {
|
||||||
|
let newURL = url.appending(queryItems: [
|
||||||
|
.init(name: "callback", value: "icecubesapp://subclub"),
|
||||||
|
.init(name: "id", value: "@\(accountName)")
|
||||||
|
])
|
||||||
|
return safariManager.open(newURL)
|
||||||
}
|
}
|
||||||
#if !targetEnvironment(macCatalyst)
|
#if !targetEnvironment(macCatalyst)
|
||||||
guard preferences.preferredBrowser == .inAppSafari else { return .systemAction }
|
guard preferences.preferredBrowser == .inAppSafari else { return .systemAction }
|
||||||
|
@ -101,6 +116,13 @@ private struct SafariRouter: ViewModifier {
|
||||||
|
|
||||||
return .handled
|
return .handled
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func dismiss() {
|
||||||
|
viewController.presentedViewController?.dismiss(animated: true)
|
||||||
|
window?.resignKey()
|
||||||
|
window?.isHidden = false
|
||||||
|
window = nil
|
||||||
|
}
|
||||||
|
|
||||||
func setupWindow(windowScene: UIWindowScene) -> UIWindow {
|
func setupWindow(windowScene: UIWindowScene) -> UIWindow {
|
||||||
let window = window ?? UIWindow(windowScene: windowScene)
|
let window = window ?? UIWindow(windowScene: windowScene)
|
||||||
|
|
|
@ -22,6 +22,7 @@ let package = Package(
|
||||||
.package(name: "StatusKit", path: "../StatusKit"),
|
.package(name: "StatusKit", path: "../StatusKit"),
|
||||||
.package(name: "Env", path: "../Env"),
|
.package(name: "Env", path: "../Env"),
|
||||||
.package(url: "https://github.com/Dean151/ButtonKit", from: "0.1.1"),
|
.package(url: "https://github.com/Dean151/ButtonKit", from: "0.1.1"),
|
||||||
|
.package(url: "https://github.com/dkk/WrappingHStack", from: "2.2.11"),
|
||||||
],
|
],
|
||||||
targets: [
|
targets: [
|
||||||
.target(
|
.target(
|
||||||
|
@ -32,6 +33,7 @@ let package = Package(
|
||||||
.product(name: "StatusKit", package: "StatusKit"),
|
.product(name: "StatusKit", package: "StatusKit"),
|
||||||
.product(name: "Env", package: "Env"),
|
.product(name: "Env", package: "Env"),
|
||||||
.product(name: "ButtonKit", package: "ButtonKit"),
|
.product(name: "ButtonKit", package: "ButtonKit"),
|
||||||
|
.product(name: "WrappingHStack", package: "WrappingHStack"),
|
||||||
],
|
],
|
||||||
swiftSettings: [
|
swiftSettings: [
|
||||||
.enableExperimentalFeature("StrictConcurrency"),
|
.enableExperimentalFeature("StrictConcurrency"),
|
||||||
|
|
|
@ -16,12 +16,15 @@ struct AccountDetailHeaderView: View {
|
||||||
@Environment(QuickLook.self) private var quickLook
|
@Environment(QuickLook.self) private var quickLook
|
||||||
@Environment(RouterPath.self) private var routerPath
|
@Environment(RouterPath.self) private var routerPath
|
||||||
@Environment(CurrentAccount.self) private var currentAccount
|
@Environment(CurrentAccount.self) private var currentAccount
|
||||||
|
@Environment(StreamWatcher.self) private var watcher
|
||||||
@Environment(\.redactionReasons) private var reasons
|
@Environment(\.redactionReasons) private var reasons
|
||||||
@Environment(\.isSupporter) private var isSupporter: Bool
|
@Environment(\.isSupporter) private var isSupporter: Bool
|
||||||
|
|
||||||
var viewModel: AccountDetailViewModel
|
var viewModel: AccountDetailViewModel
|
||||||
let account: Account
|
let account: Account
|
||||||
let scrollViewProxy: ScrollViewProxy?
|
let scrollViewProxy: ScrollViewProxy?
|
||||||
|
|
||||||
|
@State private var isTipSheetPresented: Bool = false
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(alignment: .leading) {
|
VStack(alignment: .leading) {
|
||||||
|
@ -44,6 +47,20 @@ struct AccountDetailHeaderView: View {
|
||||||
accountInfoView
|
accountInfoView
|
||||||
Spacer()
|
Spacer()
|
||||||
}
|
}
|
||||||
|
.onChange(of: watcher.latestEvent?.id) {
|
||||||
|
if let latestEvent = watcher.latestEvent, let latestEvent = latestEvent as? StreamEventNotification {
|
||||||
|
if latestEvent.notification.account.id == viewModel.accountId ||
|
||||||
|
latestEvent.notification.account.id == viewModel.premiumAccount?.id {
|
||||||
|
Task {
|
||||||
|
if viewModel.account?.isLinkedToPremiumAccount == true {
|
||||||
|
await viewModel.fetchAccount()
|
||||||
|
} else{
|
||||||
|
try? await viewModel.followButtonViewModel?.refreshRelationship()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var headerImageView: some View {
|
private var headerImageView: some View {
|
||||||
|
@ -210,19 +227,17 @@ struct AccountDetailHeaderView: View {
|
||||||
.accessibilityRespondsToUserInteraction(false)
|
.accessibilityRespondsToUserInteraction(false)
|
||||||
movedToView
|
movedToView
|
||||||
joinedAtView
|
joinedAtView
|
||||||
|
if viewModel.account?.isPremiumAccount == true && viewModel.relationship?.following == false || viewModel.account?.isLinkedToPremiumAccount == true && viewModel.premiumRelationship?.following == false {
|
||||||
|
tipView
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.accessibilityElement(children: .contain)
|
.accessibilityElement(children: .contain)
|
||||||
.accessibilitySortPriority(1)
|
.accessibilitySortPriority(1)
|
||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
if let relationship = viewModel.relationship, !viewModel.isCurrentUser {
|
if let followButtonViewModel = viewModel.followButtonViewModel, !viewModel.isCurrentUser {
|
||||||
HStack {
|
HStack {
|
||||||
FollowButton(viewModel: .init(accountId: account.id,
|
FollowButton(viewModel: followButtonViewModel)
|
||||||
relationship: relationship,
|
|
||||||
shouldDisplayNotify: true,
|
|
||||||
relationshipUpdated: { relationship in
|
|
||||||
viewModel.relationship = relationship
|
|
||||||
}))
|
|
||||||
}
|
}
|
||||||
} else if !viewModel.isCurrentUser {
|
} else if !viewModel.isCurrentUser {
|
||||||
ProgressView()
|
ProgressView()
|
||||||
|
@ -312,6 +327,29 @@ struct AccountDetailHeaderView: View {
|
||||||
.accessibilityElement(children: .combine)
|
.accessibilityElement(children: .combine)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
private var tipView: some View {
|
||||||
|
Button {
|
||||||
|
isTipSheetPresented = true
|
||||||
|
Task {
|
||||||
|
if viewModel.account?.isLinkedToPremiumAccount == true {
|
||||||
|
try? await viewModel.followPremiumAccount()
|
||||||
|
}
|
||||||
|
try? await viewModel.followButtonViewModel?.follow()
|
||||||
|
}
|
||||||
|
} label: {
|
||||||
|
Text("$ Subscribe")
|
||||||
|
}
|
||||||
|
.buttonStyle(.bordered)
|
||||||
|
.padding(.top, 8)
|
||||||
|
.padding(.bottom, 4)
|
||||||
|
.sheet(isPresented: $isTipSheetPresented) {
|
||||||
|
if let account = viewModel.account {
|
||||||
|
PremiumAcccountSubsciptionSheetView(account: account)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
private var movedToView: some View {
|
private var movedToView: some View {
|
||||||
|
|
|
@ -21,7 +21,6 @@ public struct AccountDetailView: View {
|
||||||
@Environment(RouterPath.self) private var routerPath
|
@Environment(RouterPath.self) private var routerPath
|
||||||
|
|
||||||
@State private var viewModel: AccountDetailViewModel
|
@State private var viewModel: AccountDetailViewModel
|
||||||
@State private var isCurrentUser: Bool = false
|
|
||||||
@State private var showBlockConfirmation: Bool = false
|
@State private var showBlockConfirmation: Bool = false
|
||||||
@State private var isEditingRelationshipNote: Bool = false
|
@State private var isEditingRelationshipNote: Bool = false
|
||||||
@State private var showTranslateView: Bool = false
|
@State private var showTranslateView: Bool = false
|
||||||
|
@ -57,8 +56,7 @@ public struct AccountDetailView: View {
|
||||||
.applyAccountDetailsRowStyle(theme: theme)
|
.applyAccountDetailsRowStyle(theme: theme)
|
||||||
|
|
||||||
Picker("", selection: $viewModel.selectedTab) {
|
Picker("", selection: $viewModel.selectedTab) {
|
||||||
ForEach(isCurrentUser ? AccountDetailViewModel.Tab.currentAccountTabs : AccountDetailViewModel.Tab.accountTabs,
|
ForEach(viewModel.tabs, id: \.self)
|
||||||
id: \.self)
|
|
||||||
{ tab in
|
{ tab in
|
||||||
if tab == .boosts {
|
if tab == .boosts {
|
||||||
Image("Rocket")
|
Image("Rocket")
|
||||||
|
@ -113,8 +111,7 @@ public struct AccountDetailView: View {
|
||||||
}
|
}
|
||||||
.onAppear {
|
.onAppear {
|
||||||
guard reasons != .placeholder else { return }
|
guard reasons != .placeholder else { return }
|
||||||
isCurrentUser = currentAccount.account?.id == viewModel.accountId
|
viewModel.isCurrentUser = currentAccount.account?.id == viewModel.accountId
|
||||||
viewModel.isCurrentUser = isCurrentUser
|
|
||||||
viewModel.client = client
|
viewModel.client = client
|
||||||
|
|
||||||
// Avoid capturing non-Sendable `self` just to access the view model.
|
// Avoid capturing non-Sendable `self` just to access the view model.
|
||||||
|
@ -314,7 +311,7 @@ public struct AccountDetailView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if isCurrentUser {
|
if viewModel.isCurrentUser {
|
||||||
Button {
|
Button {
|
||||||
routerPath.presentedSheet = .accountEditInfo
|
routerPath.presentedSheet = .accountEditInfo
|
||||||
} label: {
|
} label: {
|
||||||
|
|
|
@ -16,7 +16,7 @@ import SwiftUI
|
||||||
}
|
}
|
||||||
|
|
||||||
enum Tab: Int {
|
enum Tab: Int {
|
||||||
case statuses, favorites, bookmarks, replies, boosts, media
|
case statuses, favorites, bookmarks, replies, boosts, media, premiumPosts
|
||||||
|
|
||||||
static var currentAccountTabs: [Tab] {
|
static var currentAccountTabs: [Tab] {
|
||||||
[.statuses, .replies, .boosts, .favorites, .bookmarks]
|
[.statuses, .replies, .boosts, .favorites, .bookmarks]
|
||||||
|
@ -25,6 +25,10 @@ import SwiftUI
|
||||||
static var accountTabs: [Tab] {
|
static var accountTabs: [Tab] {
|
||||||
[.statuses, .replies, .boosts, .media]
|
[.statuses, .replies, .boosts, .media]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static var premiumAccountTabs: [Tab] {
|
||||||
|
[.statuses, .premiumPosts, .replies, .boosts, .media]
|
||||||
|
}
|
||||||
|
|
||||||
var iconName: String {
|
var iconName: String {
|
||||||
switch self {
|
switch self {
|
||||||
|
@ -34,6 +38,7 @@ import SwiftUI
|
||||||
case .replies: "bubble.left.and.bubble.right"
|
case .replies: "bubble.left.and.bubble.right"
|
||||||
case .boosts: ""
|
case .boosts: ""
|
||||||
case .media: "photo.on.rectangle.angled"
|
case .media: "photo.on.rectangle.angled"
|
||||||
|
case .premiumPosts: "dollarsign"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -45,9 +50,21 @@ import SwiftUI
|
||||||
case .replies: "accessibility.tabs.profile.picker.posts-and-replies"
|
case .replies: "accessibility.tabs.profile.picker.posts-and-replies"
|
||||||
case .boosts: "accessibility.tabs.profile.picker.boosts"
|
case .boosts: "accessibility.tabs.profile.picker.boosts"
|
||||||
case .media: "accessibility.tabs.profile.picker.media"
|
case .media: "accessibility.tabs.profile.picker.media"
|
||||||
|
case .premiumPosts: "Premium Posts"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
var tabs: [Tab] {
|
||||||
|
if isCurrentUser {
|
||||||
|
return Tab.currentAccountTabs
|
||||||
|
} else if account?.isLinkedToPremiumAccount == true && premiumAccount != nil {
|
||||||
|
return Tab.premiumAccountTabs
|
||||||
|
} else {
|
||||||
|
return Tab.accountTabs
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var accountState: AccountState = .loading
|
var accountState: AccountState = .loading
|
||||||
var statusesState: StatusesState = .loading
|
var statusesState: StatusesState = .loading
|
||||||
|
@ -61,10 +78,14 @@ import SwiftUI
|
||||||
var featuredTags: [FeaturedTag] = []
|
var featuredTags: [FeaturedTag] = []
|
||||||
var fields: [Account.Field] = []
|
var fields: [Account.Field] = []
|
||||||
var familiarFollowers: [Account] = []
|
var familiarFollowers: [Account] = []
|
||||||
|
|
||||||
|
var premiumAccount: Account?
|
||||||
|
var premiumRelationship: Relationship?
|
||||||
|
|
||||||
var selectedTab = Tab.statuses {
|
var selectedTab = Tab.statuses {
|
||||||
didSet {
|
didSet {
|
||||||
switch selectedTab {
|
switch selectedTab {
|
||||||
case .statuses, .replies, .boosts, .media:
|
case .statuses, .replies, .boosts, .media, .premiumPosts:
|
||||||
tabTask?.cancel()
|
tabTask?.cancel()
|
||||||
tabTask = Task {
|
tabTask = Task {
|
||||||
await fetchNewestStatuses(pullToRefresh: false)
|
await fetchNewestStatuses(pullToRefresh: false)
|
||||||
|
@ -79,7 +100,9 @@ import SwiftUI
|
||||||
|
|
||||||
var translation: Translation?
|
var translation: Translation?
|
||||||
var isLoadingTranslation = false
|
var isLoadingTranslation = false
|
||||||
|
|
||||||
|
var followButtonViewModel: FollowButtonViewModel?
|
||||||
|
|
||||||
private(set) var account: Account?
|
private(set) var account: Account?
|
||||||
private var tabTask: Task<Void, Never>?
|
private var tabTask: Task<Void, Never>?
|
||||||
|
|
||||||
|
@ -113,13 +136,27 @@ import SwiftUI
|
||||||
guard let client else { return }
|
guard let client else { return }
|
||||||
do {
|
do {
|
||||||
let data = try await fetchAccountData(accountId: accountId, client: client)
|
let data = try await fetchAccountData(accountId: accountId, client: client)
|
||||||
|
|
||||||
accountState = .data(account: data.account)
|
accountState = .data(account: data.account)
|
||||||
|
try await fetchPremiumAccount(fromAccount: data.account, client: client)
|
||||||
account = data.account
|
account = data.account
|
||||||
fields = data.account.fields
|
fields = data.account.fields
|
||||||
featuredTags = data.featuredTags
|
featuredTags = data.featuredTags
|
||||||
featuredTags.sort { $0.statusesCountInt > $1.statusesCountInt }
|
featuredTags.sort { $0.statusesCountInt > $1.statusesCountInt }
|
||||||
relationship = data.relationships.first
|
relationship = data.relationships.first
|
||||||
|
if let relationship {
|
||||||
|
if let followButtonViewModel {
|
||||||
|
followButtonViewModel.relationship = relationship
|
||||||
|
} else {
|
||||||
|
followButtonViewModel = .init(client: client,
|
||||||
|
accountId: accountId,
|
||||||
|
relationship: relationship,
|
||||||
|
shouldDisplayNotify: true,
|
||||||
|
relationshipUpdated: { [weak self] relationship in
|
||||||
|
self?.relationship = relationship
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
} catch {
|
} catch {
|
||||||
if let account {
|
if let account {
|
||||||
accountState = .data(account: account)
|
accountState = .data(account: account)
|
||||||
|
@ -159,8 +196,12 @@ import SwiftUI
|
||||||
do {
|
do {
|
||||||
statusesState = .loading
|
statusesState = .loading
|
||||||
boosts = []
|
boosts = []
|
||||||
|
var accountIdToFetch = accountId
|
||||||
|
if selectedTab == .premiumPosts, let accountId = premiumAccount?.id {
|
||||||
|
accountIdToFetch = accountId
|
||||||
|
}
|
||||||
statuses =
|
statuses =
|
||||||
try await client.get(endpoint: Accounts.statuses(id: accountId,
|
try await client.get(endpoint: Accounts.statuses(id: accountIdToFetch,
|
||||||
sinceId: nil,
|
sinceId: nil,
|
||||||
tag: nil,
|
tag: nil,
|
||||||
onlyMedia: selectedTab == .media,
|
onlyMedia: selectedTab == .media,
|
||||||
|
@ -197,10 +238,14 @@ import SwiftUI
|
||||||
func fetchNextPage() async throws {
|
func fetchNextPage() async throws {
|
||||||
guard let client else { return }
|
guard let client else { return }
|
||||||
switch selectedTab {
|
switch selectedTab {
|
||||||
case .statuses, .replies, .boosts, .media:
|
case .statuses, .replies, .boosts, .media, .premiumPosts:
|
||||||
guard let lastId = statuses.last?.id else { return }
|
guard let lastId = statuses.last?.id else { return }
|
||||||
|
var accountIdToFetch = accountId
|
||||||
|
if selectedTab == .premiumPosts, let accountId = premiumAccount?.id {
|
||||||
|
accountIdToFetch = accountId
|
||||||
|
}
|
||||||
let newStatuses: [Status] =
|
let newStatuses: [Status] =
|
||||||
try await client.get(endpoint: Accounts.statuses(id: accountId,
|
try await client.get(endpoint: Accounts.statuses(id: accountIdToFetch,
|
||||||
sinceId: lastId,
|
sinceId: lastId,
|
||||||
tag: nil,
|
tag: nil,
|
||||||
onlyMedia: selectedTab == .media,
|
onlyMedia: selectedTab == .media,
|
||||||
|
@ -239,7 +284,7 @@ import SwiftUI
|
||||||
|
|
||||||
private func reloadTabState() {
|
private func reloadTabState() {
|
||||||
switch selectedTab {
|
switch selectedTab {
|
||||||
case .statuses, .replies, .media:
|
case .statuses, .replies, .media, .premiumPosts:
|
||||||
statusesState = .display(statuses: statuses, nextPageState: statuses.count < 20 ? .none : .hasNextPage)
|
statusesState = .display(statuses: statuses, nextPageState: statuses.count < 20 ? .none : .hasNextPage)
|
||||||
case .boosts:
|
case .boosts:
|
||||||
statusesState = .display(statuses: boosts, nextPageState: statuses.count < 20 ? .none : .hasNextPage)
|
statusesState = .display(statuses: boosts, nextPageState: statuses.count < 20 ? .none : .hasNextPage)
|
||||||
|
@ -277,3 +322,28 @@ import SwiftUI
|
||||||
|
|
||||||
func statusDidDisappear(status _: Status) {}
|
func statusDidDisappear(status _: Status) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension AccountDetailViewModel {
|
||||||
|
private func fetchPremiumAccount(fromAccount: Account, client: Client) async throws {
|
||||||
|
if fromAccount.isLinkedToPremiumAccount, let acct = fromAccount.premiumAcct {
|
||||||
|
let results: SearchResults? = try await client.get(endpoint: Search.search(query: acct,
|
||||||
|
type: .accounts,
|
||||||
|
offset: nil,
|
||||||
|
following: nil),
|
||||||
|
forceVersion: .v2)
|
||||||
|
if let premiumAccount = results?.accounts.first {
|
||||||
|
self.premiumAccount = premiumAccount
|
||||||
|
let relationships: [Relationship] = try await client.get(endpoint: Accounts.relationships(ids: [premiumAccount.id]))
|
||||||
|
self.premiumRelationship = relationships.first
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func followPremiumAccount() async throws {
|
||||||
|
if let premiumAccount {
|
||||||
|
premiumRelationship = try await client?.post(endpoint: Accounts.follow(id: premiumAccount.id,
|
||||||
|
notify: false,
|
||||||
|
reblogs: true))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -95,7 +95,8 @@ public struct AccountsListRow: View {
|
||||||
let relationShip = viewModel.relationShip
|
let relationShip = viewModel.relationShip
|
||||||
{
|
{
|
||||||
VStack(alignment: .center) {
|
VStack(alignment: .center) {
|
||||||
FollowButton(viewModel: .init(accountId: viewModel.account.id,
|
FollowButton(viewModel: .init(client: client,
|
||||||
|
accountId: viewModel.account.id,
|
||||||
relationship: relationShip,
|
relationship: relationShip,
|
||||||
shouldDisplayNotify: false,
|
shouldDisplayNotify: false,
|
||||||
relationshipUpdated: { _ in }))
|
relationshipUpdated: { _ in }))
|
||||||
|
|
|
@ -9,18 +9,20 @@ import SwiftUI
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
@Observable public class FollowButtonViewModel {
|
@Observable public class FollowButtonViewModel {
|
||||||
var client: Client?
|
let client: Client
|
||||||
|
|
||||||
public let accountId: String
|
public let accountId: String
|
||||||
public let shouldDisplayNotify: Bool
|
public let shouldDisplayNotify: Bool
|
||||||
public let relationshipUpdated: (Relationship) -> Void
|
public let relationshipUpdated: (Relationship) -> Void
|
||||||
public private(set) var relationship: Relationship
|
public var relationship: Relationship
|
||||||
|
|
||||||
public init(accountId: String,
|
public init(client: Client,
|
||||||
|
accountId: String,
|
||||||
relationship: Relationship,
|
relationship: Relationship,
|
||||||
shouldDisplayNotify: Bool,
|
shouldDisplayNotify: Bool,
|
||||||
relationshipUpdated: @escaping ((Relationship) -> Void))
|
relationshipUpdated: @escaping ((Relationship) -> Void))
|
||||||
{
|
{
|
||||||
|
self.client = client
|
||||||
self.accountId = accountId
|
self.accountId = accountId
|
||||||
self.relationship = relationship
|
self.relationship = relationship
|
||||||
self.shouldDisplayNotify = shouldDisplayNotify
|
self.shouldDisplayNotify = shouldDisplayNotify
|
||||||
|
@ -28,7 +30,6 @@ import SwiftUI
|
||||||
}
|
}
|
||||||
|
|
||||||
func follow() async throws {
|
func follow() async throws {
|
||||||
guard let client else { return }
|
|
||||||
do {
|
do {
|
||||||
relationship = try await client.post(endpoint: Accounts.follow(id: accountId, notify: false, reblogs: true))
|
relationship = try await client.post(endpoint: Accounts.follow(id: accountId, notify: false, reblogs: true))
|
||||||
relationshipUpdated(relationship)
|
relationshipUpdated(relationship)
|
||||||
|
@ -38,7 +39,6 @@ import SwiftUI
|
||||||
}
|
}
|
||||||
|
|
||||||
func unfollow() async throws {
|
func unfollow() async throws {
|
||||||
guard let client else { return }
|
|
||||||
do {
|
do {
|
||||||
relationship = try await client.post(endpoint: Accounts.unfollow(id: accountId))
|
relationship = try await client.post(endpoint: Accounts.unfollow(id: accountId))
|
||||||
relationshipUpdated(relationship)
|
relationshipUpdated(relationship)
|
||||||
|
@ -46,9 +46,16 @@ import SwiftUI
|
||||||
throw error
|
throw error
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func refreshRelationship() async throws {
|
||||||
|
let relationships: [Relationship] = try await client.get(endpoint: Accounts.relationships(ids: [accountId]))
|
||||||
|
if let relationship = relationships.first {
|
||||||
|
self.relationship = relationship
|
||||||
|
relationshipUpdated(relationship)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func toggleNotify() async throws {
|
func toggleNotify() async throws {
|
||||||
guard let client else { return }
|
|
||||||
do {
|
do {
|
||||||
relationship = try await client.post(endpoint: Accounts.follow(id: accountId,
|
relationship = try await client.post(endpoint: Accounts.follow(id: accountId,
|
||||||
notify: !relationship.notifying,
|
notify: !relationship.notifying,
|
||||||
|
@ -60,7 +67,6 @@ import SwiftUI
|
||||||
}
|
}
|
||||||
|
|
||||||
func toggleReboosts() async throws {
|
func toggleReboosts() async throws {
|
||||||
guard let client else { return }
|
|
||||||
do {
|
do {
|
||||||
relationship = try await client.post(endpoint: Accounts.follow(id: accountId,
|
relationship = try await client.post(endpoint: Accounts.follow(id: accountId,
|
||||||
notify: relationship.notifying,
|
notify: relationship.notifying,
|
||||||
|
@ -83,7 +89,7 @@ public struct FollowButton: View {
|
||||||
public var body: some View {
|
public var body: some View {
|
||||||
VStack(alignment: .trailing) {
|
VStack(alignment: .trailing) {
|
||||||
AsyncButton {
|
AsyncButton {
|
||||||
if viewModel.relationship.following {
|
if viewModel.relationship.following || viewModel.relationship.requested {
|
||||||
try await viewModel.unfollow()
|
try await viewModel.unfollow()
|
||||||
} else {
|
} else {
|
||||||
try await viewModel.follow()
|
try await viewModel.follow()
|
||||||
|
@ -121,8 +127,5 @@ public struct FollowButton: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.buttonStyle(.bordered)
|
.buttonStyle(.bordered)
|
||||||
.onAppear {
|
|
||||||
viewModel.client = client
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,150 @@
|
||||||
|
import SwiftUI
|
||||||
|
import Models
|
||||||
|
import Env
|
||||||
|
import DesignSystem
|
||||||
|
import WrappingHStack
|
||||||
|
import AppAccount
|
||||||
|
import Network
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
struct PremiumAcccountSubsciptionSheetView: View {
|
||||||
|
@Environment(\.dismiss) private var dismiss
|
||||||
|
@Environment(Theme.self) private var theme: Theme
|
||||||
|
@Environment(\.openURL) private var openURL
|
||||||
|
@Environment(AppAccountsManager.self) private var appAccount: AppAccountsManager
|
||||||
|
@Environment(\.colorScheme) private var colorScheme
|
||||||
|
|
||||||
|
@State private var isSubscibeSelected: Bool = false
|
||||||
|
|
||||||
|
private enum SheetState: Int, Equatable {
|
||||||
|
case selection, preparing, webview
|
||||||
|
}
|
||||||
|
|
||||||
|
@State private var state: SheetState = .selection
|
||||||
|
@State private var animationsending: Bool = false
|
||||||
|
@State private var subClubUser: SubClubUser?
|
||||||
|
|
||||||
|
let account: Account
|
||||||
|
let subClubClient = SubClubClient()
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
VStack {
|
||||||
|
switch state {
|
||||||
|
case .selection:
|
||||||
|
tipView
|
||||||
|
case .preparing:
|
||||||
|
preparingView
|
||||||
|
.transition(.blurReplace)
|
||||||
|
case .webview:
|
||||||
|
webView
|
||||||
|
.transition(.blurReplace)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.presentationBackground(.thinMaterial)
|
||||||
|
.presentationCornerRadius(8)
|
||||||
|
.presentationDetents([.height(330)])
|
||||||
|
.task {
|
||||||
|
if let premiumUsername = account.premiumUsername {
|
||||||
|
let user = await subClubClient.getUser(username: premiumUsername)
|
||||||
|
withAnimation {
|
||||||
|
subClubUser = user
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
private var tipView: some View {
|
||||||
|
HStack {
|
||||||
|
Spacer()
|
||||||
|
Button {
|
||||||
|
dismiss()
|
||||||
|
} label: {
|
||||||
|
Image(systemName: "xmark.circle")
|
||||||
|
.font(.title3)
|
||||||
|
}
|
||||||
|
.padding(.trailing, 12)
|
||||||
|
.padding(.top, 8)
|
||||||
|
}
|
||||||
|
VStack(alignment: .leading, spacing: 8) {
|
||||||
|
Text("Subscribe")
|
||||||
|
.font(.title2)
|
||||||
|
Text("Subscribe to @\(account.username) to get access to exclusive content!")
|
||||||
|
if let subscription = subClubUser?.subscription {
|
||||||
|
Button {
|
||||||
|
withAnimation(.easeInOut(duration: 0.5)) {
|
||||||
|
isSubscibeSelected = true
|
||||||
|
}
|
||||||
|
} label: {
|
||||||
|
Text("\(subscription.formattedAmount) / month")
|
||||||
|
}
|
||||||
|
.buttonStyle(.borderedProminent)
|
||||||
|
.padding(.vertical, 8)
|
||||||
|
} else {
|
||||||
|
ProgressView()
|
||||||
|
.foregroundStyle(theme.labelColor)
|
||||||
|
.padding(.vertical, 8)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
|
.padding(12)
|
||||||
|
.background(theme.secondaryBackgroundColor.opacity(0.4))
|
||||||
|
.cornerRadius(8)
|
||||||
|
.padding(12)
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
if isSubscibeSelected {
|
||||||
|
Button {
|
||||||
|
withAnimation {
|
||||||
|
state = .preparing
|
||||||
|
}
|
||||||
|
} label: {
|
||||||
|
Text("Subscribe")
|
||||||
|
.font(.headline)
|
||||||
|
.fontWeight(.bold)
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
|
.frame(height: 40)
|
||||||
|
}
|
||||||
|
.buttonStyle(.borderedProminent)
|
||||||
|
.padding(.horizontal, 16)
|
||||||
|
.padding(.bottom, 38)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var preparingView: some View {
|
||||||
|
Label("Preparing...", systemImage: "wifi")
|
||||||
|
.symbolEffect(.variableColor.iterative, options: .repeating, value: animationsending)
|
||||||
|
.font(.title)
|
||||||
|
.fontWeight(.bold)
|
||||||
|
.onAppear {
|
||||||
|
animationsending = true
|
||||||
|
DispatchQueue.main.asyncAfter(deadline: .now() + 4.0) {
|
||||||
|
dismiss()
|
||||||
|
}
|
||||||
|
DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) {
|
||||||
|
withAnimation {
|
||||||
|
state = .webview
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var webView: some View {
|
||||||
|
VStack(alignment: .center) {
|
||||||
|
Text("Almost there...")
|
||||||
|
}
|
||||||
|
.font(.title)
|
||||||
|
.fontWeight(.bold)
|
||||||
|
.onAppear {
|
||||||
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
||||||
|
if let subscription = subClubUser?.subscription,
|
||||||
|
let accountName = appAccount.currentAccount.accountName,
|
||||||
|
let premiumUsername = account.premiumUsername,
|
||||||
|
let url = URL(string: "https://\(AppInfo.premiumInstance)/@\(premiumUsername)/subscribe?callback=icecubesapp://subclub&id=@\(accountName)&amount=\(subscription.unitAmount)¤cy=\(subscription.currency)&theme=\(colorScheme)") {
|
||||||
|
openURL(url)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -183,7 +183,8 @@ public enum SettingsStartingPoint {
|
||||||
{
|
{
|
||||||
navigate(to: .hashTag(tag: tag, account: nil))
|
navigate(to: .hashTag(tag: tag, account: nil))
|
||||||
return .handled
|
return .handled
|
||||||
} else if url.lastPathComponent.first == "@",
|
} else if url.lastPathComponent.first == "@" ||
|
||||||
|
(url.host() == AppInfo.premiumInstance && url.pathComponents.contains("users")),
|
||||||
let host = url.host,
|
let host = url.host,
|
||||||
!host.hasPrefix("www")
|
!host.hasPrefix("www")
|
||||||
{
|
{
|
||||||
|
|
|
@ -150,7 +150,8 @@ public struct ListEditView: View {
|
||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
} else {
|
} else {
|
||||||
FollowButton(viewModel: .init(accountId: account.id,
|
FollowButton(viewModel: .init(client: client,
|
||||||
|
accountId: account.id,
|
||||||
relationship: relationship,
|
relationship: relationship,
|
||||||
shouldDisplayNotify: false,
|
shouldDisplayNotify: false,
|
||||||
relationshipUpdated: { relationship in
|
relationshipUpdated: { relationship in
|
||||||
|
|
|
@ -71,6 +71,10 @@ public final class Account: Codable, Identifiable, Hashable, Sendable, Equatable
|
||||||
header.lastPathComponent != "missing.png"
|
header.lastPathComponent != "missing.png"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public var fullAccountName: String {
|
||||||
|
"\(acct)@\(url?.host() ?? "")"
|
||||||
|
}
|
||||||
|
|
||||||
public init(id: String, username: String, displayName: String?, avatar: URL, header: URL, acct: String, note: HTMLString, createdAt: ServerDate, followersCount: Int, followingCount: Int, statusesCount: Int, lastStatusAt: String? = nil, fields: [Account.Field], locked: Bool, emojis: [Emoji], url: URL? = nil, source: Account.Source? = nil, bot: Bool, discoverable: Bool? = nil, moved: Account? = nil) {
|
public init(id: String, username: String, displayName: String?, avatar: URL, header: URL, acct: String, note: HTMLString, createdAt: ServerDate, followersCount: Int, followingCount: Int, statusesCount: Int, lastStatusAt: String? = nil, fields: [Account.Field], locked: Bool, emojis: [Emoji], url: URL? = nil, source: Account.Source? = nil, bot: Bool, discoverable: Bool? = nil, moved: Account? = nil) {
|
||||||
self.id = id
|
self.id = id
|
||||||
self.username = username
|
self.username = username
|
||||||
|
@ -187,3 +191,36 @@ public struct FamiliarAccounts: Decodable {
|
||||||
}
|
}
|
||||||
|
|
||||||
extension FamiliarAccounts: Sendable {}
|
extension FamiliarAccounts: Sendable {}
|
||||||
|
|
||||||
|
// Premium Stuff
|
||||||
|
extension Account {
|
||||||
|
public var isLinkedToPremiumAccount: Bool {
|
||||||
|
guard url?.host() != AppInfo.premiumInstance else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return fields.first(where: { $0.value.asRawText.contains(AppInfo.premiumInstance) }) != nil
|
||||||
|
}
|
||||||
|
|
||||||
|
public var premiumAcct: String? {
|
||||||
|
if isPremiumAccount {
|
||||||
|
return "@\(acct)"
|
||||||
|
} else if let field = fields.first(where: { $0.value.asRawText.hasSuffix(AppInfo.premiumInstance) }) {
|
||||||
|
return field.value.asRawText
|
||||||
|
} else if let field = fields.first(where: { $0.value.asRawText.hasPrefix("https://\(AppInfo.premiumInstance)") }),
|
||||||
|
let url = URL(string: field.value.asRawText) {
|
||||||
|
return "\(url.lastPathComponent)@\(url.host() ?? "\(AppInfo.premiumInstance)")"
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
public var premiumUsername: String? {
|
||||||
|
var username = premiumAcct?.replacingOccurrences(of: "@\(AppInfo.premiumInstance)", with: "")
|
||||||
|
username?.removeFirst()
|
||||||
|
return username
|
||||||
|
}
|
||||||
|
|
||||||
|
public var isPremiumAccount: Bool {
|
||||||
|
url?.host() == AppInfo.premiumInstance
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
|
@ -9,4 +9,5 @@ public enum AppInfo {
|
||||||
public static let revenueCatKey = "appl_JXmiRckOzXXTsHKitQiicXCvMQi"
|
public static let revenueCatKey = "appl_JXmiRckOzXXTsHKitQiicXCvMQi"
|
||||||
public static let defaultServer = "mastodon.social"
|
public static let defaultServer = "mastodon.social"
|
||||||
public static let keychainGroup = "346J38YKE3.com.thomasricouard.IceCubesApp"
|
public static let keychainGroup = "346J38YKE3.com.thomasricouard.IceCubesApp"
|
||||||
|
public static let premiumInstance = "sub.club"
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
public struct Relationship: Codable {
|
public struct Relationship: Codable, Equatable, Identifiable {
|
||||||
public let id: String
|
public let id: String
|
||||||
public let following: Bool
|
public let following: Bool
|
||||||
public let showingReblogs: Bool
|
public let showingReblogs: Bool
|
||||||
|
|
22
Packages/Models/Sources/Models/SubClubUser.swift
Normal file
22
Packages/Models/Sources/Models/SubClubUser.swift
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
public struct SubClubUser: Sendable, Identifiable, Decodable {
|
||||||
|
public struct Subscription: Sendable, Decodable {
|
||||||
|
public let paymentType: String
|
||||||
|
public let currency: String
|
||||||
|
public let interval: String
|
||||||
|
public let intervalCount: Int
|
||||||
|
public let unitAmount: Int
|
||||||
|
|
||||||
|
public var formattedAmount: String {
|
||||||
|
let formatter = NumberFormatter()
|
||||||
|
formatter.numberStyle = .currency
|
||||||
|
formatter.locale = Locale(identifier: "en_US")
|
||||||
|
formatter.maximumFractionDigits = 0
|
||||||
|
return formatter.string(from: .init(integerLiteral: unitAmount / 100)) ?? "$NaN"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public let id: String
|
||||||
|
public let subscription: Subscription?
|
||||||
|
}
|
36
Packages/Network/Sources/Network/SubClubClient.swift
Normal file
36
Packages/Network/Sources/Network/SubClubClient.swift
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
import Foundation
|
||||||
|
import Models
|
||||||
|
|
||||||
|
public struct SubClubClient: Sendable {
|
||||||
|
public enum Endpoint {
|
||||||
|
case user(username: String)
|
||||||
|
|
||||||
|
var path: String {
|
||||||
|
switch self {
|
||||||
|
case .user(let username):
|
||||||
|
return "users/\(username)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public init() { }
|
||||||
|
|
||||||
|
private var url: String {
|
||||||
|
"https://\(AppInfo.premiumInstance)/"
|
||||||
|
}
|
||||||
|
|
||||||
|
public func getUser(username: String) async -> SubClubUser? {
|
||||||
|
guard let url = URL(string: url.appending(Endpoint.user(username: username).path)) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
let request = URLRequest(url: url)
|
||||||
|
do {
|
||||||
|
let (result, _) = try await URLSession.shared.data(for: request)
|
||||||
|
let decoder = JSONDecoder()
|
||||||
|
let user = try decoder.decode(SubClubUser.self, from: result)
|
||||||
|
return user
|
||||||
|
} catch {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -18,7 +18,6 @@ public struct StatusRowView: View {
|
||||||
@Environment(\.isHomeTimeline) private var isHomeTimeline
|
@Environment(\.isHomeTimeline) private var isHomeTimeline
|
||||||
|
|
||||||
@Environment(RouterPath.self) private var routerPath: RouterPath
|
@Environment(RouterPath.self) private var routerPath: RouterPath
|
||||||
|
|
||||||
@Environment(QuickLook.self) private var quickLook
|
@Environment(QuickLook.self) private var quickLook
|
||||||
@Environment(Theme.self) private var theme
|
@Environment(Theme.self) private var theme
|
||||||
@Environment(Client.self) private var client
|
@Environment(Client.self) private var client
|
||||||
|
@ -64,6 +63,7 @@ public struct StatusRowView: View {
|
||||||
} else {
|
} else {
|
||||||
if !isCompact && context != .detail {
|
if !isCompact && context != .detail {
|
||||||
Group {
|
Group {
|
||||||
|
StatusRowPremiumView(viewModel: viewModel)
|
||||||
StatusRowTagView(viewModel: viewModel)
|
StatusRowTagView(viewModel: viewModel)
|
||||||
StatusRowReblogView(viewModel: viewModel)
|
StatusRowReblogView(viewModel: viewModel)
|
||||||
StatusRowReplyView(viewModel: viewModel)
|
StatusRowReplyView(viewModel: viewModel)
|
||||||
|
|
|
@ -126,7 +126,9 @@ import SwiftUI
|
||||||
} else if userMentionned {
|
} else if userMentionned {
|
||||||
theme.secondaryBackgroundColor
|
theme.secondaryBackgroundColor
|
||||||
} else {
|
} else {
|
||||||
if userFollowedTag != nil {
|
if status.account.isPremiumAccount {
|
||||||
|
makeDecorativeGradient(startColor: .yellow, endColor: theme.primaryBackgroundColor)
|
||||||
|
} else if userFollowedTag != nil {
|
||||||
makeDecorativeGradient(startColor: .teal, endColor: theme.primaryBackgroundColor)
|
makeDecorativeGradient(startColor: .teal, endColor: theme.primaryBackgroundColor)
|
||||||
} else if status.reblog != nil {
|
} else if status.reblog != nil {
|
||||||
makeDecorativeGradient(startColor: theme.tintColor, endColor: theme.primaryBackgroundColor)
|
makeDecorativeGradient(startColor: theme.tintColor, endColor: theme.primaryBackgroundColor)
|
||||||
|
@ -142,6 +144,8 @@ import SwiftUI
|
||||||
theme.tintColor.opacity(0.15)
|
theme.tintColor.opacity(0.15)
|
||||||
} else if userMentionned {
|
} else if userMentionned {
|
||||||
theme.secondaryBackgroundColor
|
theme.secondaryBackgroundColor
|
||||||
|
} else if status.account.isPremiumAccount {
|
||||||
|
Color.yellow.opacity(0.4)
|
||||||
} else {
|
} else {
|
||||||
theme.primaryBackgroundColor
|
theme.primaryBackgroundColor
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,18 @@
|
||||||
|
import DesignSystem
|
||||||
|
import Env
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct StatusRowPremiumView: View {
|
||||||
|
@Environment(\.isHomeTimeline) private var isHomeTimeline
|
||||||
|
|
||||||
|
let viewModel: StatusRowViewModel
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
if isHomeTimeline, viewModel.status.account.isPremiumAccount {
|
||||||
|
Text("From a subscribed premium account")
|
||||||
|
.font(.scaledFootnote)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
.fontWeight(.semibold)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue