From 6b8cbec48ea9b2a0b5af96d6ff79d5f546409ed1 Mon Sep 17 00:00:00 2001 From: Marcin Czachursk Date: Fri, 14 Apr 2023 16:50:47 +0200 Subject: [PATCH] Add favourite icon on the timelines --- .../ClientKit/Models/StatusModel.swift | 2 +- ...plicationSettings+CoreDataProperties.swift | 1 + CoreData/ApplicationSettingsHandler.swift | 7 ++ CoreData/StatusDataHandler.swift | 13 ++- .../Vernissage.xcdatamodeld/.xccurrentversion | 2 +- .../Vernissage-009.xcdatamodel/contents | 96 +++++++++++++++++++ .../EnvironmentKit/ApplicationState.swift | 3 + Localization/en.lproj/Localizable.strings | 5 +- Localization/eu.lproj/Localizable.strings | 5 +- Localization/pl.lproj/Localizable.strings | 5 +- Vernissage.xcodeproj/project.pbxproj | 8 +- Vernissage/ViewModifiers/ImageFavourite.swift | 53 ++++++++++ .../Subviews/GeneralSectionView.swift | 25 ++++- Vernissage/Widgets/ImageRowItem.swift | 17 +++- Vernissage/Widgets/ImageRowItemAsync.swift | 11 +++ 15 files changed, 241 insertions(+), 12 deletions(-) create mode 100644 CoreData/Vernissage.xcdatamodeld/Vernissage-009.xcdatamodel/contents create mode 100644 Vernissage/ViewModifiers/ImageFavourite.swift diff --git a/ClientKit/Sources/ClientKit/Models/StatusModel.swift b/ClientKit/Sources/ClientKit/Models/StatusModel.swift index c13b29d..068f750 100644 --- a/ClientKit/Sources/ClientKit/Models/StatusModel.swift +++ b/ClientKit/Sources/ClientKit/Models/StatusModel.swift @@ -23,7 +23,6 @@ public class StatusModel: ObservableObject { public let favouritesCount: Int public let repliesCount: Int public let reblogged: Bool - public let favourited: Bool public let sensitive: Bool public let bookmarked: Bool public let pinned: Bool @@ -39,6 +38,7 @@ public class StatusModel: ObservableObject { public let reblogStatus: Status? + @Published public var favourited: Bool @Published public var mediaAttachments: [AttachmentModel] public init(status: Status) { diff --git a/CoreData/ApplicationSettings+CoreDataProperties.swift b/CoreData/ApplicationSettings+CoreDataProperties.swift index 8cb4d3e..d8b798b 100644 --- a/CoreData/ApplicationSettings+CoreDataProperties.swift +++ b/CoreData/ApplicationSettings+CoreDataProperties.swift @@ -30,6 +30,7 @@ extension ApplicationSettings { @NSManaged public var showPhotoDescription: Bool @NSManaged public var menuPosition: Int32 @NSManaged public var showAvatarsOnTimeline: Bool + @NSManaged public var showFavouritesOnTimeline: Bool } extension ApplicationSettings: Identifiable { diff --git a/CoreData/ApplicationSettingsHandler.swift b/CoreData/ApplicationSettingsHandler.swift index 6503d8f..1623556 100644 --- a/CoreData/ApplicationSettingsHandler.swift +++ b/CoreData/ApplicationSettingsHandler.swift @@ -55,6 +55,7 @@ class ApplicationSettingsHandler { applicationState.showSensitive = defaultSettings.showSensitive applicationState.showPhotoDescription = defaultSettings.showPhotoDescription applicationState.showAvatarsOnTimeline = defaultSettings.showAvatarsOnTimeline + applicationState.showFavouritesOnTimeline = defaultSettings.showFavouritesOnTimeline if let menuPosition = MenuPosition(rawValue: Int(defaultSettings.menuPosition)) { applicationState.menuPosition = menuPosition @@ -151,6 +152,12 @@ class ApplicationSettingsHandler { CoreDataHandler.shared.save() } + func set(showFavouritesOnTimeline: Bool) { + let defaultSettings = self.get() + defaultSettings.showFavouritesOnTimeline = showFavouritesOnTimeline + 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/StatusDataHandler.swift b/CoreData/StatusDataHandler.swift index 6f34353..c520e98 100644 --- a/CoreData/StatusDataHandler.swift +++ b/CoreData/StatusDataHandler.swift @@ -28,8 +28,8 @@ class StatusDataHandler { } } - func getStatusData(accountId: String, statusId: String) -> StatusData? { - let context = CoreDataHandler.shared.container.viewContext + func getStatusData(accountId: String, statusId: String, viewContext: NSManagedObjectContext? = nil) -> StatusData? { + let context = viewContext ?? CoreDataHandler.shared.container.viewContext let fetchRequest = StatusData.fetchRequest() fetchRequest.fetchLimit = 1 @@ -114,6 +114,15 @@ class StatusDataHandler { } } + func setFavourited(accountId: String, statusId: String) { + let backgroundContext = CoreDataHandler.shared.newBackgroundContext() + + if let statusData = self.getStatusData(accountId: accountId, statusId: statusId, viewContext: backgroundContext) { + statusData.favourited = true + CoreDataHandler.shared.save(viewContext: backgroundContext) + } + } + func createStatusDataEntity(viewContext: NSManagedObjectContext? = nil) -> StatusData { let context = viewContext ?? CoreDataHandler.shared.container.viewContext return StatusData(context: context) diff --git a/CoreData/Vernissage.xcdatamodeld/.xccurrentversion b/CoreData/Vernissage.xcdatamodeld/.xccurrentversion index 7d4618c..c28b4fb 100644 --- a/CoreData/Vernissage.xcdatamodeld/.xccurrentversion +++ b/CoreData/Vernissage.xcdatamodeld/.xccurrentversion @@ -3,6 +3,6 @@ _XCCurrentVersionName - Vernissage-008.xcdatamodel + Vernissage-009.xcdatamodel diff --git a/CoreData/Vernissage.xcdatamodeld/Vernissage-009.xcdatamodel/contents b/CoreData/Vernissage.xcdatamodeld/Vernissage-009.xcdatamodel/contents new file mode 100644 index 0000000..a13dd6c --- /dev/null +++ b/CoreData/Vernissage.xcdatamodeld/Vernissage-009.xcdatamodel/contents @@ -0,0 +1,96 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/EnvironmentKit/Sources/EnvironmentKit/ApplicationState.swift b/EnvironmentKit/Sources/EnvironmentKit/ApplicationState.swift index e81fd10..4a60d11 100644 --- a/EnvironmentKit/Sources/EnvironmentKit/ApplicationState.swift +++ b/EnvironmentKit/Sources/EnvironmentKit/ApplicationState.swift @@ -92,6 +92,9 @@ public class ApplicationState: ObservableObject { /// Should avatars be visible on timelines. @Published public var showAvatarsOnTimeline = false + /// Should favourites be visible on timelines. + @Published public var showFavouritesOnTimeline = 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 a32c92b..039e176 100644 --- a/Localization/en.lproj/Localizable.strings +++ b/Localization/en.lproj/Localizable.strings @@ -211,7 +211,10 @@ "settings.title.topMenu" = "Navigation bar"; "settings.title.bottomRightMenu" = "Bottom right"; "settings.title.bottomLeftMenu" = "Bottom left"; -"settings.title.showAvatarsOnTimeline" = "Show avatars on timelines"; +"settings.title.showAvatars" = "Show avatars"; +"settings.title.showAvatarsOnTimeline" = "Avatars will be displayed on timelines"; +"settings.title.showFavourite" = "Show favourites"; +"settings.title.showFavouriteOnTimeline" = "Favourites will be displayed 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 e64bd42..4fdcfc3 100644 --- a/Localization/eu.lproj/Localizable.strings +++ b/Localization/eu.lproj/Localizable.strings @@ -211,7 +211,10 @@ "settings.title.topMenu" = "Nabigazio barra"; "settings.title.bottomRightMenu" = "Behe eskumaldean"; "settings.title.bottomLeftMenu" = "Behe ezkerraldean"; -"settings.title.showAvatarsOnTimeline" = "Show avatars on timelines"; +"settings.title.showAvatars" = "Show avatars"; +"settings.title.showAvatarsOnTimeline" = "Avatars will be displayed on timelines"; +"settings.title.showFavourite" = "Show favourites"; +"settings.title.showFavouriteOnTimeline" = "Favourites will be displayed 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 112865e..c2cae72 100644 --- a/Localization/pl.lproj/Localizable.strings +++ b/Localization/pl.lproj/Localizable.strings @@ -211,7 +211,10 @@ "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ęć"; +"settings.title.showAvatars" = "Wyświetlaj awatary"; +"settings.title.showAvatarsOnTimeline" = "Awatary będą widoczne na osiach zdjęć"; +"settings.title.showFavourite" = "Wyświetlaj polubienia"; +"settings.title.showFavouriteOnTimeline" = "Polubienia będą widoczne 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 4e8eca2..bbfd7c8 100644 --- a/Vernissage.xcodeproj/project.pbxproj +++ b/Vernissage.xcodeproj/project.pbxproj @@ -176,6 +176,7 @@ F8F6E44D29BCC1F90004795E /* MediumWidgetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8F6E44829BCC0F00004795E /* MediumWidgetView.swift */; }; F8F6E44E29BCC1FB0004795E /* LargeWidgetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8F6E44A29BCC0FF0004795E /* LargeWidgetView.swift */; }; F8F6E45129BCE9190004795E /* UIImage+Resize.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8F6E45029BCE9190004795E /* UIImage+Resize.swift */; }; + F8FFBD4829E9901E0047EE80 /* ImageFavourite.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8FFBD4729E9901E0047EE80 /* ImageFavourite.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -357,6 +358,8 @@ F8F6E44829BCC0F00004795E /* MediumWidgetView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediumWidgetView.swift; sourceTree = ""; }; F8F6E44A29BCC0FF0004795E /* LargeWidgetView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LargeWidgetView.swift; sourceTree = ""; }; F8F6E45029BCE9190004795E /* UIImage+Resize.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIImage+Resize.swift"; sourceTree = ""; }; + F8FFBD4729E9901E0047EE80 /* ImageFavourite.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageFavourite.swift; sourceTree = ""; }; + F8FFBD4929E99BEE0047EE80 /* Vernissage-009.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "Vernissage-009.xcdatamodel"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -636,6 +639,7 @@ children = ( F88BC53A29E06A5100CE6141 /* ImageContextMenu.swift */, F8A4A88229E3FD1C00267E36 /* ImageAvatar.swift */, + F8FFBD4729E9901E0047EE80 /* ImageFavourite.swift */, ); path = ViewModifiers; sourceTree = ""; @@ -1076,6 +1080,7 @@ F86B7221296C49A300EE59EC /* EmptyButtonStyle.swift in Sources */, F80048042961850500E6868A /* AttachmentData+CoreDataProperties.swift in Sources */, F88E4D4A297EA0490057491A /* RouterPath.swift in Sources */, + F8FFBD4829E9901E0047EE80 /* ImageFavourite.swift in Sources */, F88E4D48297E90CD0057491A /* TrendStatusesView.swift in Sources */, F8A4A88329E3FD1C00267E36 /* ImageAvatar.swift in Sources */, F800480A2961EA1900E6868A /* AttachmentDataHandler.swift in Sources */, @@ -1612,6 +1617,7 @@ F88C2476295C37BB0006098B /* Vernissage.xcdatamodeld */ = { isa = XCVersionGroup; children = ( + F8FFBD4929E99BEE0047EE80 /* Vernissage-009.xcdatamodel */, F8A4A88429E4099900267E36 /* Vernissage-008.xcdatamodel */, F8911A1829DE9E5500770F44 /* Vernissage-007.xcdatamodel */, F8EF371429C624DA00669F45 /* Vernissage-006.xcdatamodel */, @@ -1622,7 +1628,7 @@ F8C937A929882CA90004D782 /* Vernissage-001.xcdatamodel */, F88C2477295C37BB0006098B /* Vernissage.xcdatamodel */, ); - currentVersion = F8A4A88429E4099900267E36 /* Vernissage-008.xcdatamodel */; + currentVersion = F8FFBD4929E99BEE0047EE80 /* Vernissage-009.xcdatamodel */; path = Vernissage.xcdatamodeld; sourceTree = ""; versionGroupType = wrapper.xcdatamodel; diff --git a/Vernissage/ViewModifiers/ImageFavourite.swift b/Vernissage/ViewModifiers/ImageFavourite.swift new file mode 100644 index 0000000..d9a8d9f --- /dev/null +++ b/Vernissage/ViewModifiers/ImageFavourite.swift @@ -0,0 +1,53 @@ +// +// 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 imageFavourite(isFavourited: Binding) -> some View { + modifier(ImageFavourite(isFavourited: isFavourited)) + } +} + +private struct ImageFavourite: ViewModifier { + @EnvironmentObject var applicationState: ApplicationState + @Binding private var isFavourited: Bool + + init(isFavourited: Binding) { + self._isFavourited = isFavourited + } + + func body(content: Content) -> some View { + if self.applicationState.showFavouritesOnTimeline && self.isFavourited { + ZStack { + // Image. + content + + // Avatar. + VStack(alignment: .leading) { + Spacer() + + HStack(alignment: .center) { + Image(systemName: "star.fill") + .font(.system(size: 12)) + .shadow(color: .black, radius: 4) + .foregroundColor(.white.opacity(0.8)) + Spacer() + } + } + .padding(.leading, 12) + .padding(.bottom, 14) + } + } else { + content + } + } +} diff --git a/Vernissage/Views/SettingsView/Subviews/GeneralSectionView.swift b/Vernissage/Views/SettingsView/Subviews/GeneralSectionView.swift index af8d09b..01ab3b5 100644 --- a/Vernissage/Views/SettingsView/Subviews/GeneralSectionView.swift +++ b/Vernissage/Views/SettingsView/Subviews/GeneralSectionView.swift @@ -84,10 +84,29 @@ struct GeneralSectionView: View { ApplicationSettingsHandler.shared.set(menuPosition: menuPosition) } - Toggle("settings.title.showAvatarsOnTimeline", isOn: $applicationState.showAvatarsOnTimeline) - .onChange(of: self.applicationState.showAvatarsOnTimeline) { newValue in - ApplicationSettingsHandler.shared.set(showAvatarsOnTimeline: newValue) + Toggle(isOn: $applicationState.showAvatarsOnTimeline) { + VStack(alignment: .leading) { + Text("settings.title.showAvatars", comment: "Show avatars") + Text("settings.title.showAvatarsOnTimeline", comment: "Show avatars on timeline") + .font(.footnote) + .foregroundColor(.lightGrayColor) } + } + .onChange(of: self.applicationState.showAvatarsOnTimeline) { newValue in + ApplicationSettingsHandler.shared.set(showAvatarsOnTimeline: newValue) + } + + Toggle(isOn: $applicationState.showFavouritesOnTimeline) { + VStack(alignment: .leading) { + Text("settings.title.showFavourite", comment: "Show favourites") + Text("settings.title.showFavouriteOnTimeline", comment: "Show favourites on timeline") + .font(.footnote) + .foregroundColor(.lightGrayColor) + } + } + .onChange(of: self.applicationState.showFavouritesOnTimeline) { newValue in + ApplicationSettingsHandler.shared.set(showFavouritesOnTimeline: newValue) + } } } } diff --git a/Vernissage/Widgets/ImageRowItem.swift b/Vernissage/Widgets/ImageRowItem.swift index 1d9e5cd..5533122 100644 --- a/Vernissage/Widgets/ImageRowItem.swift +++ b/Vernissage/Widgets/ImageRowItem.swift @@ -23,6 +23,7 @@ struct ImageRowItem: View { @State private var cancelled = true @State private var error: Error? @State private var opacity = 0.0 + @State private var isFavourited = false private let onImageDownloaded: (Double, Double) -> Void @@ -114,18 +115,32 @@ struct ImageRowItem: View { .aspectRatio(contentMode: .fit) .onTapGesture(count: 2) { Task { - try? await self.client.statuses?.favourite(statusId: self.status.id) + // Update favourite in Pixelfed server. + _ = try? await self.client.statuses?.favourite(statusId: self.status.id) + + // Update favourite in local cache (core data). + if let accountId = self.applicationState.account?.id { + StatusDataHandler.shared.setFavourited(accountId: accountId, statusId: self.status.id) + } } + // Run adnimation and haptic feedback. self.showThumbImage = true HapticService.shared.fireHaptic(of: .buttonPress) + + // Mark favourite booleans used to show star in the timeline view. + self.isFavourited = true } .onTapGesture { self.navigateToStatus() } .imageAvatar(displayName: self.status.accountDisplayName, avatarUrl: self.status.accountAvatar) + .imageFavourite(isFavourited: $isFavourited) .imageContextMenu(statusData: self.status) + .onAppear { + self.isFavourited = self.status.favourited + } } private func downloadImage(attachmentData: AttachmentData) async { diff --git a/Vernissage/Widgets/ImageRowItemAsync.swift b/Vernissage/Widgets/ImageRowItemAsync.swift index dbca37b..c3619a2 100644 --- a/Vernissage/Widgets/ImageRowItemAsync.swift +++ b/Vernissage/Widgets/ImageRowItemAsync.swift @@ -23,6 +23,7 @@ struct ImageRowItemAsync: View { @State private var showThumbImage = false @State private var opacity = 0.0 + @State private var isFavourited = false private let onImageDownloaded: (Double, Double) -> Void @@ -122,11 +123,17 @@ struct ImageRowItemAsync: View { .aspectRatio(contentMode: .fit) .onTapGesture(count: 2) { Task { + // Update favourite in Pixelfed server. try? await self.client.statuses?.favourite(statusId: self.statusViewModel.id) } + // Run adnimation and haptic feedback. self.showThumbImage = true HapticService.shared.fireHaptic(of: .buttonPress) + + // Mark favourite booleans used to show star in the timeline view. + self.statusViewModel.favourited = true + self.isFavourited = true } .onTapGesture { self.navigateToStatus() @@ -135,7 +142,11 @@ struct ImageRowItemAsync: View { $0.imageAvatar(displayName: self.statusViewModel.account.displayNameWithoutEmojis, avatarUrl: self.statusViewModel.account.avatar) } + .imageFavourite(isFavourited: $isFavourited) .imageContextMenu(statusModel: self.statusViewModel) + .onAppear { + self.isFavourited = self.statusViewModel.favourited + } } private func navigateToStatus() {