Add indicator to notifications icon

This commit is contained in:
Marcin Czachurski 2023-10-22 10:09:02 +02:00
parent 06af34d4c8
commit 02c62b86d9
17 changed files with 249 additions and 122 deletions

View File

@ -27,7 +27,8 @@ import Foundation
public let url: URL?
public let username: String
public let lastSeenStatusId: String?
public let lastSeenNotificationId: String?
public var avatarData: Data?
public init(id: String,
@ -50,6 +51,7 @@ import Foundation
url: URL?,
username: String,
lastSeenStatusId: String?,
lastSeenNotificationId: String?,
avatarData: Data? = nil) {
self.id = id
self.accessToken = accessToken
@ -72,6 +74,7 @@ import Foundation
self.username = username
self.lastSeenStatusId = lastSeenStatusId
self.avatarData = avatarData
self.lastSeenNotificationId = lastSeenNotificationId
}
}

View File

@ -10,24 +10,62 @@ import ClientKit
@Model final public class AccountData {
@Attribute(.unique) public var id: String
/// Access token to the server API.
public var accessToken: String?
/// Refresh token which can be used to download new access token.
public var refreshToken: String?
/// Full user name (user name with server address).
public var acct: String
/// URL to user avatar.
public var avatar: URL?
public var avatarData: Data?
/// Avatar downloaded from server (visible mainly in top navigation bar).
@Attribute(.externalStorage) public var avatarData: Data?
/// Id of OAuth client.
public var clientId: String
/// Secret of OAutch client.
public var clientSecret: String
/// Vapid key of OAuth client.
public var clientVapidKey: String
/// Date of creating user.
public var createdAt: String
/// Human readable user name.
public var displayName: String?
/// Number of followers.
public var followersCount: Int32
/// Number of following users.
public var followingCount: Int32
/// URL to header image visible on user profile.
public var header: URL?
/// User profile is locked.
public var locked: Bool
/// Description on user profile.
public var note: String?
/// Address to server.
public var serverUrl: URL
/// NUmber of statuses added by the user.
public var statusesCount: Int32
/// Url to user profile.
public var url: URL?
/// User name (without server address).
public var username: String
/// Last status seen on home timeline by the user.
@ -39,10 +77,12 @@ import ClientKit
/// JSON string with last objects loaded into home timeline.
public var timelineCache: String?
/// Last notification seen by the user.
public var lastSeenNotificationId: String?
@Relationship(deleteRule: .cascade, inverse: \ViewedStatus.pixelfedAccount) public var viewedStatuses: [ViewedStatus]
@Relationship(deleteRule: .cascade, inverse: \AccountRelationship.pixelfedAccount) public var accountRelationships: [AccountRelationship]
init(
accessToken: String? = nil,
refreshToken: String? = nil,
@ -119,6 +159,7 @@ extension AccountData {
url: self.url,
username: self.username,
lastSeenStatusId: self.lastSeenStatusId,
lastSeenNotificationId: self.lastSeenNotificationId,
avatarData: self.avatarData)
return accountModel
}

View File

@ -7,6 +7,7 @@
import Foundation
import SwiftData
import PixelfedKit
import EnvironmentKit
class AccountDataHandler {
public static let shared = AccountDataHandler()
@ -63,13 +64,18 @@ class AccountDataHandler {
}
}
func update(lastSeenStatusId: String?, lastLoadedStatusId: String?, statuses: [Status]? = nil, accountId: String, modelContext: ModelContext) throws {
func update(lastSeenStatusId: String?, lastLoadedStatusId: String?, statuses: [Status]? = nil, applicationState: ApplicationState, modelContext: ModelContext) throws {
guard let accountId = applicationState.account?.id else {
return
}
guard let accountDataFromDb = self.getAccountData(accountId: accountId, modelContext: modelContext) else {
return
}
if (accountDataFromDb.lastSeenStatusId ?? "0") < (lastSeenStatusId ?? "0") {
accountDataFromDb.lastSeenStatusId = lastSeenStatusId
applicationState.lastSeenStatusId = lastSeenStatusId
}
if (accountDataFromDb.lastLoadedStatusId ?? "0") < (lastLoadedStatusId ?? "0") {
@ -82,4 +88,21 @@ class AccountDataHandler {
try modelContext.save()
}
func update(lastSeenNotificationId: String?, applicationState: ApplicationState, modelContext: ModelContext) throws {
guard let accountId = applicationState.account?.id else {
return
}
guard let accountDataFromDb = self.getAccountData(accountId: accountId, modelContext: modelContext) else {
return
}
if (accountDataFromDb.lastSeenNotificationId ?? "0") < (lastSeenNotificationId ?? "0") {
accountDataFromDb.lastSeenNotificationId = lastSeenNotificationId
applicationState.lastSeenNotificationId = lastSeenNotificationId
}
try modelContext.save()
}
}

View File

@ -35,9 +35,15 @@ import ClientKit
/// Each URL in a status will be assumed to be exactly this many characters.
public private(set) var statusCharactersReservedPerUrl = defaults.statusCharactersReservedPerUrl
/// Last notification seen by the user.
public var lastSeenNotificationId: String?
/// Information about new notifications.
public var newNotificationsHasBeenAdded = false
/// Last status seen by the user.
public var lastSeenStatusId: String?
/// Amount of new statuses which are not displayed yet to the user.
public var amountOfNewStatuses = 0
@ -113,10 +119,12 @@ import ClientKit
/// Hide statuses without ALT text.
public var hideStatusesWithoutAlt = false
public func changeApplicationState(accountModel: AccountModel, instance: Instance?, lastSeenStatusId: String?) {
public func changeApplicationState(accountModel: AccountModel, instance: Instance?, lastSeenStatusId: String?, lastSeenNotificationId: String?) {
self.account = accountModel
self.lastSeenNotificationId = lastSeenNotificationId
self.lastSeenStatusId = lastSeenStatusId
self.amountOfNewStatuses = 0
self.newNotificationsHasBeenAdded = false
if let statusesConfiguration = instance?.configuration?.statuses {
self.statusMaxCharacters = statusesConfiguration.maxCharacters
@ -132,7 +140,9 @@ import ClientKit
public func clearApplicationState() {
self.account = nil
self.lastSeenStatusId = nil
self.lastSeenNotificationId = nil
self.amountOfNewStatuses = 0
self.newNotificationsHasBeenAdded = false
self.statusMaxCharacters = ApplicationState.defaults.statusMaxCharacters
self.statusMaxMediaAttachments = ApplicationState.defaults.statusMaxMediaAttachments

View File

@ -77,7 +77,6 @@
F8705A7B29FF872F00DA818A /* QRCodeGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8705A7A29FF872F00DA818A /* QRCodeGenerator.swift */; };
F8705A7E29FF880600DA818A /* FileFetcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8705A7D29FF880600DA818A /* FileFetcher.swift */; };
F870EE5229F1645C00A2D43B /* MainNavigationOptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = F870EE5129F1645C00A2D43B /* MainNavigationOptions.swift */; };
F871F21D29EF0D7000A351EF /* NavigationMenuItemDetails.swift in Sources */ = {isa = PBXBuildFile; fileRef = F871F21C29EF0D7000A351EF /* NavigationMenuItemDetails.swift */; };
F8742FC429990AFB00E9642B /* ClientError.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8742FC329990AFB00E9642B /* ClientError.swift */; };
F8764187298ABB520057D362 /* ViewState.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8764186298ABB520057D362 /* ViewState.swift */; };
F876418D298AE5020057D362 /* PaginableStatusesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F876418C298AE5020057D362 /* PaginableStatusesView.swift */; };
@ -159,6 +158,7 @@
F8D8E0CF2ACC23B300AA1374 /* ViewedStatusHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8D8E0CE2ACC23B300AA1374 /* ViewedStatusHandler.swift */; };
F8D8E0D02ACC23B300AA1374 /* ViewedStatusHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8D8E0CE2ACC23B300AA1374 /* ViewedStatusHandler.swift */; };
F8D8E0D12ACC23B300AA1374 /* ViewedStatusHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8D8E0CE2ACC23B300AA1374 /* ViewedStatusHandler.swift */; };
F8DE749F2AE4F7B500ACD188 /* NotificationsService.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8DE749E2AE4F7B500ACD188 /* NotificationsService.swift */; };
F8DF38E429DD68820047F1AA /* ViewOffsetKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8DF38E329DD68820047F1AA /* ViewOffsetKey.swift */; };
F8DF38E629DDB98A0047F1AA /* SocialsSectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8DF38E529DDB98A0047F1AA /* SocialsSectionView.swift */; };
F8E36E462AB8745300769C55 /* Sizable.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8E36E452AB8745300769C55 /* Sizable.swift */; };
@ -274,7 +274,6 @@
F8705A7A29FF872F00DA818A /* QRCodeGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QRCodeGenerator.swift; sourceTree = "<group>"; };
F8705A7D29FF880600DA818A /* FileFetcher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileFetcher.swift; sourceTree = "<group>"; };
F870EE5129F1645C00A2D43B /* MainNavigationOptions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainNavigationOptions.swift; sourceTree = "<group>"; };
F871F21C29EF0D7000A351EF /* NavigationMenuItemDetails.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationMenuItemDetails.swift; sourceTree = "<group>"; };
F8742FC329990AFB00E9642B /* ClientError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClientError.swift; sourceTree = "<group>"; };
F8764186298ABB520057D362 /* ViewState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewState.swift; sourceTree = "<group>"; };
F876418C298AE5020057D362 /* PaginableStatusesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaginableStatusesView.swift; sourceTree = "<group>"; };
@ -341,6 +340,7 @@
F8D5444229D4066C002225D6 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
F8D8E0CA2ACC237000AA1374 /* ViewedStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewedStatus.swift; sourceTree = "<group>"; };
F8D8E0CE2ACC23B300AA1374 /* ViewedStatusHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewedStatusHandler.swift; sourceTree = "<group>"; };
F8DE749E2AE4F7B500ACD188 /* NotificationsService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationsService.swift; sourceTree = "<group>"; };
F8DF38E329DD68820047F1AA /* ViewOffsetKey.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewOffsetKey.swift; sourceTree = "<group>"; };
F8DF38E529DDB98A0047F1AA /* SocialsSectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SocialsSectionView.swift; sourceTree = "<group>"; };
F8E36E452AB8745300769C55 /* Sizable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Sizable.swift; sourceTree = "<group>"; };
@ -479,7 +479,6 @@
F85D4DFD29B78C8400345267 /* HashtagModel.swift */,
F89F57AF29D1C11200001EE3 /* RelationshipModel.swift */,
F8DF38E329DD68820047F1AA /* ViewOffsetKey.swift */,
F871F21C29EF0D7000A351EF /* NavigationMenuItemDetails.swift */,
F8624D3C29F2D3AC00204986 /* SelectedMenuItemDetails.swift */,
F8E36E452AB8745300769C55 /* Sizable.swift */,
F8B758DD2AB9DD85000C8068 /* ColumnData.swift */,
@ -773,6 +772,7 @@
F878842129A4A4E3003CFAD2 /* AppMetadataService.swift */,
F86BC9E829EBBB66009415EC /* ImageSaver.swift */,
F8D0E5232AE2A88A0061C561 /* HomeTimelineService.swift */,
F8DE749E2AE4F7B500ACD188 /* NotificationsService.swift */,
);
path = Services;
sourceTree = "<group>";
@ -1084,7 +1084,6 @@
F87AEB972986D16D00434FB6 /* AuthorisationError.swift in Sources */,
F8742FC429990AFB00E9642B /* ClientError.swift in Sources */,
F883402029B62AE900C3E096 /* SearchView.swift in Sources */,
F871F21D29EF0D7000A351EF /* NavigationMenuItemDetails.swift in Sources */,
F8E36E462AB8745300769C55 /* Sizable.swift in Sources */,
F85DBF8F296732E20069BF89 /* AccountsView.swift in Sources */,
F805DCF129DBEF83006A1FD9 /* ReportView.swift in Sources */,
@ -1109,6 +1108,7 @@
F8A93D7E2965FD89001D8331 /* UserProfileView.swift in Sources */,
F88C246E295C37B80006098B /* MainView.swift in Sources */,
F8AFF7C429B25EF40087D083 /* ImagesGrid.swift in Sources */,
F8DE749F2AE4F7B500ACD188 /* NotificationsService.swift in Sources */,
F8AFF7C129B259150087D083 /* HashtagsView.swift in Sources */,
F8DF38E429DD68820047F1AA /* ViewOffsetKey.swift in Sources */,
F89A46DE296EABA20062125F /* StatusPlaceholderView.swift in Sources */,

View File

@ -1,26 +0,0 @@
//
// https://mczachurski.dev
// Copyright © 2023 Marcin Czachurski and the repository contributors.
// Licensed under the Apache License 2.0.
//
import Foundation
import SwiftUI
@Observable class NavigationMenuItemDetails: Identifiable {
var title: LocalizedStringKey
var image: String
var viewMode: MainView.ViewMode {
didSet {
self.title = viewMode.title
self.image = viewMode.image
}
}
init(viewMode: MainView.ViewMode) {
self.viewMode = viewMode
self.title = viewMode.title
self.image = viewMode.image
}
}

View File

@ -6,11 +6,12 @@
import Foundation
class SelectedMenuItemDetails: NavigationMenuItemDetails {
class SelectedMenuItemDetails: Identifiable {
public let position: Int
public var viewMode: MainView.ViewMode
init(position: Int, viewMode: MainView.ViewMode) {
self.position = position
super.init(viewMode: viewMode)
self.viewMode = viewMode
}
}

View File

@ -105,23 +105,7 @@ public class HomeTimelineService {
return visibleStatuses
}
public func update(lastSeenStatusId: String?, lastLoadedStatusId: String?, statuses: [Status]? = nil, applicationState: ApplicationState, modelContext: ModelContext) throws {
guard let accountId = applicationState.account?.id else {
return
}
try AccountDataHandler.shared.update(lastSeenStatusId: lastSeenStatusId,
lastLoadedStatusId: lastLoadedStatusId,
statuses: statuses,
accountId: accountId,
modelContext: modelContext)
if (applicationState.lastSeenStatusId ?? "0") < (lastSeenStatusId ?? "0") {
applicationState.lastSeenStatusId = lastSeenStatusId
}
}
private func hasBeenAlreadyOnTimeline(accountId: String, status: Status, modelContext: ModelContext) -> Bool {
return ViewedStatusHandler.shared.hasBeenAlreadyOnTimeline(accountId: accountId, status: status, modelContext: modelContext)
}

View File

@ -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 SwiftData
import PixelfedKit
import ClientKit
import ServicesKit
import Nuke
import OSLog
import EnvironmentKit
import Semaphore
/// Service responsible for managing notifications.
@MainActor
public class NotificationsService {
public static let shared = NotificationsService()
private init() { }
private let semaphore = AsyncSemaphore(value: 1)
public func newNotificationsHasBeenAdded(for account: AccountModel, modelContext: ModelContext) async -> Bool {
await semaphore.wait()
defer { semaphore.signal() }
guard let accessToken = account.accessToken else {
return false
}
// Get maximimum downloaded stauts id.
guard let lastSeenNotificationId = self.getLastSeenNotificationId(accountId: account.id, modelContext: modelContext) else {
return false
}
let client = PixelfedClient(baseURL: account.serverUrl).getAuthenticated(token: accessToken)
do {
let linkableNotifications = try await client.notifications(minId: lastSeenNotificationId, limit: 5)
return linkableNotifications.data.first(where: { $0.id != lastSeenNotificationId }) != nil
} catch {
ErrorService.shared.handle(error, message: "notifications.error.loadingNotificationsFailed")
return false
}
}
private func getLastSeenNotificationId(accountId: String, modelContext: ModelContext) -> String? {
let accountData = AccountDataHandler.shared.getAccountData(accountId: accountId, modelContext: modelContext)
return accountData?.lastSeenNotificationId
}
}

View File

@ -64,8 +64,8 @@ struct VernissageApp: App {
.onReceive(NotificationCenter.default.publisher(for: UIApplication.didBecomeActiveNotification)) { _ in
DispatchQueue.main.asyncAfter(deadline: .now() + 5) {
Task {
// Refresh indicator of new photos when application is become active.
await self.calculateNewPhotosInBackground()
// Refresh indicator of new photos and new statuses when application is become active.
_ = await (self.calculateNewPhotosInBackground(), self.calculateNewNotificationsInBackground())
}
}
@ -74,8 +74,8 @@ struct VernissageApp: App {
}
.onReceive(timer) { _ in
Task {
// Refresh indicator of new photos each two minutes (when application is in the foreground)..
await self.calculateNewPhotosInBackground()
// Refresh indicator of new photos and new notifications each two minutes (when application is in the foreground)..
_ = await (self.calculateNewPhotosInBackground(), self.calculateNewNotificationsInBackground())
}
}
.onChange(of: applicationState.theme) { oldValue, newValue in
@ -155,14 +155,15 @@ struct VernissageApp: App {
// Refresh application state.
self.applicationState.changeApplicationState(accountModel: accountModel,
instance: instance,
lastSeenStatusId: accountModel.lastSeenStatusId)
lastSeenStatusId: accountModel.lastSeenStatusId,
lastSeenNotificationId: accountModel.lastSeenNotificationId)
// Change view displayed by application.
self.applicationViewMode = .mainView
// Check amount of newly added photos.
if checkNewPhotos {
await self.calculateNewPhotosInBackground()
_ = await (self.calculateNewPhotosInBackground(), self.calculateNewNotificationsInBackground())
}
}
}
@ -211,7 +212,7 @@ struct VernissageApp: App {
}
private func calculateNewPhotosInBackground() async {
let modelContext = self.modelContainer.mainContext
let modelContext = self.modelContainer.mainContext
if let account = self.applicationState.account {
self.applicationState.amountOfNewStatuses = await HomeTimelineService.shared.amountOfNewStatuses(
@ -222,4 +223,12 @@ struct VernissageApp: App {
)
}
}
private func calculateNewNotificationsInBackground() async {
let modelContext = self.modelContainer.mainContext
if let account = self.applicationState.account {
self.applicationState.newNotificationsHasBeenAdded = await NotificationsService.shared.newNotificationsHasBeenAdded(for: account, modelContext: modelContext)
}
}
}

View File

@ -21,6 +21,7 @@ extension View {
@MainActor
private struct NavigationMenuButtons: ViewModifier {
@Environment(ApplicationState.self) var applicationState
@Environment(RouterPath.self) var routerPath
@Environment(\.modelContext) private var modelContext
@ -28,13 +29,13 @@ private struct NavigationMenuButtons: ViewModifier {
private let onViewModeIconTap: (MainView.ViewMode) -> Void
private let imageFontSize = 20.0
private let customMenuItems = [
NavigationMenuItemDetails(viewMode: .home),
NavigationMenuItemDetails(viewMode: .local),
NavigationMenuItemDetails(viewMode: .federated),
NavigationMenuItemDetails(viewMode: .search),
NavigationMenuItemDetails(viewMode: .profile),
NavigationMenuItemDetails(viewMode: .notifications)
private let customMenuItems: [MainView.ViewMode] = [
.home,
.local,
.federated,
.search,
.profile,
.notifications
]
@State private var displayedCustomMenuItems = [
@ -168,7 +169,7 @@ private struct NavigationMenuButtons: ViewModifier {
Button {
self.onViewModeIconTap(displayedCustomMenuItem.viewMode)
} label: {
Image(systemName: displayedCustomMenuItem.image)
displayedCustomMenuItem.viewMode.getImage(applicationState: applicationState)
.font(.system(size: self.imageFontSize))
.foregroundColor(.mainTextColor.opacity(0.75))
.padding(.vertical, 10)
@ -183,24 +184,28 @@ private struct NavigationMenuButtons: ViewModifier {
ForEach(self.customMenuItems) { item in
Button {
withAnimation {
displayedCustomMenuItem.viewMode = item.viewMode
displayedCustomMenuItem.viewMode = item
}
// Saving in core data.
// Saving in database.
switch displayedCustomMenuItem.position {
case 1:
ApplicationSettingsHandler.shared.set(customNavigationMenuItem1: item.viewMode.rawValue, modelContext: modelContext)
ApplicationSettingsHandler.shared.set(customNavigationMenuItem1: item.rawValue, modelContext: modelContext)
case 2:
ApplicationSettingsHandler.shared.set(customNavigationMenuItem2: item.viewMode.rawValue, modelContext: modelContext)
ApplicationSettingsHandler.shared.set(customNavigationMenuItem2: item.rawValue, modelContext: modelContext)
case 3:
ApplicationSettingsHandler.shared.set(customNavigationMenuItem3: item.viewMode.rawValue, modelContext: modelContext)
ApplicationSettingsHandler.shared.set(customNavigationMenuItem3: item.rawValue, modelContext: modelContext)
default:
break
}
self.hiddenMenuItems = self.displayedCustomMenuItems.map({ $0.viewMode })
} label: {
Label(item.title, systemImage: item.image)
Label {
Text(item.title, comment: "Menu item")
} icon: {
item.getImage(applicationState: applicationState)
}
}
}
}
@ -217,10 +222,8 @@ private struct NavigationMenuButtons: ViewModifier {
private func setCustomMenuItem(position: Int, viewMode: MainView.ViewMode) {
if let displayedCustomMenuItem = self.displayedCustomMenuItems.first(where: { $0.position == position }),
let customMenuItem = self.customMenuItems.first(where: { $0.viewMode == viewMode }) {
displayedCustomMenuItem.title = customMenuItem.title
displayedCustomMenuItem.viewMode = customMenuItem.viewMode
displayedCustomMenuItem.image = customMenuItem.image
let customMenuItem = self.customMenuItems.first(where: { $0 == viewMode }) {
displayedCustomMenuItem.viewMode = customMenuItem
}
}
}

View File

@ -197,10 +197,10 @@ struct HomeTimelineView: View {
modelContext: modelContext)
// Remeber first status returned by API in user context (when it's newer then remembered).
try HomeTimelineService.shared.update(lastSeenStatusId: nil,
lastLoadedStatusId: statuses.first?.id,
applicationState: self.applicationState,
modelContext: modelContext)
try AccountDataHandler.shared.update(lastSeenStatusId: nil,
lastLoadedStatusId: statuses.first?.id,
applicationState: self.applicationState,
modelContext: modelContext)
// Append statuses to viewed.
try ViewedStatusHandler.shared.append(contentsOf: statuses, accountId: accountId, modelContext: modelContext)
@ -274,11 +274,11 @@ struct HomeTimelineView: View {
modelContext: modelContext)
// Remeber first status returned by API in user context (when it's newer then remembered).
try HomeTimelineService.shared.update(lastSeenStatusId: self.statusViewModels.first?.id,
lastLoadedStatusId: statuses.first?.id,
statuses: statuses,
applicationState: self.applicationState,
modelContext: modelContext)
try AccountDataHandler.shared.update(lastSeenStatusId: self.statusViewModels.first?.id,
lastLoadedStatusId: statuses.first?.id,
statuses: statuses,
applicationState: self.applicationState,
modelContext: modelContext)
// Append statuses to viewed.
try ViewedStatusHandler.shared.append(contentsOf: statuses, accountId: accountId, modelContext: modelContext)

View File

@ -33,7 +33,7 @@ struct MainView: View {
@Query(sort: \AccountData.acct, order: .forward) var dbAccounts: [AccountData]
public enum ViewMode: Int {
public enum ViewMode: Int, Identifiable {
case home = 1
case local = 2
case federated = 3
@ -44,6 +44,10 @@ struct MainView: View {
case trendingTags = 8
case trendingAccounts = 9
var id: Self {
return self
}
public var title: LocalizedStringKey {
switch self {
case .home:
@ -67,26 +71,36 @@ struct MainView: View {
}
}
public var image: String {
@ViewBuilder
public func getImage(applicationState: ApplicationState) -> some View {
switch self {
case .home:
return "house"
Image(systemName: "house")
case .trendingPhotos:
return "photo.stack"
Image(systemName: "photo.stack")
case .trendingTags:
return "tag"
Image(systemName: "tag")
case .trendingAccounts:
return "person.3"
Image(systemName: "person.3")
case .local:
return "building"
Image(systemName: "building")
case .federated:
return "globe.europe.africa"
Image(systemName: "globe.europe.africa")
case .profile:
return "person.crop.circle"
Image(systemName: "person.crop.circle")
case .notifications:
return "bell.badge"
if applicationState.menuPosition == .top {
applicationState.newNotificationsHasBeenAdded ? Image(systemName: "bell.badge") : Image(systemName: "bell")
} else {
applicationState.newNotificationsHasBeenAdded
? AnyView(
Image(systemName: "bell.badge")
.symbolRenderingMode(.palette)
.foregroundStyle(applicationState.tintColor.color().opacity(0.75), Color.mainTextColor.opacity(0.75)))
: AnyView(Image(systemName: "bell"))
}
case .search:
return "magnifyingglass"
Image(systemName: "magnifyingglass")
}
}
}
@ -309,7 +323,8 @@ struct MainView: View {
// Refresh application state.
self.applicationState.changeApplicationState(accountModel: signedInAccountModel,
instance: instance,
lastSeenStatusId: signedInAccountModel.lastSeenStatusId)
lastSeenStatusId: signedInAccountModel.lastSeenStatusId,
lastSeenNotificationId: signedInAccountModel.lastSeenNotificationId)
// Set account as default (application will open this account after restart).
ApplicationSettingsHandler.shared.set(accountId: signedInAccountModel.id, modelContext: modelContext)

View File

@ -15,6 +15,7 @@ import WidgetsKit
struct NotificationsView: View {
@Environment(ApplicationState.self) var applicationState
@Environment(Client.self) var client
@Environment(\.modelContext) private var modelContext
@State var accountId: String
@State private var notifications: [PixelfedKit.Notification] = []
@ -76,7 +77,7 @@ struct NotificationsView: View {
.listStyle(PlainListStyle())
.refreshable {
HapticService.shared.fireHaptic(of: .dataRefresh(intensity: 0.3))
await self.loadNewNotifications()
await self.refreshNotifications()
HapticService.shared.fireHaptic(of: .dataRefresh(intensity: 0.7))
}
}
@ -95,6 +96,9 @@ struct NotificationsView: View {
withAnimation {
self.state = .loaded
}
try AccountDataHandler.shared.update(lastSeenNotificationId: linkable.data.first?.id, applicationState: self.applicationState, modelContext: modelContext)
self.applicationState.newNotificationsHasBeenAdded = false
}
} catch {
if !Task.isCancelled {
@ -122,7 +126,7 @@ struct NotificationsView: View {
}
}
private func loadNewNotifications() async {
private func refreshNotifications() async {
do {
if let linkable = try await self.client.notifications?.notifications(minId: self.minId, limit: self.defaultPageSize) {
if let first = linkable.data.first, self.notifications.contains(where: { notification in notification.id == first.id }) {
@ -130,6 +134,9 @@ struct NotificationsView: View {
return
}
try AccountDataHandler.shared.update(lastSeenNotificationId: linkable.data.first?.id, applicationState: self.applicationState, modelContext: modelContext)
self.applicationState.newNotificationsHasBeenAdded = false
self.minId = linkable.link?.minId
self.notifications.insert(contentsOf: linkable.data, at: 0)
}

View File

@ -198,10 +198,10 @@ struct StatusesView: View {
if self.listType == .home {
// Remeber first status returned by API in user context (when it's newer then remembered).
try HomeTimelineService.shared.update(lastSeenStatusId: nil,
lastLoadedStatusId: statuses.first?.id,
applicationState: self.applicationState,
modelContext: modelContext)
try AccountDataHandler.shared.update(lastSeenStatusId: nil,
lastLoadedStatusId: statuses.first?.id,
applicationState: self.applicationState,
modelContext: modelContext)
// Append statuses to viewed.
try ViewedStatusHandler.shared.append(contentsOf: statuses, accountId: accountId, modelContext: modelContext)
@ -276,10 +276,10 @@ struct StatusesView: View {
if self.listType == .home {
// Remeber first status returned by API in user context (when it's newer then remembered).
try HomeTimelineService.shared.update(lastSeenStatusId: self.statusViewModels.first?.id,
lastLoadedStatusId: statuses.first?.id,
applicationState: self.applicationState,
modelContext: modelContext)
try AccountDataHandler.shared.update(lastSeenStatusId: self.statusViewModels.first?.id,
lastLoadedStatusId: statuses.first?.id,
applicationState: self.applicationState,
modelContext: modelContext)
// Append statuses to viewed.
try ViewedStatusHandler.shared.append(contentsOf: statuses, accountId: accountId, modelContext: modelContext)

View File

@ -6,8 +6,11 @@
import Foundation
import SwiftUI
import EnvironmentKit
struct MainNavigationOptions: View {
@Environment(ApplicationState.self) var applicationState
let onViewModeIconTap: (MainView.ViewMode) -> Void
@Binding var hiddenMenuItems: [MainView.ViewMode]
@ -24,7 +27,7 @@ struct MainNavigationOptions: View {
} label: {
HStack {
Text(MainView.ViewMode.home.title)
Image(systemName: MainView.ViewMode.home.image)
MainView.ViewMode.home.getImage(applicationState: applicationState)
}
}
}
@ -35,7 +38,7 @@ struct MainNavigationOptions: View {
} label: {
HStack {
Text(MainView.ViewMode.local.title)
Image(systemName: MainView.ViewMode.local.image)
MainView.ViewMode.local.getImage(applicationState: applicationState)
}
}
}
@ -46,7 +49,7 @@ struct MainNavigationOptions: View {
} label: {
HStack {
Text(MainView.ViewMode.federated.title)
Image(systemName: MainView.ViewMode.federated.image)
MainView.ViewMode.federated.getImage(applicationState: applicationState)
}
}
}
@ -57,7 +60,7 @@ struct MainNavigationOptions: View {
} label: {
HStack {
Text(MainView.ViewMode.search.title)
Image(systemName: MainView.ViewMode.search.image)
MainView.ViewMode.search.getImage(applicationState: applicationState)
}
}
}
@ -70,7 +73,7 @@ struct MainNavigationOptions: View {
} label: {
HStack {
Text(MainView.ViewMode.trendingPhotos.title)
Image(systemName: MainView.ViewMode.trendingPhotos.image)
MainView.ViewMode.trendingPhotos.getImage(applicationState: applicationState)
}
}
@ -79,7 +82,7 @@ struct MainNavigationOptions: View {
} label: {
HStack {
Text(MainView.ViewMode.trendingTags.title)
Image(systemName: MainView.ViewMode.trendingTags.image)
MainView.ViewMode.trendingTags.getImage(applicationState: applicationState)
}
}
@ -88,7 +91,7 @@ struct MainNavigationOptions: View {
} label: {
HStack {
Text(MainView.ViewMode.trendingAccounts.title)
Image(systemName: MainView.ViewMode.trendingAccounts.image)
MainView.ViewMode.trendingAccounts.getImage(applicationState: applicationState)
}
}
} label: {
@ -106,7 +109,7 @@ struct MainNavigationOptions: View {
} label: {
HStack {
Text(MainView.ViewMode.profile.title)
Image(systemName: MainView.ViewMode.profile.image)
MainView.ViewMode.profile.getImage(applicationState: applicationState)
}
}
}
@ -117,7 +120,7 @@ struct MainNavigationOptions: View {
} label: {
HStack {
Text(MainView.ViewMode.notifications.title)
Image(systemName: MainView.ViewMode.notifications.image)
MainView.ViewMode.notifications.getImage(applicationState: applicationState)
}
}
}

View File

@ -40,7 +40,8 @@ class ShareViewController: UIViewController {
// Set application state (with default instance settings).
applicationState.changeApplicationState(accountModel: accountModel,
instance: nil,
lastSeenStatusId: accountModel.lastSeenStatusId)
lastSeenStatusId: accountModel.lastSeenStatusId,
lastSeenNotificationId: accountModel.lastSeenNotificationId)
// Update application settings from database.
ApplicationSettingsHandler.shared.update(applicationState: applicationState, modelContext: modelContext)