diff --git a/Threaded.xcodeproj/project.pbxproj b/Threaded.xcodeproj/project.pbxproj index a595210..d43682c 100644 --- a/Threaded.xcodeproj/project.pbxproj +++ b/Threaded.xcodeproj/project.pbxproj @@ -41,6 +41,10 @@ B9BED51A2B5D662D00C9B715 /* ShareSheetController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9BED5192B5D662D00C9B715 /* ShareSheetController.swift */; }; B9CC45B82B40A2D6001E4FA5 /* AboutView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9CC45B72B40A2D6001E4FA5 /* AboutView.swift */; }; B9CFC43B2B4F08C9004CFCB7 /* LaunchStoryboard.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = B9CFC43A2B4F08C9004CFCB7 /* LaunchStoryboard.storyboard */; }; + B9D9C6C12B6A56E000C26A41 /* Notification.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9D9C6C02B6A56E000C26A41 /* Notification.swift */; }; + B9D9C6C32B6A576C00C26A41 /* NotificationsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9D9C6C22B6A576C00C26A41 /* NotificationsView.swift */; }; + B9D9C6C52B6A587700C26A41 /* NotificationRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9D9C6C42B6A587700C26A41 /* NotificationRow.swift */; }; + B9D9C6C72B6A590F00C26A41 /* ProfilePicture.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9D9C6C62B6A590F00C26A41 /* ProfilePicture.swift */; }; B9EBE8562B47256900FB594D /* PostAttachment.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9EBE8552B47256900FB594D /* PostAttachment.swift */; }; B9EBE8582B474FD600FB594D /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9EBE8572B474FD600FB594D /* AppDelegate.swift */; }; B9F8FA162B5D3AC30044DAB4 /* SafariView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9F8FA152B5D3AC30044DAB4 /* SafariView.swift */; }; @@ -130,6 +134,10 @@ B9CC45B72B40A2D6001E4FA5 /* AboutView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutView.swift; sourceTree = ""; }; B9CC45B92B40AA1E001E4FA5 /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; B9CFC43A2B4F08C9004CFCB7 /* LaunchStoryboard.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = LaunchStoryboard.storyboard; sourceTree = ""; }; + B9D9C6C02B6A56E000C26A41 /* Notification.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Notification.swift; sourceTree = ""; }; + B9D9C6C22B6A576C00C26A41 /* NotificationsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationsView.swift; sourceTree = ""; }; + B9D9C6C42B6A587700C26A41 /* NotificationRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationRow.swift; sourceTree = ""; }; + B9D9C6C62B6A590F00C26A41 /* ProfilePicture.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfilePicture.swift; sourceTree = ""; }; B9EBE8552B47256900FB594D /* PostAttachment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostAttachment.swift; sourceTree = ""; }; B9EBE8572B474FD600FB594D /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; B9F8FA152B5D3AC30044DAB4 /* SafariView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SafariView.swift; sourceTree = ""; }; @@ -220,6 +228,14 @@ path = Post; sourceTree = ""; }; + B9D9C6BF2B6A56D500C26A41 /* Notifications */ = { + isa = PBXGroup; + children = ( + B9D9C6C02B6A56E000C26A41 /* Notification.swift */, + ); + path = Notifications; + sourceTree = ""; + }; B9FB944E2B2DEECE00D81C07 = { isa = PBXGroup; children = ( @@ -270,6 +286,7 @@ B9FB946C2B2DF3A600D81C07 /* Data */ = { isa = PBXGroup; children = ( + B9D9C6BF2B6A56D500C26A41 /* Notifications */, B93BCC3F2B5E38E5008EEA19 /* Content */, B9FB94BD2B2F038D00D81C07 /* Accounts */, B9FB946F2B2DF3CD00D81C07 /* Navigator.swift */, @@ -296,6 +313,7 @@ B98BC7462B46CE6300595441 /* PostDetailsView.swift */, B93B677B2B433A6E000892E9 /* PostingView.swift */, B9F8FA152B5D3AC30044DAB4 /* SafariView.swift */, + B9D9C6C22B6A576C00C26A41 /* NotificationsView.swift */, ); path = Views; sourceTree = ""; @@ -312,6 +330,8 @@ B9B63B202B442D1500BBC82D /* DynamicTextEditor.swift */, B98BC74A2B46CF0400595441 /* ListStyle.swift */, B9BED5192B5D662D00C9B715 /* ShareSheetController.swift */, + B9D9C6C42B6A587700C26A41 /* NotificationRow.swift */, + B9D9C6C62B6A590F00C26A41 /* ProfilePicture.swift */, ); path = Components; sourceTree = ""; @@ -429,6 +449,7 @@ knownRegions = ( en, Base, + fr, ); mainGroup = B9FB944E2B2DEECE00D81C07; packageReferences = ( @@ -488,6 +509,7 @@ B98F47982B64670F0092000F /* ShopView.swift in Sources */, B9842C0E2B2F21B700D9F3C1 /* CompactPostView.swift in Sources */, B98BC7492B46CEDA00595441 /* AppearenceView.swift in Sources */, + B9D9C6C52B6A587700C26A41 /* NotificationRow.swift in Sources */, B9FB94992B2EEB9400D81C07 /* AddInstanceView.swift in Sources */, B9FB94972B2EDABF00D81C07 /* PrivacyView.swift in Sources */, B9F8FA162B5D3AC30044DAB4 /* SafariView.swift in Sources */, @@ -496,8 +518,10 @@ B9B63B232B447B8000BBC82D /* PostCardView.swift in Sources */, B9FB949B2B2EF09A00D81C07 /* Client.swift in Sources */, B9FB949D2B2EF0D600D81C07 /* Instance.swift in Sources */, + B9D9C6C72B6A590F00C26A41 /* ProfilePicture.swift in Sources */, B9842C102B2F228C00D9F3C1 /* Status.swift in Sources */, B93B677A2B42EC51000892E9 /* MetaPicker.swift in Sources */, + B9D9C6C32B6A576C00C26A41 /* NotificationsView.swift in Sources */, B9FB94722B2DF49700D81C07 /* ConnectView.swift in Sources */, B9FB945B2B2DEECE00D81C07 /* ThreadedApp.swift in Sources */, B9FB94862B2E211200D81C07 /* Account+Elms.swift in Sources */, @@ -525,6 +549,7 @@ B9FB948E2B2E28E800D81C07 /* MediaTransferables.swift in Sources */, B98BC74D2B46CFCE00595441 /* UserPreferences.swift in Sources */, B9FB94A22B2EF24A00D81C07 /* AppInfo.swift in Sources */, + B9D9C6C12B6A56E000C26A41 /* Notification.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/Threaded/Components/NotificationRow.swift b/Threaded/Components/NotificationRow.swift new file mode 100644 index 0000000..6e4fe58 --- /dev/null +++ b/Threaded/Components/NotificationRow.swift @@ -0,0 +1,95 @@ +//Made by Lumaa + +import SwiftUI + +struct NotificationRow: View { + var notif: Notification = .placeholder() + + var body: some View { + VStack { + HStack(spacing: 5) { + ProfilePicture(url: notif.account.avatar) + .padding(.trailing) + .overlay(alignment: .bottomTrailing) { + notifIcon() + .offset(x: -5, y: 5) + } + .padding() + Text(localizedString()) + } + .padding(.horizontal) + } + } + + private func localizedString() -> String { + switch (notif.supportedType) { + case .favourite: + return String(localized: "activity.favorite.%@").replacingOccurrences(of: "%@", with: "@\(notif.account.username)") + case .follow: + return String(localized: "activity.followed.%@").replacingOccurrences(of: "%@", with: "@\(notif.account.username)") + case .mention: + return String(localized: "activity.mentionned.%@").replacingOccurrences(of: "%@", with: "@\(notif.account.username)") + case .reblog: + return String(localized: "activity.reblogged.%@").replacingOccurrences(of: "%@", with: "@\(notif.account.username)") + case .status: + return String(localized: "activity.status.%@").replacingOccurrences(of: "%@", with: "@\(notif.account.username)") + default: + return String(localized: "activity.unknown") + } + } + + private func notifColor() -> Color { + switch (notif.supportedType) { + case .favourite: + return Color.red + case .follow: + return Color.purple + case .mention: + return Color.blue + case .reblog: + return Color.pink + case .status: + return Color.yellow + default: + return Color.gray + } + } + + @ViewBuilder + private func notifIcon() -> some View { + ZStack { + switch (notif.supportedType) { + case .favourite: + Image(systemName: "heart.fill") + .font(.caption) + case .follow: + Image(systemName: "person.fill.badge.plus") + .font(.caption) + case .mention: + Image(systemName: "tag.fill") + .font(.caption) + case .reblog: + Image(systemName: "bolt.horizontal.fill") + .font(.caption) + case .status: + Image(systemName: "text.badge.plus") + .font(.caption) + default: + Image(systemName: "questionmark") + .font(.caption) + } + } + .padding(5) + .background(notifColor()) + .clipShape(.circle) + .overlay { + Circle() + .stroke(Color.appBackground, lineWidth: 3) + } + .fixedSize() + } +} + +#Preview { + NotificationRow() +} diff --git a/Threaded/Components/ProfilePicture.swift b/Threaded/Components/ProfilePicture.swift new file mode 100644 index 0000000..500180f --- /dev/null +++ b/Threaded/Components/ProfilePicture.swift @@ -0,0 +1,25 @@ +//Made by Lumaa + +import SwiftUI + +struct ProfilePicture: View { + @Environment(UserPreferences.self) private var pref + var url: URL + var cornerRadius: CGFloat { + return pref.profilePictureShape == .circle ? (50 / 2) : 15.0 + } + + init(url: URL) { + self.url = url + } + + init(url: String) { + self.url = .init(string: url)! + } + + var body: some View { + OnlineImage(url: url, size: 50, useNuke: true) + .frame(width: 40, height: 40) + .clipShape(RoundedRectangle(cornerRadius: cornerRadius)) + } +} diff --git a/Threaded/Data/Accounts/AccountManager.swift b/Threaded/Data/Accounts/AccountManager.swift index 72d2a4b..9a7cbb3 100644 --- a/Threaded/Data/Accounts/AccountManager.swift +++ b/Threaded/Data/Accounts/AccountManager.swift @@ -2,6 +2,8 @@ import Foundation +//TODO: Change this to SwiftData + @Observable public class AccountManager { private var client: Client? @@ -46,7 +48,6 @@ public class AccountManager { } } -//TODO: Change this to SwiftData public struct AppAccount: Codable, Identifiable, Hashable { public let server: String public var accountName: String? diff --git a/Threaded/Data/Content/FetchTimeline.swift b/Threaded/Data/Content/FetchTimeline.swift index e25b07e..41ac6e8 100644 --- a/Threaded/Data/Content/FetchTimeline.swift +++ b/Threaded/Data/Content/FetchTimeline.swift @@ -26,7 +26,7 @@ struct FetchTimeline { } public mutating func addStatuses(lastStatusIndex: Int) async -> [Status] { - print("i: \(lastStatusIndex)\ndatasource-6: \(self.datasource.count - 6)") +// print("i: \(lastStatusIndex)\ndatasource-6: \(self.datasource.count - 6)") guard client != nil && lastStatusIndex >= self.datasource.count - 6 else { return self.datasource } self.statusesState = .loading diff --git a/Threaded/Data/MastodonRequest.swift b/Threaded/Data/MastodonRequest.swift index 528782f..cc1ffcc 100644 --- a/Threaded/Data/MastodonRequest.swift +++ b/Threaded/Data/MastodonRequest.swift @@ -572,3 +572,39 @@ public struct MediaDescriptionData: Encodable, Sendable { self.description = description } } + +public enum Notifications: Endpoint { + case notifications(minId: String?, + maxId: String?, + types: [String]?, + limit: Int) + case notification(id: String) + case clear + + public func path() -> String { + switch self { + case .notifications: + "notifications" + case let .notification(id): + "notifications/\(id)" + case .clear: + "notifications/clear" + } + } + + public func queryItems() -> [URLQueryItem]? { + switch self { + case let .notifications(mindId, maxId, types, limit): + var params = makePaginationParam(sinceId: nil, maxId: maxId, mindId: mindId) ?? [] + params.append(.init(name: "limit", value: String(limit))) + if let types { + for type in types { + params.append(.init(name: "exclude_types[]", value: type)) + } + } + return params + default: + return nil + } + } +} diff --git a/Threaded/Data/Notifications/Notification.swift b/Threaded/Data/Notifications/Notification.swift new file mode 100644 index 0000000..cfb8d48 --- /dev/null +++ b/Threaded/Data/Notifications/Notification.swift @@ -0,0 +1,30 @@ +//Made by Lumaa + +import Foundation + +public struct Notification: Decodable, Identifiable, Equatable { + public enum NotificationType: String, CaseIterable { + case follow, follow_request, mention, reblog, status, favourite, poll, update + } + + public let id: String + public let type: String + public let createdAt: ServerDate + public let account: Account + public let status: Status? + + public var supportedType: NotificationType? { + .init(rawValue: type) + } + + public static func placeholder() -> Notification { + .init(id: UUID().uuidString, + type: NotificationType.favourite.rawValue, + createdAt: ServerDate(), + account: .placeholder(), + status: .placeholder()) + } +} + +extension Notification: Sendable {} +extension Notification.NotificationType: Sendable {} diff --git a/Threaded/Localizable.xcstrings b/Threaded/Localizable.xcstrings index d5a8fbf..db21d80 100644 --- a/Threaded/Localizable.xcstrings +++ b/Threaded/Localizable.xcstrings @@ -20,6 +20,12 @@ "state" : "translated", "value" : "About" } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "À propos" + } } } }, @@ -30,6 +36,12 @@ "state" : "translated", "value" : "About Threaded" } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "À propos de Threaded" + } } } }, @@ -71,6 +83,12 @@ "state" : "translated", "value" : "An audio file" } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Un fichier audio" + } } } }, @@ -82,6 +100,12 @@ "state" : "translated", "value" : "An animated GIF" } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Un GIF animé" + } } } }, @@ -93,6 +117,12 @@ "state" : "translated", "value" : "An image" } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Une image" + } } } }, @@ -104,6 +134,12 @@ "state" : "translated", "value" : "A video" } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Une vidéo" + } } } }, @@ -114,6 +150,12 @@ "state" : "translated", "value" : "Follow" } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Suivre" + } } } }, @@ -124,6 +166,12 @@ "state" : "translated", "value" : "Follow back" } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Suivre en retour" + } } } }, @@ -156,6 +204,12 @@ "state" : "translated", "value" : "Mention" } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mentionner" + } } } }, @@ -166,6 +220,118 @@ "state" : "translated", "value" : "Unfollow" } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ne plus suivre" + } + } + } + }, + "activity.favorite.%@" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ liked your post" + } + } + } + }, + "activity.followed.%@" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ followed you" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ vous a suivi(e)" + } + } + } + }, + "activity.mentionned.%@" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ mentionned you " + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ vous a mentionné(e)" + } + } + } + }, + "activity.no-notifications" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "No notifications" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pas de notifications" + } + } + } + }, + "activity.reblogged.%@" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ reposted your post" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ a republié votre publication" + } + } + } + }, + "activity.status.%@" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ posted" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ a publié une publication" + } + } + } + }, + "activity.unknown" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Unknown activity" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Activité inconnue" + } } } }, @@ -176,6 +342,12 @@ "state" : "translated", "value" : "Image Error" } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Erreur Image" + } } } }, @@ -186,6 +358,12 @@ "state" : "translated", "value" : "Experimental" } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Expérimental" + } } } }, @@ -196,6 +374,12 @@ "state" : "translated", "value" : "Rules" } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Règles" + } } } }, @@ -206,6 +390,12 @@ "state" : "translated", "value" : "Log in using Mastodon" } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Connexion avec Mastodon" + } } } }, @@ -216,6 +406,12 @@ "state" : "translated", "value" : "Log in your Mastodon account using its instance URL. You cannot create an account using Threaded." } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Connectez-vous à l'aide de votre compte Mastodon en utilisant l'URL de votre instance. Vous ne pouvez pas créer de compte via Threaded" + } } } }, @@ -226,6 +422,12 @@ "state" : "translated", "value" : "Enter the instance's URL" } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Entrez l'URL de l'instance" + } } } }, @@ -236,6 +438,12 @@ "state" : "translated", "value" : "Log in" } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Connexion" + } } } }, @@ -246,6 +454,12 @@ "state" : "translated", "value" : "Verify" } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Vérifier" + } } } }, @@ -256,6 +470,12 @@ "state" : "translated", "value" : "This might not be a Mastodon instance." } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ce n'est sûrement pas une instance Mastodon" + } } } }, @@ -266,6 +486,12 @@ "state" : "translated", "value" : "Stay anonymous" } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Rester anonyme" + } } } }, @@ -276,6 +502,12 @@ "state" : "translated", "value" : "Without an account, you cannot interact with posts, users and instances. You can only read posts and users' public data." } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sans compte, vous ne pouvez pas interagir avec les publications, les utilisateurs et instances. Vous pouvez seulement lire données publiques des publications et des utilisateurs." + } } } }, @@ -286,6 +518,12 @@ "state" : "translated", "value" : "Welcome to Threaded!" } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bienvenue sur Threaded !" + } } } }, @@ -296,6 +534,12 @@ "state" : "translated", "value" : "Log out" } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Déconnexion" + } } } }, @@ -306,6 +550,12 @@ "state" : "translated", "value" : "Appearence" } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Apparence" + } } } }, @@ -316,6 +566,12 @@ "state" : "translated", "value" : "Open links" } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ouvrir les liens" + } } } }, @@ -326,6 +582,12 @@ "state" : "translated", "value" : "In-app" } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dans l'application" + } } } }, @@ -336,6 +598,12 @@ "state" : "translated", "value" : "In a browser" } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "En dehors de l'application" + } } } }, @@ -346,6 +614,12 @@ "state" : "translated", "value" : "Displayed Name" } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nom affiché" + } } } }, @@ -356,6 +630,12 @@ "state" : "translated", "value" : "Both" } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Les deux" + } } } }, @@ -366,6 +646,12 @@ "state" : "translated", "value" : "Display Name" } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Le nom d'affichage" + } } } }, @@ -376,6 +662,12 @@ "state" : "translated", "value" : "Username" } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Le nom d'utilisateur" + } } } }, @@ -386,6 +678,12 @@ "state" : "translated", "value" : "Shape of Profile Pictures" } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Forme de la photo de profil" + } } } }, @@ -396,6 +694,12 @@ "state" : "translated", "value" : "Circle" } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Circulaire" + } } } }, @@ -406,6 +710,12 @@ "state" : "translated", "value" : "Rounded Rectangle" } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Rectangulaire" + } } } }, @@ -416,6 +726,12 @@ "state" : "translated", "value" : "Show Reply Symbols" } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Afficher les symboles de réponses" + } } } }, @@ -426,6 +742,12 @@ "state" : "translated", "value" : "Show experimental features" } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Afficher les fonctionnalités expérimentales" + } } } }, @@ -436,6 +758,12 @@ "state" : "translated", "value" : "Privacy" } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Confidentialité" + } } } }, @@ -446,6 +774,12 @@ "state" : "translated", "value" : "Settings" } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Paramètres" + } } } }, @@ -456,6 +790,12 @@ "state" : "translated", "value" : "Cancel" } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Annuler" + } } } }, @@ -466,6 +806,12 @@ "state" : "translated", "value" : "Done" } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Terminé" + } } } }, @@ -476,6 +822,12 @@ "state" : "translated", "value" : "Cancel" } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Annuler" + } } } }, @@ -496,6 +848,12 @@ "state" : "translated", "value" : "Editing post" } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Modification d'une publication" + } } } }, @@ -518,6 +876,24 @@ } } } + }, + "fr" : { + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld likes" + } + }, + "other" : { + "stringUnit" : { + "state" : "new", + "value" : "%lld likes" + } + } + } + } } } }, @@ -528,6 +904,12 @@ "state" : "translated", "value" : "Copy text" } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Copier le texte" + } } } }, @@ -538,6 +920,12 @@ "state" : "translated", "value" : "Delete" } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Supprimer" + } } } }, @@ -548,6 +936,12 @@ "state" : "translated", "value" : "Edit" } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Modifier" + } } } }, @@ -558,6 +952,12 @@ "state" : "translated", "value" : "Share" } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Partager" + } } } }, @@ -568,6 +968,12 @@ "state" : "translated", "value" : "Share as image" } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Partager en tant qu'image" + } } } }, @@ -578,6 +984,12 @@ "state" : "translated", "value" : "Share as link" } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Partager le lien" + } } } }, @@ -588,6 +1000,12 @@ "state" : "translated", "value" : "Pinned" } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Épinglé" + } } } }, @@ -598,6 +1016,12 @@ "state" : "translated", "value" : "New post" } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nouvelle publication " + } } } }, @@ -608,6 +1032,12 @@ "state" : "translated", "value" : "Cancel" } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Annuler" + } } } }, @@ -618,6 +1048,12 @@ "state" : "translated", "value" : "No custom emojis" } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pas d'emojis personnalisés" + } } } }, @@ -628,6 +1064,12 @@ "state" : "translated", "value" : "What's new?" } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Quoi de neuf ?" + } } } }, @@ -638,6 +1080,12 @@ "state" : "translated", "value" : "Post" } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Publier" + } } } }, @@ -648,6 +1096,12 @@ "state" : "translated", "value" : "Visibility" } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Visibilité" + } } } }, @@ -658,6 +1112,12 @@ "state" : "translated", "value" : "Direct Message" } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Message privé" + } } } }, @@ -668,6 +1128,12 @@ "state" : "translated", "value" : "Private" } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Privé" + } } } }, @@ -678,6 +1144,12 @@ "state" : "translated", "value" : "Public" } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Public" + } } } }, @@ -688,6 +1160,12 @@ "state" : "translated", "value" : "Unlisted" } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Non répertorié" + } } } }, @@ -698,6 +1176,12 @@ "state" : "translated", "value" : "Replied to @%@" } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "A répondu à %@" + } } } }, @@ -720,6 +1204,24 @@ } } } + }, + "fr" : { + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld réponse" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld réponses" + } + } + } + } } } }, @@ -730,6 +1232,12 @@ "state" : "translated", "value" : "%@ reposted" } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ a republié" + } } } }, @@ -740,6 +1248,12 @@ "state" : "translated", "value" : "No posts" } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pas de publications" + } } } }, @@ -750,6 +1264,12 @@ "state" : "translated", "value" : "You don't have posts in your home timeline. Follow users or change timelines to see posts!" } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Vous n'avez pas de publications dans votre chronologie. Suivez des utilisateurs ou changez de chronologie pour voir des publications !" + } } } }, @@ -770,4 +1290,4 @@ } }, "version" : "1.0" -} +} \ No newline at end of file diff --git a/Threaded/Views/ContentView.swift b/Threaded/Views/ContentView.swift index 7526b2a..5a9e510 100644 --- a/Threaded/Views/ContentView.swift +++ b/Threaded/Views/ContentView.swift @@ -32,7 +32,7 @@ struct ContentView: View { .tag(TabDestination.search) //TODO: Messaging UI in Activity tab - Text(String("Activity")) + NotificationsView() .background(Color.appBackground) .tag(TabDestination.activity) @@ -62,6 +62,7 @@ struct ContentView: View { .environment(accountManager) .environment(navigator) .environment(appDelegate) + .environment(preferences) .onAppear { do { preferences = try UserPreferences.loadAsCurrent() ?? .defaultPreferences diff --git a/Threaded/Views/NotificationsView.swift b/Threaded/Views/NotificationsView.swift new file mode 100644 index 0000000..f6a3ad6 --- /dev/null +++ b/Threaded/Views/NotificationsView.swift @@ -0,0 +1,86 @@ +//Made by Lumaa + +import SwiftUI + +struct NotificationsView: View { + @Environment(AccountManager.self) private var accountManager + + @State private var navigator: Navigator = Navigator() + @State private var notifications: [Notification] = [] + @State private var loadingNotifs: Bool = false + @State private var lastId: Int? = nil + private let notifLimit = 50 + + + var body: some View { + NavigationStack(path: $navigator.path) { + if !notifications.isEmpty { + ScrollView(.vertical, showsIndicators: false) { + LazyVStack(alignment: .leading) { + ForEach(notifications) { notif in + NotificationRow(notif: notif) + .onDisappear() { + guard !notifications.isEmpty else { return } + lastId = notifications.firstIndex(where: { $0.id == notif.id }) + } + } + } + .refreshable { + notifications = [] + await fetchNotifications(lastId: nil) + } + .onChange(of: lastId ?? 0) { _, new in + guard !loadingNotifs else { return } + Task { + loadingNotifs = true + await fetchNotifications(lastId: new) + loadingNotifs = false + } + } + } + } else if loadingNotifs == false && notifications.isEmpty { + ZStack { + Color.appBackground + .ignoresSafeArea() + + ContentUnavailableView("activity.no-notifications", systemImage: "bolt.heart") + } + } else if loadingNotifs == true && notifications.isEmpty { + ZStack { + Color.appBackground + .ignoresSafeArea() + + ProgressView() + .progressViewStyle(.circular) + } + } + } + .task { + loadingNotifs = true + await fetchNotifications(lastId: nil) + loadingNotifs = false + } + } + + func fetchNotifications(lastId: Int? = nil) async { + guard let client = accountManager.getClient() else { return } + + if lastId != nil { + guard lastId! >= notifications.count - 6 else { return } + } + + do { + let allCases = Notification.NotificationType.allCases.map({ $0.rawValue }) + let notifs: [Notification] = try await client.get(endpoint: Notifications.notifications(minId: nil, maxId: nil, types: nil, limit: lastId != nil ? notifLimit : 30)) + guard !notifs.isEmpty else { return } + + if notifications.isEmpty { + notifications = notifs + } else { + notifications.append(contentsOf: notifs) + } + } catch { + print(error) + } + } +}