mirror of
https://github.com/Dimillian/IceCubesApp.git
synced 2024-11-25 17:51:01 +00:00
StatusRow: Split into proper view struct
This commit is contained in:
parent
b7e7ee0736
commit
a3744525df
15 changed files with 459 additions and 395 deletions
|
@ -214,14 +214,14 @@ public struct ExploreView: View {
|
|||
Section("explore.section.trending.links") {
|
||||
ForEach(viewModel.trendingLinks
|
||||
.prefix(upTo: viewModel.trendingLinks.count > 3 ? 3 : viewModel.trendingLinks.count)) { card in
|
||||
StatusCardView(card: card)
|
||||
StatusRowCardView(card: card)
|
||||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
.padding(.vertical, 8)
|
||||
}
|
||||
NavigationLink {
|
||||
List {
|
||||
ForEach(viewModel.trendingLinks) { card in
|
||||
StatusCardView(card: card)
|
||||
StatusRowCardView(card: card)
|
||||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
.padding(.vertical, 8)
|
||||
}
|
||||
|
|
|
@ -8,8 +8,9 @@ import SwiftUI
|
|||
|
||||
public struct StatusRowView: View {
|
||||
@Environment(\.redactionReasons) private var reasons
|
||||
@EnvironmentObject private var preferences: UserPreferences
|
||||
|
||||
@EnvironmentObject private var theme: Theme
|
||||
|
||||
@StateObject var viewModel: StatusRowViewModel
|
||||
|
||||
public init(viewModel: StatusRowViewModel) {
|
||||
|
@ -31,16 +32,15 @@ public struct StatusRowView: View {
|
|||
.listRowSeparator(.hidden)
|
||||
}
|
||||
} else {
|
||||
let status: AnyStatus = viewModel.status.reblog ?? viewModel.status
|
||||
VStack(alignment: .leading) {
|
||||
if !viewModel.isCompact, theme.avatarPosition == .leading {
|
||||
reblogView
|
||||
replyView
|
||||
StatusRowReblogView(viewModel: viewModel)
|
||||
StatusRowReplyView(viewModel: viewModel)
|
||||
}
|
||||
HStack(alignment: .top, spacing: .statusColumnsSpacing) {
|
||||
if !viewModel.isCompact,
|
||||
theme.avatarPosition == .leading,
|
||||
let status: AnyStatus = viewModel.status.reblog ?? viewModel.status
|
||||
{
|
||||
theme.avatarPosition == .leading {
|
||||
Button {
|
||||
viewModel.routerPath.navigate(to: .accountDetailWithAccount(account: status.account))
|
||||
} label: {
|
||||
|
@ -49,12 +49,26 @@ public struct StatusRowView: View {
|
|||
}
|
||||
VStack(alignment: .leading) {
|
||||
if !viewModel.isCompact, theme.avatarPosition == .top {
|
||||
reblogView
|
||||
replyView
|
||||
StatusRowReblogView(viewModel: viewModel)
|
||||
StatusRowReplyView(viewModel: viewModel)
|
||||
}
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
let status: AnyStatus = viewModel.status.reblog ?? viewModel.status
|
||||
if !viewModel.isCompact {
|
||||
StatusRowHeaderView(status: status, viewModel: viewModel)
|
||||
}
|
||||
StatusRowContentView(status: status, viewModel: viewModel)
|
||||
.contentShape(Rectangle())
|
||||
.onTapGesture {
|
||||
viewModel.navigateToDetail()
|
||||
}
|
||||
}
|
||||
.accessibilityElement(children: viewModel.isFocused ? .contain : .combine)
|
||||
.accessibilityAction {
|
||||
viewModel.navigateToDetail()
|
||||
}
|
||||
statusView
|
||||
if viewModel.showActions, theme.statusActionsDisplay != .none {
|
||||
StatusActionsView(viewModel: viewModel)
|
||||
StatusRowActionsView(viewModel: viewModel)
|
||||
.padding(.top, 8)
|
||||
.tint(viewModel.isFocused ? theme.tintColor : .gray)
|
||||
.contentShape(Rectangle())
|
||||
|
@ -78,15 +92,14 @@ public struct StatusRowView: View {
|
|||
.contextMenu {
|
||||
contextMenu
|
||||
}
|
||||
|
||||
.swipeActions(edge: .trailing) {
|
||||
if !viewModel.isCompact {
|
||||
trailingSwipeActions
|
||||
StatusRowSwipeView(viewModel: viewModel, mode: .trailing)
|
||||
}
|
||||
}
|
||||
.swipeActions(edge: .leading) {
|
||||
if !viewModel.isCompact {
|
||||
leadingSwipeActions
|
||||
StatusRowSwipeView(viewModel: viewModel, mode: .leading)
|
||||
}
|
||||
}
|
||||
.listRowBackground(viewModel.highlightRowColor)
|
||||
|
@ -166,286 +179,7 @@ public struct StatusRowView: View {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var reblogView: some View {
|
||||
if viewModel.status.reblog != nil {
|
||||
HStack(spacing: 2) {
|
||||
Image(systemName: "arrow.left.arrow.right.circle.fill")
|
||||
AvatarView(url: viewModel.status.account.avatar, size: .boost)
|
||||
EmojiTextApp(.init(stringValue: viewModel.status.account.safeDisplayName), emojis: viewModel.status.account.emojis)
|
||||
Text("status.row.was-boosted")
|
||||
}
|
||||
.accessibilityElement()
|
||||
.accessibilityLabel(
|
||||
Text("\(viewModel.status.account.safeDisplayName)")
|
||||
+ Text(" ")
|
||||
+ Text("status.row.was-boosted")
|
||||
)
|
||||
.font(.scaledFootnote)
|
||||
.foregroundColor(.gray)
|
||||
.fontWeight(.semibold)
|
||||
.onTapGesture {
|
||||
viewModel.navigateToAccountDetail(account: viewModel.status.account)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
var replyView: some View {
|
||||
if let accountId = viewModel.status.inReplyToAccountId,
|
||||
let mention = viewModel.status.mentions.first(where: { $0.id == accountId })
|
||||
{
|
||||
HStack(spacing: 2) {
|
||||
Image(systemName: "arrowshape.turn.up.left.fill")
|
||||
Text("status.row.was-reply")
|
||||
Text(mention.username)
|
||||
}
|
||||
.font(.scaledFootnote)
|
||||
.foregroundColor(.gray)
|
||||
.fontWeight(.semibold)
|
||||
.onTapGesture {
|
||||
viewModel.navigateToMention(mention: mention)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var statusView: some View {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
if let status: AnyStatus = viewModel.status.reblog ?? viewModel.status {
|
||||
if !viewModel.isCompact {
|
||||
HStack(alignment: .center) {
|
||||
Button {
|
||||
viewModel.navigateToAccountDetail(account: status.account)
|
||||
} label: {
|
||||
accountView(status: status)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
Spacer()
|
||||
threadIcon
|
||||
contextMenuButton
|
||||
}
|
||||
.accessibilityElement()
|
||||
.accessibilityLabel(Text("\(status.account.displayName)"))
|
||||
}
|
||||
makeStatusContentView(status: status)
|
||||
.contentShape(Rectangle())
|
||||
.onTapGesture {
|
||||
viewModel.navigateToDetail()
|
||||
}
|
||||
}
|
||||
}
|
||||
.accessibilityElement(children: viewModel.isFocused ? .contain : .combine)
|
||||
.accessibilityAction {
|
||||
viewModel.navigateToDetail()
|
||||
}
|
||||
}
|
||||
|
||||
private func makeStatusContentView(status: AnyStatus) -> some View {
|
||||
Group {
|
||||
if !status.spoilerText.asRawText.isEmpty {
|
||||
HStack(alignment: .top) {
|
||||
Text("⚠︎")
|
||||
.font(.system(.subheadline, weight: .bold))
|
||||
.foregroundColor(.secondary)
|
||||
EmojiTextApp(status.spoilerText, emojis: status.emojis, language: status.language)
|
||||
.font(.system(.subheadline, weight: .bold))
|
||||
.foregroundColor(.secondary)
|
||||
.multilineTextAlignment(.leading)
|
||||
Spacer()
|
||||
Button {
|
||||
withAnimation {
|
||||
viewModel.displaySpoiler.toggle()
|
||||
}
|
||||
} label: {
|
||||
Image(systemName: "chevron.down")
|
||||
.rotationEffect(Angle(degrees: viewModel.displaySpoiler ? 0 : 180))
|
||||
}
|
||||
.buttonStyle(.bordered)
|
||||
.accessibility(label: viewModel.displaySpoiler ? Text("status.show-more") : Text("status.show-less"))
|
||||
.accessibilityHidden(true)
|
||||
}
|
||||
.onTapGesture { // make whole row tapable to make up for smaller button size
|
||||
withAnimation {
|
||||
viewModel.displaySpoiler.toggle()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !viewModel.displaySpoiler {
|
||||
HStack {
|
||||
EmojiTextApp(status.content, emojis: status.emojis, language: status.language)
|
||||
.font(.scaledBody)
|
||||
.environment(\.openURL, OpenURLAction { url in
|
||||
viewModel.routerPath.handleStatus(status: status, url: url)
|
||||
})
|
||||
Spacer()
|
||||
}
|
||||
|
||||
makeTranslateView(status: status)
|
||||
|
||||
if let poll = status.poll {
|
||||
StatusPollView(poll: poll, status: status)
|
||||
}
|
||||
|
||||
embedStatusView
|
||||
|
||||
makeMediasView(status: status)
|
||||
.accessibilityHidden(!viewModel.isFocused)
|
||||
makeCardView(status: status)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private func accountView(status: AnyStatus) -> some View {
|
||||
HStack(alignment: .center) {
|
||||
if theme.avatarPosition == .top {
|
||||
AvatarView(url: status.account.avatar, size: .status)
|
||||
}
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
EmojiTextApp(.init(stringValue: status.account.safeDisplayName), emojis: status.account.emojis)
|
||||
.font(.scaledSubheadline)
|
||||
.fontWeight(.semibold)
|
||||
Group {
|
||||
Text("@\(status.account.acct)") +
|
||||
Text(" ⸱ ") +
|
||||
Text(status.createdAt.relativeFormatted) +
|
||||
Text(" ⸱ ") +
|
||||
Text(Image(systemName: viewModel.status.visibility.iconName))
|
||||
}
|
||||
.font(.scaledFootnote)
|
||||
.foregroundColor(.gray)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var threadIcon: some View {
|
||||
if viewModel.status.reblog?.inReplyToAccountId != nil || viewModel.status.inReplyToAccountId != nil {
|
||||
Image(systemName: "bubble.left.and.bubble.right")
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: 15)
|
||||
.foregroundColor(.gray)
|
||||
}
|
||||
}
|
||||
|
||||
private var contextMenuButton: some View {
|
||||
Menu {
|
||||
contextMenu
|
||||
} label: {
|
||||
Image(systemName: "ellipsis")
|
||||
.frame(width: 20, height: 30)
|
||||
}
|
||||
.menuStyle(.borderlessButton)
|
||||
.foregroundColor(.gray)
|
||||
.contentShape(Rectangle())
|
||||
.accessibilityHidden(true)
|
||||
}
|
||||
|
||||
private func shouldShowTranslateButton(status: AnyStatus) -> Bool {
|
||||
let statusLang = viewModel.getStatusLang()
|
||||
|
||||
if let userLang = preferences.serverPreferences?.postLanguage,
|
||||
preferences.showTranslateButton,
|
||||
!status.content.asRawText.isEmpty,
|
||||
viewModel.translation == nil
|
||||
{
|
||||
return userLang != statusLang
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private func makeTranslateView(status: AnyStatus) -> some View {
|
||||
if let userLang = preferences.serverPreferences?.postLanguage,
|
||||
shouldShowTranslateButton(status: status)
|
||||
{
|
||||
Button {
|
||||
Task {
|
||||
await viewModel.translate(userLang: userLang)
|
||||
}
|
||||
} label: {
|
||||
if viewModel.isLoadingTranslation {
|
||||
ProgressView()
|
||||
} else {
|
||||
if let statusLanguage = viewModel.getStatusLang(),
|
||||
let languageName = Locale.current.localizedString(forLanguageCode: statusLanguage)
|
||||
{
|
||||
Text("status.action.translate-from-\(languageName)")
|
||||
} else {
|
||||
Text("status.action.translate")
|
||||
}
|
||||
}
|
||||
}
|
||||
.buttonStyle(.borderless)
|
||||
}
|
||||
|
||||
if let translation = viewModel.translation, !viewModel.isLoadingTranslation {
|
||||
GroupBox {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text(translation.content.asSafeMarkdownAttributedString)
|
||||
.font(.scaledBody)
|
||||
Text("status.action.translated-label-\(translation.provider)")
|
||||
.font(.footnote)
|
||||
.foregroundColor(.gray)
|
||||
}
|
||||
}
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private func makeMediasView(status: AnyStatus) -> some View {
|
||||
if !status.mediaAttachments.isEmpty {
|
||||
if theme.statusDisplayStyle == .compact {
|
||||
HStack {
|
||||
StatusMediaPreviewView(attachments: status.mediaAttachments,
|
||||
sensitive: status.sensitive,
|
||||
isNotifications: viewModel.isCompact)
|
||||
Spacer()
|
||||
}
|
||||
.padding(.vertical, 4)
|
||||
} else {
|
||||
StatusMediaPreviewView(attachments: status.mediaAttachments,
|
||||
sensitive: status.sensitive,
|
||||
isNotifications: viewModel.isCompact)
|
||||
.padding(.vertical, 4)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private func makeCardView(status: AnyStatus) -> some View {
|
||||
if let card = status.card,
|
||||
!viewModel.isEmbedLoading,
|
||||
!viewModel.isCompact,
|
||||
theme.statusDisplayStyle == .large,
|
||||
status.content.statusesURLs.isEmpty,
|
||||
status.mediaAttachments.isEmpty
|
||||
{
|
||||
StatusCardView(card: card)
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var embedStatusView: some View {
|
||||
if !reasons.contains(.placeholder) {
|
||||
if !viewModel.isCompact, !viewModel.isEmbedLoading,
|
||||
let embed = viewModel.embeddedStatus
|
||||
{
|
||||
StatusEmbeddedView(status: embed, client: viewModel.client, routerPath: viewModel.routerPath)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
} else if viewModel.isEmbedLoading, !viewModel.isCompact {
|
||||
StatusEmbeddedView(status: .placeholder(), client: viewModel.client, routerPath: viewModel.routerPath)
|
||||
.redacted(reason: .placeholder)
|
||||
.shimmering()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private var remoteContentLoadingView: some View {
|
||||
ZStack(alignment: .center) {
|
||||
VStack {
|
||||
|
@ -461,101 +195,4 @@ public struct StatusRowView: View {
|
|||
.background(Color.black.opacity(0.40))
|
||||
.transition(.opacity)
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var trailingSwipeActions: some View {
|
||||
if preferences.swipeActionsStatusTrailingRight != StatusAction.none, !viewModel.isRemote {
|
||||
makeSwipeButton(action: preferences.swipeActionsStatusTrailingRight)
|
||||
.tint(preferences.swipeActionsStatusTrailingRight.color(themeTintColor: theme.tintColor, useThemeColor: preferences.swipeActionsUseThemeColor, outside: true))
|
||||
}
|
||||
if preferences.swipeActionsStatusTrailingLeft != StatusAction.none, !viewModel.isRemote {
|
||||
makeSwipeButton(action: preferences.swipeActionsStatusTrailingLeft)
|
||||
.tint(preferences.swipeActionsStatusTrailingLeft.color(themeTintColor: theme.tintColor, useThemeColor: preferences.swipeActionsUseThemeColor, outside: false))
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var leadingSwipeActions: some View {
|
||||
if preferences.swipeActionsStatusLeadingLeft != StatusAction.none, !viewModel.isRemote {
|
||||
makeSwipeButton(action: preferences.swipeActionsStatusLeadingLeft)
|
||||
.tint(preferences.swipeActionsStatusLeadingLeft.color(themeTintColor: theme.tintColor, useThemeColor: preferences.swipeActionsUseThemeColor, outside: true))
|
||||
}
|
||||
if preferences.swipeActionsStatusLeadingRight != StatusAction.none, !viewModel.isRemote {
|
||||
makeSwipeButton(action: preferences.swipeActionsStatusLeadingRight)
|
||||
.tint(preferences.swipeActionsStatusLeadingRight.color(themeTintColor: theme.tintColor, useThemeColor: preferences.swipeActionsUseThemeColor, outside: false))
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private func makeSwipeButton(action: StatusAction) -> some View {
|
||||
switch action {
|
||||
case .reply:
|
||||
makeSwipeButtonForRouterPath(action: action, destination: .replyToStatusEditor(status: viewModel.status))
|
||||
case .quote:
|
||||
makeSwipeButtonForRouterPath(action: action, destination: .quoteStatusEditor(status: viewModel.status))
|
||||
case .favorite:
|
||||
makeSwipeButtonForTask(action: action) {
|
||||
if viewModel.isFavorited {
|
||||
await viewModel.unFavorite()
|
||||
} else {
|
||||
await viewModel.favorite()
|
||||
}
|
||||
}
|
||||
case .boost:
|
||||
makeSwipeButtonForTask(action: action) {
|
||||
if viewModel.isReblogged {
|
||||
await viewModel.unReblog()
|
||||
} else {
|
||||
await viewModel.reblog()
|
||||
}
|
||||
}
|
||||
case .bookmark:
|
||||
makeSwipeButtonForTask(action: action) {
|
||||
if viewModel.isBookmarked {
|
||||
await viewModel.unbookmark()
|
||||
} else {
|
||||
await
|
||||
viewModel.bookmark()
|
||||
}
|
||||
}
|
||||
case .none:
|
||||
EmptyView()
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private func makeSwipeButtonForRouterPath(action: StatusAction, destination: SheetDestinations) -> some View {
|
||||
Button {
|
||||
HapticManager.shared.fireHaptic(of: .notification(.success))
|
||||
viewModel.routerPath.presentedSheet = destination
|
||||
} label: {
|
||||
makeSwipeLabel(action: action, style: preferences.swipeActionsIconStyle)
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private func makeSwipeButtonForTask(action: StatusAction, task: @escaping () async -> Void) -> some View {
|
||||
Button {
|
||||
Task {
|
||||
HapticManager.shared.fireHaptic(of: .notification(.success))
|
||||
await task()
|
||||
}
|
||||
} label: {
|
||||
makeSwipeLabel(action: action, style: preferences.swipeActionsIconStyle)
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private func makeSwipeLabel(action: StatusAction, style: UserPreferences.SwipeActionsIconStyle) -> some View {
|
||||
switch (style) {
|
||||
case .iconOnly:
|
||||
Label(action.displayName(isReblogged: viewModel.isReblogged, isFavorited: viewModel.isFavorited, isBookmarked: viewModel.isBookmarked), systemImage: action.iconName(isReblogged: viewModel.isReblogged, isFavorited: viewModel.isFavorited, isBookmarked: viewModel.isBookmarked))
|
||||
.labelStyle(.iconOnly)
|
||||
.environment(\.symbolVariants, .none)
|
||||
case .iconWithText:
|
||||
Label(action.displayName(isReblogged: viewModel.isReblogged, isFavorited: viewModel.isFavorited, isBookmarked: viewModel.isBookmarked), systemImage: action.iconName(isReblogged: viewModel.isReblogged, isFavorited: viewModel.isFavorited, isBookmarked: viewModel.isBookmarked))
|
||||
.labelStyle(.titleAndIcon)
|
||||
.environment(\.symbolVariants, .none)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,7 +4,7 @@ import Models
|
|||
import Network
|
||||
import SwiftUI
|
||||
|
||||
struct StatusActionsView: View {
|
||||
struct StatusRowActionsView: View {
|
||||
@EnvironmentObject private var theme: Theme
|
||||
@ObservedObject var viewModel: StatusRowViewModel
|
||||
|
|
@ -4,7 +4,7 @@ import NukeUI
|
|||
import Shimmer
|
||||
import SwiftUI
|
||||
|
||||
public struct StatusCardView: View {
|
||||
public struct StatusRowCardView: View {
|
||||
@EnvironmentObject private var theme: Theme
|
||||
@Environment(\.openURL) private var openURL
|
||||
let card: Card
|
|
@ -0,0 +1,59 @@
|
|||
import SwiftUI
|
||||
import DesignSystem
|
||||
import Models
|
||||
import Env
|
||||
|
||||
struct StatusRowContentView: View {
|
||||
@Environment(\.redactionReasons) private var reasons
|
||||
@EnvironmentObject private var theme: Theme
|
||||
|
||||
let status: AnyStatus
|
||||
@ObservedObject var viewModel: StatusRowViewModel
|
||||
|
||||
var body: some View {
|
||||
if !status.spoilerText.asRawText.isEmpty {
|
||||
StatusRowSpoilerView(status: status, displaySpoiler: $viewModel.displaySpoiler)
|
||||
}
|
||||
|
||||
if !viewModel.displaySpoiler {
|
||||
StatusRowTextView(status: status, viewModel: viewModel)
|
||||
StatusRowTranslateView(status: status, viewModel: viewModel)
|
||||
if let poll = status.poll {
|
||||
StatusPollView(poll: poll, status: status)
|
||||
}
|
||||
|
||||
if !reasons.contains(.placeholder),
|
||||
!viewModel.isCompact,
|
||||
(viewModel.isEmbedLoading || viewModel.embeddedStatus != nil) {
|
||||
StatusEmbeddedView(status: viewModel.embeddedStatus ?? Status.placeholder(),
|
||||
client: viewModel.client,
|
||||
routerPath: viewModel.routerPath)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
.redacted(reason: viewModel.isEmbedLoading ? .placeholder : [])
|
||||
.shimmering(active: viewModel.isEmbedLoading)
|
||||
}
|
||||
|
||||
if !status.mediaAttachments.isEmpty {
|
||||
HStack {
|
||||
StatusRowMediaPreviewView(attachments: status.mediaAttachments,
|
||||
sensitive: status.sensitive,
|
||||
isNotifications: viewModel.isCompact)
|
||||
if theme.statusDisplayStyle == .compact {
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
.padding(.vertical, 4)
|
||||
}
|
||||
|
||||
if let card = status.card,
|
||||
!viewModel.isEmbedLoading,
|
||||
!viewModel.isCompact,
|
||||
theme.statusDisplayStyle == .large,
|
||||
status.content.statusesURLs.isEmpty,
|
||||
status.mediaAttachments.isEmpty
|
||||
{
|
||||
StatusRowCardView(card: card)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,73 @@
|
|||
import SwiftUI
|
||||
import DesignSystem
|
||||
import Models
|
||||
|
||||
struct StatusRowHeaderView: View {
|
||||
@EnvironmentObject private var theme: Theme
|
||||
|
||||
let status: AnyStatus
|
||||
let viewModel: StatusRowViewModel
|
||||
|
||||
var body: some View {
|
||||
HStack(alignment: .center) {
|
||||
Button {
|
||||
viewModel.navigateToAccountDetail(account: status.account)
|
||||
} label: {
|
||||
accountView(status: status)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
Spacer()
|
||||
threadIcon
|
||||
contextMenuButton
|
||||
}
|
||||
.accessibilityElement()
|
||||
.accessibilityLabel(Text("\(status.account.displayName)"))
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private func accountView(status: AnyStatus) -> some View {
|
||||
HStack(alignment: .center) {
|
||||
if theme.avatarPosition == .top {
|
||||
AvatarView(url: status.account.avatar, size: .status)
|
||||
}
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
EmojiTextApp(.init(stringValue: status.account.safeDisplayName), emojis: status.account.emojis)
|
||||
.font(.scaledSubheadline)
|
||||
.fontWeight(.semibold)
|
||||
Group {
|
||||
Text("@\(status.account.acct)") +
|
||||
Text(" ⸱ ") +
|
||||
Text(status.createdAt.relativeFormatted) +
|
||||
Text(" ⸱ ") +
|
||||
Text(Image(systemName: viewModel.status.visibility.iconName))
|
||||
}
|
||||
.font(.scaledFootnote)
|
||||
.foregroundColor(.gray)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var threadIcon: some View {
|
||||
if viewModel.status.reblog?.inReplyToAccountId != nil || viewModel.status.inReplyToAccountId != nil {
|
||||
Image(systemName: "bubble.left.and.bubble.right")
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: 15)
|
||||
.foregroundColor(.gray)
|
||||
}
|
||||
}
|
||||
|
||||
private var contextMenuButton: some View {
|
||||
Menu {
|
||||
StatusRowContextMenu(viewModel: viewModel)
|
||||
} label: {
|
||||
Image(systemName: "ellipsis")
|
||||
.frame(width: 20, height: 30)
|
||||
}
|
||||
.menuStyle(.borderlessButton)
|
||||
.foregroundColor(.gray)
|
||||
.contentShape(Rectangle())
|
||||
.accessibilityHidden(true)
|
||||
}
|
||||
}
|
|
@ -5,7 +5,7 @@ import Nuke
|
|||
import NukeUI
|
||||
import SwiftUI
|
||||
|
||||
public struct StatusMediaPreviewView: View {
|
||||
public struct StatusRowMediaPreviewView: View {
|
||||
@Environment(\.openURL) private var openURL
|
||||
@Environment(\.isSecondaryColumn) private var isSecondaryColumn
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
import SwiftUI
|
||||
import DesignSystem
|
||||
|
||||
struct StatusRowReblogView: View {
|
||||
let viewModel: StatusRowViewModel
|
||||
|
||||
var body: some View {
|
||||
if viewModel.status.reblog != nil {
|
||||
HStack(spacing: 2) {
|
||||
Image(systemName: "arrow.left.arrow.right.circle.fill")
|
||||
AvatarView(url: viewModel.status.account.avatar, size: .boost)
|
||||
EmojiTextApp(.init(stringValue: viewModel.status.account.safeDisplayName), emojis: viewModel.status.account.emojis)
|
||||
Text("status.row.was-boosted")
|
||||
}
|
||||
.accessibilityElement()
|
||||
.accessibilityLabel(
|
||||
Text("\(viewModel.status.account.safeDisplayName)")
|
||||
+ Text(" ")
|
||||
+ Text("status.row.was-boosted")
|
||||
)
|
||||
.font(.scaledFootnote)
|
||||
.foregroundColor(.gray)
|
||||
.fontWeight(.semibold)
|
||||
.onTapGesture {
|
||||
viewModel.navigateToAccountDetail(account: viewModel.status.account)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
import SwiftUI
|
||||
import DesignSystem
|
||||
|
||||
struct StatusRowReplyView: View {
|
||||
let viewModel: StatusRowViewModel
|
||||
|
||||
var body: some View {
|
||||
if let accountId = viewModel.status.inReplyToAccountId,
|
||||
let mention = viewModel.status.mentions.first(where: { $0.id == accountId })
|
||||
{
|
||||
HStack(spacing: 2) {
|
||||
Image(systemName: "arrowshape.turn.up.left.fill")
|
||||
Text("status.row.was-reply")
|
||||
Text(mention.username)
|
||||
}
|
||||
.font(.scaledFootnote)
|
||||
.foregroundColor(.gray)
|
||||
.fontWeight(.semibold)
|
||||
.onTapGesture {
|
||||
viewModel.navigateToMention(mention: mention)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
import SwiftUI
|
||||
import DesignSystem
|
||||
import Models
|
||||
|
||||
struct StatusRowSpoilerView: View {
|
||||
let status: AnyStatus
|
||||
@Binding var displaySpoiler: Bool
|
||||
|
||||
var body: some View {
|
||||
HStack(alignment: .top) {
|
||||
Text("⚠︎")
|
||||
.font(.system(.subheadline, weight: .bold))
|
||||
.foregroundColor(.secondary)
|
||||
EmojiTextApp(status.spoilerText, emojis: status.emojis, language: status.language)
|
||||
.font(.system(.subheadline, weight: .bold))
|
||||
.foregroundColor(.secondary)
|
||||
.multilineTextAlignment(.leading)
|
||||
Spacer()
|
||||
Button {
|
||||
withAnimation {
|
||||
displaySpoiler.toggle()
|
||||
}
|
||||
} label: {
|
||||
Image(systemName: "chevron.down")
|
||||
.rotationEffect(Angle(degrees: displaySpoiler ? 0 : 180))
|
||||
}
|
||||
.buttonStyle(.bordered)
|
||||
.accessibility(label: displaySpoiler ? Text("status.show-more") : Text("status.show-less"))
|
||||
.accessibilityHidden(true)
|
||||
}
|
||||
.onTapGesture { // make whole row tapable to make up for smaller button size
|
||||
withAnimation {
|
||||
displaySpoiler.toggle()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,122 @@
|
|||
import SwiftUI
|
||||
import Env
|
||||
import Models
|
||||
import DesignSystem
|
||||
|
||||
struct StatusRowSwipeView: View {
|
||||
@EnvironmentObject private var theme: Theme
|
||||
@EnvironmentObject private var preferences: UserPreferences
|
||||
|
||||
enum Mode {
|
||||
case leading, trailing
|
||||
}
|
||||
|
||||
let viewModel: StatusRowViewModel
|
||||
let mode: Mode
|
||||
|
||||
var body: some View {
|
||||
switch mode {
|
||||
case .leading:
|
||||
leadingSwipeActions
|
||||
case .trailing:
|
||||
trailingSwipeActions
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var trailingSwipeActions: some View {
|
||||
if preferences.swipeActionsStatusTrailingRight != StatusAction.none, !viewModel.isRemote {
|
||||
makeSwipeButton(action: preferences.swipeActionsStatusTrailingRight)
|
||||
.tint(preferences.swipeActionsStatusTrailingRight.color(themeTintColor: theme.tintColor, useThemeColor: preferences.swipeActionsUseThemeColor, outside: true))
|
||||
}
|
||||
if preferences.swipeActionsStatusTrailingLeft != StatusAction.none, !viewModel.isRemote {
|
||||
makeSwipeButton(action: preferences.swipeActionsStatusTrailingLeft)
|
||||
.tint(preferences.swipeActionsStatusTrailingLeft.color(themeTintColor: theme.tintColor, useThemeColor: preferences.swipeActionsUseThemeColor, outside: false))
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var leadingSwipeActions: some View {
|
||||
if preferences.swipeActionsStatusLeadingLeft != StatusAction.none, !viewModel.isRemote {
|
||||
makeSwipeButton(action: preferences.swipeActionsStatusLeadingLeft)
|
||||
.tint(preferences.swipeActionsStatusLeadingLeft.color(themeTintColor: theme.tintColor, useThemeColor: preferences.swipeActionsUseThemeColor, outside: true))
|
||||
}
|
||||
if preferences.swipeActionsStatusLeadingRight != StatusAction.none, !viewModel.isRemote {
|
||||
makeSwipeButton(action: preferences.swipeActionsStatusLeadingRight)
|
||||
.tint(preferences.swipeActionsStatusLeadingRight.color(themeTintColor: theme.tintColor, useThemeColor: preferences.swipeActionsUseThemeColor, outside: false))
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private func makeSwipeButton(action: StatusAction) -> some View {
|
||||
switch action {
|
||||
case .reply:
|
||||
makeSwipeButtonForRouterPath(action: action, destination: .replyToStatusEditor(status: viewModel.status))
|
||||
case .quote:
|
||||
makeSwipeButtonForRouterPath(action: action, destination: .quoteStatusEditor(status: viewModel.status))
|
||||
case .favorite:
|
||||
makeSwipeButtonForTask(action: action) {
|
||||
if viewModel.isFavorited {
|
||||
await viewModel.unFavorite()
|
||||
} else {
|
||||
await viewModel.favorite()
|
||||
}
|
||||
}
|
||||
case .boost:
|
||||
makeSwipeButtonForTask(action: action) {
|
||||
if viewModel.isReblogged {
|
||||
await viewModel.unReblog()
|
||||
} else {
|
||||
await viewModel.reblog()
|
||||
}
|
||||
}
|
||||
case .bookmark:
|
||||
makeSwipeButtonForTask(action: action) {
|
||||
if viewModel.isBookmarked {
|
||||
await viewModel.unbookmark()
|
||||
} else {
|
||||
await
|
||||
viewModel.bookmark()
|
||||
}
|
||||
}
|
||||
case .none:
|
||||
EmptyView()
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private func makeSwipeButtonForRouterPath(action: StatusAction, destination: SheetDestinations) -> some View {
|
||||
Button {
|
||||
HapticManager.shared.fireHaptic(of: .notification(.success))
|
||||
viewModel.routerPath.presentedSheet = destination
|
||||
} label: {
|
||||
makeSwipeLabel(action: action, style: preferences.swipeActionsIconStyle)
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private func makeSwipeButtonForTask(action: StatusAction, task: @escaping () async -> Void) -> some View {
|
||||
Button {
|
||||
Task {
|
||||
HapticManager.shared.fireHaptic(of: .notification(.success))
|
||||
await task()
|
||||
}
|
||||
} label: {
|
||||
makeSwipeLabel(action: action, style: preferences.swipeActionsIconStyle)
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private func makeSwipeLabel(action: StatusAction, style: UserPreferences.SwipeActionsIconStyle) -> some View {
|
||||
switch (style) {
|
||||
case .iconOnly:
|
||||
Label(action.displayName(isReblogged: viewModel.isReblogged, isFavorited: viewModel.isFavorited, isBookmarked: viewModel.isBookmarked), systemImage: action.iconName(isReblogged: viewModel.isReblogged, isFavorited: viewModel.isFavorited, isBookmarked: viewModel.isBookmarked))
|
||||
.labelStyle(.iconOnly)
|
||||
.environment(\.symbolVariants, .none)
|
||||
case .iconWithText:
|
||||
Label(action.displayName(isReblogged: viewModel.isReblogged, isFavorited: viewModel.isFavorited, isBookmarked: viewModel.isBookmarked), systemImage: action.iconName(isReblogged: viewModel.isReblogged, isFavorited: viewModel.isFavorited, isBookmarked: viewModel.isBookmarked))
|
||||
.labelStyle(.titleAndIcon)
|
||||
.environment(\.symbolVariants, .none)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
import SwiftUI
|
||||
import DesignSystem
|
||||
import Models
|
||||
|
||||
struct StatusRowTextView: View {
|
||||
let status: AnyStatus
|
||||
let viewModel: StatusRowViewModel
|
||||
|
||||
var body: some View {
|
||||
HStack {
|
||||
EmojiTextApp(status.content, emojis: status.emojis, language: status.language)
|
||||
.font(.scaledBody)
|
||||
.environment(\.openURL, OpenURLAction { url in
|
||||
viewModel.routerPath.handleStatus(status: status, url: url)
|
||||
})
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,63 @@
|
|||
import SwiftUI
|
||||
import Models
|
||||
import DesignSystem
|
||||
import Env
|
||||
|
||||
struct StatusRowTranslateView: View {
|
||||
@EnvironmentObject private var preferences: UserPreferences
|
||||
|
||||
let status: AnyStatus
|
||||
@ObservedObject var viewModel: StatusRowViewModel
|
||||
|
||||
private var shouldShowTranslateButton: Bool {
|
||||
let statusLang = viewModel.getStatusLang()
|
||||
|
||||
if let userLang = preferences.serverPreferences?.postLanguage,
|
||||
preferences.showTranslateButton,
|
||||
!status.content.asRawText.isEmpty,
|
||||
viewModel.translation == nil
|
||||
{
|
||||
return userLang != statusLang
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
if let userLang = preferences.serverPreferences?.postLanguage,
|
||||
shouldShowTranslateButton
|
||||
{
|
||||
Button {
|
||||
Task {
|
||||
await viewModel.translate(userLang: userLang)
|
||||
}
|
||||
} label: {
|
||||
if viewModel.isLoadingTranslation {
|
||||
ProgressView()
|
||||
} else {
|
||||
if let statusLanguage = viewModel.getStatusLang(),
|
||||
let languageName = Locale.current.localizedString(forLanguageCode: statusLanguage)
|
||||
{
|
||||
Text("status.action.translate-from-\(languageName)")
|
||||
} else {
|
||||
Text("status.action.translate")
|
||||
}
|
||||
}
|
||||
}
|
||||
.buttonStyle(.borderless)
|
||||
}
|
||||
|
||||
if let translation = viewModel.translation, !viewModel.isLoadingTranslation {
|
||||
GroupBox {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text(translation.content.asSafeMarkdownAttributedString)
|
||||
.font(.scaledBody)
|
||||
Text("status.action.translated-label-\(translation.provider)")
|
||||
.font(.footnote)
|
||||
.foregroundColor(.gray)
|
||||
}
|
||||
}
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue