diff --git a/CoreData/ApplicationSettings+CoreDataProperties.swift b/CoreData/ApplicationSettings+CoreDataProperties.swift
index 4263b4a..8cb4d3e 100644
--- a/CoreData/ApplicationSettings+CoreDataProperties.swift
+++ b/CoreData/ApplicationSettings+CoreDataProperties.swift
@@ -29,6 +29,7 @@ extension ApplicationSettings {
@NSManaged public var showSensitive: Bool
@NSManaged public var showPhotoDescription: Bool
@NSManaged public var menuPosition: Int32
+ @NSManaged public var showAvatarsOnTimeline: Bool
}
extension ApplicationSettings: Identifiable {
diff --git a/CoreData/ApplicationSettingsHandler.swift b/CoreData/ApplicationSettingsHandler.swift
index 8ebe66e..0143946 100644
--- a/CoreData/ApplicationSettingsHandler.swift
+++ b/CoreData/ApplicationSettingsHandler.swift
@@ -144,6 +144,12 @@ class ApplicationSettingsHandler {
CoreDataHandler.shared.save()
}
+ func set(showAvatarsOnTimeline: Bool) {
+ let defaultSettings = self.get()
+ defaultSettings.showAvatarsOnTimeline = showAvatarsOnTimeline
+ CoreDataHandler.shared.save()
+ }
+
private func createApplicationSettingsEntity(viewContext: NSManagedObjectContext? = nil) -> ApplicationSettings {
let context = viewContext ?? CoreDataHandler.shared.container.viewContext
return ApplicationSettings(context: context)
diff --git a/CoreData/Vernissage.xcdatamodeld/.xccurrentversion b/CoreData/Vernissage.xcdatamodeld/.xccurrentversion
index caec0e4..7d4618c 100644
--- a/CoreData/Vernissage.xcdatamodeld/.xccurrentversion
+++ b/CoreData/Vernissage.xcdatamodeld/.xccurrentversion
@@ -3,6 +3,6 @@
_XCCurrentVersionName
- Vernissage-007.xcdatamodel
+ Vernissage-008.xcdatamodel
diff --git a/CoreData/Vernissage.xcdatamodeld/Vernissage-008.xcdatamodel/contents b/CoreData/Vernissage.xcdatamodeld/Vernissage-008.xcdatamodel/contents
new file mode 100644
index 0000000..c0023cb
--- /dev/null
+++ b/CoreData/Vernissage.xcdatamodeld/Vernissage-008.xcdatamodel/contents
@@ -0,0 +1,95 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/EnvironmentKit/Sources/EnvironmentKit/ApplicationState.swift b/EnvironmentKit/Sources/EnvironmentKit/ApplicationState.swift
index bc7ade4..e81fd10 100644
--- a/EnvironmentKit/Sources/EnvironmentKit/ApplicationState.swift
+++ b/EnvironmentKit/Sources/EnvironmentKit/ApplicationState.swift
@@ -89,6 +89,9 @@ public class ApplicationState: ObservableObject {
/// Information which menu should be shown (top or bottom).
@Published public var menuPosition = MenuPosition.top
+ /// Should avatars be visible on timelines.
+ @Published public var showAvatarsOnTimeline = false
+
public func changeApplicationState(accountModel: AccountModel, instance: Instance?, lastSeenStatusId: String?) {
self.account = accountModel
self.lastSeenStatusId = lastSeenStatusId
diff --git a/Localization/en.lproj/Localizable.strings b/Localization/en.lproj/Localizable.strings
index 9181c01..a32c92b 100644
--- a/Localization/en.lproj/Localizable.strings
+++ b/Localization/en.lproj/Localizable.strings
@@ -211,6 +211,7 @@
"settings.title.topMenu" = "Navigation bar";
"settings.title.bottomRightMenu" = "Bottom right";
"settings.title.bottomLeftMenu" = "Bottom left";
+"settings.title.showAvatarsOnTimeline" = "Show avatars on timelines";
// Mark: Signin view.
"signin.navigationBar.title" = "Sign in to Pixelfed";
diff --git a/Localization/eu.lproj/Localizable.strings b/Localization/eu.lproj/Localizable.strings
index ea9eb8c..e64bd42 100644
--- a/Localization/eu.lproj/Localizable.strings
+++ b/Localization/eu.lproj/Localizable.strings
@@ -211,6 +211,7 @@
"settings.title.topMenu" = "Nabigazio barra";
"settings.title.bottomRightMenu" = "Behe eskumaldean";
"settings.title.bottomLeftMenu" = "Behe ezkerraldean";
+"settings.title.showAvatarsOnTimeline" = "Show avatars on timelines";
// Mark: Signin view.
"signin.navigationBar.title" = "Hasi saioa Pixelfed-en";
diff --git a/Localization/pl.lproj/Localizable.strings b/Localization/pl.lproj/Localizable.strings
index 5814d1a..112865e 100644
--- a/Localization/pl.lproj/Localizable.strings
+++ b/Localization/pl.lproj/Localizable.strings
@@ -211,6 +211,7 @@
"settings.title.topMenu" = "Panel tytułowy";
"settings.title.bottomRightMenu" = "Dolny prawy";
"settings.title.bottomLeftMenu" = "Dolny lewy";
+"settings.title.showAvatarsOnTimeline" = "Wyświetlaj awatary na osiach zdjęć";
// Mark: Signin view.
"signin.navigationBar.title" = "Zaloguj się do Pixelfed";
diff --git a/Vernissage.xcodeproj/project.pbxproj b/Vernissage.xcodeproj/project.pbxproj
index fb7e15a..5c0a875 100644
--- a/Vernissage.xcodeproj/project.pbxproj
+++ b/Vernissage.xcodeproj/project.pbxproj
@@ -157,6 +157,7 @@
F89D6C4629718193001DA3D4 /* GeneralSectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F89D6C4529718193001DA3D4 /* GeneralSectionView.swift */; };
F89D6C4A297196FF001DA3D4 /* ImageViewer.swift in Sources */ = {isa = PBXBuildFile; fileRef = F89D6C49297196FF001DA3D4 /* ImageViewer.swift */; };
F89F57B029D1C11200001EE3 /* RelationshipModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F89F57AF29D1C11200001EE3 /* RelationshipModel.swift */; };
+ F8A4A88329E3FD1C00267E36 /* ImageAvatar.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8A4A88229E3FD1C00267E36 /* ImageAvatar.swift */; };
F8A93D7E2965FD89001D8331 /* UserProfileView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8A93D7D2965FD89001D8331 /* UserProfileView.swift */; };
F8AFF7C129B259150087D083 /* HashtagsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8AFF7C029B259150087D083 /* HashtagsView.swift */; };
F8AFF7C429B25EF40087D083 /* ImagesGrid.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8AFF7C329B25EF40087D083 /* ImagesGrid.swift */; };
@@ -326,6 +327,8 @@
F89D6C49297196FF001DA3D4 /* ImageViewer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageViewer.swift; sourceTree = ""; };
F89F0605299139F6003DC875 /* Vernissage-002.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "Vernissage-002.xcdatamodel"; sourceTree = ""; };
F89F57AF29D1C11200001EE3 /* RelationshipModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelationshipModel.swift; sourceTree = ""; };
+ F8A4A88229E3FD1C00267E36 /* ImageAvatar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageAvatar.swift; sourceTree = ""; };
+ F8A4A88429E4099900267E36 /* Vernissage-008.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "Vernissage-008.xcdatamodel"; sourceTree = ""; };
F8A93D7D2965FD89001D8331 /* UserProfileView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserProfileView.swift; sourceTree = ""; };
F8AFF7C029B259150087D083 /* HashtagsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HashtagsView.swift; sourceTree = ""; };
F8AFF7C329B25EF40087D083 /* ImagesGrid.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImagesGrid.swift; sourceTree = ""; };
@@ -630,6 +633,7 @@
isa = PBXGroup;
children = (
F88BC53A29E06A5100CE6141 /* ImageContextMenu.swift */,
+ F8A4A88229E3FD1C00267E36 /* ImageAvatar.swift */,
);
path = ViewModifiers;
sourceTree = "";
@@ -1069,6 +1073,7 @@
F80048042961850500E6868A /* AttachmentData+CoreDataProperties.swift in Sources */,
F88E4D4A297EA0490057491A /* RouterPath.swift in Sources */,
F88E4D48297E90CD0057491A /* TrendStatusesView.swift in Sources */,
+ F8A4A88329E3FD1C00267E36 /* ImageAvatar.swift in Sources */,
F800480A2961EA1900E6868A /* AttachmentDataHandler.swift in Sources */,
F80048032961850500E6868A /* AttachmentData+CoreDataClass.swift in Sources */,
F891E7D029C368750022C449 /* ImageRowItemAsync.swift in Sources */,
@@ -1603,6 +1608,7 @@
F88C2476295C37BB0006098B /* Vernissage.xcdatamodeld */ = {
isa = XCVersionGroup;
children = (
+ F8A4A88429E4099900267E36 /* Vernissage-008.xcdatamodel */,
F8911A1829DE9E5500770F44 /* Vernissage-007.xcdatamodel */,
F8EF371429C624DA00669F45 /* Vernissage-006.xcdatamodel */,
F8CAE64129B8F1AF001E0372 /* Vernissage-005.xcdatamodel */,
@@ -1612,7 +1618,7 @@
F8C937A929882CA90004D782 /* Vernissage-001.xcdatamodel */,
F88C2477295C37BB0006098B /* Vernissage.xcdatamodel */,
);
- currentVersion = F8911A1829DE9E5500770F44 /* Vernissage-007.xcdatamodel */;
+ currentVersion = F8A4A88429E4099900267E36 /* Vernissage-008.xcdatamodel */;
path = Vernissage.xcdatamodeld;
sourceTree = "";
versionGroupType = wrapper.xcdatamodel;
diff --git a/Vernissage/ViewModifiers/ImageAvatar.swift b/Vernissage/ViewModifiers/ImageAvatar.swift
new file mode 100644
index 0000000..564c3ab
--- /dev/null
+++ b/Vernissage/ViewModifiers/ImageAvatar.swift
@@ -0,0 +1,82 @@
+//
+// https://mczachurski.dev
+// Copyright © 2023 Marcin Czachurski and the repository contributors.
+// Licensed under the Apache License 2.0.
+//
+
+import Foundation
+import SwiftUI
+import NukeUI
+import ClientKit
+import ServicesKit
+import EnvironmentKit
+
+public extension View {
+ func imageAvatar(applicationState: ApplicationState, displayName: String?, avatarUrl: URL?) -> some View {
+ modifier(ImageAvatar(applicationState: applicationState, displayName: displayName, avatarUrl: avatarUrl))
+ }
+}
+
+private struct ImageAvatar: ViewModifier {
+ private let applicationState: ApplicationState
+ private let displayName: String?
+ private let avatarUrl: URL?
+
+ init(applicationState: ApplicationState, displayName: String?, avatarUrl: URL?) {
+ self.applicationState = applicationState
+ self.displayName = displayName
+ self.avatarUrl = avatarUrl
+ }
+
+ func body(content: Content) -> some View {
+ if self.applicationState.showAvatarsOnTimeline {
+ ZStack {
+ // Image.
+ content
+
+ // Avatar.
+ VStack(alignment: .leading) {
+
+ HStack(alignment: .center) {
+ LazyImage(url: avatarUrl) { state in
+ if let image = state.image {
+ self.buildAvatar(image: image)
+ } else if state.isLoading {
+ self.buildAvatar()
+ } else {
+ self.buildAvatar()
+ }
+ }
+
+ Text(displayName ?? "")
+ .foregroundColor(.white)
+ .fontWeight(.semibold)
+ .shadow(color: .black, radius: 2)
+ Spacer()
+ }
+
+ Spacer()
+ }
+ .padding(.leading, 6)
+ .padding(.top, 6)
+ }
+ } else {
+ content
+ }
+ }
+
+ @ViewBuilder
+ private func buildAvatar(image: Image? = nil) -> some View {
+ (image ?? Image("Avatar"))
+ .resizable()
+ .clipShape(applicationState.avatarShape.shape())
+ .aspectRatio(contentMode: .fit)
+ .frame(width: 24, height: 24)
+ .overlay(
+ applicationState.avatarShape.shape()
+ .stroke(Color.white.opacity(0.6), lineWidth: 1)
+ .frame(width: 24, height: 24)
+ )
+ .shadow(color: .black, radius: 2)
+ }
+}
diff --git a/Vernissage/Views/SettingsView/Subviews/GeneralSectionView.swift b/Vernissage/Views/SettingsView/Subviews/GeneralSectionView.swift
index 1b0d61c..af8d09b 100644
--- a/Vernissage/Views/SettingsView/Subviews/GeneralSectionView.swift
+++ b/Vernissage/Views/SettingsView/Subviews/GeneralSectionView.swift
@@ -68,7 +68,6 @@ struct GeneralSectionView: View {
Text("settings.title.theme", comment: "Theme")
}
.onChange(of: self.applicationState.theme) { theme in
- self.applicationState.theme = theme
ApplicationSettingsHandler.shared.set(theme: theme)
}
@@ -82,9 +81,13 @@ struct GeneralSectionView: View {
Text("settings.title.menuPosition", comment: "Menu position")
}
.onChange(of: self.applicationState.menuPosition) { menuPosition in
- self.applicationState.menuPosition = menuPosition
ApplicationSettingsHandler.shared.set(menuPosition: menuPosition)
}
+
+ Toggle("settings.title.showAvatarsOnTimeline", isOn: $applicationState.showAvatarsOnTimeline)
+ .onChange(of: self.applicationState.showAvatarsOnTimeline) { newValue in
+ ApplicationSettingsHandler.shared.set(showAvatarsOnTimeline: newValue)
+ }
}
}
}
diff --git a/Vernissage/Views/UserProfileView/Subviews/UserProfileStatusesView.swift b/Vernissage/Views/UserProfileView/Subviews/UserProfileStatusesView.swift
index 0f64749..ad5f293 100644
--- a/Vernissage/Views/UserProfileView/Subviews/UserProfileStatusesView.swift
+++ b/Vernissage/Views/UserProfileView/Subviews/UserProfileStatusesView.swift
@@ -26,7 +26,7 @@ struct UserProfileStatusesView: View {
LazyVStack(alignment: .center) {
if firstLoadFinished == true {
ForEach(self.statusViewModels, id: \.id) { item in
- ImageRowAsync(statusViewModel: item)
+ ImageRowAsync(statusViewModel: item, withAvatar: false)
}
if allItemsLoaded == false && firstLoadFinished == true {
diff --git a/Vernissage/Widgets/ImageRowAsync.swift b/Vernissage/Widgets/ImageRowAsync.swift
index c4f7f5d..9563f08 100644
--- a/Vernissage/Widgets/ImageRowAsync.swift
+++ b/Vernissage/Widgets/ImageRowAsync.swift
@@ -12,12 +12,14 @@ import ServicesKit
struct ImageRowAsync: View {
private let statusViewModel: StatusModel
private let firstAttachment: AttachmentModel?
+ private let showAvatar: Bool
@State private var selected: String
@State private var imageHeight: Double
@State private var imageWidth: Double
- init(statusViewModel: StatusModel) {
+ init(statusViewModel: StatusModel, withAvatar showAvatar: Bool = true) {
+ self.showAvatar = showAvatar
self.statusViewModel = statusViewModel
self.firstAttachment = statusViewModel.mediaAttachments.first
self.selected = String.empty()
@@ -41,7 +43,7 @@ struct ImageRowAsync: View {
var body: some View {
if statusViewModel.mediaAttachments.count == 1, let firstAttachment = self.firstAttachment {
- ImageRowItemAsync(statusViewModel: self.statusViewModel, attachment: firstAttachment) { (imageWidth, imageHeight) in
+ ImageRowItemAsync(statusViewModel: self.statusViewModel, attachment: firstAttachment, withAvatar: self.showAvatar) { (imageWidth, imageHeight) in
// When we download image and calculate real size we have to change view size.
if imageWidth != self.imageWidth || imageHeight != self.imageHeight {
withAnimation(.linear(duration: 0.4)) {
@@ -54,7 +56,7 @@ struct ImageRowAsync: View {
} else {
TabView(selection: $selected) {
ForEach(statusViewModel.mediaAttachments, id: \.id) { attachment in
- ImageRowItemAsync(statusViewModel: self.statusViewModel, attachment: attachment) { (imageWidth, imageHeight) in
+ ImageRowItemAsync(statusViewModel: self.statusViewModel, attachment: attachment, withAvatar: self.showAvatar) { (imageWidth, imageHeight) in
// When we download image and calculate real size we have to change view size (only when image is now visible).
if attachment.id == self.selected {
if imageWidth != self.imageWidth || imageHeight != self.imageHeight {
diff --git a/Vernissage/Widgets/ImageRowItem.swift b/Vernissage/Widgets/ImageRowItem.swift
index 803ea20..c742a03 100644
--- a/Vernissage/Widgets/ImageRowItem.swift
+++ b/Vernissage/Widgets/ImageRowItem.swift
@@ -38,32 +38,9 @@ struct ImageRowItem: View {
var body: some View {
if let uiImage {
- ZStack {
- if self.status.sensitive && !self.applicationState.showSensitive {
- ZStack {
- ContentWarning(spoilerText: self.status.spoilerText) {
- self.imageView(uiImage: uiImage)
-
- if showThumbImage {
- FavouriteTouch {
- self.showThumbImage = false
- }
- }
- } blurred: {
- BlurredImage(blurhash: attachmentData.blurhash)
- .onTapGesture {
- self.navigateToStatus()
- }
- }
- }
- .opacity(self.opacity)
- .onAppear {
- withAnimation {
- self.opacity = 1.0
- }
- }
- } else {
- ZStack {
+ if self.status.sensitive && !self.applicationState.showSensitive {
+ ZStack {
+ ContentWarning(spoilerText: self.status.spoilerText) {
self.imageView(uiImage: uiImage)
if showThumbImage {
@@ -71,14 +48,35 @@ struct ImageRowItem: View {
self.showThumbImage = false
}
}
+ } blurred: {
+ BlurredImage(blurhash: attachmentData.blurhash)
+ .onTapGesture {
+ self.navigateToStatus()
+ }
}
- .opacity(self.opacity)
- .onAppear {
- withAnimation {
- self.opacity = 1.0
+ }
+ .opacity(self.opacity)
+ .onAppear {
+ withAnimation {
+ self.opacity = 1.0
+ }
+ }
+ } else {
+ ZStack {
+ self.imageView(uiImage: uiImage)
+
+ if showThumbImage {
+ FavouriteTouch {
+ self.showThumbImage = false
}
}
}
+ .opacity(self.opacity)
+ .onAppear {
+ withAnimation {
+ self.opacity = 1.0
+ }
+ }
}
} else {
if cancelled {
@@ -123,6 +121,9 @@ struct ImageRowItem: View {
.onTapGesture {
self.navigateToStatus()
}
+ .imageAvatar(applicationState: self.applicationState,
+ displayName: self.status.accountDisplayName,
+ avatarUrl: self.status.accountAvatar)
.imageContextMenu(client: self.client, statusData: self.status)
}
diff --git a/Vernissage/Widgets/ImageRowItemAsync.swift b/Vernissage/Widgets/ImageRowItemAsync.swift
index 6eeee11..49a157e 100644
--- a/Vernissage/Widgets/ImageRowItemAsync.swift
+++ b/Vernissage/Widgets/ImageRowItemAsync.swift
@@ -19,13 +19,17 @@ struct ImageRowItemAsync: View {
private var statusViewModel: StatusModel
private var attachment: AttachmentModel
+ private let showAvatar: Bool
@State private var showThumbImage = false
@State private var opacity = 0.0
private let onImageDownloaded: (Double, Double) -> Void
- init(statusViewModel: StatusModel, attachment: AttachmentModel, onImageDownloaded: @escaping (_: Double, _: Double) -> Void) {
+ init(statusViewModel: StatusModel,
+ attachment: AttachmentModel,
+ withAvatar showAvatar: Bool = true, onImageDownloaded: @escaping (_: Double, _: Double) -> Void) {
+ self.showAvatar = showAvatar
self.statusViewModel = statusViewModel
self.attachment = attachment
self.onImageDownloaded = onImageDownloaded
@@ -123,6 +127,11 @@ struct ImageRowItemAsync: View {
.onTapGesture {
self.navigateToStatus()
}
+ .if(self.showAvatar) {
+ $0.imageAvatar(applicationState: self.applicationState,
+ displayName: self.statusViewModel.account.displayNameWithoutEmojis,
+ avatarUrl: self.statusViewModel.account.avatar)
+ }
.imageContextMenu(client: self.client, statusModel: self.statusViewModel)
}
diff --git a/WidgetsKit/Sources/WidgetsKit/Extensions/View+If.swift b/WidgetsKit/Sources/WidgetsKit/Extensions/View+If.swift
new file mode 100644
index 0000000..5929eb5
--- /dev/null
+++ b/WidgetsKit/Sources/WidgetsKit/Extensions/View+If.swift
@@ -0,0 +1,19 @@
+//
+// https://mczachurski.dev
+// Copyright © 2023 Marcin Czachurski and the repository contributors.
+// Licensed under the MIT License.
+//
+
+import Foundation
+import SwiftUI
+
+public extension View {
+ @ViewBuilder
+ func `if`(_ conditional: Bool, content: (Self) -> Content) -> some View {
+ if conditional {
+ content(self)
+ } else {
+ self
+ }
+ }
+}