diff --git a/Localizations/en.lproj/Localizable.strings b/Localizations/en.lproj/Localizable.strings index f7f6054..ca5f7b0 100644 --- a/Localizations/en.lproj/Localizable.strings +++ b/Localizations/en.lproj/Localizable.strings @@ -207,6 +207,7 @@ "preferences.app.color-scheme.system" = "System"; "preferences.blocked-domains" = "Blocked Domains"; "preferences.blocked-users" = "Blocked Users"; +"preferences.edge-to-edge-view" = "Edge to edge view"; "preferences.media" = "Media"; "preferences.media.avatars" = "Avatars"; "preferences.media.avatars.animate" = "Animate avatars"; @@ -235,6 +236,7 @@ "preferences.notification-types" = "Notification Types"; "preferences.notification-types.follow" = "Follow"; "preferences.notification-types.favourite" = "Favorite"; +"preferences.notification-types.like" = "Like"; "preferences.notification-types.follow-request" = "Follow Request"; "preferences.notification-types.reblog" = "Reblog"; "preferences.notification-types.mention" = "Mention"; @@ -253,6 +255,7 @@ "preferences.require-double-tap-to-favorite" = "Require double tap to favorite"; "preferences.show-reblog-and-favorite-counts" = "Show boost and favorite counts"; "preferences.status-word" = "Status word"; +"preferences.favorite-word" = "Favorite word"; "filters.active" = "Active"; "filters.expired" = "Expired"; "filter.add-new" = "Add New Filter"; @@ -279,6 +282,7 @@ "notifications" = "Notifications"; "notifications.reblogged-your-status-%@" = "%@ boosted your status"; "notifications.favourited-your-status-%@" = "%@ favorited your status"; +"notifications.liked-your-status-%@" = "%@ liked your status"; "notifications.followed-you-%@" = "%@ followed you"; "notifications.poll-ended" = "A poll you have voted in has ended"; "notifications.your-poll-ended" = "Your poll has ended"; diff --git a/ServiceLayer/Sources/ServiceLayer/Utilities/AppPreferences.swift b/ServiceLayer/Sources/ServiceLayer/Utilities/AppPreferences.swift index 828fa03..96faeb8 100644 --- a/ServiceLayer/Sources/ServiceLayer/Utilities/AppPreferences.swift +++ b/ServiceLayer/Sources/ServiceLayer/Utilities/AppPreferences.swift @@ -32,6 +32,14 @@ public extension AppPreferences { public var id: String { rawValue } } + enum FavoriteWord: String, CaseIterable, Identifiable { + case favorite + case favourite + case like + + public var id: String { rawValue } + } + enum AnimateAvatars: String, CaseIterable, Identifiable { case everywhere case profiles @@ -79,6 +87,18 @@ public extension AppPreferences { set { self[.statusWord] = newValue.rawValue } } + var favoriteWord: FavoriteWord { + get { + if let rawValue = self[.favoriteWord] as String?, + let value = FavoriteWord(rawValue: rawValue) { + return value + } + + return .favorite + } + set { self[.favoriteWord] = newValue.rawValue } + } + var animateAvatars: AnimateAvatars { get { if let rawValue = self[.animateAvatars] as String?, @@ -171,6 +191,11 @@ public extension AppPreferences { get { self[.showReblogAndFavoriteCounts] ?? false } set { self[.showReblogAndFavoriteCounts] = newValue } } + + var edgeToEdgeView: Bool { + get { self[.edgeToEdgeView] ?? false } + set { self[.edgeToEdgeView] = newValue } + } var requireDoubleTapToReblog: Bool { get { self[.requireDoubleTapToReblog] ?? false } @@ -207,6 +232,7 @@ private extension AppPreferences { enum Item: String { case colorScheme case statusWord + case favoriteWord case requireDoubleTapToReblog case requireDoubleTapToFavorite case animateAvatars @@ -223,6 +249,7 @@ private extension AppPreferences { case notificationSounds case openLinksInDefaultBrowser case useUniversalLinks + case edgeToEdgeView } subscript(index: Item) -> T? { diff --git a/Views/SwiftUI/PreferencesView.swift b/Views/SwiftUI/PreferencesView.swift index 296e58d..ac1ba5e 100644 --- a/Views/SwiftUI/PreferencesView.swift +++ b/Views/SwiftUI/PreferencesView.swift @@ -101,6 +101,8 @@ struct PreferencesView: View { Text(option.localizedStringKey).tag(option) } } + Toggle("preferences.edge-to-edge-view", + isOn: $identityContext.appPreferences.edgeToEdgeView) Toggle("preferences.show-reblog-and-favorite-counts", isOn: $identityContext.appPreferences.showReblogAndFavoriteCounts) Toggle("preferences.require-double-tap-to-reblog", diff --git a/Views/UIKit/AttachmentsView.swift b/Views/UIKit/AttachmentsView.swift index 2c9e50d..0383d91 100644 --- a/Views/UIKit/AttachmentsView.swift +++ b/Views/UIKit/AttachmentsView.swift @@ -15,6 +15,7 @@ final class AttachmentsView: UIView { private let hideButton = UIButton() private var aspectRatioConstraint: NSLayoutConstraint? private var cancellables = Set() + var identityContext: IdentityContext? var viewModel: AttachmentsRenderingViewModel? { didSet { @@ -104,7 +105,7 @@ final class AttachmentsView: UIView { initialSetup() } - + @available(*, unavailable) required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") @@ -126,6 +127,11 @@ extension AttachmentsView { return height } + + func displayEdgeToEdge(_ edgeToEdge: Bool) { + layer.cornerRadius = edgeToEdge ? 0 : .defaultCornerRadius + } + var shouldAutoplay: Bool { guard !isHidden, let viewModel = viewModel, viewModel.shouldShowAttachments else { return false } @@ -148,10 +154,11 @@ extension AttachmentsView { private extension AttachmentsView { // swiftlint:disable:next function_body_length func initialSetup() { + let isEdgeToEdge = identityContext?.appPreferences.edgeToEdgeView ?? false backgroundColor = .clear layoutMargins = .zero clipsToBounds = true - layer.cornerRadius = .defaultCornerRadius + layer.cornerRadius = isEdgeToEdge ? 0 : .defaultCornerRadius addSubview(containerStackView) containerStackView.translatesAutoresizingMaskIntoConstraints = false containerStackView.distribution = .fillEqually diff --git a/Views/UIKit/CompositionView.swift b/Views/UIKit/CompositionView.swift index 5c0cda0..fcac212 100644 --- a/Views/UIKit/CompositionView.swift +++ b/Views/UIKit/CompositionView.swift @@ -88,7 +88,7 @@ private extension CompositionView { parentViewModel: parentViewModel, autocompleteQueryPublisher: viewModel.$contentWarningAutocompleteQuery.eraseToAnyPublisher()) - stackView.addArrangedSubview(spoilerTextField) + stackView.addSubview(spoilerTextField) spoilerTextField.borderStyle = .roundedRect spoilerTextField.adjustsFontForContentSizeCategory = true spoilerTextField.font = .preferredFont(forTextStyle: .body) @@ -106,7 +106,7 @@ private extension CompositionView { parentViewModel: parentViewModel, autocompleteQueryPublisher: viewModel.$autocompleteQuery.eraseToAnyPublisher()) - stackView.addArrangedSubview(textView) + addSubview(textView) textView.keyboardType = .twitter textView.isScrollEnabled = false textView.adjustsFontForContentSizeCategory = true @@ -125,6 +125,7 @@ private extension CompositionView { textViewPlaceholder.textColor = .secondaryLabel textViewPlaceholder.text = NSLocalizedString("compose.prompt", comment: "") + attachmentsView.displayEdgeToEdge(parentViewModel.identityContext.appPreferences.edgeToEdgeView) stackView.addArrangedSubview(attachmentsView) attachmentsView.isHidden_stackViewSafe = true stackView.addArrangedSubview(attachmentUploadsStackView) @@ -282,8 +283,8 @@ private extension CompositionView { changeIdentityButton.bottomAnchor.constraint(equalTo: avatarImageView.bottomAnchor), changeIdentityButton.trailingAnchor.constraint(equalTo: avatarImageView.trailingAnchor), stackView.leadingAnchor.constraint(equalTo: avatarImageView.trailingAnchor, constant: .defaultSpacing), - stackView.topAnchor.constraint(greaterThanOrEqualTo: guide.topAnchor), - stackView.bottomAnchor.constraint(lessThanOrEqualTo: guide.bottomAnchor), +// stackView.topAnchor.constraint(greaterThanOrEqualTo: guide.topAnchor), +// stackView.bottomAnchor.constraint(lessThanOrEqualTo: guide.bottomAnchor), textViewPlaceholder.leadingAnchor.constraint(equalTo: textView.leadingAnchor), textViewPlaceholder.topAnchor.constraint(equalTo: textView.topAnchor), textViewPlaceholder.trailingAnchor.constraint(equalTo: textView.trailingAnchor), diff --git a/Views/UIKit/Content Views/StatusView.swift b/Views/UIKit/Content Views/StatusView.swift index 89a103e..ae99e78 100644 --- a/Views/UIKit/Content Views/StatusView.swift +++ b/Views/UIKit/Content Views/StatusView.swift @@ -18,6 +18,7 @@ final class StatusView: UIView { let accountLabel = UILabel() let nameButton = UIButton() let timeLabel = UILabel() + let timeSeparatorLabel = UILabel() let bodyView = StatusBodyView() let showThreadIndicator = UIButton(type: .system) let contextParentTimeLabel = UILabel() @@ -39,6 +40,7 @@ final class StatusView: UIView { private let avatarContainerView = UIView() private let nameAccountContainerStackView = UIStackView() private let nameAccountTimeStackView = UIStackView() + private let nameTimeStackView = UIStackView() private let contextParentTimeApplicationStackView = UIStackView() private let timeVisibilityDividerLabel = UILabel() private let visibilityApplicationDividerLabel = UILabel() @@ -211,14 +213,21 @@ private extension StatusView { displayNameLabel.setContentHuggingPriority(.required, for: .horizontal) displayNameLabel.setContentHuggingPriority(.required, for: .vertical) displayNameLabel.setContentCompressionResistancePriority(.required, for: .horizontal) - nameAccountTimeStackView.addArrangedSubview(displayNameLabel) +// nameTimeStackView.addArrangedSubview(displayNameLabel) accountLabel.font = .preferredFont(forTextStyle: .subheadline) accountLabel.adjustsFontForContentSizeCategory = true accountLabel.textColor = .secondaryLabel accountLabel.setContentHuggingPriority(.required, for: .horizontal) accountLabel.setContentHuggingPriority(.required, for: .vertical) - nameAccountTimeStackView.addArrangedSubview(accountLabel) + + timeSeparatorLabel.text = "∙ " + timeSeparatorLabel.font = .preferredFont(forTextStyle: .subheadline) + timeSeparatorLabel.adjustsFontForContentSizeCategory = true + timeSeparatorLabel.textColor = .secondaryLabel + timeSeparatorLabel.textAlignment = .right + timeSeparatorLabel.setContentCompressionResistancePriority(.required, for: .horizontal) + timeSeparatorLabel.setContentHuggingPriority(.required, for: .vertical) timeLabel.font = .preferredFont(forTextStyle: .subheadline) timeLabel.adjustsFontForContentSizeCategory = true @@ -226,10 +235,14 @@ private extension StatusView { timeLabel.textAlignment = .right timeLabel.setContentCompressionResistancePriority(.required, for: .horizontal) timeLabel.setContentHuggingPriority(.required, for: .vertical) - nameAccountTimeStackView.addArrangedSubview(timeLabel) + nameTimeStackView.addArrangedSubview(displayNameLabel) + nameTimeStackView.addArrangedSubview(timeSeparatorLabel) + nameTimeStackView.addArrangedSubview(timeLabel) + nameAccountTimeStackView.addArrangedSubview(nameTimeStackView) nameAccountContainerStackView.spacing = .defaultSpacing nameAccountContainerStackView.addArrangedSubview(nameAccountTimeStackView) + nameAccountTimeStackView.addArrangedSubview(accountLabel) mainStackView.addArrangedSubview(nameAccountContainerStackView) nameButton.translatesAutoresizingMaskIntoConstraints = false @@ -475,7 +488,10 @@ private extension StatusView { rebloggerAvatarImageView.isHidden = !viewModel.isReblog rebloggerAvatarImageView.sd_setImage(with: viewModel.isReblog ? viewModel.rebloggerAvatarURL : nil) - if isContextParent, avatarContainerView.superview !== nameAccountContainerStackView { + if viewModel.identityContext.appPreferences.edgeToEdgeView { + nameAccountContainerStackView.insertArrangedSubview(avatarContainerView, at: 0) + sideStackView.isHidden = true + } else if isContextParent, avatarContainerView.superview !== nameAccountContainerStackView { nameAccountContainerStackView.insertArrangedSubview(avatarContainerView, at: 0) } else if avatarContainerView.superview !== sideStackView { sideStackView.insertArrangedSubview(avatarContainerView, at: 1) @@ -553,9 +569,9 @@ private extension StatusView { nameButtonAccessibilityAttributedLabel.appendWithSeparator(viewModel.accountName) nameButton.accessibilityAttributedLabel = nameButtonAccessibilityAttributedLabel - nameAccountTimeStackView.axis = isContextParent ? .vertical : .horizontal - nameAccountTimeStackView.alignment = isContextParent ? .leading : .fill - nameAccountTimeStackView.spacing = isContextParent ? 0 : .compactSpacing + nameAccountTimeStackView.axis = .vertical + nameAccountTimeStackView.alignment = .leading + nameAccountTimeStackView.spacing = 0 contextParentTopNameAccountSpacingView.removeFromSuperview() contextParentBottomNameAccountSpacingView.removeFromSuperview() @@ -571,6 +587,8 @@ private extension StatusView { timeLabel.accessibilityLabel = viewModel.accessibilityTime timeLabel.isHidden = isContextParent + timeSeparatorLabel.isHidden = isContextParent + bodyView.viewModel = viewModel showThreadIndicator.isHidden = !viewModel.configuration.isReplyOutOfContext diff --git a/Views/UIKit/StatusBodyView.swift b/Views/UIKit/StatusBodyView.swift index 6573c71..30aea27 100644 --- a/Views/UIKit/StatusBodyView.swift +++ b/Views/UIKit/StatusBodyView.swift @@ -51,6 +51,7 @@ final class StatusBodyView: UIView { contentTextView.isHidden = !viewModel.shouldShowContent + attachmentsView.displayEdgeToEdge(viewModel.identityContext.appPreferences.edgeToEdgeView) attachmentsView.isHidden = viewModel.attachmentViewModels.isEmpty attachmentsView.viewModel = viewModel diff --git a/Views/ViewConstants.swift b/Views/ViewConstants.swift index cdc8c20..15ad34f 100644 --- a/Views/ViewConstants.swift +++ b/Views/ViewConstants.swift @@ -15,6 +15,7 @@ extension CGFloat { static let defaultShadowRadius: Self = 2 static let systemMenuWidth: Self = 250 static let systemMenuInset: Self = 15 + static let tabBarEdgeInsetSize: Self = 64 } extension Float {